Skip to content

Add graceful degradation for large SQL queries#2291

Open
kkm-horikawa wants to merge 8 commits intodjango-commons:mainfrom
kkm-horikawa:fix/graceful-degradation-large-sql
Open

Add graceful degradation for large SQL queries#2291
kkm-horikawa wants to merge 8 commits intodjango-commons:mainfrom
kkm-horikawa:fix/graceful-degradation-large-sql

Conversation

@kkm-horikawa
Copy link
Copy Markdown

@kkm-horikawa kkm-horikawa commented Jan 9, 2026

Description

Add graceful degradation for SQL queries that exceed sqlparse's token limits, preventing crashes in the SQL panel.

Problem:
When SQL queries contain thousands of parameters (e.g., UUIDs in IN clauses), the SQL panel crashes with SQLParseError (sqlparse >= 0.5.5).

Solution:
Catch SQLParseError and retry formatting with grouping disabled. This approach:

  • Handles the crash automatically without requiring configuration
  • Eliminates the need for a new setting like SQL_PRETTIFY_MAX_LENGTH

Out of scope: The freezing behavior with older sqlparse versions (< 0.5.5) is a pre-existing issue and not addressed in this PR.

Before (debug-toolbar 5.2 + sqlparse 0.5.3) - page freezes (out of scope):

before-page-freeze

Before (debug-toolbar 6.1 + sqlparse 0.5.5) - SQLParseError:

before-sqlparse-error

After (this PR):

after-fix

Reproduction project: https://github.com/kkm-horikawa/debug-toolbar-perf-issue

Fixes #2293

Checklist:

  • I have added the relevant tests for this change.
  • I have added an item to the Pending section of docs/changes.rst.

@tim-schilling

This comment was marked as outdated.

@tim-schilling
Copy link
Copy Markdown
Member

tim-schilling commented Jan 13, 2026

Let's split the error handling of SQLParseError out of this PR, into its own.

@kkm-horikawa kkm-horikawa force-pushed the fix/graceful-degradation-large-sql branch from 140dd68 to 9ac9ddb Compare January 14, 2026 05:24
@kkm-horikawa
Copy link
Copy Markdown
Author

Thanks for the feedback! I've removed the SQLParseError handling from this PR.

This PR now focuses solely on the SQL_PRETTIFY_MAX_LENGTH setting (proactive length check).

I'll create a separate PR for SQLParseError handling after this one is merged. Does this approach work for you?

kkm-horikawa and others added 5 commits January 14, 2026 14:32
- Add SQL_PRETTIFY_MAX_LENGTH setting (default 50000) to skip formatting for long queries
- Catch SQLParseError from sqlparse >= 0.5.5 when MAX_GROUPING_TOKENS is exceeded
- Display informative message with SQL preview when formatting is skipped
- Add tests for the new behavior

This prevents the SQL panel from freezing or crashing when queries contain
large IN clauses with thousands of parameters (e.g., UUIDs).

Fixes django-commons#1402
The preview was being escaped twice - once when assigned to the preview
variable and again when inserted into the HTML string.
Split SQLParseError exception handling into a separate PR as requested.
This PR now focuses solely on the SQL_PRETTIFY_MAX_LENGTH setting.
@kkm-horikawa kkm-horikawa force-pushed the fix/graceful-degradation-large-sql branch from 9ac9ddb to 0809510 Compare January 14, 2026 05:33
Copy link
Copy Markdown
Member

@tim-schilling tim-schilling left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for suggesting this. However, I think we have too many utility functions for rendering the SQL at this point. A change will need to review more of the whole process and identify why it's appropriate compared to other options. For example, I think there may be something achievable by modifying parse_sql and/or using SQLParse's FilterStack API. Let me know what you think please.

Comment on lines +119 to +120
max_length = dt_settings.get_config()["SQL_PRETTIFY_MAX_LENGTH"]
if max_length and len(sql) > max_length:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this should be integrated into the sqlparse logic in get_filter_stack(). Have you looked into that at all to see if we can do something with FilterStack?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I've integrated the length check directly into parse_sql instead.

"""
Test that SQL formatting is skipped for queries exceeding the max length threshold.
"""
from debug_toolbar.panels.sql.utils import reformat_sql
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid importing things within functions.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Moved the import to the top of the file.

if max_length and len(sql) > max_length:
skipped = _format_skipped_sql(
sql,
f"query length {len(sql):,} exceeds threshold {max_length:,}",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to convert this all to a template and use render_to_string() so that it's easier to use the intcomma template filter. This would better handle internationalization of using commas versus periods.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the message display for now to keep the change minimal. If a notice is desired, I can add it using a template as you suggested - see my comment above for details.

"django.utils.functional",
),
"PRETTIFY_SQL": True,
"SQL_PRETTIFY_MAX_LENGTH": 50000, # Skip formatting for SQL longer than this
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"SQL_PRETTIFY_MAX_LENGTH": 50000, # Skip formatting for SQL longer than this
"PRETTIFY_SQL_MAX_LENGTH": 10000, # Skip formatting for SQL longer than this

Let's rename this to better match the other setting. Let's also use the default SQLParse max value from https://sqlparse.readthedocs.io/en/latest/api.html#security-and-performance-considerations.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@kkm-horikawa kkm-horikawa force-pushed the fix/graceful-degradation-large-sql branch from ddb1b7e to 7da3aa9 Compare January 15, 2026 02:59
@kkm-horikawa
Copy link
Copy Markdown
Author

Thank you for the feedback. You're right that adding a separate utility function is unnecessary.

I've refactored to integrate the length check directly into parse_sql. I've also renamed the setting to PRETTIFY_SQL_MAX_LENGTH with a default of 10000 to match sqlparse's MAX_GROUPING_TOKENS.

The current implementation doesn't show a message when formatting is skipped - it just returns the escaped SQL. I made this choice to avoid adding new files or functions for this specific case, keeping the change minimal. However, if you'd prefer to have a notice displayed to users, I can add that using a template with render_to_string() and intcomma as you suggested. What do you think?

Comment on lines +106 to +107
max_length = dt_settings.get_config()["PRETTIFY_SQL_MAX_LENGTH"]
if max_length and len(sql) > max_length:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is right. This would limit sql queries to a string length of 10,000 characters. I was under the impression we were trying to improve performance when the query has more than 10,000 tokens. I believe that's not the same thing as a query with a string length of 10,000 characters.

And if we're removing the formatting in this case, it would make sense to avoid calling stack.enable_grouping() in that case.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe catching the error explicitly does what you're after:

@lru_cache(maxsize=128)
def parse_sql(sql, *, simplify=False):
    stack = get_filter_stack(simplify=simplify)
    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))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review!

Your suggested approach of catching SQLParseError is much better. It handles the issue only when it actually occurs,
and eliminates the need for a new configuration setting.

I'll update the implementation in this direction. Thank you for your patience and continued guidance through multiple
reviews!

Copy link
Copy Markdown
Author

@kkm-horikawa kkm-horikawa Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thank you for the review!

I've updated the implementation to catch SQLParseError as you suggested. This approach also eliminates the need for
the PRETTIFY_SQL_MAX_LENGTH setting.

This PR focuses on handling the SQLParseError crash introduced in sqlparse >= 0.5.5. The freezing behavior with older
versions (< 0.5.5) is pre-existing and considered out of scope for this PR.

I've also updated the PR description to reflect these changes.

Thank you for your patience and continued guidance through multiple reviews!

@kkm-horikawa kkm-horikawa force-pushed the fix/graceful-degradation-large-sql branch from 248e656 to ae42a1e Compare January 20, 2026 05:39
@kkm-horikawa kkm-horikawa force-pushed the fix/graceful-degradation-large-sql branch from 41e9083 to e16db82 Compare January 23, 2026 05:39
Comment on lines +877 to +881
from unittest.mock import patch

from sqlparse.exceptions import SQLParseError

from debug_toolbar.panels.sql.utils import parse_sql
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kkm-horikawa please review the LLM output more thoroughly in the future. For some reason, LLMs love to generate inline import statements. We try to avoid that practice in this project.

@github-actions
Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  debug_toolbar/panels/sql
  utils.py
Project Total  

This report was generated by python-coverage-comment-action

call_count = 0
original_run = None

def mock_run(sql):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give the side_effect attribute a look and then the call_count attribute. We can reuse some standard library tools to avoid rebuilding the wheel 😁

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you get stuck, let me know. I purposefully didn't write out the code, but I'm happy to include that if you're just wanting this PR to wrap up quickly.

Copy link
Copy Markdown
Member

@tim-schilling tim-schilling left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see the test improved a bit more before we merge this. Thank you for continuing to iterate on this!

@tim-schilling
Copy link
Copy Markdown
Member

@kkm-horikawa can you merge your changes in from your other PR when you have time please?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQL panel freezes or crashes when queries contain large IN clauses (e.g., thousands of UUIDs)

2 participants