Skip to content

Commit 969f0a7

Browse files
authored
Support select and explain for UNION queries (#1972)
* Support select and explain for UNION queries Some UNION queries can start with "(", causing it to not be classified as a SELECT query, and consequently the select and explain buttons are missing on the SQL panel. * Remove unit test limitation to postgresql * Document integration test for postgres union select explain test.
1 parent ee258fc commit 969f0a7

File tree

6 files changed

+44
-6
lines changed

6 files changed

+44
-6
lines changed

debug_toolbar/panels/sql/forms.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.db import connections
66
from django.utils.functional import cached_property
77

8-
from debug_toolbar.panels.sql.utils import reformat_sql
8+
from debug_toolbar.panels.sql.utils import is_select_query, reformat_sql
99

1010

1111
class SQLSelectForm(forms.Form):
@@ -27,7 +27,7 @@ class SQLSelectForm(forms.Form):
2727
def clean_raw_sql(self):
2828
value = self.cleaned_data["raw_sql"]
2929

30-
if not value.lower().strip().startswith("select"):
30+
if not is_select_query(value):
3131
raise ValidationError("Only 'select' queries are allowed.")
3232

3333
return value

debug_toolbar/panels/sql/panel.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
from debug_toolbar.panels.sql import views
1313
from debug_toolbar.panels.sql.forms import SQLSelectForm
1414
from debug_toolbar.panels.sql.tracking import wrap_cursor
15-
from debug_toolbar.panels.sql.utils import contrasting_color_generator, reformat_sql
15+
from debug_toolbar.panels.sql.utils import (
16+
contrasting_color_generator,
17+
is_select_query,
18+
reformat_sql,
19+
)
1620
from debug_toolbar.utils import render_stacktrace
1721

1822

@@ -266,9 +270,7 @@ def generate_stats(self, request, response):
266270
query["sql"] = reformat_sql(query["sql"], with_toggle=True)
267271

268272
query["is_slow"] = query["duration"] > sql_warning_threshold
269-
query["is_select"] = (
270-
query["raw_sql"].lower().lstrip().startswith("select")
271-
)
273+
query["is_select"] = is_select_query(query["raw_sql"])
272274

273275
query["rgb_color"] = self._databases[alias]["rgb_color"]
274276
try:

debug_toolbar/panels/sql/utils.py

+5
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ def process(stmt):
8686
return "".join(escaped_value(token) for token in stmt.flatten())
8787

8888

89+
def is_select_query(sql):
90+
# UNION queries can start with "(".
91+
return sql.lower().lstrip(" (").startswith("select")
92+
93+
8994
def reformat_sql(sql, *, with_toggle=False):
9095
formatted = parse_sql(sql)
9196
if not with_toggle:

docs/changes.rst

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Change log
33

44
Pending
55
-------
6+
* Support select and explain buttons for ``UNION`` queries on PostgreSQL.
67

78
* Fixed internal toolbar requests being instrumented if the Django setting
89
``FORCE_SCRIPT_NAME`` was set.

tests/panels/test_sql.py

+7
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,13 @@ def test_similar_and_duplicate_grouping(self):
729729
self.assertNotEqual(queries[0]["similar_color"], queries[3]["similar_color"])
730730
self.assertNotEqual(queries[0]["duplicate_color"], queries[3]["similar_color"])
731731

732+
def test_explain_with_union(self):
733+
list(User.objects.filter(id__lt=20).union(User.objects.filter(id__gt=10)))
734+
response = self.panel.process_request(self.request)
735+
self.panel.generate_stats(self.request, response)
736+
query = self.panel._queries[0]
737+
self.assertTrue(query["is_select"])
738+
732739

733740
class SQLPanelMultiDBTestCase(BaseMultiDBTestCase):
734741
panel_id = "SQLPanel"

tests/test_integration.py

+23
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,29 @@ def test_sql_explain_checks_show_toolbar(self):
445445
)
446446
self.assertEqual(response.status_code, 404)
447447

448+
@unittest.skipUnless(
449+
connection.vendor == "postgresql", "Test valid only on PostgreSQL"
450+
)
451+
def test_sql_explain_postgres_union_query(self):
452+
"""
453+
Confirm select queries that start with a parenthesis can be explained.
454+
"""
455+
url = "/__debug__/sql_explain/"
456+
data = {
457+
"signed": SignedDataForm.sign(
458+
{
459+
"sql": "(SELECT * FROM auth_user) UNION (SELECT * from auth_user)",
460+
"raw_sql": "(SELECT * FROM auth_user) UNION (SELECT * from auth_user)",
461+
"params": "{}",
462+
"alias": "default",
463+
"duration": "0",
464+
}
465+
)
466+
}
467+
468+
response = self.client.post(url, data)
469+
self.assertEqual(response.status_code, 200)
470+
448471
@unittest.skipUnless(
449472
connection.vendor == "postgresql", "Test valid only on PostgreSQL"
450473
)

0 commit comments

Comments
 (0)