Skip to content

Commit 1b76012

Browse files
committed
#214 Use Django 2.0 execute_wrapper()
1 parent 1fd2c4f commit 1b76012

File tree

5 files changed

+73
-201
lines changed

5 files changed

+73
-201
lines changed

project/tests/test_db.py

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Test profiling of DB queries without mocking, to catch possible
33
incompatibility
44
"""
5+
56
from django.shortcuts import reverse
67
from django.test import Client, TestCase
78

@@ -20,24 +21,28 @@ def setUpClass(cls):
2021
BlindFactory.create_batch(size=5)
2122
SilkyConfig().SILKY_META = False
2223

24+
def setUp(self):
25+
DataCollector().clear()
26+
2327
def test_profile_request_to_db(self):
2428
DataCollector().configure(Request(reverse('example_app:index')))
2529

2630
with silk_profile(name='test_profile'):
2731
resp = self.client.get(reverse('example_app:index'))
2832

29-
DataCollector().profiles.values()
30-
assert len(resp.context['blinds']) == 5
33+
self.assertEqual(len(DataCollector().queries), 1, [q['query'] for q in DataCollector().queries.values()])
34+
self.assertEqual(len(resp.context['blinds']), 5)
3135

3236
def test_profile_request_to_db_with_constraints(self):
3337
DataCollector().configure(Request(reverse('example_app:create')))
3438

3539
resp = self.client.post(reverse('example_app:create'), {'name': 'Foo'})
40+
self.assertEqual(len(DataCollector().queries), 2)
41+
self.assertTrue(list(DataCollector().queries.values())[-1]['query'].startswith('INSERT'))
3642
self.assertEqual(resp.status_code, 302)
3743

3844

3945
class TestAnalyzeQueries(TestCase):
40-
4146
@classmethod
4247
def setUpClass(cls):
4348
super().setUpClass()
@@ -48,7 +53,7 @@ def setUpClass(cls):
4853
@classmethod
4954
def tearDownClass(cls):
5055
super().tearDownClass()
51-
SilkyConfig().SILKLY_ANALYZE_QUERIES = False
56+
SilkyConfig().SILKY_ANALYZE_QUERIES = False
5257

5358
def test_analyze_queries(self):
5459
DataCollector().configure(Request(reverse('example_app:index')))
@@ -59,16 +64,3 @@ def test_analyze_queries(self):
5964

6065
DataCollector().profiles.values()
6166
assert len(resp.context['blinds']) == 5
62-
63-
64-
class TestAnalyzeQueriesExplainParams(TestAnalyzeQueries):
65-
66-
@classmethod
67-
def setUpClass(cls):
68-
super().setUpClass()
69-
SilkyConfig().SILKY_EXPLAIN_FLAGS = {'verbose': True}
70-
71-
@classmethod
72-
def tearDownClass(cls):
73-
super().tearDownClass()
74-
SilkyConfig().SILKY_EXPLAIN_FLAGS = None

project/tests/test_execute_sql.py

Lines changed: 0 additions & 120 deletions
This file was deleted.

silk/apps.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
from django.apps import AppConfig
2+
from django.db import connection
3+
4+
from silk.sql import SilkQueryWrapper
25

36

47
class SilkAppConfig(AppConfig):
58
default_auto_field = "django.db.models.AutoField"
69
name = "silk"
10+
11+
def ready(self):
12+
# Add wrapper to db connection
13+
if not any(isinstance(wrapper, SilkQueryWrapper) for wrapper in connection.execute_wrappers):
14+
connection.execute_wrappers.append(SilkQueryWrapper())

silk/middleware.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import random
33

44
from django.db import DatabaseError, transaction
5-
from django.db.models.sql.compiler import SQLCompiler
65
from django.urls import NoReverseMatch, reverse
76
from django.utils import timezone
87

@@ -11,7 +10,6 @@
1110
from silk.model_factory import RequestModelFactory, ResponseModelFactory
1211
from silk.profiling import dynamic
1312
from silk.profiling.profiler import silk_meta_profiler
14-
from silk.sql import execute_sql
1513

1614
Logger = logging.getLogger('silk.middleware')
1715

@@ -85,15 +83,11 @@ def _apply_dynamic_mappings(self):
8583
name = conf.get('name')
8684
if module and function:
8785
if start_line and end_line: # Dynamic context manager
88-
dynamic.inject_context_manager_func(module=module,
89-
func=function,
90-
start_line=start_line,
91-
end_line=end_line,
92-
name=name)
86+
dynamic.inject_context_manager_func(
87+
module=module, func=function, start_line=start_line, end_line=end_line, name=name
88+
)
9389
else: # Dynamic decorator
94-
dynamic.profile_function_or_method(module=module,
95-
func=function,
96-
name=name)
90+
dynamic.profile_function_or_method(module=module, func=function, name=name)
9791
else:
9892
raise KeyError('Invalid dynamic mapping %s' % conf)
9993

@@ -107,9 +101,6 @@ def process_request(self, request):
107101
Logger.debug('process_request')
108102
request.silk_is_intercepted = True
109103
self._apply_dynamic_mappings()
110-
if not hasattr(SQLCompiler, '_execute_sql'):
111-
SQLCompiler._execute_sql = SQLCompiler.execute_sql
112-
SQLCompiler.execute_sql = execute_sql
113104

114105
silky_config = SilkyConfig()
115106

silk/sql.py

Lines changed: 52 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
import logging
22
import traceback
33

4-
from django.core.exceptions import EmptyResultSet
4+
from django.apps import apps
5+
from django.db import connection
56
from django.utils import timezone
67
from django.utils.encoding import force_str
78

8-
from silk.collector import DataCollector
99
from silk.config import SilkyConfig
1010

1111
Logger = logging.getLogger('silk.sql')
1212

1313

14-
def _should_wrap(sql_query):
15-
if not DataCollector().request:
16-
return False
17-
18-
for ignore_str in SilkyConfig().SILKY_IGNORE_QUERIES:
19-
if ignore_str in sql_query:
20-
return False
21-
return True
22-
23-
2414
def _unpack_explanation(result):
2515
for row in result:
2616
if not isinstance(row, str):
@@ -34,16 +24,14 @@ def _explain_query(connection, q, params):
3424
if SilkyConfig().SILKY_ANALYZE_QUERIES:
3525
# Work around some DB engines not supporting analyze option
3626
try:
37-
prefix = connection.ops.explain_query_prefix(
38-
analyze=True, **(SilkyConfig().SILKY_EXPLAIN_FLAGS or {})
39-
)
27+
prefix = connection.ops.explain_query_prefix(analyze=True, **(SilkyConfig().SILKY_EXPLAIN_FLAGS or {}))
4028
except ValueError as error:
4129
error_str = str(error)
4230
if error_str.startswith("Unknown options:"):
4331
Logger.warning(
44-
"Database does not support analyzing queries with provided params. %s."
32+
"Database does not support analyzing queries with provided params. %s. "
4533
"SILKY_ANALYZE_QUERIES option will be ignored",
46-
error_str
34+
error_str,
4735
)
4836
prefix = connection.ops.explain_query_prefix()
4937
else:
@@ -61,40 +49,53 @@ def _explain_query(connection, q, params):
6149
return None
6250

6351

64-
def execute_sql(self, *args, **kwargs):
65-
"""wrapper around real execute_sql in order to extract information"""
52+
class SilkQueryWrapper:
53+
def __init__(self):
54+
# Local import to prevent messing app.ready()
55+
from silk.collector import DataCollector
6656

67-
try:
68-
q, params = self.as_sql()
69-
if not q:
70-
raise EmptyResultSet
71-
except EmptyResultSet:
72-
try:
73-
result_type = args[0]
74-
except IndexError:
75-
result_type = kwargs.get('result_type', 'multi')
76-
if result_type == 'multi':
77-
return iter([])
78-
else:
79-
return
80-
tb = ''.join(reversed(traceback.format_stack()))
81-
sql_query = q % tuple(force_str(param) for param in params)
82-
if _should_wrap(sql_query):
83-
query_dict = {
84-
'query': sql_query,
85-
'start_time': timezone.now(),
86-
'traceback': tb
87-
}
57+
self.data_collector = DataCollector()
58+
self.silk_model_table_names = [model._meta.db_table for model in apps.get_app_config('silk').get_models()]
59+
60+
def __call__(self, execute, sql, params, many, context):
61+
sql_query = sql % tuple(force_str(param) for param in params) if params else sql
62+
query_dict = None
63+
if self._should_wrap(sql_query):
64+
tb = ''.join(reversed(traceback.format_stack()))
65+
query_dict = {'query': sql_query, 'start_time': timezone.now(), 'traceback': tb}
8866
try:
89-
return self._execute_sql(*args, **kwargs)
67+
return execute(sql, params, many, context)
9068
finally:
91-
query_dict['end_time'] = timezone.now()
92-
request = DataCollector().request
93-
if request:
94-
query_dict['request'] = request
95-
if getattr(self.query.model, '__module__', '') != 'silk.models':
96-
query_dict['analysis'] = _explain_query(self.connection, q, params)
97-
DataCollector().register_query(query_dict)
98-
else:
99-
DataCollector().register_silk_query(query_dict)
100-
return self._execute_sql(*args, **kwargs)
69+
if query_dict:
70+
query_dict['end_time'] = timezone.now()
71+
request = self.data_collector.request
72+
if request:
73+
query_dict['request'] = request
74+
if not any(table_name in sql_query for table_name in self.silk_model_table_names):
75+
query_dict['analysis'] = _explain_query(connection, sql, params)
76+
self.data_collector.register_query(query_dict)
77+
else:
78+
self.data_collector.register_silk_query(query_dict)
79+
80+
def _should_wrap(self, sql_query):
81+
# Must have a request ongoing
82+
if not self.data_collector.request:
83+
return False
84+
85+
# Must not try to explain 'EXPLAIN' queries or transaction stuff
86+
if any(
87+
sql_query.startswith(keyword)
88+
for keyword in [
89+
'SAVEPOINT',
90+
'RELEASE SAVEPOINT',
91+
'ROLLBACK TO SAVEPOINT',
92+
'PRAGMA',
93+
connection.ops.explain_query_prefix(),
94+
]
95+
):
96+
return False
97+
98+
for ignore_str in SilkyConfig().SILKY_IGNORE_QUERIES:
99+
if ignore_str in sql_query:
100+
return False
101+
return True

0 commit comments

Comments
 (0)