]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Warn if an engine bind left an open transaction
authorFederico Caselli <cfederico87@gmail.com>
Fri, 15 Mar 2024 18:15:27 +0000 (19:15 +0100)
committerFederico Caselli <cfederico87@gmail.com>
Tue, 25 Jun 2024 20:01:07 +0000 (20:01 +0000)
Added a warning noting when an
:meth:`_engine.ConnectionEvents.engine_connect` event may be leaving
a transaction open, which can alter the behavior of a
class:`_orm.Session` using such an engine as bind.
On SQLAlchemy 2.1 :paramref:`_orm.Session.join_transaction_mode` will
instead be ignored in all cases when the session bind is
an :class:`_engine.Engine`.

Fixes: #11163
Change-Id: I10147876d07352f2dab898d615e98a9acd6eb91b
(cherry picked from commit 69a2d0903e427e99acceedcd4e29a17d0b012bbe)

doc/build/changelog/unreleased_20/11163.rst [new file with mode: 0644]
lib/sqlalchemy/orm/session.py
test/orm/test_transaction.py

diff --git a/doc/build/changelog/unreleased_20/11163.rst b/doc/build/changelog/unreleased_20/11163.rst
new file mode 100644 (file)
index 0000000..da21b45
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: orm
+    :tickets: 11163
+
+    Added a warning noting when an
+    :meth:`_engine.ConnectionEvents.engine_connect` event may be leaving
+    a transaction open, which can alter the behavior of a
+    :class:`_orm.Session` using such an engine as bind.
+    On SQLAlchemy 2.1 :paramref:`_orm.Session.join_transaction_mode` will
+    instead be ignored in all cases when the session bind is
+    an :class:`_engine.Engine`.
index c455ffac93046264d279971f9e41048f1535c48e..eb81f16e01e9efd2911e703c0011ef57f29ea225 100644 (file)
@@ -1211,6 +1211,17 @@ class SessionTransaction(_StateChange, TransactionalContext):
                         else:
                             join_transaction_mode = "rollback_only"
 
+                        if local_connect:
+                            util.warn(
+                                "The engine provided as bind produced a "
+                                "connection that is already in a transaction. "
+                                "This is usually caused by a core event, "
+                                "such as 'engine_connect', that has left a "
+                                "transaction open. The effective join "
+                                "transaction mode used by this session is "
+                                f"{join_transaction_mode!r}. To silence this "
+                                "warning, do not leave transactions open"
+                            )
                     if join_transaction_mode in (
                         "control_fully",
                         "rollback_only",
index e502a88833086966a63daa591fc92c4de2bfea1a..47bcf69b5718f1a952fa17b8e38322fada5ecadf 100644 (file)
@@ -108,7 +108,7 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
         trans.commit()
         assert len(sess.query(User).all()) == 1
 
-    @testing.variation(
+    join_transaction_mode = testing.variation(
         "join_transaction_mode",
         [
             "none",
@@ -118,6 +118,8 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
             "rollback_only",
         ],
     )
+
+    @join_transaction_mode
     @testing.variation("operation", ["commit", "close", "rollback", "nothing"])
     @testing.variation("external_state", ["none", "transaction", "savepoint"])
     def test_join_transaction_modes(
@@ -243,6 +245,36 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
         else:
             external_state.fail()
 
+    @join_transaction_mode
+    def test_join_transaction_mode_with_event(self, join_transaction_mode):
+        eng = engines.testing_engine()
+
+        @event.listens_for(eng, "engine_connect")
+        def make_transaction(conn):
+            conn.begin()
+
+        if join_transaction_mode.none:
+            s = Session(eng)
+        else:
+            s = Session(eng, join_transaction_mode=join_transaction_mode.name)
+        if (
+            join_transaction_mode.none
+            or join_transaction_mode.conditional_savepoint
+        ):
+            with expect_warnings(
+                "The engine provided as bind produced a "
+                "connection that is already in a transaction. "
+                "This is usually caused by a core event, "
+                "such as 'engine_connect', that has left a "
+                "transaction open. The effective join "
+                "transaction mode used by this session is "
+                "'rollback_only'. To silence this "
+                "warning, do not leave transactions open"
+            ):
+                s.connection()
+        else:
+            s.connection()
+
     def test_subtransaction_on_external_commit(self, connection_no_trans):
         users, User = self.tables.users, self.classes.User
 
@@ -839,7 +871,10 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
                 return_value=mock.Mock(
                     _is_future=False,
                     execution_options=mock.Mock(
-                        return_value=mock.Mock(_is_future=False)
+                        return_value=mock.Mock(
+                            _is_future=False,
+                            in_transaction=mock.Mock(return_value=False),
+                        )
                     ),
                 )
             )
@@ -857,7 +892,9 @@ class SessionTransactionTest(fixtures.RemovesEvents, FixtureTest):
 
     def test_execution_options_ignored_mid_transaction(self):
         bind = mock.Mock()
-        conn = mock.Mock(engine=bind)
+        conn = mock.Mock(
+            engine=bind, in_transaction=mock.Mock(return_value=False)
+        )
         bind.connect = mock.Mock(return_value=conn)
         sess = Session(bind=bind)
         sess.execute(text("select 1"))