nested "begin_nested" blocks don't track owning states

Issue #3352 resolved
Mike Bayer repo owner created an issue

as a result of #2452 in 9cf10db8aa4692dc6, the scope of objects within a begin_nested() is tracked based on dirtyness only. This fails when there was another begin_nested() completing successfully within the block, because dirtyness is reset in that case. Additional bookkeeping will be needed to ensure objects that are dirtied within the nested trans are tracked without being reset by a sub-transaction.

from sqlalchemy import Column, Integer
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session

Base = declarative_base()


class Test(Base):
    __tablename__ = 'test'
    id = Column(Integer, primary_key=True)
    value = Column(Integer)

engine = create_engine('postgresql://scott:tiger@localhost/test', echo='debug')

Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)

s = Session(engine)
t1 = Test(value=1)
t2 = Test(value=1)
t3 = Test(value=1)
s.add_all([t1, t2, t3])
s.commit()

try:
    with s.begin_nested():
        t1.value = 2
        with s.begin_nested():
            t2.value = 2
        t3.value = 2
        raise ValueError("x")
except ValueError:
    assert (t1.value, t2.value, t3.value) == (1, 1, 1), (t1.value, t2.value, t3.value)
else:
    assert False

it seems like the "_dirty" list on transaction is only populated in the subtransaction within the flush, so this never has a chance to propagate outwards on a successful commit. We will need to add something to SessionTransaction.commit() so that the subtransaction has a chance to propagate its dirty list to the parent.

may be safe for a 0.9.10 backport.

Comments (4)

  1. Mike Bayer reporter

    OK, this is all it needs:

    diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
    index f3ad234..4619027 100644
    --- a/lib/sqlalchemy/orm/session.py
    +++ b/lib/sqlalchemy/orm/session.py
    @@ -305,6 +305,7 @@ class SessionTransaction(object):
                 self._deleted.clear()
             elif self.nested:
                 self._parent._new.update(self._new)
    +            self._parent._dirty.update(self._dirty)
                 self._parent._deleted.update(self._deleted)
                 self._parent._key_switches.update(self._key_switches)
    
  2. Mike Bayer reporter
    • Fixed bug where the state tracking within multiple, nested :meth:.Session.begin_nested operations would fail to propagate the "dirty" flag for an object that had been updated within the inner savepoint, such that if the enclosing savepoint were rolled back, the object would not be part of the state that was expired and therefore reverted to its database state. fixes #3352

    → <<cset 359f471a1203>>

  3. Mike Bayer reporter
    • Fixed bug where the state tracking within multiple, nested :meth:.Session.begin_nested operations would fail to propagate the "dirty" flag for an object that had been updated within the inner savepoint, such that if the enclosing savepoint were rolled back, the object would not be part of the state that was expired and therefore reverted to its database state. fixes #3352

    (cherry picked from commit 359f471a1203cafd5dc99b5b078ba7d788b67cec)

    → <<cset 7c32b4c2a61e>>

  4. Log in to comment