Skip to content

Parser: fix exponential parse time on compound keyword chains#2350

Merged
iffyio merged 3 commits into
apache:mainfrom
LucaCappelletti94:pathological3
Jun 6, 2026
Merged

Parser: fix exponential parse time on compound keyword chains#2350
iffyio merged 3 commits into
apache:mainfrom
LucaCappelletti94:pathological3

Conversation

@LucaCappelletti94

Copy link
Copy Markdown
Contributor

Same family as #2344 and #2349. A reserved keyword in field position (e.g. NOT in .not-b.not-b...) drives parse_prefix -> parse_not -> parse_subexpr, re-walking the chain at every segment and doubling the work. The result is always rejected (UnaryOp), so the speculation is wasted, and parse_identifier in the existing None branch produces the same Identifier directly. The solution I identified is to skip the speculative parse_prefix in parse_compound_expr when the next token is a Word not followed by (. Surfaced by subql fuzzing on a 527 B input that took 2.65 s and now takes 270 us.

N before after
5 63.3 us 8.06 us
10 1.97 ms 15.1 us
15 65.3 ms 22.4 us

@iffyio iffyio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM! Thanks for these fixes @LucaCappelletti94!

@iffyio iffyio added this pull request to the merge queue Jun 6, 2026
Merged via the queue into apache:main with commit 928783d Jun 6, 2026
10 checks passed
moshap-firebolt added a commit to firebolt-analytics/datafusion-sqlparser-rs that referenced this pull request Jun 9, 2026
Mirrors the position-keyed failure cache pattern from apache#2344, apache#2350, apache#2352.

`parse_table_factor`'s `(` arm speculatively parses a derived table; on
failure it rewinds and tries `parse_table_and_joins` (nested-join). Both
arms recurse back into `parse_table_factor` consuming the next `(`, so
on pathological inputs like `SELECT 1 FROM (((((...` each level
re-runs the speculative arm — work doubles at each level. With 30 nested
parens this takes >7s; with 50, the libFuzzer per-test timeout fires
(>1300s seen in CI).

Cache the parser position at which `parse_derived_table_factor` was
already attempted and failed. The next time `parse_table_factor` reaches
that position (via the nested-join arm's recursive descent), skip the
speculative call and go straight to the fallback. The cache only stores
positions where a non-`RecursionLimitExceeded` failure occurred, so the
recursion-limit guard still propagates.

Regression test: `parse_table_factor_paren_chain_no_exponential_blowup`
runs the parse on a worker thread and asserts it returns within 5 s;
pre-fix it hangs the libFuzzer worker for >20 minutes on a 666-byte
ClickHouse seed surfaced by the `sql_parser_dialects` fuzz harness.

Bench: `parse_table_factor_paren_chain/chain_{10,20,30}`.

Drive-by: add the missing comma in `criterion_group!` between
`parse_compound_keyword_chain` and `parse_prefix_keyword_call_chain`
(was a parse error preventing the new bench from registering).
moshap-firebolt added a commit to firebolt-analytics/datafusion-sqlparser-rs that referenced this pull request Jun 9, 2026
Mirrors the position-keyed failure cache pattern from apache#2344, apache#2350, apache#2352.

`parse_table_factor`'s `(` arm speculatively parses a derived table; on
failure it rewinds and tries `parse_table_and_joins` (nested-join). Both
arms recurse back into `parse_table_factor` consuming the next `(`, so
on pathological inputs like `SELECT 1 FROM (((((...` each level
re-runs the speculative arm — work doubles at each level. With 30 nested
parens this takes >7s; with 50, the libFuzzer per-test timeout fires
(>1300s seen in CI).

Cache the parser position at which `parse_derived_table_factor` was
already attempted and failed. The next time `parse_table_factor` reaches
that position (via the nested-join arm's recursive descent), skip the
speculative call and go straight to the fallback. The cache only stores
positions where a non-`RecursionLimitExceeded` failure occurred, so the
recursion-limit guard still propagates.

Regression test: `parse_table_factor_paren_chain_no_exponential_blowup`
runs the parse on a worker thread and asserts it returns within 5 s;
pre-fix it hangs the libFuzzer worker for >20 minutes on a 666-byte
ClickHouse seed surfaced by the `sql_parser_dialects` fuzz harness.

Bench: `parse_table_factor_paren_chain/chain_{10,20,30}`.

Drive-by: add the missing comma in `criterion_group!` between
`parse_compound_keyword_chain` and `parse_prefix_keyword_call_chain`
(was a parse error preventing the new bench from registering).
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.

2 participants