Skip to content

Commit 798e38c

Browse files
contextreffelixxm
authored andcommitted
Fixed #31090 -- Logged transaction management queries.
Thanks to Petter Strandmark for the original idea and Mariusz Felisiak for advice during the DjangoConUS 2022 Sprint!
1 parent c0a93d3 commit 798e38c

File tree

7 files changed

+109
-12
lines changed

7 files changed

+109
-12
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ answer newbie questions, and generally made Django that much better:
410410
Ifedapo Olarewaju <[email protected]>
411411
Igor Kolar <[email protected]>
412412
Illia Volochii <[email protected]>
413+
Ilya Bass
413414
Ilya Semenov <[email protected]>
414415
Ingo Klöcker <[email protected]>
415416
I.S. van Oostveen <[email protected]>

django/db/backends/base/base.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from collections import deque
99
from contextlib import contextmanager
1010

11+
from django.db.backends.utils import debug_transaction
12+
1113
try:
1214
import zoneinfo
1315
except ImportError:
@@ -307,12 +309,12 @@ def _cursor(self, name=None):
307309

308310
def _commit(self):
309311
if self.connection is not None:
310-
with self.wrap_database_errors:
312+
with debug_transaction(self, "COMMIT"), self.wrap_database_errors:
311313
return self.connection.commit()
312314

313315
def _rollback(self):
314316
if self.connection is not None:
315-
with self.wrap_database_errors:
317+
with debug_transaction(self, "ROLLBACK"), self.wrap_database_errors:
316318
return self.connection.rollback()
317319

318320
def _close(self):
@@ -488,9 +490,11 @@ def set_autocommit(
488490

489491
if start_transaction_under_autocommit:
490492
self._start_transaction_under_autocommit()
491-
else:
493+
elif autocommit:
492494
self._set_autocommit(autocommit)
493-
495+
else:
496+
with debug_transaction(self, "BEGIN"):
497+
self._set_autocommit(autocommit)
494498
self.autocommit = autocommit
495499

496500
if autocommit and self.run_commit_hooks_on_set_autocommit_on:

django/db/backends/oracle/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django.core.exceptions import ImproperlyConfigured
1414
from django.db import IntegrityError
1515
from django.db.backends.base.base import BaseDatabaseWrapper
16+
from django.db.backends.utils import debug_transaction
1617
from django.utils.asyncio import async_unsafe
1718
from django.utils.encoding import force_bytes, force_str
1819
from django.utils.functional import cached_property
@@ -306,7 +307,7 @@ def create_cursor(self, name=None):
306307

307308
def _commit(self):
308309
if self.connection is not None:
309-
with wrap_oracle_errors():
310+
with debug_transaction(self, "COMMIT"), wrap_oracle_errors():
310311
return self.connection.commit()
311312

312313
# Oracle doesn't support releasing savepoints. But we fake them when query

django/db/backends/utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,35 @@ def debug_sql(
144144
)
145145

146146

147+
@contextmanager
148+
def debug_transaction(connection, sql):
149+
start = time.monotonic()
150+
try:
151+
yield
152+
finally:
153+
if connection.queries_logged:
154+
stop = time.monotonic()
155+
duration = stop - start
156+
connection.queries_log.append(
157+
{
158+
"sql": "%s" % sql,
159+
"time": "%.3f" % duration,
160+
}
161+
)
162+
logger.debug(
163+
"(%.3f) %s; args=%s; alias=%s",
164+
duration,
165+
sql,
166+
None,
167+
connection.alias,
168+
extra={
169+
"duration": duration,
170+
"sql": sql,
171+
"alias": connection.alias,
172+
},
173+
)
174+
175+
147176
def split_tzname_delta(tzname):
148177
"""
149178
Split a time zone name into a 3-tuple of (name, sign, offset).

docs/ref/logging.txt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,13 @@ For performance reasons, SQL logging is only enabled when
196196
level or handlers that are installed.
197197

198198
This logging does not include framework-level initialization (e.g.
199-
``SET TIMEZONE``) or transaction management queries (e.g. ``BEGIN``,
200-
``COMMIT``, and ``ROLLBACK``). Turn on query logging in your database if you
201-
wish to view all database queries.
199+
``SET TIMEZONE``). Turn on query logging in your database if you wish to view
200+
all database queries.
201+
202+
.. versionchanged:: 4.2
203+
204+
Support for logging transaction management queries (``BEGIN``, ``COMMIT``,
205+
and ``ROLLBACK``) was added.
202206

203207
.. _django-security-logger:
204208

docs/releases/4.2.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@ Internationalization
199199
Logging
200200
~~~~~~~
201201

202-
* ...
202+
* The :ref:`django-db-logger` logger now logs transaction management queries
203+
(``BEGIN``, ``COMMIT``, and ``ROLLBACK``) at the ``DEBUG`` level.
203204

204205
Management Commands
205206
~~~~~~~~~~~~~~~~~~~

tests/backends/base/test_base.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
from unittest.mock import MagicMock, patch
22

3-
from django.db import DEFAULT_DB_ALIAS, connection, connections
3+
from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
44
from django.db.backends.base.base import BaseDatabaseWrapper
5-
from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
5+
from django.test import (
6+
SimpleTestCase,
7+
TestCase,
8+
TransactionTestCase,
9+
skipUnlessDBFeature,
10+
)
11+
from django.test.utils import CaptureQueriesContext, override_settings
612

7-
from ..models import Square
13+
from ..models import Person, Square
814

915

1016
class DatabaseWrapperTests(SimpleTestCase):
@@ -55,6 +61,57 @@ def test_check_database_version_supported_with_none_as_database_version(self):
5561
connection.check_database_version_supported()
5662

5763

64+
class DatabaseWrapperLoggingTests(TransactionTestCase):
65+
available_apps = []
66+
67+
@override_settings(DEBUG=True)
68+
def test_commit_debug_log(self):
69+
conn = connections[DEFAULT_DB_ALIAS]
70+
with CaptureQueriesContext(conn):
71+
with self.assertLogs("django.db.backends", "DEBUG") as cm:
72+
with transaction.atomic():
73+
Person.objects.create(first_name="first", last_name="last")
74+
75+
self.assertGreaterEqual(len(conn.queries_log), 3)
76+
self.assertEqual(conn.queries_log[-3]["sql"], "BEGIN")
77+
self.assertRegex(
78+
cm.output[0],
79+
r"DEBUG:django.db.backends:\(\d+.\d{3}\) "
80+
rf"BEGIN; args=None; alias={DEFAULT_DB_ALIAS}",
81+
)
82+
self.assertEqual(conn.queries_log[-1]["sql"], "COMMIT")
83+
self.assertRegex(
84+
cm.output[-1],
85+
r"DEBUG:django.db.backends:\(\d+.\d{3}\) "
86+
rf"COMMIT; args=None; alias={DEFAULT_DB_ALIAS}",
87+
)
88+
89+
@override_settings(DEBUG=True)
90+
def test_rollback_debug_log(self):
91+
conn = connections[DEFAULT_DB_ALIAS]
92+
with CaptureQueriesContext(conn):
93+
with self.assertLogs("django.db.backends", "DEBUG") as cm:
94+
with self.assertRaises(Exception), transaction.atomic():
95+
Person.objects.create(first_name="first", last_name="last")
96+
raise Exception("Force rollback")
97+
98+
self.assertEqual(conn.queries_log[-1]["sql"], "ROLLBACK")
99+
self.assertRegex(
100+
cm.output[-1],
101+
r"DEBUG:django.db.backends:\(\d+.\d{3}\) "
102+
rf"ROLLBACK; args=None; alias={DEFAULT_DB_ALIAS}",
103+
)
104+
105+
def test_no_logs_without_debug(self):
106+
with self.assertNoLogs("django.db.backends", "DEBUG"):
107+
with self.assertRaises(Exception), transaction.atomic():
108+
Person.objects.create(first_name="first", last_name="last")
109+
raise Exception("Force rollback")
110+
111+
conn = connections[DEFAULT_DB_ALIAS]
112+
self.assertEqual(len(conn.queries_log), 0)
113+
114+
58115
class ExecuteWrapperTests(TestCase):
59116
@staticmethod
60117
def call_execute(connection, params=None):

0 commit comments

Comments
 (0)