From 50938ee6f846ddd792921f5a0353c73134dbbeda Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sun, 5 Jul 2026 21:54:30 -0700 Subject: [PATCH] Rename ensure_autocommit_off() to ensure_autocommit_on(), closes #705 --- docs/changelog.rst | 1 + docs/upgrading.rst | 2 ++ sqlite_utils/db.py | 20 +++++++++++++------- tests/test_wal.py | 11 +++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7efeb95..0220967 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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`) - 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: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index e7fd273..09a9e2f 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -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. +**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. **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: diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index d009f35..d984bcf 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -610,16 +610,22 @@ class Database: self.conn.execute("ROLLBACK") @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:: - with db.ensure_autocommit_off(): + with db.ensure_autocommit_on(): # 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 try: @@ -783,7 +789,7 @@ class Database: if self.conn.in_transaction: cursor = self.conn.execute(sql, *args) else: - with self.ensure_autocommit_off(): + with self.ensure_autocommit_on(): cursor = self.conn.execute(sql, *args) if cursor.description is None: raise ValueError(message) @@ -1085,7 +1091,7 @@ class Database: """ if self.journal_mode != "wal": self._ensure_no_open_transaction("enable_wal()") - with self.ensure_autocommit_off(): + with self.ensure_autocommit_on(): self.execute("PRAGMA journal_mode=wal;") def disable_wal(self) -> None: @@ -1097,7 +1103,7 @@ class Database: """ if self.journal_mode != "delete": self._ensure_no_open_transaction("disable_wal()") - with self.ensure_autocommit_off(): + with self.ensure_autocommit_on(): self.execute("PRAGMA journal_mode=delete;") def _ensure_no_open_transaction(self, operation: str) -> None: diff --git a/tests/test_wal.py b/tests/test_wal.py index ee7ecf0..c5a9c60 100644 --- a/tests/test_wal.py +++ b/tests/test_wal.py @@ -49,6 +49,17 @@ def test_disable_wal_inside_transaction_raises(db_path_tmpdir): 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): # Calling enable_wal() when WAL is already enabled is a no-op, # so it is fine inside a transaction