Repository navigation
Report unparsable SQL and pathological nesting instead of raising - #2
Merged
Merged
Conversation
explain() promises never to raise on malformed input, but two kinds of input escaped it and became bare 500s from every endpoint: - SQL whose string or comment never ends. sqlglot raises TokenError for that, which is a sibling of the ParseError the adapter caught, not a subclass. Half-typed SQL like `SELECT 'abc` hits it. - Deeply nested code. Lowering and analysis recurse over the tree, so a 1,000-term `a + b + ...` chain or a few thousand nested brackets hit the recursion limit, and CPython's own parser reports the same thing as MemoryError. The adapter now catches SqlglotError, and explain() catches RecursionError around the static pass and returns a degraded result with an error diagnostic that says why, the same way a parse failure is reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UigC7feaY8NkAy9oay3GDW
This was referenced Nov 2, 2026
ethanzhoucool
marked this pull request as ready for review
November 3, 2026 17:08
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
explain()'s docstring says it "Never raises on malformed input", and the README says broken code still explains. Two kinds of input broke that promise. Both came back from/v1/explain,/v1/explain/all-levels,/v1/analyze(and the CLI) as an unhandled exception, which the API serves as a bare500 Internal Server Error.SQL whose string or comment never closes, e.g.
SELECT 'abcorSELECT a FROM t /* note. This is common for half-typed queries. sqlglot fails in the tokenizer and raisesTokenError. That is a sibling of theParseErrorthe adapter catches, not a subclass, so it escaped.Deeply nested code. The lowering and analysis passes recurse over the tree, so these exhausted Python's recursion limit:
x = a + a + ... + a(about 4 KB of valid Python),CPython's own parser reports the same condition as
MemoryError("Parser stack overflowed", bare on 3.11).Changes
adapters/sql.py: catchsqlglot.errors.SqlglotError, the base class ofParseErrorandTokenError. The result is the existing "Could not parse SQL: ..." error diagnostic.core.py: parse, analyse and narrate now run in a small_static_pass()helper. If that raisesRecursionError,explain()runs it again on an empty degraded parse. That parse carries one error diagnostic: "This code is nested too deeply to explain ... Split it up and try again." This is how a parse failure is already reported, so the UI shows it as a toast and the rest of the response keeps its usual shape.adapters/python.py: aMemoryErrorfromast.parseis turned into that sameRecursionError. Input is already capped at 200 KB, so it can only mean the parser's stack overflowed. The prefix-salvage loop also skips prefixes that overflow, instead of crashing.Code that is deep but reasonable is still explained in full. A 150-term chain gives a full, non-degraded result, and a test covers it.
Testing
pytestpasses on Python 3.13, 3.12 and 3.11: 163 passed, 3 skipped.ruff check explain_my_code testsis clean.mypystill reports the 26 errors it reported before; none are new (CI runs it withcontinue-on-error).main's source):test_sql_that_fails_to_tokenize_is_reported_not_raised(3 cases)test_pathologically_deep_nesting_is_reported_not_raised(Python chain, Python parser overflow, JS, Java, C++, SQL)test_hostile_input_gets_a_degraded_answer_not_a_500:/v1/explain,/v1/explain/all-levelsand/v1/analyze, each with both inputs, using aTestClient(raise_server_exceptions=False)so a 500 shows up as a status code.test_moderately_deep_code_is_still_explainedNotes
Not covered here:
includeIr: trueon a deep tree, around 100+ levels such as a 200-term chain, still returns 500. pydantic-core's serializer refuses to nestdict[str, Any]that deep ("Circular reference detected (depth exceeded)"). It's opt-in and the UI never sets it, so I left it out rather than change the response model in this PR.core.explainis also touched by the language-detection PR (#1), but in a different region (SIGNATURES), so the two should merge cleanly.Merge note: #6 rewrites
_parse_longest_prefix, so whichever of #2 and #6 lands second gets a small conflict there. To resolve it, keep #6's loop and addexcept (MemoryError, RecursionError): end -= 1after itsSyntaxErrorbranch. I merged all of the open PRs together locally with that resolution, and the combined suite passes (266 passed, 4 skipped on 3.11, 3.12 and 3.13).https://claude.ai/code/session_01UigC7feaY8NkAy9oay3GDW
Generated by Claude Code