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