Skip to content

Keep a set operation after an unaliased derived table outside it - #18

Merged
ravenac95 merged 2 commits into
mainfrom
unaliased-derived-table-set-op
Nov 10, 2026
Merged

ravenac95 merged 2 commits into
mainfrom
unaliased-derived-table-set-op

Conversation

@ravenac95

@ravenac95 ravenac95 commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

After a parenthesized query in FROM, the parser folded any following UNION/INTERSECT/EXCEPT (and a trailing LIMIT) into the derived table:

SELECT 1 AS s, * FROM (SELECT x FROM a) UNION ALL SELECT 2, x FROM b
-- was reprinted as
SELECT 1 AS s, * FROM ((SELECT x FROM a) UNION ALL SELECT 2, x FROM b)

That changes the query: the outer projection is applied to the whole union. Found in production as TYPE_MISMATCH: UNION query has different number of fields: 6, 7. qualify_tables' set-operation-subquery normalization undid the wrapper, but only when the outer query is a bare SELECT *, which hid the bug in the common case. Upstream 0.14.1 has the same behaviour.

Change

  • The parenthesized-query branch no longer parses a set operation after its closing paren; that set operation belongs to the enclosing query.
  • Inside nested parens (FROM ((SELECT 1) UNION (SELECT 2))), the nested-paren branch already parsed the set operation. It now wraps it in the same Subquery the inner branch used to return. Its ORDER BY/LIMIT/OFFSET handling relies on that shape: without it, SELECT * FROM (((SELECT 1) UNION SELECT 2) ORDER BY x LIMIT 1 OFFSET 1) moved the modifiers outside the outer parens (caught by sqlglot_identity).
  • qualify_tables::test_can_disable_set_operation_wrapper_normalization asserted the misparsed shape with the normalization turned off. It now expects the correct one, with no wrapper left to normalize.

Tests

  • tests/unaliased_derived_table_set_operation.rs (new):
    • the bug: projection-plus-star, inner LIMIT with an outer LIMIT, a comma join, and EXCEPT, in Trino and generic;
    • parenthesized set operations that must keep their shape, including the ORDER BY … LIMIT … OFFSET one above.
  • With the sqlglot fixtures extracted: sqlglot_transpilation, sqlglot_identity, sqlglot_dialect_identity, sqlglot_parser and sqlglot_transpile all pass in full. cargo test --lib (1343), identity_roundtrip and query_analysis pass.
  • kuma's query-rewriter suite, including its column and lineage fixtures, passes against main with this PR, Reprint Trino/Presto TIMESTAMP(x) as CAST(x AS TIMESTAMP) #16 and Keep Presto/Trino two-argument MAX(x, n)/MIN(x, n) as aggregates #17 combined.

🤖 Generated with Claude Code

https://claude.ai/code/session_017CxHoCFbTWJA9bcZWC9ydu

In `SELECT 1 AS s, * FROM (SELECT x FROM a) UNION ALL SELECT 2, x FROM b` the
parser folded the UNION ALL (and any trailing LIMIT) into the derived table,
changing the query: the outer projection then applied to the whole union.
qualify_tables' set-operation normalization undid it only for a bare
`SELECT *` outer query. A set operation after the closing paren belongs to
the enclosing query; nested parens (`FROM ((SELECT 1) UNION (SELECT 2))`)
are still parsed by the nested-paren branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017CxHoCFbTWJA9bcZWC9ydu
The nested-paren branch now parses the set operation itself, so wrap it in the
Subquery the inner paren branch used to return. The ORDER BY/LIMIT/OFFSET
handling after it relies on that shape: without it,
SELECT * FROM (((SELECT 1) UNION SELECT 2) ORDER BY x LIMIT 1 OFFSET 1)
moved the modifiers outside the outer parens (sqlglot_identity).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017CxHoCFbTWJA9bcZWC9ydu
@ravenac95
ravenac95 merged commit d95fbbb into main Nov 10, 2026
29 checks passed
ravenac95 added a commit that referenced this pull request Nov 10, 2026
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