//! The SQL expression grammar fragment (ADR-0031).
//!
//! This is the advanced-mode counterpart of `grammar::expr` (the
//! DSL `WHERE` grammar, ADR-0026). It fills every expression slot
//! in advanced-mode SQL — `WHERE`, `HAVING`, `SELECT` projections,
//! `CHECK`, `DEFAULT` — and is the *superset* of ADR-0026's
//! grammar: it adds arithmetic, string concatenation, function
//! calls, and `CASE` on top of the comparison / `LIKE` / `IN` /
//! `BETWEEN` / `IS NULL` predicate set.
//!
//! # One unified ladder
//!
//! ADR-0026's DSL grammar stratifies a *boolean* layer above a
//! *predicate* layer because the DSL forbids a boolean
//! sub-expression as a comparison operand. Standard SQL draws no
//! such line — a boolean is a value — so this grammar is a single
//! precedence ladder, loosest tier first:
//!
//! ```text
//! or_expr         := and_expr      ( OR  and_expr )*
//! and_expr        := not_expr      ( AND not_expr )*
//! not_expr        := NOT not_expr  |  predicate
//! predicate       := additive predicate_tail?
//! predicate_tail  := cmp_op additive
//!                  | [ NOT ] LIKE additive
//!                  | [ NOT ] BETWEEN additive AND additive
//!                  | [ NOT ] IN ( additive ( , additive )* )
//!                  | IS [ NOT ] NULL
//! additive        := multiplicative ( ( + | - | || ) multiplicative )*
//! multiplicative  := unary ( ( * | / | % ) unary )*
//! unary           := ( - | + ) unary  |  primary
//! primary         := literal | ( or_expr ) | case_expr | name_or_call
//! name_or_call    := identifier  [ '(' call_args? ')' ]
//! call_args       := '*'  |  DISTINCT arg_list  |  arg_list
//! case_expr       := CASE [ or_expr ] ( WHEN or_expr THEN or_expr )+
//!                         [ ELSE or_expr ] END
//! ```
//!
//! Stratification removes left recursion (every recursion is
//! guarded by a token) and encodes precedence in the layering.
//! Each tier transition, and each genuine recursion, goes through
//! [`Node::Subgrammar`] — a `Seq` / `Choice` embeds its children
//! by value and cannot close a cycle. The walker counts active
//! `Subgrammar` frames and refuses past `MAX_SUBGRAMMAR_DEPTH`
//! (ADR-0026 §1) — reused here unchanged.
//!
//! # No AST (ADR-0031 §2)
//!
//! Unlike `grammar::expr`, this fragment carries **no AST
//! builder**. Its consumers run or store SQL as text (ADR-0030
//! §4/§6), so there is no `Expr` to build. The fragment's output
//! is the three other walker products — accept/reject, the flat
//! `MatchedPath` of terminals (which drives highlighting,
//! completion, the expected-set, and hints), and, via those
//! terminals' `span`s, the source range a consumer slices when it
//! needs the expression as text. The grammar tier owns
//! validation, highlight, completion, and the no-left-recursion
//! guarantee; it simply has no tree to hand back.

use crate::dsl::grammar::{IdentSource, Node, Word, sql_select};

// =================================================================
// Shared leaf nodes
// =================================================================

/// `,` — the separator inside `IN ( … )` and a function-call
/// argument list.
static COMMA: Node = Node::Punct(',');

/// A column reference inside an expression. `IdentSource::Columns`
/// drives Tab completion against the statement's table(s) from the
/// same `SchemaCache` the DSL uses (ADR-0031 §5). The same slot
/// also begins a `name_or_call` function call — the grammar does
/// not decide which (ADR-0031 §1, §2), so the slot optimises for
/// the common case (a column) and a function name simply is not a
/// completion candidate. `const` so it can be embedded by value.
const EXPR_IDENT: Node = Node::Ident {
    source: IdentSource::Columns,
    role: "sql_expr_ident",
    validator: None,
    highlight_override: None,
    writes_table: false,
    writes_column: false,
    writes_user_listed_column: false,
    writes_table_alias: false,
    writes_cte_name: false,
    writes_projection_alias: false,
};

// =================================================================
// or_expr := and_expr ( OR and_expr )*  — the fragment entry point
// =================================================================

static OR_TAIL_NODES: &[Node] = &[Node::Word(Word::keyword("or")), Node::Subgrammar(&AND_EXPR)];
static OR_TAIL: Node = Node::Seq(OR_TAIL_NODES);
static SQL_OR_EXPR_NODES: &[Node] = &[
    Node::Subgrammar(&AND_EXPR),
    Node::Repeated {
        inner: &OR_TAIL,
        separator: None,
        min: 0,
    },
];
/// `or_expr` — the loosest tier and the grammar's entry point.
pub static SQL_OR_EXPR: Node = Node::Seq(SQL_OR_EXPR_NODES);

/// The SQL expression fragment, ready to drop into a SQL command
/// `Seq` as one node — the advanced-mode counterpart of
/// [`crate::dsl::grammar::expr::EXPRESSION`]. Walking it consumes a
/// whole `or_expr`.
pub static SQL_EXPRESSION: Node = Node::Subgrammar(&SQL_OR_EXPR);

// =================================================================
// and_expr := not_expr ( AND not_expr )*
// =================================================================

static AND_TAIL_NODES: &[Node] = &[
    Node::Word(Word::keyword("and")),
    Node::Subgrammar(&NOT_EXPR),
];
static AND_TAIL: Node = Node::Seq(AND_TAIL_NODES);
static AND_EXPR_NODES: &[Node] = &[
    Node::Subgrammar(&NOT_EXPR),
    Node::Repeated {
        inner: &AND_TAIL,
        separator: None,
        min: 0,
    },
];
static AND_EXPR: Node = Node::Seq(AND_EXPR_NODES);

// =================================================================
// not_expr := NOT not_expr | predicate
// =================================================================

static NOT_FORM_NODES: &[Node] = &[
    Node::Word(Word::keyword("not")),
    Node::Subgrammar(&NOT_EXPR),
];
static NOT_EXPR_CHOICES: &[Node] = &[Node::Seq(NOT_FORM_NODES), Node::Subgrammar(&PREDICATE)];
static NOT_EXPR: Node = Node::Choice(NOT_EXPR_CHOICES);

// =================================================================
// predicate := additive predicate_tail?
// =================================================================
//
// `predicate_tail` is optional: a bare `additive` with no
// comparison operator is itself a valid expression — that is what
// a `SELECT` projection item (`a + b`) or a literal `WHERE 1`
// needs. ADR-0026's DSL grammar made the tail mandatory because it
// forbade a bare column as a boolean; SQL does not.

static PREDICATE_NODES: &[Node] = &[Node::Subgrammar(&ADDITIVE), Node::Optional(&PREDICATE_TAIL)];
static PREDICATE: Node = Node::Seq(PREDICATE_NODES);

// ---- cmp_op := <= | <> | >= | != | < | > | = --------------------
//
// Two-character operators come before their one-character
// prefixes: `walk_literal` matches `<` against the `<` of `<=`,
// so `<=` / `<>` must be tried first (ADR-0026's note, inherited).

static CMP_OP_CHOICES: &[Node] = &[
    Node::Literal("<="),
    Node::Literal("<>"),
    Node::Literal(">="),
    Node::Literal("!="),
    Node::Literal("<"),
    Node::Literal(">"),
    Node::Literal("="),
];

// ---- predicate_tail branches ------------------------------------

/// `cmp_op additive`.
static COMPARE_FORM_NODES: &[Node] = &[Node::Choice(CMP_OP_CHOICES), Node::Subgrammar(&ADDITIVE)];

/// `IS [NOT] NULL`.
static IS_NULL_NODES: &[Node] = &[
    Node::Word(Word::keyword("is")),
    Node::Optional(&Node::Word(Word::keyword("not"))),
    Node::Word(Word::keyword("null")),
];

/// `LIKE additive`.
static LIKE_FORM_NODES: &[Node] = &[
    Node::Word(Word::keyword("like")),
    Node::Subgrammar(&ADDITIVE),
];

/// `BETWEEN additive AND additive`. The inner `and` is consumed
/// here, so a stray `and` at the `and_expr` tier is always a
/// connective.
static BETWEEN_FORM_NODES: &[Node] = &[
    Node::Word(Word::keyword("between")),
    Node::Subgrammar(&ADDITIVE),
    Node::Word(Word::keyword("and")),
    Node::Subgrammar(&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_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(')'),
];

/// The negatable predicate bodies — each starts with a distinct
/// keyword, so this `Choice` discriminates cleanly.
static NEGATABLE_CHOICES: &[Node] = &[
    Node::Seq(LIKE_FORM_NODES),
    Node::Seq(BETWEEN_FORM_NODES),
    Node::Seq(IN_FORM_NODES),
];

/// `NOT (LIKE … | BETWEEN … | IN …)` — the infix `NOT` is factored
/// in front of the negatable choice (ADR-0026's factoring).
static NOT_NEGATABLE_NODES: &[Node] = &[
    Node::Word(Word::keyword("not")),
    Node::Choice(NEGATABLE_CHOICES),
];

/// `predicate_tail`. Branch discrimination relies on each branch's
/// *first* child reporting a clean `NoMatch` on a non-match — no
/// branch starts with an `Optional` (ADR-0026's hazard). The infix
/// `NOT` is its own explicit `NOT negatable` branch.
static PREDICATE_TAIL_CHOICES: &[Node] = &[
    Node::Seq(COMPARE_FORM_NODES),
    Node::Seq(IS_NULL_NODES),
    Node::Seq(NOT_NEGATABLE_NODES),
    Node::Seq(LIKE_FORM_NODES),
    Node::Seq(BETWEEN_FORM_NODES),
    Node::Seq(IN_FORM_NODES),
];
static PREDICATE_TAIL: Node = Node::Choice(PREDICATE_TAIL_CHOICES);

// =================================================================
// additive := multiplicative ( ( + | - | || ) multiplicative )*
// =================================================================

static ADD_OP_CHOICES: &[Node] = &[Node::Punct('+'), Node::Punct('-'), Node::Literal("||")];
static ADD_TAIL_NODES: &[Node] = &[
    Node::Choice(ADD_OP_CHOICES),
    Node::Subgrammar(&MULTIPLICATIVE),
];
static ADD_TAIL: Node = Node::Seq(ADD_TAIL_NODES);
static ADDITIVE_NODES: &[Node] = &[
    Node::Subgrammar(&MULTIPLICATIVE),
    Node::Repeated {
        inner: &ADD_TAIL,
        separator: None,
        min: 0,
    },
];
static ADDITIVE: Node = Node::Seq(ADDITIVE_NODES);

// =================================================================
// multiplicative := unary ( ( * | / | % ) unary )*
// =================================================================

static MUL_OP_CHOICES: &[Node] = &[Node::Punct('*'), Node::Punct('/'), Node::Punct('%')];
static MUL_TAIL_NODES: &[Node] = &[Node::Choice(MUL_OP_CHOICES), Node::Subgrammar(&UNARY)];
static MUL_TAIL: Node = Node::Seq(MUL_TAIL_NODES);
static MULTIPLICATIVE_NODES: &[Node] = &[
    Node::Subgrammar(&UNARY),
    Node::Repeated {
        inner: &MUL_TAIL,
        separator: None,
        min: 0,
    },
];
static MULTIPLICATIVE: Node = Node::Seq(MULTIPLICATIVE_NODES);

// =================================================================
// unary := ( - | + ) unary | primary
// =================================================================

static SIGN_CHOICES: &[Node] = &[Node::Punct('-'), Node::Punct('+')];
static UNARY_SIGN_NODES: &[Node] = &[Node::Choice(SIGN_CHOICES), Node::Subgrammar(&UNARY)];
static UNARY_CHOICES: &[Node] = &[Node::Seq(UNARY_SIGN_NODES), Node::Subgrammar(&PRIMARY)];
static UNARY: Node = Node::Choice(UNARY_CHOICES);

// =================================================================
// primary := literal | ( or_expr ) | case_expr | name_or_call
// =================================================================

/// `( 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::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(')'),
];

// ---- case_expr --------------------------------------------------
//
// `CASE [operand] (WHEN cond THEN result)+ [ELSE result] END`.
// Searched-CASE (no operand) and simple-CASE (with operand) are two
// `Choice` branches *after* the shared `CASE` keyword is factored
// out: the searched branch opens with `WHEN`, the simple branch
// with an operand expression. Branch 1's leading `Repeated(min 1)`
// reports a clean `NoMatch` when the next token is not `WHEN`, so
// the `Choice` falls through to the simple branch cleanly.

static WHEN_CLAUSE_NODES: &[Node] = &[
    Node::Word(Word::keyword("when")),
    Node::Subgrammar(&SQL_OR_EXPR),
    Node::Word(Word::keyword("then")),
    Node::Subgrammar(&SQL_OR_EXPR),
];
static WHEN_CLAUSE: Node = Node::Seq(WHEN_CLAUSE_NODES);

static ELSE_CLAUSE_NODES: &[Node] = &[
    Node::Word(Word::keyword("else")),
    Node::Subgrammar(&SQL_OR_EXPR),
];
static ELSE_CLAUSE: Node = Node::Seq(ELSE_CLAUSE_NODES);

/// Searched-CASE body: `(WHEN … THEN …)+ [ELSE …] END`.
static SEARCHED_CASE_NODES: &[Node] = &[
    Node::Repeated {
        inner: &WHEN_CLAUSE,
        separator: None,
        min: 1,
    },
    Node::Optional(&ELSE_CLAUSE),
    Node::Word(Word::keyword("end")),
];
/// Simple-CASE body: `operand (WHEN … THEN …)+ [ELSE …] END`.
static SIMPLE_CASE_NODES: &[Node] = &[
    Node::Subgrammar(&SQL_OR_EXPR),
    Node::Repeated {
        inner: &WHEN_CLAUSE,
        separator: None,
        min: 1,
    },
    Node::Optional(&ELSE_CLAUSE),
    Node::Word(Word::keyword("end")),
];
static CASE_BODY_CHOICES: &[Node] = &[Node::Seq(SEARCHED_CASE_NODES), Node::Seq(SIMPLE_CASE_NODES)];
static CASE_NODES: &[Node] = &[
    Node::Word(Word::keyword("case")),
    Node::Choice(CASE_BODY_CHOICES),
];

// ---- name_or_call -----------------------------------------------
//
// `identifier [ '(' call_args? ')' ]`. The identifier is matched
// once; the `( call_args )` group is an `Optional` tail — present
// is a function call, absent is a column reference. Factoring this
// (rather than two `Choice` branches sharing the identifier
// prefix) avoids the function-call branch committing on the
// identifier and discarding the column-ref branch.

/// One function-call argument — a whole expression.
static CALL_ARG: Node = Node::Subgrammar(&SQL_OR_EXPR);

/// `call_args := '*' | DISTINCT arg_list | arg_list`. `count(*)`
/// is the one place `*` is an argument; `count(distinct col)` the
/// one place `DISTINCT` leads the list. The grammar admits the
/// call shape structurally — it does not know which names are
/// aggregates (ADR-0031 §1).
static DISTINCT_ARGS_NODES: &[Node] = &[
    Node::Word(Word::keyword("distinct")),
    Node::Repeated {
        inner: &CALL_ARG,
        separator: Some(&COMMA),
        min: 1,
    },
];
static CALL_ARGS_CHOICES: &[Node] = &[
    Node::Punct('*'),
    Node::Seq(DISTINCT_ARGS_NODES),
    Node::Repeated {
        inner: &CALL_ARG,
        separator: Some(&COMMA),
        min: 1,
    },
];
static CALL_ARGS: Node = Node::Choice(CALL_ARGS_CHOICES);

static CALL_TAIL_NODES: &[Node] = &[
    Node::Punct('('),
    Node::Optional(&CALL_ARGS),
    Node::Punct(')'),
];

// 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,
    writes_table_alias: false,
    writes_cte_name: false,
    writes_projection_alias: 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` / `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),
];
static PRIMARY: Node = Node::Choice(PRIMARY_CHOICES);

// =================================================================
// Tests
// =================================================================

#[cfg(test)]
mod tests {
    use super::SQL_OR_EXPR;
    use crate::dsl::walker::context::WalkContext;
    use crate::dsl::walker::driver::{NodeWalkResult, walk_node};
    use crate::dsl::walker::outcome::MatchedPath;

    /// Walk `input` against the SQL expression fragment. Returns
    /// `true` only when the walk matches *and* consumes all of
    /// `input` (trailing whitespace allowed).
    fn walks(input: &str) -> bool {
        let mut ctx = WalkContext::new();
        let mut path = MatchedPath::new();
        let mut per_byte = Vec::new();
        match walk_node(input, 0, &SQL_OR_EXPR, &mut ctx, &mut path, &mut per_byte) {
            NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(),
            _ => false,
        }
    }

    /// Assert `input` is a complete SQL expression.
    fn good(input: &str) {
        assert!(walks(input), "{input:?} should be a valid SQL expression");
    }

    /// Assert `input` is *not* a complete SQL expression.
    fn bad(input: &str) {
        assert!(
            !walks(input),
            "{input:?} should NOT walk as a complete expression"
        );
    }

    #[test]
    fn bare_operands_are_expressions() {
        // A projection item / `WHERE 1` — no predicate operator.
        for input in ["1", "col", "'text'", "true", "false", "null", "-7"] {
            good(input);
        }
    }

    #[test]
    fn every_comparison_operator() {
        for op in ["=", "!=", "<>", "<", "<=", ">", ">="] {
            good(&format!("a {op} 1"));
        }
    }

    #[test]
    fn arithmetic_and_precedence() {
        for input in [
            "a + b",
            "a - b",
            "a * b",
            "a / b",
            "a % b",
            "a + b * c",
            "(a + b) * c",
            "a + b - c + d",
            "-a + b",
            "a - -b",
            "- -a",
        ] {
            good(input);
        }
    }

    #[test]
    fn string_concatenation() {
        good("first || ' ' || last");
    }

    #[test]
    fn function_calls() {
        for input in [
            "upper(name)",
            "length(x)",
            "f()",
            "round(price, 2)",
            "count(*)",
            "count(distinct customer_id)",
            "coalesce(a, b, c)",
            "upper(lower(name))",
        ] {
            good(input);
        }
    }

    #[test]
    fn case_searched_and_simple() {
        good("case when a > 0 then 'pos' else 'neg' end");
        good("case when a > 0 then 1 when a < 0 then -1 else 0 end");
        good("case grade when 1 then 'A' when 2 then 'B' end");
        good("case status when 'open' then 1 else 0 end");
    }

    #[test]
    fn the_predicate_set() {
        good("name like 'A%'");
        good("name not like 'A%'");
        good("age between 18 and 65");
        good("age not between 0 and 17");
        good("status in (1, 2, 3)");
        good("status not in ('a', 'b')");
        good("email is null");
        good("email is not null");
    }

    #[test]
    fn boolean_connectives_and_precedence() {
        good("a = 1 and b = 2");
        good("a = 1 or b = 2");
        good("a = 1 or b = 2 and c = 3");
        good("not a = 1");
        good("not (a = 1 or b = 2)");
        good("(a = 1 or b = 2) and c = 3");
    }

    #[test]
    fn predicates_over_arithmetic_operands() {
        // The operands of a comparison are full scalar
        // expressions — the whole point of the superset.
        good("price * 1.1 > budget");
        good("upper(name) = 'ADA'");
        good("a + b between c and d");
    }

    #[test]
    fn keywords_are_case_insensitive() {
        good("A = 1 AND B IS NOT NULL");
        good("CASE WHEN x > 0 THEN 1 ELSE 0 END");
        good("name LIKE 'x%' OR age BETWEEN 1 AND 9");
    }

    #[test]
    fn nested_parentheses_walk() {
        good("((a = 1 and b = 2) or (c = 3))");
        good("(((1)))");
    }

    #[test]
    fn malformed_expressions_do_not_walk() {
        bad("a +"); // dangling operator
        bad("a in b"); // IN requires a parenthesised list
        bad("= 1"); // no left operand
        bad("a = "); // no right operand
        bad("case a end"); // CASE with no WHEN clause
        bad("and b"); // leading connective
        bad("upper("); // unclosed call
    }

    #[test]
    fn deeply_nested_parentheses_are_capped() {
        // Far past MAX_SUBGRAMMAR_DEPTH — must fail with a
        // friendly error rather than overflowing the stack.
        let depth = 400;
        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 …)` / `(WITH …)` start
        // with `(`. The ADR factors the `(` and the first inside
        // token discriminates — `SELECT` / `WITH` → subquery,
        // anything else → expression. Per ADR-0032 §10.3, a
        // subquery body may declare its own CTEs (shadowing
        // outer ones), so `(WITH …)` IS admitted as a scalar
        // subquery shape.
        good("(a + 1)");
        good("(select 1)");
        good("(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))");
    }
}
