summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/unreleased_14/6002.rst9
-rw-r--r--lib/sqlalchemy/engine/base.py36
-rw-r--r--test/engine/test_logging.py85
3 files changed, 123 insertions, 7 deletions
diff --git a/doc/build/changelog/unreleased_14/6002.rst b/doc/build/changelog/unreleased_14/6002.rst
new file mode 100644
index 000000000..ded1ff3fa
--- /dev/null
+++ b/doc/build/changelog/unreleased_14/6002.rst
@@ -0,0 +1,9 @@
+.. change::
+ :tags: bug, engine
+ :tickets: 6002
+
+ Improved engine logging to note ROLLBACK and COMMIT which is logged while
+ the DBAPI driver is in AUTOCOMMIT mode. These ROLLBACK/COMMIT are library
+ level and do not have any effect when AUTOCOMMIT is in effect, however it's
+ still worthwhile to log as these indicate where SQLAlchemy sees the
+ "transaction" demarcation. \ No newline at end of file
diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py
index aa657cc52..b5a37f67e 100644
--- a/lib/sqlalchemy/engine/base.py
+++ b/lib/sqlalchemy/engine/base.py
@@ -826,6 +826,12 @@ class Connection(Connectable):
and self._nested_transaction.is_active
)
+ def _is_autocommit(self):
+ return (
+ self._execution_options.get("isolation_level", None)
+ == "AUTOCOMMIT"
+ )
+
def get_transaction(self):
"""Return the current root transaction in progress, if any.
@@ -876,7 +882,13 @@ class Connection(Connectable):
if self._still_open_and_dbapi_connection_is_valid:
if self._echo:
- self._log_info("ROLLBACK")
+ if self._is_autocommit():
+ self._log_info(
+ "ROLLBACK using DBAPI connection.rollback(), "
+ "DBAPI should ignore due to autocommit mode"
+ )
+ else:
+ self._log_info("ROLLBACK")
try:
self.engine.dialect.do_rollback(self.connection)
except BaseException as e:
@@ -889,11 +901,7 @@ class Connection(Connectable):
# if a connection has this set as the isolation level, we can skip
# the "autocommit" warning as the operation will do "autocommit"
# in any case
- if (
- autocommit
- and self._execution_options.get("isolation_level", None)
- != "AUTOCOMMIT"
- ):
+ if autocommit and not self._is_autocommit():
util.warn_deprecated_20(
"The current statement is being autocommitted using "
"implicit autocommit, which will be removed in "
@@ -906,7 +914,13 @@ class Connection(Connectable):
self.dispatch.commit(self)
if self._echo:
- self._log_info("COMMIT")
+ if self._is_autocommit():
+ self._log_info(
+ "COMMIT using DBAPI connection.commit(), "
+ "DBAPI should ignore due to autocommit mode"
+ )
+ else:
+ self._log_info("COMMIT")
try:
self.engine.dialect.do_commit(self.connection)
except BaseException as e:
@@ -1056,7 +1070,15 @@ class Connection(Connectable):
if self._dbapi_connection is not None:
conn = self._dbapi_connection
+
+ # this will do a reset-on-return every time, even if we
+ # called rollback() already. it might be worth optimizing
+ # this for the case that we are able to close without issue
conn.close()
+
+ # this is in fact never true outside of a bunch of
+ # artificial scenarios created by the test suite and its
+ # fixtures. the reset_agent should no longer be necessary.
if conn._reset_agent is self._transaction:
conn._reset_agent = None
diff --git a/test/engine/test_logging.py b/test/engine/test_logging.py
index ffabddb37..2c90b9bf1 100644
--- a/test/engine/test_logging.py
+++ b/test/engine/test_logging.py
@@ -586,6 +586,91 @@ class LoggingNameTest(fixtures.TestBase):
self._assert_no_name_in_execute(eng)
+class TransactionContextLoggingTest(fixtures.TestBase):
+ @testing.fixture()
+ def assert_buf(self, logging_engine):
+ buf = logging.handlers.BufferingHandler(100)
+ for log in [
+ logging.getLogger("sqlalchemy.engine"),
+ ]:
+ log.addHandler(buf)
+
+ def go(expected):
+ assert buf.buffer
+
+ buflines = [rec.msg % rec.args for rec in buf.buffer]
+
+ eq_(buflines, expected)
+ buf.flush()
+
+ yield go
+ for log in [
+ logging.getLogger("sqlalchemy.engine"),
+ ]:
+ log.removeHandler(buf)
+
+ @testing.fixture()
+ def logging_engine(self, testing_engine):
+ kw = {"echo": True, "future": True}
+ e = testing_engine(options=kw)
+ e.connect().close()
+ return e
+
+ def test_begin_once_block(self, logging_engine, assert_buf):
+ with logging_engine.begin():
+ pass
+
+ assert_buf(["BEGIN (implicit)", "COMMIT"])
+
+ def test_commit_as_you_go_block_commit(self, logging_engine, assert_buf):
+ with logging_engine.connect() as conn:
+ conn.begin()
+ conn.commit()
+
+ assert_buf(["BEGIN (implicit)", "COMMIT"])
+
+ def test_commit_as_you_go_block_rollback(self, logging_engine, assert_buf):
+ with logging_engine.connect() as conn:
+ conn.begin()
+ conn.rollback()
+
+ assert_buf(["BEGIN (implicit)", "ROLLBACK"])
+
+ def test_commit_as_you_go_block_commit_autocommit(
+ self, logging_engine, assert_buf
+ ):
+ with logging_engine.connect().execution_options(
+ isolation_level="AUTOCOMMIT"
+ ) as conn:
+ conn.begin()
+ conn.commit()
+
+ assert_buf(
+ [
+ "BEGIN (implicit)",
+ "COMMIT using DBAPI connection.commit(), DBAPI "
+ "should ignore due to autocommit mode",
+ ]
+ )
+
+ def test_commit_as_you_go_block_rollback_autocommit(
+ self, logging_engine, assert_buf
+ ):
+ with logging_engine.connect().execution_options(
+ isolation_level="AUTOCOMMIT"
+ ) as conn:
+ conn.begin()
+ conn.rollback()
+
+ assert_buf(
+ [
+ "BEGIN (implicit)",
+ "ROLLBACK using DBAPI connection.rollback(), DBAPI "
+ "should ignore due to autocommit mode",
+ ]
+ )
+
+
class LoggingTokenTest(fixtures.TestBase):
def setup_test(self):
self.buf = logging.handlers.BufferingHandler(100)