Skip to content
Merged
12 changes: 11 additions & 1 deletion debug_toolbar/panels/sql/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from django.dispatch import receiver
from django.test.signals import setting_changed
from sqlparse import tokens as T
from sqlparse.exceptions import SQLParseError

from debug_toolbar import settings as dt_settings

Expand Down Expand Up @@ -97,6 +98,7 @@ def reformat_sql(sql, *, with_toggle=False):
if not with_toggle:
return formatted
simplified = parse_sql(sql, simplify=True)

uncollapsed = f'<span class="djDebugUncollapsed">{simplified}</span>'
collapsed = f'<span class="djDebugCollapsed djdt-hidden">{formatted}</span>'
return collapsed + uncollapsed
Expand All @@ -105,7 +107,15 @@ def reformat_sql(sql, *, with_toggle=False):
@lru_cache(maxsize=128)
def parse_sql(sql, *, simplify=False):
stack = get_filter_stack(simplify=simplify)
return "".join(stack.run(sql))
try:
return "".join(stack.run(sql))
except SQLParseError:
# The query either exceeds the number of tokens or depth of tokens.
# Recreate the FilterStack and explicitly disable the grouping to avoid
# those errors.
stack = get_filter_stack(simplify=simplify)
stack._grouping = False
return "".join(stack.run(sql))


@cache
Expand Down
5 changes: 5 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Change log

Pending
-------

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

6.3.0 (2026-04-01)
------------------
Expand Down
29 changes: 29 additions & 0 deletions tests/panels/test_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
from django.db.utils import DatabaseError
from django.shortcuts import render
from django.test.utils import override_settings
from sqlparse.exceptions import SQLParseError

import debug_toolbar.panels.sql.tracking as sql_tracking
from debug_toolbar import settings as dt_settings
from debug_toolbar.models import HistoryEntry
from debug_toolbar.panels.sql import SQLPanel, tracking
from debug_toolbar.panels.sql.utils import parse_sql

try:
import psycopg
Expand Down Expand Up @@ -869,6 +871,33 @@ def test_explain_with_union(self):
query = self.panel._queries[0]
self.assertTrue(query["is_select"])

@override_settings(DEBUG_TOOLBAR_CONFIG={"PRETTIFY_SQL": True})
def test_sql_parse_error_graceful_degradation(self):
"""
Test that SQLParseError is handled gracefully by disabling grouping.
"""
parse_sql.cache_clear()

def run_side_effect(sql):
if mock_stack.run.call_count == 1:
raise SQLParseError("Token limit exceeded")
return [sql]

with patch("debug_toolbar.panels.sql.utils.get_filter_stack") as mock_get_stack:
mock_stack = mock_get_stack.return_value
mock_stack.run.side_effect = run_side_effect
mock_stack._grouping = True

result = parse_sql("SELECT * FROM test")

# Should have been called twice (once for error, once for retry)
self.assertEqual(mock_stack.run.call_count, 2)
# On retry, _grouping should be set to False
self.assertFalse(mock_stack._grouping)
self.assertIn("SELECT", result)

parse_sql.cache_clear()


class SQLPanelMultiDBTestCase(BaseMultiDBTestCase):
panel_id = SQLPanel.panel_id
Expand Down
Loading