parser: accept a LEADING parenthesized set-operation operand#50
Merged
Conversation
A statement could begin with a parenthesized query block only in the RHS
of a set operation; a LEADING '(' at statement level -- e.g.
`(SELECT 1) UNION (SELECT 2)` -- was never dispatched (parse_statement
bailed on the non-keyword/identifier token) and the whole statement
failed to parse.
Change:
- Extract the set-operation fold out of parse_select_stmt into a member
`fold_set_operations(first_operand)`, and the trailing ORDER BY / LIMIT
attach into `attach_trailing_order_limit(target)`. Pure extraction --
no change to precedence (INTERSECT tighter than UNION/EXCEPT), left-
associativity, ALL-modifier placement, or trailing-clause binding.
- Add `parse_parenthesized_query()` for a `( <query> )` operand, reused by
both the RHS operand path and the new leading path. It is DepthGuard-
protected and recurses for nested `((...))`.
- parse_statement now dispatches a leading '(' to
parse_parenthesized_query() then fold_set_operations(), so a leading
parenthesized operand folds like any other and a trailing ORDER BY /
LIMIT binds to the whole result.
Parentheses correctly override the INTERSECT-binds-tighter precedence:
`(A UNION B) INTERSECT C` roots at INTERSECT with the UNION as its left
operand, the opposite grouping from the unparenthesized form.
Verification:
- 5 new regression tests pinning AST shape for leading-paren cases
(both-paren, paren-left/bare-right, precedence override, nested +
trailing ORDER BY, lone-paren unwrap). Falsifiable: all 5 fail with the
dispatch hook reverted, pass with it.
- Malformed/adversarial inputs reject gracefully (no crash); leftover-
token leniency is identical to the unparenthesized forms (parity).
- 200k-deep `((((...))))` nesting is rejected via the DepthGuard, no
stack overflow.
- Full parser suite 37/37; precedence suite (21 tests) clean under
ASan/UBSan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyKDAbMWGwiZq4iJx3imsg
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
A statement could begin with a parenthesized query block only as the right-hand operand of a set operation. A leading
(at statement level — the canonical(SELECT 1) UNION (SELECT 2)— was never dispatched:parse_statementbailed out the moment it saw a token that was neither a keyword nor an identifier, so the whole statement failed to parse. This closes that gap.Change
parse_select_stmtinto a memberfold_set_operations(first_operand), and the trailingORDER BY/LIMITattach intoattach_trailing_order_limit(target). This is a pure extraction — no change to precedence (INTERSECTbinds tighter thanUNION/EXCEPT/MINUS), left-associativity,ALL-modifier placement, or trailing-clause binding.parse_parenthesized_query()for a( <query> )operand, reused by both the RHS-operand path and the new leading path. It isDepthGuard-protected and recurses for nested((...)).parse_statementnow routes a leading(throughparse_parenthesized_query()thenfold_set_operations(), so a leading parenthesized operand folds like any other and a trailingORDER BY/LIMITbinds to the whole result.Parentheses correctly override the INTERSECT-binds-tighter precedence:
(A UNION B) INTERSECT Croots atINTERSECTwith theUNIONas its left operand — the opposite grouping from the unparenthesizedA UNION B INTERSECT C.Verification
ORDER BY, and lone-paren unwrap. Falsifiable — all 5 fail with the dispatch hook reverted and pass with it.((((...))))nesting is rejected via theDepthGuard— no stack overflow.Scope
Top-level leading
(only. A leading(after aWITHclause (WITH t AS (...) (SELECT 1) UNION (SELECT 2)) remains a separate, rarer follow-up.