diff --git a/src/dsl/grammar/data.rs b/src/dsl/grammar/data.rs index 4d8109f..b3e6b60 100644 --- a/src/dsl/grammar/data.rs +++ b/src/dsl/grammar/data.rs @@ -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 ` — 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 `. -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 ( , )*`, 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), diff --git a/src/dsl/grammar/sql_expr.rs b/src/dsl/grammar/sql_expr.rs index d38d040..521e2ca 100644 --- a/src/dsl/grammar/sql_expr.rs +++ b/src/dsl/grammar/sql_expr.rs @@ -57,7 +57,7 @@ //! validation, highlight, completion, and the no-left-recursion //! guarantee; it simply has no tree to hand back. -use crate::dsl::grammar::{IdentSource, Node, Word}; +use crate::dsl::grammar::{IdentSource, Node, Word, sql_select}; // ================================================================= // Shared leaf nodes @@ -206,16 +206,26 @@ static BETWEEN_FORM_NODES: &[Node] = &[ Node::Subgrammar(&ADDITIVE), ]; -/// `IN ( additive [, additive]* )`. +/// `IN ( additive [, additive]* | compound_select )` — +/// ADR-0032 §6.2. The `IN (` prefix is factored; after the +/// opening paren a `Choice` dispatches between the +/// compound-select subquery and the comma-separated value +/// list. The first inside token disambiguates the same way the +/// scalar-subquery `primary` does. The subquery recurses +/// through `ScopedSubgrammar`. static IN_ITEM: Node = Node::Subgrammar(&ADDITIVE); -static IN_FORM_NODES: &[Node] = &[ - Node::Word(Word::keyword("in")), - Node::Punct('('), +static IN_INSIDE_CHOICES: &[Node] = &[ + Node::ScopedSubgrammar(&sql_select::SQL_SELECT_COMPOUND), Node::Repeated { inner: &IN_ITEM, separator: Some(&COMMA), min: 1, }, +]; +static IN_FORM_NODES: &[Node] = &[ + Node::Word(Word::keyword("in")), + Node::Punct('('), + Node::Choice(IN_INSIDE_CHOICES), Node::Punct(')'), ]; @@ -315,10 +325,32 @@ static UNARY: Node = Node::Choice(UNARY_CHOICES); // primary := literal | ( or_expr ) | case_expr | name_or_call // ================================================================= -/// `( or_expr )` — a parenthesised group is a whole expression. +/// `( or_expr )` and the scalar subquery `( compound_select )` +/// share a leading `(`. Per ADR-0032 §6.1, the `(` is matched +/// once and the inside is a `Choice` between +/// `compound_select` (the scalar subquery) and `or_expr` (the +/// parenthesised expression). The first inside token +/// disambiguates: `SELECT` or `WITH` → subquery; anything else → +/// expression. Subquery recursion goes through +/// `ScopedSubgrammar` to push a new lexical scope (§10.2). +static PAREN_INSIDE_CHOICES: &[Node] = &[ + Node::ScopedSubgrammar(&sql_select::SQL_SELECT_COMPOUND), + Node::Subgrammar(&SQL_OR_EXPR), +]; static PAREN_GROUP_NODES: &[Node] = &[ Node::Punct('('), - Node::Subgrammar(&SQL_OR_EXPR), + Node::Choice(PAREN_INSIDE_CHOICES), + Node::Punct(')'), +]; + +// ADR-0032 §6.3 — `EXISTS ( compound_select )` as a primary. +// `[ NOT ] EXISTS` falls out via the existing `not_expr := NOT +// not_expr` tier above `primary`, so only the bare `EXISTS` +// form lives here. +static EXISTS_PRIMARY_NODES: &[Node] = &[ + Node::Word(Word::keyword("exists")), + Node::Punct('('), + Node::ScopedSubgrammar(&sql_select::SQL_SELECT_COMPOUND), Node::Punct(')'), ]; @@ -417,19 +449,45 @@ static CALL_TAIL_NODES: &[Node] = &[ Node::Optional(&CALL_ARGS), Node::Punct(')'), ]; -static CALL_TAIL: Node = Node::Seq(CALL_TAIL_NODES); -static NAME_OR_CALL_NODES: &[Node] = &[EXPR_IDENT, Node::Optional(&CALL_TAIL)]; +// ADR-0032 §5 — qualified column reference. `name_or_call` gains +// a `.identifier` suffix as a Choice sibling of the function-call +// `( args )` tail. The leading identifier is matched once (no +// Choice branch begins with an identifier per ADR-0031 §1's +// factoring); the optional tail dispatches between the two +// suffixes by their first character (`.` vs `(`). +const QUALIFIED_REF_IDENT: Node = Node::Ident { + source: IdentSource::Columns, + role: "sql_expr_qualified_ref", + validator: None, + highlight_override: None, + writes_table: false, + writes_column: false, + writes_user_listed_column: false, +}; +static QUALIFIED_REF_TAIL_NODES: &[Node] = &[ + Node::Punct('.'), + QUALIFIED_REF_IDENT, +]; + +static NAME_OR_CALL_TAIL_CHOICES: &[Node] = &[ + Node::Seq(QUALIFIED_REF_TAIL_NODES), + Node::Seq(CALL_TAIL_NODES), +]; +static NAME_OR_CALL_TAIL: Node = Node::Choice(NAME_OR_CALL_TAIL_CHOICES); + +static NAME_OR_CALL_NODES: &[Node] = &[EXPR_IDENT, Node::Optional(&NAME_OR_CALL_TAIL)]; /// `primary`. Keyword literals (`null` / `true` / `false`) and the -/// `CASE` keyword come before `name_or_call`, so they parse as -/// what they are rather than as column references. +/// `CASE` / `EXISTS` keywords come before `name_or_call`, so they +/// parse as what they are rather than as column references. static PRIMARY_CHOICES: &[Node] = &[ Node::Word(Word::keyword("null")), Node::Word(Word::keyword("true")), Node::Word(Word::keyword("false")), Node::NumberLit { validator: None }, Node::StringLit, + Node::Seq(EXISTS_PRIMARY_NODES), Node::Seq(PAREN_GROUP_NODES), Node::Seq(CASE_NODES), Node::Seq(NAME_OR_CALL_NODES), @@ -596,4 +654,91 @@ mod tests { let input = format!("{}1{}", "(".repeat(depth), ")".repeat(depth)); assert!(!walks(&input), "pathological nesting must be rejected"); } + + // ---- ADR-0032 §5 additive: qualified column references ---- + + #[test] + fn qualified_ref_basic_shapes() { + good("t.c"); + good("t.c = 1"); + good("a.b + c.d"); + good("upper(t.name)"); + good("t.a is null"); + good("t.x like 'A%'"); + good("t.x between 1 and 10"); + good("t.x in (1, 2, 3)"); + } + + #[test] + fn qualified_ref_function_call_disambiguation() { + // The optional tail dispatches `.identifier` (qualified + // ref) vs `(args)` (function call) by first token — a + // bare ident remains a column ref. + good("foo(x)"); // function call + good("foo.bar"); // qualified ref + good("foo"); // bare ref + } + + #[test] + fn qualified_ref_not_admitted_as_function() { + // No schema.fn() form — qualified ref and call don't + // compose. `t.foo(x)` would only parse as `t.foo` + // followed by `(x)` — but `(x)` is not a valid + // continuation of an expression, so the walk fails. + bad("t.foo(x)"); + } + + // ---- ADR-0032 §6 additive: subquery expressions ---- + + #[test] + fn scalar_subquery_as_primary() { + good("(select 1)"); + good("x = (select y from t)"); + good("(select count(*) from t) > 100"); + good("upper((select name from t where id = 1))"); + } + + #[test] + fn scalar_subquery_dispatches_against_paren_group() { + // Both `(or_expr)` and `(SELECT …)` start with `(`. + // The ADR factors the `(` and the first inside token + // discriminates — `SELECT` → subquery, anything else + // → expression. (Per ADR-0032 §1 / §9, subqueries + // recurse through `SQL_SELECT_COMPOUND` which omits + // the outer `WITH` — so `(WITH …)` is NOT admitted as + // a scalar subquery; that form is only valid at + // statement top-level.) + good("(a + 1)"); + good("(select 1)"); + bad("(with x as (select 1) select * from x)"); + } + + #[test] + fn in_subquery_predicate() { + good("x in (select y from t)"); + good("x not in (select y from t)"); + good("x in (select y from t union select z from u)"); + } + + #[test] + fn in_value_list_still_works() { + // The existing IN-value-list form is preserved + // alongside the new IN-subquery form. + good("status in (1, 2, 3)"); + good("name not in ('a', 'b', 'c')"); + } + + #[test] + fn exists_primary() { + good("exists (select 1)"); + good("not exists (select 1)"); + good("exists (select 1 from t where x = 1)"); + good("exists (select * from t) and a > 0"); + } + + #[test] + fn subquery_recursion_through_compound() { + good("x in (select y from t where y in (select z from u))"); + good("exists (select 1 from t where exists (select 1 from u))"); + } } diff --git a/src/dsl/grammar/sql_select.rs b/src/dsl/grammar/sql_select.rs index 953c77f..5fc96d6 100644 --- a/src/dsl/grammar/sql_select.rs +++ b/src/dsl/grammar/sql_select.rs @@ -106,17 +106,20 @@ const LPAREN: Node = Node::Punct('('); const RPAREN: Node = Node::Punct(')'); const SEMI: Node = Node::Punct(';'); -/// SQL expression slot — recursion into ADR-0031's fragment -/// through `Node::Subgrammar`. Stays `Subgrammar` (not -/// `ScopedSubgrammar`) — `sql_expr` recursion is part of the -/// precedence ladder, not a new lexical scope (ADR-0032 §10.2). -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 rather than aliased through a +// named `const`. The `const SQL_EXPR: Node = …` form triggered +// a Rust const-evaluation cycle through the sql_expr ⇄ +// sql_select recursion (valid at link time, where statics +// resolve lazily, but not at const-eval). Stays as a plain +// `Subgrammar` — sql_expr recursion is part of the precedence +// ladder, not a new lexical scope (ADR-0032 §10.2). /// A node that never matches. Used as the "no" branch of /// lookahead-driven disambiguation: an empty `Choice` walks to /// `NoMatch`, which `Optional` / `Choice` gracefully treat as /// "skip" or "fall through to the next branch". -const EMPTY_NOMATCH: Node = Node::Choice(&[]); +static EMPTY_NOMATCH: Node = Node::Choice(&[]); // ================================================================= // Bare-alias dispatch (ADR-0032 §1) @@ -168,10 +171,10 @@ fn projection_bare_alias_factory( Some(word) if PROJECTION_FOLLOW_SET.iter().any(|k| *k == word) => { - EMPTY_NOMATCH + Node::Subgrammar(&EMPTY_NOMATCH) } Some(_) => BARE_ALIAS_IDENT, - None => EMPTY_NOMATCH, + None => Node::Subgrammar(&EMPTY_NOMATCH), } } @@ -184,10 +187,10 @@ fn table_source_bare_alias_factory( Some(word) if TABLE_SOURCE_FOLLOW_SET.iter().any(|k| *k == word) => { - EMPTY_NOMATCH + Node::Subgrammar(&EMPTY_NOMATCH) } Some(_) => BARE_ALIAS_IDENT, - None => EMPTY_NOMATCH, + None => Node::Subgrammar(&EMPTY_NOMATCH), } } @@ -209,23 +212,23 @@ static AS_ALIAS_NODES: &[Node] = &[ Node::Word(Word::keyword("as")), BARE_ALIAS_IDENT, ]; -const AS_ALIAS_EXPLICIT: Node = Node::Seq(AS_ALIAS_NODES); +static AS_ALIAS_EXPLICIT: Node = Node::Seq(AS_ALIAS_NODES); static PROJECTION_ALIAS_CHOICES: &[Node] = &[ - AS_ALIAS_EXPLICIT, + Node::Subgrammar(&AS_ALIAS_EXPLICIT), Node::Lookahead(projection_bare_alias_factory), ]; -const PROJECTION_ALIAS_CHOICE: Node = Node::Choice(PROJECTION_ALIAS_CHOICES); -const PROJECTION_ALIAS_OPTIONAL: Node = +static PROJECTION_ALIAS_CHOICE: Node = Node::Choice(PROJECTION_ALIAS_CHOICES); +static PROJECTION_ALIAS_OPTIONAL: Node = Node::Optional(&PROJECTION_ALIAS_CHOICE); static TABLE_SOURCE_ALIAS_CHOICES: &[Node] = &[ - AS_ALIAS_EXPLICIT, + Node::Subgrammar(&AS_ALIAS_EXPLICIT), Node::Lookahead(table_source_bare_alias_factory), ]; -const TABLE_SOURCE_ALIAS_CHOICE: Node = +static TABLE_SOURCE_ALIAS_CHOICE: Node = Node::Choice(TABLE_SOURCE_ALIAS_CHOICES); -const TABLE_SOURCE_ALIAS_OPTIONAL: Node = +static TABLE_SOURCE_ALIAS_OPTIONAL: Node = Node::Optional(&TABLE_SOURCE_ALIAS_CHOICE); // ================================================================= @@ -247,13 +250,13 @@ static QUALIFIED_STAR_NODES: &[Node] = &[ Node::Punct('.'), Node::Punct('*'), ]; -const QUALIFIED_STAR: Node = Node::Seq(QUALIFIED_STAR_NODES); +static QUALIFIED_STAR: Node = Node::Seq(QUALIFIED_STAR_NODES); static PROJECTION_EXPR_ITEM_NODES: &[Node] = &[ - SQL_EXPR, - PROJECTION_ALIAS_OPTIONAL, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), + Node::Subgrammar(&PROJECTION_ALIAS_OPTIONAL), ]; -const PROJECTION_EXPR_ITEM: Node = Node::Seq(PROJECTION_EXPR_ITEM_NODES); +static PROJECTION_EXPR_ITEM: Node = Node::Seq(PROJECTION_EXPR_ITEM_NODES); /// Dispatch one projection item via a 3-token lookahead. /// @@ -280,16 +283,16 @@ fn projection_item_factory( if bytes.get(after_ident) == Some(&b'.') { let after_dot = skip_whitespace(source, after_ident + 1); if bytes.get(after_dot) == Some(&b'*') { - return QUALIFIED_STAR; + return Node::Subgrammar(&QUALIFIED_STAR); } } } - PROJECTION_EXPR_ITEM + Node::Subgrammar(&PROJECTION_EXPR_ITEM) } -const PROJECTION_ITEM: Node = Node::Lookahead(projection_item_factory); +static PROJECTION_ITEM: Node = Node::Lookahead(projection_item_factory); -const PROJECTION_LIST: Node = Node::Repeated { +static PROJECTION_LIST: Node = Node::Repeated { inner: &PROJECTION_ITEM, separator: Some(&COMMA), min: 1, @@ -303,8 +306,8 @@ static DISTINCT_OR_ALL_CHOICES: &[Node] = &[ Node::Word(Word::keyword("distinct")), Node::Word(Word::keyword("all")), ]; -const DISTINCT_OR_ALL_CHOICE: Node = Node::Choice(DISTINCT_OR_ALL_CHOICES); -const DISTINCT_OR_ALL_OPTIONAL: Node = +static DISTINCT_OR_ALL_CHOICE: Node = Node::Choice(DISTINCT_OR_ALL_CHOICES); +static DISTINCT_OR_ALL_OPTIONAL: Node = Node::Optional(&DISTINCT_OR_ALL_CHOICE); // ================================================================= @@ -323,9 +326,9 @@ const TABLE_NAME_IDENT: Node = Node::Ident { static TABLE_SOURCE_NODES: &[Node] = &[ TABLE_NAME_IDENT, - TABLE_SOURCE_ALIAS_OPTIONAL, + Node::Subgrammar(&TABLE_SOURCE_ALIAS_OPTIONAL), ]; -const TABLE_SOURCE: Node = Node::Seq(TABLE_SOURCE_NODES); +static TABLE_SOURCE: Node = Node::Seq(TABLE_SOURCE_NODES); // ================================================================= // JOIN flavours @@ -333,7 +336,7 @@ const TABLE_SOURCE: Node = Node::Seq(TABLE_SOURCE_NODES); const JOIN_WORD: Node = Node::Word(Word::keyword("join")); const ON_WORD: Node = Node::Word(Word::keyword("on")); -const OUTER_OPTIONAL: Node = +static OUTER_OPTIONAL: Node = Node::Optional(&Node::Word(Word::keyword("outer"))); // `INNER JOIN` and bare `JOIN` are split into two Choice @@ -344,49 +347,49 @@ const OUTER_OPTIONAL: Node = static INNER_JOIN_NODES: &[Node] = &[ Node::Word(Word::keyword("inner")), JOIN_WORD, - TABLE_SOURCE, + Node::Subgrammar(&TABLE_SOURCE), ON_WORD, - SQL_EXPR, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), ]; static BARE_JOIN_NODES: &[Node] = &[ JOIN_WORD, - TABLE_SOURCE, + Node::Subgrammar(&TABLE_SOURCE), ON_WORD, - SQL_EXPR, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), ]; static LEFT_JOIN_NODES: &[Node] = &[ Node::Word(Word::keyword("left")), - OUTER_OPTIONAL, + Node::Subgrammar(&OUTER_OPTIONAL), JOIN_WORD, - TABLE_SOURCE, + Node::Subgrammar(&TABLE_SOURCE), ON_WORD, - SQL_EXPR, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), ]; static RIGHT_JOIN_NODES: &[Node] = &[ Node::Word(Word::keyword("right")), - OUTER_OPTIONAL, + Node::Subgrammar(&OUTER_OPTIONAL), JOIN_WORD, - TABLE_SOURCE, + Node::Subgrammar(&TABLE_SOURCE), ON_WORD, - SQL_EXPR, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), ]; static FULL_JOIN_NODES: &[Node] = &[ Node::Word(Word::keyword("full")), - OUTER_OPTIONAL, + Node::Subgrammar(&OUTER_OPTIONAL), JOIN_WORD, - TABLE_SOURCE, + Node::Subgrammar(&TABLE_SOURCE), ON_WORD, - SQL_EXPR, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), ]; static CROSS_JOIN_NODES: &[Node] = &[ Node::Word(Word::keyword("cross")), JOIN_WORD, - TABLE_SOURCE, + Node::Subgrammar(&TABLE_SOURCE), ]; /// JOIN flavour dispatch. Each branch has a distinct leading @@ -401,7 +404,7 @@ static JOIN_CLAUSE_CHOICES: &[Node] = &[ Node::Seq(INNER_JOIN_NODES), Node::Seq(BARE_JOIN_NODES), ]; -const JOIN_CLAUSE: Node = Node::Choice(JOIN_CLAUSE_CHOICES); +static JOIN_CLAUSE: Node = Node::Choice(JOIN_CLAUSE_CHOICES); // ================================================================= // FROM / WHERE / GROUP BY / HAVING @@ -409,37 +412,37 @@ const JOIN_CLAUSE: Node = Node::Choice(JOIN_CLAUSE_CHOICES); static FROM_CLAUSE_NODES: &[Node] = &[ Node::Word(Word::keyword("from")), - TABLE_SOURCE, + Node::Subgrammar(&TABLE_SOURCE), Node::Repeated { inner: &JOIN_CLAUSE, separator: None, min: 0, }, ]; -const FROM_CLAUSE: Node = Node::Seq(FROM_CLAUSE_NODES); +static FROM_CLAUSE: Node = Node::Seq(FROM_CLAUSE_NODES); static WHERE_CLAUSE_NODES: &[Node] = &[ Node::Word(Word::keyword("where")), - SQL_EXPR, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), ]; -const WHERE_CLAUSE: Node = Node::Seq(WHERE_CLAUSE_NODES); +static WHERE_CLAUSE: Node = Node::Seq(WHERE_CLAUSE_NODES); static GROUP_BY_CLAUSE_NODES: &[Node] = &[ Node::Word(Word::keyword("group")), Node::Word(Word::keyword("by")), Node::Repeated { - inner: &SQL_EXPR, + inner: &Node::Subgrammar(&sql_expr::SQL_OR_EXPR), separator: Some(&COMMA), min: 1, }, ]; -const GROUP_BY_CLAUSE: Node = Node::Seq(GROUP_BY_CLAUSE_NODES); +static GROUP_BY_CLAUSE: Node = Node::Seq(GROUP_BY_CLAUSE_NODES); static HAVING_CLAUSE_NODES: &[Node] = &[ Node::Word(Word::keyword("having")), - SQL_EXPR, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), ]; -const HAVING_CLAUSE: Node = Node::Seq(HAVING_CLAUSE_NODES); +static HAVING_CLAUSE: Node = Node::Seq(HAVING_CLAUSE_NODES); // ================================================================= // ORDER BY / LIMIT / OFFSET @@ -449,12 +452,12 @@ static ASC_DESC_CHOICES: &[Node] = &[ Node::Word(Word::keyword("asc")), Node::Word(Word::keyword("desc")), ]; -const ASC_DESC_CHOICE: Node = Node::Choice(ASC_DESC_CHOICES); +static ASC_DESC_CHOICE: Node = Node::Choice(ASC_DESC_CHOICES); static ORDER_ITEM_NODES: &[Node] = &[ - SQL_EXPR, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), Node::Optional(&ASC_DESC_CHOICE), ]; -const ORDER_ITEM: Node = Node::Seq(ORDER_ITEM_NODES); +static ORDER_ITEM: Node = Node::Seq(ORDER_ITEM_NODES); static ORDER_BY_CLAUSE_NODES: &[Node] = &[ Node::Word(Word::keyword("order")), @@ -465,21 +468,21 @@ static ORDER_BY_CLAUSE_NODES: &[Node] = &[ min: 1, }, ]; -const ORDER_BY_CLAUSE: Node = Node::Seq(ORDER_BY_CLAUSE_NODES); +static ORDER_BY_CLAUSE: Node = Node::Seq(ORDER_BY_CLAUSE_NODES); static OFFSET_NODES: &[Node] = &[ Node::Word(Word::keyword("offset")), - SQL_EXPR, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), ]; -const OFFSET_SEQ: Node = Node::Seq(OFFSET_NODES); -const OFFSET_OPTIONAL: Node = Node::Optional(&OFFSET_SEQ); +static OFFSET_SEQ: Node = Node::Seq(OFFSET_NODES); +static OFFSET_OPTIONAL: Node = Node::Optional(&OFFSET_SEQ); static LIMIT_CLAUSE_NODES: &[Node] = &[ Node::Word(Word::keyword("limit")), - SQL_EXPR, - OFFSET_OPTIONAL, + Node::Subgrammar(&sql_expr::SQL_OR_EXPR), + Node::Subgrammar(&OFFSET_OPTIONAL), ]; -const LIMIT_CLAUSE: Node = Node::Seq(LIMIT_CLAUSE_NODES); +static LIMIT_CLAUSE: Node = Node::Seq(LIMIT_CLAUSE_NODES); // ================================================================= // select_core (per-leg of a compound) @@ -487,14 +490,14 @@ const LIMIT_CLAUSE: Node = Node::Seq(LIMIT_CLAUSE_NODES); static SELECT_CORE_NODES: &[Node] = &[ Node::Word(Word::keyword("select")), - DISTINCT_OR_ALL_OPTIONAL, - PROJECTION_LIST, + Node::Subgrammar(&DISTINCT_OR_ALL_OPTIONAL), + Node::Subgrammar(&PROJECTION_LIST), Node::Optional(&FROM_CLAUSE), Node::Optional(&WHERE_CLAUSE), Node::Optional(&GROUP_BY_CLAUSE), Node::Optional(&HAVING_CLAUSE), ]; -const SELECT_CORE: Node = Node::Seq(SELECT_CORE_NODES); +static SELECT_CORE: Node = Node::Seq(SELECT_CORE_NODES); // ================================================================= // compound_select @@ -518,13 +521,14 @@ static SET_OP_CHOICES: &[Node] = &[ Node::Word(Word::keyword("intersect")), Node::Word(Word::keyword("except")), ]; -const SET_OP: Node = Node::Choice(SET_OP_CHOICES); +static SET_OP: Node = Node::Choice(SET_OP_CHOICES); -static SET_OP_TAIL_NODES: &[Node] = &[SET_OP, SELECT_CORE]; -const SET_OP_TAIL: Node = Node::Seq(SET_OP_TAIL_NODES); +static SET_OP_TAIL_NODES: &[Node] = + &[Node::Subgrammar(&SET_OP), Node::Subgrammar(&SELECT_CORE)]; +static SET_OP_TAIL: Node = Node::Seq(SET_OP_TAIL_NODES); static COMPOUND_SELECT_NODES: &[Node] = &[ - SELECT_CORE, + Node::Subgrammar(&SELECT_CORE), Node::Repeated { inner: &SET_OP_TAIL, separator: None, @@ -572,24 +576,28 @@ static CTE_COLUMN_LIST_NODES: &[Node] = &[ }, RPAREN, ]; -const CTE_COLUMN_LIST_SEQ: Node = Node::Seq(CTE_COLUMN_LIST_NODES); -const CTE_COLUMN_LIST_OPTIONAL: Node = +static CTE_COLUMN_LIST_SEQ: Node = Node::Seq(CTE_COLUMN_LIST_NODES); +static CTE_COLUMN_LIST_OPTIONAL: Node = Node::Optional(&CTE_COLUMN_LIST_SEQ); +// CTE body recursion pushes a fresh lexical scope frame (ADR- +// 0032 §4 / §10.2). Subqueries in `sql_expr.rs` do the same; +// the top-level statement's own COMPOUND embedding does not +// (it shares the implicit bottom frame). static CTE_BODY_NODES: &[Node] = &[ LPAREN, - Node::Subgrammar(&SQL_SELECT_COMPOUND), + Node::ScopedSubgrammar(&SQL_SELECT_COMPOUND), RPAREN, ]; -const CTE_BODY: Node = Node::Seq(CTE_BODY_NODES); +static CTE_BODY: Node = Node::Seq(CTE_BODY_NODES); static CTE_DEF_NODES: &[Node] = &[ CTE_NAME_IDENT, - CTE_COLUMN_LIST_OPTIONAL, + Node::Subgrammar(&CTE_COLUMN_LIST_OPTIONAL), Node::Word(Word::keyword("as")), - CTE_BODY, + Node::Subgrammar(&CTE_BODY), ]; -const CTE_DEF: Node = Node::Seq(CTE_DEF_NODES); +static CTE_DEF: Node = Node::Seq(CTE_DEF_NODES); static WITH_CLAUSE_NODES: &[Node] = &[ Node::Word(Word::keyword("with")), @@ -600,7 +608,7 @@ static WITH_CLAUSE_NODES: &[Node] = &[ min: 1, }, ]; -const WITH_CLAUSE: Node = Node::Seq(WITH_CLAUSE_NODES); +static WITH_CLAUSE: Node = Node::Seq(WITH_CLAUSE_NODES); // ================================================================= // select_statement — the fragment entry point @@ -1168,4 +1176,60 @@ mod tests { "with x as (select 1) select * from x", )); } + + // ---- ADR-0032 §5/§6 — subqueries and qualified refs in + // ---- statement-level positions (sql_expr extensions + // ---- recurse through SQL_SELECT_COMPOUND via + // ---- ScopedSubgrammar). + + #[test] + fn qualified_ref_in_where_clause() { + good("select * from t where t.id = 1"); + good("select * from a join b on a.id = b.id"); + good("select t.name from t where t.age > 18"); + } + + #[test] + fn scalar_subquery_in_where_clause() { + good("select * from t where x = (select y from u)"); + good("select * from t where x > (select count(*) from u)"); + } + + #[test] + fn in_subquery_in_where_clause() { + good("select * from t where id in (select user_id from orders)"); + good( + "select * from customers where id not in (select customer_id from blocklist)", + ); + } + + #[test] + fn exists_subquery_in_where_clause() { + good( + "select * from customers c where exists (select 1 from orders o where o.customer_id = c.id)", + ); + good("select * from t where not exists (select 1 from u)"); + } + + #[test] + fn nested_subqueries() { + good( + "select * from t where x in (select y from u where y in (select z from v))", + ); + } + + #[test] + fn subquery_in_projection() { + good("select (select max(price) from products) from t"); + good( + "select name, (select count(*) from orders where customer_id = c.id) from customers c", + ); + } + + #[test] + fn cte_body_references_qualified_columns() { + good( + "with x as (select t.name, t.age from t) select x.name from x", + ); + } }