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