--- /dev/null
+.. change::
+ :tags: bug, regression, orm
+ :tickets: 6233
+
+ Fixed critical regression where the :class:`_orm.Session` could fail to
+ "autobegin" a new transaction when a flush occurred without an existing
+ transaction in place, implicitly placing the :class:`_orm.Session` into
+ legacy autocommit mode which commit the transaction. The
+ :class:`_orm.Session` now has a check that will prevent this condition from
+ occurring, in addition to repairing the flush issue.
+
+ Additionally, scaled back part of the change made as part of :ticket:`5226`
+ which can run autoflush during an unexpire operation, to not actually
+ do this in the case of a :class:`_orm.Session` using legacy
+ :paramref:`_orm.Session.autocommit` mode, as this incurs a commit within
+ a refresh operation.
\ No newline at end of file
result = False
- no_autoflush = bool(passive & attributes.NO_AUTOFLUSH)
+ no_autoflush = (
+ bool(passive & attributes.NO_AUTOFLUSH) or state.session.autocommit
+ )
# in the case of inheritance, particularly concrete and abstract
# concrete inheritance, the class manager might have some keys
if autocommit:
if future:
raise sa_exc.ArgumentError(
- "Cannot use autocommit mode with future=True. "
- "use the autobegin flag."
+ "Cannot use autocommit mode with future=True."
)
self.autocommit = True
else:
"Session objects."
)
if self._autobegin():
- if not subtransactions and not nested:
+ if not subtransactions and not nested and not _subtrans:
return self._transaction
if self._transaction is not None:
raise sa_exc.InvalidRequestError(
"A transaction is already begun on this Session."
)
+ elif not self.autocommit:
+ # outermost transaction. must be a not nested and not
+ # a subtransaction
+ assert not nested and not _subtrans and not subtransactions
+ trans = SessionTransaction(self)
+ assert self._transaction is trans
else:
+ # legacy autocommit mode
+ assert not self.future
trans = SessionTransaction(self, nested=nested)
assert self._transaction is trans
+
return self._transaction # needed for __enter__/__exit__ hook
def begin_nested(self):
s.commit()
is_(s._transaction, None)
+ def test_autobegin_within_flush(self):
+ """test :ticket:`6233`"""
+
+ s = Session(testing.db)
+
+ User, users = self.classes.User, self.tables.users
+
+ mapper(User, users)
+ s.add(User(name="u1"))
+ s.commit()
+
+ u1 = s.query(User).first()
+
+ s.commit()
+
+ u1.name = "newname"
+
+ s.flush()
+
+ eq_(s.connection().scalar(select(User.name)), "newname")
+ assert s.in_transaction()
+ s.rollback()
+ assert not s.in_transaction()
+ eq_(s.connection().scalar(select(User.name)), "u1")
+
+ def test_no_autoflush_or_commit_in_expire_w_autocommit(self):
+ """test second part of :ticket:`6233`.
+
+ Here we test that the "autoflush on unexpire" feature added
+ in :ticket:`5226` is turned off for a legacy autocommit session.
+
+ """
+
+ s = Session(
+ testing.db, autocommit=True, expire_on_commit=True, autoflush=True
+ )
+
+ User, users = self.classes.User, self.tables.users
+
+ mapper(User, users)
+
+ u1 = User(name="u1")
+ s.add(u1)
+ s.flush() # this commits
+
+ u1.name = "u2" # this does not commit
+
+ assert "id" not in u1.__dict__
+ u1.id # this unexpires
+
+ # never expired
+ eq_(u1.__dict__["name"], "u2")
+
+ eq_(u1.name, "u2")
+
+ # still in dirty collection
+ assert u1 in s.dirty
+
def test_autobegin_begin_method(self):
s = Session(testing.db)