grammar: sql_expr additive extensions for §5/§6, CTE body rewires to ScopedSubgrammar

Sub-phase 2b checkpoint 2 — closes the recursion loop between
sql_expr.rs and sql_select.rs so subquery expressions and
qualified column refs become structurally valid in every SQL
context where they belong.

sql_expr.rs:

- §5 qualified-ref tail. `name_or_call` gains a `.identifier`
  suffix as a Choice sibling of the function-call `(args)`
  tail. The leading identifier is still matched once (per
  ADR-0031 §1's factoring); the optional tail dispatches
  between the two suffixes by their first character (`.` vs
  `(`).
- §6.1 scalar subquery as primary. The `(or_expr)` and
  `(SELECT …)` branches share the leading `(`; the first
  inside token (`SELECT` → subquery, anything else →
  expression) discriminates. The subquery recurses through
  `Node::ScopedSubgrammar(&sql_select::SQL_SELECT_COMPOUND)`.
- §6.2 IN (subquery) predicate. Sibling of the existing
  IN-value-list; same `(` factoring, same dispatch.
- §6.3 [NOT] EXISTS primary. Bare `EXISTS (compound_select)`
  lives in `primary`; `NOT EXISTS` falls out via the existing
  `not_expr := NOT not_expr` tier above `primary`.

sql_select.rs:

- CTE body recursion rewires `Node::Subgrammar` →
  `Node::ScopedSubgrammar`, matching §10.2. The top-level
  statement's COMPOUND embedding stays plain Subgrammar — the
  implicit bottom frame is the right scope for a statement-
  level SELECT.

Structural side-effect — const-eval cycle workaround:

Closing the sql_expr ⇄ sql_select reference loop made Rust's
const-evaluator follow the cycle through every `const Node`
that transitively reaches it. Mirroring sql_expr.rs's existing
pattern, composition Nodes in sql_select.rs (Seq / Choice /
Optional / Repeated / Lookahead) are now `static Node` and
appear in slice positions through `Node::Subgrammar(&NAME)`
wraps; only leaf items (Punct, Word, Ident) remain `const`.
Same workaround applies to data.rs's SELECT_PROJ_LIST /
SELECT_PROJECTION chain and the inlined `SQL_EXPR` reference.
Statics resolve lazily at link time, so the cycle is valid;
const-eval is not, and the named `const SQL_EXPR` alias is
gone in both files (replaced with the inline `Node::Subgrammar
(&sql_expr::SQL_OR_EXPR)` expression at every use site).

Test coverage:

- sql_expr.rs gains 11 new tests for qualified refs, scalar
  subquery, IN-subquery, EXISTS / NOT EXISTS, nested
  subqueries, and the existing IN-value-list form (regression).
- sql_select.rs gains 7 new tests for qualified refs in WHERE,
  scalar subqueries in WHERE / projection, IN / EXISTS / NOT
  EXISTS in WHERE, nested subqueries, and qualified refs
  inside CTE bodies.
- All 70 prior sql_select tests still pass; the 2a baseline
  is preserved.

`(WITH x AS (…) SELECT * FROM x)` is explicitly NOT admitted
as a scalar subquery — ADR-0032 §1 / §9 wire subqueries to
SQL_SELECT_COMPOUND, which omits the outer with_clause. WITH
remains a statement-level-only construct. Documented in the
relevant test.

Test totals: 1333 → 1351 passing, 0 failed, 1 ignored
(unchanged). Clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-20 11:47:27 +00:00
parent 4f89106a63
commit 98a74b23d3
3 changed files with 314 additions and 98 deletions
+16 -9
View File
@@ -373,8 +373,10 @@ const EXPLAIN_SHAPE: Node = Node::Choice(EXPLAIN_CHOICES);
// column aliasing (`select a x`) and qualified `t.*` are out of
// Phase 1 (see the inline notes).
/// A SQL expression slot — the ADR-0031 fragment as one node.
const SQL_EXPR: Node = Node::Subgrammar(&sql_expr::SQL_OR_EXPR);
// SQL expression slot — `Node::Subgrammar(&sql_expr::SQL_OR_EXPR)`
// is inlined at each use site to avoid a Rust const-evaluation
// cycle through the sql_expr ⇄ sql_select recursion (see the
// matching note in sql_select.rs).
/// `as <alias>` — the explicit projection alias. Implicit
/// aliasing (`select a x`) is not supported: a bare alias is
@@ -396,13 +398,17 @@ static SELECT_AS_ALIAS: Node = Node::Seq(SELECT_AS_ALIAS_NODES);
/// A projection item: a SQL expression with an optional alias.
static SELECT_PROJ_ITEM_NODES: &[Node] = &[
SQL_EXPR,
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
Node::Optional(&SELECT_AS_ALIAS),
];
static SELECT_PROJ_ITEM: Node = Node::Seq(SELECT_PROJ_ITEM_NODES);
/// `proj_item ( , proj_item )*`.
const SELECT_PROJ_LIST: Node = Node::Repeated {
/// `static` (not `const`) to avoid a Rust const-evaluation
/// cycle through the `sql_expr` ⇄ `sql_select` recursion. The
/// cycle is valid at link-time (statics resolve lazily) but
/// not at const-eval — see notes in sql_select.rs.
static SELECT_PROJ_LIST: Node = Node::Repeated {
inner: &SELECT_PROJ_ITEM,
separator: Some(&Node::Punct(',')),
min: 1,
@@ -410,8 +416,9 @@ const SELECT_PROJ_LIST: Node = Node::Repeated {
/// `projection := '*' | proj_item ( , proj_item )*`. (`t.*`
/// qualified star is Phase 2 — it needs join scope.)
const SELECT_PROJECTION_CHOICES: &[Node] = &[Node::Punct('*'), SELECT_PROJ_LIST];
const SELECT_PROJECTION: Node = Node::Choice(SELECT_PROJECTION_CHOICES);
static SELECT_PROJECTION_CHOICES: &[Node] =
&[Node::Punct('*'), Node::Subgrammar(&SELECT_PROJ_LIST)];
static SELECT_PROJECTION: Node = Node::Choice(SELECT_PROJECTION_CHOICES);
/// The `FROM` table. `writes_table` so the `WHERE` / `ORDER BY`
/// expression column slots complete against this table; the
@@ -428,7 +435,7 @@ const SELECT_FROM_TABLE: Node = Node::Ident {
};
/// `where <sql_expr>`.
static SELECT_WHERE_NODES: &[Node] = &[Node::Word(Word::keyword("where")), SQL_EXPR];
static SELECT_WHERE_NODES: &[Node] = &[Node::Word(Word::keyword("where")), Node::Subgrammar(&sql_expr::SQL_OR_EXPR)];
static SELECT_WHERE: Node = Node::Seq(SELECT_WHERE_NODES);
/// `order by <item> ( , <item> )*`, each item a SQL expression
@@ -438,7 +445,7 @@ const SELECT_SORT_DIR_CHOICES: &[Node] = &[
Node::Word(Word::keyword("desc")),
];
static SELECT_ORDER_ITEM_NODES: &[Node] = &[
SQL_EXPR,
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
Node::Optional(&Node::Choice(SELECT_SORT_DIR_CHOICES)),
];
static SELECT_ORDER_ITEM: Node = Node::Seq(SELECT_ORDER_ITEM_NODES);
@@ -476,7 +483,7 @@ static SELECT_FROM_CLAUSE_NODES: &[Node] = &[
static SELECT_FROM_CLAUSE: Node = Node::Seq(SELECT_FROM_CLAUSE_NODES);
const SELECT_NODES: &[Node] = &[
SELECT_PROJECTION,
Node::Subgrammar(&SELECT_PROJECTION),
Node::Optional(&SELECT_FROM_CLAUSE),
Node::Optional(&SELECT_WHERE),
Node::Optional(&SELECT_ORDER_BY),