mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-07-23 01:14:31 +02:00
db.execute() write statements now commit automatically
Raw write statements previously opened an implicit transaction that stayed open until something committed it - the write was visible on the same connection, making it look saved, but was silently rolled back when the connection closed. execute() now commits any implicit transaction it opens, so raw writes behave like every other write in the library: committed as soon as they run, unless an explicit transaction (db.begin() or db.atomic()) is open, in which case they join it. Row-returning writes such as INSERT ... RETURNING via db.query() commit once their rows have been iterated. atomic(), commit() and rollback() now issue literal COMMIT/ROLLBACK statements, which have identical semantics in every sqlite3 connection mode. The CLI bulk command uses db.atomic() instead of 'with db.conn:'. This simplifies the transaction contract to: everything commits immediately unless you explicitly opened a transaction. Docs updated throughout; changelog and upgrading guide cover the breaking change for code that relied on rolling back uncommitted execute() writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd
This commit is contained in:
parent
bcd9a26560
commit
f7ff3e2027
7 changed files with 114 additions and 33 deletions
|
|
@ -1140,7 +1140,7 @@ def insert_upsert_implementation(
|
|||
else:
|
||||
doc_chunks = [docs]
|
||||
for doc_chunk in doc_chunks:
|
||||
with db.conn:
|
||||
with db.atomic():
|
||||
db.conn.cursor().executemany(bulk_sql, doc_chunk)
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -324,6 +324,16 @@ CREATE TABLE IF NOT EXISTS "{}"(
|
|||
""".strip()
|
||||
|
||||
|
||||
_TRANSACTION_CONTROL_PREFIXES = (
|
||||
"BEGIN",
|
||||
"COMMIT",
|
||||
"END",
|
||||
"ROLLBACK",
|
||||
"SAVEPOINT",
|
||||
"RELEASE",
|
||||
)
|
||||
|
||||
|
||||
class Database:
|
||||
"""
|
||||
Wrapper for a SQLite database connection that adds a variety of useful utility methods.
|
||||
|
|
@ -462,13 +472,13 @@ class Database:
|
|||
try:
|
||||
yield self
|
||||
except BaseException:
|
||||
self.conn.rollback()
|
||||
self.conn.execute("ROLLBACK")
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
self.conn.commit()
|
||||
self.conn.execute("COMMIT")
|
||||
except BaseException:
|
||||
self.conn.rollback()
|
||||
self.conn.execute("ROLLBACK")
|
||||
raise
|
||||
|
||||
def begin(self) -> None:
|
||||
|
|
@ -487,14 +497,16 @@ class Database:
|
|||
"""
|
||||
Commit the current transaction. Does nothing if no transaction is open.
|
||||
"""
|
||||
self.conn.commit()
|
||||
if self.conn.in_transaction:
|
||||
self.conn.execute("COMMIT")
|
||||
|
||||
def rollback(self) -> None:
|
||||
"""
|
||||
Roll back the current transaction, discarding its changes. Does nothing
|
||||
if no transaction is open.
|
||||
"""
|
||||
self.conn.rollback()
|
||||
if self.conn.in_transaction:
|
||||
self.conn.execute("ROLLBACK")
|
||||
|
||||
@contextlib.contextmanager
|
||||
def ensure_autocommit_off(self) -> Generator[None, None, None]:
|
||||
|
|
@ -648,6 +660,7 @@ class Database:
|
|||
:raises ValueError: if the SQL statement does not return rows - use
|
||||
:meth:`execute` for those statements instead
|
||||
"""
|
||||
was_in_transaction = self.conn.in_transaction
|
||||
cursor = self.execute(sql, params or tuple())
|
||||
if cursor.description is None:
|
||||
raise ValueError(
|
||||
|
|
@ -659,6 +672,11 @@ class Database:
|
|||
def rows() -> Generator[dict, None, None]:
|
||||
for row in cursor:
|
||||
yield dict(zip(keys, row))
|
||||
if not was_in_transaction and self.conn.in_transaction:
|
||||
# A row-returning write such as INSERT ... RETURNING opened
|
||||
# an implicit transaction - commit it now that its rows have
|
||||
# been consumed
|
||||
self.conn.execute("COMMIT")
|
||||
|
||||
return rows()
|
||||
|
||||
|
|
@ -668,16 +686,33 @@ class Database:
|
|||
"""
|
||||
Execute SQL query and return a ``sqlite3.Cursor``.
|
||||
|
||||
A write statement - ``INSERT``, ``UPDATE``, ``CREATE TABLE`` and so on -
|
||||
is committed automatically, unless a transaction is already open, in
|
||||
which case it becomes part of that transaction. See
|
||||
:ref:`python_api_transactions`.
|
||||
|
||||
:param sql: SQL query to execute
|
||||
:param parameters: Parameters to use in that query - an iterable for ``where id = ?``
|
||||
parameters, or a dictionary for ``where id = :id``
|
||||
"""
|
||||
if self._tracer:
|
||||
self._tracer(sql, parameters)
|
||||
was_in_transaction = self.conn.in_transaction
|
||||
if parameters is not None:
|
||||
return self.conn.execute(sql, parameters)
|
||||
cursor = self.conn.execute(sql, parameters)
|
||||
else:
|
||||
return self.conn.execute(sql)
|
||||
cursor = self.conn.execute(sql)
|
||||
if (
|
||||
not was_in_transaction
|
||||
and self.conn.in_transaction
|
||||
and cursor.description is None
|
||||
and not sql.lstrip().upper().startswith(_TRANSACTION_CONTROL_PREFIXES)
|
||||
):
|
||||
# The statement opened an implicit transaction - commit it, so
|
||||
# that execute() behaves consistently with the rest of the
|
||||
# library and identically across connection modes
|
||||
self.conn.execute("COMMIT")
|
||||
return cursor
|
||||
|
||||
def executescript(self, sql: str) -> sqlite3.Cursor:
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue