oracle: parse PIVOT/UNPIVOT inside the table_reference chain#373
Conversation
PIVOT and UNPIVOT were parsed at the select level after the whole FROM
clause, retro-attaching the last from-item as Source. That misplaces them
in the grammar: Oracle's table_reference is
query_table_expression [pivot_clause | unpivot_clause] [t_alias]
so the pivoted result can carry a trailing bare-identifier alias and
participate in joins. The old shape rejected both:
SELECT p.a FROM (SELECT a, b, m FROM t1) PIVOT (SUM(b) FOR m IN (1, 2)) p
SELECT ... FROM t1 PIVOT (...) p JOIN d ON p.a = d.a
Move the parse into the table_reference chain (plain table, subquery, and
parenthesized table reference paths) via parsePivotSuffix, which attaches
the clause to its source and consumes the optional t_alias. AS aliases and
column alias lists stay rejected, matching Oracle (ORA-03048; t_alias
takes neither). Remove the now-dead SelectStmt.Pivot/Unpivot fields, their
outfuncs writes, and the generated walker edges; the clauses already
implement TableExpr and appear as from-items.
All accept/reject shapes in the new tests verified against Oracle 23ai
Free, including the PL/SQL OPEN ... FOR cursor context the report came
from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1eada5e6cd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Address three review findings, each adjudicated against Oracle 23ai Free:
- Wire parsePivotSuffix into the query_table_expression paths that
returned early: TABLE() collections, XMLTABLE, JSON_TABLE,
CONTAINERS/SHARDS, and MATCH_RECOGNIZE (standalone and post-table) —
Oracle accepts PIVOT/UNPIVOT after all of them. LATERAL stays
unwired (ORA-56905: pivot not supported on lateral views) and inline
EXTERNAL tables sit outside query_table_expression in the BNF.
- Gate the suffix on the clause body actually following (PIVOT + '('
or XML; UNPIVOT + '(' or INCLUDE/EXCLUDE), and make the clause
parsers hard-error on a missing '(' instead of returning a partial
node — 't PIVOT p' no longer parses as an empty pivot aliased p
(Oracle: ORA-03049). PIVOT/UNPIVOT are non-reserved, so a bare
trailing PIVOT is an alias position in Oracle; we keep rejecting
that degenerate form, as before this change.
- Emit :source from writePivotClause/writeUnpivotClause: the base
relation now lives only in the clause's Source field, so outfuncs
must serialize it or NodeToString loses the table.
Also loop the suffix: Oracle chains pivot/unpivot clauses with
optional per-step aliases (t PIVOT (...) a PIVOT (...) b), nesting
each clause as the next one's Source. New tests cover the full
source-form × pivot/unpivot matrix, chaining, and the reject shapes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02967b4176
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Round-2 review findings, adjudicated against Oracle 23ai Free: - FOR and IN are mandatory inside pivot_clause/unpivot_clause (Oracle: ORA-02000 missing FOR, ORA-01738 missing IN), as is the closing paren. The parsers previously treated them as optional, so malformed bodies like PIVOT (SUM(b)) parsed and then took a trailing alias. - Oracle accepts a pivot AFTER an aliased source (t alias PIVOT (...) p) even though the documented grammar orders the alias last; TABLE() and the other wrapped sources already handled this via their internal alias parse, but the plain-table, subquery, and parenthesized paths returned right after the alias and rejected valid SQL. All three now tail into parsePivotSuffix, which no-ops when nothing follows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
omni now parses PIVOT/UNPIVOT inside the table_reference chain (bytebase/omni#373): the clauses appear as from-items carrying their source and optional trailing t_alias, and SelectStmt.Pivot/Unpivot are removed. The span extractor already handles the from-item form, so the dead select-level branches go away, along with the redundant isSimpleOracleSelect conjuncts (a pivot from-item is not a DUAL TableRef, so the FROM check already excludes it). This makes the customer-reported shape parse end to end: OPEN cur FOR SELECT p.a FROM (SELECT ...) PIVOT (SUM(b) FOR m IN (...)) p ORDER BY p.a; New parity tests cover the trailing-alias and pivot-join shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Problem
PIVOT/UNPIVOT were parsed at the select level after the whole FROM clause, retro-attaching the last from-item as
Source. Oracle's grammar places them insidetable_reference:so a pivoted result can carry a trailing bare-identifier alias and participate in joins. The old placement rejected both:
Reported via a PL/SQL function using
OPEN ... FOR (subquery) PIVOT (...) alias, which failed to parse while being valid Oracle.Fix
parsePivotSuffixattaches PIVOT/UNPIVOT (and the optionalt_alias) to the just-parsed table reference; wired into all threetable_referenceentry paths (plain table, subquery, parenthesized table reference). The clause becomes the from-item itself —PivotClause/UnpivotClausealready implementTableExprand already hadAlias/Sourcefields.AS pand column alias listsp(x, y)after the pivot stay rejected — Oracle raises ORA-03048 on both (t_aliastakes neither).SelectStmt.Pivot/Unpivotfields, their outfuncs writes, and regenerated the walker. This is an intentional fail-loud API break for consumers readingSelectStmt.Pivot; the Bytebase span extractor already handles the from-item form (case *ast.PivotClause) and will be updated in the bump PR.Verification
Every accept/reject shape in
pivot_alias_test.gowas run against Oracle 23ai Free: the 5 positive shapes (table/subquery/XML pivot + alias, unpivot + alias, pivot-then-join) execute cleanly, the 2 negative shapes fail with ORA-03048, and the PL/SQL cursor function compiles with no errors. Full./oracle/...suite passes.