parser: don't let a bare VALUES set-op arm swallow the whole query's ORDER BY/LIMIT - #56
Merged
Merged
Conversation
…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
This was referenced Jul 27, 2026
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.
Problem
parse_values_stmt()consumes a trailingORDER BY/LIMITunconditionally. WhenVALUESis 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:... UNION SELECT 2 ORDER BY 1... UNION (VALUES (2)) ORDER BY 1... UNION VALUES (2) ORDER BY 1So 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 clearsin_setop_rhs_at entry (so nested parses behave normally) and skips its trailingORDER BY/LIMITwhen the flag was set.parse_setop_operandsetsin_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:
VALUES (...) ORDER BY 1(flag false) still keeps its ownORDER BY/LIMIT.(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 armSetOpBareValuesTrailingLimitBindsToWhole— same for LIMITTopLevelValuesKeepsOwnOrderBy— control: standalone VALUES still owns its ORDER BYFull suite: 37/37 green.