Skip to content

Commit 4acf095

Browse files
authored
Add graceful degradation for large SQL queries by disabling grouping (#2291)
1 parent d9ab23b commit 4acf095

3 files changed

Lines changed: 45 additions & 1 deletion

File tree

debug_toolbar/panels/sql/utils.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.dispatch import receiver
77
from django.test.signals import setting_changed
88
from sqlparse import tokens as T
9+
from sqlparse.exceptions import SQLParseError
910

1011
from debug_toolbar import settings as dt_settings
1112

@@ -97,6 +98,7 @@ def reformat_sql(sql, *, with_toggle=False):
9798
if not with_toggle:
9899
return formatted
99100
simplified = parse_sql(sql, simplify=True)
101+
100102
uncollapsed = f'<span class="djDebugUncollapsed">{simplified}</span>'
101103
collapsed = f'<span class="djDebugCollapsed djdt-hidden">{formatted}</span>'
102104
return collapsed + uncollapsed
@@ -105,7 +107,15 @@ def reformat_sql(sql, *, with_toggle=False):
105107
@lru_cache(maxsize=128)
106108
def parse_sql(sql, *, simplify=False):
107109
stack = get_filter_stack(simplify=simplify)
108-
return "".join(stack.run(sql))
110+
try:
111+
return "".join(stack.run(sql))
112+
except SQLParseError:
113+
# The query either exceeds the number of tokens or depth of tokens.
114+
# Recreate the FilterStack and explicitly disable the grouping to avoid
115+
# those errors.
116+
stack = get_filter_stack(simplify=simplify)
117+
stack._grouping = False
118+
return "".join(stack.run(sql))
109119

110120

111121
@cache

docs/changes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ Change log
33

44
Pending
55
-------
6+
67
* Prevent check from failing when ``ROOT_URLCONF`` is not defined.
78
* Prevent debounce race conditions in the history panel for rapid
89
fetch requests.
@@ -16,6 +17,10 @@ Pending
1617
those overrides to ``#djDebug``, and custom panels that rely on external
1718
styles or DOM lookups reaching into the toolbar will need updates to
1819
work with the shadow DOM.
20+
* Added graceful degradation for SQL queries that exceed sqlparse's token
21+
limits. When ``SQLParseError`` is raised, the SQL panel now automatically
22+
disables grouping and retries formatting, preventing crashes with large
23+
queries.
1924

2025
6.3.0 (2026-04-01)
2126
------------------

tests/panels/test_sql.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
from django.db.utils import DatabaseError
1515
from django.shortcuts import render
1616
from django.test.utils import override_settings
17+
from sqlparse.exceptions import SQLParseError
1718

1819
import debug_toolbar.panels.sql.tracking as sql_tracking
1920
from debug_toolbar import settings as dt_settings
2021
from debug_toolbar.models import HistoryEntry
2122
from debug_toolbar.panels.sql import SQLPanel, tracking
23+
from debug_toolbar.panels.sql.utils import parse_sql
2224

2325
try:
2426
import psycopg
@@ -869,6 +871,33 @@ def test_explain_with_union(self):
869871
query = self.panel._queries[0]
870872
self.assertTrue(query["is_select"])
871873

874+
@override_settings(DEBUG_TOOLBAR_CONFIG={"PRETTIFY_SQL": True})
875+
def test_sql_parse_error_graceful_degradation(self):
876+
"""
877+
Test that SQLParseError is handled gracefully by disabling grouping.
878+
"""
879+
parse_sql.cache_clear()
880+
881+
def run_side_effect(sql):
882+
if mock_stack.run.call_count == 1:
883+
raise SQLParseError("Token limit exceeded")
884+
return [sql]
885+
886+
with patch("debug_toolbar.panels.sql.utils.get_filter_stack") as mock_get_stack:
887+
mock_stack = mock_get_stack.return_value
888+
mock_stack.run.side_effect = run_side_effect
889+
mock_stack._grouping = True
890+
891+
result = parse_sql("SELECT * FROM test")
892+
893+
# Should have been called twice (once for error, once for retry)
894+
self.assertEqual(mock_stack.run.call_count, 2)
895+
# On retry, _grouping should be set to False
896+
self.assertFalse(mock_stack._grouping)
897+
self.assertIn("SELECT", result)
898+
899+
parse_sql.cache_clear()
900+
872901

873902
class SQLPanelMultiDBTestCase(BaseMultiDBTestCase):
874903
panel_id = SQLPanel.panel_id

0 commit comments

Comments
 (0)