feat(sql,core): derived tables in FROM and JOIN — grammar, AST, column scope, loud guard (story 22.1) - #331
Merged
Merged
Conversation
…m loudly until the engine can run them
`FROM (SELECT ...) [AS] alias` and `[join_type] JOIN (SELECT ...) [AS] alias [ON ...]` now parse
into a `DerivedTable` node whose `name` IS its correlation name, so every existing alias map,
qualifier resolution and join-leg key works with no new arm. The statement is validated -- an outer
reference must name a column the subquery projects, a body that reads an enclosing alias is
rejected as LATERAL, a correlation name may not name anything else in the same FROM, and the body's
own rules are checked one level down -- and then refused with HTTP 400 at every execution site,
because a derived table's `sources` is an ALIAS and executing it naively would search an index
named after the subquery.
Nine of the eleven derived-table statements captured from Tableau and Superset now parse; the two
that do not are declared residuals with a named owner. The Epic 21 headline is unchanged at
SCORES 56/99 -- these rows score `residual` -- while the raw parse count moves 81 -> 90.
Also here, because the grammar needs it: `whereCriteria` becomes a depth-aware scanner that leaves
a depth-0 `)` to whoever opened it. Without it, a body whose last clause is WHERE or HAVING loses
its own closing parenthesis to the inner clause and the whole statement is rejected as unbalanced.
Behaviour changes, all release-noted:
- the direct client API (`search` / `searchAsync` / `scroll` / `searchWithInnerHits`) returns 400
for a cross-index JOIN where it used to return rows from the first index with HTTP 200;
- `searchAs` / `scrollAs` no longer compile for a cross-index JOIN or a derived table;
- the closure rejection message names the shape and is venue-neutral;
- `DELETE`, `CREATE WATCHER` and `CREATE MATERIALIZED VIEW` refuse derived tables by name;
- a stray `)` is still rejected, as trailing input rather than as unbalanced parentheses.
`Table` gains a defaulted `derived` field (arity 4->5), so downstream repositories rebuild on the
next core bump.
Closed Issue #330
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.
Story 22.1 — Derived tables in FROM and JOIN: grammar, AST, column scope, loud guard
FROM (SELECT …) [AS] aliasand[join_type] JOIN (SELECT …) [AS] alias [ON …]now parse into aDerivedTablenode, are validated (projection scope, LATERAL refusal, duplicate correlationnames, the body's own rules), and are refused loudly with HTTP 400 at every execution site until
the relational engine can run them.
This is epic 22's Layer 0: every later story consumes this node and the ONE
relationalClosureRequiredpredicate.Corpus movement: 9 of the 11 captured derived-table statements now parse (Tableau's connection
probe
SELECT COL FROM (SELECT 1 AS COL) AS SUBQUERY, its canvas row count, and Superset'sJOIN (… GROUP BY … LIMIT 10) AS series_limitseries limit among them). The Epic-21 scoreboardheadline is unchanged at SCORES 56/99 — these rows score
residual, because Epic 21 did not fixthem — while the raw parse count moves 81 → 90.
Release notes
engine (story 22.4) and are refused with HTTP 400 elsewhere. The alias is mandatory
(SQL-92 §7.6 correlation name; MySQL raises error 1248 for its absence).
Tablearity 4→5 (derived, defaulted, appended last); newDerivedTableclass; newSubqueryScopeobject. Downstream (arrow / jdbc / extensions) rebuildon the next core bump — already owed by 21.x.
client.search/searchAsync/scroll/searchWithInnerHitsover a cross-index JOIN now return 400 where they used to return rowsfrom the first index. MEASURED before the change on a stub client: the JOIN leg was silently
dropped and the query became
match_allover the first table, HTTP 200. That is JOIN queries without a join-capable extension silently return wrong results #157's residualon the raw API (no sibling production code calls it — every one goes through
gateway.run— whichis why it survived), closed here on the lead's ruling.
searchAs/scrollAsover a cross-index JOIN or a derivedtable no longer COMPILE. The JOIN spelling used to compile and then run the first index alone.
CoreDqlExtension's JOIN queries without a join-capable extension silently return wrong results #157 rejection is reworded, names the shape(a derived table or a cross-index JOIN) and is venue-neutral: the same text now reaches a Scala
embedder, the JDBC driver and the Flight sidecar verbatim, so "re-run the installer" is given as
the REPL's spelling of the remedy rather than the only one. The pre-existing wrong
repl.md#extensionsanchor is corrected.DELETE FROM (SELECT …) d,CREATE WATCHER … FROM (SELECT …) dandCREATE MATERIALIZED VIEW … FROM (SELECT …) dare nowrefused with a message that names the construct and the remedy.
)changes its wording.SELECT a FROM t WHERE a = 1)is stillrejected, but as trailing input rather than as
"Unbalanced parentheses". An unmatched OPENINGparenthesis keeps
"Unbalanced parentheses"byte-for-byte.CREATE MATERIALIZED VIEWvalidates its SELECT body at parse time — already true onmainsince BIDC-8; this story adds only the derived-table arm, ordered first so its message wins. No MV
fixture in this repository or in
softclient4es-extensionscarries a derived table, so nothingthat used to parse is newly rejected.
0.23.0with this story unless the ArrowJoinPlannerrefuses derived tables (the sibling PR linked below, or story 22.4).From.enrichmentRequiredistrueforFROM t JOIN (SELECT …) d, so a venue that HASsoftclient4es-arrow-extensionsroutes the statement to Arrow, whose planner reads thecorrelation name as an index name. Tableau aliases a derived table with the inner index's own
name, so the failure mode is an HTTP 200 wrong answer, not a 404.
Sibling PR that lifts the gate: SOFTNETWORK-APP/softclient4es-arrow#181.
Arrow planner probe (the reason for PD-4 and the sibling PR)
JoinDetector.CrossIndexJoinPatternalready routesJOIN (SELECT …)toJoinPlanner.plan, where itused to die at
Parser(cleanedSql). Now that it parses:FROM (SELECT …) d WHERE …(no JOIN)Passthrough, never reaches the plannerJOIN (SELECT …) AS d ON …StandardJoin;TableInfo(name = sj.source.name)is the alias; the leg SQL becomesSELECT … FROM dindex_not_foundif no such index exists, a wrong answer with HTTP 200 if one doesFROM (SELECT …) d JOIN customers c ON …TableInfo(name = primaryTable.name)is"d"FROM (SELECT … JOIN … ) v GROUP BY …primaryTable.joinsis emptyParserSpecwall time (10 runs each, no assertion)Verification
sql1146 ·core1012 ·macros-tests23 · bridge template 203 · es6 bridge 203, all green;cross-compiled on 2.12.20 and 2.13.16;
headerCheck/scalafmtCheck/Test/scalafmtCheckgreen.ES integration green on all five clients (es6 REST + Jest, es7, es8, es9) including the three new
derived-table rows.
Found while verifying — PRE-EXISTING, measured against a control checkout, NOT fixed here
A double-quoted qualifier on the RIGHT-HAND side of a JOIN
ONcomparison is rejected.A quoted table name in JOIN position is rejected.That first characterisation was WRONG andis retracted — it was inferred from one failing statement with no control. Re-measured on
unmodified
main, shape by shape:ON b.k = t0.k(control)ON "b"."k" = t0.k— quoted qualifier, LEFT sideON b.k = `t0`.`k`— BACKTICK qualifier, right sideON b.k = t0."k"— quoted column, bare qualifierON b.k = "t0".k/ON b.k = "t0"."k"end of input expected)The JOIN table name is NOT the cause:
JOIN u AS "t0" ON b.k = "t0"."k"(bare name) failsidentically. The render round-trip is NOT broken either: every quoted-JOIN render measured
(aliased, un-aliased, qualified, CROSS, backtick-canonicalised) is a fixed point.
Root cause: story 21.1 AD-13 deliberately keeps
literalFIRST in the alternation for valuepositions, so on the right of
=the lexeme"t0"is read as the string literalt0and."k"becomes trailing input. Backticks are unambiguous, which is why they parse.Why it matters: Tableau's SQL-92 dialect double-quotes every identifier, so this is the exact
ONit emits for a join (its MySQL/backtick equivalent already works, e.g. corpus rowtableau.mysql.w8.054). It is also self-inconsistent — the mirroredON "b"."k" = t0.kparses.Does not affect this story's nine corpus rows: every JOIN they carry is a derived table.
SchemaCacheTtlApiSpec"refuse a non-positive value" fails on the Scala 2.12 leg ofcore/test, identically on the control.+ core/testis not part of the documented gate;++ 2.12.20 core/Test/compileis, and is green.Residual recorded, not filed (lead ruling: narrow)
The duplicate-source rule is scoped to collisions involving a derived table.
FROM orders JOIN orders(two sources, no written aliases) still collapses in the alias maps and still parses.Sibling PR
SOFTNETWORK-APP/softclient4es-arrow#181 — the loud
JoinPlannerrefusal. Both were approved up front; this one lands first, then the core snapshot publishes, then the Arrow arm.Closes #330