Fixed bug in "future" version of :class:`.Engine` where emitting SQL during
the :meth:`.EngineEvents.do_begin` event hook would cause a re-entrant
condition due to autobegin, including the recipe documented for SQLite to
allow for savepoints and serializable isolation support.
Fixed issue in new :class:`_orm.Session` similar to that of the
:class:`_engine.Connection` where the new "autobegin" logic could be
tripped into a re-entrant state if SQL were executed within the
:meth:`.SessionEvents.after_transaction_create` event hook.
Also repair the new "testing_engine" pytest fixture to
set up for "future" engine appropriately, which wasn't working
leading to the test_execute.py tests not using the future
engine since recent
f1e96cb0874927a475d0c11139.
Fixes: #5845
Change-Id: Ib2432d8c8bd753e24be60720ec47affb2df15a4a
--- /dev/null
+.. change::
+ :tags: bug, engine, sqlite
+ :tickets: 5845
+
+ Fixed bug in the 2.0 "future" version of :class:`.Engine` where emitting
+ SQL during the :meth:`.EngineEvents.begin` event hook would cause a
+ re-entrant (recursive) condition due to autobegin, affecting among other
+ things the recipe documented for SQLite to allow for savepoints and
+ serializable isolation support.
+
+
+.. change::
+ :tags: bug, orm, regression
+ :tickets: 5845
+
+ Fixed issue in new :class:`_orm.Session` similar to that of the
+ :class:`_engine.Connection` where the new "autobegin" logic could be
+ tripped into a re-entrant (recursive) state if SQL were executed within the
+ :meth:`.SessionEvents.after_transaction_create` event hook.
\ No newline at end of file
if self._echo:
self.engine.logger.info("BEGIN (implicit)")
+ self.__in_begin = True
+
if self._has_events or self.engine._has_events:
self.dispatch.begin(self)
- self.__in_begin = True
try:
self.engine.dialect.do_begin(self.connection)
except BaseException as e:
self._take_snapshot(autobegin=autobegin)
+ # make sure transaction is assigned before we call the
+ # dispatch
+ self.session._transaction = self
+
self.session.dispatch.after_transaction_create(self.session, self)
@property
def _autobegin(self):
if not self.autocommit and self._transaction is None:
- self._transaction = SessionTransaction(self, autobegin=True)
+ trans = SessionTransaction(self, autobegin=True)
+ assert self._transaction is trans
return True
return False
if self._transaction is not None:
if subtransactions or _subtrans or nested:
trans = self._transaction._begin(nested=nested)
- self._transaction = trans
+ assert self._transaction is trans
if nested:
self._nested_transaction = trans
else:
"A transaction is already begun on this Session."
)
else:
- self._transaction = SessionTransaction(self, nested=nested)
+ trans = SessionTransaction(self, nested=nested)
+ assert self._transaction is trans
return self._transaction # needed for __enter__/__exit__ hook
def begin_nested(self):
from . import engines
def gen_testing_engine(
- url=None, options=None, future=False, asyncio=False
+ url=None, options=None, future=None, asyncio=False
):
if options is None:
options = {}
connection.close()
+class FutureSavepointTest(fixtures.FutureEngineMixin, SavepointTest):
+ pass
+
+
class TypeReflectionTest(fixtures.TestBase):
__only_on__ = "sqlite"
eq_(canary.be2.call_count, 1)
eq_(canary.be3.call_count, 2)
+ def test_emit_sql_in_autobegin(self, testing_engine):
+ e1 = testing_engine(config.db_url)
+
+ canary = Mock()
+
+ @event.listens_for(e1, "begin")
+ def begin(connection):
+ result = connection.execute(select(1)).scalar()
+ canary.got_result(result)
+
+ with e1.connect() as conn:
+ assert not conn._is_future
+
+ with conn.begin():
+ conn.execute(select(1)).scalar()
+ assert conn.in_transaction()
+
+ assert not conn.in_transaction()
+
+ eq_(canary.mock_calls, [call.got_result(1)])
+
def test_per_connection_plus_engine(self, testing_engine):
canary = Mock()
e1 = testing_engine(config.db_url)
class FutureEngineEventsTest(fixtures.FutureEngineMixin, EngineEventsTest):
- pass
+ def test_future_fixture(self, testing_engine):
+ e1 = testing_engine()
+
+ assert e1._is_future
+ with e1.connect() as conn:
+ assert conn._is_future
+
+ def test_emit_sql_in_autobegin(self, testing_engine):
+ e1 = testing_engine(config.db_url)
+
+ canary = Mock()
+
+ @event.listens_for(e1, "begin")
+ def begin(connection):
+ result = connection.execute(select(1)).scalar()
+ canary.got_result(result)
+
+ with e1.connect() as conn:
+ assert conn._is_future
+ conn.execute(select(1)).scalar()
+
+ assert conn.in_transaction()
+
+ conn.commit()
+
+ assert not conn.in_transaction()
+
+ eq_(canary.mock_calls, [call.got_result(1)])
class HandleErrorTest(fixtures.TestBase):
sess.rollback()
eq_(assertions, [True, True])
+ @testing.combinations((True,), (False,))
+ def test_autobegin_no_reentrant(self, future):
+ s1 = fixture_session(future=future)
+
+ canary = Mock()
+
+ @event.listens_for(s1, "after_transaction_create")
+ def after_transaction_create(session, transaction):
+ result = session.execute(select(1)).scalar()
+ canary.got_result(result)
+
+ with s1.begin():
+ pass
+
+ eq_(canary.mock_calls, [call.got_result(1)])
+
def test_flush_noautocommit_hook(self):
User, users = self.classes.User, self.tables.users