mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-08 10:44:20 +01:00
Merge 'Improve Python bindings' from Diego Reis
Yet another PR to close #494. While testing the code provided in the issue I noticed that it wasn't closing the connection as it should, leading to lifetime issues like: `Connection is unsendable, but is being dropped on another thread`. The following code works fine: ```python import limbo def main(): con = limbo.connect("test.db") cur = con.cursor() try: cur.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL, email TEXT NOT NULL, role TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT (datetime('now')) ) """) # Insert some sample data sample_users = [ ("alice", "alice@example.com", "admin"), ("bob", "bob@example.com", "user"), ("charlie", "charlie@example.com", "moderator"), ("diana", "diana@example.com", "user") ] for username, email, role in sample_users: cur.execute(""" INSERT INTO users (username, email, role) VALUES (?, ?, ?) """, (username, email, role)) con.commit() # Query the table res = cur.execute("SELECT * FROM users") record = res.fetchone() print(record) finally: # Ensure connection is closed on the same thread <---- con.close() pass main() ``` You can test it [here](https://colab.research.google.com/drive/1NJau6Y9H TRJrnYK_xp2AzwP_qEH8VsQx?usp=sharing) To address these issues, this PR: - Adds support for `with statement` a common resource management pattern in Python; - Close connection if it is dropped Closes #1164
This commit is contained in:
@@ -1,6 +1,38 @@
|
||||
import limbo
|
||||
|
||||
con = limbo.connect("sqlite.db")
|
||||
cur = con.cursor()
|
||||
res = cur.execute("SELECT * FROM users")
|
||||
print(res.fetchone())
|
||||
# Use the context manager to automatically close the connection
|
||||
with limbo.connect("sqlite.db") as con:
|
||||
cur = con.cursor()
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
""")
|
||||
|
||||
# Insert some sample data
|
||||
sample_users = [
|
||||
("alice", "alice@example.com", "admin"),
|
||||
("bob", "bob@example.com", "user"),
|
||||
("charlie", "charlie@example.com", "moderator"),
|
||||
("diana", "diana@example.com", "user"),
|
||||
]
|
||||
for username, email, role in sample_users:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO users (username, email, role)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(username, email, role),
|
||||
)
|
||||
|
||||
# Use commit to ensure the data is saved
|
||||
con.commit()
|
||||
|
||||
# Query the table
|
||||
res = cur.execute("SELECT * FROM users")
|
||||
record = res.fetchone()
|
||||
print(record)
|
||||
|
||||
@@ -193,9 +193,9 @@ impl Cursor {
|
||||
}
|
||||
|
||||
pub fn close(&self) -> PyResult<()> {
|
||||
Err(PyErr::new::<NotSupportedError, _>(
|
||||
"close() is not supported in this version",
|
||||
))
|
||||
self.conn.close()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pyo3(signature = (sql, parameters=None))]
|
||||
@@ -244,8 +244,12 @@ impl Connection {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
drop(self.conn.clone());
|
||||
pub fn close(&self) -> PyResult<()> {
|
||||
self.conn.close().map_err(|e| {
|
||||
PyErr::new::<OperationalError, _>(format!("Failed to close connection: {:?}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn commit(&self) -> PyResult<()> {
|
||||
@@ -266,6 +270,27 @@ impl Connection {
|
||||
"Transactions are not supported in this version",
|
||||
))
|
||||
}
|
||||
|
||||
fn __enter__(&self) -> PyResult<Self> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
|
||||
fn __exit__(
|
||||
&self,
|
||||
_exc_type: Option<&Bound<'_, PyAny>>,
|
||||
_exc_val: Option<&Bound<'_, PyAny>>,
|
||||
_exc_tb: Option<&Bound<'_, PyAny>>,
|
||||
) -> PyResult<()> {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Connection {
|
||||
fn drop(&mut self) {
|
||||
self.conn
|
||||
.close()
|
||||
.expect("Failed to drop (close) connection");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
|
||||
@@ -12,27 +12,41 @@ def setup_database():
|
||||
db_wal_path = "tests/database.db-wal"
|
||||
|
||||
# Ensure the database file is created fresh for each test
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
if os.path.exists(db_wal_path):
|
||||
os.remove(db_wal_path)
|
||||
try:
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
if os.path.exists(db_wal_path):
|
||||
os.remove(db_wal_path)
|
||||
except PermissionError as e:
|
||||
print(f"Failed to clean up: {e}")
|
||||
|
||||
# Create a new database file
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("CREATE TABLE users (id INT PRIMARY KEY, username TEXT)")
|
||||
cursor.execute("INSERT INTO users VALUES (1, 'alice')")
|
||||
cursor.execute("INSERT INTO users VALUES (2, 'bob')")
|
||||
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INT PRIMARY KEY, username TEXT)")
|
||||
cursor.execute("""
|
||||
INSERT INTO users (id, username)
|
||||
SELECT 1, 'alice'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM users WHERE id = 1)
|
||||
""")
|
||||
cursor.execute("""
|
||||
INSERT INTO users (id, username)
|
||||
SELECT 2, 'bob'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM users WHERE id = 2)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
yield db_path
|
||||
|
||||
# Cleanup after the test
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
if os.path.exists(db_wal_path):
|
||||
os.remove(db_wal_path)
|
||||
try:
|
||||
if os.path.exists(db_path):
|
||||
os.remove(db_path)
|
||||
if os.path.exists(db_wal_path):
|
||||
os.remove(db_wal_path)
|
||||
except PermissionError as e:
|
||||
print(f"Failed to clean up: {e}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["sqlite3", "limbo"])
|
||||
@@ -145,6 +159,18 @@ def test_commit(provider):
|
||||
assert record
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provider", ["sqlite3", "limbo"])
|
||||
def test_with_statement(provider):
|
||||
with connect(provider, "tests/database.db") as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT MAX(id) FROM users")
|
||||
|
||||
max_id = cursor.fetchone()
|
||||
|
||||
assert max_id
|
||||
assert max_id == (2,)
|
||||
|
||||
|
||||
def connect(provider, database):
|
||||
if provider == "limbo":
|
||||
return limbo.connect(database)
|
||||
|
||||
Reference in New Issue
Block a user