Add graceful degradation for large SQL queries#2291
Add graceful degradation for large SQL queries#2291kkm-horikawa wants to merge 8 commits intodjango-commons:mainfrom
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
|
Let's split the error handling of |
140dd68 to
9ac9ddb
Compare
|
Thanks for the feedback! I've removed the This PR now focuses solely on the I'll create a separate PR for |
- 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.
for more information, see https://pre-commit.ci
Split SQLParseError exception handling into a separate PR as requested. This PR now focuses solely on the SQL_PRETTIFY_MAX_LENGTH setting.
9ac9ddb to
0809510
Compare
tim-schilling
left a comment
There was a problem hiding this comment.
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.
debug_toolbar/panels/sql/utils.py
Outdated
| max_length = dt_settings.get_config()["SQL_PRETTIFY_MAX_LENGTH"] | ||
| if max_length and len(sql) > max_length: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Done. I've integrated the length check directly into parse_sql instead.
tests/panels/test_sql.py
Outdated
| """ | ||
| Test that SQL formatting is skipped for queries exceeding the max length threshold. | ||
| """ | ||
| from debug_toolbar.panels.sql.utils import reformat_sql |
There was a problem hiding this comment.
Please avoid importing things within functions.
There was a problem hiding this comment.
Fixed. Moved the import to the top of the file.
debug_toolbar/panels/sql/utils.py
Outdated
| if max_length and len(sql) > max_length: | ||
| skipped = _format_skipped_sql( | ||
| sql, | ||
| f"query length {len(sql):,} exceeds threshold {max_length:,}", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
debug_toolbar/settings.py
Outdated
| "django.utils.functional", | ||
| ), | ||
| "PRETTIFY_SQL": True, | ||
| "SQL_PRETTIFY_MAX_LENGTH": 50000, # Skip formatting for SQL longer than this |
There was a problem hiding this comment.
| "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.
ddb1b7e to
7da3aa9
Compare
|
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 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 |
debug_toolbar/panels/sql/utils.py
Outdated
| max_length = dt_settings.get_config()["PRETTIFY_SQL_MAX_LENGTH"] | ||
| if max_length and len(sql) > max_length: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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))There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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!
248e656 to
ae42a1e
Compare
41e9083 to
e16db82
Compare
for more information, see https://pre-commit.ci
| from unittest.mock import patch | ||
|
|
||
| from sqlparse.exceptions import SQLParseError | ||
|
|
||
| from debug_toolbar.panels.sql.utils import parse_sql |
There was a problem hiding this comment.
@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.
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||
| call_count = 0 | ||
| original_run = None | ||
|
|
||
| def mock_run(sql): |
There was a problem hiding this comment.
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 😁
There was a problem hiding this comment.
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.
tim-schilling
left a comment
There was a problem hiding this comment.
I'd like to see the test improved a bit more before we merge this. Thank you for continuing to iterate on this!
|
@kkm-horikawa can you merge your changes in from your other PR when you have time please? |
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
SQLParseErrorand retry formatting with grouping disabled. This approach:SQL_PRETTIFY_MAX_LENGTHOut 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 (debug-toolbar 6.1 + sqlparse 0.5.5) - SQLParseError:
After (this PR):
Reproduction project: https://github.com/kkm-horikawa/debug-toolbar-perf-issue
Fixes #2293
Checklist:
docs/changes.rst.