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