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/// Reconstruct PowQL source text from a slice of tokens. Used to store the
2610/// view's source query for re-execution on refresh. Not perfectly
2611/// round-trippable (whitespace is normalised) but semantically identical.
2612fn tokens_to_text(tokens: &[Token]) -> String {
2613    let mut out = String::with_capacity(64);
2614    for tok in tokens {
2615        if !out.is_empty() && !matches!(tok, Token::Eof) {
2616            out.push(' ');
2617        }
2618        match tok {
2619            Token::Ident(s) => out.push_str(s),
2620            Token::DotIdent(s) => {
2621                out.push('.');
2622                out.push_str(s);
2623            }
2624            Token::IntLit(v) => out.push_str(&v.to_string()),
2625            Token::FloatLit(v) => out.push_str(&v.to_string()),
2626            Token::StringLit(s) => {
2627                out.push('"');
2628                out.push_str(s);
2629                out.push('"');
2630            }
2631            Token::BoolLit(v) => out.push_str(if *v { "true" } else { "false" }),
2632            Token::Param(s) => {
2633                out.push('$');
2634                out.push_str(s);
2635            }
2636            Token::Type => out.push_str("type"),
2637            Token::Filter => out.push_str("filter"),
2638            Token::Order => out.push_str("order"),
2639            Token::Limit => out.push_str("limit"),
2640            Token::Offset => out.push_str("offset"),
2641            Token::Insert => out.push_str("insert"),
2642            Token::Update => out.push_str("update"),
2643            Token::Delete => out.push_str("delete"),
2644            Token::Upsert => out.push_str("upsert"),
2645            Token::Returning => out.push_str("returning"),
2646            Token::Conflict => out.push_str("conflict"),
2647            Token::Select => out.push_str("select"),
2648            Token::Required => out.push_str("required"),
2649            Token::Default => out.push_str("default"),
2650            Token::Auto => out.push_str("auto"),
2651            Token::Multi => out.push_str("multi"),
2652            Token::Link => out.push_str("link"),
2653            Token::Index => out.push_str("index"),
2654            Token::Unique => out.push_str("unique"),
2655            Token::On => out.push_str("on"),
2656            Token::Asc => out.push_str("asc"),
2657            Token::Desc => out.push_str("desc"),
2658            Token::And => out.push_str("and"),
2659            Token::Or => out.push_str("or"),
2660            Token::Not => out.push_str("not"),
2661            Token::Exists => out.push_str("exists"),
2662            Token::Let => out.push_str("let"),
2663            Token::As => out.push_str("as"),
2664            Token::Match => out.push_str("match"),
2665            Token::Group => out.push_str("group"),
2666            Token::Join => out.push_str("join"),
2667            Token::Inner => out.push_str("inner"),
2668            Token::LeftKw => out.push_str("left"),
2669            Token::RightKw => out.push_str("right"),
2670            Token::Outer => out.push_str("outer"),
2671            Token::Cross => out.push_str("cross"),
2672            Token::Transaction => out.push_str("transaction"),
2673            Token::Begin => out.push_str("begin"),
2674            Token::Commit => out.push_str("commit"),
2675            Token::Rollback => out.push_str("rollback"),
2676            Token::View => out.push_str("view"),
2677            Token::Materialized => out.push_str("materialized"),
2678            Token::Refresh => out.push_str("refresh"),
2679            Token::Union => out.push_str("union"),
2680            Token::Having => out.push_str("having"),
2681            Token::Distinct => out.push_str("distinct"),
2682            Token::In => out.push_str("in"),
2683            Token::Between => out.push_str("between"),
2684            Token::Like => out.push_str("like"),
2685            Token::Count => out.push_str("count"),
2686            Token::Avg => out.push_str("avg"),
2687            Token::Sum => out.push_str("sum"),
2688            Token::Raw => out.push_str("raw"),
2689            Token::Min => out.push_str("min"),
2690            Token::Max => out.push_str("max"),
2691            Token::Is => out.push_str("is"),
2692            Token::Null => out.push_str("null"),
2693            Token::Upper => out.push_str("upper"),
2694            Token::Lower => out.push_str("lower"),
2695            Token::Length => out.push_str("length"),
2696            Token::Trim => out.push_str("trim"),
2697            Token::Substring => out.push_str("substring"),
2698            Token::Concat => out.push_str("concat"),
2699            Token::Abs => out.push_str("abs"),
2700            Token::Round => out.push_str("round"),
2701            Token::Ceil => out.push_str("ceil"),
2702            Token::Floor => out.push_str("floor"),
2703            Token::Sqrt => out.push_str("sqrt"),
2704            Token::Pow => out.push_str("pow"),
2705            Token::Now => out.push_str("now"),
2706            Token::Extract => out.push_str("extract"),
2707            Token::DateAdd => out.push_str("date_add"),
2708            Token::DateDiff => out.push_str("date_diff"),
2709            Token::JsonType => out.push_str("json_type"),
2710            Token::JsonText => out.push_str("json_text"),
2711            Token::Cast => out.push_str("cast"),
2712            Token::Case => out.push_str("case"),
2713            Token::When => out.push_str("when"),
2714            Token::Then => out.push_str("then"),
2715            Token::Else => out.push_str("else"),
2716            Token::End => out.push_str("end"),
2717            Token::Over => out.push_str("over"),
2718            Token::Partition => out.push_str("partition"),
2719            Token::RowNumber => out.push_str("row_number"),
2720            Token::Rank => out.push_str("rank"),
2721            Token::DenseRank => out.push_str("dense_rank"),
2722            Token::Alter => out.push_str("alter"),
2723            Token::Drop => out.push_str("drop"),
2724            Token::Add => out.push_str("add"),
2725            Token::Column => out.push_str("column"),
2726            Token::Eq => out.push('='),
2727            Token::Neq => out.push_str("!="),
2728            Token::Lt => out.push('<'),
2729            Token::Gt => out.push('>'),
2730            Token::Lte => out.push_str("<="),
2731            Token::Gte => out.push_str(">="),
2732            Token::Assign => out.push_str(":="),
2733            Token::Arrow => out.push_str("->"),
2734            Token::Pipe => out.push('|'),
2735            Token::Coalesce => out.push_str("??"),
2736            Token::Plus => out.push('+'),
2737            Token::Minus => out.push('-'),
2738            Token::Star => out.push('*'),
2739            Token::Slash => out.push('/'),
2740            Token::LBrace => out.push('{'),
2741            Token::RBrace => out.push('}'),
2742            Token::LParen => out.push('('),
2743            Token::RParen => out.push(')'),
2744            Token::Comma => out.push(','),
2745            Token::Colon => out.push(':'),
2746            Token::Dot => out.push('.'),
2747            Token::Explain => out.push_str("explain"),
2748            Token::Schema => out.push_str("schema"),
2749            Token::Describe => out.push_str("describe"),
2750            Token::Eof => {}
2751        }
2752    }
2753    out
2754}
2755
2756#[cfg(test)]
2757mod tests {
2758    use super::*;
2759    #[test]
2760    fn test_parse_simple_query() {
2761        let stmt = parse("User").unwrap();
2762        match stmt {
2763            Statement::Query(q) => {
2764                assert_eq!(q.source, "User");
2765                assert!(q.filter.is_none());
2766                assert!(q.projection.is_none());
2767            }
2768            _ => panic!("expected query"),
2769        }
2770    }
2771
2772    #[test]
2773    fn test_parse_filter() {
2774        let stmt = parse("User filter .age > 30").unwrap();
2775        match stmt {
2776            Statement::Query(q) => {
2777                assert_eq!(q.source, "User");
2778                assert!(q.filter.is_some());
2779            }
2780            _ => panic!("expected query"),
2781        }
2782    }
2783
2784    #[test]
2785    fn test_parse_projection() {
2786        let stmt = parse("User { name, email }").unwrap();
2787        match stmt {
2788            Statement::Query(q) => {
2789                let proj = q.projection.unwrap();
2790                assert_eq!(proj.len(), 2);
2791            }
2792            _ => panic!("expected query"),
2793        }
2794    }
2795
2796    #[test]
2797    fn test_bare_dotted_path_projection_is_a_parse_error() {
2798        // `.user.name` without an outer alias is token-identical to two
2799        // comma-less fields, so it is rejected with alias guidance instead of
2800        // silently parsing as two fields (2026-07-23 plan-quality audit P3).
2801        for q in [
2802            "Order { .user.name }",
2803            "Order { .id, uname: .user.name }",
2804            "Order { uname: .user.company.name }",
2805        ] {
2806            let err = parse(q).unwrap_err();
2807            let msg = err.to_string();
2808            assert!(
2809                msg.contains("alias the table"),
2810                "`{q}` should error with alias guidance, got: {msg}"
2811            );
2812        }
2813    }
2814
2815    #[test]
2816    fn test_aliased_scalar_link_path_parses_as_one_field() {
2817        let stmt = parse("Order as o { uname: o.user.name }").unwrap();
2818        match stmt {
2819            Statement::Query(q) => {
2820                let proj = q.projection.unwrap();
2821                assert_eq!(proj.len(), 1);
2822                match &proj[0].expr {
2823                    Expr::LinkPath {
2824                        outer_alias,
2825                        links,
2826                        column,
2827                    } => {
2828                        assert_eq!(outer_alias, "o");
2829                        assert_eq!(links, &["user".to_string()]);
2830                        assert_eq!(column, "name");
2831                    }
2832                    other => panic!("expected LinkPath, got {other:?}"),
2833                }
2834            }
2835            _ => panic!("expected query"),
2836        }
2837    }
2838
2839    #[test]
2840    fn test_parse_filter_order_limit() {
2841        let stmt = parse("User filter .age > 30 order .name desc limit 10").unwrap();
2842        match stmt {
2843            Statement::Query(q) => {
2844                assert!(q.filter.is_some());
2845                let order = q.order.unwrap();
2846                assert_eq!(order.keys.len(), 1);
2847                assert_eq!(order.keys[0].expr, Expr::Field("name".into()));
2848                assert!(order.keys[0].descending);
2849                assert!(q.limit.is_some());
2850            }
2851            _ => panic!("expected query"),
2852        }
2853    }
2854
2855    #[test]
2856    fn test_parse_insert() {
2857        let stmt = parse(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
2858        match stmt {
2859            Statement::Insert(ins) => {
2860                assert_eq!(ins.target, "User");
2861                assert_eq!(ins.rows.len(), 1);
2862                assert_eq!(ins.rows[0].len(), 2);
2863                assert_eq!(ins.rows[0][0].field, "name");
2864                assert_eq!(ins.rows[0][1].field, "age");
2865            }
2866            _ => panic!("expected insert"),
2867        }
2868    }
2869
2870    #[test]
2871    fn test_parse_insert_multi_row() {
2872        let stmt =
2873            parse(r#"insert User { name := "Alice", age := 30 }, { name := "Bob", age := 25 }, { name := "Cy" }"#)
2874                .unwrap();
2875        match stmt {
2876            Statement::Insert(ins) => {
2877                assert_eq!(ins.target, "User");
2878                assert_eq!(ins.rows.len(), 3);
2879                assert_eq!(ins.rows[0].len(), 2);
2880                assert_eq!(ins.rows[1][0].field, "name");
2881                assert_eq!(ins.rows[2].len(), 1);
2882                assert_eq!(ins.rows[2][0].field, "name");
2883            }
2884            _ => panic!("expected insert"),
2885        }
2886    }
2887
2888    #[test]
2889    fn test_parse_update() {
2890        let stmt = parse(r#"User filter .email = "alice@ex.com" update { age := 31 }"#).unwrap();
2891        match stmt {
2892            Statement::UpdateQuery(upd) => {
2893                assert_eq!(upd.source, "User");
2894                assert!(upd.filter.is_some());
2895                assert_eq!(upd.assignments.len(), 1);
2896                assert!(!upd.returning);
2897            }
2898            _ => panic!("expected update"),
2899        }
2900    }
2901
2902    #[test]
2903    fn test_parse_update_returning() {
2904        let stmt = parse(r#"User filter .name = "Alice" update { age := 31 } returning"#).unwrap();
2905        match stmt {
2906            Statement::UpdateQuery(upd) => assert!(upd.returning),
2907            _ => panic!("expected update"),
2908        }
2909    }
2910
2911    #[test]
2912    fn test_parse_delete() {
2913        let stmt = parse("User filter .age < 18 delete").unwrap();
2914        match stmt {
2915            Statement::DeleteQuery(del) => {
2916                assert_eq!(del.source, "User");
2917                assert!(del.filter.is_some());
2918                assert!(!del.returning);
2919            }
2920            _ => panic!("expected delete"),
2921        }
2922    }
2923
2924    #[test]
2925    fn test_parse_delete_returning() {
2926        let stmt = parse("User filter .age < 18 delete returning").unwrap();
2927        match stmt {
2928            Statement::DeleteQuery(del) => assert!(del.returning),
2929            _ => panic!("expected delete"),
2930        }
2931    }
2932
2933    #[test]
2934    fn test_parse_count() {
2935        let stmt = parse("count(User)").unwrap();
2936        match stmt {
2937            Statement::Query(q) => {
2938                let agg = q.aggregation.unwrap();
2939                assert_eq!(agg.function, AggFunc::Count);
2940                assert!(q.filter.is_none());
2941            }
2942            _ => panic!("expected query with aggregation"),
2943        }
2944    }
2945
2946    #[test]
2947    fn test_parse_count_with_filter() {
2948        // Regression: previously returned "expected RParen, got Filter".
2949        // count(<query>) must accept a full read-pipeline tail.
2950        let stmt = parse("count(User filter .age > 30)").unwrap();
2951        match stmt {
2952            Statement::Query(q) => {
2953                assert_eq!(q.source, "User");
2954                let agg = q.aggregation.unwrap();
2955                assert_eq!(agg.function, AggFunc::Count);
2956                assert!(q.filter.is_some(), "filter should have been parsed");
2957            }
2958            _ => panic!("expected query with aggregation"),
2959        }
2960    }
2961
2962    #[test]
2963    fn test_parse_count_with_filter_and_limit() {
2964        let stmt = parse("count(User filter .age > 30 limit 100)").unwrap();
2965        match stmt {
2966            Statement::Query(q) => {
2967                assert_eq!(q.source, "User");
2968                assert!(q.filter.is_some());
2969                assert!(q.limit.is_some());
2970                assert_eq!(q.aggregation.unwrap().function, AggFunc::Count);
2971            }
2972            _ => panic!("expected query with aggregation"),
2973        }
2974    }
2975
2976    #[test]
2977    fn test_parse_create_type() {
2978        let stmt = parse("type User { required name: str, age: int }").unwrap();
2979        match stmt {
2980            Statement::CreateType(ct) => {
2981                assert_eq!(ct.name, "User");
2982                assert_eq!(ct.fields.len(), 2);
2983                assert!(ct.fields[0].required);
2984                assert!(!ct.fields[1].required);
2985            }
2986            _ => panic!("expected create type"),
2987        }
2988    }
2989
2990    #[test]
2991    fn test_parse_sum_with_field_projection() {
2992        // `sum(... { .age })` should lift `.age` into AggregateExpr.argument and
2993        // clear the projection so the executor's aggregate fast path fires.
2994        let stmt = parse("sum(User filter .age > 30 { .age })").unwrap();
2995        match stmt {
2996            Statement::Query(q) => {
2997                let agg = q.aggregation.expect("aggregate");
2998                assert_eq!(agg.function, AggFunc::Sum);
2999                assert_eq!(agg.argument, Some(Expr::Field("age".into())));
3000                assert!(
3001                    q.projection.is_none(),
3002                    "projection should be lifted into agg.field"
3003                );
3004            }
3005            _ => panic!("expected query"),
3006        }
3007    }
3008
3009    #[test]
3010    fn test_parse_raw_aggregate_modes() {
3011        let Statement::Query(top_level) = parse("sum(raw User { .age })").unwrap() else {
3012            panic!("expected query");
3013        };
3014        assert_eq!(top_level.aggregation.unwrap().mode, AggregateMode::Raw);
3015
3016        let Statement::Query(grouped) = parse("User group .dept { total: sum(raw .age) }").unwrap()
3017        else {
3018            panic!("expected query");
3019        };
3020        assert!(matches!(
3021            grouped.projection.unwrap()[0].expr,
3022            Expr::FunctionCall(AggFunc::Sum, _, AggregateMode::Raw)
3023        ));
3024    }
3025
3026    #[test]
3027    fn test_parse_avg_min_max_with_field() {
3028        for (src, expected) in [
3029            ("avg(User { .age })", AggFunc::Avg),
3030            ("min(User { .age })", AggFunc::Min),
3031            ("max(User { .age })", AggFunc::Max),
3032        ] {
3033            let stmt = parse(src).unwrap();
3034            match stmt {
3035                Statement::Query(q) => {
3036                    let agg = q.aggregation.unwrap();
3037                    assert_eq!(agg.function, expected, "func mismatch for {src}");
3038                    assert_eq!(
3039                        agg.argument,
3040                        Some(Expr::Field("age".into())),
3041                        "field mismatch for {src}"
3042                    );
3043                    assert!(
3044                        q.projection.is_none(),
3045                        "projection should be cleared for {src}"
3046                    );
3047                }
3048                _ => panic!("expected query for {src}"),
3049            }
3050        }
3051    }
3052
3053    #[test]
3054    fn test_parse_count_lifts_projection_into_argument() {
3055        // A projected column names what to count: `count(User { .age })` is a
3056        // non-null count of `.age`, matching the grouped `count(.age)` path and
3057        // SQL's `COUNT(age)`. Leaving the projection in place (the previous
3058        // behavior) silently made it a row count.
3059        let stmt = parse("count(User { .age })").unwrap();
3060        match stmt {
3061            Statement::Query(q) => {
3062                let agg = q.aggregation.unwrap();
3063                assert_eq!(agg.function, AggFunc::Count);
3064                assert_eq!(agg.argument, Some(Expr::Field("age".into())));
3065                assert!(q.projection.is_none(), "projection should be lifted");
3066            }
3067            _ => panic!("expected query"),
3068        }
3069    }
3070
3071    #[test]
3072    fn test_parse_count_without_projection_has_no_argument() {
3073        // `count(User)` stays a row count.
3074        let stmt = parse("count(User)").unwrap();
3075        match stmt {
3076            Statement::Query(q) => {
3077                let agg = q.aggregation.unwrap();
3078                assert_eq!(agg.function, AggFunc::Count);
3079                assert!(agg.argument.is_none());
3080            }
3081            _ => panic!("expected query"),
3082        }
3083    }
3084
3085    // ---- Mission E1.1: JOIN parser tests ----------------------------------
3086    // Parser-level only. The planner rejects joins with a clean error until
3087    // E1.2 wires up execution.
3088
3089    #[test]
3090    fn test_parse_source_alias() {
3091        let stmt = parse("User as u filter u.age > 30").unwrap();
3092        match stmt {
3093            Statement::Query(q) => {
3094                assert_eq!(q.source, "User");
3095                assert_eq!(q.alias.as_deref(), Some("u"));
3096                assert!(q.joins.is_empty());
3097                match q.filter.unwrap() {
3098                    Expr::BinaryOp(l, BinOp::Gt, _) => match *l {
3099                        Expr::QualifiedField { qualifier, field } => {
3100                            assert_eq!(qualifier, "u");
3101                            assert_eq!(field, "age");
3102                        }
3103                        other => panic!("expected qualified field, got {other:?}"),
3104                    },
3105                    other => panic!("expected >, got {other:?}"),
3106                }
3107            }
3108            _ => panic!("expected query"),
3109        }
3110    }
3111
3112    #[test]
3113    fn test_parse_inner_join_on() {
3114        let stmt = parse("User as u inner join Order as o on u.id = o.user_id").unwrap();
3115        match stmt {
3116            Statement::Query(q) => {
3117                assert_eq!(q.source, "User");
3118                assert_eq!(q.alias.as_deref(), Some("u"));
3119                assert_eq!(q.joins.len(), 1);
3120                let j = &q.joins[0];
3121                assert_eq!(j.kind, JoinKind::Inner);
3122                assert_eq!(j.source, "Order");
3123                assert_eq!(j.alias.as_deref(), Some("o"));
3124                let on = j.on.as_ref().expect("on clause");
3125                match on {
3126                    Expr::BinaryOp(l, BinOp::Eq, r) => {
3127                        assert!(matches!(**l, Expr::QualifiedField { .. }));
3128                        assert!(matches!(**r, Expr::QualifiedField { .. }));
3129                    }
3130                    other => panic!("expected eq, got {other:?}"),
3131                }
3132            }
3133            _ => panic!("expected query"),
3134        }
3135    }
3136
3137    #[test]
3138    fn test_parse_bare_join_defaults_to_inner() {
3139        let stmt = parse("User join Order on User.id = Order.user_id").unwrap();
3140        match stmt {
3141            Statement::Query(q) => {
3142                assert_eq!(q.joins.len(), 1);
3143                assert_eq!(q.joins[0].kind, JoinKind::Inner);
3144            }
3145            _ => panic!("expected query"),
3146        }
3147    }
3148
3149    #[test]
3150    fn test_parse_left_outer_join() {
3151        let stmt = parse("User as u left outer join Order as o on u.id = o.user_id").unwrap();
3152        match stmt {
3153            Statement::Query(q) => {
3154                assert_eq!(q.joins.len(), 1);
3155                assert_eq!(q.joins[0].kind, JoinKind::LeftOuter);
3156            }
3157            _ => panic!("expected query"),
3158        }
3159    }
3160
3161    #[test]
3162    fn test_parse_left_join_without_outer_keyword() {
3163        // `left join` is shorthand for `left outer join` in SQL — we accept it.
3164        let stmt = parse("User as u left join Order as o on u.id = o.user_id").unwrap();
3165        match stmt {
3166            Statement::Query(q) => {
3167                assert_eq!(q.joins[0].kind, JoinKind::LeftOuter);
3168            }
3169            _ => panic!("expected query"),
3170        }
3171    }
3172
3173    #[test]
3174    fn test_parse_right_join() {
3175        let stmt = parse("User as u right join Order as o on u.id = o.user_id").unwrap();
3176        match stmt {
3177            Statement::Query(q) => {
3178                assert_eq!(q.joins[0].kind, JoinKind::RightOuter);
3179            }
3180            _ => panic!("expected query"),
3181        }
3182    }
3183
3184    #[test]
3185    fn test_parse_cross_join_has_no_on() {
3186        let stmt = parse("User cross join Order").unwrap();
3187        match stmt {
3188            Statement::Query(q) => {
3189                assert_eq!(q.joins[0].kind, JoinKind::Cross);
3190                assert!(q.joins[0].on.is_none());
3191            }
3192            _ => panic!("expected query"),
3193        }
3194    }
3195
3196    #[test]
3197    fn test_parse_multi_join_chain() {
3198        let stmt = parse(
3199            "User as u join Order as o on u.id = o.user_id \
3200             join Product as p on o.product_id = p.id",
3201        )
3202        .unwrap();
3203        match stmt {
3204            Statement::Query(q) => {
3205                assert_eq!(q.joins.len(), 2);
3206                assert_eq!(q.joins[0].source, "Order");
3207                assert_eq!(q.joins[1].source, "Product");
3208            }
3209            _ => panic!("expected query"),
3210        }
3211    }
3212
3213    #[test]
3214    fn test_parse_join_with_filter_tail() {
3215        // Filter/order/limit still work after a join clause.
3216        let stmt = parse(
3217            "User as u join Order as o on u.id = o.user_id \
3218             filter o.total > 100 order .name limit 10",
3219        )
3220        .unwrap();
3221        match stmt {
3222            Statement::Query(q) => {
3223                assert_eq!(q.joins.len(), 1);
3224                assert!(q.filter.is_some());
3225                assert!(q.order.is_some());
3226                assert!(q.limit.is_some());
3227            }
3228            _ => panic!("expected query"),
3229        }
3230    }
3231
3232    #[test]
3233    fn test_parse_join_requires_on_for_inner() {
3234        // Non-cross joins require `on <expr>`. Missing `on` is a parse error.
3235        let err = parse("User join Order").unwrap_err();
3236        assert!(
3237            err.message().contains("on"),
3238            "expected on-clause error, got {:?}",
3239            err.message()
3240        );
3241    }
3242
3243    #[test]
3244    fn test_parse_update_on_joined_query_errors() {
3245        // E1.1 explicitly rejects update/delete on joined queries — SQL
3246        // semantics here are messy and we're not implementing them yet.
3247        let err =
3248            parse("User as u join Order as o on u.id = o.user_id update { age := 1 }").unwrap_err();
3249        assert!(err.message().contains("update"));
3250    }
3251
3252    #[test]
3253    fn test_parse_delete_on_joined_query_errors() {
3254        let err = parse("User as u join Order as o on u.id = o.user_id delete").unwrap_err();
3255        assert!(err.message().contains("delete"));
3256    }
3257
3258    // ---- Mission E2a: DISTINCT + IN-list + BETWEEN + LIKE -----------------
3259
3260    #[test]
3261    fn test_parse_distinct() {
3262        let stmt = parse("User distinct { .name }").unwrap();
3263        match stmt {
3264            Statement::Query(q) => {
3265                assert!(q.distinct);
3266                assert!(q.projection.is_some());
3267            }
3268            _ => panic!("expected query"),
3269        }
3270    }
3271
3272    #[test]
3273    fn test_parse_in_list() {
3274        let stmt = parse(r#"User filter .name in ("Alice", "Bob")"#).unwrap();
3275        match stmt {
3276            Statement::Query(q) => match q.filter.unwrap() {
3277                Expr::InList {
3278                    expr,
3279                    list,
3280                    negated,
3281                } => {
3282                    assert!(!negated);
3283                    assert!(matches!(*expr, Expr::Field(f) if f == "name"));
3284                    assert_eq!(list.len(), 2);
3285                }
3286                other => panic!("expected InList, got {other:?}"),
3287            },
3288            _ => panic!("expected query"),
3289        }
3290    }
3291
3292    #[test]
3293    fn test_parse_not_in_list() {
3294        let stmt = parse("User filter .age not in (1, 2, 3)").unwrap();
3295        match stmt {
3296            Statement::Query(q) => match q.filter.unwrap() {
3297                Expr::InList { negated, list, .. } => {
3298                    assert!(negated);
3299                    assert_eq!(list.len(), 3);
3300                }
3301                other => panic!("expected InList, got {other:?}"),
3302            },
3303            _ => panic!("expected query"),
3304        }
3305    }
3306
3307    #[test]
3308    fn test_parse_between() {
3309        // BETWEEN desugars into >= AND <=.
3310        let stmt = parse("User filter .age between 10 and 20").unwrap();
3311        match stmt {
3312            Statement::Query(q) => {
3313                match q.filter.unwrap() {
3314                    Expr::BinaryOp(_, BinOp::And, _) => {} // desugared
3315                    other => panic!("expected And (desugared between), got {other:?}"),
3316                }
3317            }
3318            _ => panic!("expected query"),
3319        }
3320    }
3321
3322    #[test]
3323    fn test_parse_not_between() {
3324        // NOT BETWEEN desugars into < OR >.
3325        let stmt = parse("User filter .age not between 10 and 20").unwrap();
3326        match stmt {
3327            Statement::Query(q) => {
3328                match q.filter.unwrap() {
3329                    Expr::BinaryOp(_, BinOp::Or, _) => {} // desugared
3330                    other => panic!("expected Or (desugared not between), got {other:?}"),
3331                }
3332            }
3333            _ => panic!("expected query"),
3334        }
3335    }
3336
3337    #[test]
3338    fn test_parse_like() {
3339        let stmt = parse(r#"User filter .name like "A%""#).unwrap();
3340        match stmt {
3341            Statement::Query(q) => match q.filter.unwrap() {
3342                Expr::BinaryOp(l, BinOp::Like, r) => {
3343                    assert!(matches!(*l, Expr::Field(f) if f == "name"));
3344                    assert!(matches!(*r, Expr::Literal(Literal::String(s)) if s == "A%"));
3345                }
3346                other => panic!("expected Like, got {other:?}"),
3347            },
3348            _ => panic!("expected query"),
3349        }
3350    }
3351
3352    #[test]
3353    fn test_parse_not_like() {
3354        let stmt = parse(r#"User filter .name not like "A%""#).unwrap();
3355        match stmt {
3356            Statement::Query(q) => match q.filter.unwrap() {
3357                Expr::UnaryOp(UnaryOp::Not, inner) => {
3358                    assert!(matches!(*inner, Expr::BinaryOp(_, BinOp::Like, _)));
3359                }
3360                other => panic!("expected Not(Like), got {other:?}"),
3361            },
3362            _ => panic!("expected query"),
3363        }
3364    }
3365
3366    // ---- Mission E2b: GROUP BY + HAVING ------------------------------------
3367
3368    #[test]
3369    fn test_parse_group_by_single_key() {
3370        let stmt = parse("User group .status { .status, n: count(.name) }").unwrap();
3371        match stmt {
3372            Statement::Query(q) => {
3373                let gb = q.group_by.unwrap();
3374                assert_eq!(
3375                    gb.keys,
3376                    vec![GroupKey {
3377                        expr: Expr::Field("status".into()),
3378                        output_name: "status".into(),
3379                    }]
3380                );
3381                assert!(gb.having.is_none());
3382                let proj = q.projection.unwrap();
3383                assert_eq!(proj.len(), 2);
3384                assert!(matches!(
3385                    &proj[1].expr,
3386                    Expr::FunctionCall(AggFunc::Count, _, _)
3387                ));
3388                assert_eq!(proj[1].alias.as_deref(), Some("n"));
3389            }
3390            _ => panic!("expected query"),
3391        }
3392    }
3393
3394    #[test]
3395    fn test_parse_group_by_multi_key() {
3396        let stmt = parse("User group .status, .age { .status, .age }").unwrap();
3397        match stmt {
3398            Statement::Query(q) => {
3399                let gb = q.group_by.unwrap();
3400                assert_eq!(
3401                    gb.keys,
3402                    vec![
3403                        GroupKey {
3404                            expr: Expr::Field("status".into()),
3405                            output_name: "status".into(),
3406                        },
3407                        GroupKey {
3408                            expr: Expr::Field("age".into()),
3409                            output_name: "age".into(),
3410                        }
3411                    ]
3412                );
3413            }
3414            _ => panic!("expected query"),
3415        }
3416    }
3417
3418    #[test]
3419    fn test_parse_group_by_having() {
3420        let stmt = parse("User group .status having count(.name) > 1 { .status }").unwrap();
3421        match stmt {
3422            Statement::Query(q) => {
3423                let gb = q.group_by.unwrap();
3424                assert_eq!(
3425                    gb.keys,
3426                    vec![GroupKey {
3427                        expr: Expr::Field("status".into()),
3428                        output_name: "status".into(),
3429                    }]
3430                );
3431                assert!(gb.having.is_some());
3432                // HAVING is `count(.name) > 1` — BinaryOp(FunctionCall, Gt, Literal)
3433                match gb.having.unwrap() {
3434                    Expr::BinaryOp(l, BinOp::Gt, _) => {
3435                        assert!(matches!(*l, Expr::FunctionCall(AggFunc::Count, _, _)));
3436                    }
3437                    other => panic!("expected BinaryOp, got {other:?}"),
3438                }
3439            }
3440            _ => panic!("expected query"),
3441        }
3442    }
3443
3444    #[test]
3445    fn test_parse_aggregate_in_projection() {
3446        // Unaliased aggregate function calls in projection.
3447        let stmt = parse("User group .status { .status, count(.name), sum(.age) }").unwrap();
3448        match stmt {
3449            Statement::Query(q) => {
3450                let proj = q.projection.unwrap();
3451                assert_eq!(proj.len(), 3);
3452                assert!(matches!(
3453                    &proj[1].expr,
3454                    Expr::FunctionCall(AggFunc::Count, _, _)
3455                ));
3456                assert!(matches!(
3457                    &proj[2].expr,
3458                    Expr::FunctionCall(AggFunc::Sum, _, _)
3459                ));
3460            }
3461            _ => panic!("expected query"),
3462        }
3463    }
3464
3465    #[test]
3466    fn test_parse_aggregate_in_aliased_projection() {
3467        let stmt = parse("User group .status { .status, total: count(.name), average: avg(.age) }")
3468            .unwrap();
3469        match stmt {
3470            Statement::Query(q) => {
3471                let proj = q.projection.unwrap();
3472                assert_eq!(proj[1].alias.as_deref(), Some("total"));
3473                assert!(matches!(
3474                    &proj[1].expr,
3475                    Expr::FunctionCall(AggFunc::Count, _, _)
3476                ));
3477                assert_eq!(proj[2].alias.as_deref(), Some("average"));
3478                assert!(matches!(
3479                    &proj[2].expr,
3480                    Expr::FunctionCall(AggFunc::Avg, _, _)
3481                ));
3482            }
3483            _ => panic!("expected query"),
3484        }
3485    }
3486
3487    // ─── IS NULL / IS NOT NULL parser tests ────────────────────────────
3488
3489    #[test]
3490    fn test_parse_is_null() {
3491        let stmt = parse("User filter .age is null").unwrap();
3492        match stmt {
3493            Statement::Query(q) => {
3494                let filter = q.filter.unwrap();
3495                assert_eq!(
3496                    filter,
3497                    Expr::UnaryOp(UnaryOp::IsNull, Box::new(Expr::Field("age".into())))
3498                );
3499            }
3500            _ => panic!("expected query"),
3501        }
3502    }
3503
3504    #[test]
3505    fn test_parse_is_not_null() {
3506        let stmt = parse("User filter .age is not null").unwrap();
3507        match stmt {
3508            Statement::Query(q) => {
3509                let filter = q.filter.unwrap();
3510                assert_eq!(
3511                    filter,
3512                    Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(Expr::Field("age".into())))
3513                );
3514            }
3515            _ => panic!("expected query"),
3516        }
3517    }
3518
3519    #[test]
3520    fn test_parse_eq_null_desugars_to_is_null() {
3521        let stmt = parse("User filter .age = null").unwrap();
3522        match stmt {
3523            Statement::Query(q) => {
3524                let filter = q.filter.unwrap();
3525                assert_eq!(
3526                    filter,
3527                    Expr::UnaryOp(UnaryOp::IsNull, Box::new(Expr::Field("age".into())))
3528                );
3529            }
3530            _ => panic!("expected query"),
3531        }
3532    }
3533
3534    #[test]
3535    fn test_parse_neq_null_desugars_to_is_not_null() {
3536        let stmt = parse("User filter .age != null").unwrap();
3537        match stmt {
3538            Statement::Query(q) => {
3539                let filter = q.filter.unwrap();
3540                assert_eq!(
3541                    filter,
3542                    Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(Expr::Field("age".into())))
3543                );
3544            }
3545            _ => panic!("expected query"),
3546        }
3547    }
3548
3549    #[test]
3550    fn test_parse_null_comparisons_parse_ok() {
3551        // `< null`, `>= null` etc. parse successfully now that `null` is a
3552        // valid expression. At runtime they evaluate to Empty (no match),
3553        // which is correct null-propagation semantics.
3554        assert!(parse("User filter .age < null").is_ok());
3555        assert!(parse("User filter .age >= null").is_ok());
3556    }
3557
3558    #[test]
3559    fn test_parse_count_star_expr() {
3560        let stmt = parse("User filter count(*) > 0").unwrap();
3561        match stmt {
3562            Statement::Query(q) => {
3563                let filter = q.filter.unwrap();
3564                match filter {
3565                    Expr::BinaryOp(left, BinOp::Gt, _) => {
3566                        assert_eq!(
3567                            *left,
3568                            Expr::FunctionCall(
3569                                AggFunc::Count,
3570                                Box::new(Expr::Field("*".into())),
3571                                AggregateMode::Symmetric,
3572                            )
3573                        );
3574                    }
3575                    _ => panic!("expected comparison"),
3576                }
3577            }
3578            _ => panic!("expected query"),
3579        }
3580    }
3581
3582    // ─── String function parser tests ──────────────────────────────────
3583
3584    #[test]
3585    fn test_parse_upper_in_filter() {
3586        let stmt = parse(r#"User filter upper(.name) = "ALICE""#).unwrap();
3587        match stmt {
3588            Statement::Query(q) => {
3589                let f = q.filter.unwrap();
3590                match f {
3591                    Expr::BinaryOp(left, BinOp::Eq, _right) => {
3592                        assert!(matches!(*left, Expr::ScalarFunc(ScalarFn::Upper, _)));
3593                    }
3594                    _ => panic!("expected binary op with upper"),
3595                }
3596            }
3597            _ => panic!("expected query"),
3598        }
3599    }
3600
3601    #[test]
3602    fn test_parse_substring() {
3603        let stmt = parse("User { sub: substring(.name, 1, 3) }").unwrap();
3604        match stmt {
3605            Statement::Query(q) => {
3606                let proj = q.projection.unwrap();
3607                match &proj[0].expr {
3608                    Expr::ScalarFunc(ScalarFn::Substring, args) => {
3609                        assert_eq!(args.len(), 3);
3610                    }
3611                    other => panic!("expected ScalarFunc Substring, got {other:?}"),
3612                }
3613            }
3614            _ => panic!("expected query"),
3615        }
3616    }
3617
3618    #[test]
3619    fn test_parse_concat() {
3620        let stmt = parse(r#"User { full: concat(.name, " - ", .email) }"#).unwrap();
3621        match stmt {
3622            Statement::Query(q) => {
3623                let proj = q.projection.unwrap();
3624                match &proj[0].expr {
3625                    Expr::ScalarFunc(ScalarFn::Concat, args) => {
3626                        assert_eq!(args.len(), 3);
3627                    }
3628                    other => panic!("expected ScalarFunc Concat, got {other:?}"),
3629                }
3630            }
3631            _ => panic!("expected query"),
3632        }
3633    }
3634
3635    // ─── CASE WHEN parser tests ────────────────────────────────────────
3636
3637    #[test]
3638    fn test_parse_case_single_when() {
3639        let stmt = parse(r#"User filter case when .age > 30 then true else false end"#).unwrap();
3640        match stmt {
3641            Statement::Query(q) => {
3642                let filter = q.filter.unwrap();
3643                match filter {
3644                    Expr::Case { whens, else_expr } => {
3645                        assert_eq!(whens.len(), 1);
3646                        assert!(else_expr.is_some());
3647                    }
3648                    other => panic!("expected Case expr, got {other:?}"),
3649                }
3650            }
3651            _ => panic!("expected query"),
3652        }
3653    }
3654
3655    #[test]
3656    fn test_parse_case_multiple_whens() {
3657        let stmt = parse(
3658            r#"User { label: case when .age > 30 then "senior" when .age > 20 then "adult" else "young" end }"#
3659        ).unwrap();
3660        match stmt {
3661            Statement::Query(q) => {
3662                let proj = q.projection.unwrap();
3663                match &proj[0].expr {
3664                    Expr::Case { whens, else_expr } => {
3665                        assert_eq!(whens.len(), 2);
3666                        assert!(else_expr.is_some());
3667                    }
3668                    other => panic!("expected Case expr, got {other:?}"),
3669                }
3670            }
3671            _ => panic!("expected query"),
3672        }
3673    }
3674
3675    #[test]
3676    fn test_parse_case_without_else() {
3677        let stmt = parse(r#"User filter case when .age > 30 then true end"#).unwrap();
3678        match stmt {
3679            Statement::Query(q) => {
3680                let filter = q.filter.unwrap();
3681                match filter {
3682                    Expr::Case { whens, else_expr } => {
3683                        assert_eq!(whens.len(), 1);
3684                        assert!(else_expr.is_none());
3685                    }
3686                    other => panic!("expected Case expr, got {other:?}"),
3687                }
3688            }
3689            _ => panic!("expected query"),
3690        }
3691    }
3692
3693    // ─── Mul/Div expression tests (E2f) ───────────────────────────────
3694
3695    #[test]
3696    fn test_parse_mul_expr() {
3697        let stmt = parse("User filter .price * .quantity > 100").unwrap();
3698        match stmt {
3699            Statement::Query(q) => {
3700                let filter = q.filter.unwrap();
3701                match filter {
3702                    Expr::BinaryOp(left, BinOp::Gt, _) => match *left {
3703                        Expr::BinaryOp(_, BinOp::Mul, _) => {}
3704                        other => panic!("expected Mul, got {other:?}"),
3705                    },
3706                    other => panic!("expected BinaryOp Gt, got {other:?}"),
3707                }
3708            }
3709            _ => panic!("expected query"),
3710        }
3711    }
3712
3713    #[test]
3714    fn test_parse_div_expr() {
3715        let stmt = parse("User { ratio: .total / .count }").unwrap();
3716        match stmt {
3717            Statement::Query(q) => {
3718                let proj = q.projection.unwrap();
3719                assert_eq!(proj[0].alias.as_deref(), Some("ratio"));
3720                match &proj[0].expr {
3721                    Expr::BinaryOp(_, BinOp::Div, _) => {}
3722                    other => panic!("expected Div, got {other:?}"),
3723                }
3724            }
3725            _ => panic!("expected query"),
3726        }
3727    }
3728
3729    #[test]
3730    fn test_parse_mul_div_precedence() {
3731        // .a + .b * .c should parse as .a + (.b * .c)
3732        let stmt = parse("User filter .a + .b * .c > 0").unwrap();
3733        match stmt {
3734            Statement::Query(q) => {
3735                let filter = q.filter.unwrap();
3736                match filter {
3737                    Expr::BinaryOp(left, BinOp::Gt, _) => match *left {
3738                        Expr::BinaryOp(_, BinOp::Add, right) => {
3739                            assert!(matches!(*right, Expr::BinaryOp(_, BinOp::Mul, _)));
3740                        }
3741                        other => panic!("expected Add, got {other:?}"),
3742                    },
3743                    other => panic!("expected Gt, got {other:?}"),
3744                }
3745            }
3746            _ => panic!("expected query"),
3747        }
3748    }
3749
3750    // ─── Multi-column ORDER BY tests (E2f) ────────────────────────────
3751
3752    #[test]
3753    fn test_parse_multi_order() {
3754        let stmt = parse("User order .name asc, .age desc").unwrap();
3755        match stmt {
3756            Statement::Query(q) => {
3757                let order = q.order.unwrap();
3758                assert_eq!(order.keys.len(), 2);
3759                assert_eq!(order.keys[0].expr, Expr::Field("name".into()));
3760                assert!(!order.keys[0].descending);
3761                assert_eq!(order.keys[1].expr, Expr::Field("age".into()));
3762                assert!(order.keys[1].descending);
3763            }
3764            _ => panic!("expected query"),
3765        }
3766    }
3767
3768    #[test]
3769    fn test_parse_order_default_asc() {
3770        let stmt = parse("User order .name").unwrap();
3771        match stmt {
3772            Statement::Query(q) => {
3773                let order = q.order.unwrap();
3774                assert_eq!(order.keys.len(), 1);
3775                assert!(!order.keys[0].descending);
3776            }
3777            _ => panic!("expected query"),
3778        }
3779    }
3780
3781    // ─── ALTER TABLE / DROP TABLE parser tests (E2g) ──────────────────
3782
3783    #[test]
3784    fn test_parse_alter_add_column() {
3785        let stmt = parse("alter User add column status: str").unwrap();
3786        match stmt {
3787            Statement::AlterTable(at) => {
3788                assert_eq!(at.table, "User");
3789                match at.action {
3790                    AlterAction::AddColumn {
3791                        name,
3792                        type_name,
3793                        required,
3794                    } => {
3795                        assert_eq!(name, "status");
3796                        assert_eq!(type_name, "str");
3797                        assert!(!required);
3798                    }
3799                    other => panic!("expected AddColumn, got {other:?}"),
3800                }
3801            }
3802            other => panic!("expected AlterTable, got {other:?}"),
3803        }
3804    }
3805
3806    #[test]
3807    fn test_parse_alter_add_required_column() {
3808        let stmt = parse("alter User add required status: str").unwrap();
3809        match stmt {
3810            Statement::AlterTable(at) => match at.action {
3811                AlterAction::AddColumn { required, .. } => assert!(required),
3812                other => panic!("expected AddColumn, got {other:?}"),
3813            },
3814            other => panic!("expected AlterTable, got {other:?}"),
3815        }
3816    }
3817
3818    #[test]
3819    fn test_parse_type_with_unique_modifier() {
3820        let stmt = parse("type User { required unique email: str, age: int }").unwrap();
3821        match stmt {
3822            Statement::CreateType(ct) => {
3823                assert!(ct.fields[0].required && ct.fields[0].unique);
3824                assert!(!ct.fields[1].unique);
3825            }
3826            other => panic!("expected CreateType, got {other:?}"),
3827        }
3828    }
3829
3830    #[test]
3831    fn test_parse_type_unique_before_required() {
3832        // Modifiers accepted in either order.
3833        let stmt = parse("type User { unique required email: str }").unwrap();
3834        match stmt {
3835            Statement::CreateType(ct) => {
3836                assert!(ct.fields[0].required && ct.fields[0].unique);
3837            }
3838            other => panic!("expected CreateType, got {other:?}"),
3839        }
3840    }
3841
3842    #[test]
3843    fn test_parse_alter_add_unique() {
3844        let stmt = parse("alter User add unique .email").unwrap();
3845        match stmt {
3846            Statement::AlterTable(at) => assert!(matches!(
3847                at.action,
3848                AlterAction::AddUnique {
3849                    target: IndexTarget::Column(ref column),
3850                    ..
3851                } if column == "email"
3852            )),
3853            other => panic!("expected AlterTable, got {other:?}"),
3854        }
3855    }
3856
3857    #[test]
3858    fn test_parse_alter_drop_column() {
3859        let stmt = parse("alter User drop column status").unwrap();
3860        match stmt {
3861            Statement::AlterTable(at) => {
3862                assert_eq!(at.table, "User");
3863                match at.action {
3864                    AlterAction::DropColumn { name, .. } => assert_eq!(name, "status"),
3865                    other => panic!("expected DropColumn, got {other:?}"),
3866                }
3867            }
3868            other => panic!("expected AlterTable, got {other:?}"),
3869        }
3870    }
3871
3872    #[test]
3873    fn test_parse_alter_drop_without_column_keyword() {
3874        let stmt = parse("alter User drop status").unwrap();
3875        match stmt {
3876            Statement::AlterTable(at) => match at.action {
3877                AlterAction::DropColumn { name, .. } => assert_eq!(name, "status"),
3878                other => panic!("expected DropColumn, got {other:?}"),
3879            },
3880            other => panic!("expected AlterTable, got {other:?}"),
3881        }
3882    }
3883
3884    #[test]
3885    fn test_parse_drop_table() {
3886        let stmt = parse("drop User").unwrap();
3887        match stmt {
3888            Statement::DropTable(dt) => assert_eq!(dt.table, "User"),
3889            other => panic!("expected DropTable, got {other:?}"),
3890        }
3891    }
3892
3893    // ─── IN subquery parser tests (E2h) ───────────────────────────────
3894
3895    #[test]
3896    fn test_parse_in_subquery() {
3897        let stmt = parse("User filter .name in (VIP { .name })").unwrap();
3898        match stmt {
3899            Statement::Query(q) => {
3900                let filter = q.filter.unwrap();
3901                match filter {
3902                    Expr::InSubquery {
3903                        expr,
3904                        subquery,
3905                        negated,
3906                    } => {
3907                        assert!(!negated);
3908                        assert!(matches!(*expr, Expr::Field(ref f) if f == "name"));
3909                        assert_eq!(subquery.source, "VIP");
3910                    }
3911                    other => panic!("expected InSubquery, got {other:?}"),
3912                }
3913            }
3914            _ => panic!("expected query"),
3915        }
3916    }
3917
3918    #[test]
3919    fn test_parse_not_in_subquery() {
3920        let stmt = parse("User filter .id not in (Order { .user_id })").unwrap();
3921        match stmt {
3922            Statement::Query(q) => match q.filter.unwrap() {
3923                Expr::InSubquery { negated, .. } => assert!(negated),
3924                other => panic!("expected InSubquery, got {other:?}"),
3925            },
3926            _ => panic!("expected query"),
3927        }
3928    }
3929
3930    #[test]
3931    fn test_parse_in_literal_list_still_works() {
3932        // Ensure existing IN (literal) parsing isn't broken
3933        let stmt = parse("User filter .age in (25, 30, 35)").unwrap();
3934        match stmt {
3935            Statement::Query(q) => match q.filter.unwrap() {
3936                Expr::InList { list, negated, .. } => {
3937                    assert!(!negated);
3938                    assert_eq!(list.len(), 3);
3939                }
3940                other => panic!("expected InList, got {other:?}"),
3941            },
3942            _ => panic!("expected query"),
3943        }
3944    }
3945
3946    // ---- Materialized view parser tests ------------------------------------
3947
3948    #[test]
3949    fn test_parse_create_view() {
3950        let stmt = parse("materialize OldUsers as User filter .age > 28").unwrap();
3951        match stmt {
3952            Statement::CreateView(cv) => {
3953                assert_eq!(cv.name, "OldUsers");
3954                assert_eq!(cv.query.source, "User");
3955                assert!(cv.query.filter.is_some());
3956                assert!(!cv.query_text.is_empty());
3957            }
3958            _ => panic!("expected CreateView"),
3959        }
3960    }
3961
3962    #[test]
3963    fn test_parse_create_view_with_projection() {
3964        let stmt = parse("materialize UserNames as User { .name }").unwrap();
3965        match stmt {
3966            Statement::CreateView(cv) => {
3967                assert_eq!(cv.name, "UserNames");
3968                assert!(cv.query.projection.is_some());
3969            }
3970            _ => panic!("expected CreateView"),
3971        }
3972    }
3973
3974    #[test]
3975    fn test_parse_refresh_view() {
3976        let stmt = parse("refresh OldUsers").unwrap();
3977        match stmt {
3978            Statement::RefreshView(rv) => {
3979                assert_eq!(rv.name, "OldUsers");
3980            }
3981            _ => panic!("expected RefreshView"),
3982        }
3983    }
3984
3985    #[test]
3986    fn test_parse_drop_view() {
3987        let stmt = parse("drop view OldUsers").unwrap();
3988        match stmt {
3989            Statement::DropView(dv) => {
3990                assert_eq!(dv.name, "OldUsers");
3991            }
3992            _ => panic!("expected DropView"),
3993        }
3994    }
3995
3996    #[test]
3997    fn test_parse_drop_table_still_works() {
3998        let stmt = parse("drop Users").unwrap();
3999        match stmt {
4000            Statement::DropTable(dt) => {
4001                assert_eq!(dt.table, "Users");
4002            }
4003            _ => panic!("expected DropTable"),
4004        }
4005    }
4006
4007    #[test]
4008    fn test_parse_union() {
4009        let stmt = parse("User union Order").unwrap();
4010        match stmt {
4011            Statement::Union(u) => {
4012                assert!(!u.all);
4013                match *u.left {
4014                    Statement::Query(_) => {}
4015                    _ => panic!("expected Query on left"),
4016                }
4017                match *u.right {
4018                    Statement::Query(_) => {}
4019                    _ => panic!("expected Query on right"),
4020                }
4021            }
4022            _ => panic!("expected Union"),
4023        }
4024    }
4025
4026    #[test]
4027    fn test_parse_union_all() {
4028        let stmt = parse("User union all Order").unwrap();
4029        match stmt {
4030            Statement::Union(u) => {
4031                assert!(u.all, "expected UNION ALL");
4032                match *u.left {
4033                    Statement::Query(_) => {}
4034                    _ => panic!("expected Query on left"),
4035                }
4036                match *u.right {
4037                    Statement::Query(_) => {}
4038                    _ => panic!("expected Query on right"),
4039                }
4040            }
4041            _ => panic!("expected Union"),
4042        }
4043    }
4044
4045    #[test]
4046    fn test_parse_union_chain() {
4047        // Left-associative: A union B union C => Union(Union(A, B), C)
4048        let stmt = parse("User union Order union Product").unwrap();
4049        match stmt {
4050            Statement::Union(outer) => {
4051                assert!(!outer.all);
4052                // Right side is Product
4053                match *outer.right {
4054                    Statement::Query(q) => assert_eq!(q.source, "Product"),
4055                    _ => panic!("expected Query(Product) on right"),
4056                }
4057                // Left side is Union(User, Order)
4058                match *outer.left {
4059                    Statement::Union(inner) => {
4060                        assert!(!inner.all);
4061                        match *inner.left {
4062                            Statement::Query(q) => assert_eq!(q.source, "User"),
4063                            _ => panic!("expected Query(User)"),
4064                        }
4065                        match *inner.right {
4066                            Statement::Query(q) => assert_eq!(q.source, "Order"),
4067                            _ => panic!("expected Query(Order)"),
4068                        }
4069                    }
4070                    _ => panic!("expected inner Union"),
4071                }
4072            }
4073            _ => panic!("expected Union"),
4074        }
4075    }
4076
4077    #[test]
4078    fn test_parse_union_with_filter() {
4079        let stmt = parse("User filter .age > 10 union Order filter .total > 50").unwrap();
4080        match stmt {
4081            Statement::Union(u) => {
4082                assert!(!u.all);
4083                // Both sides should be queries (the filter is part of each query)
4084                match *u.left {
4085                    Statement::Query(q) => {
4086                        assert_eq!(q.source, "User");
4087                        assert!(q.filter.is_some());
4088                    }
4089                    _ => panic!("expected Query on left"),
4090                }
4091                match *u.right {
4092                    Statement::Query(q) => {
4093                        assert_eq!(q.source, "Order");
4094                        assert!(q.filter.is_some());
4095                    }
4096                    _ => panic!("expected Query on right"),
4097                }
4098            }
4099            _ => panic!("expected Union"),
4100        }
4101    }
4102
4103    #[test]
4104    fn test_parse_count_distinct_standalone() {
4105        let stmt = parse("count(distinct User { .name })").unwrap();
4106        match stmt {
4107            Statement::Query(q) => {
4108                let agg = q.aggregation.unwrap();
4109                assert_eq!(agg.function, AggFunc::CountDistinct);
4110                assert_eq!(agg.argument, Some(Expr::Field("name".into())));
4111            }
4112            _ => panic!("expected Query"),
4113        }
4114    }
4115
4116    #[test]
4117    fn test_parse_count_distinct_in_projection() {
4118        let stmt = parse("User group .dept { .dept, count(distinct .name) }").unwrap();
4119        match stmt {
4120            Statement::Query(q) => {
4121                let proj = q.projection.unwrap();
4122                assert_eq!(proj.len(), 2);
4123                match &proj[1].expr {
4124                    Expr::FunctionCall(func, _, _) => {
4125                        assert_eq!(*func, AggFunc::CountDistinct);
4126                    }
4127                    _ => panic!("expected FunctionCall"),
4128                }
4129            }
4130            _ => panic!("expected Query"),
4131        }
4132    }
4133
4134    // ---- Window function parser tests ----------------------------------------
4135
4136    #[test]
4137    fn test_parse_window_row_number_order() {
4138        let stmt = parse("User { .name, rn: row_number() over (order .age) }").unwrap();
4139        match stmt {
4140            Statement::Query(q) => {
4141                let proj = q.projection.unwrap();
4142                assert_eq!(proj.len(), 2);
4143                assert_eq!(proj[1].alias.as_deref(), Some("rn"));
4144                match &proj[1].expr {
4145                    Expr::Window {
4146                        function,
4147                        args,
4148                        partition_by,
4149                        order_by,
4150                        ..
4151                    } => {
4152                        assert_eq!(*function, WindowFunc::RowNumber);
4153                        assert!(args.is_empty());
4154                        assert!(partition_by.is_empty());
4155                        assert_eq!(order_by.len(), 1);
4156                        assert_eq!(order_by[0].expr, Expr::Field("age".into()));
4157                        assert!(!order_by[0].descending);
4158                    }
4159                    other => panic!("expected Window, got {other:?}"),
4160                }
4161            }
4162            _ => panic!("expected query"),
4163        }
4164    }
4165
4166    #[test]
4167    fn test_parse_window_sum_partition_order() {
4168        let stmt =
4169            parse("User { .name, s: sum(.salary) over (partition .dept order .salary) }").unwrap();
4170        match stmt {
4171            Statement::Query(q) => {
4172                let proj = q.projection.unwrap();
4173                assert_eq!(proj.len(), 2);
4174                assert_eq!(proj[1].alias.as_deref(), Some("s"));
4175                match &proj[1].expr {
4176                    Expr::Window {
4177                        function,
4178                        args,
4179                        partition_by,
4180                        order_by,
4181                        ..
4182                    } => {
4183                        assert_eq!(*function, WindowFunc::Sum);
4184                        assert_eq!(args.len(), 1);
4185                        assert!(matches!(&args[0], Expr::Field(f) if f == "salary"));
4186                        assert_eq!(partition_by, &[Expr::Field("dept".into())]);
4187                        assert_eq!(order_by.len(), 1);
4188                        assert_eq!(order_by[0].expr, Expr::Field("salary".into()));
4189                        assert!(!order_by[0].descending);
4190                    }
4191                    other => panic!("expected Window, got {other:?}"),
4192                }
4193            }
4194            _ => panic!("expected query"),
4195        }
4196    }
4197
4198    #[test]
4199    fn test_parse_window_rank_desc() {
4200        let stmt =
4201            parse("User { .dept, .salary, r: rank() over (partition .dept order .salary desc) }")
4202                .unwrap();
4203        match stmt {
4204            Statement::Query(q) => {
4205                let proj = q.projection.unwrap();
4206                assert_eq!(proj.len(), 3);
4207                match &proj[2].expr {
4208                    Expr::Window {
4209                        function,
4210                        partition_by,
4211                        order_by,
4212                        ..
4213                    } => {
4214                        assert_eq!(*function, WindowFunc::Rank);
4215                        assert_eq!(partition_by, &[Expr::Field("dept".into())]);
4216                        assert_eq!(order_by.len(), 1);
4217                        assert!(order_by[0].descending);
4218                    }
4219                    other => panic!("expected Window, got {other:?}"),
4220                }
4221            }
4222            _ => panic!("expected query"),
4223        }
4224    }
4225
4226    #[test]
4227    fn test_parse_window_dense_rank() {
4228        let stmt = parse("User { .name, dr: dense_rank() over (order .score desc) }").unwrap();
4229        match stmt {
4230            Statement::Query(q) => {
4231                let proj = q.projection.unwrap();
4232                assert_eq!(proj.len(), 2);
4233                match &proj[1].expr {
4234                    Expr::Window { function, .. } => {
4235                        assert_eq!(*function, WindowFunc::DenseRank);
4236                    }
4237                    other => panic!("expected Window, got {other:?}"),
4238                }
4239            }
4240            _ => panic!("expected query"),
4241        }
4242    }
4243
4244    #[test]
4245    fn test_parse_sum_without_over_is_aggregate() {
4246        // sum(.salary) alone (no `over`) stays as FunctionCall, not Window.
4247        let stmt = parse("User group .dept { .dept, total: sum(.salary) }").unwrap();
4248        match stmt {
4249            Statement::Query(q) => {
4250                let proj = q.projection.unwrap();
4251                assert_eq!(proj.len(), 2);
4252                match &proj[1].expr {
4253                    Expr::FunctionCall(AggFunc::Sum, _, _) => {} // correct
4254                    other => panic!("expected FunctionCall(Sum), got {other:?}"),
4255                }
4256            }
4257            _ => panic!("expected query"),
4258        }
4259    }
4260
4261    #[test]
4262    fn test_nesting_depth_limit() {
4263        // Build a deeply nested parenthesized expression that exceeds MAX_NESTING_DEPTH.
4264        let mut query = String::from("User filter ");
4265        for _ in 0..70 {
4266            query.push('(');
4267        }
4268        query.push_str(".age > 1");
4269        for _ in 0..70 {
4270            query.push(')');
4271        }
4272        let result = parse(&query);
4273        assert!(result.is_err());
4274        let err = result.unwrap_err();
4275        assert!(
4276            err.message().contains("nesting depth"),
4277            "expected nesting depth error, got: {}",
4278            err.message()
4279        );
4280    }
4281
4282    #[test]
4283    fn test_unary_prefix_nesting_depth_limit() {
4284        // A long chain of `not` prefixes recurses through parse_primary
4285        // without passing through parse_expr's guard. It must error cleanly
4286        // at the depth limit instead of overflowing the stack.
4287        let query = String::from("User filter ") + &"not ".repeat(5000) + ".active";
4288        let result = parse(&query);
4289        assert!(result.is_err());
4290        let err = result.unwrap_err();
4291        assert!(
4292            err.message().contains("nesting depth"),
4293            "expected nesting depth error, got: {}",
4294            err.message()
4295        );
4296    }
4297
4298    #[test]
4299    fn test_moderate_nesting_succeeds() {
4300        // 10 levels of nesting should be fine.
4301        let mut query = String::from("User filter ");
4302        for _ in 0..10 {
4303            query.push('(');
4304        }
4305        query.push_str(".age > 1");
4306        for _ in 0..10 {
4307            query.push(')');
4308        }
4309        assert!(parse(&query).is_ok());
4310    }
4311
4312    /// Regression for issue #26: `fuzz_parser` crashed on the 3-byte input
4313    /// `nn{` — the projection loop consumed the Eof token and then indexed
4314    /// past the end of `tokens`. Must return an error instead.
4315    #[test]
4316    fn test_parse_fuzz_repro_projection_eof() {
4317        let err = parse("nn{").expect_err("unterminated projection must error, not panic");
4318        let _ = err.message();
4319    }
4320
4321    /// Regression for issue #26: `fuzz_roundtrip` tripped the same bug with
4322    /// the 2-byte input `z{`.
4323    #[test]
4324    fn test_parse_fuzz_repro_short_projection_eof() {
4325        let err = parse("z{").expect_err("unterminated projection must error, not panic");
4326        let _ = err.message();
4327    }
4328
4329    #[test]
4330    fn test_update_at_statement_start_gives_helpful_error() {
4331        let err =
4332            parse(r#"update User filter .name = "Alice" { age := 31 }"#).expect_err("should fail");
4333        let msg = err.message();
4334        assert!(
4335            msg.contains("pipeline syntax"),
4336            "error should mention pipeline syntax, got: {msg}"
4337        );
4338        assert!(
4339            msg.contains("update"),
4340            "error should mention 'update', got: {msg}"
4341        );
4342    }
4343
4344    #[test]
4345    fn test_delete_at_statement_start_gives_helpful_error() {
4346        let err = parse("delete User filter .age < 18").expect_err("should fail");
4347        let msg = err.message();
4348        assert!(
4349            msg.contains("pipeline syntax"),
4350            "error should mention pipeline syntax, got: {msg}"
4351        );
4352        assert!(
4353            msg.contains("delete"),
4354            "error should mention 'delete', got: {msg}"
4355        );
4356    }
4357}
4358
4359#[cfg(test)]
4360mod cleanup_parser_dx_tests {
4361    use super::*;
4362
4363    #[test]
4364    fn typoed_statement_keyword_gets_suggestion() {
4365        let err = parse("updat User set age = 1").unwrap_err();
4366        let msg = err.to_string();
4367        assert!(msg.contains("near token"), "{msg}");
4368        assert!(msg.contains("did you mean `update`"), "{msg}");
4369    }
4370}
4371
4372#[cfg(test)]
4373mod dogfood_dx_tests {
4374    use super::*;
4375
4376    // ── P-6: reserved words as column names ────────────────────────────
4377
4378    #[test]
4379    fn reserved_word_field_name_gives_actionable_error() {
4380        let err = parse("type Post { type: str }").unwrap_err();
4381        let msg = err.to_string();
4382        assert!(
4383            msg.contains("'type' is a reserved word")
4384                && msg.contains("field name")
4385                && msg.contains("quote it as `type`"),
4386            "unhelpful message: {msg}"
4387        );
4388    }
4389
4390    #[test]
4391    fn reserved_modifier_word_as_field_name_gives_actionable_error() {
4392        // `required` is a modifier keyword; followed directly by `:` it is
4393        // instead the field's (reserved) name — the old error was the opaque
4394        // "expected field name, got ':'".
4395        let err = parse("type Post { required: bool }").unwrap_err();
4396        let msg = err.to_string();
4397        assert!(
4398            msg.contains("'required' is a reserved word") && msg.contains("quote it as `required`"),
4399            "unhelpful message: {msg}"
4400        );
4401    }
4402
4403    #[test]
4404    fn reserved_word_in_insert_assignment_gives_actionable_error() {
4405        let err = parse(r#"insert Post { type := "x" }"#).unwrap_err();
4406        let msg = err.to_string();
4407        assert!(msg.contains("'type' is a reserved word"), "{msg}");
4408    }
4409
4410    #[test]
4411    fn reserved_word_in_alter_column_gives_actionable_error() {
4412        let err = parse("alter Post add column order: int").unwrap_err();
4413        let msg = err.to_string();
4414        assert!(msg.contains("'order' is a reserved word"), "{msg}");
4415    }
4416
4417    #[test]
4418    fn backtick_field_name_parses_as_identifier() {
4419        let stmt = parse("type Post { `type`: str, `order`: int }").unwrap();
4420        match stmt {
4421            Statement::CreateType(ct) => {
4422                assert_eq!(ct.fields[0].name, "type");
4423                assert_eq!(ct.fields[1].name, "order");
4424            }
4425            other => panic!("expected CreateType, got {other:?}"),
4426        }
4427    }
4428
4429    #[test]
4430    fn backtick_field_still_honors_modifiers() {
4431        let stmt = parse("type Post { required `type`: str }").unwrap();
4432        match stmt {
4433            Statement::CreateType(ct) => {
4434                assert_eq!(ct.fields[0].name, "type");
4435                assert!(ct.fields[0].required);
4436            }
4437            other => panic!("expected CreateType, got {other:?}"),
4438        }
4439    }
4440
4441    // ── P-7: DDL idempotency ───────────────────────────────────────────
4442
4443    #[test]
4444    fn create_type_if_not_exists_parses() {
4445        let stmt = parse("type Post if not exists { id: int }").unwrap();
4446        match stmt {
4447            Statement::CreateType(ct) => assert!(ct.if_not_exists),
4448            other => panic!("expected CreateType, got {other:?}"),
4449        }
4450    }
4451
4452    #[test]
4453    fn create_type_without_clause_defaults_false() {
4454        let stmt = parse("type Post { id: int }").unwrap();
4455        match stmt {
4456            Statement::CreateType(ct) => assert!(!ct.if_not_exists),
4457            other => panic!("expected CreateType, got {other:?}"),
4458        }
4459    }
4460
4461    #[test]
4462    fn drop_if_exists_parses() {
4463        match parse("drop if exists Post").unwrap() {
4464            Statement::DropTable(dt) => assert!(dt.if_exists),
4465            other => panic!("expected DropTable, got {other:?}"),
4466        }
4467        match parse("drop Post").unwrap() {
4468            Statement::DropTable(dt) => assert!(!dt.if_exists),
4469            other => panic!("expected DropTable, got {other:?}"),
4470        }
4471    }
4472
4473    #[test]
4474    fn drop_view_if_exists_parses() {
4475        match parse("drop view if exists ActiveUsers").unwrap() {
4476            Statement::DropView(dv) => {
4477                assert!(dv.if_exists);
4478                assert_eq!(dv.name, "ActiveUsers");
4479            }
4480            other => panic!("expected DropView, got {other:?}"),
4481        }
4482    }
4483
4484    #[test]
4485    fn add_index_and_unique_if_not_exists_parse() {
4486        match parse("alter Post add index if not exists .slug").unwrap() {
4487            Statement::AlterTable(at) => {
4488                assert!(matches!(
4489                    at.action,
4490                    AlterAction::AddIndex {
4491                        if_not_exists: true,
4492                        ..
4493                    }
4494                ));
4495            }
4496            other => panic!("expected AlterTable, got {other:?}"),
4497        }
4498        match parse("alter Post add unique if not exists .slug").unwrap() {
4499            Statement::AlterTable(at) => {
4500                assert!(matches!(
4501                    at.action,
4502                    AlterAction::AddUnique {
4503                        if_not_exists: true,
4504                        ..
4505                    }
4506                ));
4507            }
4508            other => panic!("expected AlterTable, got {other:?}"),
4509        }
4510    }
4511
4512    #[test]
4513    fn expression_index_targets_parse_with_stable_table_local_identity() {
4514        use powdb_storage::stored_json_path::{
4515            StoredJsonPathSegmentV1 as Segment, StoredJsonPathV1,
4516        };
4517
4518        let expected = StoredJsonPathV1::new(
4519            "data",
4520            vec![Segment::Key("author".into()), Segment::Index(0)],
4521        );
4522        for query in [
4523            "alter Post add index (.data->author->0)",
4524            "alter Post add unique if not exists (.data->\"author\"->0)",
4525            "alter Post drop index if exists (.data->author->0)",
4526        ] {
4527            let Statement::AlterTable(alter) = parse(query).unwrap() else {
4528                panic!("expected alter table for {query}");
4529            };
4530            let (target, flag) = match alter.action {
4531                AlterAction::AddIndex {
4532                    target,
4533                    if_not_exists,
4534                }
4535                | AlterAction::AddUnique {
4536                    target,
4537                    if_not_exists,
4538                } => (target, if_not_exists),
4539                AlterAction::DropIndex { target, if_exists } => (target, if_exists),
4540                other => panic!("expected index action, got {other:?}"),
4541            };
4542            assert_eq!(target, IndexTarget::JsonPath(expected.clone()));
4543            assert_eq!(
4544                flag,
4545                query.contains("if not exists") || query.contains("if exists")
4546            );
4547        }
4548    }
4549
4550    #[test]
4551    fn expression_index_target_rejects_ambiguous_or_non_path_forms() {
4552        let cases = [
4553            (
4554                "alter Post add index .data->author",
4555                "must be parenthesized",
4556            ),
4557            (
4558                "alter Post add index (p.data->author)",
4559                "qualified JSON paths",
4560            ),
4561            (
4562                "alter Post add index (.data)",
4563                "use `.column` for a stored column",
4564            ),
4565            (
4566                "alter Post add index (.data->age + 1)",
4567                "only a direct JSON path",
4568            ),
4569            (
4570                "alter Post drop index ({ value := 1 })",
4571                "expected an unqualified JSON path",
4572            ),
4573        ];
4574        for (query, expected) in cases {
4575            let error = parse(query).expect_err(query).to_string();
4576            assert!(
4577                error.contains(expected),
4578                "`{query}` should mention `{expected}`, got `{error}`"
4579            );
4580        }
4581    }
4582
4583    #[test]
4584    fn alter_drop_column_if_exists_parses() {
4585        match parse("alter Post drop column if exists status").unwrap() {
4586            Statement::AlterTable(at) => {
4587                assert!(matches!(
4588                    at.action,
4589                    AlterAction::DropColumn {
4590                        if_exists: true,
4591                        ..
4592                    }
4593                ));
4594            }
4595            other => panic!("expected AlterTable, got {other:?}"),
4596        }
4597    }
4598
4599    // ── P-8: introspection ─────────────────────────────────────────────
4600
4601    #[test]
4602    fn schema_parses_to_list_types() {
4603        assert_eq!(parse("schema").unwrap(), Statement::ListTypes);
4604    }
4605
4606    #[test]
4607    fn describe_parses_to_describe() {
4608        assert_eq!(
4609            parse("describe Post").unwrap(),
4610            Statement::Describe("Post".to_string())
4611        );
4612    }
4613
4614    #[test]
4615    fn schema_with_type_aliases_describe() {
4616        assert_eq!(
4617            parse("schema Post").unwrap(),
4618            Statement::Describe("Post".to_string())
4619        );
4620    }
4621
4622    #[test]
4623    fn schema_links_parses_to_list_links() {
4624        assert_eq!(parse("schema links").unwrap(), Statement::ListLinks);
4625    }
4626
4627    #[test]
4628    fn describe_links_still_names_a_table() {
4629        // Only the `schema links` spelling is the link listing; `describe`
4630        // keeps treating `links` as an ordinary type name.
4631        assert_eq!(
4632            parse("describe links").unwrap(),
4633            Statement::Describe("links".to_string())
4634        );
4635    }
4636}
4637
4638#[cfg(test)]
4639mod json_path_tests {
4640    use super::*;
4641
4642    /// Pull the filter expression out of a single-table query.
4643    fn filter_of(src: &str) -> Expr {
4644        match parse(src).unwrap() {
4645            Statement::Query(q) => q.filter.expect("expected a filter"),
4646            other => panic!("expected a query, got {other:?}"),
4647        }
4648    }
4649
4650    #[test]
4651    fn ident_key_path() {
4652        // .data->author->name
4653        let e = filter_of(r#"Post filter .data->author->name = "x""#);
4654        let Expr::BinaryOp(lhs, BinOp::Eq, _) = e else {
4655            panic!("expected an equality, got {e:?}");
4656        };
4657        assert_eq!(
4658            *lhs,
4659            Expr::JsonPath {
4660                base: Box::new(Expr::Field("data".into())),
4661                segments: vec![PathSeg::Key("author".into()), PathSeg::Key("name".into())],
4662            }
4663        );
4664    }
4665
4666    #[test]
4667    fn string_form_key_path() {
4668        // .data->"weird key!" (PowQL strings are double-quoted)
4669        let e = filter_of(r#"Post filter .data->"weird key!" = 1"#);
4670        let Expr::BinaryOp(lhs, _, _) = e else {
4671            panic!("expected binop");
4672        };
4673        assert_eq!(
4674            *lhs,
4675            Expr::JsonPath {
4676                base: Box::new(Expr::Field("data".into())),
4677                segments: vec![PathSeg::Key("weird key!".into())],
4678            }
4679        );
4680    }
4681
4682    #[test]
4683    fn array_index_path() {
4684        // .data->tags->0
4685        let e = filter_of(r#"Post filter .data->tags->0 = "rust""#);
4686        let Expr::BinaryOp(lhs, _, _) = e else {
4687            panic!("expected binop");
4688        };
4689        assert_eq!(
4690            *lhs,
4691            Expr::JsonPath {
4692                base: Box::new(Expr::Field("data".into())),
4693                segments: vec![PathSeg::Key("tags".into()), PathSeg::Index(0)],
4694            }
4695        );
4696    }
4697
4698    #[test]
4699    fn qualified_base_path() {
4700        // posts.data->author  (join-qualified base)
4701        let e = filter_of(r#"Post as posts filter posts.data->author = "a""#);
4702        let Expr::BinaryOp(lhs, _, _) = e else {
4703            panic!("expected binop");
4704        };
4705        assert_eq!(
4706            *lhs,
4707            Expr::JsonPath {
4708                base: Box::new(Expr::QualifiedField {
4709                    qualifier: "posts".into(),
4710                    field: "data".into(),
4711                }),
4712                segments: vec![PathSeg::Key("author".into())],
4713            }
4714        );
4715    }
4716
4717    #[test]
4718    fn path_binds_tighter_than_comparison_and_arithmetic() {
4719        // `.data->age > 21` must be `(.data->age) > 21`, not `.data->(age > 21)`.
4720        let e = filter_of("Post filter .data->age > 21");
4721        let Expr::BinaryOp(lhs, BinOp::Gt, rhs) = e else {
4722            panic!("expected a top-level `>`, got {e:?}");
4723        };
4724        assert!(matches!(*lhs, Expr::JsonPath { .. }));
4725        assert_eq!(*rhs, Expr::Literal(Literal::Int(21)));
4726
4727        // `.a->b + 1` must be `(.a->b) + 1`.
4728        let e = filter_of("Post filter .a->b + 1 = 3");
4729        let Expr::BinaryOp(add, BinOp::Eq, _) = e else {
4730            panic!("expected eq");
4731        };
4732        let Expr::BinaryOp(lhs, BinOp::Add, _) = *add else {
4733            panic!("expected `+` under `=`, got {add:?}");
4734        };
4735        assert!(matches!(*lhs, Expr::JsonPath { .. }));
4736    }
4737
4738    #[test]
4739    fn dash_vs_arrow_lexing() {
4740        // `.a->1` is a path index: `->` lexes as one token because the chars
4741        // are adjacent, ahead of the single-char `-`.
4742        let idx = filter_of("Post filter .a->1 = 0");
4743        let Expr::BinaryOp(lhs, BinOp::Eq, _) = idx else {
4744            panic!("expected eq");
4745        };
4746        assert_eq!(
4747            *lhs,
4748            Expr::JsonPath {
4749                base: Box::new(Expr::Field("a".into())),
4750                segments: vec![PathSeg::Index(1)],
4751            }
4752        );
4753
4754        // `.a - 1` (spaced) is subtraction — the `-` is not glued to a digit,
4755        // so it lexes as the minus operator.
4756        let sub = filter_of("Post filter .a - 1 = 0");
4757        let Expr::BinaryOp(lhs, BinOp::Eq, _) = sub else {
4758            panic!("expected eq");
4759        };
4760        assert!(
4761            matches!(*lhs, Expr::BinaryOp(_, BinOp::Sub, _)),
4762            "`.a - 1` should be subtraction, got {lhs:?}"
4763        );
4764
4765        // `.a-1` (no spaces) is the lexer gotcha: `-1` is a NEGATIVE INTEGER
4766        // literal (the number rule fires when `-` is glued to a digit), so the
4767        // stream is `.a` then `-1` with no operator between — a parse error,
4768        // NOT subtraction and NOT a path.
4769        assert!(
4770            parse("Post filter .a-1 = 0").is_err(),
4771            "`.a-1` should fail to parse (negative-literal gotcha)"
4772        );
4773
4774        // `.a - >` is `.a` `-` `>` — a dangling `>`, a parse error.
4775        assert!(
4776            parse("Post filter .a - > 0").is_err(),
4777            "`.a - >` should fail to parse"
4778        );
4779    }
4780
4781    #[test]
4782    fn negative_index_rejected() {
4783        let err = parse("Post filter .data->-1 = 0").unwrap_err();
4784        assert!(
4785            err.to_string().contains("array index"),
4786            "expected an index error, got: {err}"
4787        );
4788    }
4789
4790    #[test]
4791    fn path_on_literal_base_rejected() {
4792        // A `->` after a non-field base is a parse error.
4793        let err = parse("Post filter 5->x = 1").unwrap_err();
4794        assert!(
4795            err.to_string().to_lowercase().contains("field base"),
4796            "expected a field-base error, got: {err}"
4797        );
4798    }
4799
4800    #[test]
4801    fn json_path_assignment_target_is_targeted_unsupported() {
4802        // `Doc update { .data->x := 5 }` must not die with a generic
4803        // "expected field name": it must name the unsupported position and
4804        // the whole-column alternative. Both the leading-dot path-target form
4805        // and the bare `data->x` form take the targeted branch.
4806        for stmt in [
4807            "Doc update { .data->x := 5 }",
4808            "Doc update { data->x := 5 }",
4809        ] {
4810            let err = parse(stmt).unwrap_err();
4811            assert!(
4812                matches!(err, ParseError::Unsupported { .. }),
4813                "{stmt}: expected Unsupported, got {err:?}"
4814            );
4815            let msg = err.to_string();
4816            assert!(
4817                msg.contains("JSON path assignment targets are not supported"),
4818                "{stmt}: message must state the unsupported feature: {msg}"
4819            );
4820            assert!(
4821                msg.contains("json_set"),
4822                "{stmt}: message must point at the whole-column alternative: {msg}"
4823            );
4824        }
4825        // A normal whole-column update still parses.
4826        assert!(parse(r#"Doc update { data := "{}" }"#).is_ok());
4827    }
4828
4829    #[test]
4830    fn json_type_scalar_parses() {
4831        let e = filter_of(r#"Post filter json_type(.data->x) = "string""#);
4832        let Expr::BinaryOp(lhs, _, _) = e else {
4833            panic!("expected binop");
4834        };
4835        let Expr::ScalarFunc(ScalarFn::JsonType, args) = *lhs else {
4836            panic!("expected json_type call, got {lhs:?}");
4837        };
4838        assert_eq!(args.len(), 1);
4839        assert!(matches!(args[0], Expr::JsonPath { .. }));
4840    }
4841
4842    #[test]
4843    fn path_in_projection() {
4844        // Projection, ordering, and grouping all retain the same structural
4845        // JsonPath expression rather than lowering it to an alias string.
4846        let stmt = parse("Post { author: .data->author }").unwrap();
4847        let Statement::Query(q) = stmt else {
4848            panic!("expected query");
4849        };
4850        let proj = q.projection.unwrap();
4851        assert_eq!(proj[0].alias.as_deref(), Some("author"));
4852        assert!(matches!(proj[0].expr, Expr::JsonPath { .. }));
4853
4854        let Statement::Query(ordered) = parse("Post order .data->author { .id }").unwrap() else {
4855            panic!("expected query");
4856        };
4857        assert!(matches!(
4858            ordered.order.unwrap().keys[0].expr,
4859            Expr::JsonPath { .. }
4860        ));
4861
4862        let Statement::Query(grouped) =
4863            parse("Post group .data->author { .data->author }").unwrap()
4864        else {
4865            panic!("expected query");
4866        };
4867        assert!(matches!(
4868            grouped.group_by.unwrap().keys[0].expr,
4869            Expr::JsonPath { .. }
4870        ));
4871    }
4872}