Skip to content

Commit 69331d0

Browse files
authored
Merge pull request #174 from SectorLabs/181799346-caller-sql-comment
Append the caller data to each SQL query
2 parents 70b1442 + 411169d commit 69331d0

File tree

8 files changed

+237
-24
lines changed

8 files changed

+237
-24
lines changed

manage.py

100644100755
File mode changed.

psqlextra/backend/operations.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
1-
from importlib import import_module
1+
from psqlextra.compiler import (
2+
PostgresAggregateCompiler,
3+
PostgresCompiler,
4+
PostgresDeleteCompiler,
5+
PostgresInsertCompiler,
6+
PostgresUpdateCompiler,
7+
)
28

39
from . import base_impl
410

511

612
class PostgresOperations(base_impl.operations()):
713
"""Simple operations specific to PostgreSQL."""
814

15+
compiler_map = {
16+
"SQLCompiler": PostgresCompiler,
17+
"SQLInsertCompiler": PostgresInsertCompiler,
18+
"SQLUpdateCompiler": PostgresUpdateCompiler,
19+
"SQLDeleteCompiler": PostgresDeleteCompiler,
20+
"SQLAggregateCompiler": PostgresAggregateCompiler,
21+
}
22+
923
def __init__(self, *args, **kwargs):
1024
super().__init__(*args, **kwargs)
1125

@@ -14,14 +28,9 @@ def __init__(self, *args, **kwargs):
1428
def compiler(self, compiler_name: str):
1529
"""Gets the SQL compiler with the specified name."""
1630

17-
# first let django try to find the compiler
18-
try:
19-
return super().compiler(compiler_name)
20-
except AttributeError:
21-
pass
22-
23-
# django can't find it, look in our own module
24-
if self._compiler_cache is None:
25-
self._compiler_cache = import_module("psqlextra.compiler")
31+
postgres_compiler = self.compiler_map.get(compiler_name)
32+
if postgres_compiler:
33+
return postgres_compiler
2634

27-
return getattr(self._compiler_cache, compiler_name)
35+
# Let Django try to find the compiler. Better run without caller comment than break
36+
return super().compiler(compiler_name)

psqlextra/compiler.py

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,100 @@
1+
import inspect
2+
import os
3+
import sys
4+
15
from collections.abc import Iterable
26
from typing import Tuple, Union
37

48
import django
59

10+
from django.conf import settings
611
from django.core.exceptions import SuspiciousOperation
712
from django.db.models import Expression, Model, Q
813
from django.db.models.fields.related import RelatedField
9-
from django.db.models.sql.compiler import SQLInsertCompiler, SQLUpdateCompiler
14+
from django.db.models.sql.compiler import (
15+
SQLAggregateCompiler,
16+
SQLCompiler,
17+
SQLDeleteCompiler,
18+
SQLInsertCompiler,
19+
SQLUpdateCompiler,
20+
)
1021
from django.db.utils import ProgrammingError
1122

1223
from .expressions import HStoreValue
1324
from .types import ConflictAction
1425

1526

27+
def append_caller_to_sql(sql):
28+
"""Append the caller to SQL queries.
29+
30+
Adds the calling file and function as an SQL comment to each query.
31+
Examples:
32+
INSERT INTO "tests_47ee19d1" ("id", "title")
33+
VALUES (1, 'Test')
34+
RETURNING "tests_47ee19d1"."id"
35+
/* 998020 test_append_caller_to_sql_crud .../django-postgres-extra/tests/test_append_caller_to_sql.py 55 */
36+
37+
SELECT "tests_47ee19d1"."id", "tests_47ee19d1"."title"
38+
FROM "tests_47ee19d1"
39+
WHERE "tests_47ee19d1"."id" = 1
40+
LIMIT 1
41+
/* 998020 test_append_caller_to_sql_crud .../django-postgres-extra/tests/test_append_caller_to_sql.py 69 */
42+
43+
UPDATE "tests_47ee19d1"
44+
SET "title" = 'success'
45+
WHERE "tests_47ee19d1"."id" = 1
46+
/* 998020 test_append_caller_to_sql_crud .../django-postgres-extra/tests/test_append_caller_to_sql.py 64 */
47+
48+
DELETE FROM "tests_47ee19d1"
49+
WHERE "tests_47ee19d1"."id" IN (1)
50+
/* 998020 test_append_caller_to_sql_crud .../django-postgres-extra/tests/test_append_caller_to_sql.py 74 */
51+
52+
Slow and blocking queries could be easily tracked down to their originator
53+
within the source code using the "pg_stat_activity" table.
54+
55+
Enable "PSQLEXTRA_ANNOTATE_SQL" within the database settings to enable this feature.
56+
"""
57+
58+
if not getattr(settings, "PSQLEXTRA_ANNOTATE_SQL", None):
59+
return sql
60+
61+
try:
62+
# Search for the first non-Django caller
63+
stack = inspect.stack()
64+
for stack_frame in stack[1:]:
65+
frame_filename = stack_frame[1]
66+
frame_line = stack_frame[2]
67+
frame_function = stack_frame[3]
68+
if "/django/" in frame_filename or "/psqlextra/" in frame_filename:
69+
continue
70+
71+
return f"{sql} /* {os.getpid()} {frame_function} {frame_filename} {frame_line} */"
72+
73+
# Django internal commands (like migrations) end up here
74+
return f"{sql} /* {os.getpid()} {sys.argv[0]} */"
75+
except Exception:
76+
# Don't break anything because this convinence function runs into an unexpected situation
77+
return sql
78+
79+
80+
class PostgresCompiler(SQLCompiler):
81+
def as_sql(self):
82+
sql, params = super().as_sql()
83+
return append_caller_to_sql(sql), params
84+
85+
86+
class PostgresDeleteCompiler(SQLDeleteCompiler):
87+
def as_sql(self):
88+
sql, params = super().as_sql()
89+
return append_caller_to_sql(sql), params
90+
91+
92+
class PostgresAggregateCompiler(SQLAggregateCompiler):
93+
def as_sql(self):
94+
sql, params = super().as_sql()
95+
return append_caller_to_sql(sql), params
96+
97+
1698
class PostgresUpdateCompiler(SQLUpdateCompiler):
1799
"""Compiler for SQL UPDATE statements that allows us to use expressions
18100
inside HStore values.
@@ -24,7 +106,8 @@ class PostgresUpdateCompiler(SQLUpdateCompiler):
24106

25107
def as_sql(self):
26108
self._prepare_query_values()
27-
return super().as_sql()
109+
sql, params = super().as_sql()
110+
return append_caller_to_sql(sql), params
28111

29112
def _prepare_query_values(self):
30113
"""Extra prep on query values by converting dictionaries into.
@@ -72,15 +155,27 @@ def _does_dict_contain_expression(data: dict) -> bool:
72155
class PostgresInsertCompiler(SQLInsertCompiler):
73156
"""Compiler for SQL INSERT statements."""
74157

75-
def __init__(self, *args, **kwargs):
76-
"""Initializes a new instance of :see:PostgresInsertCompiler."""
158+
def as_sql(self, return_id=False):
159+
"""Builds the SQL INSERT statement."""
160+
queries = [
161+
(append_caller_to_sql(sql), params)
162+
for sql, params in super().as_sql()
163+
]
164+
165+
return queries
77166

167+
168+
class PostgresInsertOnConflictCompiler(SQLInsertCompiler):
169+
"""Compiler for SQL INSERT statements."""
170+
171+
def __init__(self, *args, **kwargs):
172+
"""Initializes a new instance of
173+
:see:PostgresInsertOnConflictCompiler."""
78174
super().__init__(*args, **kwargs)
79175
self.qn = self.connection.ops.quote_name
80176

81177
def as_sql(self, return_id=False):
82178
"""Builds the SQL INSERT statement."""
83-
84179
queries = [
85180
self._rewrite_insert(sql, params, return_id)
86181
for sql, params in super().as_sql()
@@ -132,10 +227,12 @@ def _rewrite_insert(self, sql, params, return_id=False):
132227
self.qn(self.query.model._meta.pk.attname) if return_id else "*"
133228
)
134229

135-
return self._rewrite_insert_on_conflict(
230+
(sql, params) = self._rewrite_insert_on_conflict(
136231
sql, params, self.query.conflict_action.value, returning
137232
)
138233

234+
return append_caller_to_sql(sql), params
235+
139236
def _rewrite_insert_on_conflict(
140237
self, sql, params, conflict_action: ConflictAction, returning
141238
):

psqlextra/sql.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django.db.models import sql
99
from django.db.models.constants import LOOKUP_SEP
1010

11-
from .compiler import PostgresInsertCompiler, PostgresUpdateCompiler
11+
from .compiler import PostgresInsertOnConflictCompiler, PostgresUpdateCompiler
1212
from .expressions import HStoreColumn
1313
from .fields import HStoreField
1414
from .types import ConflictAction
@@ -179,7 +179,7 @@ def values(self, objs: List, insert_fields: List, update_fields: List = []):
179179
def get_compiler(self, using=None, connection=None):
180180
if using:
181181
connection = connections[using]
182-
return PostgresInsertCompiler(self, connection, using)
182+
return PostgresInsertOnConflictCompiler(self, connection, using)
183183

184184

185185
class PostgresUpdateQuery(sql.UpdateQuery):

tests/test_append_caller_to_sql.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import pytest
2+
3+
from django.db import connection, models
4+
from django.test.utils import CaptureQueriesContext, override_settings
5+
6+
from psqlextra.compiler import append_caller_to_sql
7+
8+
from .fake_model import get_fake_model
9+
10+
11+
class psqlextraSimulated:
12+
def callMockedClass(self):
13+
return MockedClass().mockedMethod()
14+
15+
16+
class MockedClass:
17+
def mockedMethod(self):
18+
return append_caller_to_sql("sql")
19+
20+
21+
def mockedFunction():
22+
return append_caller_to_sql("sql")
23+
24+
25+
@override_settings(PSQLEXTRA_ANNOTATE_SQL=False)
26+
def test_disable_append_caller_to_sql():
27+
commented_sql = mockedFunction()
28+
assert commented_sql == "sql"
29+
30+
31+
@pytest.mark.parametrize(
32+
"entry_point",
33+
[
34+
MockedClass().mockedMethod,
35+
psqlextraSimulated().callMockedClass,
36+
],
37+
)
38+
@override_settings(PSQLEXTRA_ANNOTATE_SQL=True)
39+
def test_append_caller_to_sql_class(entry_point):
40+
commented_sql = entry_point()
41+
assert commented_sql.startswith("sql /* ")
42+
assert "mockedMethod" in commented_sql
43+
assert __file__ in commented_sql
44+
45+
46+
@override_settings(PSQLEXTRA_ANNOTATE_SQL=True)
47+
def test_append_caller_to_sql_function():
48+
commented_sql = mockedFunction()
49+
assert commented_sql.startswith("sql /* ")
50+
assert "mockedFunction" in commented_sql
51+
assert __file__ in commented_sql
52+
53+
54+
@override_settings(PSQLEXTRA_ANNOTATE_SQL=True)
55+
def test_append_caller_to_sql_crud():
56+
model = get_fake_model(
57+
{
58+
"title": models.CharField(max_length=255, null=True),
59+
}
60+
)
61+
62+
obj = None
63+
with CaptureQueriesContext(connection) as queries:
64+
obj = model.objects.create(
65+
id=1,
66+
title="Test",
67+
)
68+
assert "test_append_caller_to_sql_crud " in queries[0]["sql"]
69+
70+
obj.title = "success"
71+
with CaptureQueriesContext(connection) as queries:
72+
obj.save()
73+
assert "test_append_caller_to_sql_crud " in queries[0]["sql"]
74+
75+
with CaptureQueriesContext(connection) as queries:
76+
assert model.objects.filter(id=obj.id)[0].id == obj.id
77+
assert "test_append_caller_to_sql_crud " in queries[0]["sql"]
78+
79+
with CaptureQueriesContext(connection) as queries:
80+
obj.delete()
81+
assert "test_append_caller_to_sql_crud " in queries[0]["sql"]

tests/test_on_conflict.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.core.exceptions import SuspiciousOperation
55
from django.db import connection, models
6+
from django.test.utils import CaptureQueriesContext, override_settings
67
from django.utils import timezone
78

89
from psqlextra.fields import HStoreField
@@ -13,6 +14,7 @@
1314

1415

1516
@pytest.mark.parametrize("conflict_action", ConflictAction.all())
17+
@override_settings(PSQLEXTRA_ANNOTATE_SQL=True)
1618
def test_on_conflict(conflict_action):
1719
"""Tests whether simple inserts work correctly."""
1820

@@ -23,9 +25,11 @@ def test_on_conflict(conflict_action):
2325
}
2426
)
2527

26-
obj = model.objects.on_conflict(
27-
[("title", "key1")], conflict_action
28-
).insert_and_get(title={"key1": "beer"}, cookies="cheers")
28+
with CaptureQueriesContext(connection) as queries:
29+
obj = model.objects.on_conflict(
30+
[("title", "key1")], conflict_action
31+
).insert_and_get(title={"key1": "beer"}, cookies="cheers")
32+
assert " test_on_conflict " in queries[0]["sql"]
2933

3034
model.objects.on_conflict(
3135
[("title", "key1")], conflict_action

tests/test_query.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
from django.db import models
1+
from django.db import connection, models
22
from django.db.models import Case, F, Q, Value, When
3+
from django.test.utils import CaptureQueriesContext, override_settings
34

45
from psqlextra.expressions import HStoreRef
56
from psqlextra.fields import HStoreField
@@ -134,3 +135,21 @@ def test_query_hstore_value_update_escape():
134135

135136
inst = model.objects.all().first()
136137
assert inst.title.get("en") == "console.log('test')"
138+
139+
140+
@override_settings(PSQLEXTRA_ANNOTATE_SQL=True)
141+
def test_query_comment():
142+
"""Tests whether the query is commented."""
143+
144+
model = get_fake_model(
145+
{
146+
"name": models.CharField(max_length=10),
147+
"value": models.IntegerField(),
148+
}
149+
)
150+
151+
with CaptureQueriesContext(connection) as queries:
152+
qs = model.objects.all()
153+
assert " test_query_comment " in str(qs.query)
154+
list(qs)
155+
assert " test_query_comment " in queries[0]["sql"]

tests/test_view_models.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from django.core.exceptions import ImproperlyConfigured
44
from django.db import models
5+
from django.test.utils import override_settings
56

67
from psqlextra.models import PostgresMaterializedViewModel, PostgresViewModel
78

@@ -11,6 +12,7 @@
1112
@pytest.mark.parametrize(
1213
"model_base", [PostgresViewModel, PostgresMaterializedViewModel]
1314
)
15+
@override_settings(PSQLEXTRA_ANNOTATE_SQL=True)
1416
def test_view_model_meta_query_set(model_base):
1517
"""Tests whether you can set a :see:QuerySet to be used as the underlying
1618
query for a view."""
@@ -26,7 +28,8 @@ def test_view_model_meta_query_set(model_base):
2628
expected_sql = 'SELECT "{0}"."id", "{0}"."name" FROM "{0}"'.format(
2729
model._meta.db_table
2830
)
29-
assert view_model._view_meta.query == (expected_sql, tuple())
31+
assert view_model._view_meta.query[0].startswith(expected_sql + " /* ")
32+
assert view_model._view_meta.query[1] == tuple()
3033

3134

3235
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)