mirror of
https://github.com/simonw/sqlite-utils.git
synced 2026-07-27 03:14:33 +02:00
Transaction cleanup no longer masks transaction-destroying errors
A RAISE(ROLLBACK) trigger or INSERT OR ROLLBACK conflict rolls back the
entire transaction and destroys every savepoint. The cleanup paths in
atomic() and query() then raised OperationalError ("no such savepoint" /
"cannot rollback - no transaction is active"), masking the original
IntegrityError - breaking user code that catches sqlite3.IntegrityError.
Cleanup now checks conn.in_transaction first: if the error already
destroyed the transaction there is nothing left to undo, and the
original exception propagates.
Refs https://github.com/simonw/sqlite-utils/issues/769#issuecomment-4900034150
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c2a1774409
commit
d9a0fd26e0
3 changed files with 34 additions and 6 deletions
|
|
@ -602,8 +602,13 @@ class Database:
|
|||
try:
|
||||
yield self
|
||||
except BaseException:
|
||||
self.conn.execute("ROLLBACK TO SAVEPOINT {};".format(savepoint))
|
||||
self.conn.execute("RELEASE SAVEPOINT {};".format(savepoint))
|
||||
# An error such as a RAISE(ROLLBACK) trigger can destroy
|
||||
# the whole transaction, savepoints included - cleaning up
|
||||
# anyway would mask the original exception with
|
||||
# "no such savepoint"
|
||||
if self.conn.in_transaction:
|
||||
self.conn.execute("ROLLBACK TO SAVEPOINT {};".format(savepoint))
|
||||
self.conn.execute("RELEASE SAVEPOINT {};".format(savepoint))
|
||||
raise
|
||||
else:
|
||||
self.conn.execute("RELEASE SAVEPOINT {};".format(savepoint))
|
||||
|
|
@ -612,13 +617,15 @@ class Database:
|
|||
try:
|
||||
yield self
|
||||
except BaseException:
|
||||
self.conn.execute("ROLLBACK")
|
||||
# rollback() is a no-op if the error already destroyed the
|
||||
# transaction, so the original exception propagates
|
||||
self.rollback()
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
self.conn.execute("COMMIT")
|
||||
except BaseException:
|
||||
self.conn.execute("ROLLBACK")
|
||||
self.rollback()
|
||||
raise
|
||||
|
||||
def begin(self) -> None:
|
||||
|
|
@ -870,8 +877,11 @@ class Database:
|
|||
return (dict(zip(keys, row)) for row in fetched)
|
||||
return (dict(zip(keys, row)) for row in cursor)
|
||||
finally:
|
||||
if not released:
|
||||
# An error occurred - undo anything the statement changed
|
||||
if not released and self.conn.in_transaction:
|
||||
# An error occurred - undo anything the statement changed.
|
||||
# If the error itself destroyed the transaction (such as a
|
||||
# RAISE(ROLLBACK) trigger) the savepoint is already gone
|
||||
# and there is nothing left to undo
|
||||
self.conn.execute('ROLLBACK TO "sqlite_utils_query"')
|
||||
self.conn.execute('RELEASE "sqlite_utils_query"')
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue