Skip to content

Commit f4af19d

Browse files
Fix displaying RawQuerySet in sql_explain view
Trying to do sql_explain on raw query with dictionary causes a crash. ``` ERROR Internal Server Error: /__debug__/sql_explain/ Traceback (most recent call last): File "../site-packages/django/core/handlers/exception.py", line 41, in inner response = get_response(request) File "../site-packages/django/core/handlers/base.py", line 187, in _get_response response = self.process_exception_by_middleware(e, request) File "../site-packages/django/core/handlers/base.py", line 185, in _get_response response = wrapped_callback(request, *callback_args, **callback_kwargs) File "../site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view return view_func(*args, **kwargs) File "../site-packages/debug_toolbar/decorators.py", line 15, in inner return view(request, *args, **kwargs) File "../site-packages/debug_toolbar/panels/sql/views.py", line 61, in sql_explain cursor.execute("EXPLAIN %s" % (sql,), params) File "../site-packages/debug_toolbar/panels/sql/tracking.py", line 188, in execute return self._record(self.cursor.execute, sql, params) File "../site-packages/debug_toolbar/panels/sql/tracking.py", line 121, in _record return method(sql, params) File "../site-packages/django/db/backends/utils.py", line 79, in execute return super(CursorDebugWrapper, self).execute(sql, params) File "../site-packages/django/db/backends/utils.py", line 64, in execute return self.cursor.execute(sql, params) File "../site-packages/django/db/backends/mysql/base.py", line 101, in execute return self.cursor.execute(query, args) File "../site-packages/MySQLdb/cursors.py", line 159, in execute query = query % db.literal(args) TypeError: format requires a mapping ``` The reason is because `params` is not dictionary, but list. The core of the issue is caused by `panels/sql/traking.py` ``` _params = json.dumps([self._decode(p) for p in params]) ``` This forces only dictionary keys to be send over sql_explain view. My change is going to preserve the initial params structure. ``` _params = json.dumps(self._decode(params)) ``` Now, because dictionary params are not supported by the SQLite backend. My test is going to be skipped for this vendor.
1 parent 07b063c commit f4af19d

File tree

3 files changed

+61
-1
lines changed

3 files changed

+61
-1
lines changed

debug_toolbar/panels/sql/tracking.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def _record(self, method, sql, params):
124124
stacktrace = []
125125
_params = ''
126126
try:
127-
_params = json.dumps([self._decode(p) for p in params])
127+
_params = json.dumps(self._decode(params))
128128
except TypeError:
129129
pass # object not JSON serializable
130130

tests/panels/test_sql.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,55 @@ def test_param_conversion(self):
122122
'["2017-12-22 16:07:01"]'
123123
))
124124

125+
@unittest.skipUnless(connection.vendor != 'sqlite',
126+
'Test invalid for SQLite')
127+
def test_raw_query_param_conversion(self):
128+
self.assertEqual(len(self.panel._queries), 0)
129+
130+
list(User.objects.raw(
131+
" ".join([
132+
"SELECT *",
133+
"FROM auth_user",
134+
"WHERE first_name = %s",
135+
"AND is_staff = %s",
136+
"AND is_superuser = %s",
137+
"AND date_joined = %s",
138+
]),
139+
params=['Foo', True, False, datetime.datetime(2017, 12, 22, 16, 7, 1)],
140+
))
141+
142+
list(User.objects.raw(
143+
" ".join([
144+
"SELECT *",
145+
"FROM auth_user",
146+
"WHERE first_name = %(first_name)s",
147+
"AND is_staff = %(is_staff)s",
148+
"AND is_superuser = %(is_superuser)s",
149+
"AND date_joined = %(date_joined)s"
150+
]),
151+
params={
152+
'first_name': 'Foo',
153+
'is_staff': True,
154+
'is_superuser': False,
155+
'date_joined': datetime.datetime(2017, 12, 22, 16, 7, 1)},
156+
))
157+
158+
self.panel.process_response(self.request, self.response)
159+
self.panel.generate_stats(self.request, self.response)
160+
161+
# ensure query was logged
162+
self.assertEqual(len(self.panel._queries), 2)
163+
164+
self.assertEqual(tuple([q[1]['params'] for q in self.panel._queries]), (
165+
'["Foo", true, false, "2017-12-22 16:07:01"]',
166+
" ".join([
167+
'{"first_name": "Foo",',
168+
'"is_staff": true,',
169+
'"is_superuser": false,',
170+
'"date_joined": "2017-12-22 16:07:01"}'
171+
])
172+
))
173+
125174
def test_insert_content(self):
126175
"""
127176
Test that the panel only inserts content after generate_stats and

tests/settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@
9191
'NAME': 'debug-toolbar',
9292
}
9393
}
94+
elif os.environ.get('DJANGO_DATABASE_ENGINE') == 'mysql':
95+
# % mysql
96+
# mysql> CREATE USER 'debug_toolbar'@'localhost' IDENTIFIED BY '';
97+
# mysql> GRANT ALL PRIVILEGES ON debug_toolbar.* TO 'test_debug_toolbar'@'localhost';
98+
DATABASES = {
99+
'default': {
100+
'ENGINE': 'django.db.backends.mysql',
101+
'NAME': 'debug_toolbar',
102+
'USER': 'debug_toolbar',
103+
}
104+
}
94105
else:
95106
DATABASES = {
96107
'default': {

0 commit comments

Comments
 (0)