Problematic rollback semantics with nested transactions #2767
Description
Hi there, thanks for building this library!
I am trying to do something like this:
with db.transaction():
Thing.create()
try:
with db.transaction():
raise SomeException()
except SomeException as ex:
assert len(Thing) == 1
raise ex
But what's happening is:
BEGIN
- Noop because we're in a transaction
INSERT
- Exception is raised
ROLLBACK
ROLLBACK
That is, the BEGIN and ROLLBACK are unbalanced.
I believe you are aware this is the case, as you have recommended folks use atomic
, which handles this balancing. However I do not want to use atomic
, since it must create a savepoint, and I don't want to use savepoints (I find it makes the semantics of the transaction far more difficult to reason about, Python ecosystem seems to disagree but I'm not here to argue).
What I'd humbly suggest/ask is either:
-
Add support for
atomic(savepoint=False)
, so we can use atomic without savepoints, to get the balanced transaction behavior. -
Modify
_transaction.__exit__
so it only rolls back if we're dealing with the top transaction, something like:
def __exit__(self, exc_type, exc_val, exc_tb):
try:
if self.db.transaction_depth() == 1:
try:
if exc_type: # This logic was outside the transaction_depth() == 1 check
self.rollback(False)
else:
self.commit(False)
except:
self.rollback(False)
raise
finally:
self.db.pop_transaction()
I don't believe the current transaction
rollback behavior is desirable since it breaks the semantics of context managers by leaking actions across contexts (the inner context manager rolls back the transaction begun by the outer transaction manager).