Skip to content

parser: fold a top-level bare VALUES as a set-op LEFT operand - #57

Merged
chiradip merged 1 commit into
mainfrom
fix-bare-values-left-setop-operand
Jul 27, 2026
Merged

parser: fold a top-level bare VALUES as a set-op LEFT operand#57
chiradip merged 1 commit into
mainfrom
fix-bare-values-left-setop-operand

Conversation

@chiradip

@chiradip chiradip commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

parse_statement dispatched a leading VALUES straight to parse_values_stmt() with no set-operation fold — unlike the leading-SELECT path (parse_select_stmt → fold_set_operations) and the leading-( path (parse_parenthesized_query → fold_set_operations). So a top-level bare VALUES that is the LEFT operand of a set operation silently dropped the operator and the entire right arm:

VALUES (1) UNION SELECT 2   →   bare ValuesStmt(1)   -- "UNION SELECT 2" discarded, no error

The symmetric forms already worked: SELECT 1 UNION VALUES (2)UnionStmt, and (VALUES (1)) UNION SELECT 2UnionStmt. Only the bare-LEFT top-level case was left behind. VALUES is a query primary and a legal set-op operand on either side (Postgres: VALUES (1) UNION SELECT 2 is a 2-row UNION). Found by the repeatable frontend-audit's regression sweep of the VALUES set-op work.

Fix

Route the top-level VALUES through fold_set_operations, exactly like the other two leading-operand paths:

Input Before After
VALUES (1) UNION SELECT 2 ValuesStmt(1) UNION(VALUES(1), SELECT 2)
VALUES (1) INTERSECT SELECT 2 ValuesStmt(1) INTERSECT(…)
VALUES (1) UNION SELECT 2 ORDER BY 1 ValuesStmt(1) UNION(…) with ORDER BY on the union
VALUES (3),(1) ORDER BY 1 (standalone) unchanged unchanged — keeps its own ORDER BY

A standalone VALUES has no set-op tail, so fold_set_operations returns it unchanged (and it keeps its own trailing ORDER BY/LIMIT); a VALUES … UNION … binds a trailing ORDER BY to the whole set operation.

This completes the VALUES-as-set-op-operand work of #53 (VALUES on the RHS) and #56 (bare-VALUES RHS ORDER BY).

Tests

Adds SetOpBareValuesLeftOperand (UNION/INTERSECT/EXCEPT), SetOpBareValuesLeftOperandTrailingOrderBy, and a StandaloneValuesUnchangedByFold control to test_precedence_regression. Full suite 37/37 green.

parse_statement dispatched a leading VALUES straight to parse_values_stmt()
with no set-operation fold, unlike the leading-SELECT path (parse_select_stmt
-> fold_set_operations) and the leading-'(' path (parse_parenthesized_query ->
fold_set_operations). So a top-level bare VALUES that is the LEFT operand of a
set operation silently dropped the operator and the entire right arm:
`VALUES (1) UNION SELECT 2` parsed to a bare ValuesStmt(1), discarding
`UNION SELECT 2` with no error. The symmetric forms already worked
(`SELECT 1 UNION VALUES (2)` and `(VALUES (1)) UNION SELECT 2`).

VALUES is a query primary and a legal set-op operand on either side (Postgres:
`VALUES (1) UNION SELECT 2` is a 2-row UNION). Route the top-level VALUES
through fold_set_operations, exactly like the other two leading-operand paths.
A standalone VALUES has no set-op tail, so the fold returns it unchanged and it
keeps its own trailing ORDER BY / LIMIT; a `VALUES ... UNION ...` binds a
trailing ORDER BY to the whole set operation.

This completes the VALUES-as-set-op-operand work of #53 (2548bcf, VALUES on the
RHS) and #56 (d7e4494, bare-VALUES RHS ORDER BY), which handled the RHS and
parenthesized cases but left the bare top-level LEFT operand behind.

Adds SetOpBareValuesLeftOperand (UNION/INTERSECT/EXCEPT),
SetOpBareValuesLeftOperandTrailingOrderBy, and a StandaloneValuesUnchangedByFold
control to test_precedence_regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyKDAbMWGwiZq4iJx3imsg
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.

1 participant