From 4ca5b11bed47c04ad26d699c93f530c89a2d9665 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Mon, 24 Mar 2025 12:20:13 -0300 Subject: [PATCH 1/5] ext/python: Add support for Context Manager --- bindings/python/src/lib.rs | 13 +++++++++++++ bindings/python/tests/test_database.py | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index f88666c6d..fce970139 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -266,6 +266,19 @@ impl Connection { "Transactions are not supported in this version", )) } + + fn __enter__(&self) -> PyResult { + Ok(self.clone()) + } + + fn __exit__( + &self, + _exc_type: Option<&Bound<'_, PyAny>>, + _exc_val: Option<&Bound<'_, PyAny>>, + _exc_tb: Option<&Bound<'_, PyAny>>, + ) -> PyResult<()> { + self.close() + } } #[allow(clippy::arc_with_non_send_sync)] diff --git a/bindings/python/tests/test_database.py b/bindings/python/tests/test_database.py index a8276b21c..1f1ef7243 100644 --- a/bindings/python/tests/test_database.py +++ b/bindings/python/tests/test_database.py @@ -144,6 +144,17 @@ def test_commit(provider): conn.close() assert record +@pytest.mark.parametrize("provider", ["sqlite3", "limbo"]) +def test_with_statement(provider): + with connect(provider, "tests/database.db") as conn: + conn = connect(provider, "tests/database.db") + 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": From ab8187f4e64a2258fac8897c11c7ee056148c169 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Mon, 24 Mar 2025 12:21:15 -0300 Subject: [PATCH 2/5] ext/python: Gracefully close connection by closing it at Drop --- bindings/python/src/lib.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index fce970139..6a09e851b 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -193,9 +193,9 @@ impl Cursor { } pub fn close(&self) -> PyResult<()> { - Err(PyErr::new::( - "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::(format!("Failed to close connection: {:?}", e)) + })?; + + Ok(()) } pub fn commit(&self) -> PyResult<()> { @@ -281,6 +285,14 @@ impl Connection { } } +impl Drop for Connection { + fn drop(&mut self) { + self.conn + .close() + .expect("Failed to drop (close) connection"); + } +} + #[allow(clippy::arc_with_non_send_sync)] #[pyfunction] pub fn connect(path: &str) -> Result { From 9a8970b6a88f95352d6637534a3523270e2cab17 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Mon, 24 Mar 2025 12:21:30 -0300 Subject: [PATCH 3/5] ext/python: Update example --- bindings/python/example.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/bindings/python/example.py b/bindings/python/example.py index 53bfb9b34..2a687f68f 100644 --- a/bindings/python/example.py +++ b/bindings/python/example.py @@ -1,6 +1,35 @@ 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) From 6edf3dd3b1a810b89ee0202a65a595599a8c00cd Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Mon, 24 Mar 2025 12:40:59 -0300 Subject: [PATCH 4/5] ext/python: Makes linter happy --- bindings/python/example.py | 17 ++++++++++------- bindings/python/tests/test_database.py | 6 ++++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/bindings/python/example.py b/bindings/python/example.py index 2a687f68f..870766e8f 100644 --- a/bindings/python/example.py +++ b/bindings/python/example.py @@ -15,16 +15,19 @@ with limbo.connect("sqlite.db") as con: # 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") - ] + ("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(""" + cur.execute( + """ INSERT INTO users (username, email, role) VALUES (?, ?, ?) - """, (username, email, role)) + """, + (username, email, role), + ) # Use commit to ensure the data is saved con.commit() diff --git a/bindings/python/tests/test_database.py b/bindings/python/tests/test_database.py index 1f1ef7243..c5f1e7678 100644 --- a/bindings/python/tests/test_database.py +++ b/bindings/python/tests/test_database.py @@ -144,18 +144,20 @@ def test_commit(provider): conn.close() assert record + @pytest.mark.parametrize("provider", ["sqlite3", "limbo"]) def test_with_statement(provider): with connect(provider, "tests/database.db") as conn: conn = connect(provider, "tests/database.db") 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) From 160d48d34ef07ed13565f69c0df25517630bb1da Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Mon, 24 Mar 2025 14:31:19 -0300 Subject: [PATCH 5/5] ext/python: Workaround to file permission error To get more info see: https://github.com/tursodatabase/limbo/actions/runs/14039536389/job/39312362848 --- bindings/python/tests/test_database.py | 37 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/bindings/python/tests/test_database.py b/bindings/python/tests/test_database.py index c5f1e7678..ec565a898 100644 --- a/bindings/python/tests/test_database.py +++ b/bindings/python/tests/test_database.py @@ -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"]) @@ -148,7 +162,6 @@ def test_commit(provider): @pytest.mark.parametrize("provider", ["sqlite3", "limbo"]) def test_with_statement(provider): with connect(provider, "tests/database.db") as conn: - conn = connect(provider, "tests/database.db") cursor = conn.cursor() cursor.execute("SELECT MAX(id) FROM users")