Parser: fix exponential parse time on chains of IN ( - #10
Conversation
d28ef1a to
f1fb8ca
Compare
IN ( (speculative subquery parse)IN (
|
Both findings were correct — thanks. The first was a real regression: committing on the opening keyword rejected Fixed in f1fb8ca by narrowing the commit condition to |
moshap-firebolt
left a comment
There was a problem hiding this comment.
The SELECT NOT IN(\n chain is linear after this; I checked depth 20/40 on GenericDialect.
Two issues below: a behaviour change on IN (select()) / IN (WITH()), and a residual exponential path on an extra (.
(The Bugbot note about missing MERGE in peek_query_body_start is stale — that helper is gone.)
| _ => false, | ||
| }; | ||
| // A trailing `)` or `,` means the keyword *is* the element, i.e. an identifier. | ||
| opens_query && !matches!(second.token, Token::RParen | Token::Comma) |
There was a problem hiding this comment.
Behaviour change. SELECT/WITH followed by ( commits to parse_query, so these go from InList on firebolt/v0.62.0-patches to a parse error:
SELECT 1 WHERE x IN (select())— wasInListof functionselect(), nowExpected: an expression, found: )SELECT 1 WHERE x IN (WITH())— wasInListof functionwith(), nowExpected: identifier, found: (
parse_query("SELECT()") fails, and committing drops the list fallback. The claim that no input changes verdict is false.
Don't commit on Token::LParen either. IN (SELECT(1)) then stays speculative and still becomes InSubquery (query-first, same as today). The fuzzer chain SELECT NOT IN(\nSELECT still commits — second token is NOT.
parse_in_subquery_vs_list_dispatch would not have caught this: it only checks that parsing succeeds, and it never includes IN (select()). Assert InList vs InSubquery.
| // Committing when the input is unambiguously a subquery leaves only one arm to | ||
| // descend, which is enough to make the chain linear. Everything else keeps the | ||
| // speculative path, so no input changes verdict. | ||
| let in_op = if self.peek_token_ref().token == Token::LParen { |
There was a problem hiding this comment.
Residual exponential path. This arm is unchanged, so SELECT NOT IN((\n repeated is still 2^depth. A debug run of that shape at depth 8+ had not finished after 90s; the single-paren fuzzer input is fixed.
A dialect-fuzzer mutant that inserts one ( gets the original timeout back. Worth a follow-up (or extending the regression test to this shape with a timeout) if this is meant to close FB-3290 rather than just the minimized seed.
f1fb8ca to
d6ad549
Compare
|
Thanks — first finding confirmed and fixed, second I cannot reproduce. 1. 2. Residual exponential on
Linear, and the same shape is equally fast on the unpatched parser (0.005s at 160), so it is not a regression from this change either. Your structural point is right that the If you have the exact input, I will happily take it — a shape I have not found beats my sampling. Otherwise I would rather not add a regression test asserting a bound I cannot demonstrate is currently violated. The stale- |
d6ad549 to
4fe47aa
Compare
|
Fair, and worth fixing precisely because this PR would be the first thing an agent reads next to that file. Trimmed in 4fe47aa: comment lines added to |
4fe47aa to
cc88ea7
Compare
|
Confirmed and fixed in cc88ea7 — and this one was worse than a parse error, so thank you for it.
Fixed by inverting the structure: the speculative
|
|
Since three regressions here got past hand-written case lists, I stopped writing case lists and ran a differential test instead: same harness built against 533 inputs — every Result:
That last one is worth a look rather than hiding in a footnote, since it is a real if minor change: Committing means the error now comes from the query parser at the point the body is missing, instead of from the list parser complaining about the CTE name. I think the new message is the better one — it names what is actually missing — but it is a change, so flagging it explicitly. Happy to keep the old text if error-message stability matters here. Harness and corpus are throwaway, but I can attach them if useful. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cc88ea7. Configure here.
cc88ea7 to
88c8695
Compare
|
Confirmed — and this is the fourth regression from the same lookahead, so I have abandoned that approach entirely rather than patch it a fifth time. Your finding reproduces: 12 forms x 3 contexts = 36 inputs went OK -> ERR, including 88c8695 drops the lookahead and memoizes the failed positions instead — the same mechanism Evidence, differential against
Both gates verified by breaking them: disabling the cache makes One correction to my earlier numbers: the 21s figure came from the cargo-fuzz binary (coverage + sanitizers) at depth 20. A plain release build is 1.3s at depth 20 and 21s at depth 24. The PR text now uses the plain-release figures. |
`parse_in` picks between a subquery and an expression list by speculatively
parsing a query and rolling back. The list fallback then recurses back into
`parse_in` over the same tail --- `parse_expr` accepts a reserved word as an
identifier --- so each nesting level re-attempts the identical speculative
parse. `"SELECT NOT IN(".repeat(20)` took 1.3s, and 26 levels 83s.
Memoize the failed positions, as `parse_table_factor` already does for the
`FROM ((((` shape it has the same structure as. A cached failure yields the
same fallback as re-running the parse, so behaviour is unchanged.
88c8695 to
9d03b52
Compare
|
Re-reviewed this against 1. Unnecessary refactor. I had extracted 2. Test table was near-duplicate noise (rule 4). It had 19 cases, 9 of which pinned keyword-vs-identifier behaviour of a lookahead this version no longer contains — the diff adds zero 3. Comment budget (rule 1). 8 lines, now 5 — the block comment was restating the commit message. Commit body is 8 lines, PR description is the bug, an example and the fix. Re-verified after the trim: still byte-identical to Also added the shape to tensile as a feature so it is probed continuously rather than only living in a fuzz seed: themosha/tensilelib#6. Worth reading the caveat there — it is deliberately invalid SQL, which that repo normally forbids, because balanced variants parse on the first attempt and cannot reproduce this class of bug at all. |

parse_inpicks between a subquery and an expression list by speculatively parsing a query and rolling back. The list fallback then recurses back intoparse_inover the same tail —parse_expraccepts a reserved word as an identifier — so each nesting level re-attempts the identical speculative parse.Not working before:
Memoize the failed positions, as
parse_table_factoralready does for theFROM ((((shape it has the same structure as. A cached failure yields the same fallback as re-running the parse, so behaviour is unchanged.Found by a fuzzer. Reproduces on upstream
maintoo.