Support sqlite3.connect(autocommit=False) connections too

Database() now accepts all three Python transaction handling modes and
behaves identically in each. For autocommit=False connections - where
the driver holds an implicit transaction open at all times - the
library tracks transaction ownership itself:

- A new _explicit_transaction flag distinguishes transactions opened
  with begin()/atomic() from the driver's implicit one, exposed as a
  new db.in_transaction property (conn.in_transaction is always True
  in this mode)
- begin() claims the driver's implicit transaction instead of
  executing BEGIN, which the driver would reject; BEGIN/COMMIT/
  ROLLBACK passed to db.execute() are routed through begin()/commit()/
  rollback()
- Writes outside a user transaction commit the implicit transaction
  immediately, preserving the library's auto-commit contract
- Row-returning statements outside a transaction fetch eagerly and
  commit, so the implicit read transaction does not hold a shared
  lock that blocks writes from other connections
- PRAGMA and VACUUM run in temporary driver autocommit mode
  (ensure_autocommit_on() now flips conn.autocommit), since PRAGMAs
  are silently ignored and VACUUM refused inside the implicit
  transaction

A new pytest --sqlite-autocommit-false option runs the entire suite
in this mode, wired into CI alongside --sqlite-autocommit. Tests that
asserted on conn.in_transaction or wrote through db.conn without
committing now use the mode-aware library API instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PFkrS2nuP8mo1jmT594VCd
This commit is contained in:
Claude 2026-07-09 22:06:46 +00:00
commit adfcbb4731
No known key found for this signature in database
15 changed files with 323 additions and 94 deletions

View file

@ -1,5 +1,4 @@
from sqlite_utils import Database
from sqlite_utils.db import TransactionError
from sqlite_utils.utils import sqlite3
import pytest
import sys
@ -65,23 +64,10 @@ def test_database_close(tmpdir, memory):
sys.version_info < (3, 12),
reason="sqlite3.connect(autocommit=) requires Python 3.12",
)
def test_autocommit_false_connections_are_rejected(tmpdir):
# autocommit=False keeps an implicit transaction open at all times,
# which breaks the explicit transaction handling used by every write
# method, so the constructor refuses these connections
conn = sqlite3.connect(str(tmpdir / "test.db"), autocommit=False)
with pytest.raises(TransactionError):
Database(conn)
conn.close()
@pytest.mark.skipif(
sys.version_info < (3, 12),
reason="sqlite3.connect(autocommit=) requires Python 3.12",
)
def test_autocommit_true_connection_writes_persist(tmpdir):
@pytest.mark.parametrize("autocommit", [True, False])
def test_autocommit_connection_writes_persist(tmpdir, autocommit):
path = str(tmpdir / "test.db")
conn = sqlite3.connect(path, autocommit=True)
conn = sqlite3.connect(path, autocommit=autocommit)
db = Database(conn)
db["t"].insert({"id": 1}, pk="id")
db.execute("insert into t (id) values (2)")
@ -96,8 +82,9 @@ def test_autocommit_true_connection_writes_persist(tmpdir):
sys.version_info < (3, 12),
reason="sqlite3.connect(autocommit=) requires Python 3.12",
)
def test_autocommit_true_connection_transactions(tmpdir):
conn = sqlite3.connect(str(tmpdir / "test.db"), autocommit=True)
@pytest.mark.parametrize("autocommit", [True, False])
def test_autocommit_connection_transactions(tmpdir, autocommit):
conn = sqlite3.connect(str(tmpdir / "test.db"), autocommit=autocommit)
db = Database(conn)
db["t"].insert({"id": 1}, pk="id")
# atomic() rolls back on error
@ -124,8 +111,9 @@ def test_autocommit_true_connection_transactions(tmpdir):
sys.version_info < (3, 12),
reason="sqlite3.connect(autocommit=) requires Python 3.12",
)
def test_autocommit_true_connection_wal(tmpdir):
conn = sqlite3.connect(str(tmpdir / "test.db"), autocommit=True)
@pytest.mark.parametrize("autocommit", [True, False])
def test_autocommit_connection_wal(tmpdir, autocommit):
conn = sqlite3.connect(str(tmpdir / "test.db"), autocommit=autocommit)
db = Database(conn)
db.enable_wal()
assert db.journal_mode == "wal"