Rename ensure_autocommit_off() to ensure_autocommit_on(), closes #705

The ensure_autocommit_off() context manager was misleadingly named: it
sets isolation_level = None on the underlying sqlite3 connection, which
puts the driver INTO autocommit mode (no implicit transactions) - the
opposite of what the name and docstring claimed.

The behavior was always correct for its call sites, which need
autocommit to run PRAGMA statements like journal_mode=wal. This renames
the method to ensure_autocommit_on() with an accurate docstring, updates
the three internal call sites, adds tests, and documents the rename in
the changelog and the 3.x to 4.0 upgrading guide. The old name is
removed outright since 4.0 permits breaking changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2026-07-05 21:50:20 -07:00
commit 6df6da1a8e
4 changed files with 27 additions and 7 deletions

View file

@ -13,6 +13,7 @@ Breaking changes:
- ``table.foreign_keys`` now returns ``ForeignKey`` objects that are dataclasses rather than ``namedtuple`` instances, so they can no longer be unpacked or indexed as ``(table, column, other_table, other_column)`` tuples - access their fields by name instead. Compound (multi-column) foreign keys are now represented as a single ``ForeignKey`` with ``is_compound=True`` and populated ``columns``/``other_columns`` tuples, where ``column`` and ``other_column`` are ``None``. Previously they were returned as one ``ForeignKey`` per column, misleadingly suggesting several independent foreign keys. See :ref:`upgrading_3_to_4` for details. (:issue:`594`) - ``table.foreign_keys`` now returns ``ForeignKey`` objects that are dataclasses rather than ``namedtuple`` instances, so they can no longer be unpacked or indexed as ``(table, column, other_table, other_column)`` tuples - access their fields by name instead. Compound (multi-column) foreign keys are now represented as a single ``ForeignKey`` with ``is_compound=True`` and populated ``columns``/``other_columns`` tuples, where ``column`` and ``other_column`` are ``None``. Previously they were returned as one ``ForeignKey`` per column, misleadingly suggesting several independent foreign keys. See :ref:`upgrading_3_to_4` for details. (:issue:`594`)
- Removed support for using ``sqlean.py`` as a drop-in replacement for the Python standard library ``sqlite3`` module. ``sqlite-utils`` will now use ``pysqlite3`` if it is installed, otherwise it will use ``sqlite3`` from the standard library. - Removed support for using ``sqlean.py`` as a drop-in replacement for the Python standard library ``sqlite3`` module. ``sqlite-utils`` will now use ``pysqlite3`` if it is installed, otherwise it will use ``sqlite3`` from the standard library.
- The ``db.ensure_autocommit_off()`` context manager has been renamed to ``db.ensure_autocommit_on()``, because the old name described the opposite of what it did. The method temporarily puts the connection into driver-level autocommit mode - by setting ``isolation_level = None`` - so that statements such as ``PRAGMA journal_mode=wal`` can run outside of an implicit transaction. (:issue:`705`)
Compound foreign key support: Compound foreign key support:

View file

@ -77,6 +77,8 @@ Python API changes
**table.convert() no longer skips falsey values.** Matching the CLI change above, ``table.convert()`` now converts every value. The ``skip_false`` parameter has been removed - previously it defaulted to ``True``, skipping empty strings and other falsey values. **table.convert() no longer skips falsey values.** Matching the CLI change above, ``table.convert()`` now converts every value. The ``skip_false`` parameter has been removed - previously it defaulted to ``True``, skipping empty strings and other falsey values.
**ensure_autocommit_off() is now ensure_autocommit_on().** The ``db.ensure_autocommit_off()`` context manager has been renamed to ``db.ensure_autocommit_on()``. The old name described the opposite of what the method did: it temporarily puts the connection into driver-level autocommit mode (by setting ``isolation_level = None``), so that statements such as ``PRAGMA journal_mode=wal`` can run outside of an implicit transaction. The behavior is unchanged - update any calls to use the new name.
**View.enable_fts() has been removed.** The ``View`` class previously had an ``enable_fts()`` method that existed only to raise ``NotImplementedError`` - full-text search is not supported for views. Calling it now raises ``AttributeError`` like any other missing method. **View.enable_fts() has been removed.** The ``View`` class previously had an ``enable_fts()`` method that existed only to raise ``NotImplementedError`` - full-text search is not supported for views. Calling it now raises ``AttributeError`` like any other missing method.
**ForeignKey is now a dataclass, not a namedtuple.** The ``ForeignKey`` objects returned by ``table.foreign_keys`` gained new fields - ``columns``, ``other_columns``, ``is_compound``, ``on_delete`` and ``on_update`` - so that compound (multi-column) foreign keys and foreign key actions can be represented. To make room for those fields cleanly ``ForeignKey`` is now a dataclass rather than a ``namedtuple``, so it can no longer be unpacked or indexed as a tuple. Access its fields by name instead: **ForeignKey is now a dataclass, not a namedtuple.** The ``ForeignKey`` objects returned by ``table.foreign_keys`` gained new fields - ``columns``, ``other_columns``, ``is_compound``, ``on_delete`` and ``on_update`` - so that compound (multi-column) foreign keys and foreign key actions can be represented. To make room for those fields cleanly ``ForeignKey`` is now a dataclass rather than a ``namedtuple``, so it can no longer be unpacked or indexed as a tuple. Access its fields by name instead:

View file

@ -610,16 +610,22 @@ class Database:
self.conn.execute("ROLLBACK") self.conn.execute("ROLLBACK")
@contextlib.contextmanager @contextlib.contextmanager
def ensure_autocommit_off(self) -> Generator[None, None, None]: def ensure_autocommit_on(self) -> Generator[None, None, None]:
""" """
Ensure autocommit is off for this database connection. Ensure the connection is in driver-level autocommit mode for the
duration of a block of code.
This temporarily sets ``isolation_level = None`` on the underlying
``sqlite3`` connection, so the driver does not open implicit
transactions. This is useful for statements such as
``PRAGMA journal_mode=wal`` which cannot run inside a transaction.
Example usage:: Example usage::
with db.ensure_autocommit_off(): with db.ensure_autocommit_on():
# do stuff here # do stuff here
This will reset to the previous autocommit state at the end of the block. The previous ``isolation_level`` is restored at the end of the block.
""" """
old_isolation_level = self.conn.isolation_level old_isolation_level = self.conn.isolation_level
try: try:
@ -783,7 +789,7 @@ class Database:
if self.conn.in_transaction: if self.conn.in_transaction:
cursor = self.conn.execute(sql, *args) cursor = self.conn.execute(sql, *args)
else: else:
with self.ensure_autocommit_off(): with self.ensure_autocommit_on():
cursor = self.conn.execute(sql, *args) cursor = self.conn.execute(sql, *args)
if cursor.description is None: if cursor.description is None:
raise ValueError(message) raise ValueError(message)
@ -1085,7 +1091,7 @@ class Database:
""" """
if self.journal_mode != "wal": if self.journal_mode != "wal":
self._ensure_no_open_transaction("enable_wal()") self._ensure_no_open_transaction("enable_wal()")
with self.ensure_autocommit_off(): with self.ensure_autocommit_on():
self.execute("PRAGMA journal_mode=wal;") self.execute("PRAGMA journal_mode=wal;")
def disable_wal(self) -> None: def disable_wal(self) -> None:
@ -1097,7 +1103,7 @@ class Database:
""" """
if self.journal_mode != "delete": if self.journal_mode != "delete":
self._ensure_no_open_transaction("disable_wal()") self._ensure_no_open_transaction("disable_wal()")
with self.ensure_autocommit_off(): with self.ensure_autocommit_on():
self.execute("PRAGMA journal_mode=delete;") self.execute("PRAGMA journal_mode=delete;")
def _ensure_no_open_transaction(self, operation: str) -> None: def _ensure_no_open_transaction(self, operation: str) -> None:

View file

@ -49,6 +49,17 @@ def test_disable_wal_inside_transaction_raises(db_path_tmpdir):
assert [r["id"] for r in db["test"].rows] == [1] assert [r["id"] for r in db["test"].rows] == [1]
def test_ensure_autocommit_on(db_path_tmpdir):
db, path, tmpdir = db_path_tmpdir
previous_isolation_level = db.conn.isolation_level
assert previous_isolation_level is not None
with db.ensure_autocommit_on():
# isolation_level of None means driver-level autocommit mode
assert db.conn.isolation_level is None
# Restored afterwards
assert db.conn.isolation_level == previous_isolation_level
def test_enable_wal_noop_inside_transaction_is_allowed(db_path_tmpdir): def test_enable_wal_noop_inside_transaction_is_allowed(db_path_tmpdir):
# Calling enable_wal() when WAL is already enabled is a no-op, # Calling enable_wal() when WAL is already enabled is a no-op,
# so it is fine inside a transaction # so it is fine inside a transaction