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:
+16
-9
@@ -373,8 +373,10 @@ const EXPLAIN_SHAPE: Node = Node::Choice(EXPLAIN_CHOICES);
|
|||||||
// column aliasing (`select a x`) and qualified `t.*` are out of
|
// column aliasing (`select a x`) and qualified `t.*` are out of
|
||||||
// Phase 1 (see the inline notes).
|
// Phase 1 (see the inline notes).
|
||||||
|
|
||||||
/// A SQL expression slot — the ADR-0031 fragment as one node.
|
// SQL expression slot — `Node::Subgrammar(&sql_expr::SQL_OR_EXPR)`
|
||||||
const SQL_EXPR: Node = 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
|
/// `as <alias>` — the explicit projection alias. Implicit
|
||||||
/// aliasing (`select a x`) is not supported: a bare alias is
|
/// 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.
|
/// A projection item: a SQL expression with an optional alias.
|
||||||
static SELECT_PROJ_ITEM_NODES: &[Node] = &[
|
static SELECT_PROJ_ITEM_NODES: &[Node] = &[
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
Node::Optional(&SELECT_AS_ALIAS),
|
Node::Optional(&SELECT_AS_ALIAS),
|
||||||
];
|
];
|
||||||
static SELECT_PROJ_ITEM: Node = Node::Seq(SELECT_PROJ_ITEM_NODES);
|
static SELECT_PROJ_ITEM: Node = Node::Seq(SELECT_PROJ_ITEM_NODES);
|
||||||
|
|
||||||
/// `proj_item ( , proj_item )*`.
|
/// `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,
|
inner: &SELECT_PROJ_ITEM,
|
||||||
separator: Some(&Node::Punct(',')),
|
separator: Some(&Node::Punct(',')),
|
||||||
min: 1,
|
min: 1,
|
||||||
@@ -410,8 +416,9 @@ const SELECT_PROJ_LIST: Node = Node::Repeated {
|
|||||||
|
|
||||||
/// `projection := '*' | proj_item ( , proj_item )*`. (`t.*`
|
/// `projection := '*' | proj_item ( , proj_item )*`. (`t.*`
|
||||||
/// qualified star is Phase 2 — it needs join scope.)
|
/// qualified star is Phase 2 — it needs join scope.)
|
||||||
const SELECT_PROJECTION_CHOICES: &[Node] = &[Node::Punct('*'), SELECT_PROJ_LIST];
|
static SELECT_PROJECTION_CHOICES: &[Node] =
|
||||||
const SELECT_PROJECTION: Node = Node::Choice(SELECT_PROJECTION_CHOICES);
|
&[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`
|
/// The `FROM` table. `writes_table` so the `WHERE` / `ORDER BY`
|
||||||
/// expression column slots complete against this table; the
|
/// expression column slots complete against this table; the
|
||||||
@@ -428,7 +435,7 @@ const SELECT_FROM_TABLE: Node = Node::Ident {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// `where <sql_expr>`.
|
/// `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);
|
static SELECT_WHERE: Node = Node::Seq(SELECT_WHERE_NODES);
|
||||||
|
|
||||||
/// `order by <item> ( , <item> )*`, each item a SQL expression
|
/// `order by <item> ( , <item> )*`, each item a SQL expression
|
||||||
@@ -438,7 +445,7 @@ const SELECT_SORT_DIR_CHOICES: &[Node] = &[
|
|||||||
Node::Word(Word::keyword("desc")),
|
Node::Word(Word::keyword("desc")),
|
||||||
];
|
];
|
||||||
static SELECT_ORDER_ITEM_NODES: &[Node] = &[
|
static SELECT_ORDER_ITEM_NODES: &[Node] = &[
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
Node::Optional(&Node::Choice(SELECT_SORT_DIR_CHOICES)),
|
Node::Optional(&Node::Choice(SELECT_SORT_DIR_CHOICES)),
|
||||||
];
|
];
|
||||||
static SELECT_ORDER_ITEM: Node = Node::Seq(SELECT_ORDER_ITEM_NODES);
|
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);
|
static SELECT_FROM_CLAUSE: Node = Node::Seq(SELECT_FROM_CLAUSE_NODES);
|
||||||
|
|
||||||
const SELECT_NODES: &[Node] = &[
|
const SELECT_NODES: &[Node] = &[
|
||||||
SELECT_PROJECTION,
|
Node::Subgrammar(&SELECT_PROJECTION),
|
||||||
Node::Optional(&SELECT_FROM_CLAUSE),
|
Node::Optional(&SELECT_FROM_CLAUSE),
|
||||||
Node::Optional(&SELECT_WHERE),
|
Node::Optional(&SELECT_WHERE),
|
||||||
Node::Optional(&SELECT_ORDER_BY),
|
Node::Optional(&SELECT_ORDER_BY),
|
||||||
|
|||||||
+156
-11
@@ -57,7 +57,7 @@
|
|||||||
//! validation, highlight, completion, and the no-left-recursion
|
//! validation, highlight, completion, and the no-left-recursion
|
||||||
//! guarantee; it simply has no tree to hand back.
|
//! 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
|
// Shared leaf nodes
|
||||||
@@ -206,16 +206,26 @@ static BETWEEN_FORM_NODES: &[Node] = &[
|
|||||||
Node::Subgrammar(&ADDITIVE),
|
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_ITEM: Node = Node::Subgrammar(&ADDITIVE);
|
||||||
static IN_FORM_NODES: &[Node] = &[
|
static IN_INSIDE_CHOICES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("in")),
|
Node::ScopedSubgrammar(&sql_select::SQL_SELECT_COMPOUND),
|
||||||
Node::Punct('('),
|
|
||||||
Node::Repeated {
|
Node::Repeated {
|
||||||
inner: &IN_ITEM,
|
inner: &IN_ITEM,
|
||||||
separator: Some(&COMMA),
|
separator: Some(&COMMA),
|
||||||
min: 1,
|
min: 1,
|
||||||
},
|
},
|
||||||
|
];
|
||||||
|
static IN_FORM_NODES: &[Node] = &[
|
||||||
|
Node::Word(Word::keyword("in")),
|
||||||
|
Node::Punct('('),
|
||||||
|
Node::Choice(IN_INSIDE_CHOICES),
|
||||||
Node::Punct(')'),
|
Node::Punct(')'),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -315,10 +325,32 @@ static UNARY: Node = Node::Choice(UNARY_CHOICES);
|
|||||||
// primary := literal | ( or_expr ) | case_expr | name_or_call
|
// 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] = &[
|
static PAREN_GROUP_NODES: &[Node] = &[
|
||||||
Node::Punct('('),
|
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(')'),
|
Node::Punct(')'),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -417,19 +449,45 @@ static CALL_TAIL_NODES: &[Node] = &[
|
|||||||
Node::Optional(&CALL_ARGS),
|
Node::Optional(&CALL_ARGS),
|
||||||
Node::Punct(')'),
|
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
|
/// `primary`. Keyword literals (`null` / `true` / `false`) and the
|
||||||
/// `CASE` keyword come before `name_or_call`, so they parse as
|
/// `CASE` / `EXISTS` keywords come before `name_or_call`, so they
|
||||||
/// what they are rather than as column references.
|
/// parse as what they are rather than as column references.
|
||||||
static PRIMARY_CHOICES: &[Node] = &[
|
static PRIMARY_CHOICES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("null")),
|
Node::Word(Word::keyword("null")),
|
||||||
Node::Word(Word::keyword("true")),
|
Node::Word(Word::keyword("true")),
|
||||||
Node::Word(Word::keyword("false")),
|
Node::Word(Word::keyword("false")),
|
||||||
Node::NumberLit { validator: None },
|
Node::NumberLit { validator: None },
|
||||||
Node::StringLit,
|
Node::StringLit,
|
||||||
|
Node::Seq(EXISTS_PRIMARY_NODES),
|
||||||
Node::Seq(PAREN_GROUP_NODES),
|
Node::Seq(PAREN_GROUP_NODES),
|
||||||
Node::Seq(CASE_NODES),
|
Node::Seq(CASE_NODES),
|
||||||
Node::Seq(NAME_OR_CALL_NODES),
|
Node::Seq(NAME_OR_CALL_NODES),
|
||||||
@@ -596,4 +654,91 @@ mod tests {
|
|||||||
let input = format!("{}1{}", "(".repeat(depth), ")".repeat(depth));
|
let input = format!("{}1{}", "(".repeat(depth), ")".repeat(depth));
|
||||||
assert!(!walks(&input), "pathological nesting must be rejected");
|
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))");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-78
@@ -106,17 +106,20 @@ const LPAREN: Node = Node::Punct('(');
|
|||||||
const RPAREN: Node = Node::Punct(')');
|
const RPAREN: Node = Node::Punct(')');
|
||||||
const SEMI: Node = Node::Punct(';');
|
const SEMI: Node = Node::Punct(';');
|
||||||
|
|
||||||
/// SQL expression slot — recursion into ADR-0031's fragment
|
// SQL expression slot — `Node::Subgrammar(&sql_expr::SQL_OR_EXPR)`
|
||||||
/// through `Node::Subgrammar`. Stays `Subgrammar` (not
|
// is inlined at each use site rather than aliased through a
|
||||||
/// `ScopedSubgrammar`) — `sql_expr` recursion is part of the
|
// named `const`. The `const SQL_EXPR: Node = …` form triggered
|
||||||
/// precedence ladder, not a new lexical scope (ADR-0032 §10.2).
|
// a Rust const-evaluation cycle through the sql_expr ⇄
|
||||||
const SQL_EXPR: Node = Node::Subgrammar(&sql_expr::SQL_OR_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
|
/// A node that never matches. Used as the "no" branch of
|
||||||
/// lookahead-driven disambiguation: an empty `Choice` walks to
|
/// lookahead-driven disambiguation: an empty `Choice` walks to
|
||||||
/// `NoMatch`, which `Optional` / `Choice` gracefully treat as
|
/// `NoMatch`, which `Optional` / `Choice` gracefully treat as
|
||||||
/// "skip" or "fall through to the next branch".
|
/// "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)
|
// Bare-alias dispatch (ADR-0032 §1)
|
||||||
@@ -168,10 +171,10 @@ fn projection_bare_alias_factory(
|
|||||||
Some(word)
|
Some(word)
|
||||||
if PROJECTION_FOLLOW_SET.iter().any(|k| *k == word) =>
|
if PROJECTION_FOLLOW_SET.iter().any(|k| *k == word) =>
|
||||||
{
|
{
|
||||||
EMPTY_NOMATCH
|
Node::Subgrammar(&EMPTY_NOMATCH)
|
||||||
}
|
}
|
||||||
Some(_) => BARE_ALIAS_IDENT,
|
Some(_) => BARE_ALIAS_IDENT,
|
||||||
None => EMPTY_NOMATCH,
|
None => Node::Subgrammar(&EMPTY_NOMATCH),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,10 +187,10 @@ fn table_source_bare_alias_factory(
|
|||||||
Some(word)
|
Some(word)
|
||||||
if TABLE_SOURCE_FOLLOW_SET.iter().any(|k| *k == word) =>
|
if TABLE_SOURCE_FOLLOW_SET.iter().any(|k| *k == word) =>
|
||||||
{
|
{
|
||||||
EMPTY_NOMATCH
|
Node::Subgrammar(&EMPTY_NOMATCH)
|
||||||
}
|
}
|
||||||
Some(_) => BARE_ALIAS_IDENT,
|
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")),
|
Node::Word(Word::keyword("as")),
|
||||||
BARE_ALIAS_IDENT,
|
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] = &[
|
static PROJECTION_ALIAS_CHOICES: &[Node] = &[
|
||||||
AS_ALIAS_EXPLICIT,
|
Node::Subgrammar(&AS_ALIAS_EXPLICIT),
|
||||||
Node::Lookahead(projection_bare_alias_factory),
|
Node::Lookahead(projection_bare_alias_factory),
|
||||||
];
|
];
|
||||||
const PROJECTION_ALIAS_CHOICE: Node = Node::Choice(PROJECTION_ALIAS_CHOICES);
|
static PROJECTION_ALIAS_CHOICE: Node = Node::Choice(PROJECTION_ALIAS_CHOICES);
|
||||||
const PROJECTION_ALIAS_OPTIONAL: Node =
|
static PROJECTION_ALIAS_OPTIONAL: Node =
|
||||||
Node::Optional(&PROJECTION_ALIAS_CHOICE);
|
Node::Optional(&PROJECTION_ALIAS_CHOICE);
|
||||||
|
|
||||||
static TABLE_SOURCE_ALIAS_CHOICES: &[Node] = &[
|
static TABLE_SOURCE_ALIAS_CHOICES: &[Node] = &[
|
||||||
AS_ALIAS_EXPLICIT,
|
Node::Subgrammar(&AS_ALIAS_EXPLICIT),
|
||||||
Node::Lookahead(table_source_bare_alias_factory),
|
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);
|
Node::Choice(TABLE_SOURCE_ALIAS_CHOICES);
|
||||||
const TABLE_SOURCE_ALIAS_OPTIONAL: Node =
|
static TABLE_SOURCE_ALIAS_OPTIONAL: Node =
|
||||||
Node::Optional(&TABLE_SOURCE_ALIAS_CHOICE);
|
Node::Optional(&TABLE_SOURCE_ALIAS_CHOICE);
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
@@ -247,13 +250,13 @@ static QUALIFIED_STAR_NODES: &[Node] = &[
|
|||||||
Node::Punct('.'),
|
Node::Punct('.'),
|
||||||
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] = &[
|
static PROJECTION_EXPR_ITEM_NODES: &[Node] = &[
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
PROJECTION_ALIAS_OPTIONAL,
|
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.
|
/// Dispatch one projection item via a 3-token lookahead.
|
||||||
///
|
///
|
||||||
@@ -280,16 +283,16 @@ fn projection_item_factory(
|
|||||||
if bytes.get(after_ident) == Some(&b'.') {
|
if bytes.get(after_ident) == Some(&b'.') {
|
||||||
let after_dot = skip_whitespace(source, after_ident + 1);
|
let after_dot = skip_whitespace(source, after_ident + 1);
|
||||||
if bytes.get(after_dot) == Some(&b'*') {
|
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,
|
inner: &PROJECTION_ITEM,
|
||||||
separator: Some(&COMMA),
|
separator: Some(&COMMA),
|
||||||
min: 1,
|
min: 1,
|
||||||
@@ -303,8 +306,8 @@ static DISTINCT_OR_ALL_CHOICES: &[Node] = &[
|
|||||||
Node::Word(Word::keyword("distinct")),
|
Node::Word(Word::keyword("distinct")),
|
||||||
Node::Word(Word::keyword("all")),
|
Node::Word(Word::keyword("all")),
|
||||||
];
|
];
|
||||||
const DISTINCT_OR_ALL_CHOICE: Node = Node::Choice(DISTINCT_OR_ALL_CHOICES);
|
static DISTINCT_OR_ALL_CHOICE: Node = Node::Choice(DISTINCT_OR_ALL_CHOICES);
|
||||||
const DISTINCT_OR_ALL_OPTIONAL: Node =
|
static DISTINCT_OR_ALL_OPTIONAL: Node =
|
||||||
Node::Optional(&DISTINCT_OR_ALL_CHOICE);
|
Node::Optional(&DISTINCT_OR_ALL_CHOICE);
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
@@ -323,9 +326,9 @@ const TABLE_NAME_IDENT: Node = Node::Ident {
|
|||||||
|
|
||||||
static TABLE_SOURCE_NODES: &[Node] = &[
|
static TABLE_SOURCE_NODES: &[Node] = &[
|
||||||
TABLE_NAME_IDENT,
|
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
|
// 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 JOIN_WORD: Node = Node::Word(Word::keyword("join"));
|
||||||
const ON_WORD: Node = Node::Word(Word::keyword("on"));
|
const ON_WORD: Node = Node::Word(Word::keyword("on"));
|
||||||
const OUTER_OPTIONAL: Node =
|
static OUTER_OPTIONAL: Node =
|
||||||
Node::Optional(&Node::Word(Word::keyword("outer")));
|
Node::Optional(&Node::Word(Word::keyword("outer")));
|
||||||
|
|
||||||
// `INNER JOIN` and bare `JOIN` are split into two Choice
|
// `INNER JOIN` and bare `JOIN` are split into two Choice
|
||||||
@@ -344,49 +347,49 @@ const OUTER_OPTIONAL: Node =
|
|||||||
static INNER_JOIN_NODES: &[Node] = &[
|
static INNER_JOIN_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("inner")),
|
Node::Word(Word::keyword("inner")),
|
||||||
JOIN_WORD,
|
JOIN_WORD,
|
||||||
TABLE_SOURCE,
|
Node::Subgrammar(&TABLE_SOURCE),
|
||||||
ON_WORD,
|
ON_WORD,
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
];
|
];
|
||||||
|
|
||||||
static BARE_JOIN_NODES: &[Node] = &[
|
static BARE_JOIN_NODES: &[Node] = &[
|
||||||
JOIN_WORD,
|
JOIN_WORD,
|
||||||
TABLE_SOURCE,
|
Node::Subgrammar(&TABLE_SOURCE),
|
||||||
ON_WORD,
|
ON_WORD,
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
];
|
];
|
||||||
|
|
||||||
static LEFT_JOIN_NODES: &[Node] = &[
|
static LEFT_JOIN_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("left")),
|
Node::Word(Word::keyword("left")),
|
||||||
OUTER_OPTIONAL,
|
Node::Subgrammar(&OUTER_OPTIONAL),
|
||||||
JOIN_WORD,
|
JOIN_WORD,
|
||||||
TABLE_SOURCE,
|
Node::Subgrammar(&TABLE_SOURCE),
|
||||||
ON_WORD,
|
ON_WORD,
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
];
|
];
|
||||||
|
|
||||||
static RIGHT_JOIN_NODES: &[Node] = &[
|
static RIGHT_JOIN_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("right")),
|
Node::Word(Word::keyword("right")),
|
||||||
OUTER_OPTIONAL,
|
Node::Subgrammar(&OUTER_OPTIONAL),
|
||||||
JOIN_WORD,
|
JOIN_WORD,
|
||||||
TABLE_SOURCE,
|
Node::Subgrammar(&TABLE_SOURCE),
|
||||||
ON_WORD,
|
ON_WORD,
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
];
|
];
|
||||||
|
|
||||||
static FULL_JOIN_NODES: &[Node] = &[
|
static FULL_JOIN_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("full")),
|
Node::Word(Word::keyword("full")),
|
||||||
OUTER_OPTIONAL,
|
Node::Subgrammar(&OUTER_OPTIONAL),
|
||||||
JOIN_WORD,
|
JOIN_WORD,
|
||||||
TABLE_SOURCE,
|
Node::Subgrammar(&TABLE_SOURCE),
|
||||||
ON_WORD,
|
ON_WORD,
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
];
|
];
|
||||||
|
|
||||||
static CROSS_JOIN_NODES: &[Node] = &[
|
static CROSS_JOIN_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("cross")),
|
Node::Word(Word::keyword("cross")),
|
||||||
JOIN_WORD,
|
JOIN_WORD,
|
||||||
TABLE_SOURCE,
|
Node::Subgrammar(&TABLE_SOURCE),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// JOIN flavour dispatch. Each branch has a distinct leading
|
/// 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(INNER_JOIN_NODES),
|
||||||
Node::Seq(BARE_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
|
// FROM / WHERE / GROUP BY / HAVING
|
||||||
@@ -409,37 +412,37 @@ const JOIN_CLAUSE: Node = Node::Choice(JOIN_CLAUSE_CHOICES);
|
|||||||
|
|
||||||
static FROM_CLAUSE_NODES: &[Node] = &[
|
static FROM_CLAUSE_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("from")),
|
Node::Word(Word::keyword("from")),
|
||||||
TABLE_SOURCE,
|
Node::Subgrammar(&TABLE_SOURCE),
|
||||||
Node::Repeated {
|
Node::Repeated {
|
||||||
inner: &JOIN_CLAUSE,
|
inner: &JOIN_CLAUSE,
|
||||||
separator: None,
|
separator: None,
|
||||||
min: 0,
|
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] = &[
|
static WHERE_CLAUSE_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("where")),
|
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] = &[
|
static GROUP_BY_CLAUSE_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("group")),
|
Node::Word(Word::keyword("group")),
|
||||||
Node::Word(Word::keyword("by")),
|
Node::Word(Word::keyword("by")),
|
||||||
Node::Repeated {
|
Node::Repeated {
|
||||||
inner: &SQL_EXPR,
|
inner: &Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
separator: Some(&COMMA),
|
separator: Some(&COMMA),
|
||||||
min: 1,
|
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] = &[
|
static HAVING_CLAUSE_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("having")),
|
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
|
// ORDER BY / LIMIT / OFFSET
|
||||||
@@ -449,12 +452,12 @@ static ASC_DESC_CHOICES: &[Node] = &[
|
|||||||
Node::Word(Word::keyword("asc")),
|
Node::Word(Word::keyword("asc")),
|
||||||
Node::Word(Word::keyword("desc")),
|
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] = &[
|
static ORDER_ITEM_NODES: &[Node] = &[
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
Node::Optional(&ASC_DESC_CHOICE),
|
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] = &[
|
static ORDER_BY_CLAUSE_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("order")),
|
Node::Word(Word::keyword("order")),
|
||||||
@@ -465,21 +468,21 @@ static ORDER_BY_CLAUSE_NODES: &[Node] = &[
|
|||||||
min: 1,
|
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] = &[
|
static OFFSET_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("offset")),
|
Node::Word(Word::keyword("offset")),
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
];
|
];
|
||||||
const OFFSET_SEQ: Node = Node::Seq(OFFSET_NODES);
|
static OFFSET_SEQ: Node = Node::Seq(OFFSET_NODES);
|
||||||
const OFFSET_OPTIONAL: Node = Node::Optional(&OFFSET_SEQ);
|
static OFFSET_OPTIONAL: Node = Node::Optional(&OFFSET_SEQ);
|
||||||
|
|
||||||
static LIMIT_CLAUSE_NODES: &[Node] = &[
|
static LIMIT_CLAUSE_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("limit")),
|
Node::Word(Word::keyword("limit")),
|
||||||
SQL_EXPR,
|
Node::Subgrammar(&sql_expr::SQL_OR_EXPR),
|
||||||
OFFSET_OPTIONAL,
|
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)
|
// 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] = &[
|
static SELECT_CORE_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("select")),
|
Node::Word(Word::keyword("select")),
|
||||||
DISTINCT_OR_ALL_OPTIONAL,
|
Node::Subgrammar(&DISTINCT_OR_ALL_OPTIONAL),
|
||||||
PROJECTION_LIST,
|
Node::Subgrammar(&PROJECTION_LIST),
|
||||||
Node::Optional(&FROM_CLAUSE),
|
Node::Optional(&FROM_CLAUSE),
|
||||||
Node::Optional(&WHERE_CLAUSE),
|
Node::Optional(&WHERE_CLAUSE),
|
||||||
Node::Optional(&GROUP_BY_CLAUSE),
|
Node::Optional(&GROUP_BY_CLAUSE),
|
||||||
Node::Optional(&HAVING_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
|
// compound_select
|
||||||
@@ -518,13 +521,14 @@ static SET_OP_CHOICES: &[Node] = &[
|
|||||||
Node::Word(Word::keyword("intersect")),
|
Node::Word(Word::keyword("intersect")),
|
||||||
Node::Word(Word::keyword("except")),
|
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];
|
static SET_OP_TAIL_NODES: &[Node] =
|
||||||
const SET_OP_TAIL: Node = Node::Seq(SET_OP_TAIL_NODES);
|
&[Node::Subgrammar(&SET_OP), Node::Subgrammar(&SELECT_CORE)];
|
||||||
|
static SET_OP_TAIL: Node = Node::Seq(SET_OP_TAIL_NODES);
|
||||||
|
|
||||||
static COMPOUND_SELECT_NODES: &[Node] = &[
|
static COMPOUND_SELECT_NODES: &[Node] = &[
|
||||||
SELECT_CORE,
|
Node::Subgrammar(&SELECT_CORE),
|
||||||
Node::Repeated {
|
Node::Repeated {
|
||||||
inner: &SET_OP_TAIL,
|
inner: &SET_OP_TAIL,
|
||||||
separator: None,
|
separator: None,
|
||||||
@@ -572,24 +576,28 @@ static CTE_COLUMN_LIST_NODES: &[Node] = &[
|
|||||||
},
|
},
|
||||||
RPAREN,
|
RPAREN,
|
||||||
];
|
];
|
||||||
const CTE_COLUMN_LIST_SEQ: Node = Node::Seq(CTE_COLUMN_LIST_NODES);
|
static CTE_COLUMN_LIST_SEQ: Node = Node::Seq(CTE_COLUMN_LIST_NODES);
|
||||||
const CTE_COLUMN_LIST_OPTIONAL: Node =
|
static CTE_COLUMN_LIST_OPTIONAL: Node =
|
||||||
Node::Optional(&CTE_COLUMN_LIST_SEQ);
|
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] = &[
|
static CTE_BODY_NODES: &[Node] = &[
|
||||||
LPAREN,
|
LPAREN,
|
||||||
Node::Subgrammar(&SQL_SELECT_COMPOUND),
|
Node::ScopedSubgrammar(&SQL_SELECT_COMPOUND),
|
||||||
RPAREN,
|
RPAREN,
|
||||||
];
|
];
|
||||||
const CTE_BODY: Node = Node::Seq(CTE_BODY_NODES);
|
static CTE_BODY: Node = Node::Seq(CTE_BODY_NODES);
|
||||||
|
|
||||||
static CTE_DEF_NODES: &[Node] = &[
|
static CTE_DEF_NODES: &[Node] = &[
|
||||||
CTE_NAME_IDENT,
|
CTE_NAME_IDENT,
|
||||||
CTE_COLUMN_LIST_OPTIONAL,
|
Node::Subgrammar(&CTE_COLUMN_LIST_OPTIONAL),
|
||||||
Node::Word(Word::keyword("as")),
|
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] = &[
|
static WITH_CLAUSE_NODES: &[Node] = &[
|
||||||
Node::Word(Word::keyword("with")),
|
Node::Word(Word::keyword("with")),
|
||||||
@@ -600,7 +608,7 @@ static WITH_CLAUSE_NODES: &[Node] = &[
|
|||||||
min: 1,
|
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
|
// select_statement — the fragment entry point
|
||||||
@@ -1168,4 +1176,60 @@ mod tests {
|
|||||||
"with x as (select 1) select * from x",
|
"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",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user