From f4af19d6c98a39ba2ad69caf95defb48e4596b10 Mon Sep 17 00:00:00 2001 From: Todor Velichkov Date: Wed, 19 Sep 2018 14:45:19 +0300 Subject: [PATCH] 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. --- debug_toolbar/panels/sql/tracking.py | 2 +- tests/panels/test_sql.py | 49 ++++++++++++++++++++++++++++ tests/settings.py | 11 +++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py index 73058f772..63dbc6906 100644 --- a/debug_toolbar/panels/sql/tracking.py +++ b/debug_toolbar/panels/sql/tracking.py @@ -124,7 +124,7 @@ def _record(self, method, sql, params): stacktrace = [] _params = '' try: - _params = json.dumps([self._decode(p) for p in params]) + _params = json.dumps(self._decode(params)) except TypeError: pass # object not JSON serializable diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py index e4fa16c27..b46ece32d 100644 --- a/tests/panels/test_sql.py +++ b/tests/panels/test_sql.py @@ -122,6 +122,55 @@ def test_param_conversion(self): '["2017-12-22 16:07:01"]' )) + @unittest.skipUnless(connection.vendor != 'sqlite', + 'Test invalid for SQLite') + def test_raw_query_param_conversion(self): + self.assertEqual(len(self.panel._queries), 0) + + list(User.objects.raw( + " ".join([ + "SELECT *", + "FROM auth_user", + "WHERE first_name = %s", + "AND is_staff = %s", + "AND is_superuser = %s", + "AND date_joined = %s", + ]), + params=['Foo', True, False, datetime.datetime(2017, 12, 22, 16, 7, 1)], + )) + + list(User.objects.raw( + " ".join([ + "SELECT *", + "FROM auth_user", + "WHERE first_name = %(first_name)s", + "AND is_staff = %(is_staff)s", + "AND is_superuser = %(is_superuser)s", + "AND date_joined = %(date_joined)s" + ]), + params={ + 'first_name': 'Foo', + 'is_staff': True, + 'is_superuser': False, + 'date_joined': datetime.datetime(2017, 12, 22, 16, 7, 1)}, + )) + + self.panel.process_response(self.request, self.response) + self.panel.generate_stats(self.request, self.response) + + # ensure query was logged + self.assertEqual(len(self.panel._queries), 2) + + self.assertEqual(tuple([q[1]['params'] for q in self.panel._queries]), ( + '["Foo", true, false, "2017-12-22 16:07:01"]', + " ".join([ + '{"first_name": "Foo",', + '"is_staff": true,', + '"is_superuser": false,', + '"date_joined": "2017-12-22 16:07:01"}' + ]) + )) + def test_insert_content(self): """ Test that the panel only inserts content after generate_stats and diff --git a/tests/settings.py b/tests/settings.py index bbef350c7..0a3f1d773 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -91,6 +91,17 @@ 'NAME': 'debug-toolbar', } } +elif os.environ.get('DJANGO_DATABASE_ENGINE') == 'mysql': + # % mysql + # mysql> CREATE USER 'debug_toolbar'@'localhost' IDENTIFIED BY ''; + # mysql> GRANT ALL PRIVILEGES ON debug_toolbar.* TO 'test_debug_toolbar'@'localhost'; + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'debug_toolbar', + 'USER': 'debug_toolbar', + } + } else: DATABASES = { 'default': {