Skip to content

Commit 622a27e

Browse files
Add transaction handling to connection context manager __exit__
Commit on clean exit and rollback on exception, following the convention used by psycopg2 and sqlite3. Errors from commit/rollback are suppressed since the connection is closing regardless. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8abd130 commit 622a27e

3 files changed

Lines changed: 76 additions & 4 deletions

File tree

src/dqlitedbapi/aio/connection.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,13 @@ async def __aenter__(self) -> "AsyncConnection":
100100
await self.connect()
101101
return self
102102

103-
async def __aexit__(self, *args: Any) -> None:
104-
await self.close()
103+
async def __aexit__(self, exc_type: type[BaseException] | None, *args: Any) -> None:
104+
try:
105+
if exc_type is None:
106+
await self.commit()
107+
else:
108+
await self.rollback()
109+
except Exception:
110+
pass
111+
finally:
112+
await self.close()

src/dqlitedbapi/connection.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,13 @@ def cursor(self) -> Cursor:
128128
def __enter__(self) -> "Connection":
129129
return self
130130

131-
def __exit__(self, *args: Any) -> None:
132-
self.close()
131+
def __exit__(self, exc_type: type[BaseException] | None, *args: Any) -> None:
132+
try:
133+
if exc_type is None:
134+
self.commit()
135+
else:
136+
self.rollback()
137+
except Exception:
138+
pass
139+
finally:
140+
self.close()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Integration tests for connection context manager transaction handling."""
2+
3+
import pytest
4+
5+
from dqlitedbapi import connect
6+
7+
8+
@pytest.mark.integration
9+
class TestContextManagerTransactions:
10+
def test_context_manager_commits_on_clean_exit(self, cluster_address: str) -> None:
11+
"""Connection context manager should commit on clean exit."""
12+
with connect(cluster_address, database="test_ctx_commit2") as conn:
13+
cursor = conn.cursor()
14+
cursor.execute("DROP TABLE IF EXISTS ctx_test")
15+
cursor.execute("CREATE TABLE ctx_test (id INTEGER PRIMARY KEY, val TEXT)")
16+
cursor.execute("BEGIN")
17+
cursor.execute("INSERT INTO ctx_test (id, val) VALUES (1, 'committed')")
18+
# __exit__ should commit
19+
20+
# Verify data persisted
21+
with connect(cluster_address, database="test_ctx_commit2") as conn:
22+
cursor = conn.cursor()
23+
cursor.execute("SELECT val FROM ctx_test WHERE id = 1")
24+
row = cursor.fetchone()
25+
assert row is not None
26+
assert row[0] == "committed"
27+
cursor.execute("DROP TABLE ctx_test")
28+
29+
def test_context_manager_rolls_back_on_exception(self, cluster_address: str) -> None:
30+
"""Connection context manager should rollback on exception."""
31+
# Set up a table with known state
32+
with connect(cluster_address, database="test_ctx_rollback2") as conn:
33+
cursor = conn.cursor()
34+
cursor.execute("DROP TABLE IF EXISTS ctx_rb_test")
35+
cursor.execute("CREATE TABLE ctx_rb_test (id INTEGER PRIMARY KEY, val TEXT)")
36+
cursor.execute("INSERT INTO ctx_rb_test (id, val) VALUES (1, 'original')")
37+
38+
# Update inside an explicit transaction, then raise
39+
with (
40+
pytest.raises(ValueError, match="simulated error"),
41+
connect(cluster_address, database="test_ctx_rollback2") as conn,
42+
):
43+
cursor = conn.cursor()
44+
cursor.execute("BEGIN")
45+
cursor.execute("UPDATE ctx_rb_test SET val = 'changed' WHERE id = 1")
46+
raise ValueError("simulated error")
47+
# __exit__ should rollback
48+
49+
# Verify original value is preserved (rolled back)
50+
with connect(cluster_address, database="test_ctx_rollback2") as conn:
51+
cursor = conn.cursor()
52+
cursor.execute("SELECT val FROM ctx_rb_test WHERE id = 1")
53+
row = cursor.fetchone()
54+
assert row is not None
55+
assert row[0] == "original"
56+
cursor.execute("DROP TABLE ctx_rb_test")

0 commit comments

Comments
 (0)