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