mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-07-23 09:24:31 +02:00
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:
parent
6c88067ab7
commit
788c393a30
5 changed files with 75 additions and 6 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
=================================
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -455,6 +455,31 @@ class Database:
|
|||
self.conn.rollback()
|
||||
raise
|
||||
|
||||
def begin(self) -> None:
|
||||
"""
|
||||
Start a transaction with ``BEGIN``, taking manual control of transaction
|
||||
handling. End it by calling :meth:`commit` or :meth:`rollback`.
|
||||
|
||||
Raises ``sqlite3.OperationalError`` if a transaction is already open.
|
||||
|
||||
Most code should use the :meth:`atomic` context manager instead, which
|
||||
commits and rolls back automatically. See :ref:`python_api_transactions`.
|
||||
"""
|
||||
self.execute("BEGIN")
|
||||
|
||||
def commit(self) -> None:
|
||||
"""
|
||||
Commit the current transaction. Does nothing if no transaction is open.
|
||||
"""
|
||||
self.conn.commit()
|
||||
|
||||
def rollback(self) -> None:
|
||||
"""
|
||||
Roll back the current transaction, discarding its changes. Does nothing
|
||||
if no transaction is open.
|
||||
"""
|
||||
self.conn.rollback()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ensure_autocommit_off(self) -> Generator[None, None, None]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import pytest
|
||||
|
||||
from sqlite_utils.db import _iter_complete_sql_statements
|
||||
from sqlite_utils.db import Database, _iter_complete_sql_statements
|
||||
from sqlite_utils.utils import sqlite3
|
||||
|
||||
|
||||
|
|
@ -189,3 +189,35 @@ def test_atomic_inside_manual_transaction_uses_savepoint(fresh_db):
|
|||
fresh_db["t"].insert({"id": 3}, pk="id")
|
||||
fresh_db.conn.commit()
|
||||
assert [r["id"] for r in fresh_db["t"].rows] == [1, 3]
|
||||
|
||||
|
||||
def test_begin_commit_rollback(tmpdir):
|
||||
path = str(tmpdir / "test.db")
|
||||
db = Database(path)
|
||||
db["t"].insert({"id": 1}, pk="id")
|
||||
db.begin()
|
||||
db["t"].insert({"id": 2}, pk="id")
|
||||
assert db.conn.in_transaction
|
||||
db.rollback()
|
||||
assert not db.conn.in_transaction
|
||||
assert [r["id"] for r in db["t"].rows] == [1]
|
||||
db.begin()
|
||||
db["t"].insert({"id": 3}, pk="id")
|
||||
db.commit()
|
||||
db.close()
|
||||
db2 = Database(path)
|
||||
assert [r["id"] for r in db2["t"].rows] == [1, 3]
|
||||
db2.close()
|
||||
|
||||
|
||||
def test_begin_inside_transaction_errors(fresh_db):
|
||||
fresh_db.begin()
|
||||
with pytest.raises(sqlite3.OperationalError):
|
||||
fresh_db.begin()
|
||||
fresh_db.rollback()
|
||||
|
||||
|
||||
def test_commit_and_rollback_without_transaction_are_noops(fresh_db):
|
||||
fresh_db.commit()
|
||||
fresh_db.rollback()
|
||||
assert not fresh_db.conn.in_transaction
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue