Skip to main content

powdb_query/
parser.rs

1use crate::ast::*;
2use crate::lexer::lex;
3use crate::token::Token;
4
5/// Maximum nesting depth for the AST the parser produces.
6///
7/// Recursive descent (parentheses, subqueries, CASE) bumps `Parser::depth` on
8/// the way down, so the limit falls out of the call stack there. Binary,
9/// arithmetic and `having` chains are parsed ITERATIVELY, and a loop that
10/// re-wraps its accumulator (`left = BinaryOp(left, op, right)`) grows the AST
11/// one level per iteration without ever touching the call stack. Those loops
12/// therefore count their own iterations against this same limit (see
13/// [`Parser::check_chain_depth`]): what has to stay bounded is the SHAPE OF THE
14/// PRODUCED TREE, not just parser recursion. Every later walk of that tree
15/// (planner, canonicalizer, executor, and the recursive `Drop` of the boxed
16/// expression itself) is recursive, so an unbounded chain overflows the stack
17/// well after the parse returned, aborting the process under `panic = "abort"`.
18const MAX_NESTING_DEPTH: usize = 64;
19
20/// Discriminated parse error; callers can match on category.
21///
22/// Display strings are wire-visible behavior (the server's egress
23/// sanitization prefix-matches them). Every message is pinned byte-exact by
24/// `tests/error_display.rs`; do not reword one without updating that suite
25/// deliberately.
26#[derive(Debug, thiserror::Error)]
27pub enum ParseError {
28    /// Lexer failed to tokenize the input.
29    #[error("at position {position}: {message}")]
30    Lex { message: String, position: usize },
31    /// Expected one token but found another. `position` is the char offset
32    /// of the offending token (the lexer's `at position` coordinate system);
33    /// the parse boundary fills it in, so constructors pass `None`.
34    #[error("{}", positioned(*position, &format!("expected {expected}, got {got}")))]
35    UnexpectedToken {
36        expected: String,
37        got: String,
38        position: Option<usize>,
39    },
40    /// Recursive nesting exceeded the safety limit.
41    #[error("query nesting depth exceeds maximum of {max}")]
42    NestingDepthExceeded { max: usize },
43    /// Syntactically valid construct that the engine doesn't support yet.
44    #[error("{feature}")]
45    Unsupported { feature: String },
46    /// Catch-all for other syntax errors. `position` as in
47    /// [`ParseError::UnexpectedToken`].
48    #[error("{}", positioned(*position, message))]
49    Syntax {
50        message: String,
51        position: Option<usize>,
52    },
53}
54
55/// Render `body` with the lexer-style `at position N: ` prefix when the
56/// failing offset is known. One helper so the two positioned variants can
57/// never drift apart, and so the prefix stays byte-identical to `LexError`'s
58/// (it is an allowlisted wire prefix).
59fn positioned(position: Option<usize>, body: &str) -> String {
60    match position {
61        Some(p) => format!("at position {p}: {body}"),
62        None => body.to_string(),
63    }
64}
65
66/// A pipeline clause that carries a value may be written at most once. Before
67/// this check a second `filter` simply replaced the first, so half a predicate
68/// could go missing without a word; the same held for `order`, `limit`,
69/// `offset`, `group` and the projection block. One rule covers every such
70/// clause, in the read pipeline and in nested blocks alike. `having` is the
71/// deliberate exception: repeats there chain with `and`.
72fn refuse_repeated_clause<T>(slot: &Option<T>, what: &str) -> Result<(), ParseError> {
73    if slot.is_none() {
74        return Ok(());
75    }
76    Err(ParseError::Syntax {
77        message: format!(
78            "{what} appears more than once in one pipeline; each clause is written once, \
79             so combine them into one"
80        ),
81        position: None,
82    })
83}
84
85impl ParseError {
86    /// Convenience: human-readable message for any variant.
87    pub fn message(&self) -> String {
88        self.to_string()
89    }
90}
91
92fn token_to_scalar_fn(tok: &Token) -> ScalarFn {
93    match tok {
94        Token::Upper => ScalarFn::Upper,
95        Token::Lower => ScalarFn::Lower,
96        Token::Length => ScalarFn::Length,
97        Token::Trim => ScalarFn::Trim,
98        Token::Substring => ScalarFn::Substring,
99        Token::Concat => ScalarFn::Concat,
100        Token::Abs => ScalarFn::Abs,
101        Token::Round => ScalarFn::Round,
102        Token::Ceil => ScalarFn::Ceil,
103        Token::Floor => ScalarFn::Floor,
104        Token::Sqrt => ScalarFn::Sqrt,
105        Token::Pow => ScalarFn::Pow,
106        Token::Now => ScalarFn::Now,
107        Token::Extract => ScalarFn::Extract,
108        Token::DateAdd => ScalarFn::DateAdd,
109        Token::DateDiff => ScalarFn::DateDiff,
110        Token::JsonType => ScalarFn::JsonType,
111        Token::JsonText => ScalarFn::JsonText,
112        _ => unreachable!(),
113    }
114}
115
116struct Parser {
117    tokens: Vec<Token>,
118    pos: usize,
119    depth: usize,
120}
121
122/// Parse a PowQL query string into an AST [`Statement`].
123///
124/// # Examples
125///
126/// ```
127/// use powdb_query::parser::parse;
128/// use powdb_query::ast::Statement;
129///
130/// // A bare type name is a query (select all rows).
131/// let stmt = parse("User").unwrap();
132/// assert!(matches!(stmt, Statement::Query(_)));
133/// ```
134///
135/// ```
136/// use powdb_query::parser::parse;
137/// use powdb_query::ast::Statement;
138///
139/// // DDL: define a new type (table).
140/// let stmt = parse("type User { required name: str, age: int }").unwrap();
141/// assert!(matches!(stmt, Statement::CreateType(_)));
142/// ```
143pub fn parse(input: &str) -> Result<Statement, ParseError> {
144    let (tokens, spans) = crate::lexer::lex_with_spans(input).map_err(|e| ParseError::Lex {
145        message: e.message,
146        position: e.position,
147    })?;
148    parse_tokens_with_spans(tokens, Some(spans))
149}
150
151/// Parse PowQL with `$N` placeholders bound to positional `params`.
152///
153/// Binding happens at the **token level**: the input is lexed, each
154/// `$N` placeholder token is replaced in place with the literal token
155/// for `params[N-1]` (a string param becomes a `StringLit` byte-for-byte,
156/// `null` becomes `Token::Null`), and the resulting token stream is parsed
157/// normally. Values are never re-lexed or string-interpolated, so an
158/// injection-shaped string is inert data — it can never change the query's
159/// shape.
160///
161/// Placeholders are 1-based (`$1`, `$2`, …). A reference to a placeholder
162/// with no corresponding parameter is a clean [`ParseError::Syntax`], as is
163/// a non-numeric `$name` (the named-parameter form belongs to the in-process
164/// prepared API, not the positional wire-binding path).
165pub fn parse_with_params(input: &str, params: &[ParamValue]) -> Result<Statement, ParseError> {
166    // Placeholder substitution is 1:1 in place, so the spans stay aligned.
167    let (mut tokens, spans) = crate::lexer::lex_with_spans(input).map_err(|e| ParseError::Lex {
168        message: e.message,
169        position: e.position,
170    })?;
171    let mut referenced = vec![false; params.len()];
172    for tok in tokens.iter_mut() {
173        if let Token::Param(name) = tok {
174            let n: usize = name.parse().map_err(|_| ParseError::Syntax {
175                message: format!(
176                    "positional parameters must be numeric (`$1`, `$2`, …); got `${name}`"
177                ),
178                position: None,
179            })?;
180            if n == 0 {
181                return Err(ParseError::Syntax {
182                    message: "parameter placeholders are 1-based; `$0` is invalid".into(),
183                    position: None,
184                });
185            }
186            let p = params.get(n - 1).ok_or_else(|| ParseError::Syntax {
187                message: format!(
188                    "query references ${n} but only {} parameter(s) were supplied",
189                    params.len()
190                ),
191                position: None,
192            })?;
193            referenced[n - 1] = true;
194            *tok = match p {
195                ParamValue::Null => Token::Null,
196                ParamValue::Int(v) => Token::IntLit(*v),
197                ParamValue::Float(v) => Token::FloatLit(*v),
198                ParamValue::Bool(v) => Token::BoolLit(*v),
199                ParamValue::Str(s) => Token::StringLit(s.clone()),
200            };
201        }
202    }
203    // A supplied value the query never mentions was dropped in silence, which
204    // is the same failure an off-by-one in any positional API produces and
205    // leaves nothing in the result to show it. Too FEW parameters has always
206    // been an error; too many is the same mistake counted the other way.
207    if let Some(unused) = referenced.iter().position(|used| !used) {
208        let supplied = params.len();
209        let plural = if supplied == 1 { "" } else { "s" };
210        let message = match referenced.iter().rposition(|used| *used) {
211            None => format!("{supplied} parameter{plural} supplied but the query references none"),
212            Some(highest) if unused > highest => format!(
213                "{supplied} parameter{plural} supplied but the query references only ${}",
214                highest + 1
215            ),
216            Some(_) => format!(
217                "{supplied} parameter{plural} supplied but the query never references ${}",
218                unused + 1
219            ),
220        };
221        return Err(ParseError::Syntax {
222            message,
223            position: None,
224        });
225    }
226    parse_tokens_with_spans(tokens, Some(spans))
227}
228
229fn edit_distance(a: &str, b: &str) -> usize {
230    let mut prev: Vec<usize> = (0..=b.len()).collect();
231    let mut curr = vec![0; b.len() + 1];
232    for (i, ca) in a.bytes().enumerate() {
233        curr[0] = i + 1;
234        for (j, cb) in b.bytes().enumerate() {
235            let cost = usize::from(ca != cb);
236            curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
237        }
238        std::mem::swap(&mut prev, &mut curr);
239    }
240    prev[b.len()]
241}
242
243/// Keywords that can open a statement.
244const STATEMENT_KEYWORDS: &[&str] = &[
245    "alter", "begin", "commit", "delete", "drop", "explain", "insert", "refresh", "rollback",
246    "select", "type", "update", "upsert",
247];
248
249/// Keywords that can appear at a pipeline-stage boundary, plus the operators
250/// that join a filter expression. PowQL is a left-to-right pipeline language,
251/// so these are the words people actually mistype. Two-letter keywords (`in`,
252/// `is`, `on`, `or`) are deliberately left out: at that length almost any short
253/// identifier lands within the edit-distance bound.
254const PIPELINE_KEYWORDS: &[&str] = &[
255    "and",
256    "asc",
257    "between",
258    "conflict",
259    "cross",
260    "desc",
261    "distinct",
262    "exists",
263    "filter",
264    "group",
265    "having",
266    "inner",
267    "join",
268    "left",
269    "like",
270    "limit",
271    "match",
272    "not",
273    "offset",
274    "order",
275    "outer",
276    "returning",
277    "right",
278    "union",
279];
280
281/// Shortest word length that earns a distance-2 bound. Anything shorter is
282/// held to distance 1.
283///
284/// A distance of two on a six-letter word rewrites a third of it, and that is
285/// exactly where the collisions with ordinary English nouns live: `comment`
286/// reached `commit`, `owner` reached `order`, `alert` reached `alter`, `offer`
287/// reached `offset`. Table names are ordinary English nouns, so those all fired
288/// on real schemas. From eight characters up, a distance of two rewrites at
289/// most a quarter of the word, and the only keywords that long (`conflict`,
290/// `distinct`, `returning`, `rollback`) are the ones where an adjacent
291/// transposition, which costs two under plain Levenshtein, is the likeliest
292/// slip.
293const DISTANCE_2_MIN_LEN: usize = 8;
294
295/// Largest edit distance we will call a typo, scaled off whichever of the two
296/// words is shorter.
297///
298/// Scaling off the input alone let a long identifier reach a short keyword: it
299/// is the keyword's length that decides how much of it a two-edit rewrite
300/// destroys.
301fn suggestion_threshold(word: &str, keyword: &str) -> usize {
302    if word.len().min(keyword.len()) < DISTANCE_2_MIN_LEN {
303        1
304    } else {
305        2
306    }
307}
308
309/// Closest keyword to `word`, or `None` if nothing is close enough to be worth
310/// saying out loud. A wrong suggestion is worse than no suggestion, so this is
311/// deliberately stingy.
312fn closest_keyword<'a>(
313    word: &str,
314    candidates: impl Iterator<Item = &'a &'static str>,
315) -> Option<&'static str> {
316    let lower = word.to_ascii_lowercase();
317    let initial = lower.as_bytes().first().copied();
318    candidates
319        .copied()
320        .filter(|kw| {
321            // Sharing the first letter is a hard requirement, not a tie-break.
322            // People do not typo the first letter anywhere near as often as the
323            // middle of a word, and without this rule `watch` came back as
324            // `match`, `banner` as `inner`, and `rating` as `having`. It also
325            // subsumes the tie-break it replaced: `dsc` reads as `desc`
326            // because `asc` is no longer a candidate at all.
327            kw.as_bytes().first().copied() == initial
328        })
329        .filter(|kw| {
330            let distance = edit_distance(&lower, kw);
331            // Distance zero means the word already IS the keyword, which is
332            // not a typo. Quoting it (`` `filter` ``) used to answer "did you
333            // mean `filter`?".
334            distance >= 1 && distance <= suggestion_threshold(&lower, kw)
335        })
336        // Ties go to the earliest candidate, which favours pipeline keywords
337        // over statement keywords. At a pipeline-stage boundary that is the
338        // likelier intent.
339        .min_by_key(|kw| edit_distance(&lower, kw))
340}
341
342/// Every function name PowQL knows, for naming the closest one when a call
343/// names a function it does not have.
344const FUNCTION_NAMES: &[&str] = &[
345    "upper",
346    "lower",
347    "length",
348    "trim",
349    "substring",
350    "concat",
351    "abs",
352    "round",
353    "ceil",
354    "floor",
355    "sqrt",
356    "pow",
357    "now",
358    "extract",
359    "date_add",
360    "date_diff",
361    "json_type",
362    "json_text",
363    "count",
364    "sum",
365    "avg",
366    "min",
367    "max",
368    "row_number",
369    "rank",
370    "dense_rank",
371    "uuid",
372    "bytes",
373];
374
375/// Best function-name match for a call to a function PowQL does not have.
376fn closest_function(word: &str) -> Option<&'static str> {
377    closest_keyword(word, FUNCTION_NAMES.iter())
378}
379
380/// The one message a link path outside a projection gets, whichever way it was
381/// written. `p.author.name` and `.author.name` are the same mistake and reach
382/// two different parser productions, so the wording lives here rather than at
383/// either of them: the aliased spelling was fixed first and the unaliased one
384/// kept the old opaque "unexpected trailing token" for a release.
385fn link_traversal_outside_projection(path: &str) -> ParseError {
386    ParseError::Syntax {
387        message: format!(
388            "link traversal '{path}' is only supported in a projection; \
389             `filter`, `order` and `group` cannot traverse a link, so join \
390             the target type and use its own columns"
391        ),
392        position: None,
393    }
394}
395
396/// Best keyword match for an identifier that appeared where the parser
397/// expected syntax, drawn from every keyword that can legally appear there.
398fn keyword_suggestion(word: &str) -> Option<&'static str> {
399    closest_keyword(word, PIPELINE_KEYWORDS.iter().chain(STATEMENT_KEYWORDS))
400}
401
402/// Best statement-keyword match, used only for the first token of a statement.
403fn statement_keyword_suggestion(word: &str) -> Option<&'static str> {
404    closest_keyword(word, STATEMENT_KEYWORDS.iter())
405}
406
407/// Shared tail of [`parse`] / [`parse_with_params`]: run the recursive
408/// descent over an already-lexed (and possibly param-substituted) token
409/// stream and reject any trailing tokens.
410/// Run the recursive descent with the char offsets `lex_with_spans`
411/// recorded. On any
412/// [`ParseError::UnexpectedToken`] / [`ParseError::Syntax`] that a
413/// production raised without a position, the offset of the token the parser
414/// stopped on is attached here — one boundary, so no production has to
415/// thread spans and none can forget to. `None` (a synthesized token stream
416/// with no source text) leaves errors position-free, exactly as before.
417fn parse_tokens_with_spans(
418    tokens: Vec<Token>,
419    spans: Option<Vec<usize>>,
420) -> Result<Statement, ParseError> {
421    let mut parser = Parser {
422        tokens,
423        pos: 0,
424        depth: 0,
425    };
426    let stmt = match parser.parse_statement() {
427        Ok(stmt) => stmt,
428        Err(e) => return Err(attach_failing_position(e, &parser, spans.as_deref())),
429    };
430    // Reject trailing tokens. Without this, unrecognized tails like
431    // `User create_index .email` silently succeed as `User` — which
432    // misled the TS client into thinking those non-existent DDL forms
433    // returned rows. A parse error here tells users that the syntax
434    // they wrote isn't recognized.
435    if !matches!(parser.peek(), Token::Eof) {
436        let mut message = format!(
437            "unexpected trailing token near token {}: {}",
438            parser.pos,
439            parser.peek().display_name()
440        );
441        // Suggest for the token that actually failed. Reading the suggestion
442        // off `tokens.first()` instead made every query starting with the
443        // table name `User` answer "did you mean `upsert`?", whatever the
444        // real mistake was. The first token is worth a second look only when
445        // it is itself a mistyped statement keyword (`updat User set age = 1`),
446        // and then only against the statement keywords.
447        let offending = match parser.peek() {
448            Token::Ident(word) => {
449                keyword_suggestion(word).map(|kw| format!("; did you mean `{kw}`?"))
450            }
451            _ => None,
452        };
453        let suggestion = offending.or_else(|| {
454            // This one is about the first token, not the one named above, so
455            // say which word it read. Otherwise `Comment 42` reported
456            // "number 42; did you mean `commit`?" and left the reader to guess
457            // which of the two words the advice was about.
458            let Some(Token::Ident(first)) = parser.tokens.first() else {
459                return None;
460            };
461            let kw = statement_keyword_suggestion(first)?;
462            Some(format!(
463                "; `{first}` is not a statement keyword, did you mean `{kw}`?"
464            ))
465        });
466        if let Some(suggestion) = suggestion {
467            message.push_str(&suggestion);
468        }
469        return Err(attach_failing_position(
470            ParseError::Syntax {
471                message,
472                position: None,
473            },
474            &parser,
475            spans.as_deref(),
476        ));
477    }
478    Ok(stmt)
479}
480
481/// Attach the char offset of the token the parser stopped on to an error
482/// that carries none. Productions never advance past their failure, so
483/// `parser.pos` IS the failure site; a deliberate position set inside a
484/// production wins.
485fn attach_failing_position(
486    error: ParseError,
487    parser: &Parser,
488    spans: Option<&[usize]>,
489) -> ParseError {
490    let Some(spans) = spans else { return error };
491    let offset = spans
492        .get(parser.pos.min(spans.len().saturating_sub(1)))
493        .copied();
494    match error {
495        ParseError::UnexpectedToken {
496            expected,
497            got,
498            position: None,
499        } => ParseError::UnexpectedToken {
500            expected,
501            got,
502            position: offset,
503        },
504        ParseError::Syntax {
505            message,
506            position: None,
507        } => ParseError::Syntax {
508            message,
509            position: offset,
510        },
511        other => other,
512    }
513}
514
515/// Rewrite `Field(alias)` references inside `expr` to the underlying
516/// projection expression they alias. Used to desugar post-projection HAVING
517/// (`{ ..., cnt: count(.name) } having cnt >= 2`) into a form the planner's
518/// aggregate extraction can handle.
519fn substitute_projection_aliases(expr: Expr, fields: &[ProjectionField]) -> Expr {
520    match expr {
521        Expr::Field(ref name) => {
522            for f in fields {
523                if f.alias.as_deref() == Some(name.as_str()) {
524                    return f.expr.clone();
525                }
526            }
527            expr
528        }
529        Expr::BinaryOp(l, op, r) => Expr::BinaryOp(
530            Box::new(substitute_projection_aliases(*l, fields)),
531            op,
532            Box::new(substitute_projection_aliases(*r, fields)),
533        ),
534        Expr::UnaryOp(op, inner) => {
535            Expr::UnaryOp(op, Box::new(substitute_projection_aliases(*inner, fields)))
536        }
537        Expr::Coalesce(l, r) => Expr::Coalesce(
538            Box::new(substitute_projection_aliases(*l, fields)),
539            Box::new(substitute_projection_aliases(*r, fields)),
540        ),
541        Expr::InList {
542            expr: e,
543            list,
544            negated,
545        } => Expr::InList {
546            expr: Box::new(substitute_projection_aliases(*e, fields)),
547            list: list
548                .into_iter()
549                .map(|i| substitute_projection_aliases(i, fields))
550                .collect(),
551            negated,
552        },
553        Expr::ScalarFunc(f, args) => Expr::ScalarFunc(
554            f,
555            args.into_iter()
556                .map(|a| substitute_projection_aliases(a, fields))
557                .collect(),
558        ),
559        other => other,
560    }
561}
562
563impl Parser {
564    fn peek(&self) -> &Token {
565        &self.tokens[self.pos]
566    }
567
568    fn advance(&mut self) -> Token {
569        let t = self.tokens[self.pos].clone();
570        self.pos += 1;
571        t
572    }
573
574    fn expect(&mut self, expected: &Token) -> Result<(), ParseError> {
575        let t = self.advance();
576        if &t == expected {
577            Ok(())
578        } else {
579            Err(ParseError::UnexpectedToken {
580                expected: expected.display_name(),
581                got: t.display_name(),
582                position: None,
583            })
584        }
585    }
586
587    /// Convenience: create an UnexpectedToken error.
588    fn unexpected(&self, expected: &str, got: &Token) -> ParseError {
589        ParseError::UnexpectedToken {
590            expected: expected.into(),
591            got: got.display_name(),
592            position: None,
593        }
594    }
595
596    /// Consume an identifier in a position that names something (a field,
597    /// column, …). When the caller wrote a reserved word instead, the error
598    /// says so and points at the backtick-quoting escape hatch, rather than
599    /// the opaque `expected field name, got 'type'`.
600    fn expect_named_ident(&mut self, context: &str) -> Result<String, ParseError> {
601        match self.advance() {
602            Token::Ident(n) => Ok(n),
603            t => Err(self.named_ident_error(context, &t)),
604        }
605    }
606
607    /// Build the error for a reserved word (or other token) appearing where an
608    /// identifier was required. `context` is the noun ("field name", "column
609    /// name") spliced into the message.
610    fn named_ident_error(&self, context: &str, got: &Token) -> ParseError {
611        if let Some(kw) = got.keyword_str() {
612            ParseError::Syntax {
613                // "syntax error" prefix keeps the message on the server's
614                // safe-to-forward allowlist (SAFE_ERROR_PREFIXES) so wire
615                // clients see the guidance instead of the generic mask.
616                message: format!(
617                    "syntax error: '{kw}' is a reserved word and cannot be used as a {context}; \
618                     rename it or quote it as `{kw}`"
619                ),
620                position: None,
621            }
622        } else {
623            ParseError::UnexpectedToken {
624                expected: context.into(),
625                got: got.display_name(),
626                position: None,
627            }
628        }
629    }
630
631    /// Consume an optional `if not exists` clause. `if` is not a keyword token
632    /// (it lexes as an identifier), so match it by spelling.
633    fn parse_optional_if_not_exists(&mut self) -> bool {
634        if matches!(self.peek(), Token::Ident(w) if w == "if")
635            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
636            && matches!(self.tokens.get(self.pos + 2), Some(Token::Exists))
637        {
638            self.pos += 3;
639            true
640        } else {
641            false
642        }
643    }
644
645    /// Consume an optional `if exists` clause.
646    fn parse_optional_if_exists(&mut self) -> bool {
647        if matches!(self.peek(), Token::Ident(w) if w == "if")
648            && matches!(self.tokens.get(self.pos + 1), Some(Token::Exists))
649        {
650            self.pos += 2;
651            true
652        } else {
653            false
654        }
655    }
656
657    fn parse_statement(&mut self) -> Result<Statement, ParseError> {
658        self.depth += 1;
659        if self.depth > MAX_NESTING_DEPTH {
660            self.depth -= 1;
661            return Err(ParseError::NestingDepthExceeded {
662                max: MAX_NESTING_DEPTH,
663            });
664        }
665        if matches!(self.peek(), Token::Explain) {
666            self.advance();
667            let inner = self.parse_statement()?;
668            self.depth -= 1;
669            return Ok(Statement::Explain(Box::new(inner)));
670        }
671        let stmt = match self.peek() {
672            Token::Insert => self.parse_insert(),
673            Token::Upsert => self.parse_upsert(),
674            Token::Type => self.parse_create_type(),
675            Token::Link => self.parse_create_link(),
676            Token::Alter => self.parse_alter_table(),
677            Token::Drop => self.parse_drop_or_drop_view(),
678            Token::Materialized => self.parse_create_view(),
679            Token::Refresh => self.parse_refresh_view(),
680            Token::Begin => {
681                self.advance();
682                // Optional `transaction` keyword after `begin`.
683                if *self.peek() == Token::Transaction {
684                    self.advance();
685                }
686                return Ok(Statement::Begin);
687            }
688            Token::Commit => {
689                self.advance();
690                return Ok(Statement::Commit);
691            }
692            Token::Rollback => {
693                self.advance();
694                return Ok(Statement::Rollback);
695            }
696            Token::Schema => self.parse_schema(),
697            Token::Describe => self.parse_describe(),
698            Token::Count | Token::Avg | Token::Sum | Token::Min | Token::Max => {
699                self.parse_aggregate_query()
700            }
701            Token::Ident(_) => self.parse_query_or_mutation(),
702            Token::Update => Err(ParseError::Syntax {
703                message: "'update' cannot start a statement — in PowQL, use pipeline syntax: \
704                    TableName filter ... update { ... }"
705                    .into(),
706                position: None,
707            }),
708            Token::Delete => Err(ParseError::Syntax {
709                message: "'delete' cannot start a statement — in PowQL, use pipeline syntax: \
710                    TableName filter ... delete"
711                    .into(),
712                position: None,
713            }),
714            _ => Err(self.unexpected("statement", self.peek())),
715        }?;
716        // Check for UNION chaining after any query-producing statement.
717        let result = self.maybe_parse_union(stmt);
718        self.depth -= 1;
719        result
720    }
721
722    fn parse_query_or_mutation(&mut self) -> Result<Statement, ParseError> {
723        let source = match self.advance() {
724            Token::Ident(name) => name,
725            t => {
726                return Err(ParseError::UnexpectedToken {
727                    expected: "type name".into(),
728                    got: t.display_name(),
729                    position: None,
730                })
731            }
732        };
733        let alias = self.try_parse_alias();
734        let joins = self.parse_joins()?;
735
736        // Walk filter/order/limit/offset/projection, peeling off update/delete
737        // mutations as we hit them. Anything else terminates the read pipeline
738        // and we return a Query.
739        let mut filter = None;
740        let mut order = None;
741        let mut limit = None;
742        let mut offset = None;
743        let mut projection = None;
744        let mut distinct = false;
745        let mut group_by = None;
746        // Repeated `having` clauses stack `And` nodes onto one predicate, so
747        // the pipeline loop grows the AST the same way a binary chain does and
748        // needs the same bound.
749        let mut having_chain = 0usize;
750
751        loop {
752            match self.peek() {
753                Token::Distinct => {
754                    self.advance();
755                    distinct = true;
756                }
757                Token::Group => {
758                    refuse_repeated_clause(&group_by, "'group'")?;
759                    self.advance();
760                    group_by = Some(self.parse_group_by()?);
761                }
762                Token::Filter => {
763                    refuse_repeated_clause(&filter, "'filter'")?;
764                    self.advance();
765                    filter = Some(self.parse_expr()?);
766                }
767                Token::Order => {
768                    refuse_repeated_clause(&order, "'order'")?;
769                    self.advance();
770                    order = Some(self.parse_order()?);
771                }
772                Token::Limit => {
773                    refuse_repeated_clause(&limit, "'limit'")?;
774                    self.advance();
775                    limit = Some(self.parse_expr()?);
776                }
777                Token::Offset => {
778                    refuse_repeated_clause(&offset, "'offset'")?;
779                    self.advance();
780                    offset = Some(self.parse_expr()?);
781                }
782                Token::LBrace => {
783                    refuse_repeated_clause(&projection, "a projection")?;
784                    projection = Some(self.parse_projection()?);
785                }
786                Token::Having => {
787                    // Post-projection HAVING — see parse_query_tail for details.
788                    self.advance();
789                    let having_expr = self.parse_expr()?;
790                    let group = group_by.as_mut().ok_or_else(|| ParseError::Syntax {
791                        message: "having without group by".into(),
792                        position: None,
793                    })?;
794                    let rewritten = match projection.as_ref() {
795                        Some(fields) => substitute_projection_aliases(having_expr, fields),
796                        None => having_expr,
797                    };
798                    group.having = Some(match group.having.take() {
799                        Some(existing) => {
800                            having_chain += 1;
801                            self.check_chain_depth(having_chain)?;
802                            Expr::BinaryOp(Box::new(existing), BinOp::And, Box::new(rewritten))
803                        }
804                        None => rewritten,
805                    });
806                }
807                Token::Update => {
808                    if !joins.is_empty() {
809                        return Err(ParseError::Unsupported {
810                            feature: "update on a joined query is not supported".into(),
811                        });
812                    }
813                    self.advance();
814                    let assignments = self.parse_assignments()?;
815                    // Optional trailing `returning` — return the post-update rows.
816                    let returning = *self.peek() == Token::Returning;
817                    if returning {
818                        self.advance();
819                    }
820                    return Ok(Statement::UpdateQuery(UpdateExpr {
821                        source,
822                        alias,
823                        filter,
824                        assignments,
825                        returning,
826                    }));
827                }
828                Token::Delete => {
829                    if !joins.is_empty() {
830                        return Err(ParseError::Unsupported {
831                            feature: "delete on a joined query is not supported".into(),
832                        });
833                    }
834                    self.advance();
835                    // Optional trailing `returning` — return the pre-delete rows.
836                    let returning = *self.peek() == Token::Returning;
837                    if returning {
838                        self.advance();
839                    }
840                    return Ok(Statement::DeleteQuery(DeleteExpr {
841                        source,
842                        alias,
843                        filter,
844                        returning,
845                    }));
846                }
847                _ => break,
848            }
849        }
850
851        Ok(Statement::Query(QueryExpr {
852            source,
853            alias,
854            joins,
855            filter,
856            order,
857            limit,
858            offset,
859            projection,
860            aggregation: None,
861            distinct,
862            group_by,
863        }))
864    }
865
866    /// Parse the read-only tail of a query (filter/order/limit/offset/projection)
867    /// after `source` has already been consumed. Stops at the first token that
868    /// isn't part of a read pipeline — the caller decides whether that's a
869    /// terminator (RParen for an aggregate, EOF for a top-level query, etc.).
870    /// Always returns `aggregation: None`; the caller layers that on.
871    fn parse_query_tail(&mut self, source: String) -> Result<QueryExpr, ParseError> {
872        let alias = self.try_parse_alias();
873        let joins = self.parse_joins()?;
874        let mut filter = None;
875        let mut order = None;
876        let mut limit = None;
877        let mut offset = None;
878        let mut projection = None;
879        let mut distinct = false;
880        let mut group_by = None;
881        // Repeated `having` clauses stack `And` nodes onto one predicate, so
882        // the pipeline loop grows the AST the same way a binary chain does and
883        // needs the same bound.
884        let mut having_chain = 0usize;
885
886        loop {
887            match self.peek() {
888                Token::Distinct => {
889                    self.advance();
890                    distinct = true;
891                }
892                Token::Group => {
893                    refuse_repeated_clause(&group_by, "'group'")?;
894                    self.advance();
895                    group_by = Some(self.parse_group_by()?);
896                }
897                Token::Filter => {
898                    refuse_repeated_clause(&filter, "'filter'")?;
899                    self.advance();
900                    filter = Some(self.parse_expr()?);
901                }
902                Token::Order => {
903                    refuse_repeated_clause(&order, "'order'")?;
904                    self.advance();
905                    order = Some(self.parse_order()?);
906                }
907                Token::Limit => {
908                    refuse_repeated_clause(&limit, "'limit'")?;
909                    self.advance();
910                    limit = Some(self.parse_expr()?);
911                }
912                Token::Offset => {
913                    refuse_repeated_clause(&offset, "'offset'")?;
914                    self.advance();
915                    offset = Some(self.parse_expr()?);
916                }
917                Token::LBrace => {
918                    refuse_repeated_clause(&projection, "a projection")?;
919                    projection = Some(self.parse_projection()?);
920                }
921                Token::Having => {
922                    // Post-projection HAVING — `... group .k { .k, cnt: count(.name) } having cnt >= 2`.
923                    // Only meaningful when a GROUP BY is present. We desugar
924                    // to a regular HAVING on the GroupByClause, rewriting
925                    // projection aliases back into their underlying expressions
926                    // so the planner's extract_aggregates can dedup them.
927                    self.advance();
928                    let having_expr = self.parse_expr()?;
929                    let group = group_by.as_mut().ok_or_else(|| ParseError::Syntax {
930                        message: "having without group by".into(),
931                        position: None,
932                    })?;
933                    let rewritten = match projection.as_ref() {
934                        Some(fields) => substitute_projection_aliases(having_expr, fields),
935                        None => having_expr,
936                    };
937                    group.having = Some(match group.having.take() {
938                        Some(existing) => {
939                            having_chain += 1;
940                            self.check_chain_depth(having_chain)?;
941                            Expr::BinaryOp(Box::new(existing), BinOp::And, Box::new(rewritten))
942                        }
943                        None => rewritten,
944                    });
945                }
946                _ => break,
947            }
948        }
949
950        Ok(QueryExpr {
951            source,
952            alias,
953            joins,
954            filter,
955            order,
956            limit,
957            offset,
958            projection,
959            aggregation: None,
960            distinct,
961            group_by,
962        })
963    }
964
965    /// Consume an optional `as <ident>` suffix on a source. Returns `None`
966    /// if the next token isn't `as`. Used by both the primary source and each
967    /// join source so queries can disambiguate columns via `alias.field`.
968    fn try_parse_alias(&mut self) -> Option<String> {
969        if *self.peek() == Token::As {
970            self.advance();
971            if let Token::Ident(name) = self.peek().clone() {
972                self.advance();
973                return Some(name);
974            }
975        }
976        None
977    }
978
979    /// Parse zero or more join clauses. Each clause is:
980    ///   (`inner` | `left` [`outer`] | `right` [`outer`] | `cross`)? `join`
981    ///   <Ident> [`as` <ident>] [`on` <expr>]
982    ///
983    /// `on` is required for every kind except `cross`. The default kind is
984    /// `inner` when the caller wrote bare `join` without a preceding modifier.
985    fn parse_joins(&mut self) -> Result<Vec<JoinClause>, ParseError> {
986        let mut joins = Vec::new();
987        loop {
988            let kind = match self.peek() {
989                Token::Join => {
990                    self.advance();
991                    JoinKind::Inner
992                }
993                Token::Inner => {
994                    self.advance();
995                    self.expect(&Token::Join)?;
996                    JoinKind::Inner
997                }
998                Token::LeftKw => {
999                    self.advance();
1000                    if *self.peek() == Token::Outer {
1001                        self.advance();
1002                    }
1003                    self.expect(&Token::Join)?;
1004                    JoinKind::LeftOuter
1005                }
1006                Token::RightKw => {
1007                    self.advance();
1008                    if *self.peek() == Token::Outer {
1009                        self.advance();
1010                    }
1011                    self.expect(&Token::Join)?;
1012                    JoinKind::RightOuter
1013                }
1014                Token::Cross => {
1015                    self.advance();
1016                    self.expect(&Token::Join)?;
1017                    JoinKind::Cross
1018                }
1019                _ => break,
1020            };
1021
1022            let source = match self.advance() {
1023                Token::Ident(name) => name,
1024                t => {
1025                    return Err(ParseError::UnexpectedToken {
1026                        expected: "type name after join".into(),
1027                        got: t.display_name(),
1028                        position: None,
1029                    });
1030                }
1031            };
1032            let alias = self.try_parse_alias();
1033            let on = if kind == JoinKind::Cross {
1034                None
1035            } else if *self.peek() == Token::On {
1036                self.advance();
1037                Some(self.parse_expr()?)
1038            } else {
1039                return Err(ParseError::Syntax {
1040                    message: format!("expected `on <expr>` after join {source}"),
1041                    position: None,
1042                });
1043            };
1044
1045            joins.push(JoinClause {
1046                kind,
1047                source,
1048                alias,
1049                on,
1050            });
1051        }
1052        Ok(joins)
1053    }
1054
1055    fn parse_insert(&mut self) -> Result<Statement, ParseError> {
1056        self.expect(&Token::Insert)?;
1057        let target = match self.advance() {
1058            Token::Ident(name) => name,
1059            t => {
1060                return Err(ParseError::UnexpectedToken {
1061                    expected: "type name".into(),
1062                    got: t.display_name(),
1063                    position: None,
1064                })
1065            }
1066        };
1067        // One or more comma-separated assignment blocks:
1068        //   insert T { a := 1 }
1069        //   insert T { a := 1 }, { a := 2 }, { a := 3 }
1070        let mut rows = vec![self.parse_assignments()?];
1071        while *self.peek() == Token::Comma {
1072            self.advance(); // consume the comma between row blocks
1073            rows.push(self.parse_assignments()?);
1074        }
1075        // Optional trailing `returning` — return the inserted rows.
1076        let returning = *self.peek() == Token::Returning;
1077        if returning {
1078            self.advance();
1079        }
1080        Ok(Statement::Insert(InsertExpr {
1081            target,
1082            rows,
1083            returning,
1084        }))
1085    }
1086
1087    /// Parse: `upsert Table on .key_col { assignments } [on conflict { update_assignments }]`
1088    fn parse_upsert(&mut self) -> Result<Statement, ParseError> {
1089        self.expect(&Token::Upsert)?;
1090        let target = match self.advance() {
1091            Token::Ident(name) => name,
1092            t => {
1093                return Err(ParseError::UnexpectedToken {
1094                    expected: "type name".into(),
1095                    got: t.display_name(),
1096                    position: None,
1097                })
1098            }
1099        };
1100        self.expect(&Token::On)?;
1101        let key_column = match self.advance() {
1102            Token::DotIdent(name) => name,
1103            t => {
1104                return Err(ParseError::UnexpectedToken {
1105                    expected: ".key_column".into(),
1106                    got: t.display_name(),
1107                    position: None,
1108                })
1109            }
1110        };
1111        let assignments = self.parse_assignments()?;
1112        let on_conflict = if *self.peek() == Token::On {
1113            self.advance(); // consume `on`
1114            self.expect(&Token::Conflict)?;
1115            self.parse_assignments()?
1116        } else {
1117            Vec::new()
1118        };
1119        Ok(Statement::Upsert(UpsertExpr {
1120            target,
1121            key_column,
1122            assignments,
1123            on_conflict,
1124        }))
1125    }
1126
1127    fn parse_assignments(&mut self) -> Result<Vec<Assignment>, ParseError> {
1128        self.expect(&Token::LBrace)?;
1129        let mut assignments = Vec::new();
1130        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1131            // A JSON path target (`.data->x := ...` or `data->x := ...`) is a
1132            // field/dot-field immediately followed by `->`. Detect it before
1133            // the generic ident/`:=` parse so the error names the unsupported
1134            // position and the whole-column alternative, instead of a bare
1135            // "expected field name" (the `.data` DotIdent case) or "expected
1136            // ':='" (the `data` Ident case).
1137            if matches!(self.peek(), Token::DotIdent(_) | Token::Ident(_))
1138                && matches!(self.tokens.get(self.pos + 1), Some(Token::Arrow))
1139            {
1140                let field = match self.peek() {
1141                    Token::DotIdent(n) | Token::Ident(n) => n.clone(),
1142                    _ => unreachable!("guarded by the matches! above"),
1143                };
1144                return Err(ParseError::Unsupported {
1145                    feature: format!(
1146                        "cannot assign to a JSON path target `.{field}->...` (at token {pos}): \
1147                         JSON path assignment targets are not supported; write the whole JSON \
1148                         column instead (path mutation such as json_set is not yet available)",
1149                        pos = self.pos
1150                    ),
1151                });
1152            }
1153            let field = self.expect_named_ident("field name")?;
1154            self.expect(&Token::Assign)?;
1155            let value = self.parse_expr()?;
1156            assignments.push(Assignment { field, value });
1157            if *self.peek() == Token::Comma {
1158                self.advance();
1159            }
1160        }
1161        self.expect(&Token::RBrace)?;
1162        Ok(assignments)
1163    }
1164
1165    fn parse_projection(&mut self) -> Result<Vec<ProjectionField>, ParseError> {
1166        self.expect(&Token::LBrace)?;
1167        let mut fields = Vec::new();
1168        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1169            // `alias: expr` is detected with two-token lookahead so the bare
1170            // form can parse a full expression from its first token. Every
1171            // projection slot — aliased or bare — flows through the shared
1172            // expression parser, so fields, qualified refs, aggregates, window
1173            // functions, scalar calls, CASE/CAST, and arithmetic like `.a - 1`
1174            // are all accepted. Previously the bare slot used a restricted
1175            // dispatch that rejected any binary operator with "expected field".
1176            if matches!(self.peek(), Token::Ident(_))
1177                && matches!(self.tokens.get(self.pos + 1), Some(Token::Colon))
1178            {
1179                let alias = match self.advance() {
1180                    Token::Ident(name) => name,
1181                    _ => unreachable!("guarded by the matches! above"),
1182                };
1183                self.advance(); // consume ':'
1184                self.reject_bare_dotted_path()?;
1185                // `alias: Ident as ...` is a nested sub-query projection
1186                // (language-lab slice): `orders: Order as o filter ... { ... }`.
1187                let expr = if matches!(self.peek(), Token::Ident(_))
1188                    && matches!(self.tokens.get(self.pos + 1), Some(Token::As))
1189                {
1190                    Expr::NestedQuery(Box::new(self.parse_nested_query()?))
1191                } else if self.at_link_traversal() {
1192                    // `orders: u.orders [filter ...] { ... }`: block link
1193                    // traversal, sugar over the nested-projection machinery.
1194                    // Distinguished from an ordinary `x: u.name` projection by
1195                    // the `{`/clause that follows the `alias.ident` (a plain
1196                    // qualified field is followed by `,` or `}`).
1197                    Expr::NestedQuery(Box::new(self.parse_link_traversal()?))
1198                } else if self.at_scalar_link_path() {
1199                    // `buyer: o.user.name`: scalar link traversal.
1200                    self.parse_scalar_link_path()?
1201                } else {
1202                    self.parse_expr()?
1203                };
1204                fields.push(ProjectionField {
1205                    alias: Some(alias),
1206                    expr,
1207                });
1208            } else {
1209                if matches!(self.peek(), Token::Ident(_))
1210                    && matches!(self.tokens.get(self.pos + 1), Some(Token::As))
1211                {
1212                    return Err(ParseError::Syntax {
1213                        message: "a nested projection needs a field name: \
1214                                  `<name>: <Table> as <alias> filter ... { ... }`"
1215                            .into(),
1216                        position: None,
1217                    });
1218                }
1219                self.reject_bare_dotted_path()?;
1220                let expr = if self.at_scalar_link_path() {
1221                    // `o.user.name`: scalar link traversal, named by its
1222                    // dotted spelling when no alias is written.
1223                    self.parse_scalar_link_path()?
1224                } else {
1225                    self.parse_expr()?
1226                };
1227                fields.push(ProjectionField { alias: None, expr });
1228            }
1229            if *self.peek() == Token::Comma {
1230                self.advance();
1231            }
1232        }
1233        self.expect(&Token::RBrace)?;
1234        Ok(fields)
1235    }
1236
1237    /// Reject a projection slot that starts with two adjacent dotted parts
1238    /// (`.user.name`). The token stream cannot distinguish an intended link
1239    /// path from two comma-less bare fields (`.user .name` lexes identically),
1240    /// and a link path needs the aliased form to name its outer scan, so this
1241    /// used to silently parse as TWO separate fields and project Empty
1242    /// columns. A hard error with guidance beats the silent wrong shape.
1243    fn reject_bare_dotted_path(&self) -> Result<(), ParseError> {
1244        if let (Token::DotIdent(first), Some(Token::DotIdent(second))) =
1245            (self.peek(), self.tokens.get(self.pos + 1))
1246        {
1247            return Err(ParseError::Syntax {
1248                message: format!(
1249                    "`.{first}.{second}` is ambiguous in a projection: for a link \
1250                     path, alias the table and qualify the path \
1251                     (`Order as o {{ o.{first}.{second} }}`); for separate fields, \
1252                     separate them with commas (`.{first}, .{second}`)"
1253                ),
1254                position: None,
1255            });
1256        }
1257        Ok(())
1258    }
1259
1260    /// Conservative lookahead for a block link-traversal projection value:
1261    /// `<Ident>.<DotIdent>` immediately followed by `{` or a
1262    /// `filter`/`order`/`limit`/`offset` clause. An ordinary qualified-field
1263    /// projection (`x: u.name`) is instead followed by `,` or `}`, so it never
1264    /// matches here.
1265    fn at_link_traversal(&self) -> bool {
1266        matches!(self.peek(), Token::Ident(_))
1267            && matches!(self.tokens.get(self.pos + 1), Some(Token::DotIdent(_)))
1268            && matches!(
1269                self.tokens.get(self.pos + 2),
1270                Some(Token::LBrace | Token::Filter | Token::Order | Token::Limit | Token::Offset)
1271            )
1272    }
1273
1274    /// Lookahead for a scalar link-traversal projection value:
1275    /// `<Ident>.<DotIdent>.<DotIdent>...`, i.e. three or more dotted parts.
1276    /// A plain qualified field (`o.total`) has exactly two parts and never
1277    /// matches; a block traversal (`u.orders { ... }`) is caught first by
1278    /// `at_link_traversal` (its second part is followed by `{` or a clause,
1279    /// never by another `.ident`).
1280    fn at_scalar_link_path(&self) -> bool {
1281        matches!(self.peek(), Token::Ident(_))
1282            && matches!(self.tokens.get(self.pos + 1), Some(Token::DotIdent(_)))
1283            && matches!(self.tokens.get(self.pos + 2), Some(Token::DotIdent(_)))
1284    }
1285
1286    /// Parse a scalar link traversal path: `o.user.name` or
1287    /// `o.user.company.name`. All parts but the last are declared to-one link
1288    /// names (resolved from the persistent catalog at execution time); the
1289    /// last is the target column to read. Only valid as a projection field
1290    /// value.
1291    fn parse_scalar_link_path(&mut self) -> Result<Expr, ParseError> {
1292        let outer_alias = match self.advance() {
1293            Token::Ident(n) => n,
1294            t => return Err(self.named_ident_error("link path outer alias", &t)),
1295        };
1296        let mut parts = Vec::new();
1297        while let Some(Token::DotIdent(_)) = self.tokens.get(self.pos) {
1298            match self.advance() {
1299                Token::DotIdent(n) => parts.push(n),
1300                _ => unreachable!("peeked DotIdent"),
1301            }
1302        }
1303        debug_assert!(parts.len() >= 2, "guarded by at_scalar_link_path");
1304        let column = parts.pop().expect("at least two parts");
1305        Ok(Expr::LinkPath {
1306            outer_alias,
1307            links: parts,
1308            column,
1309        })
1310    }
1311
1312    /// Parse a block link traversal projection value:
1313    /// `<outer_alias>.<link_name> [filter <conds>] [order ...] [limit N]
1314    /// [offset M] { <bare child fields> }`. Desugars to a [`NestedQuery`]
1315    /// carrying a [`ViaLink`]: the child source table and correlation columns
1316    /// are unknown here (they live in the persistent catalog), so `source` is a
1317    /// placeholder, the synthetic child alias is the link name itself, and only
1318    /// the user's residual conditions land in `filter`. Bare child columns in
1319    /// the block, filter, and order are qualified with the synthetic alias so
1320    /// downstream planning matches the explicit correlated spelling.
1321    fn parse_link_traversal(&mut self) -> Result<NestedQuery, ParseError> {
1322        self.depth += 1;
1323        if self.depth > MAX_NESTING_DEPTH {
1324            self.depth -= 1;
1325            return Err(ParseError::NestingDepthExceeded {
1326                max: MAX_NESTING_DEPTH,
1327            });
1328        }
1329        let result = self.parse_link_traversal_inner();
1330        self.depth -= 1;
1331        result
1332    }
1333
1334    fn parse_link_traversal_inner(&mut self) -> Result<NestedQuery, ParseError> {
1335        let outer_alias = match self.advance() {
1336            Token::Ident(n) => n,
1337            t => return Err(self.named_ident_error("link outer alias", &t)),
1338        };
1339        let link_name = match self.advance() {
1340            Token::DotIdent(n) => n,
1341            t => {
1342                return Err(ParseError::UnexpectedToken {
1343                    expected: "link name".into(),
1344                    got: t.display_name(),
1345                    position: None,
1346                })
1347            }
1348        };
1349        // The synthetic child alias is the link name itself; bare child
1350        // references are qualified with it below.
1351        let child_alias = link_name.clone();
1352
1353        // Optional residual filter, then order/limit/offset, matching the
1354        // explicit nested-query grammar. No correlation predicate is written
1355        // by the user: the whole filter is residual.
1356        let mut residual = None;
1357        if *self.peek() == Token::Filter {
1358            self.advance();
1359            residual = Some(qualify_bare_fields(self.parse_expr()?, &child_alias));
1360        }
1361        let mut order = None;
1362        let mut limit = None;
1363        let mut offset = None;
1364        let mut offset_before_limit = false;
1365        loop {
1366            match self.peek() {
1367                Token::Order => {
1368                    refuse_repeated_clause(&order, "'order'")?;
1369                    self.advance();
1370                    let mut clause = self.parse_order()?;
1371                    for key in &mut clause.keys {
1372                        key.expr = qualify_bare_fields(
1373                            std::mem::replace(&mut key.expr, Expr::Null),
1374                            &child_alias,
1375                        );
1376                    }
1377                    order = Some(clause);
1378                }
1379                Token::Limit => {
1380                    refuse_repeated_clause(&limit, "'limit'")?;
1381                    self.advance();
1382                    if offset.is_some() {
1383                        offset_before_limit = true;
1384                    }
1385                    limit = Some(self.parse_expr()?);
1386                }
1387                Token::Offset => {
1388                    refuse_repeated_clause(&offset, "'offset'")?;
1389                    self.advance();
1390                    offset = Some(self.parse_expr()?);
1391                }
1392                _ => break,
1393            }
1394        }
1395        self.expect(&Token::LBrace)?;
1396        let mut fields = Vec::new();
1397        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1398            let alias = if matches!(self.peek(), Token::Ident(_))
1399                && matches!(self.tokens.get(self.pos + 1), Some(Token::Colon))
1400            {
1401                let alias = self.expect_named_ident("field alias")?;
1402                self.advance(); // consume ':'
1403                Some(alias)
1404            } else {
1405                None
1406            };
1407            self.reject_bare_dotted_path()?;
1408            let expr = qualify_bare_fields(self.parse_expr()?, &child_alias);
1409            fields.push(ProjectionField { alias, expr });
1410            if *self.peek() == Token::Comma {
1411                self.advance();
1412            }
1413        }
1414        self.expect(&Token::RBrace)?;
1415        if fields.is_empty() {
1416            return Err(ParseError::Syntax {
1417                message: "link traversal requires at least one field".into(),
1418                position: None,
1419            });
1420        }
1421        Ok(NestedQuery {
1422            // Placeholder source: resolved from the catalog at execution.
1423            source: String::new(),
1424            alias: child_alias,
1425            via_link: Some(ViaLink {
1426                outer_alias,
1427                link_name,
1428            }),
1429            // A `true` residual is equivalent to no residual; supply `true`
1430            // when the user wrote no filter to keep the planner simple.
1431            filter: residual.unwrap_or(Expr::Literal(Literal::Bool(true))),
1432            order,
1433            limit,
1434            offset,
1435            offset_before_limit,
1436            fields,
1437        })
1438    }
1439
1440    /// Parse a nested sub-query projection value:
1441    /// `<ChildTable> as <alias> filter <predicate> [order ...] [limit N]
1442    /// [offset M] { <fields> }`. The block accepts plain and aliased scalar
1443    /// fields plus further `name: Table as alias ...` nesting.
1444    fn parse_nested_query(&mut self) -> Result<NestedQuery, ParseError> {
1445        // Nested blocks recurse; share the expression nesting-depth guard so
1446        // pathological inputs fail cleanly instead of overflowing the stack.
1447        self.depth += 1;
1448        if self.depth > MAX_NESTING_DEPTH {
1449            self.depth -= 1;
1450            return Err(ParseError::NestingDepthExceeded {
1451                max: MAX_NESTING_DEPTH,
1452            });
1453        }
1454        let result = self.parse_nested_query_inner();
1455        self.depth -= 1;
1456        result
1457    }
1458
1459    fn parse_nested_query_inner(&mut self) -> Result<NestedQuery, ParseError> {
1460        let source = self.expect_named_ident("nested source type")?;
1461        self.expect(&Token::As)?;
1462        let alias = self.expect_named_ident("nested source alias")?;
1463        self.expect(&Token::Filter)?;
1464        let filter = self.parse_expr()?;
1465        let mut order = None;
1466        let mut limit = None;
1467        let mut offset = None;
1468        let mut offset_before_limit = false;
1469        loop {
1470            match self.peek() {
1471                Token::Order => {
1472                    refuse_repeated_clause(&order, "'order'")?;
1473                    self.advance();
1474                    order = Some(self.parse_order()?);
1475                }
1476                Token::Limit => {
1477                    refuse_repeated_clause(&limit, "'limit'")?;
1478                    self.advance();
1479                    if offset.is_some() {
1480                        offset_before_limit = true;
1481                    }
1482                    limit = Some(self.parse_expr()?);
1483                }
1484                Token::Offset => {
1485                    refuse_repeated_clause(&offset, "'offset'")?;
1486                    self.advance();
1487                    offset = Some(self.parse_expr()?);
1488                }
1489                _ => break,
1490            }
1491        }
1492        self.expect(&Token::LBrace)?;
1493        let mut fields = Vec::new();
1494        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1495            let alias = if matches!(self.peek(), Token::Ident(_))
1496                && matches!(self.tokens.get(self.pos + 1), Some(Token::Colon))
1497            {
1498                let alias = self.expect_named_ident("field alias")?;
1499                self.advance(); // consume ':'
1500                Some(alias)
1501            } else {
1502                None
1503            };
1504            let expr = if matches!(self.peek(), Token::Ident(_))
1505                && matches!(self.tokens.get(self.pos + 1), Some(Token::As))
1506            {
1507                if alias.is_none() {
1508                    return Err(ParseError::Syntax {
1509                        message: "a nested projection needs a field name: \
1510                                  `<name>: <Table> as <alias> filter ... { ... }`"
1511                            .into(),
1512                        position: None,
1513                    });
1514                }
1515                Expr::NestedQuery(Box::new(self.parse_nested_query()?))
1516            } else {
1517                self.reject_bare_dotted_path()?;
1518                self.parse_expr()?
1519            };
1520            fields.push(ProjectionField { alias, expr });
1521            if *self.peek() == Token::Comma {
1522                self.advance();
1523            }
1524        }
1525        self.expect(&Token::RBrace)?;
1526        if fields.is_empty() {
1527            return Err(ParseError::Syntax {
1528                message: "nested projection requires at least one field".into(),
1529                position: None,
1530            });
1531        }
1532        Ok(NestedQuery {
1533            source,
1534            alias,
1535            via_link: None,
1536            filter,
1537            order,
1538            limit,
1539            offset,
1540            offset_before_limit,
1541            fields,
1542        })
1543    }
1544
1545    /// Parse the OVER clause for a window function:
1546    /// `over (partition .col1, .col2 order .col3 asc, .col4 desc)`
1547    fn parse_over_clause(&mut self) -> Result<(Vec<Expr>, Vec<OrderKey>), ParseError> {
1548        self.expect(&Token::Over)?;
1549        self.expect(&Token::LParen)?;
1550        let mut partition_by = Vec::new();
1551        let mut order_by = Vec::new();
1552        if *self.peek() == Token::Partition {
1553            self.advance();
1554            loop {
1555                partition_by.push(self.parse_expr()?);
1556                if *self.peek() == Token::Comma {
1557                    if !matches!(
1558                        self.tokens.get(self.pos + 1),
1559                        Some(Token::Order | Token::RParen)
1560                    ) {
1561                        self.advance();
1562                    } else {
1563                        break;
1564                    }
1565                } else {
1566                    break;
1567                }
1568            }
1569        }
1570        if *self.peek() == Token::Order {
1571            self.advance();
1572            loop {
1573                let expr = self.parse_expr()?;
1574                let descending = match self.peek() {
1575                    Token::Desc => {
1576                        self.advance();
1577                        true
1578                    }
1579                    Token::Asc => {
1580                        self.advance();
1581                        false
1582                    }
1583                    _ => false,
1584                };
1585                order_by.push(OrderKey { expr, descending });
1586                if *self.peek() == Token::Comma {
1587                    self.advance();
1588                } else {
1589                    break;
1590                }
1591            }
1592        }
1593        self.expect(&Token::RParen)?;
1594        Ok((partition_by, order_by))
1595    }
1596
1597    /// Parse a cast target type from a string literal: `"int"`, `"float"`, `"str"`, `"bool"`, `"datetime"`.
1598    fn parse_cast_type(&mut self) -> Result<CastType, ParseError> {
1599        match self.advance() {
1600            Token::StringLit(s) => match s.as_str() {
1601                "int" | "Int" | "INT" => Ok(CastType::Int),
1602                "float" | "Float" | "FLOAT" => Ok(CastType::Float),
1603                "str" | "Str" | "STR" | "string" | "String" => Ok(CastType::Str),
1604                "bool" | "Bool" | "BOOL" | "boolean" => Ok(CastType::Bool),
1605                "datetime" | "DateTime" | "DATETIME" => Ok(CastType::DateTime),
1606                "uuid" | "Uuid" | "UUID" => Ok(CastType::Uuid),
1607                "bytes" | "Bytes" | "BYTES" | "bytea" => Ok(CastType::Bytes),
1608                other => Err(ParseError::Syntax {
1609                    message: format!("invalid cast type: \"{other}\""),
1610                    position: None,
1611                }),
1612            },
1613            t => Err(ParseError::UnexpectedToken {
1614                expected: "string literal for cast type".into(),
1615                got: t.display_name(),
1616                position: None,
1617            }),
1618        }
1619    }
1620
1621    fn parse_order(&mut self) -> Result<OrderClause, ParseError> {
1622        let mut keys = Vec::new();
1623        loop {
1624            let expr = self.parse_expr()?;
1625            let descending = match self.peek() {
1626                Token::Desc => {
1627                    self.advance();
1628                    true
1629                }
1630                Token::Asc => {
1631                    self.advance();
1632                    false
1633                }
1634                _ => false,
1635            };
1636            keys.push(OrderKey { expr, descending });
1637            if *self.peek() == Token::Comma {
1638                self.advance();
1639            } else {
1640                break;
1641            }
1642        }
1643        Ok(OrderClause { keys })
1644    }
1645
1646    fn parse_aggregate_query(&mut self) -> Result<Statement, ParseError> {
1647        let mut func = match self.advance() {
1648            Token::Count => AggFunc::Count,
1649            Token::Avg => AggFunc::Avg,
1650            Token::Sum => AggFunc::Sum,
1651            Token::Min => AggFunc::Min,
1652            Token::Max => AggFunc::Max,
1653            t => {
1654                return Err(ParseError::UnexpectedToken {
1655                    expected: "aggregate function".into(),
1656                    got: t.display_name(),
1657                    position: None,
1658                })
1659            }
1660        };
1661        self.expect(&Token::LParen)?;
1662        let mode = if *self.peek() == Token::Raw {
1663            self.advance();
1664            AggregateMode::Raw
1665        } else {
1666            AggregateMode::Symmetric
1667        };
1668        // count(distinct User ...) → CountDistinct
1669        if func == AggFunc::Count && *self.peek() == Token::Distinct {
1670            self.advance();
1671            func = AggFunc::CountDistinct;
1672        }
1673        let source = match self.advance() {
1674            Token::Ident(name) => name,
1675            t => {
1676                return Err(ParseError::UnexpectedToken {
1677                    expected: "type name".into(),
1678                    got: t.display_name(),
1679                    position: None,
1680                })
1681            }
1682        };
1683        // Allow a full read-pipeline tail inside the parens, e.g.
1684        // `count(User filter .age > 27 limit 100)`. parse_query_tail stops at
1685        // the first non-pipeline token, which here must be RParen.
1686        let mut query = self.parse_query_tail(source)?;
1687        self.expect(&Token::RParen)?;
1688
1689        // The caller writes the aggregate's target column via the trailing
1690        // projection form:
1691        //     sum(User filter .age > 30 { .age })
1692        //     count(distinct User { .name })
1693        //     count(User { .nickname })
1694        // We lift that single unaliased `.field` into AggregateExpr.field so
1695        // the executor's aggregate fast paths can see it. Plain `count` is
1696        // included: an argument makes it a non-null count of that column
1697        // (matching the grouped `count(.col)` path and SQL's `COUNT(col)`),
1698        // while the argument-less `count(User)` stays a row count. Ignoring
1699        // the projection here used to make `count(User { .nickname })` silently
1700        // return the row count.
1701        let mut argument: Option<Expr> = None;
1702        if let Some(proj) = &query.projection {
1703            if proj.len() == 1 && proj[0].alias.is_none() {
1704                argument = Some(proj[0].expr.clone());
1705            }
1706        }
1707        if argument.is_some() {
1708            query.projection = None;
1709        }
1710        query.aggregation = Some(AggregateExpr {
1711            function: func,
1712            argument,
1713            mode,
1714        });
1715        Ok(Statement::Query(query))
1716    }
1717
1718    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
1719        self.depth += 1;
1720        if self.depth > MAX_NESTING_DEPTH {
1721            self.depth -= 1;
1722            return Err(ParseError::NestingDepthExceeded {
1723                max: MAX_NESTING_DEPTH,
1724            });
1725        }
1726        let result = self.parse_or_expr();
1727        self.depth -= 1;
1728        result
1729    }
1730
1731    /// Guard for a loop that stacks `chain` levels of AST on top of the
1732    /// current recursion depth. `chain` is the number of levels this loop has
1733    /// already added; the caller counts it locally rather than mutating
1734    /// `self.depth` so sibling expressions each get the full budget.
1735    fn check_chain_depth(&self, chain: usize) -> Result<(), ParseError> {
1736        if self.depth + chain > MAX_NESTING_DEPTH {
1737            return Err(ParseError::NestingDepthExceeded {
1738                max: MAX_NESTING_DEPTH,
1739            });
1740        }
1741        Ok(())
1742    }
1743
1744    fn parse_or_expr(&mut self) -> Result<Expr, ParseError> {
1745        let mut left = self.parse_and_expr()?;
1746        let mut chain = 0usize;
1747        while *self.peek() == Token::Or {
1748            chain += 1;
1749            self.check_chain_depth(chain)?;
1750            self.advance();
1751            let right = self.parse_and_expr()?;
1752            left = Expr::BinaryOp(Box::new(left), BinOp::Or, Box::new(right));
1753        }
1754        Ok(left)
1755    }
1756
1757    fn parse_and_expr(&mut self) -> Result<Expr, ParseError> {
1758        let mut left = self.parse_comparison()?;
1759        let mut chain = 0usize;
1760        while *self.peek() == Token::And {
1761            chain += 1;
1762            self.check_chain_depth(chain)?;
1763            self.advance();
1764            let right = self.parse_comparison()?;
1765            left = Expr::BinaryOp(Box::new(left), BinOp::And, Box::new(right));
1766        }
1767        Ok(left)
1768    }
1769
1770    fn parse_comparison(&mut self) -> Result<Expr, ParseError> {
1771        // Prefix `not` lives at precedence level 4 (docs/POWQL.md): looser
1772        // than the comparisons parsed below, tighter than `and`/`or`. Consume
1773        // the whole prefix chain here so `not .v > 0` means `not (.v > 0)`,
1774        // matching the SQL frontend. `not exists` keeps its dedicated
1775        // primary-level parse (ExistsSubquery / NotExists), and an explicit
1776        // `(not .v) > 0` still reaches parse_primary's `not` through the
1777        // parentheses. The chain is counted iteratively (no recursion) and
1778        // capped like parse_primary's guard so `not not … .x` cannot
1779        // overflow the stack in the wrapping loop below or in later walks.
1780        let mut negations = 0usize;
1781        while *self.peek() == Token::Not
1782            && !matches!(self.tokens.get(self.pos + 1), Some(Token::Exists))
1783        {
1784            self.advance();
1785            negations += 1;
1786            self.check_chain_depth(negations)?;
1787        }
1788        let mut expr = self.parse_comparison_body()?;
1789        for _ in 0..negations {
1790            expr = Expr::UnaryOp(UnaryOp::Not, Box::new(expr));
1791        }
1792        Ok(expr)
1793    }
1794
1795    fn parse_comparison_body(&mut self) -> Result<Expr, ParseError> {
1796        let left = self.parse_additive()?;
1797
1798        // IS NULL / IS NOT NULL (postfix)
1799        if *self.peek() == Token::Is {
1800            self.advance();
1801            if *self.peek() == Token::Not {
1802                self.advance();
1803                self.expect(&Token::Null)?;
1804                return Ok(Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(left)));
1805            } else {
1806                self.expect(&Token::Null)?;
1807                return Ok(Expr::UnaryOp(UnaryOp::IsNull, Box::new(left)));
1808            }
1809        }
1810
1811        // Postfix: `in (...)`, `like "..."`, `between X and Y`
1812        // and their negated forms: `not in`, `not like`, `not between`.
1813        match self.peek() {
1814            Token::In => {
1815                self.advance();
1816                return self.parse_in_list(left, false);
1817            }
1818            Token::Like => {
1819                self.advance();
1820                let pattern = self.parse_additive()?;
1821                return Ok(Expr::BinaryOp(
1822                    Box::new(left),
1823                    BinOp::Like,
1824                    Box::new(pattern),
1825                ));
1826            }
1827            Token::Between => {
1828                self.advance();
1829                return self.parse_between(left, false);
1830            }
1831            Token::Not => {
1832                // Peek ahead: `not in`, `not like`, `not between`.
1833                // If the token after `not` isn't one of these, don't consume
1834                // `not` — let the caller handle it.
1835                let next = self.tokens.get(self.pos + 1);
1836                match next {
1837                    Some(Token::In) => {
1838                        self.advance(); // not
1839                        self.advance(); // in
1840                        return self.parse_in_list(left, true);
1841                    }
1842                    Some(Token::Like) => {
1843                        self.advance(); // not
1844                        self.advance(); // like
1845                        let pattern = self.parse_additive()?;
1846                        let like = Expr::BinaryOp(Box::new(left), BinOp::Like, Box::new(pattern));
1847                        return Ok(Expr::UnaryOp(UnaryOp::Not, Box::new(like)));
1848                    }
1849                    Some(Token::Between) => {
1850                        self.advance(); // not
1851                        self.advance(); // between
1852                        return self.parse_between(left, true);
1853                    }
1854                    _ => {}
1855                }
1856            }
1857            _ => {}
1858        }
1859
1860        let op = match self.peek() {
1861            Token::Eq => BinOp::Eq,
1862            Token::Neq => BinOp::Neq,
1863            Token::Lt => BinOp::Lt,
1864            Token::Gt => BinOp::Gt,
1865            Token::Lte => BinOp::Lte,
1866            Token::Gte => BinOp::Gte,
1867            _ => return Ok(left),
1868        };
1869        self.advance();
1870        // `expr = null` / `expr != null` desugar to the same UnaryOp as
1871        // `expr is null` / `expr is not null`. Ordering comparisons against
1872        // null (`< null`, `>= null`, etc.) remain parse errors.
1873        if *self.peek() == Token::Null {
1874            match op {
1875                BinOp::Eq => {
1876                    self.advance();
1877                    return Ok(Expr::UnaryOp(UnaryOp::IsNull, Box::new(left)));
1878                }
1879                BinOp::Neq => {
1880                    self.advance();
1881                    return Ok(Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(left)));
1882                }
1883                _ => {}
1884            }
1885        }
1886        let right = self.parse_additive()?;
1887        Ok(Expr::BinaryOp(Box::new(left), op, Box::new(right)))
1888    }
1889
1890    /// Parse `(val1, val2, ...)` or `(subquery)` after `in` / `not in`.
1891    /// A subquery is detected by `(` followed by an `Ident` that is NOT
1892    /// followed by `,` or `)` — in PowQL, bare identifiers in value lists
1893    /// don't appear (field refs start with `.`).
1894    fn parse_in_list(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
1895        self.expect(&Token::LParen)?;
1896        // Detect subquery: `( Ident ...` where the Ident is a table name.
1897        if let Token::Ident(_) = self.peek() {
1898            // Peek further: if the next token after the Ident is NOT `,` or
1899            // `)`, it's a subquery source name.
1900            let after = self.tokens.get(self.pos + 1);
1901            let is_subquery = !matches!(after, Some(Token::Comma) | Some(Token::RParen));
1902            if is_subquery {
1903                let source = match self.advance() {
1904                    Token::Ident(name) => name,
1905                    _ => unreachable!(),
1906                };
1907                let subquery = self.parse_query_tail(source)?;
1908                self.expect(&Token::RParen)?;
1909                return Ok(Expr::InSubquery {
1910                    expr: Box::new(expr),
1911                    subquery: Box::new(subquery),
1912                    negated,
1913                });
1914            }
1915        }
1916        let mut list = Vec::new();
1917        while !matches!(self.peek(), Token::RParen | Token::Eof) {
1918            list.push(self.parse_expr()?);
1919            if *self.peek() == Token::Comma {
1920                self.advance();
1921            }
1922        }
1923        self.expect(&Token::RParen)?;
1924        Ok(Expr::InList {
1925            expr: Box::new(expr),
1926            list,
1927            negated,
1928        })
1929    }
1930
1931    /// Try to parse a `(subquery)` tail for `exists` / `not exists`.
1932    /// A subquery is detected when the next tokens are `( Ident ...` —
1933    /// bare identifiers inside parens are always table/view names in
1934    /// PowQL (column refs start with `.`). Returns `Ok(Some(query))` if
1935    /// consumed, `Ok(None)` if the shape doesn't match (so the caller
1936    /// falls back to parsing a scalar primary for the legacy
1937    /// `exists <expr>` form).
1938    fn try_parse_exists_subquery(&mut self) -> Result<Option<QueryExpr>, ParseError> {
1939        if *self.peek() != Token::LParen {
1940            return Ok(None);
1941        }
1942        // Peek one token inside the paren. Anything starting with `Ident`
1943        // is a source name — PowQL column references use `DotIdent`, so
1944        // an `exists (X ...)` with a bare `X` is unambiguously a subquery.
1945        let after_lparen = self.tokens.get(self.pos + 1);
1946        if !matches!(after_lparen, Some(Token::Ident(_))) {
1947            return Ok(None);
1948        }
1949        self.expect(&Token::LParen)?;
1950        let source = match self.advance() {
1951            Token::Ident(name) => name,
1952            _ => unreachable!(),
1953        };
1954        let subquery = self.parse_query_tail(source)?;
1955        self.expect(&Token::RParen)?;
1956        Ok(Some(subquery))
1957    }
1958
1959    /// Parse `low and high` after `between` / `not between`.
1960    /// Desugars into `expr >= low AND expr <= high` (or negated:
1961    /// `expr < low OR expr > high`).
1962    fn parse_between(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
1963        let low = self.parse_additive()?;
1964        self.expect(&Token::And)?;
1965        let high = self.parse_additive()?;
1966        if negated {
1967            // NOT BETWEEN: expr < low OR expr > high
1968            Ok(Expr::BinaryOp(
1969                Box::new(Expr::BinaryOp(
1970                    Box::new(expr.clone()),
1971                    BinOp::Lt,
1972                    Box::new(low),
1973                )),
1974                BinOp::Or,
1975                Box::new(Expr::BinaryOp(Box::new(expr), BinOp::Gt, Box::new(high))),
1976            ))
1977        } else {
1978            // BETWEEN: expr >= low AND expr <= high
1979            Ok(Expr::BinaryOp(
1980                Box::new(Expr::BinaryOp(
1981                    Box::new(expr.clone()),
1982                    BinOp::Gte,
1983                    Box::new(low),
1984                )),
1985                BinOp::And,
1986                Box::new(Expr::BinaryOp(Box::new(expr), BinOp::Lte, Box::new(high))),
1987            ))
1988        }
1989    }
1990
1991    /// Parse expression-valued group keys.
1992    fn parse_group_by(&mut self) -> Result<GroupByClause, ParseError> {
1993        let mut keys = Vec::new();
1994        loop {
1995            let expr = self.parse_expr()?;
1996            let output_name = match &expr {
1997                Expr::Field(_) | Expr::QualifiedField { .. } | Expr::JsonPath { .. } => {
1998                    expression_output_name(&expr)
1999                }
2000                _ => format!("__group_{}", keys.len()),
2001            };
2002            keys.push(GroupKey { expr, output_name });
2003            if *self.peek() == Token::Comma {
2004                self.advance();
2005            } else {
2006                break;
2007            }
2008        }
2009        if keys.is_empty() {
2010            return Err(ParseError::Syntax {
2011                message: "expected at least one group key after group".into(),
2012                position: None,
2013            });
2014        }
2015        let having = if *self.peek() == Token::Having {
2016            self.advance();
2017            Some(self.parse_expr()?)
2018        } else {
2019            None
2020        };
2021        Ok(GroupByClause { keys, having })
2022    }
2023
2024    fn parse_additive(&mut self) -> Result<Expr, ParseError> {
2025        let mut left = self.parse_multiplicative()?;
2026        let mut chain = 0usize;
2027        loop {
2028            let op = match self.peek() {
2029                Token::Plus => BinOp::Add,
2030                Token::Minus => BinOp::Sub,
2031                Token::Coalesce => {
2032                    chain += 1;
2033                    self.check_chain_depth(chain)?;
2034                    self.advance();
2035                    let right = self.parse_multiplicative()?;
2036                    left = Expr::Coalesce(Box::new(left), Box::new(right));
2037                    continue;
2038                }
2039                _ => break,
2040            };
2041            chain += 1;
2042            self.check_chain_depth(chain)?;
2043            self.advance();
2044            let right = self.parse_multiplicative()?;
2045            left = Expr::BinaryOp(Box::new(left), op, Box::new(right));
2046        }
2047        Ok(left)
2048    }
2049
2050    fn parse_multiplicative(&mut self) -> Result<Expr, ParseError> {
2051        let mut left = self.parse_primary()?;
2052        let mut chain = 0usize;
2053        loop {
2054            let op = match self.peek() {
2055                Token::Star => BinOp::Mul,
2056                Token::Slash => BinOp::Div,
2057                _ => break,
2058            };
2059            chain += 1;
2060            self.check_chain_depth(chain)?;
2061            self.advance();
2062            let right = self.parse_primary()?;
2063            left = Expr::BinaryOp(Box::new(left), op, Box::new(right));
2064        }
2065        Ok(left)
2066    }
2067
2068    fn parse_primary(&mut self) -> Result<Expr, ParseError> {
2069        // Guard recursion here too: unary prefixes (`not`, `exists`, `not exists`)
2070        // recurse straight back into parse_primary without going through parse_expr,
2071        // so a chain like `not not … .x` would otherwise overflow the stack (process
2072        // abort under panic=abort). See test_unary_prefix_nesting_depth_limit.
2073        self.depth += 1;
2074        if self.depth > MAX_NESTING_DEPTH {
2075            self.depth -= 1;
2076            return Err(ParseError::NestingDepthExceeded {
2077                max: MAX_NESTING_DEPTH,
2078            });
2079        }
2080        let result = self.parse_primary_inner();
2081        self.depth -= 1;
2082        // JSON `->` path access is the tightest-binding postfix level: it binds
2083        // above every binary operator, so `.data->age > 21` parses as
2084        // `(.data->age) > 21`. Applying it here (inside `parse_primary`) means
2085        // every caller — multiplicative, unary prefixes, function args — gets
2086        // path access for free without threading a new precedence level.
2087        self.parse_json_path_postfix(result?)
2088    }
2089
2090    /// If the current token is `->`, consume a chain of path segments and wrap
2091    /// `base` in an `Expr::JsonPath`. `base` must be a `Field`, `QualifiedField`,
2092    /// or (nested) `JsonPath`; any other base is a parse error. Each segment is
2093    /// an object key (bareword `Ident` or double-quoted string) or an array
2094    /// index (non-negative integer). Segments are STRUCTURAL — the plan cache
2095    /// hashes them into the query shape and never treats them as literal slots.
2096    fn parse_json_path_postfix(&mut self, base: Expr) -> Result<Expr, ParseError> {
2097        if *self.peek() != Token::Arrow {
2098            return Ok(base);
2099        }
2100        match &base {
2101            Expr::Field(_) | Expr::QualifiedField { .. } | Expr::JsonPath { .. } => {}
2102            _ => {
2103                return Err(ParseError::Syntax {
2104                    message: "'->' JSON path access requires a field base \
2105                              (e.g. .data->key or posts.data->author)"
2106                        .into(),
2107                    position: None,
2108                })
2109            }
2110        }
2111        let mut segments = Vec::new();
2112        while *self.peek() == Token::Arrow {
2113            self.advance(); // consume `->`
2114            let seg = match self.advance() {
2115                // Bareword key: `->author`.
2116                Token::Ident(name) => PathSeg::Key(name),
2117                // String-form key: `->"weird key!"` (PowQL strings are
2118                // double-quoted; the design's single-quote spelling maps here).
2119                Token::StringLit(s) => PathSeg::Key(s),
2120                // Array index: `->0`. Must be a non-negative integer that fits
2121                // a u32; the lexer produces a signed `IntLit`, so reject `< 0`
2122                // and overflow explicitly.
2123                Token::IntLit(v) => {
2124                    let idx = u32::try_from(v).map_err(|_| ParseError::Syntax {
2125                        message: format!(
2126                            "invalid JSON path array index {v}: expected a non-negative integer that fits in 32 bits"
2127                        ),
2128                        position: None,
2129                    })?;
2130                    PathSeg::Index(idx)
2131                }
2132                other => {
2133                    return Err(ParseError::Syntax {
2134                        message: format!(
2135                            "expected a JSON path segment (object key or array index) after '->', found {}",
2136                            other.display_name()
2137                        ),
2138                        position: None,
2139                    })
2140                }
2141            };
2142            segments.push(seg);
2143        }
2144        // Flatten a nested-JsonPath base into a single segment list so the AST
2145        // for `a->b->c` is one node regardless of how it was assembled.
2146        if let Expr::JsonPath {
2147            base: inner_base,
2148            segments: mut inner_segments,
2149        } = base
2150        {
2151            inner_segments.extend(segments);
2152            return Ok(Expr::JsonPath {
2153                base: inner_base,
2154                segments: inner_segments,
2155            });
2156        }
2157        Ok(Expr::JsonPath {
2158            base: Box::new(base),
2159            segments,
2160        })
2161    }
2162
2163    fn parse_primary_inner(&mut self) -> Result<Expr, ParseError> {
2164        match self.peek().clone() {
2165            Token::DotIdent(name) => {
2166                self.advance();
2167                // The unaliased spelling of a link path, `.author.name`. The
2168                // aliased one (`p.author.name`) is caught in the `Ident` arm
2169                // below; this arm returned `Field("author")` and left `.name`
2170                // for whoever came next, so the same mistake written without an
2171                // alias still got "unexpected trailing token near token 3:
2172                // field '.name'". A projection slot never reaches here with two
2173                // dotted parts -- `reject_bare_dotted_path` names the alias it
2174                // needs first -- so this is the `filter`/`order`/`group` case.
2175                if let Token::DotIdent(next) = self.peek().clone() {
2176                    return Err(link_traversal_outside_projection(&format!(
2177                        ".{name}.{next}"
2178                    )));
2179                }
2180                Ok(Expr::Field(name))
2181            }
2182            Token::IntLit(v) => {
2183                self.advance();
2184                Ok(Expr::Literal(Literal::Int(v)))
2185            }
2186            Token::FloatLit(v) => {
2187                self.advance();
2188                Ok(Expr::Literal(Literal::Float(v)))
2189            }
2190            Token::StringLit(v) => {
2191                self.advance();
2192                Ok(Expr::Literal(Literal::String(v)))
2193            }
2194            Token::BoolLit(v) => {
2195                self.advance();
2196                Ok(Expr::Literal(Literal::Bool(v)))
2197            }
2198            // `$N` placeholders are only valid through
2199            // `parse_with_params`, which substitutes them for literal
2200            // tokens before this expression parser ever runs. Reaching a
2201            // raw `Token::Param` here means the caller used the plain
2202            // (no-params) path with a placeholder — surface the standard
2203            // unexpected-token error so the message names the parameter.
2204            Token::Null => {
2205                self.advance();
2206                Ok(Expr::Null)
2207            }
2208            Token::Not => {
2209                self.advance();
2210                if *self.peek() == Token::Exists {
2211                    self.advance();
2212                    // `not exists (Q)` → ExistsSubquery{ negated: true } when
2213                    // followed by `( Ident ...` (subquery form). Otherwise
2214                    // fall back to the scalar `is not null` unary op.
2215                    if let Some(sub) = self.try_parse_exists_subquery()? {
2216                        return Ok(Expr::ExistsSubquery {
2217                            subquery: Box::new(sub),
2218                            negated: true,
2219                        });
2220                    }
2221                    let expr = self.parse_primary()?;
2222                    Ok(Expr::UnaryOp(UnaryOp::NotExists, Box::new(expr)))
2223                } else {
2224                    let expr = self.parse_primary()?;
2225                    Ok(Expr::UnaryOp(UnaryOp::Not, Box::new(expr)))
2226                }
2227            }
2228            Token::Exists => {
2229                self.advance();
2230                // `exists (Q)` → ExistsSubquery when followed by a
2231                // parenthesised query. Scalar `exists .field` still parses
2232                // as UnaryOp::Exists for backwards compatibility.
2233                if let Some(sub) = self.try_parse_exists_subquery()? {
2234                    return Ok(Expr::ExistsSubquery {
2235                        subquery: Box::new(sub),
2236                        negated: false,
2237                    });
2238                }
2239                let expr = self.parse_primary()?;
2240                Ok(Expr::UnaryOp(UnaryOp::Exists, Box::new(expr)))
2241            }
2242            Token::LParen => {
2243                self.advance();
2244                let expr = self.parse_expr()?;
2245                self.expect(&Token::RParen)?;
2246                Ok(expr)
2247            }
2248            Token::Ident(name) => {
2249                self.advance();
2250                // `uuid("…")` / `bytes("…")` cast sugar. `uuid`/`bytes` are not
2251                // lexer keywords (so `type T { id: uuid }` and identifiers named
2252                // `uuid` are untouched); an `Ident` immediately followed by `(`
2253                // with a matching name is single-argument cast sugar.
2254                if *self.peek() == Token::LParen {
2255                    let cast_type = match name.as_str() {
2256                        "uuid" => Some(CastType::Uuid),
2257                        "bytes" => Some(CastType::Bytes),
2258                        _ => None,
2259                    };
2260                    if let Some(cast_type) = cast_type {
2261                        self.advance(); // consume `(`
2262                        let inner = self.parse_expr()?;
2263                        self.expect(&Token::RParen)?;
2264                        return Ok(Expr::Cast(Box::new(inner), cast_type));
2265                    }
2266                    // Every function PowQL has is a lexer keyword, so a plain
2267                    // identifier followed by `(` is a call to one it does not
2268                    // have. Reading it as a column and leaving the `(` for the
2269                    // next production is what produced three unrelated messages
2270                    // for one mistake: `column 'foo' not found` in a
2271                    // projection, an unexpected `(` in a filter, and
2272                    // `expected ')', got ','` for anything with two arguments.
2273                    return Err(ParseError::Syntax {
2274                        message: match closest_function(&name) {
2275                            Some(known) => {
2276                                format!("unknown function '{name}'; did you mean '{known}'?")
2277                            }
2278                            None => format!("unknown function '{name}'"),
2279                        },
2280                        position: None,
2281                    });
2282                }
2283                // `alias.field` → QualifiedField. The lexer emits `t1.name` as
2284                // `Ident("t1")` + `DotIdent("name")` (see lexer.rs line 30),
2285                // so a trailing DotIdent here means a qualified reference.
2286                if let Token::DotIdent(field) = self.peek().clone() {
2287                    self.advance();
2288                    // A second `.part` makes this a link path. The projection
2289                    // has its own production for those
2290                    // (`parse_scalar_link_path`) and this parser has none, so
2291                    // without a word here the reader got "unexpected trailing
2292                    // token near token 6: field '.name'", which never says the
2293                    // word link and cannot be told from a typo.
2294                    if let Token::DotIdent(next) = self.peek().clone() {
2295                        return Err(link_traversal_outside_projection(&format!(
2296                            "{name}.{field}.{next}"
2297                        )));
2298                    }
2299                    return Ok(Expr::QualifiedField {
2300                        qualifier: name,
2301                        field,
2302                    });
2303                }
2304                Ok(Expr::Field(name))
2305            }
2306            // Window-only functions: row_number(), rank(), dense_rank()
2307            Token::RowNumber | Token::Rank | Token::DenseRank => {
2308                let wfunc = match self.advance() {
2309                    Token::RowNumber => WindowFunc::RowNumber,
2310                    Token::Rank => WindowFunc::Rank,
2311                    Token::DenseRank => WindowFunc::DenseRank,
2312                    _ => {
2313                        return Err(ParseError::Syntax {
2314                            message: "unexpected window function token".into(),
2315                            position: None,
2316                        })
2317                    }
2318                };
2319                self.expect(&Token::LParen)?;
2320                self.expect(&Token::RParen)?;
2321                let (partition_by, order_by) = self.parse_over_clause()?;
2322                Ok(Expr::Window {
2323                    function: wfunc,
2324                    args: vec![],
2325                    mode: AggregateMode::Symmetric,
2326                    partition_by,
2327                    order_by,
2328                })
2329            }
2330            // Aggregate function calls inside expressions (projections, HAVING).
2331            // Top-level `count(User)` still routes through parse_aggregate_query
2332            // in parse_statement; this arm handles `count(.id)`, `sum(.age)`, etc.
2333            Token::Count | Token::Avg | Token::Sum | Token::Min | Token::Max => {
2334                let mut func = match self.advance() {
2335                    Token::Count => AggFunc::Count,
2336                    Token::Avg => AggFunc::Avg,
2337                    Token::Sum => AggFunc::Sum,
2338                    Token::Min => AggFunc::Min,
2339                    Token::Max => AggFunc::Max,
2340                    _ => {
2341                        return Err(ParseError::Syntax {
2342                            message: "unexpected aggregate token".into(),
2343                            position: None,
2344                        })
2345                    }
2346                };
2347                self.expect(&Token::LParen)?;
2348                let mode = if *self.peek() == Token::Raw {
2349                    self.advance();
2350                    AggregateMode::Raw
2351                } else {
2352                    AggregateMode::Symmetric
2353                };
2354                // count(*) — count all rows including nulls
2355                if func == AggFunc::Count && *self.peek() == Token::Star {
2356                    self.advance();
2357                    self.expect(&Token::RParen)?;
2358                    // Check for OVER — count(*) over (...)
2359                    if *self.peek() == Token::Over {
2360                        let (partition_by, order_by) = self.parse_over_clause()?;
2361                        return Ok(Expr::Window {
2362                            function: WindowFunc::Count,
2363                            args: vec![Expr::Field("*".into())],
2364                            mode,
2365                            partition_by,
2366                            order_by,
2367                        });
2368                    }
2369                    return Ok(Expr::FunctionCall(
2370                        AggFunc::Count,
2371                        Box::new(Expr::Field("*".into())),
2372                        mode,
2373                    ));
2374                }
2375                // count(distinct .field) → CountDistinct
2376                if func == AggFunc::Count && *self.peek() == Token::Distinct {
2377                    self.advance();
2378                    func = AggFunc::CountDistinct;
2379                }
2380                let inner = self.parse_expr()?;
2381                self.expect(&Token::RParen)?;
2382                // Check for OVER — e.g. sum(.salary) over (...)
2383                if *self.peek() == Token::Over {
2384                    let wfunc = match func {
2385                        AggFunc::Count => WindowFunc::Count,
2386                        AggFunc::Avg => WindowFunc::Avg,
2387                        AggFunc::Sum => WindowFunc::Sum,
2388                        AggFunc::Min => WindowFunc::Min,
2389                        AggFunc::Max => WindowFunc::Max,
2390                        _ => {
2391                            return Err(ParseError::Unsupported {
2392                                feature: "count(distinct ...) over (...) is not supported".into(),
2393                            })
2394                        }
2395                    };
2396                    let (partition_by, order_by) = self.parse_over_clause()?;
2397                    return Ok(Expr::Window {
2398                        function: wfunc,
2399                        args: vec![inner],
2400                        mode,
2401                        partition_by,
2402                        order_by,
2403                    });
2404                }
2405                Ok(Expr::FunctionCall(func, Box::new(inner), mode))
2406            }
2407            Token::Upper
2408            | Token::Lower
2409            | Token::Length
2410            | Token::Trim
2411            | Token::Substring
2412            | Token::Concat
2413            | Token::Abs
2414            | Token::Round
2415            | Token::Ceil
2416            | Token::Floor
2417            | Token::Sqrt
2418            | Token::Pow
2419            | Token::Now
2420            | Token::Extract
2421            | Token::DateAdd
2422            | Token::DateDiff
2423            | Token::JsonType
2424            | Token::JsonText => {
2425                let tok = self.advance();
2426                let func = token_to_scalar_fn(&tok);
2427                self.expect(&Token::LParen)?;
2428                let mut args = Vec::new();
2429                while !matches!(self.peek(), Token::RParen | Token::Eof) {
2430                    args.push(self.parse_expr()?);
2431                    if *self.peek() == Token::Comma {
2432                        self.advance();
2433                    }
2434                }
2435                self.expect(&Token::RParen)?;
2436                Ok(Expr::ScalarFunc(func, args))
2437            }
2438            Token::Cast => {
2439                self.advance();
2440                self.expect(&Token::LParen)?;
2441                let inner = self.parse_expr()?;
2442                self.expect(&Token::Comma)?;
2443                let cast_type = self.parse_cast_type()?;
2444                self.expect(&Token::RParen)?;
2445                Ok(Expr::Cast(Box::new(inner), cast_type))
2446            }
2447            Token::Case => {
2448                self.advance();
2449                let mut whens = Vec::new();
2450                while *self.peek() == Token::When {
2451                    self.advance();
2452                    let condition = self.parse_expr()?;
2453                    self.expect(&Token::Then)?;
2454                    let result = self.parse_expr()?;
2455                    whens.push((Box::new(condition), Box::new(result)));
2456                }
2457                let else_expr = if *self.peek() == Token::Else {
2458                    self.advance();
2459                    Some(Box::new(self.parse_expr()?))
2460                } else {
2461                    None
2462                };
2463                self.expect(&Token::End)?;
2464                Ok(Expr::Case { whens, else_expr })
2465            }
2466            t => Err(ParseError::Syntax {
2467                message: format!("unexpected token in expression: {}", t.display_name()),
2468                position: None,
2469            }),
2470        }
2471    }
2472
2473    /// `alter <Table> add [column] [required] <name>: <type>`
2474    /// `alter <Table> drop [column] <name>`
2475    /// Parse a bare link declaration:
2476    /// `link <Owner>.<name> -> <Target> on <local> = <target>`. Called with
2477    /// the cursor on the leading `link` token. Lowers to
2478    /// `Statement::CreateLink`, which the executor routes to
2479    /// `Catalog::create_link`.
2480    fn parse_create_link(&mut self) -> Result<Statement, ParseError> {
2481        self.expect(&Token::Link)?;
2482        let owner = self.expect_named_ident("link owner type")?;
2483        let name = match self.advance() {
2484            Token::DotIdent(n) => n,
2485            t => {
2486                return Err(ParseError::UnexpectedToken {
2487                    expected: "`.<name>` after the owner type (link <Owner>.<name> -> ...)".into(),
2488                    got: t.display_name(),
2489                    position: None,
2490                })
2491            }
2492        };
2493        let (target, local_key, target_key) = self.parse_link_tail()?;
2494        Ok(Statement::CreateLink(CreateLinkExpr {
2495            owner,
2496            name,
2497            target,
2498            local_key,
2499            target_key,
2500        }))
2501    }
2502
2503    /// Parse the shared tail of a link declaration after its name:
2504    /// `-> <Target> on <local> = <target>`. Returns `(target, local, target)`.
2505    fn parse_link_tail(&mut self) -> Result<(String, String, String), ParseError> {
2506        self.expect(&Token::Arrow)?;
2507        let target = self.expect_named_ident("link target type")?;
2508        self.expect(&Token::On)?;
2509        let local_key = self.parse_link_column("link local key")?;
2510        self.expect(&Token::Eq)?;
2511        let target_key = self.parse_link_column("link target key")?;
2512        Ok((target, local_key, target_key))
2513    }
2514
2515    /// Read a column name written either bare (`user_id`) or dot-prefixed
2516    /// (`.user_id`), as used in a link's correlation clause.
2517    fn parse_link_column(&mut self, context: &str) -> Result<String, ParseError> {
2518        match self.advance() {
2519            Token::Ident(n) | Token::DotIdent(n) => Ok(n),
2520            t => Err(self.named_ident_error(context, &t)),
2521        }
2522    }
2523
2524    fn parse_alter_table(&mut self) -> Result<Statement, ParseError> {
2525        self.expect(&Token::Alter)?;
2526        let table = match self.advance() {
2527            Token::Ident(name) => name,
2528            t => {
2529                return Err(ParseError::UnexpectedToken {
2530                    expected: "table name after alter".into(),
2531                    got: t.display_name(),
2532                    position: None,
2533                })
2534            }
2535        };
2536        match self.peek() {
2537            Token::Add => {
2538                self.advance();
2539                // `alter <Table> add index [if not exists] <target>`
2540                if *self.peek() == Token::Index {
2541                    self.advance();
2542                    let if_not_exists = self.parse_optional_if_not_exists();
2543                    let target = self.parse_index_target("add index")?;
2544                    return Ok(Statement::AlterTable(AlterTableExpr {
2545                        table,
2546                        action: AlterAction::AddIndex {
2547                            target,
2548                            if_not_exists,
2549                        },
2550                    }));
2551                }
2552                // `alter <Table> add unique [if not exists] <target>`
2553                if *self.peek() == Token::Unique {
2554                    self.advance();
2555                    let if_not_exists = self.parse_optional_if_not_exists();
2556                    let target = self.parse_index_target("add unique")?;
2557                    return Ok(Statement::AlterTable(AlterTableExpr {
2558                        table,
2559                        action: AlterAction::AddUnique {
2560                            target,
2561                            if_not_exists,
2562                        },
2563                    }));
2564                }
2565                // `alter <Owner> add link <name> -> <Target> on <local> = <target>`
2566                if *self.peek() == Token::Link {
2567                    self.advance();
2568                    let name = self.expect_named_ident("link name")?;
2569                    let (target, local_key, target_key) = self.parse_link_tail()?;
2570                    return Ok(Statement::AlterTable(AlterTableExpr {
2571                        table,
2572                        action: AlterAction::AddLink {
2573                            name,
2574                            target,
2575                            local_key,
2576                            target_key,
2577                        },
2578                    }));
2579                }
2580                // optional `column` keyword
2581                if *self.peek() == Token::Column {
2582                    self.advance();
2583                }
2584                let required = if *self.peek() == Token::Required {
2585                    self.advance();
2586                    true
2587                } else {
2588                    false
2589                };
2590                let name = self.expect_named_ident("column name")?;
2591                self.expect(&Token::Colon)?;
2592                let type_name = match self.advance() {
2593                    Token::Ident(n) => n,
2594                    t => {
2595                        return Err(ParseError::UnexpectedToken {
2596                            expected: "type name".into(),
2597                            got: t.display_name(),
2598                            position: None,
2599                        })
2600                    }
2601                };
2602                Ok(Statement::AlterTable(AlterTableExpr {
2603                    table,
2604                    action: AlterAction::AddColumn {
2605                        name,
2606                        type_name,
2607                        required,
2608                    },
2609                }))
2610            }
2611            Token::Drop => {
2612                self.advance();
2613                if *self.peek() == Token::Index {
2614                    self.advance();
2615                    let if_exists = self.parse_optional_if_exists();
2616                    let target = self.parse_index_target("drop index")?;
2617                    return Ok(Statement::AlterTable(AlterTableExpr {
2618                        table,
2619                        action: AlterAction::DropIndex { target, if_exists },
2620                    }));
2621                }
2622                if *self.peek() == Token::Link {
2623                    self.advance();
2624                    let if_exists = self.parse_optional_if_exists();
2625                    let name = self.expect_named_ident("link name")?;
2626                    return Ok(Statement::DropLink(DropLinkExpr {
2627                        owner: table,
2628                        name,
2629                        if_exists,
2630                    }));
2631                }
2632                // optional `column` keyword
2633                if *self.peek() == Token::Column {
2634                    self.advance();
2635                }
2636                let if_exists = self.parse_optional_if_exists();
2637                let name = self.expect_named_ident("column name")?;
2638                Ok(Statement::AlterTable(AlterTableExpr {
2639                    table,
2640                    action: AlterAction::DropColumn { name, if_exists },
2641                }))
2642            }
2643            t => Err(ParseError::UnexpectedToken {
2644                expected: "add or drop after alter <table>".into(),
2645                got: t.display_name(),
2646                position: None,
2647            }),
2648        }
2649    }
2650
2651    /// Parse a column target (`.slug`) or a parenthesized, unqualified JSON
2652    /// path target (`(.data->slug)`) for ALTER INDEX actions. Parentheses are
2653    /// deliberately the syntax boundary between stored-column and expression
2654    /// indexes so future expression forms cannot silently change old DDL.
2655    fn parse_index_target(&mut self, action: &str) -> Result<IndexTarget, ParseError> {
2656        match self.peek() {
2657            Token::DotIdent(_) => {
2658                if matches!(self.tokens.get(self.pos + 1), Some(Token::Arrow)) {
2659                    return Err(ParseError::Syntax {
2660                        message: format!(
2661                            "JSON path index targets must be parenthesized after {action}; use `(.data->key)`"
2662                        ),
2663                        position: None,
2664                    });
2665                }
2666                let Token::DotIdent(column) = self.advance() else {
2667                    unreachable!("guarded by DotIdent match")
2668                };
2669                Ok(IndexTarget::Column(column))
2670            }
2671            Token::LParen => {
2672                self.advance();
2673                let expr = self.parse_expr().map_err(|error| match error {
2674                    ParseError::NestingDepthExceeded { .. } => error,
2675                    _ => ParseError::Syntax {
2676                        message: format!(
2677                            "invalid expression index target after {action}: expected an unqualified JSON path like `(.data->key)`"
2678                        ),
2679                        position: None,
2680                    },
2681                })?;
2682                if *self.peek() != Token::RParen {
2683                    return Err(ParseError::Syntax {
2684                        message: format!(
2685                            "invalid expression index target after {action}: only a direct JSON path is supported"
2686                        ),
2687                        position: None,
2688                    });
2689                }
2690                self.advance();
2691
2692                match JsonPathIdentityV1::from_expr(&expr) {
2693                    Some(identity) => identity.bind_table_local(None).map(IndexTarget::JsonPath).ok_or_else(|| {
2694                        ParseError::Syntax {
2695                            message: format!(
2696                                "qualified JSON paths are not valid index targets after {action}; use an unqualified table-local path like `(.data->key)`"
2697                            ),
2698                            position: None,
2699                        }
2700                    }),
2701                    None => match expr {
2702                        Expr::Field(_) => Err(ParseError::Syntax {
2703                            message: format!(
2704                                "invalid expression index target after {action}: parentheses are reserved for a direct JSON path like `(.data->key)`; use `.column` for a stored column"
2705                            ),
2706                            position: None,
2707                        }),
2708                        Expr::QualifiedField { .. } => Err(ParseError::Syntax {
2709                            message: format!(
2710                                "qualified references are not valid index targets after {action}; use a table-local `.column` or `(.data->key)`"
2711                            ),
2712                            position: None,
2713                        }),
2714                        _ => Err(ParseError::Syntax {
2715                            message: format!(
2716                                "invalid expression index target after {action}: only a direct JSON path is supported"
2717                            ),
2718                            position: None,
2719                        }),
2720                    },
2721                }
2722            }
2723            token => Err(ParseError::UnexpectedToken {
2724                expected: format!(".<column> or parenthesized JSON path after {action}"),
2725                got: token.display_name(),
2726                position: None,
2727            }),
2728        }
2729    }
2730
2731    /// `drop [if exists] <Table>` or `drop view [if exists] <ViewName>`
2732    fn parse_drop_or_drop_view(&mut self) -> Result<Statement, ParseError> {
2733        self.expect(&Token::Drop)?;
2734        if *self.peek() == Token::Link {
2735            self.advance(); // consume `link`
2736            let if_exists = self.parse_optional_if_exists();
2737            let owner = self.expect_named_ident("link owner type")?;
2738            let name = match self.advance() {
2739                Token::DotIdent(n) => n,
2740                t => {
2741                    return Err(ParseError::UnexpectedToken {
2742                        expected: "`.<name>` after the owner type (drop link <Owner>.<name>)"
2743                            .into(),
2744                        got: t.display_name(),
2745                        position: None,
2746                    })
2747                }
2748            };
2749            return Ok(Statement::DropLink(DropLinkExpr {
2750                owner,
2751                name,
2752                if_exists,
2753            }));
2754        }
2755        if *self.peek() == Token::View {
2756            self.advance(); // consume `view`
2757            let if_exists = self.parse_optional_if_exists();
2758            let name = match self.advance() {
2759                Token::Ident(name) => name,
2760                t => {
2761                    return Err(ParseError::UnexpectedToken {
2762                        expected: "view name after drop view".into(),
2763                        got: t.display_name(),
2764                        position: None,
2765                    })
2766                }
2767            };
2768            return Ok(Statement::DropView(DropViewExpr { name, if_exists }));
2769        }
2770        let if_exists = self.parse_optional_if_exists();
2771        let table = match self.advance() {
2772            Token::Ident(name) => name,
2773            t => {
2774                return Err(ParseError::UnexpectedToken {
2775                    expected: "table name after drop".into(),
2776                    got: t.display_name(),
2777                    position: None,
2778                })
2779            }
2780        };
2781        Ok(Statement::DropTable(DropTableExpr { table, if_exists }))
2782    }
2783
2784    /// `materialize <ViewName> as <Query>`
2785    ///
2786    /// The source query text is captured by slicing the original token stream
2787    /// from the position after `as` to the end.
2788    fn parse_create_view(&mut self) -> Result<Statement, ParseError> {
2789        self.expect(&Token::Materialized)?;
2790        let name = match self.advance() {
2791            Token::Ident(name) => name,
2792            t => {
2793                return Err(ParseError::UnexpectedToken {
2794                    expected: "view name after materialize".into(),
2795                    got: t.display_name(),
2796                    position: None,
2797                })
2798            }
2799        };
2800        self.expect(&Token::As)?;
2801        // Record position so we can reconstruct the query text for storage.
2802        let query_start = self.pos;
2803        let source = match self.advance() {
2804            Token::Ident(s) => s,
2805            t => {
2806                return Err(ParseError::UnexpectedToken {
2807                    expected: "source table name".into(),
2808                    got: t.display_name(),
2809                    position: None,
2810                })
2811            }
2812        };
2813        let query = self.parse_query_tail(source)?;
2814        // Reconstruct query text from tokens for storage and re-execution.
2815        let query_text = tokens_to_text(&self.tokens[query_start..self.pos])?;
2816        Ok(Statement::CreateView(CreateViewExpr {
2817            name,
2818            query,
2819            query_text,
2820        }))
2821    }
2822
2823    /// Check for `union [all]` after a query and build a left-associative
2824    /// chain if present.
2825    fn maybe_parse_union(&mut self, left: Statement) -> Result<Statement, ParseError> {
2826        if *self.peek() != Token::Union {
2827            return Ok(left);
2828        }
2829        if !matches!(left, Statement::Query(_) | Statement::Union(_)) {
2830            return Err(ParseError::Syntax {
2831                message: "UNION requires a query on the left side".into(),
2832                position: None,
2833            });
2834        }
2835        self.advance(); // consume `union`
2836        let all = if let Token::Ident(s) = self.peek() {
2837            if s == "all" {
2838                self.advance();
2839                true
2840            } else {
2841                false
2842            }
2843        } else {
2844            false
2845        };
2846        // Parse the RHS as a single query (not chained — we'll chain ourselves).
2847        let right = self.parse_single_query()?;
2848        let union = Statement::Union(UnionExpr {
2849            left: Box::new(left),
2850            right: Box::new(right),
2851            all,
2852        });
2853        // Recursively check for further chaining: `A union B union C`
2854        self.maybe_parse_union(union)
2855    }
2856
2857    /// Parse a single query statement (no UNION chaining). Used for UNION RHS.
2858    fn parse_single_query(&mut self) -> Result<Statement, ParseError> {
2859        match self.peek() {
2860            Token::Count | Token::Avg | Token::Sum | Token::Min | Token::Max => {
2861                self.parse_aggregate_query()
2862            }
2863            Token::Ident(_) => self.parse_query_or_mutation(),
2864            _ => Err(ParseError::Syntax {
2865                message: format!(
2866                    "expected query after UNION, got {}",
2867                    self.peek().display_name()
2868                ),
2869                position: None,
2870            }),
2871        }
2872    }
2873
2874    /// `refresh <ViewName>`
2875    fn parse_refresh_view(&mut self) -> Result<Statement, ParseError> {
2876        self.expect(&Token::Refresh)?;
2877        let name = match self.advance() {
2878            Token::Ident(name) => name,
2879            t => {
2880                return Err(ParseError::UnexpectedToken {
2881                    expected: "view name after refresh".into(),
2882                    got: t.display_name(),
2883                    position: None,
2884                })
2885            }
2886        };
2887        Ok(Statement::RefreshView(RefreshViewExpr { name }))
2888    }
2889
2890    fn parse_create_type(&mut self) -> Result<Statement, ParseError> {
2891        self.expect(&Token::Type)?;
2892        let name = self.expect_named_ident("type name")?;
2893        let if_not_exists = self.parse_optional_if_not_exists();
2894        self.expect(&Token::LBrace)?;
2895        let mut fields = Vec::new();
2896        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
2897            // Accept `required`, `unique`, and `auto` modifiers in any order.
2898            // A modifier keyword immediately followed by `:` is instead the
2899            // field's *name* (e.g. `required: int`) — leave it for
2900            // `expect_named_ident`, which emits the reserved-word guidance.
2901            let (mut required, mut unique, mut auto) = (false, false, false);
2902            loop {
2903                let is_modifier =
2904                    matches!(self.peek(), Token::Required | Token::Unique | Token::Auto)
2905                        && !matches!(self.tokens.get(self.pos + 1), Some(Token::Colon));
2906                if !is_modifier {
2907                    break;
2908                }
2909                match self.advance() {
2910                    Token::Required => required = true,
2911                    Token::Unique => unique = true,
2912                    Token::Auto => auto = true,
2913                    _ => unreachable!("guarded by is_modifier"),
2914                }
2915            }
2916            let field_name = self.expect_named_ident("field name")?;
2917            self.expect(&Token::Colon)?;
2918            let type_name = match self.advance() {
2919                Token::Ident(n) => n,
2920                t => {
2921                    return Err(ParseError::UnexpectedToken {
2922                        expected: "type name".into(),
2923                        got: t.display_name(),
2924                        position: None,
2925                    })
2926                }
2927            };
2928            // Optional `default <literal>` — value applied when an insert
2929            // omits this column.
2930            let default = if *self.peek() == Token::Default {
2931                self.advance();
2932                Some(self.parse_default_literal()?)
2933            } else {
2934                None
2935            };
2936            fields.push(FieldDef {
2937                name: field_name,
2938                type_name,
2939                required,
2940                unique,
2941                default,
2942                auto,
2943            });
2944            if *self.peek() == Token::Comma {
2945                self.advance();
2946            }
2947        }
2948        self.expect(&Token::RBrace)?;
2949        Ok(Statement::CreateType(CreateTypeExpr {
2950            name,
2951            fields,
2952            if_not_exists,
2953        }))
2954    }
2955
2956    /// `schema`: list all types. `schema links` lists every declared entity
2957    /// link. Any other `schema <Type>` is an alias for `describe <Type>`.
2958    fn parse_schema(&mut self) -> Result<Statement, ParseError> {
2959        self.expect(&Token::Schema)?;
2960        if let Token::Ident(name) = self.peek() {
2961            if name == "links" {
2962                self.advance();
2963                return Ok(Statement::ListLinks);
2964            }
2965            let table = self.expect_named_ident("type name")?;
2966            return Ok(Statement::Describe(table));
2967        }
2968        Ok(Statement::ListTypes)
2969    }
2970
2971    /// `describe <Type>` — the columns and indexes of one type.
2972    fn parse_describe(&mut self) -> Result<Statement, ParseError> {
2973        self.expect(&Token::Describe)?;
2974        let table = self.expect_named_ident("type name")?;
2975        Ok(Statement::Describe(table))
2976    }
2977
2978    /// Parse the literal following a `default` column modifier. Only scalar
2979    /// literals are allowed — expression defaults (e.g. `now()`) are not yet
2980    /// supported.
2981    fn parse_default_literal(&mut self) -> Result<Literal, ParseError> {
2982        match self.advance() {
2983            Token::IntLit(v) => Ok(Literal::Int(v)),
2984            Token::FloatLit(v) => Ok(Literal::Float(v)),
2985            Token::StringLit(v) => Ok(Literal::String(v)),
2986            Token::BoolLit(v) => Ok(Literal::Bool(v)),
2987            t => Err(ParseError::UnexpectedToken {
2988                expected: "literal default value".into(),
2989                got: t.display_name(),
2990                position: None,
2991            }),
2992        }
2993    }
2994}
2995
2996/// Rewrite every bare `Expr::Field(f)` in `expr` into
2997/// `Expr::QualifiedField { qualifier: alias, field: f }`, so a block link
2998/// traversal's bare child columns (`total`) match the qualified spelling
2999/// (`o.total`) the planner expects. Descends through operators but stops at a
3000/// nested query (its own scope owns its qualification) and leaves already
3001/// qualified references untouched.
3002fn qualify_bare_fields(expr: Expr, alias: &str) -> Expr {
3003    let recur = |e: Expr| Box::new(qualify_bare_fields(e, alias));
3004    match expr {
3005        Expr::Field(field) => Expr::QualifiedField {
3006            qualifier: alias.to_string(),
3007            field,
3008        },
3009        Expr::BinaryOp(l, op, r) => Expr::BinaryOp(recur(*l), op, recur(*r)),
3010        Expr::UnaryOp(op, inner) => Expr::UnaryOp(op, recur(*inner)),
3011        Expr::Coalesce(l, r) => Expr::Coalesce(recur(*l), recur(*r)),
3012        Expr::Cast(inner, ty) => Expr::Cast(recur(*inner), ty),
3013        Expr::ScalarFunc(func, args) => Expr::ScalarFunc(
3014            func,
3015            args.into_iter()
3016                .map(|a| qualify_bare_fields(a, alias))
3017                .collect(),
3018        ),
3019        Expr::InList {
3020            expr,
3021            list,
3022            negated,
3023        } => Expr::InList {
3024            expr: recur(*expr),
3025            list: list
3026                .into_iter()
3027                .map(|a| qualify_bare_fields(a, alias))
3028                .collect(),
3029            negated,
3030        },
3031        Expr::Case { whens, else_expr } => Expr::Case {
3032            whens: whens
3033                .into_iter()
3034                .map(|(c, r)| (recur(*c), recur(*r)))
3035                .collect(),
3036            else_expr: else_expr.map(|e| recur(*e)),
3037        },
3038        Expr::JsonPath { base, segments } => Expr::JsonPath {
3039            base: recur(*base),
3040            segments,
3041        },
3042        // Leaves and cross-scope nodes are left as-is.
3043        other => other,
3044    }
3045}
3046
3047/// True when `text` lexes to exactly the one token `tok` (plus EOF).
3048///
3049/// Lets the writers below *ask the lexer* which spelling round-trips instead
3050/// of re-deriving its rules, so the two can never drift apart.
3051fn relexes_to(text: &str, tok: &Token) -> bool {
3052    match lex(text) {
3053        Ok(toks) => matches!(toks.as_slice(), [t, Token::Eof] if t == tok),
3054        Err(_) => false,
3055    }
3056}
3057
3058/// Write a string literal in source form, escaping what the lexer decodes.
3059///
3060/// Exact inverse of the lexer's string rule: it turns `\"`, `\\`, `\n` and
3061/// `\t` into `"`, `\`, LF and TAB, copies every other character through
3062/// verbatim, and swallows the backslash of any other escape (`\r` decodes to
3063/// `r`). So those four are the only escapes that may be emitted, and every
3064/// other character (CR included) must be written raw.
3065fn push_string_literal(out: &mut String, s: &str) {
3066    out.push('"');
3067    for c in s.chars() {
3068        match c {
3069            '"' => out.push_str("\\\""),
3070            '\\' => out.push_str("\\\\"),
3071            '\n' => out.push_str("\\n"),
3072            '\t' => out.push_str("\\t"),
3073            other => out.push(other),
3074        }
3075    }
3076    out.push('"');
3077}
3078
3079/// Write an identifier (`prefix` is `""` for a bare name, `"."` for a field
3080/// reference) in whichever spelling re-lexes to `tok` itself: bare when the
3081/// lexer reads it back as this exact token, backtick-quoted otherwise.
3082///
3083/// Quoting is not cosmetic. `` `order` `` lexes to `Ident("order")`; written
3084/// back bare it re-lexes to the *keyword* `order`, and the stored view runs a
3085/// different query. Same for names with spaces, leading digits, or symbols.
3086fn push_ident(out: &mut String, prefix: &str, name: &str, tok: &Token) -> Result<(), ParseError> {
3087    let bare = format!("{prefix}{name}");
3088    if relexes_to(&bare, tok) {
3089        out.push_str(&bare);
3090        return Ok(());
3091    }
3092    let quoted = format!("{prefix}`{name}`");
3093    if relexes_to(&quoted, tok) {
3094        out.push_str(&quoted);
3095        return Ok(());
3096    }
3097    Err(ParseError::Unsupported {
3098        feature: format!(
3099            "cannot store view source: identifier '{name}' has no PowQL spelling that reads back unchanged"
3100        ),
3101    })
3102}
3103
3104/// Reconstruct PowQL source text from a slice of tokens. Used to store the
3105/// view's source query for re-execution on refresh.
3106///
3107/// Whitespace is normalised, but the token stream is not: re-lexing the result
3108/// yields exactly the tokens passed in. That is the whole contract. A
3109/// reconstruction that re-lexes to *different* tokens makes the stored view run
3110/// a different query than the user wrote, with no error anywhere, so the result
3111/// is verified against the lexer before it is returned and a token with no
3112/// faithful spelling is a typed error rather than a quiet mismatch.
3113fn tokens_to_text(tokens: &[Token]) -> Result<String, ParseError> {
3114    let mut out = String::with_capacity(64);
3115    for tok in tokens {
3116        if !out.is_empty() && !matches!(tok, Token::Eof) {
3117            out.push(' ');
3118        }
3119        match tok {
3120            Token::Ident(s) => push_ident(&mut out, "", s, tok)?,
3121            Token::DotIdent(s) => push_ident(&mut out, ".", s, tok)?,
3122            Token::IntLit(v) => out.push_str(&v.to_string()),
3123            Token::FloatLit(v) => {
3124                if !v.is_finite() {
3125                    return Err(ParseError::Unsupported {
3126                        feature: "cannot store view source: non-finite number literal".into(),
3127                    });
3128                }
3129                let rendered = v.to_string();
3130                out.push_str(&rendered);
3131                // `Display` drops a redundant fraction (`3.0` prints as `3`,
3132                // `-0.0` as `-0`), and the lexer only reads a float when a
3133                // digit follows a dot, so without this the literal comes back
3134                // as an INTEGER token.
3135                if !rendered.contains('.') {
3136                    out.push_str(".0");
3137                }
3138            }
3139            Token::StringLit(s) => push_string_literal(&mut out, s),
3140            Token::BoolLit(v) => out.push_str(if *v { "true" } else { "false" }),
3141            Token::Param(s) => {
3142                out.push('$');
3143                out.push_str(s);
3144            }
3145            Token::Type => out.push_str("type"),
3146            Token::Filter => out.push_str("filter"),
3147            Token::Order => out.push_str("order"),
3148            Token::Limit => out.push_str("limit"),
3149            Token::Offset => out.push_str("offset"),
3150            Token::Insert => out.push_str("insert"),
3151            Token::Update => out.push_str("update"),
3152            Token::Delete => out.push_str("delete"),
3153            Token::Upsert => out.push_str("upsert"),
3154            Token::Returning => out.push_str("returning"),
3155            Token::Conflict => out.push_str("conflict"),
3156            Token::Select => out.push_str("select"),
3157            Token::Required => out.push_str("required"),
3158            Token::Default => out.push_str("default"),
3159            Token::Auto => out.push_str("auto"),
3160            Token::Multi => out.push_str("multi"),
3161            Token::Link => out.push_str("link"),
3162            Token::Index => out.push_str("index"),
3163            Token::Unique => out.push_str("unique"),
3164            Token::On => out.push_str("on"),
3165            Token::Asc => out.push_str("asc"),
3166            Token::Desc => out.push_str("desc"),
3167            Token::And => out.push_str("and"),
3168            Token::Or => out.push_str("or"),
3169            Token::Not => out.push_str("not"),
3170            Token::Exists => out.push_str("exists"),
3171            Token::Let => out.push_str("let"),
3172            Token::As => out.push_str("as"),
3173            Token::Match => out.push_str("match"),
3174            Token::Group => out.push_str("group"),
3175            Token::Join => out.push_str("join"),
3176            Token::Inner => out.push_str("inner"),
3177            Token::LeftKw => out.push_str("left"),
3178            Token::RightKw => out.push_str("right"),
3179            Token::Outer => out.push_str("outer"),
3180            Token::Cross => out.push_str("cross"),
3181            Token::Transaction => out.push_str("transaction"),
3182            Token::Begin => out.push_str("begin"),
3183            Token::Commit => out.push_str("commit"),
3184            Token::Rollback => out.push_str("rollback"),
3185            Token::View => out.push_str("view"),
3186            Token::Materialized => out.push_str("materialized"),
3187            Token::Refresh => out.push_str("refresh"),
3188            Token::Union => out.push_str("union"),
3189            Token::Having => out.push_str("having"),
3190            Token::Distinct => out.push_str("distinct"),
3191            Token::In => out.push_str("in"),
3192            Token::Between => out.push_str("between"),
3193            Token::Like => out.push_str("like"),
3194            Token::Count => out.push_str("count"),
3195            Token::Avg => out.push_str("avg"),
3196            Token::Sum => out.push_str("sum"),
3197            Token::Raw => out.push_str("raw"),
3198            Token::Min => out.push_str("min"),
3199            Token::Max => out.push_str("max"),
3200            Token::Is => out.push_str("is"),
3201            Token::Null => out.push_str("null"),
3202            Token::Upper => out.push_str("upper"),
3203            Token::Lower => out.push_str("lower"),
3204            Token::Length => out.push_str("length"),
3205            Token::Trim => out.push_str("trim"),
3206            Token::Substring => out.push_str("substring"),
3207            Token::Concat => out.push_str("concat"),
3208            Token::Abs => out.push_str("abs"),
3209            Token::Round => out.push_str("round"),
3210            Token::Ceil => out.push_str("ceil"),
3211            Token::Floor => out.push_str("floor"),
3212            Token::Sqrt => out.push_str("sqrt"),
3213            Token::Pow => out.push_str("pow"),
3214            Token::Now => out.push_str("now"),
3215            Token::Extract => out.push_str("extract"),
3216            Token::DateAdd => out.push_str("date_add"),
3217            Token::DateDiff => out.push_str("date_diff"),
3218            Token::JsonType => out.push_str("json_type"),
3219            Token::JsonText => out.push_str("json_text"),
3220            Token::Cast => out.push_str("cast"),
3221            Token::Case => out.push_str("case"),
3222            Token::When => out.push_str("when"),
3223            Token::Then => out.push_str("then"),
3224            Token::Else => out.push_str("else"),
3225            Token::End => out.push_str("end"),
3226            Token::Over => out.push_str("over"),
3227            Token::Partition => out.push_str("partition"),
3228            Token::RowNumber => out.push_str("row_number"),
3229            Token::Rank => out.push_str("rank"),
3230            Token::DenseRank => out.push_str("dense_rank"),
3231            Token::Alter => out.push_str("alter"),
3232            Token::Drop => out.push_str("drop"),
3233            Token::Add => out.push_str("add"),
3234            Token::Column => out.push_str("column"),
3235            Token::Eq => out.push('='),
3236            Token::Neq => out.push_str("!="),
3237            Token::Lt => out.push('<'),
3238            Token::Gt => out.push('>'),
3239            Token::Lte => out.push_str("<="),
3240            Token::Gte => out.push_str(">="),
3241            Token::Assign => out.push_str(":="),
3242            Token::Arrow => out.push_str("->"),
3243            Token::Pipe => out.push('|'),
3244            Token::Coalesce => out.push_str("??"),
3245            Token::Plus => out.push('+'),
3246            Token::Minus => out.push('-'),
3247            Token::Star => out.push('*'),
3248            Token::Slash => out.push('/'),
3249            Token::LBrace => out.push('{'),
3250            Token::RBrace => out.push('}'),
3251            Token::LParen => out.push('('),
3252            Token::RParen => out.push(')'),
3253            Token::Comma => out.push(','),
3254            Token::Colon => out.push(':'),
3255            Token::Dot => out.push('.'),
3256            Token::Explain => out.push_str("explain"),
3257            Token::Schema => out.push_str("schema"),
3258            Token::Describe => out.push_str("describe"),
3259            Token::Eof => {}
3260        }
3261    }
3262    // Verify rather than trust. Every arm above is meant to be the lexer's
3263    // inverse, but a wrong one is invisible: it stores a query that merely
3264    // *looks* like the user's. Re-lex the result and require the same stream
3265    // back, so any arm that is (or later becomes) wrong fails loudly here
3266    // instead of silently redefining someone's view. EOF is dropped, since a
3267    // slice of the middle of a stream never carries one.
3268    let mut expected: Vec<Token> = tokens
3269        .iter()
3270        .filter(|t| **t != Token::Eof)
3271        .cloned()
3272        .collect();
3273    expected.push(Token::Eof);
3274    match lex(&out) {
3275        Ok(round_tripped) if round_tripped == expected => Ok(out),
3276        _ => Err(ParseError::Unsupported {
3277            feature: "cannot store view source: query text does not read back unchanged".into(),
3278        }),
3279    }
3280}
3281
3282#[cfg(test)]
3283mod tests {
3284    use super::*;
3285    #[test]
3286    fn test_parse_simple_query() {
3287        let stmt = parse("User").unwrap();
3288        match stmt {
3289            Statement::Query(q) => {
3290                assert_eq!(q.source, "User");
3291                assert!(q.filter.is_none());
3292                assert!(q.projection.is_none());
3293            }
3294            _ => panic!("expected query"),
3295        }
3296    }
3297
3298    #[test]
3299    fn test_parse_filter() {
3300        let stmt = parse("User filter .age > 30").unwrap();
3301        match stmt {
3302            Statement::Query(q) => {
3303                assert_eq!(q.source, "User");
3304                assert!(q.filter.is_some());
3305            }
3306            _ => panic!("expected query"),
3307        }
3308    }
3309
3310    #[test]
3311    fn test_parse_projection() {
3312        let stmt = parse("User { name, email }").unwrap();
3313        match stmt {
3314            Statement::Query(q) => {
3315                let proj = q.projection.unwrap();
3316                assert_eq!(proj.len(), 2);
3317            }
3318            _ => panic!("expected query"),
3319        }
3320    }
3321
3322    #[test]
3323    fn test_bare_dotted_path_projection_is_a_parse_error() {
3324        // `.user.name` without an outer alias is token-identical to two
3325        // comma-less fields, so it is rejected with alias guidance instead of
3326        // silently parsing as two fields (2026-07-23 plan-quality audit P3).
3327        for q in [
3328            "Order { .user.name }",
3329            "Order { .id, uname: .user.name }",
3330            "Order { uname: .user.company.name }",
3331        ] {
3332            let err = parse(q).unwrap_err();
3333            let msg = err.to_string();
3334            assert!(
3335                msg.contains("alias the table"),
3336                "`{q}` should error with alias guidance, got: {msg}"
3337            );
3338        }
3339    }
3340
3341    #[test]
3342    fn test_aliased_scalar_link_path_parses_as_one_field() {
3343        let stmt = parse("Order as o { uname: o.user.name }").unwrap();
3344        match stmt {
3345            Statement::Query(q) => {
3346                let proj = q.projection.unwrap();
3347                assert_eq!(proj.len(), 1);
3348                match &proj[0].expr {
3349                    Expr::LinkPath {
3350                        outer_alias,
3351                        links,
3352                        column,
3353                    } => {
3354                        assert_eq!(outer_alias, "o");
3355                        assert_eq!(links, &["user".to_string()]);
3356                        assert_eq!(column, "name");
3357                    }
3358                    other => panic!("expected LinkPath, got {other:?}"),
3359                }
3360            }
3361            _ => panic!("expected query"),
3362        }
3363    }
3364
3365    #[test]
3366    fn test_parse_filter_order_limit() {
3367        let stmt = parse("User filter .age > 30 order .name desc limit 10").unwrap();
3368        match stmt {
3369            Statement::Query(q) => {
3370                assert!(q.filter.is_some());
3371                let order = q.order.unwrap();
3372                assert_eq!(order.keys.len(), 1);
3373                assert_eq!(order.keys[0].expr, Expr::Field("name".into()));
3374                assert!(order.keys[0].descending);
3375                assert!(q.limit.is_some());
3376            }
3377            _ => panic!("expected query"),
3378        }
3379    }
3380
3381    #[test]
3382    fn test_parse_insert() {
3383        let stmt = parse(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
3384        match stmt {
3385            Statement::Insert(ins) => {
3386                assert_eq!(ins.target, "User");
3387                assert_eq!(ins.rows.len(), 1);
3388                assert_eq!(ins.rows[0].len(), 2);
3389                assert_eq!(ins.rows[0][0].field, "name");
3390                assert_eq!(ins.rows[0][1].field, "age");
3391            }
3392            _ => panic!("expected insert"),
3393        }
3394    }
3395
3396    #[test]
3397    fn test_parse_insert_multi_row() {
3398        let stmt =
3399            parse(r#"insert User { name := "Alice", age := 30 }, { name := "Bob", age := 25 }, { name := "Cy" }"#)
3400                .unwrap();
3401        match stmt {
3402            Statement::Insert(ins) => {
3403                assert_eq!(ins.target, "User");
3404                assert_eq!(ins.rows.len(), 3);
3405                assert_eq!(ins.rows[0].len(), 2);
3406                assert_eq!(ins.rows[1][0].field, "name");
3407                assert_eq!(ins.rows[2].len(), 1);
3408                assert_eq!(ins.rows[2][0].field, "name");
3409            }
3410            _ => panic!("expected insert"),
3411        }
3412    }
3413
3414    #[test]
3415    fn test_parse_update() {
3416        let stmt = parse(r#"User filter .email = "alice@ex.com" update { age := 31 }"#).unwrap();
3417        match stmt {
3418            Statement::UpdateQuery(upd) => {
3419                assert_eq!(upd.source, "User");
3420                assert!(upd.filter.is_some());
3421                assert_eq!(upd.assignments.len(), 1);
3422                assert!(!upd.returning);
3423            }
3424            _ => panic!("expected update"),
3425        }
3426    }
3427
3428    #[test]
3429    fn test_parse_update_returning() {
3430        let stmt = parse(r#"User filter .name = "Alice" update { age := 31 } returning"#).unwrap();
3431        match stmt {
3432            Statement::UpdateQuery(upd) => assert!(upd.returning),
3433            _ => panic!("expected update"),
3434        }
3435    }
3436
3437    #[test]
3438    fn test_parse_delete() {
3439        let stmt = parse("User filter .age < 18 delete").unwrap();
3440        match stmt {
3441            Statement::DeleteQuery(del) => {
3442                assert_eq!(del.source, "User");
3443                assert!(del.filter.is_some());
3444                assert!(!del.returning);
3445            }
3446            _ => panic!("expected delete"),
3447        }
3448    }
3449
3450    #[test]
3451    fn test_parse_delete_returning() {
3452        let stmt = parse("User filter .age < 18 delete returning").unwrap();
3453        match stmt {
3454            Statement::DeleteQuery(del) => assert!(del.returning),
3455            _ => panic!("expected delete"),
3456        }
3457    }
3458
3459    #[test]
3460    fn test_parse_count() {
3461        let stmt = parse("count(User)").unwrap();
3462        match stmt {
3463            Statement::Query(q) => {
3464                let agg = q.aggregation.unwrap();
3465                assert_eq!(agg.function, AggFunc::Count);
3466                assert!(q.filter.is_none());
3467            }
3468            _ => panic!("expected query with aggregation"),
3469        }
3470    }
3471
3472    #[test]
3473    fn test_parse_count_with_filter() {
3474        // Regression: previously returned "expected RParen, got Filter".
3475        // count(<query>) must accept a full read-pipeline tail.
3476        let stmt = parse("count(User filter .age > 30)").unwrap();
3477        match stmt {
3478            Statement::Query(q) => {
3479                assert_eq!(q.source, "User");
3480                let agg = q.aggregation.unwrap();
3481                assert_eq!(agg.function, AggFunc::Count);
3482                assert!(q.filter.is_some(), "filter should have been parsed");
3483            }
3484            _ => panic!("expected query with aggregation"),
3485        }
3486    }
3487
3488    #[test]
3489    fn test_parse_count_with_filter_and_limit() {
3490        let stmt = parse("count(User filter .age > 30 limit 100)").unwrap();
3491        match stmt {
3492            Statement::Query(q) => {
3493                assert_eq!(q.source, "User");
3494                assert!(q.filter.is_some());
3495                assert!(q.limit.is_some());
3496                assert_eq!(q.aggregation.unwrap().function, AggFunc::Count);
3497            }
3498            _ => panic!("expected query with aggregation"),
3499        }
3500    }
3501
3502    #[test]
3503    fn test_parse_create_type() {
3504        let stmt = parse("type User { required name: str, age: int }").unwrap();
3505        match stmt {
3506            Statement::CreateType(ct) => {
3507                assert_eq!(ct.name, "User");
3508                assert_eq!(ct.fields.len(), 2);
3509                assert!(ct.fields[0].required);
3510                assert!(!ct.fields[1].required);
3511            }
3512            _ => panic!("expected create type"),
3513        }
3514    }
3515
3516    #[test]
3517    fn test_parse_sum_with_field_projection() {
3518        // `sum(... { .age })` should lift `.age` into AggregateExpr.argument and
3519        // clear the projection so the executor's aggregate fast path fires.
3520        let stmt = parse("sum(User filter .age > 30 { .age })").unwrap();
3521        match stmt {
3522            Statement::Query(q) => {
3523                let agg = q.aggregation.expect("aggregate");
3524                assert_eq!(agg.function, AggFunc::Sum);
3525                assert_eq!(agg.argument, Some(Expr::Field("age".into())));
3526                assert!(
3527                    q.projection.is_none(),
3528                    "projection should be lifted into agg.field"
3529                );
3530            }
3531            _ => panic!("expected query"),
3532        }
3533    }
3534
3535    #[test]
3536    fn test_parse_raw_aggregate_modes() {
3537        let Statement::Query(top_level) = parse("sum(raw User { .age })").unwrap() else {
3538            panic!("expected query");
3539        };
3540        assert_eq!(top_level.aggregation.unwrap().mode, AggregateMode::Raw);
3541
3542        let Statement::Query(grouped) = parse("User group .dept { total: sum(raw .age) }").unwrap()
3543        else {
3544            panic!("expected query");
3545        };
3546        assert!(matches!(
3547            grouped.projection.unwrap()[0].expr,
3548            Expr::FunctionCall(AggFunc::Sum, _, AggregateMode::Raw)
3549        ));
3550    }
3551
3552    #[test]
3553    fn test_parse_avg_min_max_with_field() {
3554        for (src, expected) in [
3555            ("avg(User { .age })", AggFunc::Avg),
3556            ("min(User { .age })", AggFunc::Min),
3557            ("max(User { .age })", AggFunc::Max),
3558        ] {
3559            let stmt = parse(src).unwrap();
3560            match stmt {
3561                Statement::Query(q) => {
3562                    let agg = q.aggregation.unwrap();
3563                    assert_eq!(agg.function, expected, "func mismatch for {src}");
3564                    assert_eq!(
3565                        agg.argument,
3566                        Some(Expr::Field("age".into())),
3567                        "field mismatch for {src}"
3568                    );
3569                    assert!(
3570                        q.projection.is_none(),
3571                        "projection should be cleared for {src}"
3572                    );
3573                }
3574                _ => panic!("expected query for {src}"),
3575            }
3576        }
3577    }
3578
3579    #[test]
3580    fn test_parse_count_lifts_projection_into_argument() {
3581        // A projected column names what to count: `count(User { .age })` is a
3582        // non-null count of `.age`, matching the grouped `count(.age)` path and
3583        // SQL's `COUNT(age)`. Leaving the projection in place (the previous
3584        // behavior) silently made it a row count.
3585        let stmt = parse("count(User { .age })").unwrap();
3586        match stmt {
3587            Statement::Query(q) => {
3588                let agg = q.aggregation.unwrap();
3589                assert_eq!(agg.function, AggFunc::Count);
3590                assert_eq!(agg.argument, Some(Expr::Field("age".into())));
3591                assert!(q.projection.is_none(), "projection should be lifted");
3592            }
3593            _ => panic!("expected query"),
3594        }
3595    }
3596
3597    #[test]
3598    fn test_parse_count_without_projection_has_no_argument() {
3599        // `count(User)` stays a row count.
3600        let stmt = parse("count(User)").unwrap();
3601        match stmt {
3602            Statement::Query(q) => {
3603                let agg = q.aggregation.unwrap();
3604                assert_eq!(agg.function, AggFunc::Count);
3605                assert!(agg.argument.is_none());
3606            }
3607            _ => panic!("expected query"),
3608        }
3609    }
3610
3611    // ---- Mission E1.1: JOIN parser tests ----------------------------------
3612    // Parser-level only. The planner rejects joins with a clean error until
3613    // E1.2 wires up execution.
3614
3615    #[test]
3616    fn test_parse_source_alias() {
3617        let stmt = parse("User as u filter u.age > 30").unwrap();
3618        match stmt {
3619            Statement::Query(q) => {
3620                assert_eq!(q.source, "User");
3621                assert_eq!(q.alias.as_deref(), Some("u"));
3622                assert!(q.joins.is_empty());
3623                match q.filter.unwrap() {
3624                    Expr::BinaryOp(l, BinOp::Gt, _) => match *l {
3625                        Expr::QualifiedField { qualifier, field } => {
3626                            assert_eq!(qualifier, "u");
3627                            assert_eq!(field, "age");
3628                        }
3629                        other => panic!("expected qualified field, got {other:?}"),
3630                    },
3631                    other => panic!("expected >, got {other:?}"),
3632                }
3633            }
3634            _ => panic!("expected query"),
3635        }
3636    }
3637
3638    #[test]
3639    fn test_parse_inner_join_on() {
3640        let stmt = parse("User as u inner join Order as o on u.id = o.user_id").unwrap();
3641        match stmt {
3642            Statement::Query(q) => {
3643                assert_eq!(q.source, "User");
3644                assert_eq!(q.alias.as_deref(), Some("u"));
3645                assert_eq!(q.joins.len(), 1);
3646                let j = &q.joins[0];
3647                assert_eq!(j.kind, JoinKind::Inner);
3648                assert_eq!(j.source, "Order");
3649                assert_eq!(j.alias.as_deref(), Some("o"));
3650                let on = j.on.as_ref().expect("on clause");
3651                match on {
3652                    Expr::BinaryOp(l, BinOp::Eq, r) => {
3653                        assert!(matches!(**l, Expr::QualifiedField { .. }));
3654                        assert!(matches!(**r, Expr::QualifiedField { .. }));
3655                    }
3656                    other => panic!("expected eq, got {other:?}"),
3657                }
3658            }
3659            _ => panic!("expected query"),
3660        }
3661    }
3662
3663    #[test]
3664    fn test_parse_bare_join_defaults_to_inner() {
3665        let stmt = parse("User join Order on User.id = Order.user_id").unwrap();
3666        match stmt {
3667            Statement::Query(q) => {
3668                assert_eq!(q.joins.len(), 1);
3669                assert_eq!(q.joins[0].kind, JoinKind::Inner);
3670            }
3671            _ => panic!("expected query"),
3672        }
3673    }
3674
3675    #[test]
3676    fn test_parse_left_outer_join() {
3677        let stmt = parse("User as u left outer join Order as o on u.id = o.user_id").unwrap();
3678        match stmt {
3679            Statement::Query(q) => {
3680                assert_eq!(q.joins.len(), 1);
3681                assert_eq!(q.joins[0].kind, JoinKind::LeftOuter);
3682            }
3683            _ => panic!("expected query"),
3684        }
3685    }
3686
3687    #[test]
3688    fn test_parse_left_join_without_outer_keyword() {
3689        // `left join` is shorthand for `left outer join` in SQL — we accept it.
3690        let stmt = parse("User as u left join Order as o on u.id = o.user_id").unwrap();
3691        match stmt {
3692            Statement::Query(q) => {
3693                assert_eq!(q.joins[0].kind, JoinKind::LeftOuter);
3694            }
3695            _ => panic!("expected query"),
3696        }
3697    }
3698
3699    #[test]
3700    fn test_parse_right_join() {
3701        let stmt = parse("User as u right join Order as o on u.id = o.user_id").unwrap();
3702        match stmt {
3703            Statement::Query(q) => {
3704                assert_eq!(q.joins[0].kind, JoinKind::RightOuter);
3705            }
3706            _ => panic!("expected query"),
3707        }
3708    }
3709
3710    #[test]
3711    fn test_parse_cross_join_has_no_on() {
3712        let stmt = parse("User cross join Order").unwrap();
3713        match stmt {
3714            Statement::Query(q) => {
3715                assert_eq!(q.joins[0].kind, JoinKind::Cross);
3716                assert!(q.joins[0].on.is_none());
3717            }
3718            _ => panic!("expected query"),
3719        }
3720    }
3721
3722    #[test]
3723    fn test_parse_multi_join_chain() {
3724        let stmt = parse(
3725            "User as u join Order as o on u.id = o.user_id \
3726             join Product as p on o.product_id = p.id",
3727        )
3728        .unwrap();
3729        match stmt {
3730            Statement::Query(q) => {
3731                assert_eq!(q.joins.len(), 2);
3732                assert_eq!(q.joins[0].source, "Order");
3733                assert_eq!(q.joins[1].source, "Product");
3734            }
3735            _ => panic!("expected query"),
3736        }
3737    }
3738
3739    #[test]
3740    fn test_parse_join_with_filter_tail() {
3741        // Filter/order/limit still work after a join clause.
3742        let stmt = parse(
3743            "User as u join Order as o on u.id = o.user_id \
3744             filter o.total > 100 order .name limit 10",
3745        )
3746        .unwrap();
3747        match stmt {
3748            Statement::Query(q) => {
3749                assert_eq!(q.joins.len(), 1);
3750                assert!(q.filter.is_some());
3751                assert!(q.order.is_some());
3752                assert!(q.limit.is_some());
3753            }
3754            _ => panic!("expected query"),
3755        }
3756    }
3757
3758    #[test]
3759    fn test_parse_join_requires_on_for_inner() {
3760        // Non-cross joins require `on <expr>`. Missing `on` is a parse error.
3761        let err = parse("User join Order").unwrap_err();
3762        assert!(
3763            err.message().contains("on"),
3764            "expected on-clause error, got {:?}",
3765            err.message()
3766        );
3767    }
3768
3769    #[test]
3770    fn test_parse_update_on_joined_query_errors() {
3771        // E1.1 explicitly rejects update/delete on joined queries — SQL
3772        // semantics here are messy and we're not implementing them yet.
3773        let err =
3774            parse("User as u join Order as o on u.id = o.user_id update { age := 1 }").unwrap_err();
3775        assert!(err.message().contains("update"));
3776    }
3777
3778    #[test]
3779    fn test_parse_delete_on_joined_query_errors() {
3780        let err = parse("User as u join Order as o on u.id = o.user_id delete").unwrap_err();
3781        assert!(err.message().contains("delete"));
3782    }
3783
3784    // ---- Mission E2a: DISTINCT + IN-list + BETWEEN + LIKE -----------------
3785
3786    #[test]
3787    fn test_parse_distinct() {
3788        let stmt = parse("User distinct { .name }").unwrap();
3789        match stmt {
3790            Statement::Query(q) => {
3791                assert!(q.distinct);
3792                assert!(q.projection.is_some());
3793            }
3794            _ => panic!("expected query"),
3795        }
3796    }
3797
3798    #[test]
3799    fn test_parse_in_list() {
3800        let stmt = parse(r#"User filter .name in ("Alice", "Bob")"#).unwrap();
3801        match stmt {
3802            Statement::Query(q) => match q.filter.unwrap() {
3803                Expr::InList {
3804                    expr,
3805                    list,
3806                    negated,
3807                } => {
3808                    assert!(!negated);
3809                    assert!(matches!(*expr, Expr::Field(f) if f == "name"));
3810                    assert_eq!(list.len(), 2);
3811                }
3812                other => panic!("expected InList, got {other:?}"),
3813            },
3814            _ => panic!("expected query"),
3815        }
3816    }
3817
3818    #[test]
3819    fn test_parse_not_in_list() {
3820        let stmt = parse("User filter .age not in (1, 2, 3)").unwrap();
3821        match stmt {
3822            Statement::Query(q) => match q.filter.unwrap() {
3823                Expr::InList { negated, list, .. } => {
3824                    assert!(negated);
3825                    assert_eq!(list.len(), 3);
3826                }
3827                other => panic!("expected InList, got {other:?}"),
3828            },
3829            _ => panic!("expected query"),
3830        }
3831    }
3832
3833    #[test]
3834    fn test_parse_between() {
3835        // BETWEEN desugars into >= AND <=.
3836        let stmt = parse("User filter .age between 10 and 20").unwrap();
3837        match stmt {
3838            Statement::Query(q) => {
3839                match q.filter.unwrap() {
3840                    Expr::BinaryOp(_, BinOp::And, _) => {} // desugared
3841                    other => panic!("expected And (desugared between), got {other:?}"),
3842                }
3843            }
3844            _ => panic!("expected query"),
3845        }
3846    }
3847
3848    #[test]
3849    fn test_parse_not_between() {
3850        // NOT BETWEEN desugars into < OR >.
3851        let stmt = parse("User filter .age not between 10 and 20").unwrap();
3852        match stmt {
3853            Statement::Query(q) => {
3854                match q.filter.unwrap() {
3855                    Expr::BinaryOp(_, BinOp::Or, _) => {} // desugared
3856                    other => panic!("expected Or (desugared not between), got {other:?}"),
3857                }
3858            }
3859            _ => panic!("expected query"),
3860        }
3861    }
3862
3863    #[test]
3864    fn test_parse_like() {
3865        let stmt = parse(r#"User filter .name like "A%""#).unwrap();
3866        match stmt {
3867            Statement::Query(q) => match q.filter.unwrap() {
3868                Expr::BinaryOp(l, BinOp::Like, r) => {
3869                    assert!(matches!(*l, Expr::Field(f) if f == "name"));
3870                    assert!(matches!(*r, Expr::Literal(Literal::String(s)) if s == "A%"));
3871                }
3872                other => panic!("expected Like, got {other:?}"),
3873            },
3874            _ => panic!("expected query"),
3875        }
3876    }
3877
3878    #[test]
3879    fn test_parse_not_like() {
3880        let stmt = parse(r#"User filter .name not like "A%""#).unwrap();
3881        match stmt {
3882            Statement::Query(q) => match q.filter.unwrap() {
3883                Expr::UnaryOp(UnaryOp::Not, inner) => {
3884                    assert!(matches!(*inner, Expr::BinaryOp(_, BinOp::Like, _)));
3885                }
3886                other => panic!("expected Not(Like), got {other:?}"),
3887            },
3888            _ => panic!("expected query"),
3889        }
3890    }
3891
3892    // ---- Mission E2b: GROUP BY + HAVING ------------------------------------
3893
3894    #[test]
3895    fn test_parse_group_by_single_key() {
3896        let stmt = parse("User group .status { .status, n: count(.name) }").unwrap();
3897        match stmt {
3898            Statement::Query(q) => {
3899                let gb = q.group_by.unwrap();
3900                assert_eq!(
3901                    gb.keys,
3902                    vec![GroupKey {
3903                        expr: Expr::Field("status".into()),
3904                        output_name: "status".into(),
3905                    }]
3906                );
3907                assert!(gb.having.is_none());
3908                let proj = q.projection.unwrap();
3909                assert_eq!(proj.len(), 2);
3910                assert!(matches!(
3911                    &proj[1].expr,
3912                    Expr::FunctionCall(AggFunc::Count, _, _)
3913                ));
3914                assert_eq!(proj[1].alias.as_deref(), Some("n"));
3915            }
3916            _ => panic!("expected query"),
3917        }
3918    }
3919
3920    #[test]
3921    fn test_parse_group_by_multi_key() {
3922        let stmt = parse("User group .status, .age { .status, .age }").unwrap();
3923        match stmt {
3924            Statement::Query(q) => {
3925                let gb = q.group_by.unwrap();
3926                assert_eq!(
3927                    gb.keys,
3928                    vec![
3929                        GroupKey {
3930                            expr: Expr::Field("status".into()),
3931                            output_name: "status".into(),
3932                        },
3933                        GroupKey {
3934                            expr: Expr::Field("age".into()),
3935                            output_name: "age".into(),
3936                        }
3937                    ]
3938                );
3939            }
3940            _ => panic!("expected query"),
3941        }
3942    }
3943
3944    #[test]
3945    fn test_parse_group_by_having() {
3946        let stmt = parse("User group .status having count(.name) > 1 { .status }").unwrap();
3947        match stmt {
3948            Statement::Query(q) => {
3949                let gb = q.group_by.unwrap();
3950                assert_eq!(
3951                    gb.keys,
3952                    vec![GroupKey {
3953                        expr: Expr::Field("status".into()),
3954                        output_name: "status".into(),
3955                    }]
3956                );
3957                assert!(gb.having.is_some());
3958                // HAVING is `count(.name) > 1` — BinaryOp(FunctionCall, Gt, Literal)
3959                match gb.having.unwrap() {
3960                    Expr::BinaryOp(l, BinOp::Gt, _) => {
3961                        assert!(matches!(*l, Expr::FunctionCall(AggFunc::Count, _, _)));
3962                    }
3963                    other => panic!("expected BinaryOp, got {other:?}"),
3964                }
3965            }
3966            _ => panic!("expected query"),
3967        }
3968    }
3969
3970    #[test]
3971    fn test_parse_aggregate_in_projection() {
3972        // Unaliased aggregate function calls in projection.
3973        let stmt = parse("User group .status { .status, count(.name), sum(.age) }").unwrap();
3974        match stmt {
3975            Statement::Query(q) => {
3976                let proj = q.projection.unwrap();
3977                assert_eq!(proj.len(), 3);
3978                assert!(matches!(
3979                    &proj[1].expr,
3980                    Expr::FunctionCall(AggFunc::Count, _, _)
3981                ));
3982                assert!(matches!(
3983                    &proj[2].expr,
3984                    Expr::FunctionCall(AggFunc::Sum, _, _)
3985                ));
3986            }
3987            _ => panic!("expected query"),
3988        }
3989    }
3990
3991    #[test]
3992    fn test_parse_aggregate_in_aliased_projection() {
3993        let stmt = parse("User group .status { .status, total: count(.name), average: avg(.age) }")
3994            .unwrap();
3995        match stmt {
3996            Statement::Query(q) => {
3997                let proj = q.projection.unwrap();
3998                assert_eq!(proj[1].alias.as_deref(), Some("total"));
3999                assert!(matches!(
4000                    &proj[1].expr,
4001                    Expr::FunctionCall(AggFunc::Count, _, _)
4002                ));
4003                assert_eq!(proj[2].alias.as_deref(), Some("average"));
4004                assert!(matches!(
4005                    &proj[2].expr,
4006                    Expr::FunctionCall(AggFunc::Avg, _, _)
4007                ));
4008            }
4009            _ => panic!("expected query"),
4010        }
4011    }
4012
4013    // ─── IS NULL / IS NOT NULL parser tests ────────────────────────────
4014
4015    #[test]
4016    fn test_parse_is_null() {
4017        let stmt = parse("User filter .age is null").unwrap();
4018        match stmt {
4019            Statement::Query(q) => {
4020                let filter = q.filter.unwrap();
4021                assert_eq!(
4022                    filter,
4023                    Expr::UnaryOp(UnaryOp::IsNull, Box::new(Expr::Field("age".into())))
4024                );
4025            }
4026            _ => panic!("expected query"),
4027        }
4028    }
4029
4030    #[test]
4031    fn test_parse_is_not_null() {
4032        let stmt = parse("User filter .age is not null").unwrap();
4033        match stmt {
4034            Statement::Query(q) => {
4035                let filter = q.filter.unwrap();
4036                assert_eq!(
4037                    filter,
4038                    Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(Expr::Field("age".into())))
4039                );
4040            }
4041            _ => panic!("expected query"),
4042        }
4043    }
4044
4045    #[test]
4046    fn test_parse_eq_null_desugars_to_is_null() {
4047        let stmt = parse("User filter .age = null").unwrap();
4048        match stmt {
4049            Statement::Query(q) => {
4050                let filter = q.filter.unwrap();
4051                assert_eq!(
4052                    filter,
4053                    Expr::UnaryOp(UnaryOp::IsNull, Box::new(Expr::Field("age".into())))
4054                );
4055            }
4056            _ => panic!("expected query"),
4057        }
4058    }
4059
4060    #[test]
4061    fn test_parse_neq_null_desugars_to_is_not_null() {
4062        let stmt = parse("User filter .age != null").unwrap();
4063        match stmt {
4064            Statement::Query(q) => {
4065                let filter = q.filter.unwrap();
4066                assert_eq!(
4067                    filter,
4068                    Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(Expr::Field("age".into())))
4069                );
4070            }
4071            _ => panic!("expected query"),
4072        }
4073    }
4074
4075    #[test]
4076    fn test_parse_null_comparisons_parse_ok() {
4077        // `< null`, `>= null` etc. parse successfully now that `null` is a
4078        // valid expression. At runtime they evaluate to Empty (no match),
4079        // which is correct null-propagation semantics.
4080        assert!(parse("User filter .age < null").is_ok());
4081        assert!(parse("User filter .age >= null").is_ok());
4082    }
4083
4084    #[test]
4085    fn test_parse_count_star_expr() {
4086        let stmt = parse("User filter count(*) > 0").unwrap();
4087        match stmt {
4088            Statement::Query(q) => {
4089                let filter = q.filter.unwrap();
4090                match filter {
4091                    Expr::BinaryOp(left, BinOp::Gt, _) => {
4092                        assert_eq!(
4093                            *left,
4094                            Expr::FunctionCall(
4095                                AggFunc::Count,
4096                                Box::new(Expr::Field("*".into())),
4097                                AggregateMode::Symmetric,
4098                            )
4099                        );
4100                    }
4101                    _ => panic!("expected comparison"),
4102                }
4103            }
4104            _ => panic!("expected query"),
4105        }
4106    }
4107
4108    // ─── String function parser tests ──────────────────────────────────
4109
4110    #[test]
4111    fn test_parse_upper_in_filter() {
4112        let stmt = parse(r#"User filter upper(.name) = "ALICE""#).unwrap();
4113        match stmt {
4114            Statement::Query(q) => {
4115                let f = q.filter.unwrap();
4116                match f {
4117                    Expr::BinaryOp(left, BinOp::Eq, _right) => {
4118                        assert!(matches!(*left, Expr::ScalarFunc(ScalarFn::Upper, _)));
4119                    }
4120                    _ => panic!("expected binary op with upper"),
4121                }
4122            }
4123            _ => panic!("expected query"),
4124        }
4125    }
4126
4127    #[test]
4128    fn test_parse_substring() {
4129        let stmt = parse("User { sub: substring(.name, 1, 3) }").unwrap();
4130        match stmt {
4131            Statement::Query(q) => {
4132                let proj = q.projection.unwrap();
4133                match &proj[0].expr {
4134                    Expr::ScalarFunc(ScalarFn::Substring, args) => {
4135                        assert_eq!(args.len(), 3);
4136                    }
4137                    other => panic!("expected ScalarFunc Substring, got {other:?}"),
4138                }
4139            }
4140            _ => panic!("expected query"),
4141        }
4142    }
4143
4144    #[test]
4145    fn test_parse_concat() {
4146        let stmt = parse(r#"User { full: concat(.name, " - ", .email) }"#).unwrap();
4147        match stmt {
4148            Statement::Query(q) => {
4149                let proj = q.projection.unwrap();
4150                match &proj[0].expr {
4151                    Expr::ScalarFunc(ScalarFn::Concat, args) => {
4152                        assert_eq!(args.len(), 3);
4153                    }
4154                    other => panic!("expected ScalarFunc Concat, got {other:?}"),
4155                }
4156            }
4157            _ => panic!("expected query"),
4158        }
4159    }
4160
4161    // ─── CASE WHEN parser tests ────────────────────────────────────────
4162
4163    #[test]
4164    fn test_parse_case_single_when() {
4165        let stmt = parse(r#"User filter case when .age > 30 then true else false end"#).unwrap();
4166        match stmt {
4167            Statement::Query(q) => {
4168                let filter = q.filter.unwrap();
4169                match filter {
4170                    Expr::Case { whens, else_expr } => {
4171                        assert_eq!(whens.len(), 1);
4172                        assert!(else_expr.is_some());
4173                    }
4174                    other => panic!("expected Case expr, got {other:?}"),
4175                }
4176            }
4177            _ => panic!("expected query"),
4178        }
4179    }
4180
4181    #[test]
4182    fn test_parse_case_multiple_whens() {
4183        let stmt = parse(
4184            r#"User { label: case when .age > 30 then "senior" when .age > 20 then "adult" else "young" end }"#
4185        ).unwrap();
4186        match stmt {
4187            Statement::Query(q) => {
4188                let proj = q.projection.unwrap();
4189                match &proj[0].expr {
4190                    Expr::Case { whens, else_expr } => {
4191                        assert_eq!(whens.len(), 2);
4192                        assert!(else_expr.is_some());
4193                    }
4194                    other => panic!("expected Case expr, got {other:?}"),
4195                }
4196            }
4197            _ => panic!("expected query"),
4198        }
4199    }
4200
4201    #[test]
4202    fn test_parse_case_without_else() {
4203        let stmt = parse(r#"User filter case when .age > 30 then true end"#).unwrap();
4204        match stmt {
4205            Statement::Query(q) => {
4206                let filter = q.filter.unwrap();
4207                match filter {
4208                    Expr::Case { whens, else_expr } => {
4209                        assert_eq!(whens.len(), 1);
4210                        assert!(else_expr.is_none());
4211                    }
4212                    other => panic!("expected Case expr, got {other:?}"),
4213                }
4214            }
4215            _ => panic!("expected query"),
4216        }
4217    }
4218
4219    // ─── Mul/Div expression tests (E2f) ───────────────────────────────
4220
4221    #[test]
4222    fn test_parse_mul_expr() {
4223        let stmt = parse("User filter .price * .quantity > 100").unwrap();
4224        match stmt {
4225            Statement::Query(q) => {
4226                let filter = q.filter.unwrap();
4227                match filter {
4228                    Expr::BinaryOp(left, BinOp::Gt, _) => match *left {
4229                        Expr::BinaryOp(_, BinOp::Mul, _) => {}
4230                        other => panic!("expected Mul, got {other:?}"),
4231                    },
4232                    other => panic!("expected BinaryOp Gt, got {other:?}"),
4233                }
4234            }
4235            _ => panic!("expected query"),
4236        }
4237    }
4238
4239    #[test]
4240    fn test_parse_div_expr() {
4241        let stmt = parse("User { ratio: .total / .count }").unwrap();
4242        match stmt {
4243            Statement::Query(q) => {
4244                let proj = q.projection.unwrap();
4245                assert_eq!(proj[0].alias.as_deref(), Some("ratio"));
4246                match &proj[0].expr {
4247                    Expr::BinaryOp(_, BinOp::Div, _) => {}
4248                    other => panic!("expected Div, got {other:?}"),
4249                }
4250            }
4251            _ => panic!("expected query"),
4252        }
4253    }
4254
4255    #[test]
4256    fn test_parse_mul_div_precedence() {
4257        // .a + .b * .c should parse as .a + (.b * .c)
4258        let stmt = parse("User filter .a + .b * .c > 0").unwrap();
4259        match stmt {
4260            Statement::Query(q) => {
4261                let filter = q.filter.unwrap();
4262                match filter {
4263                    Expr::BinaryOp(left, BinOp::Gt, _) => match *left {
4264                        Expr::BinaryOp(_, BinOp::Add, right) => {
4265                            assert!(matches!(*right, Expr::BinaryOp(_, BinOp::Mul, _)));
4266                        }
4267                        other => panic!("expected Add, got {other:?}"),
4268                    },
4269                    other => panic!("expected Gt, got {other:?}"),
4270                }
4271            }
4272            _ => panic!("expected query"),
4273        }
4274    }
4275
4276    // ─── Multi-column ORDER BY tests (E2f) ────────────────────────────
4277
4278    #[test]
4279    fn test_parse_multi_order() {
4280        let stmt = parse("User order .name asc, .age desc").unwrap();
4281        match stmt {
4282            Statement::Query(q) => {
4283                let order = q.order.unwrap();
4284                assert_eq!(order.keys.len(), 2);
4285                assert_eq!(order.keys[0].expr, Expr::Field("name".into()));
4286                assert!(!order.keys[0].descending);
4287                assert_eq!(order.keys[1].expr, Expr::Field("age".into()));
4288                assert!(order.keys[1].descending);
4289            }
4290            _ => panic!("expected query"),
4291        }
4292    }
4293
4294    #[test]
4295    fn test_parse_order_default_asc() {
4296        let stmt = parse("User order .name").unwrap();
4297        match stmt {
4298            Statement::Query(q) => {
4299                let order = q.order.unwrap();
4300                assert_eq!(order.keys.len(), 1);
4301                assert!(!order.keys[0].descending);
4302            }
4303            _ => panic!("expected query"),
4304        }
4305    }
4306
4307    // ─── ALTER TABLE / DROP TABLE parser tests (E2g) ──────────────────
4308
4309    #[test]
4310    fn test_parse_alter_add_column() {
4311        let stmt = parse("alter User add column status: str").unwrap();
4312        match stmt {
4313            Statement::AlterTable(at) => {
4314                assert_eq!(at.table, "User");
4315                match at.action {
4316                    AlterAction::AddColumn {
4317                        name,
4318                        type_name,
4319                        required,
4320                    } => {
4321                        assert_eq!(name, "status");
4322                        assert_eq!(type_name, "str");
4323                        assert!(!required);
4324                    }
4325                    other => panic!("expected AddColumn, got {other:?}"),
4326                }
4327            }
4328            other => panic!("expected AlterTable, got {other:?}"),
4329        }
4330    }
4331
4332    #[test]
4333    fn test_parse_alter_add_required_column() {
4334        let stmt = parse("alter User add required status: str").unwrap();
4335        match stmt {
4336            Statement::AlterTable(at) => match at.action {
4337                AlterAction::AddColumn { required, .. } => assert!(required),
4338                other => panic!("expected AddColumn, got {other:?}"),
4339            },
4340            other => panic!("expected AlterTable, got {other:?}"),
4341        }
4342    }
4343
4344    #[test]
4345    fn test_parse_type_with_unique_modifier() {
4346        let stmt = parse("type User { required unique email: str, age: int }").unwrap();
4347        match stmt {
4348            Statement::CreateType(ct) => {
4349                assert!(ct.fields[0].required && ct.fields[0].unique);
4350                assert!(!ct.fields[1].unique);
4351            }
4352            other => panic!("expected CreateType, got {other:?}"),
4353        }
4354    }
4355
4356    #[test]
4357    fn test_parse_type_unique_before_required() {
4358        // Modifiers accepted in either order.
4359        let stmt = parse("type User { unique required email: str }").unwrap();
4360        match stmt {
4361            Statement::CreateType(ct) => {
4362                assert!(ct.fields[0].required && ct.fields[0].unique);
4363            }
4364            other => panic!("expected CreateType, got {other:?}"),
4365        }
4366    }
4367
4368    #[test]
4369    fn test_parse_alter_add_unique() {
4370        let stmt = parse("alter User add unique .email").unwrap();
4371        match stmt {
4372            Statement::AlterTable(at) => assert!(matches!(
4373                at.action,
4374                AlterAction::AddUnique {
4375                    target: IndexTarget::Column(ref column),
4376                    ..
4377                } if column == "email"
4378            )),
4379            other => panic!("expected AlterTable, got {other:?}"),
4380        }
4381    }
4382
4383    #[test]
4384    fn test_parse_alter_drop_column() {
4385        let stmt = parse("alter User drop column status").unwrap();
4386        match stmt {
4387            Statement::AlterTable(at) => {
4388                assert_eq!(at.table, "User");
4389                match at.action {
4390                    AlterAction::DropColumn { name, .. } => assert_eq!(name, "status"),
4391                    other => panic!("expected DropColumn, got {other:?}"),
4392                }
4393            }
4394            other => panic!("expected AlterTable, got {other:?}"),
4395        }
4396    }
4397
4398    #[test]
4399    fn test_parse_alter_drop_without_column_keyword() {
4400        let stmt = parse("alter User drop status").unwrap();
4401        match stmt {
4402            Statement::AlterTable(at) => match at.action {
4403                AlterAction::DropColumn { name, .. } => assert_eq!(name, "status"),
4404                other => panic!("expected DropColumn, got {other:?}"),
4405            },
4406            other => panic!("expected AlterTable, got {other:?}"),
4407        }
4408    }
4409
4410    #[test]
4411    fn test_parse_drop_table() {
4412        let stmt = parse("drop User").unwrap();
4413        match stmt {
4414            Statement::DropTable(dt) => assert_eq!(dt.table, "User"),
4415            other => panic!("expected DropTable, got {other:?}"),
4416        }
4417    }
4418
4419    // ─── IN subquery parser tests (E2h) ───────────────────────────────
4420
4421    #[test]
4422    fn test_parse_in_subquery() {
4423        let stmt = parse("User filter .name in (VIP { .name })").unwrap();
4424        match stmt {
4425            Statement::Query(q) => {
4426                let filter = q.filter.unwrap();
4427                match filter {
4428                    Expr::InSubquery {
4429                        expr,
4430                        subquery,
4431                        negated,
4432                    } => {
4433                        assert!(!negated);
4434                        assert!(matches!(*expr, Expr::Field(ref f) if f == "name"));
4435                        assert_eq!(subquery.source, "VIP");
4436                    }
4437                    other => panic!("expected InSubquery, got {other:?}"),
4438                }
4439            }
4440            _ => panic!("expected query"),
4441        }
4442    }
4443
4444    #[test]
4445    fn test_parse_not_in_subquery() {
4446        let stmt = parse("User filter .id not in (Order { .user_id })").unwrap();
4447        match stmt {
4448            Statement::Query(q) => match q.filter.unwrap() {
4449                Expr::InSubquery { negated, .. } => assert!(negated),
4450                other => panic!("expected InSubquery, got {other:?}"),
4451            },
4452            _ => panic!("expected query"),
4453        }
4454    }
4455
4456    #[test]
4457    fn test_parse_in_literal_list_still_works() {
4458        // Ensure existing IN (literal) parsing isn't broken
4459        let stmt = parse("User filter .age in (25, 30, 35)").unwrap();
4460        match stmt {
4461            Statement::Query(q) => match q.filter.unwrap() {
4462                Expr::InList { list, negated, .. } => {
4463                    assert!(!negated);
4464                    assert_eq!(list.len(), 3);
4465                }
4466                other => panic!("expected InList, got {other:?}"),
4467            },
4468            _ => panic!("expected query"),
4469        }
4470    }
4471
4472    // ---- Materialized view parser tests ------------------------------------
4473
4474    #[test]
4475    fn test_parse_create_view() {
4476        let stmt = parse("materialize OldUsers as User filter .age > 28").unwrap();
4477        match stmt {
4478            Statement::CreateView(cv) => {
4479                assert_eq!(cv.name, "OldUsers");
4480                assert_eq!(cv.query.source, "User");
4481                assert!(cv.query.filter.is_some());
4482                assert!(!cv.query_text.is_empty());
4483            }
4484            _ => panic!("expected CreateView"),
4485        }
4486    }
4487
4488    #[test]
4489    fn test_parse_create_view_with_projection() {
4490        let stmt = parse("materialize UserNames as User { .name }").unwrap();
4491        match stmt {
4492            Statement::CreateView(cv) => {
4493                assert_eq!(cv.name, "UserNames");
4494                assert!(cv.query.projection.is_some());
4495            }
4496            _ => panic!("expected CreateView"),
4497        }
4498    }
4499
4500    #[test]
4501    fn test_parse_refresh_view() {
4502        let stmt = parse("refresh OldUsers").unwrap();
4503        match stmt {
4504            Statement::RefreshView(rv) => {
4505                assert_eq!(rv.name, "OldUsers");
4506            }
4507            _ => panic!("expected RefreshView"),
4508        }
4509    }
4510
4511    #[test]
4512    fn test_parse_drop_view() {
4513        let stmt = parse("drop view OldUsers").unwrap();
4514        match stmt {
4515            Statement::DropView(dv) => {
4516                assert_eq!(dv.name, "OldUsers");
4517            }
4518            _ => panic!("expected DropView"),
4519        }
4520    }
4521
4522    #[test]
4523    fn test_parse_drop_table_still_works() {
4524        let stmt = parse("drop Users").unwrap();
4525        match stmt {
4526            Statement::DropTable(dt) => {
4527                assert_eq!(dt.table, "Users");
4528            }
4529            _ => panic!("expected DropTable"),
4530        }
4531    }
4532
4533    #[test]
4534    fn test_parse_union() {
4535        let stmt = parse("User union Order").unwrap();
4536        match stmt {
4537            Statement::Union(u) => {
4538                assert!(!u.all);
4539                match *u.left {
4540                    Statement::Query(_) => {}
4541                    _ => panic!("expected Query on left"),
4542                }
4543                match *u.right {
4544                    Statement::Query(_) => {}
4545                    _ => panic!("expected Query on right"),
4546                }
4547            }
4548            _ => panic!("expected Union"),
4549        }
4550    }
4551
4552    #[test]
4553    fn test_parse_union_all() {
4554        let stmt = parse("User union all Order").unwrap();
4555        match stmt {
4556            Statement::Union(u) => {
4557                assert!(u.all, "expected UNION ALL");
4558                match *u.left {
4559                    Statement::Query(_) => {}
4560                    _ => panic!("expected Query on left"),
4561                }
4562                match *u.right {
4563                    Statement::Query(_) => {}
4564                    _ => panic!("expected Query on right"),
4565                }
4566            }
4567            _ => panic!("expected Union"),
4568        }
4569    }
4570
4571    #[test]
4572    fn test_parse_union_chain() {
4573        // Left-associative: A union B union C => Union(Union(A, B), C)
4574        let stmt = parse("User union Order union Product").unwrap();
4575        match stmt {
4576            Statement::Union(outer) => {
4577                assert!(!outer.all);
4578                // Right side is Product
4579                match *outer.right {
4580                    Statement::Query(q) => assert_eq!(q.source, "Product"),
4581                    _ => panic!("expected Query(Product) on right"),
4582                }
4583                // Left side is Union(User, Order)
4584                match *outer.left {
4585                    Statement::Union(inner) => {
4586                        assert!(!inner.all);
4587                        match *inner.left {
4588                            Statement::Query(q) => assert_eq!(q.source, "User"),
4589                            _ => panic!("expected Query(User)"),
4590                        }
4591                        match *inner.right {
4592                            Statement::Query(q) => assert_eq!(q.source, "Order"),
4593                            _ => panic!("expected Query(Order)"),
4594                        }
4595                    }
4596                    _ => panic!("expected inner Union"),
4597                }
4598            }
4599            _ => panic!("expected Union"),
4600        }
4601    }
4602
4603    #[test]
4604    fn test_parse_union_with_filter() {
4605        let stmt = parse("User filter .age > 10 union Order filter .total > 50").unwrap();
4606        match stmt {
4607            Statement::Union(u) => {
4608                assert!(!u.all);
4609                // Both sides should be queries (the filter is part of each query)
4610                match *u.left {
4611                    Statement::Query(q) => {
4612                        assert_eq!(q.source, "User");
4613                        assert!(q.filter.is_some());
4614                    }
4615                    _ => panic!("expected Query on left"),
4616                }
4617                match *u.right {
4618                    Statement::Query(q) => {
4619                        assert_eq!(q.source, "Order");
4620                        assert!(q.filter.is_some());
4621                    }
4622                    _ => panic!("expected Query on right"),
4623                }
4624            }
4625            _ => panic!("expected Union"),
4626        }
4627    }
4628
4629    #[test]
4630    fn test_parse_count_distinct_standalone() {
4631        let stmt = parse("count(distinct User { .name })").unwrap();
4632        match stmt {
4633            Statement::Query(q) => {
4634                let agg = q.aggregation.unwrap();
4635                assert_eq!(agg.function, AggFunc::CountDistinct);
4636                assert_eq!(agg.argument, Some(Expr::Field("name".into())));
4637            }
4638            _ => panic!("expected Query"),
4639        }
4640    }
4641
4642    #[test]
4643    fn test_parse_count_distinct_in_projection() {
4644        let stmt = parse("User group .dept { .dept, count(distinct .name) }").unwrap();
4645        match stmt {
4646            Statement::Query(q) => {
4647                let proj = q.projection.unwrap();
4648                assert_eq!(proj.len(), 2);
4649                match &proj[1].expr {
4650                    Expr::FunctionCall(func, _, _) => {
4651                        assert_eq!(*func, AggFunc::CountDistinct);
4652                    }
4653                    _ => panic!("expected FunctionCall"),
4654                }
4655            }
4656            _ => panic!("expected Query"),
4657        }
4658    }
4659
4660    // ---- Window function parser tests ----------------------------------------
4661
4662    #[test]
4663    fn test_parse_window_row_number_order() {
4664        let stmt = parse("User { .name, rn: row_number() over (order .age) }").unwrap();
4665        match stmt {
4666            Statement::Query(q) => {
4667                let proj = q.projection.unwrap();
4668                assert_eq!(proj.len(), 2);
4669                assert_eq!(proj[1].alias.as_deref(), Some("rn"));
4670                match &proj[1].expr {
4671                    Expr::Window {
4672                        function,
4673                        args,
4674                        partition_by,
4675                        order_by,
4676                        ..
4677                    } => {
4678                        assert_eq!(*function, WindowFunc::RowNumber);
4679                        assert!(args.is_empty());
4680                        assert!(partition_by.is_empty());
4681                        assert_eq!(order_by.len(), 1);
4682                        assert_eq!(order_by[0].expr, Expr::Field("age".into()));
4683                        assert!(!order_by[0].descending);
4684                    }
4685                    other => panic!("expected Window, got {other:?}"),
4686                }
4687            }
4688            _ => panic!("expected query"),
4689        }
4690    }
4691
4692    #[test]
4693    fn test_parse_window_sum_partition_order() {
4694        let stmt =
4695            parse("User { .name, s: sum(.salary) over (partition .dept order .salary) }").unwrap();
4696        match stmt {
4697            Statement::Query(q) => {
4698                let proj = q.projection.unwrap();
4699                assert_eq!(proj.len(), 2);
4700                assert_eq!(proj[1].alias.as_deref(), Some("s"));
4701                match &proj[1].expr {
4702                    Expr::Window {
4703                        function,
4704                        args,
4705                        partition_by,
4706                        order_by,
4707                        ..
4708                    } => {
4709                        assert_eq!(*function, WindowFunc::Sum);
4710                        assert_eq!(args.len(), 1);
4711                        assert!(matches!(&args[0], Expr::Field(f) if f == "salary"));
4712                        assert_eq!(partition_by, &[Expr::Field("dept".into())]);
4713                        assert_eq!(order_by.len(), 1);
4714                        assert_eq!(order_by[0].expr, Expr::Field("salary".into()));
4715                        assert!(!order_by[0].descending);
4716                    }
4717                    other => panic!("expected Window, got {other:?}"),
4718                }
4719            }
4720            _ => panic!("expected query"),
4721        }
4722    }
4723
4724    #[test]
4725    fn test_parse_window_rank_desc() {
4726        let stmt =
4727            parse("User { .dept, .salary, r: rank() over (partition .dept order .salary desc) }")
4728                .unwrap();
4729        match stmt {
4730            Statement::Query(q) => {
4731                let proj = q.projection.unwrap();
4732                assert_eq!(proj.len(), 3);
4733                match &proj[2].expr {
4734                    Expr::Window {
4735                        function,
4736                        partition_by,
4737                        order_by,
4738                        ..
4739                    } => {
4740                        assert_eq!(*function, WindowFunc::Rank);
4741                        assert_eq!(partition_by, &[Expr::Field("dept".into())]);
4742                        assert_eq!(order_by.len(), 1);
4743                        assert!(order_by[0].descending);
4744                    }
4745                    other => panic!("expected Window, got {other:?}"),
4746                }
4747            }
4748            _ => panic!("expected query"),
4749        }
4750    }
4751
4752    #[test]
4753    fn test_parse_window_dense_rank() {
4754        let stmt = parse("User { .name, dr: dense_rank() over (order .score desc) }").unwrap();
4755        match stmt {
4756            Statement::Query(q) => {
4757                let proj = q.projection.unwrap();
4758                assert_eq!(proj.len(), 2);
4759                match &proj[1].expr {
4760                    Expr::Window { function, .. } => {
4761                        assert_eq!(*function, WindowFunc::DenseRank);
4762                    }
4763                    other => panic!("expected Window, got {other:?}"),
4764                }
4765            }
4766            _ => panic!("expected query"),
4767        }
4768    }
4769
4770    #[test]
4771    fn test_parse_sum_without_over_is_aggregate() {
4772        // sum(.salary) alone (no `over`) stays as FunctionCall, not Window.
4773        let stmt = parse("User group .dept { .dept, total: sum(.salary) }").unwrap();
4774        match stmt {
4775            Statement::Query(q) => {
4776                let proj = q.projection.unwrap();
4777                assert_eq!(proj.len(), 2);
4778                match &proj[1].expr {
4779                    Expr::FunctionCall(AggFunc::Sum, _, _) => {} // correct
4780                    other => panic!("expected FunctionCall(Sum), got {other:?}"),
4781                }
4782            }
4783            _ => panic!("expected query"),
4784        }
4785    }
4786
4787    #[test]
4788    fn test_nesting_depth_limit() {
4789        // Build a deeply nested parenthesized expression that exceeds MAX_NESTING_DEPTH.
4790        let mut query = String::from("User filter ");
4791        for _ in 0..70 {
4792            query.push('(');
4793        }
4794        query.push_str(".age > 1");
4795        for _ in 0..70 {
4796            query.push(')');
4797        }
4798        let result = parse(&query);
4799        assert!(result.is_err());
4800        let err = result.unwrap_err();
4801        assert!(
4802            err.message().contains("nesting depth"),
4803            "expected nesting depth error, got: {}",
4804            err.message()
4805        );
4806    }
4807
4808    #[test]
4809    fn test_unary_prefix_nesting_depth_limit() {
4810        // A long chain of `not` prefixes recurses through parse_primary
4811        // without passing through parse_expr's guard. It must error cleanly
4812        // at the depth limit instead of overflowing the stack.
4813        let query = String::from("User filter ") + &"not ".repeat(5000) + ".active";
4814        let result = parse(&query);
4815        assert!(result.is_err());
4816        let err = result.unwrap_err();
4817        assert!(
4818            err.message().contains("nesting depth"),
4819            "expected nesting depth error, got: {}",
4820            err.message()
4821        );
4822    }
4823
4824    #[test]
4825    fn test_moderate_nesting_succeeds() {
4826        // 10 levels of nesting should be fine.
4827        let mut query = String::from("User filter ");
4828        for _ in 0..10 {
4829            query.push('(');
4830        }
4831        query.push_str(".age > 1");
4832        for _ in 0..10 {
4833            query.push(')');
4834        }
4835        assert!(parse(&query).is_ok());
4836    }
4837
4838    /// Regression for issue #26: `fuzz_parser` crashed on the 3-byte input
4839    /// `nn{` — the projection loop consumed the Eof token and then indexed
4840    /// past the end of `tokens`. Must return an error instead.
4841    #[test]
4842    fn test_parse_fuzz_repro_projection_eof() {
4843        let err = parse("nn{").expect_err("unterminated projection must error, not panic");
4844        let _ = err.message();
4845    }
4846
4847    /// Regression for issue #26: `fuzz_roundtrip` tripped the same bug with
4848    /// the 2-byte input `z{`.
4849    #[test]
4850    fn test_parse_fuzz_repro_short_projection_eof() {
4851        let err = parse("z{").expect_err("unterminated projection must error, not panic");
4852        let _ = err.message();
4853    }
4854
4855    #[test]
4856    fn test_update_at_statement_start_gives_helpful_error() {
4857        let err =
4858            parse(r#"update User filter .name = "Alice" { age := 31 }"#).expect_err("should fail");
4859        let msg = err.message();
4860        assert!(
4861            msg.contains("pipeline syntax"),
4862            "error should mention pipeline syntax, got: {msg}"
4863        );
4864        assert!(
4865            msg.contains("update"),
4866            "error should mention 'update', got: {msg}"
4867        );
4868    }
4869
4870    #[test]
4871    fn test_delete_at_statement_start_gives_helpful_error() {
4872        let err = parse("delete User filter .age < 18").expect_err("should fail");
4873        let msg = err.message();
4874        assert!(
4875            msg.contains("pipeline syntax"),
4876            "error should mention pipeline syntax, got: {msg}"
4877        );
4878        assert!(
4879            msg.contains("delete"),
4880            "error should mention 'delete', got: {msg}"
4881        );
4882    }
4883}
4884
4885#[cfg(test)]
4886mod cleanup_parser_dx_tests {
4887    use super::*;
4888
4889    #[test]
4890    fn typoed_statement_keyword_gets_suggestion() {
4891        let err = parse("updat User set age = 1").unwrap_err();
4892        let msg = err.to_string();
4893        assert!(msg.contains("near token"), "{msg}");
4894        assert!(msg.contains("did you mean `update`"), "{msg}");
4895    }
4896}
4897
4898#[cfg(test)]
4899mod dogfood_dx_tests {
4900    use super::*;
4901
4902    // ── P-6: reserved words as column names ────────────────────────────
4903
4904    #[test]
4905    fn reserved_word_field_name_gives_actionable_error() {
4906        let err = parse("type Post { type: str }").unwrap_err();
4907        let msg = err.to_string();
4908        assert!(
4909            msg.contains("'type' is a reserved word")
4910                && msg.contains("field name")
4911                && msg.contains("quote it as `type`"),
4912            "unhelpful message: {msg}"
4913        );
4914    }
4915
4916    #[test]
4917    fn reserved_modifier_word_as_field_name_gives_actionable_error() {
4918        // `required` is a modifier keyword; followed directly by `:` it is
4919        // instead the field's (reserved) name — the old error was the opaque
4920        // "expected field name, got ':'".
4921        let err = parse("type Post { required: bool }").unwrap_err();
4922        let msg = err.to_string();
4923        assert!(
4924            msg.contains("'required' is a reserved word") && msg.contains("quote it as `required`"),
4925            "unhelpful message: {msg}"
4926        );
4927    }
4928
4929    #[test]
4930    fn reserved_word_in_insert_assignment_gives_actionable_error() {
4931        let err = parse(r#"insert Post { type := "x" }"#).unwrap_err();
4932        let msg = err.to_string();
4933        assert!(msg.contains("'type' is a reserved word"), "{msg}");
4934    }
4935
4936    #[test]
4937    fn reserved_word_in_alter_column_gives_actionable_error() {
4938        let err = parse("alter Post add column order: int").unwrap_err();
4939        let msg = err.to_string();
4940        assert!(msg.contains("'order' is a reserved word"), "{msg}");
4941    }
4942
4943    #[test]
4944    fn backtick_field_name_parses_as_identifier() {
4945        let stmt = parse("type Post { `type`: str, `order`: int }").unwrap();
4946        match stmt {
4947            Statement::CreateType(ct) => {
4948                assert_eq!(ct.fields[0].name, "type");
4949                assert_eq!(ct.fields[1].name, "order");
4950            }
4951            other => panic!("expected CreateType, got {other:?}"),
4952        }
4953    }
4954
4955    #[test]
4956    fn backtick_field_still_honors_modifiers() {
4957        let stmt = parse("type Post { required `type`: str }").unwrap();
4958        match stmt {
4959            Statement::CreateType(ct) => {
4960                assert_eq!(ct.fields[0].name, "type");
4961                assert!(ct.fields[0].required);
4962            }
4963            other => panic!("expected CreateType, got {other:?}"),
4964        }
4965    }
4966
4967    // ── P-7: DDL idempotency ───────────────────────────────────────────
4968
4969    #[test]
4970    fn create_type_if_not_exists_parses() {
4971        let stmt = parse("type Post if not exists { id: int }").unwrap();
4972        match stmt {
4973            Statement::CreateType(ct) => assert!(ct.if_not_exists),
4974            other => panic!("expected CreateType, got {other:?}"),
4975        }
4976    }
4977
4978    #[test]
4979    fn create_type_without_clause_defaults_false() {
4980        let stmt = parse("type Post { id: int }").unwrap();
4981        match stmt {
4982            Statement::CreateType(ct) => assert!(!ct.if_not_exists),
4983            other => panic!("expected CreateType, got {other:?}"),
4984        }
4985    }
4986
4987    #[test]
4988    fn drop_if_exists_parses() {
4989        match parse("drop if exists Post").unwrap() {
4990            Statement::DropTable(dt) => assert!(dt.if_exists),
4991            other => panic!("expected DropTable, got {other:?}"),
4992        }
4993        match parse("drop Post").unwrap() {
4994            Statement::DropTable(dt) => assert!(!dt.if_exists),
4995            other => panic!("expected DropTable, got {other:?}"),
4996        }
4997    }
4998
4999    #[test]
5000    fn drop_view_if_exists_parses() {
5001        match parse("drop view if exists ActiveUsers").unwrap() {
5002            Statement::DropView(dv) => {
5003                assert!(dv.if_exists);
5004                assert_eq!(dv.name, "ActiveUsers");
5005            }
5006            other => panic!("expected DropView, got {other:?}"),
5007        }
5008    }
5009
5010    #[test]
5011    fn add_index_and_unique_if_not_exists_parse() {
5012        match parse("alter Post add index if not exists .slug").unwrap() {
5013            Statement::AlterTable(at) => {
5014                assert!(matches!(
5015                    at.action,
5016                    AlterAction::AddIndex {
5017                        if_not_exists: true,
5018                        ..
5019                    }
5020                ));
5021            }
5022            other => panic!("expected AlterTable, got {other:?}"),
5023        }
5024        match parse("alter Post add unique if not exists .slug").unwrap() {
5025            Statement::AlterTable(at) => {
5026                assert!(matches!(
5027                    at.action,
5028                    AlterAction::AddUnique {
5029                        if_not_exists: true,
5030                        ..
5031                    }
5032                ));
5033            }
5034            other => panic!("expected AlterTable, got {other:?}"),
5035        }
5036    }
5037
5038    #[test]
5039    fn expression_index_targets_parse_with_stable_table_local_identity() {
5040        use powdb_storage::stored_json_path::{
5041            StoredJsonPathSegmentV1 as Segment, StoredJsonPathV1,
5042        };
5043
5044        let expected = StoredJsonPathV1::new(
5045            "data",
5046            vec![Segment::Key("author".into()), Segment::Index(0)],
5047        );
5048        for query in [
5049            "alter Post add index (.data->author->0)",
5050            "alter Post add unique if not exists (.data->\"author\"->0)",
5051            "alter Post drop index if exists (.data->author->0)",
5052        ] {
5053            let Statement::AlterTable(alter) = parse(query).unwrap() else {
5054                panic!("expected alter table for {query}");
5055            };
5056            let (target, flag) = match alter.action {
5057                AlterAction::AddIndex {
5058                    target,
5059                    if_not_exists,
5060                }
5061                | AlterAction::AddUnique {
5062                    target,
5063                    if_not_exists,
5064                } => (target, if_not_exists),
5065                AlterAction::DropIndex { target, if_exists } => (target, if_exists),
5066                other => panic!("expected index action, got {other:?}"),
5067            };
5068            assert_eq!(target, IndexTarget::JsonPath(expected.clone()));
5069            assert_eq!(
5070                flag,
5071                query.contains("if not exists") || query.contains("if exists")
5072            );
5073        }
5074    }
5075
5076    #[test]
5077    fn expression_index_target_rejects_ambiguous_or_non_path_forms() {
5078        let cases = [
5079            (
5080                "alter Post add index .data->author",
5081                "must be parenthesized",
5082            ),
5083            (
5084                "alter Post add index (p.data->author)",
5085                "qualified JSON paths",
5086            ),
5087            (
5088                "alter Post add index (.data)",
5089                "use `.column` for a stored column",
5090            ),
5091            (
5092                "alter Post add index (.data->age + 1)",
5093                "only a direct JSON path",
5094            ),
5095            (
5096                "alter Post drop index ({ value := 1 })",
5097                "expected an unqualified JSON path",
5098            ),
5099        ];
5100        for (query, expected) in cases {
5101            let error = parse(query).expect_err(query).to_string();
5102            assert!(
5103                error.contains(expected),
5104                "`{query}` should mention `{expected}`, got `{error}`"
5105            );
5106        }
5107    }
5108
5109    #[test]
5110    fn alter_drop_column_if_exists_parses() {
5111        match parse("alter Post drop column if exists status").unwrap() {
5112            Statement::AlterTable(at) => {
5113                assert!(matches!(
5114                    at.action,
5115                    AlterAction::DropColumn {
5116                        if_exists: true,
5117                        ..
5118                    }
5119                ));
5120            }
5121            other => panic!("expected AlterTable, got {other:?}"),
5122        }
5123    }
5124
5125    // ── P-8: introspection ─────────────────────────────────────────────
5126
5127    #[test]
5128    fn schema_parses_to_list_types() {
5129        assert_eq!(parse("schema").unwrap(), Statement::ListTypes);
5130    }
5131
5132    #[test]
5133    fn describe_parses_to_describe() {
5134        assert_eq!(
5135            parse("describe Post").unwrap(),
5136            Statement::Describe("Post".to_string())
5137        );
5138    }
5139
5140    #[test]
5141    fn schema_with_type_aliases_describe() {
5142        assert_eq!(
5143            parse("schema Post").unwrap(),
5144            Statement::Describe("Post".to_string())
5145        );
5146    }
5147
5148    #[test]
5149    fn schema_links_parses_to_list_links() {
5150        assert_eq!(parse("schema links").unwrap(), Statement::ListLinks);
5151    }
5152
5153    #[test]
5154    fn describe_links_still_names_a_table() {
5155        // Only the `schema links` spelling is the link listing; `describe`
5156        // keeps treating `links` as an ordinary type name.
5157        assert_eq!(
5158            parse("describe links").unwrap(),
5159            Statement::Describe("links".to_string())
5160        );
5161    }
5162}
5163
5164#[cfg(test)]
5165mod json_path_tests {
5166    use super::*;
5167
5168    /// Pull the filter expression out of a single-table query.
5169    fn filter_of(src: &str) -> Expr {
5170        match parse(src).unwrap() {
5171            Statement::Query(q) => q.filter.expect("expected a filter"),
5172            other => panic!("expected a query, got {other:?}"),
5173        }
5174    }
5175
5176    #[test]
5177    fn ident_key_path() {
5178        // .data->author->name
5179        let e = filter_of(r#"Post filter .data->author->name = "x""#);
5180        let Expr::BinaryOp(lhs, BinOp::Eq, _) = e else {
5181            panic!("expected an equality, got {e:?}");
5182        };
5183        assert_eq!(
5184            *lhs,
5185            Expr::JsonPath {
5186                base: Box::new(Expr::Field("data".into())),
5187                segments: vec![PathSeg::Key("author".into()), PathSeg::Key("name".into())],
5188            }
5189        );
5190    }
5191
5192    #[test]
5193    fn string_form_key_path() {
5194        // .data->"weird key!" (PowQL strings are double-quoted)
5195        let e = filter_of(r#"Post filter .data->"weird key!" = 1"#);
5196        let Expr::BinaryOp(lhs, _, _) = e else {
5197            panic!("expected binop");
5198        };
5199        assert_eq!(
5200            *lhs,
5201            Expr::JsonPath {
5202                base: Box::new(Expr::Field("data".into())),
5203                segments: vec![PathSeg::Key("weird key!".into())],
5204            }
5205        );
5206    }
5207
5208    #[test]
5209    fn array_index_path() {
5210        // .data->tags->0
5211        let e = filter_of(r#"Post filter .data->tags->0 = "rust""#);
5212        let Expr::BinaryOp(lhs, _, _) = e else {
5213            panic!("expected binop");
5214        };
5215        assert_eq!(
5216            *lhs,
5217            Expr::JsonPath {
5218                base: Box::new(Expr::Field("data".into())),
5219                segments: vec![PathSeg::Key("tags".into()), PathSeg::Index(0)],
5220            }
5221        );
5222    }
5223
5224    #[test]
5225    fn qualified_base_path() {
5226        // posts.data->author  (join-qualified base)
5227        let e = filter_of(r#"Post as posts filter posts.data->author = "a""#);
5228        let Expr::BinaryOp(lhs, _, _) = e else {
5229            panic!("expected binop");
5230        };
5231        assert_eq!(
5232            *lhs,
5233            Expr::JsonPath {
5234                base: Box::new(Expr::QualifiedField {
5235                    qualifier: "posts".into(),
5236                    field: "data".into(),
5237                }),
5238                segments: vec![PathSeg::Key("author".into())],
5239            }
5240        );
5241    }
5242
5243    #[test]
5244    fn path_binds_tighter_than_comparison_and_arithmetic() {
5245        // `.data->age > 21` must be `(.data->age) > 21`, not `.data->(age > 21)`.
5246        let e = filter_of("Post filter .data->age > 21");
5247        let Expr::BinaryOp(lhs, BinOp::Gt, rhs) = e else {
5248            panic!("expected a top-level `>`, got {e:?}");
5249        };
5250        assert!(matches!(*lhs, Expr::JsonPath { .. }));
5251        assert_eq!(*rhs, Expr::Literal(Literal::Int(21)));
5252
5253        // `.a->b + 1` must be `(.a->b) + 1`.
5254        let e = filter_of("Post filter .a->b + 1 = 3");
5255        let Expr::BinaryOp(add, BinOp::Eq, _) = e else {
5256            panic!("expected eq");
5257        };
5258        let Expr::BinaryOp(lhs, BinOp::Add, _) = *add else {
5259            panic!("expected `+` under `=`, got {add:?}");
5260        };
5261        assert!(matches!(*lhs, Expr::JsonPath { .. }));
5262    }
5263
5264    #[test]
5265    fn dash_vs_arrow_lexing() {
5266        // `.a->1` is a path index: `->` lexes as one token because the chars
5267        // are adjacent, ahead of the single-char `-`.
5268        let idx = filter_of("Post filter .a->1 = 0");
5269        let Expr::BinaryOp(lhs, BinOp::Eq, _) = idx else {
5270            panic!("expected eq");
5271        };
5272        assert_eq!(
5273            *lhs,
5274            Expr::JsonPath {
5275                base: Box::new(Expr::Field("a".into())),
5276                segments: vec![PathSeg::Index(1)],
5277            }
5278        );
5279
5280        // `.a - 1` (spaced) is subtraction — the `-` is not glued to a digit,
5281        // so it lexes as the minus operator.
5282        let sub = filter_of("Post filter .a - 1 = 0");
5283        let Expr::BinaryOp(lhs, BinOp::Eq, _) = sub else {
5284            panic!("expected eq");
5285        };
5286        assert!(
5287            matches!(*lhs, Expr::BinaryOp(_, BinOp::Sub, _)),
5288            "`.a - 1` should be subtraction, got {lhs:?}"
5289        );
5290
5291        // `.a-1` (no spaces) is the lexer gotcha: `-1` is a NEGATIVE INTEGER
5292        // literal (the number rule fires when `-` is glued to a digit), so the
5293        // stream is `.a` then `-1` with no operator between — a parse error,
5294        // NOT subtraction and NOT a path.
5295        assert!(
5296            parse("Post filter .a-1 = 0").is_err(),
5297            "`.a-1` should fail to parse (negative-literal gotcha)"
5298        );
5299
5300        // `.a - >` is `.a` `-` `>` — a dangling `>`, a parse error.
5301        assert!(
5302            parse("Post filter .a - > 0").is_err(),
5303            "`.a - >` should fail to parse"
5304        );
5305    }
5306
5307    #[test]
5308    fn negative_index_rejected() {
5309        let err = parse("Post filter .data->-1 = 0").unwrap_err();
5310        assert!(
5311            err.to_string().contains("array index"),
5312            "expected an index error, got: {err}"
5313        );
5314    }
5315
5316    #[test]
5317    fn path_on_literal_base_rejected() {
5318        // A `->` after a non-field base is a parse error.
5319        let err = parse("Post filter 5->x = 1").unwrap_err();
5320        assert!(
5321            err.to_string().to_lowercase().contains("field base"),
5322            "expected a field-base error, got: {err}"
5323        );
5324    }
5325
5326    #[test]
5327    fn json_path_assignment_target_is_targeted_unsupported() {
5328        // `Doc update { .data->x := 5 }` must not die with a generic
5329        // "expected field name": it must name the unsupported position and
5330        // the whole-column alternative. Both the leading-dot path-target form
5331        // and the bare `data->x` form take the targeted branch.
5332        for stmt in [
5333            "Doc update { .data->x := 5 }",
5334            "Doc update { data->x := 5 }",
5335        ] {
5336            let err = parse(stmt).unwrap_err();
5337            assert!(
5338                matches!(err, ParseError::Unsupported { .. }),
5339                "{stmt}: expected Unsupported, got {err:?}"
5340            );
5341            let msg = err.to_string();
5342            assert!(
5343                msg.contains("JSON path assignment targets are not supported"),
5344                "{stmt}: message must state the unsupported feature: {msg}"
5345            );
5346            assert!(
5347                msg.contains("json_set"),
5348                "{stmt}: message must point at the whole-column alternative: {msg}"
5349            );
5350        }
5351        // A normal whole-column update still parses.
5352        assert!(parse(r#"Doc update { data := "{}" }"#).is_ok());
5353    }
5354
5355    #[test]
5356    fn json_type_scalar_parses() {
5357        let e = filter_of(r#"Post filter json_type(.data->x) = "string""#);
5358        let Expr::BinaryOp(lhs, _, _) = e else {
5359            panic!("expected binop");
5360        };
5361        let Expr::ScalarFunc(ScalarFn::JsonType, args) = *lhs else {
5362            panic!("expected json_type call, got {lhs:?}");
5363        };
5364        assert_eq!(args.len(), 1);
5365        assert!(matches!(args[0], Expr::JsonPath { .. }));
5366    }
5367
5368    #[test]
5369    fn path_in_projection() {
5370        // Projection, ordering, and grouping all retain the same structural
5371        // JsonPath expression rather than lowering it to an alias string.
5372        let stmt = parse("Post { author: .data->author }").unwrap();
5373        let Statement::Query(q) = stmt else {
5374            panic!("expected query");
5375        };
5376        let proj = q.projection.unwrap();
5377        assert_eq!(proj[0].alias.as_deref(), Some("author"));
5378        assert!(matches!(proj[0].expr, Expr::JsonPath { .. }));
5379
5380        let Statement::Query(ordered) = parse("Post order .data->author { .id }").unwrap() else {
5381            panic!("expected query");
5382        };
5383        assert!(matches!(
5384            ordered.order.unwrap().keys[0].expr,
5385            Expr::JsonPath { .. }
5386        ));
5387
5388        let Statement::Query(grouped) =
5389            parse("Post group .data->author { .data->author }").unwrap()
5390        else {
5391            panic!("expected query");
5392        };
5393        assert!(matches!(
5394            grouped.group_by.unwrap().keys[0].expr,
5395            Expr::JsonPath { .. }
5396        ));
5397    }
5398}
5399
5400/// `tokens_to_text` is the inverse of the lexer: whatever it writes must read
5401/// back as the very tokens it was given. A view stores its defining query as
5402/// that text and re-lexes it on every refresh, so any disagreement silently
5403/// redefines the view.
5404#[cfg(test)]
5405mod token_text_roundtrip {
5406    use super::*;
5407    use proptest::prelude::*;
5408
5409    /// One value of every payload-carrying `Token` variant (with payloads
5410    /// chosen to break a naive reconstruction: reserved words that exist
5411    /// only as quoted identifiers, names needing quotes, escapes the lexer
5412    /// decodes, numbers whose `Display` changes their type) plus every
5413    /// payload-free variant except EOF.
5414    ///
5415    /// A variant added to `Token` and not added here still cannot slip
5416    /// through unnoticed: `tokens_to_text` re-lexes what it built and
5417    /// refuses to return a mismatch.
5418    fn every_token() -> Vec<Token> {
5419        let mut toks = vec![
5420            // Identifiers: plain, reserved words, and spellings that only
5421            // exist inside backticks.
5422            Token::Ident("User".into()),
5423            Token::Ident("order".into()),
5424            Token::Ident("true".into()),
5425            Token::Ident("null".into()),
5426            Token::Ident("column name".into()),
5427            Token::Ident("1st".into()),
5428            Token::Ident("a-b".into()),
5429            Token::Ident("has#hash".into()),
5430            Token::Ident("has.dot".into()),
5431            Token::Ident("héllo".into()),
5432            Token::DotIdent("name".into()),
5433            Token::DotIdent("order".into()),
5434            Token::DotIdent("field name".into()),
5435            Token::DotIdent("1st".into()),
5436            Token::DotIdent("a-b".into()),
5437            // Numbers, including the ones whose `Display` loses the
5438            // fraction or overflows to a very long expansion.
5439            Token::IntLit(0),
5440            Token::IntLit(-1),
5441            Token::IntLit(i64::MIN),
5442            Token::IntLit(i64::MAX),
5443            Token::FloatLit(0.0),
5444            Token::FloatLit(-0.0),
5445            Token::FloatLit(3.0),
5446            Token::FloatLit(-2.0),
5447            Token::FloatLit(1.5),
5448            Token::FloatLit(1e300),
5449            Token::FloatLit(1e-300),
5450            Token::FloatLit(f64::MIN_POSITIVE),
5451            // Strings: every escape the lexer decodes, plus characters
5452            // that would otherwise leak into the grammar.
5453            Token::StringLit(String::new()),
5454            Token::StringLit("plain".into()),
5455            Token::StringLit("back\\slash".into()),
5456            Token::StringLit("he said \"hi\"".into()),
5457            Token::StringLit("line\nbreak".into()),
5458            Token::StringLit("tab\there".into()),
5459            Token::StringLit("carriage\rreturn".into()),
5460            Token::StringLit("`backtick`".into()),
5461            Token::StringLit("# not a comment".into()),
5462            Token::StringLit("} filter .x = 1".into()),
5463            Token::StringLit("\\\"".into()),
5464            Token::BoolLit(true),
5465            Token::BoolLit(false),
5466            Token::Param("1".into()),
5467            Token::Param("name".into()),
5468            Token::Param(String::new()),
5469        ];
5470        toks.extend([
5471            Token::Type,
5472            Token::Filter,
5473            Token::Order,
5474            Token::Limit,
5475            Token::Offset,
5476            Token::Insert,
5477            Token::Update,
5478            Token::Delete,
5479            Token::Upsert,
5480            Token::Returning,
5481            Token::Select,
5482            Token::Required,
5483            Token::Default,
5484            Token::Auto,
5485            Token::Multi,
5486            Token::Link,
5487            Token::Index,
5488            Token::Unique,
5489            Token::On,
5490            Token::Conflict,
5491            Token::Asc,
5492            Token::Desc,
5493            Token::And,
5494            Token::Or,
5495            Token::Not,
5496            Token::Exists,
5497            Token::Let,
5498            Token::As,
5499            Token::Match,
5500            Token::Group,
5501            Token::Join,
5502            Token::Inner,
5503            Token::LeftKw,
5504            Token::RightKw,
5505            Token::Outer,
5506            Token::Cross,
5507            Token::Transaction,
5508            Token::Begin,
5509            Token::Commit,
5510            Token::Rollback,
5511            Token::View,
5512            Token::Materialized,
5513            Token::Refresh,
5514            Token::Union,
5515            Token::Having,
5516            Token::Distinct,
5517            Token::In,
5518            Token::Between,
5519            Token::Like,
5520            Token::Count,
5521            Token::Avg,
5522            Token::Sum,
5523            Token::Min,
5524            Token::Max,
5525            Token::Raw,
5526            Token::Is,
5527            Token::Null,
5528        ]);
5529        toks.extend([
5530            Token::Upper,
5531            Token::Lower,
5532            Token::Length,
5533            Token::Trim,
5534            Token::Substring,
5535            Token::Concat,
5536            Token::Abs,
5537            Token::Round,
5538            Token::Ceil,
5539            Token::Floor,
5540            Token::Sqrt,
5541            Token::Pow,
5542            Token::Now,
5543            Token::Extract,
5544            Token::DateAdd,
5545            Token::DateDiff,
5546            Token::JsonType,
5547            Token::JsonText,
5548            Token::Cast,
5549            Token::Case,
5550            Token::When,
5551            Token::Then,
5552            Token::Else,
5553            Token::End,
5554            Token::Over,
5555            Token::Partition,
5556            Token::RowNumber,
5557            Token::Rank,
5558            Token::DenseRank,
5559            Token::Alter,
5560            Token::Drop,
5561            Token::Add,
5562            Token::Column,
5563            Token::Explain,
5564            Token::Schema,
5565            Token::Describe,
5566        ]);
5567        toks.extend([
5568            Token::Eq,
5569            Token::Neq,
5570            Token::Lt,
5571            Token::Gt,
5572            Token::Lte,
5573            Token::Gte,
5574            Token::Assign,
5575            Token::Arrow,
5576            Token::Pipe,
5577            Token::Coalesce,
5578            Token::Plus,
5579            Token::Minus,
5580            Token::Star,
5581            Token::Slash,
5582            Token::LBrace,
5583            Token::RBrace,
5584            Token::LParen,
5585            Token::RParen,
5586            Token::Comma,
5587            Token::Colon,
5588            Token::Dot,
5589        ]);
5590        toks
5591    }
5592
5593    /// Reconstruct, re-lex, and compare against the input stream.
5594    fn assert_round_trips(tokens: &[Token]) {
5595        let text =
5596            tokens_to_text(tokens).unwrap_or_else(|e| panic!("no source text for {tokens:?}: {e}"));
5597        let relexed = lex(&text)
5598            .unwrap_or_else(|e| panic!("`{text}` from {tokens:?} does not lex: {}", e.message));
5599        let mut expected = tokens.to_vec();
5600        expected.push(Token::Eof);
5601        assert_eq!(relexed, expected, "`{text}` re-lexes to different tokens");
5602    }
5603
5604    #[test]
5605    fn every_token_round_trips_on_its_own() {
5606        for tok in every_token() {
5607            assert_round_trips(std::slice::from_ref(&tok));
5608        }
5609    }
5610
5611    #[test]
5612    fn eof_contributes_no_text() {
5613        assert_eq!(tokens_to_text(&[Token::Eof]).unwrap(), "");
5614        assert_eq!(tokens_to_text(&[]).unwrap(), "");
5615    }
5616
5617    /// A token with no faithful spelling is refused, not written wrong.
5618    /// A backtick inside an identifier is the case that cannot be quoted
5619    /// (the lexer has no escape inside backticks), and a non-finite float has
5620    /// no literal form at all. The parameter name is caught by nothing but the
5621    /// closing re-lex check, which is the point of having one.
5622    #[test]
5623    fn unspellable_tokens_are_typed_errors() {
5624        for tok in [
5625            Token::Ident("has`tick".into()),
5626            Token::DotIdent("has`tick".into()),
5627            Token::Ident(String::new()),
5628            Token::FloatLit(f64::INFINITY),
5629            Token::FloatLit(f64::NAN),
5630            Token::Param("two words".into()),
5631        ] {
5632            let err = tokens_to_text(std::slice::from_ref(&tok))
5633                .expect_err("{tok:?} must not be written back wrong");
5634            assert!(
5635                matches!(err, ParseError::Unsupported { .. }),
5636                "expected a typed Unsupported error for {tok:?}, got {err:?}"
5637            );
5638        }
5639    }
5640
5641    /// A view whose source text cannot be stored faithfully is refused at
5642    /// creation instead of quietly becoming a different query.
5643    ///
5644    /// The lexer now refuses a literal with no finite value outright, so this
5645    /// one never reaches the round-trip check; the check still guards the
5646    /// token streams that are built rather than lexed, which
5647    /// `unspellable_tokens_are_typed_errors` covers directly.
5648    #[test]
5649    fn unspellable_view_source_is_refused() {
5650        let huge = format!("1{}.0", "0".repeat(400)); // overflows f64 to inf
5651        let err = parse(&format!("materialize V as U filter .x = {huge}"))
5652            .expect_err("a view source that cannot round-trip must be refused");
5653        assert!(
5654            matches!(err, ParseError::Lex { ref message, .. } if message.contains("out of range")),
5655            "expected the out-of-range refusal, got {err:?}"
5656        );
5657    }
5658
5659    proptest! {
5660        /// The property the concrete cases are instances of: for ANY token
5661        /// stream, `lex(tokens_to_text(tokens)) == tokens`.
5662        #[test]
5663        fn any_token_stream_round_trips(
5664            tokens in proptest::collection::vec(
5665                proptest::sample::select(every_token()),
5666                0..12usize,
5667            )
5668        ) {
5669            let text = tokens_to_text(&tokens)
5670                .map_err(|e| TestCaseError::fail(format!("no source text: {e}")))?;
5671            let relexed = lex(&text)
5672                .map_err(|e| TestCaseError::fail(format!("`{text}` does not lex: {}", e.message)))?;
5673            let mut expected = tokens.clone();
5674            expected.push(Token::Eof);
5675            prop_assert_eq!(relexed, expected, "`{}` re-lexes to different tokens", text);
5676        }
5677    }
5678}