Add db.begin(), db.commit() and db.rollback() methods

Manual transaction control previously required mixing raw
db.execute("begin") strings with methods on db.conn. These thin
wrappers give the documentation and callers a single consistent
idiom. Docs updated to use them throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
This commit is contained in:
Claude 2026-07-04 22:31:22 +00:00
commit 788c393a30
No known key found for this signature in database
5 changed files with 75 additions and 6 deletions

View file

@ -26,6 +26,7 @@ Everything else:
- ``table.upsert()`` and ``table.upsert_all()`` now detect the primary key or compound primary key of an existing table, so the ``pk=`` argument is no longer required when upserting into a table that already has a primary key.
- ``db.table(table_name).insert({})`` can now be used to insert a row consisting entirely of default values into an existing table, using ``INSERT INTO ... DEFAULT VALUES``. (:issue:`759`)
- Improvements to the ``sqlite-utils migrate`` command: ``--stop-before`` values that do not match any known migration are now an error instead of being silently ignored, ``--stop-before`` now works correctly with migration files that still use the older ``sqlite_migrate.Migrations`` class, and ``--list`` is now a read-only operation that no longer creates the database file or the migrations tracking table. ``migrations.applied()`` now returns migrations in the order they were applied.
- New ``db.begin()``, ``db.commit()`` and ``db.rollback()`` methods for taking manual control of transactions, as an alternative to the ``db.atomic()`` context manager.
- New documentation: :ref:`python_api_transactions` describes how transactions work and when changes are committed, and a new :ref:`upgrading` page details the changes needed to move between major versions.
.. _v4_0rc1:

View file

@ -87,7 +87,7 @@ Some operations cannot run inside a transaction, for example ``VACUUM`` or chang
A migration registered with ``transactional=False`` runs without a wrapping transaction, so if it fails part way through any changes it already made will not be rolled back, and re-applying will run the whole function again.
Avoid calling ``db.conn.commit()`` or otherwise managing transactions manually inside a transactional migration - register the migration with ``transactional=False`` if it needs to control its own transactions. Using ``with db.atomic():`` blocks inside a migration is fine: they nest as savepoints within the migration's transaction, so the migration as a whole still commits or rolls back as a single unit. See :ref:`python_api_transactions`.
Avoid calling ``db.commit()`` or otherwise managing transactions manually inside a transactional migration - register the migration with ``transactional=False`` if it needs to control its own transactions. Using ``with db.atomic():`` blocks inside a migration is fine: they nest as savepoints within the migration's transaction, so the migration as a whole still commits or rolls back as a single unit. See :ref:`python_api_transactions`.
Applying migrations using the CLI
=================================

View file

@ -156,7 +156,7 @@ only be left open here if you opened it yourself - through raw ``db.execute()``
writes or an explicit ``BEGIN``. In that case the decision to commit stays
with you: committing automatically on exit could silently persist
half-finished work, for example if your code returned early from the block.
Commit explicitly with ``db.conn.commit()``, or wrap your writes in
Commit explicitly with ``db.commit()``, or wrap your writes in
``db.atomic()`` which commits for you.
Note this differs from the ``sqlite3.Connection`` context manager in the
@ -350,12 +350,12 @@ The transaction is opened with a deferred ``BEGIN`` - SQLite takes the necessary
Raw SQL writes with db.execute()
--------------------------------
:ref:`db.execute() <python_api_execute>` is a thin wrapper around the underlying ``sqlite3`` connection, and it follows that connection's rules rather than this library's: a write statement opens a transaction that stays open until something commits it. Commit explicitly:
:ref:`db.execute() <python_api_execute>` is a thin wrapper around the underlying ``sqlite3`` connection, and it follows that connection's rules rather than this library's: a write statement opens a transaction that stays open until something commits it. Commit explicitly with ``db.commit()``:
.. code-block:: python
db.execute("insert into news (headline) values (?)", ["Dog wins award"])
db.conn.commit()
db.commit()
Or wrap the calls in ``db.atomic()``, which commits for you:
@ -371,7 +371,18 @@ If you do neither, the write can be deceptive: reads on the same connection will
Managing transactions yourself
------------------------------
You can take full manual control using ``db.execute("begin")`` (or any raw write, as above) followed by ``db.conn.commit()`` or ``db.conn.rollback()``.
You can take full manual control using the ``db.begin()``, ``db.commit()`` and ``db.rollback()`` methods:
.. code-block:: python
db.begin()
db.table("news").insert({"headline": "Dog wins award"})
if all_looks_good:
db.commit()
else:
db.rollback()
``db.begin()`` raises ``sqlite3.OperationalError`` if a transaction is already open. ``db.commit()`` and ``db.rollback()`` do nothing if there is no open transaction. A raw write executed with ``db.execute()`` (as above) opens a transaction implicitly, without needing ``db.begin()``.
The library will never commit a transaction you opened. If you call write methods such as ``insert()`` - or use ``db.atomic()`` - while your transaction is open, they participate in it using SQLite savepoints instead of committing: exiting an ``atomic()`` block releases its savepoint, but nothing is saved to disk until you commit the outer transaction yourself. If you roll back, their changes are rolled back too.