Skip to content

Commit acb8345

Browse files
committed
Added some debug functionality which makes it possible to see from where (python source file and line) an SQL query comes when looking at the sql query summary available at the bottom of pages in debug mode, on INTERNAL_IPS.
- Legacy-Id: 13188
1 parent 814a6d4 commit acb8345

4 files changed

Lines changed: 268 additions & 3 deletions

File tree

ietf/static/ietf/css/ietf.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,3 +824,22 @@ blockquote {
824824
page-break-before: always;
825825
}
826826
}
827+
828+
.modal-max {
829+
width: 90%;
830+
}
831+
832+
#debug-query-table .code {
833+
background-color: #eee;
834+
font-family: 'PT Mono', monospace;
835+
font-size: 0.8em;
836+
word-break: break-all;
837+
}
838+
839+
#debug-query-table table {
840+
max-width: 100%;
841+
}
842+
843+
#debug-query-table .sql {
844+
border-top: 1px solid #555;
845+
}

ietf/templates/debug.html

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
onclick="$('#debug-query-table').toggleClass('hide');">Show</a>
1414
{% endif %}
1515
</p>
16-
<table class="table table-condensed table-striped tablesorter hide" id="debug-query-table">
16+
<table class="table table-condensed tablesorter hide" id="debug-query-table">
1717
<thead>
1818
<tr>
1919
<th data-header="sequence">#</th>
@@ -28,12 +28,52 @@
2828
<tbody>
2929
{% with sql_queries|annotate_sql_queries as sql_query_info %}
3030
{% for query in sql_query_info %}
31-
<tr>
31+
<tr class="sql">
3232
<td>{{ forloop.counter }}</td>
3333
<td>{{ query.sql|expand_comma|escape }}</td>
3434
<td>{{ query.count }}</td>
3535
<td>{{ query.where }}</td>
36-
<td>{{ query.loc }}</td>
36+
<td>
37+
{{ query.loc }}
38+
{% if query.origin %}
39+
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#modal-{{forloop.counter}}" >Origin</button>
40+
{% endif %}
41+
<div class="modal fade" id="modal-{{forloop.counter}}" tabindex="-1" role="dialog" aria-labelledby="modal-title-{{forloop.counter}}">
42+
<div class="modal-dialog modal-max" role="document">
43+
<div class="modal-content">
44+
<div class="modal-header">
45+
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
46+
<h4 class="modal-title" id="modal-title-{{forloop.counter}}">QuerySet Origin for Query #{{forloop.counter}}</h4>
47+
</div>
48+
<div class="modal-body">
49+
<table class="table table-condensed">
50+
<thead>
51+
<tr>
52+
<th>File (line)</th>
53+
<th>Method</th>
54+
<th>Code</th>
55+
</tr>
56+
</thead>
57+
<tbody>
58+
{% if query.origin %}
59+
{% for origin in query.origin %}
60+
<tr class="code">
61+
<td>{{ origin.1 }}({{ origin.2 }})</td>
62+
<td>{{ origin.6 }}()</td>
63+
<td>{{ origin.4.0 }}</td>
64+
</tr>
65+
{% endfor %}
66+
{% endif %}
67+
</tbody>
68+
</table>
69+
</div>
70+
<div class="modal-footer">
71+
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
72+
</div>
73+
</div>
74+
</div>
75+
</div>
76+
</td>
3777
<td>{{ query.time }}</td>
3878
<td>{{ query.time_accum }}</td>
3979
</tr>

patch/README

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
This directory is for patches to the datatracker environment, not for patches
3+
to the datatracker itself (those are ephemeral, in the sense that they should
4+
not stay around as patches, they should be committed to the repository if they
5+
are of long-term value).
6+
7+
As an example, trace-django-queryset-origin.patch is a patch to django 1.10
8+
which adds some meta-information to the query entries provided for debug
9+
purpose by django.template.context_processors.debug as sql_queries.
10+
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
--- ../x/env/lib/python2.7/site-packages/django/db/models/query.py 2017-04-07 15:10:29.426831000 -0700
2+
+++ env/lib/python2.7/site-packages/django/db/models/query.py 2017-04-07 15:18:39.938521412 -0700
3+
@@ -5,6 +5,8 @@
4+
import copy
5+
import sys
6+
import warnings
7+
+import inspect
8+
+import logging
9+
from collections import OrderedDict, deque
10+
11+
from django.conf import settings
12+
@@ -34,6 +36,7 @@
13+
# Pull into this namespace for backwards compatibility.
14+
EmptyResultSet = sql.EmptyResultSet
15+
16+
+logger = logging.getLogger('django.db.backends')
17+
18+
class BaseIterable(object):
19+
def __init__(self, queryset):
20+
@@ -51,6 +54,7 @@
21+
compiler = queryset.query.get_compiler(using=db)
22+
# Execute the query. This will also fill compiler.select, klass_info,
23+
# and annotations.
24+
+
25+
results = compiler.execute_sql()
26+
select, klass_info, annotation_col_map = (compiler.select, compiler.klass_info,
27+
compiler.annotation_col_map)
28+
@@ -174,6 +178,8 @@
29+
self._known_related_objects = {} # {rel_field, {pk: rel_obj}}
30+
self._iterable_class = ModelIterable
31+
self._fields = None
32+
+ self._origin = []
33+
+ self._djangodir = __file__[:(__file__.index('django')+len('django')+1)]
34+
35+
def as_manager(cls):
36+
# Address the circular dependency between `Queryset` and `Manager`.
37+
@@ -316,6 +322,37 @@
38+
combined.query.combine(other.query, sql.OR)
39+
return combined
40+
41+
+ def _add_origin(self, depth=1):
42+
+ if settings.DEBUG:
43+
+ # get list of frame records. Each is:
44+
+ # [ frame, filename, lineno, function, code_context, index ]
45+
+ stack = inspect.stack()
46+
+ # caller stack record
47+
+ method = stack[depth][3]
48+
+ # look for the first stack entry which is not from django
49+
+ i = 0
50+
+ while i<len(stack) and stack[i][1].startswith(self._djangodir) and not stack[i][3] == 'render':
51+
+ i += 1
52+
+ if i<len(stack):
53+
+ stack = stack[i:]
54+
+ frame = stack[0][0]
55+
+ function = stack[0][3]
56+
+ if function == 'render' and 'context' in frame.f_locals:
57+
+ that = frame.f_locals['self']
58+
+ if hasattr(that, 'filename'):
59+
+ import debug
60+
+ debug.show('that.filename')
61+
+ self._origin += [stack[0]+(method,), ]
62+
+ else:
63+
+ self._origin += [stack[2]+(method,), ]
64+
+
65+
+ def _log_origin(self):
66+
+ if settings.DEBUG:
67+
+ self._add_origin(depth=3)
68+
+ for place in self._origin:
69+
+ frame, filename, lineno, function, code_context, index, method = place[:7]
70+
+ logger.debug("QuerySet: %s(%s) in %s: %s()\n %s", filename, lineno, function, method, code_context[index].rstrip())
71+
+
72+
####################################
73+
# METHODS THAT DO DATABASE QUERIES #
74+
####################################
75+
@@ -793,6 +830,7 @@
76+
Returns a new QuerySet instance with the args ANDed to the existing
77+
set.
78+
"""
79+
+ self._add_origin()
80+
return self._filter_or_exclude(False, *args, **kwargs)
81+
82+
def exclude(self, *args, **kwargs):
83+
@@ -800,6 +838,7 @@
84+
Returns a new QuerySet instance with NOT (args) ANDed to the existing
85+
set.
86+
"""
87+
+ self._add_origin()
88+
return self._filter_or_exclude(True, *args, **kwargs)
89+
90+
def _filter_or_exclude(self, negate, *args, **kwargs):
91+
@@ -824,6 +863,7 @@
92+
This exists to support framework features such as 'limit_choices_to',
93+
and usually it will be more natural to use other methods.
94+
"""
95+
+ self._add_origin()
96+
if isinstance(filter_obj, Q) or hasattr(filter_obj, 'add_to_query'):
97+
clone = self._clone()
98+
clone.query.add_q(filter_obj)
99+
@@ -836,6 +876,7 @@
100+
Returns a new QuerySet instance that will select objects with a
101+
FOR UPDATE lock.
102+
"""
103+
+ self._add_origin()
104+
obj = self._clone()
105+
obj._for_write = True
106+
obj.query.select_for_update = True
107+
@@ -855,6 +896,7 @@
108+
if self._fields is not None:
109+
raise TypeError("Cannot call select_related() after .values() or .values_list()")
110+
111+
+ self._add_origin()
112+
obj = self._clone()
113+
if fields == (None,):
114+
obj.query.select_related = False
115+
@@ -874,6 +916,7 @@
116+
prefetch is appended to. If prefetch_related(None) is called, the list
117+
is cleared.
118+
"""
119+
+ self._add_origin()
120+
clone = self._clone()
121+
if lookups == (None,):
122+
clone._prefetch_related_lookups = []
123+
@@ -886,6 +929,7 @@
124+
Return a query set in which the returned objects have been annotated
125+
with extra data or aggregations.
126+
"""
127+
+ self._add_origin()
128+
annotations = OrderedDict() # To preserve ordering of args
129+
for arg in args:
130+
# The default_alias property may raise a TypeError, so we use
131+
@@ -929,6 +973,7 @@
132+
"""
133+
assert self.query.can_filter(), \
134+
"Cannot reorder a query once a slice has been taken."
135+
+ self._add_origin()
136+
obj = self._clone()
137+
obj.query.clear_ordering(force_empty=False)
138+
obj.query.add_ordering(*field_names)
139+
@@ -940,6 +985,7 @@
140+
"""
141+
assert self.query.can_filter(), \
142+
"Cannot create distinct fields once a slice has been taken."
143+
+ self._add_origin()
144+
obj = self._clone()
145+
obj.query.add_distinct_fields(*field_names)
146+
return obj
147+
@@ -951,6 +997,7 @@
148+
"""
149+
assert self.query.can_filter(), \
150+
"Cannot change a query once a slice has been taken"
151+
+ self._add_origin()
152+
clone = self._clone()
153+
clone.query.add_extra(select, select_params, where, params, tables, order_by)
154+
return clone
155+
@@ -959,6 +1006,7 @@
156+
"""
157+
Reverses the ordering of the QuerySet.
158+
"""
159+
+ self._add_origin()
160+
clone = self._clone()
161+
clone.query.standard_ordering = not clone.query.standard_ordering
162+
return clone
163+
@@ -973,6 +1021,7 @@
164+
"""
165+
if self._fields is not None:
166+
raise TypeError("Cannot call defer() after .values() or .values_list()")
167+
+ self._add_origin()
168+
clone = self._clone()
169+
if fields == (None,):
170+
clone.query.clear_deferred_loading()
171+
@@ -992,6 +1041,7 @@
172+
# Can only pass None to defer(), not only(), as the rest option.
173+
# That won't stop people trying to do this, so let's be explicit.
174+
raise TypeError("Cannot pass None as an argument to only().")
175+
+ self._add_origin()
176+
clone = self._clone()
177+
clone.query.add_immediate_loading(fields)
178+
return clone
179+
@@ -1078,13 +1128,17 @@
180+
clone._known_related_objects = self._known_related_objects
181+
clone._iterable_class = self._iterable_class
182+
clone._fields = self._fields
183+
+ clone._origin = self._origin
184+
185+
clone.__dict__.update(kwargs)
186+
return clone
187+
188+
def _fetch_all(self):
189+
if self._result_cache is None:
190+
+ self._log_origin()
191+
self._result_cache = list(self.iterator())
192+
+ if settings.DEBUG:
193+
+ connections[self.db].queries_log[-1]['origin'] = self._origin
194+
if self._prefetch_related_lookups and not self._prefetch_done:
195+
self._prefetch_related_objects()
196+

0 commit comments

Comments
 (0)