Skip to content

parser: don't let a bare VALUES set-op arm swallow the whole query's ORDER BY/LIMIT - #56

Merged
chiradip merged 1 commit into
mainfrom
fix-values-setop-trailing-orderby
Jul 27, 2026
Merged

parser: don't let a bare VALUES set-op arm swallow the whole query's ORDER BY/LIMIT#56
chiradip merged 1 commit into
mainfrom
fix-values-setop-trailing-orderby

Conversation

@chiradip

@chiradip chiradip commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

parse_values_stmt() consumes a trailing ORDER BY / LIMIT unconditionally. When VALUES is the right-hand operand of a set operation, that clause belongs to the whole set operation — but the bare-VALUES arm ate it and nested the clause under the VALUES arm:

SELECT 1 UNION VALUES (2) ORDER BY 1
Right operand form ORDER BY attaches to Correct?
... UNION SELECT 2 ORDER BY 1 the UNION (3 children)
... UNION (VALUES (2)) ORDER BY 1 the UNION (3 children)
... UNION VALUES (2) ORDER BY 1 the VALUES arm (UNION has only 2 children)

So the same trailing clause produced three different tree shapes depending on the right operand. In Postgres an unparenthesized set-op arm cannot own an ORDER BY — it belongs to the union. Found by the repeatable frontend-audit's regression sweep of the VALUES set-op-operand commit.

Fix

Mirror what the bare-SELECT arm already does via in_setop_rhs_:

  • parse_values_stmt() captures and clears in_setop_rhs_ at entry (so nested parses behave normally) and skips its trailing ORDER BY / LIMIT when the flag was set.
  • The bare-VALUES branch of parse_setop_operand sets in_setop_rhs_ before calling, exactly like the SELECT arm.

The trailing clause is then bound once to the combined node by attach_trailing_order_limit, matching the SELECT and parenthesized-VALUES arms.

Unchanged behavior preserved:

  • A standalone top-level VALUES (...) ORDER BY 1 (flag false) still keeps its own ORDER BY / LIMIT.
  • A parenthesized (VALUES ...) operand still keeps its own (it re-enters with the flag cleared).

Tests

Adds to test_precedence_regression.cpp:

  • SetOpBareValuesTrailingOrderByBindsToWhole — ORDER BY on the UNION, not the VALUES arm
  • SetOpBareValuesTrailingLimitBindsToWhole — same for LIMIT
  • TopLevelValuesKeepsOwnOrderBy — control: standalone VALUES still owns its ORDER BY

Full suite: 37/37 green.

…ORDER BY/LIMIT

parse_values_stmt() unconditionally consumed a trailing ORDER BY / LIMIT. When
VALUES was the right-hand operand of a set operation -- `SELECT 1 UNION VALUES
(2) ORDER BY 1` -- that clause belongs to the WHOLE set operation, but the
bare-VALUES operand ate it, nesting the OrderByClause/LimitClause UNDER the
VALUES arm (the UNION ended up with only two children). The bare-SELECT arm
already avoids this via the in_setop_rhs_ guard, and the parenthesized-VALUES
arm avoids it because the parens re-enter outside setop-rhs mode; only the bare
VALUES arm was left behind, so the same query parsed to three different shapes
depending on the right operand.

Mirror the SELECT arm: parse_values_stmt() now captures and clears in_setop_rhs_
at entry and skips its trailing ORDER BY / LIMIT when set, and the bare-VALUES
branch of parse_setop_operand sets in_setop_rhs_ before calling. The trailing
clause is then bound once to the combined node by attach_trailing_order_limit,
matching the SELECT and parenthesized-VALUES arms.

A standalone top-level VALUES (in_setop_rhs_ false) still keeps its own
ORDER BY / LIMIT, as does a parenthesized (VALUES ...) operand. Adds
SetOpBareValuesTrailingOrderByBindsToWhole / ...TrailingLimitBindsToWhole and
the TopLevelValuesKeepsOwnOrderBy 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