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#[derive(Debug)]
12pub enum ParseError {
13    /// Lexer failed to tokenize the input.
14    Lex { message: String, position: usize },
15    /// Expected one token but found another.
16    UnexpectedToken { expected: String, got: String },
17    /// Recursive nesting exceeded the safety limit.
18    NestingDepthExceeded { max: usize },
19    /// Syntactically valid construct that the engine doesn't support yet.
20    Unsupported { feature: String },
21    /// Catch-all for other syntax errors.
22    Syntax { message: String },
23}
24
25impl ParseError {
26    /// Convenience: human-readable message for any variant.
27    pub fn message(&self) -> String {
28        self.to_string()
29    }
30}
31
32impl std::fmt::Display for ParseError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Self::Lex { message, position } => write!(f, "at position {position}: {message}"),
36            Self::UnexpectedToken { expected, got } => {
37                write!(f, "expected {expected}, got {got}")
38            }
39            Self::NestingDepthExceeded { max } => {
40                write!(f, "query nesting depth exceeds maximum of {max}")
41            }
42            Self::Unsupported { feature } => write!(f, "{feature}"),
43            Self::Syntax { message } => write!(f, "{message}"),
44        }
45    }
46}
47
48impl std::error::Error for ParseError {}
49
50fn token_to_scalar_fn(tok: &Token) -> ScalarFn {
51    match tok {
52        Token::Upper => ScalarFn::Upper,
53        Token::Lower => ScalarFn::Lower,
54        Token::Length => ScalarFn::Length,
55        Token::Trim => ScalarFn::Trim,
56        Token::Substring => ScalarFn::Substring,
57        Token::Concat => ScalarFn::Concat,
58        Token::Abs => ScalarFn::Abs,
59        Token::Round => ScalarFn::Round,
60        Token::Ceil => ScalarFn::Ceil,
61        Token::Floor => ScalarFn::Floor,
62        Token::Sqrt => ScalarFn::Sqrt,
63        Token::Pow => ScalarFn::Pow,
64        Token::Now => ScalarFn::Now,
65        Token::Extract => ScalarFn::Extract,
66        Token::DateAdd => ScalarFn::DateAdd,
67        Token::DateDiff => ScalarFn::DateDiff,
68        _ => unreachable!(),
69    }
70}
71
72struct Parser {
73    tokens: Vec<Token>,
74    pos: usize,
75    depth: usize,
76}
77
78/// Parse a PowQL query string into an AST [`Statement`].
79///
80/// # Examples
81///
82/// ```
83/// use powdb_query::parser::parse;
84/// use powdb_query::ast::Statement;
85///
86/// // A bare type name is a query (select all rows).
87/// let stmt = parse("User").unwrap();
88/// assert!(matches!(stmt, Statement::Query(_)));
89/// ```
90///
91/// ```
92/// use powdb_query::parser::parse;
93/// use powdb_query::ast::Statement;
94///
95/// // DDL: define a new type (table).
96/// let stmt = parse("type User { required name: str, age: int }").unwrap();
97/// assert!(matches!(stmt, Statement::CreateType(_)));
98/// ```
99pub fn parse(input: &str) -> Result<Statement, ParseError> {
100    let tokens = lex(input).map_err(|e| ParseError::Lex {
101        message: e.message,
102        position: e.position,
103    })?;
104    parse_tokens(tokens)
105}
106
107/// Parse PowQL with `$N` placeholders bound to positional `params`.
108///
109/// Binding happens at the **token level**: the input is lexed, each
110/// `$N` placeholder token is replaced in place with the literal token
111/// for `params[N-1]` (a string param becomes a `StringLit` byte-for-byte,
112/// `null` becomes `Token::Null`), and the resulting token stream is parsed
113/// normally. Values are never re-lexed or string-interpolated, so an
114/// injection-shaped string is inert data — it can never change the query's
115/// shape.
116///
117/// Placeholders are 1-based (`$1`, `$2`, …). A reference to a placeholder
118/// with no corresponding parameter is a clean [`ParseError::Syntax`], as is
119/// a non-numeric `$name` (the named-parameter form belongs to the in-process
120/// prepared API, not the positional wire-binding path).
121pub fn parse_with_params(input: &str, params: &[ParamValue]) -> Result<Statement, ParseError> {
122    let mut tokens = lex(input).map_err(|e| ParseError::Lex {
123        message: e.message,
124        position: e.position,
125    })?;
126    for tok in tokens.iter_mut() {
127        if let Token::Param(name) = tok {
128            let n: usize = name.parse().map_err(|_| ParseError::Syntax {
129                message: format!(
130                    "positional parameters must be numeric (`$1`, `$2`, …); got `${name}`"
131                ),
132            })?;
133            if n == 0 {
134                return Err(ParseError::Syntax {
135                    message: "parameter placeholders are 1-based; `$0` is invalid".into(),
136                });
137            }
138            let p = params.get(n - 1).ok_or_else(|| ParseError::Syntax {
139                message: format!(
140                    "query references ${n} but only {} parameter(s) were supplied",
141                    params.len()
142                ),
143            })?;
144            *tok = match p {
145                ParamValue::Null => Token::Null,
146                ParamValue::Int(v) => Token::IntLit(*v),
147                ParamValue::Float(v) => Token::FloatLit(*v),
148                ParamValue::Bool(v) => Token::BoolLit(*v),
149                ParamValue::Str(s) => Token::StringLit(s.clone()),
150            };
151        }
152    }
153    parse_tokens(tokens)
154}
155
156fn edit_distance(a: &str, b: &str) -> usize {
157    let mut prev: Vec<usize> = (0..=b.len()).collect();
158    let mut curr = vec![0; b.len() + 1];
159    for (i, ca) in a.bytes().enumerate() {
160        curr[0] = i + 1;
161        for (j, cb) in b.bytes().enumerate() {
162            let cost = usize::from(ca != cb);
163            curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
164        }
165        std::mem::swap(&mut prev, &mut curr);
166    }
167    prev[b.len()]
168}
169
170fn keyword_suggestion(word: &str) -> Option<&'static str> {
171    const STATEMENT_KEYWORDS: &[&str] = &[
172        "alter", "begin", "commit", "delete", "drop", "explain", "insert", "refresh", "rollback",
173        "select", "type", "update", "upsert",
174    ];
175    let lower = word.to_ascii_lowercase();
176    STATEMENT_KEYWORDS
177        .iter()
178        .copied()
179        .filter(|kw| edit_distance(&lower, kw) <= 2)
180        .min_by_key(|kw| edit_distance(&lower, kw))
181}
182
183/// Shared tail of [`parse`] / [`parse_with_params`]: run the recursive
184/// descent over an already-lexed (and possibly param-substituted) token
185/// stream and reject any trailing tokens.
186fn parse_tokens(tokens: Vec<Token>) -> Result<Statement, ParseError> {
187    let mut parser = Parser {
188        tokens,
189        pos: 0,
190        depth: 0,
191    };
192    let stmt = parser.parse_statement()?;
193    // Reject trailing tokens. Without this, unrecognized tails like
194    // `User create_index .email` silently succeed as `User` — which
195    // misled the TS client into thinking those non-existent DDL forms
196    // returned rows. A parse error here tells users that the syntax
197    // they wrote isn't recognized.
198    if !matches!(parser.peek(), Token::Eof) {
199        let mut message = format!(
200            "unexpected trailing token near token {}: {}",
201            parser.pos,
202            parser.peek().display_name()
203        );
204        if let Some(Token::Ident(first)) = parser.tokens.first() {
205            if let Some(suggestion) = keyword_suggestion(first) {
206                message.push_str(&format!("; did you mean `{suggestion}`?"));
207            }
208        }
209        return Err(ParseError::Syntax { message });
210    }
211    Ok(stmt)
212}
213
214/// Rewrite `Field(alias)` references inside `expr` to the underlying
215/// projection expression they alias. Used to desugar post-projection HAVING
216/// (`{ ..., cnt: count(.name) } having cnt >= 2`) into a form the planner's
217/// aggregate extraction can handle.
218fn substitute_projection_aliases(expr: Expr, fields: &[ProjectionField]) -> Expr {
219    match expr {
220        Expr::Field(ref name) => {
221            for f in fields {
222                if f.alias.as_deref() == Some(name.as_str()) {
223                    return f.expr.clone();
224                }
225            }
226            expr
227        }
228        Expr::BinaryOp(l, op, r) => Expr::BinaryOp(
229            Box::new(substitute_projection_aliases(*l, fields)),
230            op,
231            Box::new(substitute_projection_aliases(*r, fields)),
232        ),
233        Expr::UnaryOp(op, inner) => {
234            Expr::UnaryOp(op, Box::new(substitute_projection_aliases(*inner, fields)))
235        }
236        Expr::Coalesce(l, r) => Expr::Coalesce(
237            Box::new(substitute_projection_aliases(*l, fields)),
238            Box::new(substitute_projection_aliases(*r, fields)),
239        ),
240        Expr::InList {
241            expr: e,
242            list,
243            negated,
244        } => Expr::InList {
245            expr: Box::new(substitute_projection_aliases(*e, fields)),
246            list: list
247                .into_iter()
248                .map(|i| substitute_projection_aliases(i, fields))
249                .collect(),
250            negated,
251        },
252        Expr::ScalarFunc(f, args) => Expr::ScalarFunc(
253            f,
254            args.into_iter()
255                .map(|a| substitute_projection_aliases(a, fields))
256                .collect(),
257        ),
258        other => other,
259    }
260}
261
262impl Parser {
263    fn peek(&self) -> &Token {
264        &self.tokens[self.pos]
265    }
266
267    fn advance(&mut self) -> Token {
268        let t = self.tokens[self.pos].clone();
269        self.pos += 1;
270        t
271    }
272
273    fn expect(&mut self, expected: &Token) -> Result<(), ParseError> {
274        let t = self.advance();
275        if &t == expected {
276            Ok(())
277        } else {
278            Err(ParseError::UnexpectedToken {
279                expected: expected.display_name(),
280                got: t.display_name(),
281            })
282        }
283    }
284
285    /// Convenience: create an UnexpectedToken error.
286    fn unexpected(&self, expected: &str, got: &Token) -> ParseError {
287        ParseError::UnexpectedToken {
288            expected: expected.into(),
289            got: got.display_name(),
290        }
291    }
292
293    /// Consume an identifier in a position that names something (a field,
294    /// column, …). When the caller wrote a reserved word instead, the error
295    /// says so and points at the backtick-quoting escape hatch, rather than
296    /// the opaque `expected field name, got 'type'`.
297    fn expect_named_ident(&mut self, context: &str) -> Result<String, ParseError> {
298        match self.advance() {
299            Token::Ident(n) => Ok(n),
300            t => Err(self.named_ident_error(context, &t)),
301        }
302    }
303
304    /// Build the error for a reserved word (or other token) appearing where an
305    /// identifier was required. `context` is the noun ("field name", "column
306    /// name") spliced into the message.
307    fn named_ident_error(&self, context: &str, got: &Token) -> ParseError {
308        if let Some(kw) = got.keyword_str() {
309            ParseError::Syntax {
310                // "syntax error" prefix keeps the message on the server's
311                // safe-to-forward allowlist (SAFE_ERROR_PREFIXES) so wire
312                // clients see the guidance instead of the generic mask.
313                message: format!(
314                    "syntax error: '{kw}' is a reserved word and cannot be used as a {context}; \
315                     rename it or quote it as `{kw}`"
316                ),
317            }
318        } else {
319            ParseError::UnexpectedToken {
320                expected: context.into(),
321                got: got.display_name(),
322            }
323        }
324    }
325
326    /// Consume an optional `if not exists` clause. `if` is not a keyword token
327    /// (it lexes as an identifier), so match it by spelling.
328    fn parse_optional_if_not_exists(&mut self) -> bool {
329        if matches!(self.peek(), Token::Ident(w) if w == "if")
330            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
331            && matches!(self.tokens.get(self.pos + 2), Some(Token::Exists))
332        {
333            self.pos += 3;
334            true
335        } else {
336            false
337        }
338    }
339
340    /// Consume an optional `if exists` clause.
341    fn parse_optional_if_exists(&mut self) -> bool {
342        if matches!(self.peek(), Token::Ident(w) if w == "if")
343            && matches!(self.tokens.get(self.pos + 1), Some(Token::Exists))
344        {
345            self.pos += 2;
346            true
347        } else {
348            false
349        }
350    }
351
352    fn parse_statement(&mut self) -> Result<Statement, ParseError> {
353        self.depth += 1;
354        if self.depth > MAX_NESTING_DEPTH {
355            self.depth -= 1;
356            return Err(ParseError::NestingDepthExceeded {
357                max: MAX_NESTING_DEPTH,
358            });
359        }
360        if matches!(self.peek(), Token::Explain) {
361            self.advance();
362            let inner = self.parse_statement()?;
363            self.depth -= 1;
364            return Ok(Statement::Explain(Box::new(inner)));
365        }
366        let stmt = match self.peek() {
367            Token::Insert => self.parse_insert(),
368            Token::Upsert => self.parse_upsert(),
369            Token::Type => self.parse_create_type(),
370            Token::Alter => self.parse_alter_table(),
371            Token::Drop => self.parse_drop_or_drop_view(),
372            Token::Materialized => self.parse_create_view(),
373            Token::Refresh => self.parse_refresh_view(),
374            Token::Begin => {
375                self.advance();
376                // Optional `transaction` keyword after `begin`.
377                if *self.peek() == Token::Transaction {
378                    self.advance();
379                }
380                return Ok(Statement::Begin);
381            }
382            Token::Commit => {
383                self.advance();
384                return Ok(Statement::Commit);
385            }
386            Token::Rollback => {
387                self.advance();
388                return Ok(Statement::Rollback);
389            }
390            Token::Schema => self.parse_schema(),
391            Token::Describe => self.parse_describe(),
392            Token::Count | Token::Avg | Token::Sum | Token::Min | Token::Max => {
393                self.parse_aggregate_query()
394            }
395            Token::Ident(_) => self.parse_query_or_mutation(),
396            Token::Update => Err(ParseError::Syntax {
397                message: "'update' cannot start a statement — in PowQL, use pipeline syntax: \
398                    TableName filter ... update { ... }"
399                    .into(),
400            }),
401            Token::Delete => Err(ParseError::Syntax {
402                message: "'delete' cannot start a statement — in PowQL, use pipeline syntax: \
403                    TableName filter ... delete"
404                    .into(),
405            }),
406            _ => Err(self.unexpected("statement", self.peek())),
407        }?;
408        // Check for UNION chaining after any query-producing statement.
409        let result = self.maybe_parse_union(stmt);
410        self.depth -= 1;
411        result
412    }
413
414    fn parse_query_or_mutation(&mut self) -> Result<Statement, ParseError> {
415        let source = match self.advance() {
416            Token::Ident(name) => name,
417            t => {
418                return Err(ParseError::UnexpectedToken {
419                    expected: "type name".into(),
420                    got: t.display_name(),
421                })
422            }
423        };
424        let alias = self.try_parse_alias();
425        let joins = self.parse_joins()?;
426
427        // Walk filter/order/limit/offset/projection, peeling off update/delete
428        // mutations as we hit them. Anything else terminates the read pipeline
429        // and we return a Query.
430        let mut filter = None;
431        let mut order = None;
432        let mut limit = None;
433        let mut offset = None;
434        let mut projection = None;
435        let mut distinct = false;
436        let mut group_by = None;
437
438        loop {
439            match self.peek() {
440                Token::Distinct => {
441                    self.advance();
442                    distinct = true;
443                }
444                Token::Group => {
445                    self.advance();
446                    group_by = Some(self.parse_group_by()?);
447                }
448                Token::Filter => {
449                    self.advance();
450                    filter = Some(self.parse_expr()?);
451                }
452                Token::Order => {
453                    self.advance();
454                    order = Some(self.parse_order()?);
455                }
456                Token::Limit => {
457                    self.advance();
458                    limit = Some(self.parse_expr()?);
459                }
460                Token::Offset => {
461                    self.advance();
462                    offset = Some(self.parse_expr()?);
463                }
464                Token::LBrace => {
465                    projection = Some(self.parse_projection()?);
466                }
467                Token::Having => {
468                    // Post-projection HAVING — see parse_query_tail for details.
469                    self.advance();
470                    let having_expr = self.parse_expr()?;
471                    let group = group_by.as_mut().ok_or_else(|| ParseError::Syntax {
472                        message: "having without group by".into(),
473                    })?;
474                    let rewritten = match projection.as_ref() {
475                        Some(fields) => substitute_projection_aliases(having_expr, fields),
476                        None => having_expr,
477                    };
478                    group.having = Some(match group.having.take() {
479                        Some(existing) => {
480                            Expr::BinaryOp(Box::new(existing), BinOp::And, Box::new(rewritten))
481                        }
482                        None => rewritten,
483                    });
484                }
485                Token::Update => {
486                    if !joins.is_empty() {
487                        return Err(ParseError::Unsupported {
488                            feature: "update on a joined query is not supported".into(),
489                        });
490                    }
491                    self.advance();
492                    let assignments = self.parse_assignments()?;
493                    // Optional trailing `returning` — return the post-update rows.
494                    let returning = *self.peek() == Token::Returning;
495                    if returning {
496                        self.advance();
497                    }
498                    return Ok(Statement::UpdateQuery(UpdateExpr {
499                        source,
500                        filter,
501                        assignments,
502                        returning,
503                    }));
504                }
505                Token::Delete => {
506                    if !joins.is_empty() {
507                        return Err(ParseError::Unsupported {
508                            feature: "delete on a joined query is not supported".into(),
509                        });
510                    }
511                    self.advance();
512                    // Optional trailing `returning` — return the pre-delete rows.
513                    let returning = *self.peek() == Token::Returning;
514                    if returning {
515                        self.advance();
516                    }
517                    return Ok(Statement::DeleteQuery(DeleteExpr {
518                        source,
519                        filter,
520                        returning,
521                    }));
522                }
523                _ => break,
524            }
525        }
526
527        Ok(Statement::Query(QueryExpr {
528            source,
529            alias,
530            joins,
531            filter,
532            order,
533            limit,
534            offset,
535            projection,
536            aggregation: None,
537            distinct,
538            group_by,
539        }))
540    }
541
542    /// Parse the read-only tail of a query (filter/order/limit/offset/projection)
543    /// after `source` has already been consumed. Stops at the first token that
544    /// isn't part of a read pipeline — the caller decides whether that's a
545    /// terminator (RParen for an aggregate, EOF for a top-level query, etc.).
546    /// Always returns `aggregation: None`; the caller layers that on.
547    fn parse_query_tail(&mut self, source: String) -> Result<QueryExpr, ParseError> {
548        let alias = self.try_parse_alias();
549        let joins = self.parse_joins()?;
550        let mut filter = None;
551        let mut order = None;
552        let mut limit = None;
553        let mut offset = None;
554        let mut projection = None;
555        let mut distinct = false;
556        let mut group_by = None;
557
558        loop {
559            match self.peek() {
560                Token::Distinct => {
561                    self.advance();
562                    distinct = true;
563                }
564                Token::Group => {
565                    self.advance();
566                    group_by = Some(self.parse_group_by()?);
567                }
568                Token::Filter => {
569                    self.advance();
570                    filter = Some(self.parse_expr()?);
571                }
572                Token::Order => {
573                    self.advance();
574                    order = Some(self.parse_order()?);
575                }
576                Token::Limit => {
577                    self.advance();
578                    limit = Some(self.parse_expr()?);
579                }
580                Token::Offset => {
581                    self.advance();
582                    offset = Some(self.parse_expr()?);
583                }
584                Token::LBrace => {
585                    projection = Some(self.parse_projection()?);
586                }
587                Token::Having => {
588                    // Post-projection HAVING — `... group .k { .k, cnt: count(.name) } having cnt >= 2`.
589                    // Only meaningful when a GROUP BY is present. We desugar
590                    // to a regular HAVING on the GroupByClause, rewriting
591                    // projection aliases back into their underlying expressions
592                    // so the planner's extract_aggregates can dedup them.
593                    self.advance();
594                    let having_expr = self.parse_expr()?;
595                    let group = group_by.as_mut().ok_or_else(|| ParseError::Syntax {
596                        message: "having without group by".into(),
597                    })?;
598                    let rewritten = match projection.as_ref() {
599                        Some(fields) => substitute_projection_aliases(having_expr, fields),
600                        None => having_expr,
601                    };
602                    group.having = Some(match group.having.take() {
603                        Some(existing) => {
604                            Expr::BinaryOp(Box::new(existing), BinOp::And, Box::new(rewritten))
605                        }
606                        None => rewritten,
607                    });
608                }
609                _ => break,
610            }
611        }
612
613        Ok(QueryExpr {
614            source,
615            alias,
616            joins,
617            filter,
618            order,
619            limit,
620            offset,
621            projection,
622            aggregation: None,
623            distinct,
624            group_by,
625        })
626    }
627
628    /// Consume an optional `as <ident>` suffix on a source. Returns `None`
629    /// if the next token isn't `as`. Used by both the primary source and each
630    /// join source so queries can disambiguate columns via `alias.field`.
631    fn try_parse_alias(&mut self) -> Option<String> {
632        if *self.peek() == Token::As {
633            self.advance();
634            if let Token::Ident(name) = self.peek().clone() {
635                self.advance();
636                return Some(name);
637            }
638        }
639        None
640    }
641
642    /// Parse zero or more join clauses. Each clause is:
643    ///   (`inner` | `left` [`outer`] | `right` [`outer`] | `cross`)? `join`
644    ///   <Ident> [`as` <ident>] [`on` <expr>]
645    ///
646    /// `on` is required for every kind except `cross`. The default kind is
647    /// `inner` when the caller wrote bare `join` without a preceding modifier.
648    fn parse_joins(&mut self) -> Result<Vec<JoinClause>, ParseError> {
649        let mut joins = Vec::new();
650        loop {
651            let kind = match self.peek() {
652                Token::Join => {
653                    self.advance();
654                    JoinKind::Inner
655                }
656                Token::Inner => {
657                    self.advance();
658                    self.expect(&Token::Join)?;
659                    JoinKind::Inner
660                }
661                Token::LeftKw => {
662                    self.advance();
663                    if *self.peek() == Token::Outer {
664                        self.advance();
665                    }
666                    self.expect(&Token::Join)?;
667                    JoinKind::LeftOuter
668                }
669                Token::RightKw => {
670                    self.advance();
671                    if *self.peek() == Token::Outer {
672                        self.advance();
673                    }
674                    self.expect(&Token::Join)?;
675                    JoinKind::RightOuter
676                }
677                Token::Cross => {
678                    self.advance();
679                    self.expect(&Token::Join)?;
680                    JoinKind::Cross
681                }
682                _ => break,
683            };
684
685            let source = match self.advance() {
686                Token::Ident(name) => name,
687                t => {
688                    return Err(ParseError::UnexpectedToken {
689                        expected: "type name after join".into(),
690                        got: t.display_name(),
691                    });
692                }
693            };
694            let alias = self.try_parse_alias();
695            let on = if kind == JoinKind::Cross {
696                None
697            } else if *self.peek() == Token::On {
698                self.advance();
699                Some(self.parse_expr()?)
700            } else {
701                return Err(ParseError::Syntax {
702                    message: format!("expected `on <expr>` after join {source}"),
703                });
704            };
705
706            joins.push(JoinClause {
707                kind,
708                source,
709                alias,
710                on,
711            });
712        }
713        Ok(joins)
714    }
715
716    fn parse_insert(&mut self) -> Result<Statement, ParseError> {
717        self.expect(&Token::Insert)?;
718        let target = match self.advance() {
719            Token::Ident(name) => name,
720            t => {
721                return Err(ParseError::UnexpectedToken {
722                    expected: "type name".into(),
723                    got: t.display_name(),
724                })
725            }
726        };
727        // One or more comma-separated assignment blocks:
728        //   insert T { a := 1 }
729        //   insert T { a := 1 }, { a := 2 }, { a := 3 }
730        let mut rows = vec![self.parse_assignments()?];
731        while *self.peek() == Token::Comma {
732            self.advance(); // consume the comma between row blocks
733            rows.push(self.parse_assignments()?);
734        }
735        // Optional trailing `returning` — return the inserted rows.
736        let returning = *self.peek() == Token::Returning;
737        if returning {
738            self.advance();
739        }
740        Ok(Statement::Insert(InsertExpr {
741            target,
742            rows,
743            returning,
744        }))
745    }
746
747    /// Parse: `upsert Table on .key_col { assignments } [on conflict { update_assignments }]`
748    fn parse_upsert(&mut self) -> Result<Statement, ParseError> {
749        self.expect(&Token::Upsert)?;
750        let target = match self.advance() {
751            Token::Ident(name) => name,
752            t => {
753                return Err(ParseError::UnexpectedToken {
754                    expected: "type name".into(),
755                    got: t.display_name(),
756                })
757            }
758        };
759        self.expect(&Token::On)?;
760        let key_column = match self.advance() {
761            Token::DotIdent(name) => name,
762            t => {
763                return Err(ParseError::UnexpectedToken {
764                    expected: ".key_column".into(),
765                    got: t.display_name(),
766                })
767            }
768        };
769        let assignments = self.parse_assignments()?;
770        let on_conflict = if *self.peek() == Token::On {
771            self.advance(); // consume `on`
772            self.expect(&Token::Conflict)?;
773            self.parse_assignments()?
774        } else {
775            Vec::new()
776        };
777        Ok(Statement::Upsert(UpsertExpr {
778            target,
779            key_column,
780            assignments,
781            on_conflict,
782        }))
783    }
784
785    fn parse_assignments(&mut self) -> Result<Vec<Assignment>, ParseError> {
786        self.expect(&Token::LBrace)?;
787        let mut assignments = Vec::new();
788        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
789            let field = self.expect_named_ident("field name")?;
790            self.expect(&Token::Assign)?;
791            let value = self.parse_expr()?;
792            assignments.push(Assignment { field, value });
793            if *self.peek() == Token::Comma {
794                self.advance();
795            }
796        }
797        self.expect(&Token::RBrace)?;
798        Ok(assignments)
799    }
800
801    fn parse_projection(&mut self) -> Result<Vec<ProjectionField>, ParseError> {
802        self.expect(&Token::LBrace)?;
803        let mut fields = Vec::new();
804        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
805            // `alias: expr` is detected with two-token lookahead so the bare
806            // form can parse a full expression from its first token. Every
807            // projection slot — aliased or bare — flows through the shared
808            // expression parser, so fields, qualified refs, aggregates, window
809            // functions, scalar calls, CASE/CAST, and arithmetic like `.a - 1`
810            // are all accepted. Previously the bare slot used a restricted
811            // dispatch that rejected any binary operator with "expected field".
812            if matches!(self.peek(), Token::Ident(_))
813                && matches!(self.tokens.get(self.pos + 1), Some(Token::Colon))
814            {
815                let alias = match self.advance() {
816                    Token::Ident(name) => name,
817                    _ => unreachable!("guarded by the matches! above"),
818                };
819                self.advance(); // consume ':'
820                let expr = self.parse_expr()?;
821                fields.push(ProjectionField {
822                    alias: Some(alias),
823                    expr,
824                });
825            } else {
826                let expr = self.parse_expr()?;
827                fields.push(ProjectionField { alias: None, expr });
828            }
829            if *self.peek() == Token::Comma {
830                self.advance();
831            }
832        }
833        self.expect(&Token::RBrace)?;
834        Ok(fields)
835    }
836
837    /// Parse the OVER clause for a window function:
838    /// `over (partition .col1, .col2 order .col3 asc, .col4 desc)`
839    fn parse_over_clause(&mut self) -> Result<(Vec<String>, Vec<OrderKey>), ParseError> {
840        self.expect(&Token::Over)?;
841        self.expect(&Token::LParen)?;
842        let mut partition_by = Vec::new();
843        let mut order_by = Vec::new();
844        if *self.peek() == Token::Partition {
845            self.advance();
846            while let Token::DotIdent(name) = self.peek() {
847                let name = name.clone();
848                self.advance();
849                partition_by.push(name);
850                if *self.peek() == Token::Comma {
851                    // Only consume comma if the next token is another DotIdent
852                    // (i.e. still in the partition list). If the next meaningful
853                    // token is `order`, `RParen`, etc., stop.
854                    if matches!(self.tokens.get(self.pos + 1), Some(Token::DotIdent(_))) {
855                        self.advance();
856                    } else {
857                        break;
858                    }
859                } else {
860                    break;
861                }
862            }
863        }
864        if *self.peek() == Token::Order {
865            self.advance();
866            while let Token::DotIdent(name) = self.peek() {
867                let field = name.clone();
868                self.advance();
869                let descending = match self.peek() {
870                    Token::Desc => {
871                        self.advance();
872                        true
873                    }
874                    Token::Asc => {
875                        self.advance();
876                        false
877                    }
878                    _ => false,
879                };
880                order_by.push(OrderKey { field, descending });
881                if *self.peek() == Token::Comma {
882                    self.advance();
883                } else {
884                    break;
885                }
886            }
887        }
888        self.expect(&Token::RParen)?;
889        Ok((partition_by, order_by))
890    }
891
892    /// Parse a cast target type from a string literal: `"int"`, `"float"`, `"str"`, `"bool"`, `"datetime"`.
893    fn parse_cast_type(&mut self) -> Result<CastType, ParseError> {
894        match self.advance() {
895            Token::StringLit(s) => match s.as_str() {
896                "int" | "Int" | "INT" => Ok(CastType::Int),
897                "float" | "Float" | "FLOAT" => Ok(CastType::Float),
898                "str" | "Str" | "STR" | "string" | "String" => Ok(CastType::Str),
899                "bool" | "Bool" | "BOOL" | "boolean" => Ok(CastType::Bool),
900                "datetime" | "DateTime" | "DATETIME" => Ok(CastType::DateTime),
901                "uuid" | "Uuid" | "UUID" => Ok(CastType::Uuid),
902                "bytes" | "Bytes" | "BYTES" | "bytea" => Ok(CastType::Bytes),
903                other => Err(ParseError::Syntax {
904                    message: format!("invalid cast type: \"{other}\""),
905                }),
906            },
907            t => Err(ParseError::UnexpectedToken {
908                expected: "string literal for cast type".into(),
909                got: t.display_name(),
910            }),
911        }
912    }
913
914    fn parse_order(&mut self) -> Result<OrderClause, ParseError> {
915        let mut keys = Vec::new();
916        loop {
917            let field = match self.advance() {
918                Token::DotIdent(name) => name,
919                t => {
920                    return Err(ParseError::UnexpectedToken {
921                        expected: ".field after order".into(),
922                        got: t.display_name(),
923                    })
924                }
925            };
926            let descending = match self.peek() {
927                Token::Desc => {
928                    self.advance();
929                    true
930                }
931                Token::Asc => {
932                    self.advance();
933                    false
934                }
935                _ => false,
936            };
937            keys.push(OrderKey { field, descending });
938            if *self.peek() == Token::Comma {
939                self.advance();
940            } else {
941                break;
942            }
943        }
944        Ok(OrderClause { keys })
945    }
946
947    fn parse_aggregate_query(&mut self) -> Result<Statement, ParseError> {
948        let mut func = match self.advance() {
949            Token::Count => AggFunc::Count,
950            Token::Avg => AggFunc::Avg,
951            Token::Sum => AggFunc::Sum,
952            Token::Min => AggFunc::Min,
953            Token::Max => AggFunc::Max,
954            t => {
955                return Err(ParseError::UnexpectedToken {
956                    expected: "aggregate function".into(),
957                    got: t.display_name(),
958                })
959            }
960        };
961        self.expect(&Token::LParen)?;
962        // count(distinct User ...) → CountDistinct
963        if func == AggFunc::Count && *self.peek() == Token::Distinct {
964            self.advance();
965            func = AggFunc::CountDistinct;
966        }
967        let source = match self.advance() {
968            Token::Ident(name) => name,
969            t => {
970                return Err(ParseError::UnexpectedToken {
971                    expected: "type name".into(),
972                    got: t.display_name(),
973                })
974            }
975        };
976        // Allow a full read-pipeline tail inside the parens, e.g.
977        // `count(User filter .age > 27 limit 100)`. parse_query_tail stops at
978        // the first non-pipeline token, which here must be RParen.
979        let mut query = self.parse_query_tail(source)?;
980        self.expect(&Token::RParen)?;
981
982        // For non-count aggregates (and count distinct), the caller typically
983        // writes the target column via the trailing projection form:
984        //     sum(User filter .age > 30 { .age })
985        //     count(distinct User { .name })
986        // We lift that single unaliased `.field` into AggregateExpr.field so
987        // the executor's aggregate fast paths can see it.
988        let mut agg_field: Option<String> = None;
989        if func != AggFunc::Count {
990            if let Some(proj) = &query.projection {
991                if proj.len() == 1 && proj[0].alias.is_none() {
992                    if let Expr::Field(name) = &proj[0].expr {
993                        agg_field = Some(name.clone());
994                    }
995                }
996            }
997            if agg_field.is_some() {
998                query.projection = None;
999            }
1000        }
1001        query.aggregation = Some(AggregateExpr {
1002            function: func,
1003            field: agg_field,
1004        });
1005        Ok(Statement::Query(query))
1006    }
1007
1008    fn parse_expr(&mut self) -> Result<Expr, ParseError> {
1009        self.depth += 1;
1010        if self.depth > MAX_NESTING_DEPTH {
1011            self.depth -= 1;
1012            return Err(ParseError::NestingDepthExceeded {
1013                max: MAX_NESTING_DEPTH,
1014            });
1015        }
1016        let result = self.parse_or_expr();
1017        self.depth -= 1;
1018        result
1019    }
1020
1021    fn parse_or_expr(&mut self) -> Result<Expr, ParseError> {
1022        let mut left = self.parse_and_expr()?;
1023        while *self.peek() == Token::Or {
1024            self.advance();
1025            let right = self.parse_and_expr()?;
1026            left = Expr::BinaryOp(Box::new(left), BinOp::Or, Box::new(right));
1027        }
1028        Ok(left)
1029    }
1030
1031    fn parse_and_expr(&mut self) -> Result<Expr, ParseError> {
1032        let mut left = self.parse_comparison()?;
1033        while *self.peek() == Token::And {
1034            self.advance();
1035            let right = self.parse_comparison()?;
1036            left = Expr::BinaryOp(Box::new(left), BinOp::And, Box::new(right));
1037        }
1038        Ok(left)
1039    }
1040
1041    fn parse_comparison(&mut self) -> Result<Expr, ParseError> {
1042        let left = self.parse_additive()?;
1043
1044        // IS NULL / IS NOT NULL (postfix)
1045        if *self.peek() == Token::Is {
1046            self.advance();
1047            if *self.peek() == Token::Not {
1048                self.advance();
1049                self.expect(&Token::Null)?;
1050                return Ok(Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(left)));
1051            } else {
1052                self.expect(&Token::Null)?;
1053                return Ok(Expr::UnaryOp(UnaryOp::IsNull, Box::new(left)));
1054            }
1055        }
1056
1057        // Postfix: `in (...)`, `like "..."`, `between X and Y`
1058        // and their negated forms: `not in`, `not like`, `not between`.
1059        match self.peek() {
1060            Token::In => {
1061                self.advance();
1062                return self.parse_in_list(left, false);
1063            }
1064            Token::Like => {
1065                self.advance();
1066                let pattern = self.parse_additive()?;
1067                return Ok(Expr::BinaryOp(
1068                    Box::new(left),
1069                    BinOp::Like,
1070                    Box::new(pattern),
1071                ));
1072            }
1073            Token::Between => {
1074                self.advance();
1075                return self.parse_between(left, false);
1076            }
1077            Token::Not => {
1078                // Peek ahead: `not in`, `not like`, `not between`.
1079                // If the token after `not` isn't one of these, don't consume
1080                // `not` — let the caller handle it.
1081                let next = self.tokens.get(self.pos + 1);
1082                match next {
1083                    Some(Token::In) => {
1084                        self.advance(); // not
1085                        self.advance(); // in
1086                        return self.parse_in_list(left, true);
1087                    }
1088                    Some(Token::Like) => {
1089                        self.advance(); // not
1090                        self.advance(); // like
1091                        let pattern = self.parse_additive()?;
1092                        let like = Expr::BinaryOp(Box::new(left), BinOp::Like, Box::new(pattern));
1093                        return Ok(Expr::UnaryOp(UnaryOp::Not, Box::new(like)));
1094                    }
1095                    Some(Token::Between) => {
1096                        self.advance(); // not
1097                        self.advance(); // between
1098                        return self.parse_between(left, true);
1099                    }
1100                    _ => {}
1101                }
1102            }
1103            _ => {}
1104        }
1105
1106        let op = match self.peek() {
1107            Token::Eq => BinOp::Eq,
1108            Token::Neq => BinOp::Neq,
1109            Token::Lt => BinOp::Lt,
1110            Token::Gt => BinOp::Gt,
1111            Token::Lte => BinOp::Lte,
1112            Token::Gte => BinOp::Gte,
1113            _ => return Ok(left),
1114        };
1115        self.advance();
1116        // `expr = null` / `expr != null` desugar to the same UnaryOp as
1117        // `expr is null` / `expr is not null`. Ordering comparisons against
1118        // null (`< null`, `>= null`, etc.) remain parse errors.
1119        if *self.peek() == Token::Null {
1120            match op {
1121                BinOp::Eq => {
1122                    self.advance();
1123                    return Ok(Expr::UnaryOp(UnaryOp::IsNull, Box::new(left)));
1124                }
1125                BinOp::Neq => {
1126                    self.advance();
1127                    return Ok(Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(left)));
1128                }
1129                _ => {}
1130            }
1131        }
1132        let right = self.parse_additive()?;
1133        Ok(Expr::BinaryOp(Box::new(left), op, Box::new(right)))
1134    }
1135
1136    /// Parse `(val1, val2, ...)` or `(subquery)` after `in` / `not in`.
1137    /// A subquery is detected by `(` followed by an `Ident` that is NOT
1138    /// followed by `,` or `)` — in PowQL, bare identifiers in value lists
1139    /// don't appear (field refs start with `.`).
1140    fn parse_in_list(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
1141        self.expect(&Token::LParen)?;
1142        // Detect subquery: `( Ident ...` where the Ident is a table name.
1143        if let Token::Ident(_) = self.peek() {
1144            // Peek further: if the next token after the Ident is NOT `,` or
1145            // `)`, it's a subquery source name.
1146            let after = self.tokens.get(self.pos + 1);
1147            let is_subquery = !matches!(after, Some(Token::Comma) | Some(Token::RParen));
1148            if is_subquery {
1149                let source = match self.advance() {
1150                    Token::Ident(name) => name,
1151                    _ => unreachable!(),
1152                };
1153                let subquery = self.parse_query_tail(source)?;
1154                self.expect(&Token::RParen)?;
1155                return Ok(Expr::InSubquery {
1156                    expr: Box::new(expr),
1157                    subquery: Box::new(subquery),
1158                    negated,
1159                });
1160            }
1161        }
1162        let mut list = Vec::new();
1163        while !matches!(self.peek(), Token::RParen | Token::Eof) {
1164            list.push(self.parse_expr()?);
1165            if *self.peek() == Token::Comma {
1166                self.advance();
1167            }
1168        }
1169        self.expect(&Token::RParen)?;
1170        Ok(Expr::InList {
1171            expr: Box::new(expr),
1172            list,
1173            negated,
1174        })
1175    }
1176
1177    /// Try to parse a `(subquery)` tail for `exists` / `not exists`.
1178    /// A subquery is detected when the next tokens are `( Ident ...` —
1179    /// bare identifiers inside parens are always table/view names in
1180    /// PowQL (column refs start with `.`). Returns `Ok(Some(query))` if
1181    /// consumed, `Ok(None)` if the shape doesn't match (so the caller
1182    /// falls back to parsing a scalar primary for the legacy
1183    /// `exists <expr>` form).
1184    fn try_parse_exists_subquery(&mut self) -> Result<Option<QueryExpr>, ParseError> {
1185        if *self.peek() != Token::LParen {
1186            return Ok(None);
1187        }
1188        // Peek one token inside the paren. Anything starting with `Ident`
1189        // is a source name — PowQL column references use `DotIdent`, so
1190        // an `exists (X ...)` with a bare `X` is unambiguously a subquery.
1191        let after_lparen = self.tokens.get(self.pos + 1);
1192        if !matches!(after_lparen, Some(Token::Ident(_))) {
1193            return Ok(None);
1194        }
1195        self.expect(&Token::LParen)?;
1196        let source = match self.advance() {
1197            Token::Ident(name) => name,
1198            _ => unreachable!(),
1199        };
1200        let subquery = self.parse_query_tail(source)?;
1201        self.expect(&Token::RParen)?;
1202        Ok(Some(subquery))
1203    }
1204
1205    /// Parse `low and high` after `between` / `not between`.
1206    /// Desugars into `expr >= low AND expr <= high` (or negated:
1207    /// `expr < low OR expr > high`).
1208    fn parse_between(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
1209        let low = self.parse_additive()?;
1210        self.expect(&Token::And)?;
1211        let high = self.parse_additive()?;
1212        if negated {
1213            // NOT BETWEEN: expr < low OR expr > high
1214            Ok(Expr::BinaryOp(
1215                Box::new(Expr::BinaryOp(
1216                    Box::new(expr.clone()),
1217                    BinOp::Lt,
1218                    Box::new(low),
1219                )),
1220                BinOp::Or,
1221                Box::new(Expr::BinaryOp(Box::new(expr), BinOp::Gt, Box::new(high))),
1222            ))
1223        } else {
1224            // BETWEEN: expr >= low AND expr <= high
1225            Ok(Expr::BinaryOp(
1226                Box::new(Expr::BinaryOp(
1227                    Box::new(expr.clone()),
1228                    BinOp::Gte,
1229                    Box::new(low),
1230                )),
1231                BinOp::And,
1232                Box::new(Expr::BinaryOp(Box::new(expr), BinOp::Lte, Box::new(high))),
1233            ))
1234        }
1235    }
1236
1237    /// Parse `group .field1, alias.field2 [having <expr>]`.
1238    ///
1239    /// Two key forms are accepted:
1240    ///   - `.field` is a bare `DotIdent`, an `Unqualified` key.
1241    ///   - `alias.field` is an `Ident` followed by a `DotIdent`, a `Qualified`
1242    ///     key (the lexer splits `u.status` into `Ident("u")` + `DotIdent("status")`).
1243    fn parse_group_by(&mut self) -> Result<GroupByClause, ParseError> {
1244        let mut keys = Vec::new();
1245        loop {
1246            match self.peek().clone() {
1247                Token::DotIdent(field) => {
1248                    self.advance();
1249                    keys.push(GroupKey::Unqualified(field));
1250                }
1251                Token::Ident(alias) => {
1252                    self.advance();
1253                    match self.peek().clone() {
1254                        Token::DotIdent(field) => {
1255                            self.advance();
1256                            keys.push(GroupKey::Qualified { alias, field });
1257                        }
1258                        _ => {
1259                            return Err(ParseError::Syntax {
1260                                message: "expected .field after qualifier in group key".into(),
1261                            });
1262                        }
1263                    }
1264                }
1265                _ => break,
1266            }
1267            if *self.peek() == Token::Comma {
1268                self.advance();
1269            } else {
1270                break;
1271            }
1272        }
1273        if keys.is_empty() {
1274            return Err(ParseError::Syntax {
1275                message: "expected at least one group key after group".into(),
1276            });
1277        }
1278        let having = if *self.peek() == Token::Having {
1279            self.advance();
1280            Some(self.parse_expr()?)
1281        } else {
1282            None
1283        };
1284        Ok(GroupByClause { keys, having })
1285    }
1286
1287    fn parse_additive(&mut self) -> Result<Expr, ParseError> {
1288        let mut left = self.parse_multiplicative()?;
1289        loop {
1290            let op = match self.peek() {
1291                Token::Plus => BinOp::Add,
1292                Token::Minus => BinOp::Sub,
1293                Token::Coalesce => {
1294                    self.advance();
1295                    let right = self.parse_multiplicative()?;
1296                    left = Expr::Coalesce(Box::new(left), Box::new(right));
1297                    continue;
1298                }
1299                _ => break,
1300            };
1301            self.advance();
1302            let right = self.parse_multiplicative()?;
1303            left = Expr::BinaryOp(Box::new(left), op, Box::new(right));
1304        }
1305        Ok(left)
1306    }
1307
1308    fn parse_multiplicative(&mut self) -> Result<Expr, ParseError> {
1309        let mut left = self.parse_primary()?;
1310        loop {
1311            let op = match self.peek() {
1312                Token::Star => BinOp::Mul,
1313                Token::Slash => BinOp::Div,
1314                _ => break,
1315            };
1316            self.advance();
1317            let right = self.parse_primary()?;
1318            left = Expr::BinaryOp(Box::new(left), op, Box::new(right));
1319        }
1320        Ok(left)
1321    }
1322
1323    fn parse_primary(&mut self) -> Result<Expr, ParseError> {
1324        // Guard recursion here too: unary prefixes (`not`, `exists`, `not exists`)
1325        // recurse straight back into parse_primary without going through parse_expr,
1326        // so a chain like `not not … .x` would otherwise overflow the stack (process
1327        // abort under panic=abort). See test_unary_prefix_nesting_depth_limit.
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_primary_inner();
1336        self.depth -= 1;
1337        result
1338    }
1339
1340    fn parse_primary_inner(&mut self) -> Result<Expr, ParseError> {
1341        match self.peek().clone() {
1342            Token::DotIdent(name) => {
1343                self.advance();
1344                Ok(Expr::Field(name))
1345            }
1346            Token::IntLit(v) => {
1347                self.advance();
1348                Ok(Expr::Literal(Literal::Int(v)))
1349            }
1350            Token::FloatLit(v) => {
1351                self.advance();
1352                Ok(Expr::Literal(Literal::Float(v)))
1353            }
1354            Token::StringLit(v) => {
1355                self.advance();
1356                Ok(Expr::Literal(Literal::String(v)))
1357            }
1358            Token::BoolLit(v) => {
1359                self.advance();
1360                Ok(Expr::Literal(Literal::Bool(v)))
1361            }
1362            // `$N` placeholders are only valid through
1363            // `parse_with_params`, which substitutes them for literal
1364            // tokens before this expression parser ever runs. Reaching a
1365            // raw `Token::Param` here means the caller used the plain
1366            // (no-params) path with a placeholder — surface the standard
1367            // unexpected-token error so the message names the parameter.
1368            Token::Null => {
1369                self.advance();
1370                Ok(Expr::Null)
1371            }
1372            Token::Not => {
1373                self.advance();
1374                if *self.peek() == Token::Exists {
1375                    self.advance();
1376                    // `not exists (Q)` → ExistsSubquery{ negated: true } when
1377                    // followed by `( Ident ...` (subquery form). Otherwise
1378                    // fall back to the scalar `is not null` unary op.
1379                    if let Some(sub) = self.try_parse_exists_subquery()? {
1380                        return Ok(Expr::ExistsSubquery {
1381                            subquery: Box::new(sub),
1382                            negated: true,
1383                        });
1384                    }
1385                    let expr = self.parse_primary()?;
1386                    Ok(Expr::UnaryOp(UnaryOp::NotExists, Box::new(expr)))
1387                } else {
1388                    let expr = self.parse_primary()?;
1389                    Ok(Expr::UnaryOp(UnaryOp::Not, Box::new(expr)))
1390                }
1391            }
1392            Token::Exists => {
1393                self.advance();
1394                // `exists (Q)` → ExistsSubquery when followed by a
1395                // parenthesised query. Scalar `exists .field` still parses
1396                // as UnaryOp::Exists for backwards compatibility.
1397                if let Some(sub) = self.try_parse_exists_subquery()? {
1398                    return Ok(Expr::ExistsSubquery {
1399                        subquery: Box::new(sub),
1400                        negated: false,
1401                    });
1402                }
1403                let expr = self.parse_primary()?;
1404                Ok(Expr::UnaryOp(UnaryOp::Exists, Box::new(expr)))
1405            }
1406            Token::LParen => {
1407                self.advance();
1408                let expr = self.parse_expr()?;
1409                self.expect(&Token::RParen)?;
1410                Ok(expr)
1411            }
1412            Token::Ident(name) => {
1413                self.advance();
1414                // `uuid("…")` / `bytes("…")` cast sugar. `uuid`/`bytes` are not
1415                // lexer keywords (so `type T { id: uuid }` and identifiers named
1416                // `uuid` are untouched); an `Ident` immediately followed by `(`
1417                // with a matching name is single-argument cast sugar.
1418                if *self.peek() == Token::LParen {
1419                    let cast_type = match name.as_str() {
1420                        "uuid" => Some(CastType::Uuid),
1421                        "bytes" => Some(CastType::Bytes),
1422                        _ => None,
1423                    };
1424                    if let Some(cast_type) = cast_type {
1425                        self.advance(); // consume `(`
1426                        let inner = self.parse_expr()?;
1427                        self.expect(&Token::RParen)?;
1428                        return Ok(Expr::Cast(Box::new(inner), cast_type));
1429                    }
1430                }
1431                // `alias.field` → QualifiedField. The lexer emits `t1.name` as
1432                // `Ident("t1")` + `DotIdent("name")` (see lexer.rs line 30),
1433                // so a trailing DotIdent here means a qualified reference.
1434                if let Token::DotIdent(field) = self.peek().clone() {
1435                    self.advance();
1436                    return Ok(Expr::QualifiedField {
1437                        qualifier: name,
1438                        field,
1439                    });
1440                }
1441                Ok(Expr::Field(name))
1442            }
1443            // Window-only functions: row_number(), rank(), dense_rank()
1444            Token::RowNumber | Token::Rank | Token::DenseRank => {
1445                let wfunc = match self.advance() {
1446                    Token::RowNumber => WindowFunc::RowNumber,
1447                    Token::Rank => WindowFunc::Rank,
1448                    Token::DenseRank => WindowFunc::DenseRank,
1449                    _ => {
1450                        return Err(ParseError::Syntax {
1451                            message: "unexpected window function token".into(),
1452                        })
1453                    }
1454                };
1455                self.expect(&Token::LParen)?;
1456                self.expect(&Token::RParen)?;
1457                let (partition_by, order_by) = self.parse_over_clause()?;
1458                Ok(Expr::Window {
1459                    function: wfunc,
1460                    args: vec![],
1461                    partition_by,
1462                    order_by,
1463                })
1464            }
1465            // Aggregate function calls inside expressions (projections, HAVING).
1466            // Top-level `count(User)` still routes through parse_aggregate_query
1467            // in parse_statement; this arm handles `count(.id)`, `sum(.age)`, etc.
1468            Token::Count | Token::Avg | Token::Sum | Token::Min | Token::Max => {
1469                let mut func = match self.advance() {
1470                    Token::Count => AggFunc::Count,
1471                    Token::Avg => AggFunc::Avg,
1472                    Token::Sum => AggFunc::Sum,
1473                    Token::Min => AggFunc::Min,
1474                    Token::Max => AggFunc::Max,
1475                    _ => {
1476                        return Err(ParseError::Syntax {
1477                            message: "unexpected aggregate token".into(),
1478                        })
1479                    }
1480                };
1481                self.expect(&Token::LParen)?;
1482                // count(*) — count all rows including nulls
1483                if func == AggFunc::Count && *self.peek() == Token::Star {
1484                    self.advance();
1485                    self.expect(&Token::RParen)?;
1486                    // Check for OVER — count(*) over (...)
1487                    if *self.peek() == Token::Over {
1488                        let (partition_by, order_by) = self.parse_over_clause()?;
1489                        return Ok(Expr::Window {
1490                            function: WindowFunc::Count,
1491                            args: vec![Expr::Field("*".into())],
1492                            partition_by,
1493                            order_by,
1494                        });
1495                    }
1496                    return Ok(Expr::FunctionCall(
1497                        AggFunc::Count,
1498                        Box::new(Expr::Field("*".into())),
1499                    ));
1500                }
1501                // count(distinct .field) → CountDistinct
1502                if func == AggFunc::Count && *self.peek() == Token::Distinct {
1503                    self.advance();
1504                    func = AggFunc::CountDistinct;
1505                }
1506                let inner = self.parse_expr()?;
1507                self.expect(&Token::RParen)?;
1508                // Check for OVER — e.g. sum(.salary) over (...)
1509                if *self.peek() == Token::Over {
1510                    let wfunc = match func {
1511                        AggFunc::Count => WindowFunc::Count,
1512                        AggFunc::Avg => WindowFunc::Avg,
1513                        AggFunc::Sum => WindowFunc::Sum,
1514                        AggFunc::Min => WindowFunc::Min,
1515                        AggFunc::Max => WindowFunc::Max,
1516                        _ => {
1517                            return Err(ParseError::Unsupported {
1518                                feature: "count(distinct ...) over (...) is not supported".into(),
1519                            })
1520                        }
1521                    };
1522                    let (partition_by, order_by) = self.parse_over_clause()?;
1523                    return Ok(Expr::Window {
1524                        function: wfunc,
1525                        args: vec![inner],
1526                        partition_by,
1527                        order_by,
1528                    });
1529                }
1530                Ok(Expr::FunctionCall(func, Box::new(inner)))
1531            }
1532            Token::Upper
1533            | Token::Lower
1534            | Token::Length
1535            | Token::Trim
1536            | Token::Substring
1537            | Token::Concat
1538            | Token::Abs
1539            | Token::Round
1540            | Token::Ceil
1541            | Token::Floor
1542            | Token::Sqrt
1543            | Token::Pow
1544            | Token::Now
1545            | Token::Extract
1546            | Token::DateAdd
1547            | Token::DateDiff => {
1548                let tok = self.advance();
1549                let func = token_to_scalar_fn(&tok);
1550                self.expect(&Token::LParen)?;
1551                let mut args = Vec::new();
1552                while !matches!(self.peek(), Token::RParen | Token::Eof) {
1553                    args.push(self.parse_expr()?);
1554                    if *self.peek() == Token::Comma {
1555                        self.advance();
1556                    }
1557                }
1558                self.expect(&Token::RParen)?;
1559                Ok(Expr::ScalarFunc(func, args))
1560            }
1561            Token::Cast => {
1562                self.advance();
1563                self.expect(&Token::LParen)?;
1564                let inner = self.parse_expr()?;
1565                self.expect(&Token::Comma)?;
1566                let cast_type = self.parse_cast_type()?;
1567                self.expect(&Token::RParen)?;
1568                Ok(Expr::Cast(Box::new(inner), cast_type))
1569            }
1570            Token::Case => {
1571                self.advance();
1572                let mut whens = Vec::new();
1573                while *self.peek() == Token::When {
1574                    self.advance();
1575                    let condition = self.parse_expr()?;
1576                    self.expect(&Token::Then)?;
1577                    let result = self.parse_expr()?;
1578                    whens.push((Box::new(condition), Box::new(result)));
1579                }
1580                let else_expr = if *self.peek() == Token::Else {
1581                    self.advance();
1582                    Some(Box::new(self.parse_expr()?))
1583                } else {
1584                    None
1585                };
1586                self.expect(&Token::End)?;
1587                Ok(Expr::Case { whens, else_expr })
1588            }
1589            t => Err(ParseError::Syntax {
1590                message: format!("unexpected token in expression: {}", t.display_name()),
1591            }),
1592        }
1593    }
1594
1595    /// `alter <Table> add [column] [required] <name>: <type>`
1596    /// `alter <Table> drop [column] <name>`
1597    fn parse_alter_table(&mut self) -> Result<Statement, ParseError> {
1598        self.expect(&Token::Alter)?;
1599        let table = match self.advance() {
1600            Token::Ident(name) => name,
1601            t => {
1602                return Err(ParseError::UnexpectedToken {
1603                    expected: "table name after alter".into(),
1604                    got: t.display_name(),
1605                })
1606            }
1607        };
1608        match self.peek() {
1609            Token::Add => {
1610                self.advance();
1611                // `alter <Table> add index [if not exists] .<column>`
1612                if *self.peek() == Token::Index {
1613                    self.advance();
1614                    let if_not_exists = self.parse_optional_if_not_exists();
1615                    let column = match self.advance() {
1616                        Token::DotIdent(n) => n,
1617                        t => {
1618                            return Err(ParseError::UnexpectedToken {
1619                                expected: ".<column> after add index".into(),
1620                                got: t.display_name(),
1621                            })
1622                        }
1623                    };
1624                    return Ok(Statement::AlterTable(AlterTableExpr {
1625                        table,
1626                        action: AlterAction::AddIndex {
1627                            column,
1628                            if_not_exists,
1629                        },
1630                    }));
1631                }
1632                // `alter <Table> add unique [if not exists] .<column>`
1633                if *self.peek() == Token::Unique {
1634                    self.advance();
1635                    let if_not_exists = self.parse_optional_if_not_exists();
1636                    let column = match self.advance() {
1637                        Token::DotIdent(n) => n,
1638                        t => {
1639                            return Err(ParseError::UnexpectedToken {
1640                                expected: ".<column> after add unique".into(),
1641                                got: t.display_name(),
1642                            })
1643                        }
1644                    };
1645                    return Ok(Statement::AlterTable(AlterTableExpr {
1646                        table,
1647                        action: AlterAction::AddUnique {
1648                            column,
1649                            if_not_exists,
1650                        },
1651                    }));
1652                }
1653                // optional `column` keyword
1654                if *self.peek() == Token::Column {
1655                    self.advance();
1656                }
1657                let required = if *self.peek() == Token::Required {
1658                    self.advance();
1659                    true
1660                } else {
1661                    false
1662                };
1663                let name = self.expect_named_ident("column name")?;
1664                self.expect(&Token::Colon)?;
1665                let type_name = match self.advance() {
1666                    Token::Ident(n) => n,
1667                    t => {
1668                        return Err(ParseError::UnexpectedToken {
1669                            expected: "type name".into(),
1670                            got: t.display_name(),
1671                        })
1672                    }
1673                };
1674                Ok(Statement::AlterTable(AlterTableExpr {
1675                    table,
1676                    action: AlterAction::AddColumn {
1677                        name,
1678                        type_name,
1679                        required,
1680                    },
1681                }))
1682            }
1683            Token::Drop => {
1684                self.advance();
1685                // optional `column` keyword
1686                if *self.peek() == Token::Column {
1687                    self.advance();
1688                }
1689                let if_exists = self.parse_optional_if_exists();
1690                let name = self.expect_named_ident("column name")?;
1691                Ok(Statement::AlterTable(AlterTableExpr {
1692                    table,
1693                    action: AlterAction::DropColumn { name, if_exists },
1694                }))
1695            }
1696            t => Err(ParseError::UnexpectedToken {
1697                expected: "add or drop after alter <table>".into(),
1698                got: t.display_name(),
1699            }),
1700        }
1701    }
1702
1703    /// `drop [if exists] <Table>` or `drop view [if exists] <ViewName>`
1704    fn parse_drop_or_drop_view(&mut self) -> Result<Statement, ParseError> {
1705        self.expect(&Token::Drop)?;
1706        if *self.peek() == Token::View {
1707            self.advance(); // consume `view`
1708            let if_exists = self.parse_optional_if_exists();
1709            let name = match self.advance() {
1710                Token::Ident(name) => name,
1711                t => {
1712                    return Err(ParseError::UnexpectedToken {
1713                        expected: "view name after drop view".into(),
1714                        got: t.display_name(),
1715                    })
1716                }
1717            };
1718            return Ok(Statement::DropView(DropViewExpr { name, if_exists }));
1719        }
1720        let if_exists = self.parse_optional_if_exists();
1721        let table = match self.advance() {
1722            Token::Ident(name) => name,
1723            t => {
1724                return Err(ParseError::UnexpectedToken {
1725                    expected: "table name after drop".into(),
1726                    got: t.display_name(),
1727                })
1728            }
1729        };
1730        Ok(Statement::DropTable(DropTableExpr { table, if_exists }))
1731    }
1732
1733    /// `materialize <ViewName> as <Query>`
1734    ///
1735    /// The source query text is captured by slicing the original token stream
1736    /// from the position after `as` to the end.
1737    fn parse_create_view(&mut self) -> Result<Statement, ParseError> {
1738        self.expect(&Token::Materialized)?;
1739        let name = match self.advance() {
1740            Token::Ident(name) => name,
1741            t => {
1742                return Err(ParseError::UnexpectedToken {
1743                    expected: "view name after materialize".into(),
1744                    got: t.display_name(),
1745                })
1746            }
1747        };
1748        self.expect(&Token::As)?;
1749        // Record position so we can reconstruct the query text for storage.
1750        let query_start = self.pos;
1751        let source = match self.advance() {
1752            Token::Ident(s) => s,
1753            t => {
1754                return Err(ParseError::UnexpectedToken {
1755                    expected: "source table name".into(),
1756                    got: t.display_name(),
1757                })
1758            }
1759        };
1760        let query = self.parse_query_tail(source)?;
1761        // Reconstruct query text from tokens for storage and re-execution.
1762        let query_text = tokens_to_text(&self.tokens[query_start..self.pos]);
1763        Ok(Statement::CreateView(CreateViewExpr {
1764            name,
1765            query,
1766            query_text,
1767        }))
1768    }
1769
1770    /// Check for `union [all]` after a query and build a left-associative
1771    /// chain if present.
1772    fn maybe_parse_union(&mut self, left: Statement) -> Result<Statement, ParseError> {
1773        if *self.peek() != Token::Union {
1774            return Ok(left);
1775        }
1776        if !matches!(left, Statement::Query(_) | Statement::Union(_)) {
1777            return Err(ParseError::Syntax {
1778                message: "UNION requires a query on the left side".into(),
1779            });
1780        }
1781        self.advance(); // consume `union`
1782        let all = if let Token::Ident(s) = self.peek() {
1783            if s == "all" {
1784                self.advance();
1785                true
1786            } else {
1787                false
1788            }
1789        } else {
1790            false
1791        };
1792        // Parse the RHS as a single query (not chained — we'll chain ourselves).
1793        let right = self.parse_single_query()?;
1794        let union = Statement::Union(UnionExpr {
1795            left: Box::new(left),
1796            right: Box::new(right),
1797            all,
1798        });
1799        // Recursively check for further chaining: `A union B union C`
1800        self.maybe_parse_union(union)
1801    }
1802
1803    /// Parse a single query statement (no UNION chaining). Used for UNION RHS.
1804    fn parse_single_query(&mut self) -> Result<Statement, ParseError> {
1805        match self.peek() {
1806            Token::Count | Token::Avg | Token::Sum | Token::Min | Token::Max => {
1807                self.parse_aggregate_query()
1808            }
1809            Token::Ident(_) => self.parse_query_or_mutation(),
1810            _ => Err(ParseError::Syntax {
1811                message: format!(
1812                    "expected query after UNION, got {}",
1813                    self.peek().display_name()
1814                ),
1815            }),
1816        }
1817    }
1818
1819    /// `refresh <ViewName>`
1820    fn parse_refresh_view(&mut self) -> Result<Statement, ParseError> {
1821        self.expect(&Token::Refresh)?;
1822        let name = match self.advance() {
1823            Token::Ident(name) => name,
1824            t => {
1825                return Err(ParseError::UnexpectedToken {
1826                    expected: "view name after refresh".into(),
1827                    got: t.display_name(),
1828                })
1829            }
1830        };
1831        Ok(Statement::RefreshView(RefreshViewExpr { name }))
1832    }
1833
1834    fn parse_create_type(&mut self) -> Result<Statement, ParseError> {
1835        self.expect(&Token::Type)?;
1836        let name = self.expect_named_ident("type name")?;
1837        let if_not_exists = self.parse_optional_if_not_exists();
1838        self.expect(&Token::LBrace)?;
1839        let mut fields = Vec::new();
1840        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1841            // Accept `required`, `unique`, and `auto` modifiers in any order.
1842            // A modifier keyword immediately followed by `:` is instead the
1843            // field's *name* (e.g. `required: int`) — leave it for
1844            // `expect_named_ident`, which emits the reserved-word guidance.
1845            let (mut required, mut unique, mut auto) = (false, false, false);
1846            loop {
1847                let is_modifier =
1848                    matches!(self.peek(), Token::Required | Token::Unique | Token::Auto)
1849                        && !matches!(self.tokens.get(self.pos + 1), Some(Token::Colon));
1850                if !is_modifier {
1851                    break;
1852                }
1853                match self.advance() {
1854                    Token::Required => required = true,
1855                    Token::Unique => unique = true,
1856                    Token::Auto => auto = true,
1857                    _ => unreachable!("guarded by is_modifier"),
1858                }
1859            }
1860            let field_name = self.expect_named_ident("field name")?;
1861            self.expect(&Token::Colon)?;
1862            let type_name = match self.advance() {
1863                Token::Ident(n) => n,
1864                t => {
1865                    return Err(ParseError::UnexpectedToken {
1866                        expected: "type name".into(),
1867                        got: t.display_name(),
1868                    })
1869                }
1870            };
1871            // Optional `default <literal>` — value applied when an insert
1872            // omits this column.
1873            let default = if *self.peek() == Token::Default {
1874                self.advance();
1875                Some(self.parse_default_literal()?)
1876            } else {
1877                None
1878            };
1879            fields.push(FieldDef {
1880                name: field_name,
1881                type_name,
1882                required,
1883                unique,
1884                default,
1885                auto,
1886            });
1887            if *self.peek() == Token::Comma {
1888                self.advance();
1889            }
1890        }
1891        self.expect(&Token::RBrace)?;
1892        Ok(Statement::CreateType(CreateTypeExpr {
1893            name,
1894            fields,
1895            if_not_exists,
1896        }))
1897    }
1898
1899    /// `schema` — list all types. `schema <Type>` is an alias for
1900    /// `describe <Type>`.
1901    fn parse_schema(&mut self) -> Result<Statement, ParseError> {
1902        self.expect(&Token::Schema)?;
1903        if let Token::Ident(_) = self.peek() {
1904            let table = self.expect_named_ident("type name")?;
1905            return Ok(Statement::Describe(table));
1906        }
1907        Ok(Statement::ListTypes)
1908    }
1909
1910    /// `describe <Type>` — the columns and indexes of one type.
1911    fn parse_describe(&mut self) -> Result<Statement, ParseError> {
1912        self.expect(&Token::Describe)?;
1913        let table = self.expect_named_ident("type name")?;
1914        Ok(Statement::Describe(table))
1915    }
1916
1917    /// Parse the literal following a `default` column modifier. Only scalar
1918    /// literals are allowed — expression defaults (e.g. `now()`) are not yet
1919    /// supported.
1920    fn parse_default_literal(&mut self) -> Result<Literal, ParseError> {
1921        match self.advance() {
1922            Token::IntLit(v) => Ok(Literal::Int(v)),
1923            Token::FloatLit(v) => Ok(Literal::Float(v)),
1924            Token::StringLit(v) => Ok(Literal::String(v)),
1925            Token::BoolLit(v) => Ok(Literal::Bool(v)),
1926            t => Err(ParseError::UnexpectedToken {
1927                expected: "literal default value".into(),
1928                got: t.display_name(),
1929            }),
1930        }
1931    }
1932}
1933
1934/// Reconstruct PowQL source text from a slice of tokens. Used to store the
1935/// view's source query for re-execution on refresh. Not perfectly
1936/// round-trippable (whitespace is normalised) but semantically identical.
1937fn tokens_to_text(tokens: &[Token]) -> String {
1938    let mut out = String::with_capacity(64);
1939    for tok in tokens {
1940        if !out.is_empty() && !matches!(tok, Token::Eof) {
1941            out.push(' ');
1942        }
1943        match tok {
1944            Token::Ident(s) => out.push_str(s),
1945            Token::DotIdent(s) => {
1946                out.push('.');
1947                out.push_str(s);
1948            }
1949            Token::IntLit(v) => out.push_str(&v.to_string()),
1950            Token::FloatLit(v) => out.push_str(&v.to_string()),
1951            Token::StringLit(s) => {
1952                out.push('"');
1953                out.push_str(s);
1954                out.push('"');
1955            }
1956            Token::BoolLit(v) => out.push_str(if *v { "true" } else { "false" }),
1957            Token::Param(s) => {
1958                out.push('$');
1959                out.push_str(s);
1960            }
1961            Token::Type => out.push_str("type"),
1962            Token::Filter => out.push_str("filter"),
1963            Token::Order => out.push_str("order"),
1964            Token::Limit => out.push_str("limit"),
1965            Token::Offset => out.push_str("offset"),
1966            Token::Insert => out.push_str("insert"),
1967            Token::Update => out.push_str("update"),
1968            Token::Delete => out.push_str("delete"),
1969            Token::Upsert => out.push_str("upsert"),
1970            Token::Returning => out.push_str("returning"),
1971            Token::Conflict => out.push_str("conflict"),
1972            Token::Select => out.push_str("select"),
1973            Token::Required => out.push_str("required"),
1974            Token::Default => out.push_str("default"),
1975            Token::Auto => out.push_str("auto"),
1976            Token::Multi => out.push_str("multi"),
1977            Token::Link => out.push_str("link"),
1978            Token::Index => out.push_str("index"),
1979            Token::Unique => out.push_str("unique"),
1980            Token::On => out.push_str("on"),
1981            Token::Asc => out.push_str("asc"),
1982            Token::Desc => out.push_str("desc"),
1983            Token::And => out.push_str("and"),
1984            Token::Or => out.push_str("or"),
1985            Token::Not => out.push_str("not"),
1986            Token::Exists => out.push_str("exists"),
1987            Token::Let => out.push_str("let"),
1988            Token::As => out.push_str("as"),
1989            Token::Match => out.push_str("match"),
1990            Token::Group => out.push_str("group"),
1991            Token::Join => out.push_str("join"),
1992            Token::Inner => out.push_str("inner"),
1993            Token::LeftKw => out.push_str("left"),
1994            Token::RightKw => out.push_str("right"),
1995            Token::Outer => out.push_str("outer"),
1996            Token::Cross => out.push_str("cross"),
1997            Token::Transaction => out.push_str("transaction"),
1998            Token::Begin => out.push_str("begin"),
1999            Token::Commit => out.push_str("commit"),
2000            Token::Rollback => out.push_str("rollback"),
2001            Token::View => out.push_str("view"),
2002            Token::Materialized => out.push_str("materialized"),
2003            Token::Refresh => out.push_str("refresh"),
2004            Token::Union => out.push_str("union"),
2005            Token::Having => out.push_str("having"),
2006            Token::Distinct => out.push_str("distinct"),
2007            Token::In => out.push_str("in"),
2008            Token::Between => out.push_str("between"),
2009            Token::Like => out.push_str("like"),
2010            Token::Count => out.push_str("count"),
2011            Token::Avg => out.push_str("avg"),
2012            Token::Sum => out.push_str("sum"),
2013            Token::Min => out.push_str("min"),
2014            Token::Max => out.push_str("max"),
2015            Token::Is => out.push_str("is"),
2016            Token::Null => out.push_str("null"),
2017            Token::Upper => out.push_str("upper"),
2018            Token::Lower => out.push_str("lower"),
2019            Token::Length => out.push_str("length"),
2020            Token::Trim => out.push_str("trim"),
2021            Token::Substring => out.push_str("substring"),
2022            Token::Concat => out.push_str("concat"),
2023            Token::Abs => out.push_str("abs"),
2024            Token::Round => out.push_str("round"),
2025            Token::Ceil => out.push_str("ceil"),
2026            Token::Floor => out.push_str("floor"),
2027            Token::Sqrt => out.push_str("sqrt"),
2028            Token::Pow => out.push_str("pow"),
2029            Token::Now => out.push_str("now"),
2030            Token::Extract => out.push_str("extract"),
2031            Token::DateAdd => out.push_str("date_add"),
2032            Token::DateDiff => out.push_str("date_diff"),
2033            Token::Cast => out.push_str("cast"),
2034            Token::Case => out.push_str("case"),
2035            Token::When => out.push_str("when"),
2036            Token::Then => out.push_str("then"),
2037            Token::Else => out.push_str("else"),
2038            Token::End => out.push_str("end"),
2039            Token::Over => out.push_str("over"),
2040            Token::Partition => out.push_str("partition"),
2041            Token::RowNumber => out.push_str("row_number"),
2042            Token::Rank => out.push_str("rank"),
2043            Token::DenseRank => out.push_str("dense_rank"),
2044            Token::Alter => out.push_str("alter"),
2045            Token::Drop => out.push_str("drop"),
2046            Token::Add => out.push_str("add"),
2047            Token::Column => out.push_str("column"),
2048            Token::Eq => out.push('='),
2049            Token::Neq => out.push_str("!="),
2050            Token::Lt => out.push('<'),
2051            Token::Gt => out.push('>'),
2052            Token::Lte => out.push_str("<="),
2053            Token::Gte => out.push_str(">="),
2054            Token::Assign => out.push_str(":="),
2055            Token::Arrow => out.push_str("->"),
2056            Token::Pipe => out.push('|'),
2057            Token::Coalesce => out.push_str("??"),
2058            Token::Plus => out.push('+'),
2059            Token::Minus => out.push('-'),
2060            Token::Star => out.push('*'),
2061            Token::Slash => out.push('/'),
2062            Token::LBrace => out.push('{'),
2063            Token::RBrace => out.push('}'),
2064            Token::LParen => out.push('('),
2065            Token::RParen => out.push(')'),
2066            Token::Comma => out.push(','),
2067            Token::Colon => out.push(':'),
2068            Token::Dot => out.push('.'),
2069            Token::Explain => out.push_str("explain"),
2070            Token::Schema => out.push_str("schema"),
2071            Token::Describe => out.push_str("describe"),
2072            Token::Eof => {}
2073        }
2074    }
2075    out
2076}
2077
2078#[cfg(test)]
2079mod tests {
2080    use super::*;
2081    #[test]
2082    fn test_parse_simple_query() {
2083        let stmt = parse("User").unwrap();
2084        match stmt {
2085            Statement::Query(q) => {
2086                assert_eq!(q.source, "User");
2087                assert!(q.filter.is_none());
2088                assert!(q.projection.is_none());
2089            }
2090            _ => panic!("expected query"),
2091        }
2092    }
2093
2094    #[test]
2095    fn test_parse_filter() {
2096        let stmt = parse("User filter .age > 30").unwrap();
2097        match stmt {
2098            Statement::Query(q) => {
2099                assert_eq!(q.source, "User");
2100                assert!(q.filter.is_some());
2101            }
2102            _ => panic!("expected query"),
2103        }
2104    }
2105
2106    #[test]
2107    fn test_parse_projection() {
2108        let stmt = parse("User { name, email }").unwrap();
2109        match stmt {
2110            Statement::Query(q) => {
2111                let proj = q.projection.unwrap();
2112                assert_eq!(proj.len(), 2);
2113            }
2114            _ => panic!("expected query"),
2115        }
2116    }
2117
2118    #[test]
2119    fn test_parse_filter_order_limit() {
2120        let stmt = parse("User filter .age > 30 order .name desc limit 10").unwrap();
2121        match stmt {
2122            Statement::Query(q) => {
2123                assert!(q.filter.is_some());
2124                let order = q.order.unwrap();
2125                assert_eq!(order.keys.len(), 1);
2126                assert_eq!(order.keys[0].field, "name");
2127                assert!(order.keys[0].descending);
2128                assert!(q.limit.is_some());
2129            }
2130            _ => panic!("expected query"),
2131        }
2132    }
2133
2134    #[test]
2135    fn test_parse_insert() {
2136        let stmt = parse(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
2137        match stmt {
2138            Statement::Insert(ins) => {
2139                assert_eq!(ins.target, "User");
2140                assert_eq!(ins.rows.len(), 1);
2141                assert_eq!(ins.rows[0].len(), 2);
2142                assert_eq!(ins.rows[0][0].field, "name");
2143                assert_eq!(ins.rows[0][1].field, "age");
2144            }
2145            _ => panic!("expected insert"),
2146        }
2147    }
2148
2149    #[test]
2150    fn test_parse_insert_multi_row() {
2151        let stmt =
2152            parse(r#"insert User { name := "Alice", age := 30 }, { name := "Bob", age := 25 }, { name := "Cy" }"#)
2153                .unwrap();
2154        match stmt {
2155            Statement::Insert(ins) => {
2156                assert_eq!(ins.target, "User");
2157                assert_eq!(ins.rows.len(), 3);
2158                assert_eq!(ins.rows[0].len(), 2);
2159                assert_eq!(ins.rows[1][0].field, "name");
2160                assert_eq!(ins.rows[2].len(), 1);
2161                assert_eq!(ins.rows[2][0].field, "name");
2162            }
2163            _ => panic!("expected insert"),
2164        }
2165    }
2166
2167    #[test]
2168    fn test_parse_update() {
2169        let stmt = parse(r#"User filter .email = "alice@ex.com" update { age := 31 }"#).unwrap();
2170        match stmt {
2171            Statement::UpdateQuery(upd) => {
2172                assert_eq!(upd.source, "User");
2173                assert!(upd.filter.is_some());
2174                assert_eq!(upd.assignments.len(), 1);
2175                assert!(!upd.returning);
2176            }
2177            _ => panic!("expected update"),
2178        }
2179    }
2180
2181    #[test]
2182    fn test_parse_update_returning() {
2183        let stmt = parse(r#"User filter .name = "Alice" update { age := 31 } returning"#).unwrap();
2184        match stmt {
2185            Statement::UpdateQuery(upd) => assert!(upd.returning),
2186            _ => panic!("expected update"),
2187        }
2188    }
2189
2190    #[test]
2191    fn test_parse_delete() {
2192        let stmt = parse("User filter .age < 18 delete").unwrap();
2193        match stmt {
2194            Statement::DeleteQuery(del) => {
2195                assert_eq!(del.source, "User");
2196                assert!(del.filter.is_some());
2197                assert!(!del.returning);
2198            }
2199            _ => panic!("expected delete"),
2200        }
2201    }
2202
2203    #[test]
2204    fn test_parse_delete_returning() {
2205        let stmt = parse("User filter .age < 18 delete returning").unwrap();
2206        match stmt {
2207            Statement::DeleteQuery(del) => assert!(del.returning),
2208            _ => panic!("expected delete"),
2209        }
2210    }
2211
2212    #[test]
2213    fn test_parse_count() {
2214        let stmt = parse("count(User)").unwrap();
2215        match stmt {
2216            Statement::Query(q) => {
2217                let agg = q.aggregation.unwrap();
2218                assert_eq!(agg.function, AggFunc::Count);
2219                assert!(q.filter.is_none());
2220            }
2221            _ => panic!("expected query with aggregation"),
2222        }
2223    }
2224
2225    #[test]
2226    fn test_parse_count_with_filter() {
2227        // Regression: previously returned "expected RParen, got Filter".
2228        // count(<query>) must accept a full read-pipeline tail.
2229        let stmt = parse("count(User filter .age > 30)").unwrap();
2230        match stmt {
2231            Statement::Query(q) => {
2232                assert_eq!(q.source, "User");
2233                let agg = q.aggregation.unwrap();
2234                assert_eq!(agg.function, AggFunc::Count);
2235                assert!(q.filter.is_some(), "filter should have been parsed");
2236            }
2237            _ => panic!("expected query with aggregation"),
2238        }
2239    }
2240
2241    #[test]
2242    fn test_parse_count_with_filter_and_limit() {
2243        let stmt = parse("count(User filter .age > 30 limit 100)").unwrap();
2244        match stmt {
2245            Statement::Query(q) => {
2246                assert_eq!(q.source, "User");
2247                assert!(q.filter.is_some());
2248                assert!(q.limit.is_some());
2249                assert_eq!(q.aggregation.unwrap().function, AggFunc::Count);
2250            }
2251            _ => panic!("expected query with aggregation"),
2252        }
2253    }
2254
2255    #[test]
2256    fn test_parse_create_type() {
2257        let stmt = parse("type User { required name: str, age: int }").unwrap();
2258        match stmt {
2259            Statement::CreateType(ct) => {
2260                assert_eq!(ct.name, "User");
2261                assert_eq!(ct.fields.len(), 2);
2262                assert!(ct.fields[0].required);
2263                assert!(!ct.fields[1].required);
2264            }
2265            _ => panic!("expected create type"),
2266        }
2267    }
2268
2269    #[test]
2270    fn test_parse_sum_with_field_projection() {
2271        // `sum(... { .age })` should lift `.age` into AggregateExpr.field and
2272        // clear the projection so the executor's aggregate fast path fires.
2273        let stmt = parse("sum(User filter .age > 30 { .age })").unwrap();
2274        match stmt {
2275            Statement::Query(q) => {
2276                let agg = q.aggregation.expect("aggregate");
2277                assert_eq!(agg.function, AggFunc::Sum);
2278                assert_eq!(agg.field.as_deref(), Some("age"));
2279                assert!(
2280                    q.projection.is_none(),
2281                    "projection should be lifted into agg.field"
2282                );
2283            }
2284            _ => panic!("expected query"),
2285        }
2286    }
2287
2288    #[test]
2289    fn test_parse_avg_min_max_with_field() {
2290        for (src, expected) in [
2291            ("avg(User { .age })", AggFunc::Avg),
2292            ("min(User { .age })", AggFunc::Min),
2293            ("max(User { .age })", AggFunc::Max),
2294        ] {
2295            let stmt = parse(src).unwrap();
2296            match stmt {
2297                Statement::Query(q) => {
2298                    let agg = q.aggregation.unwrap();
2299                    assert_eq!(agg.function, expected, "func mismatch for {src}");
2300                    assert_eq!(
2301                        agg.field.as_deref(),
2302                        Some("age"),
2303                        "field mismatch for {src}"
2304                    );
2305                    assert!(
2306                        q.projection.is_none(),
2307                        "projection should be cleared for {src}"
2308                    );
2309                }
2310                _ => panic!("expected query for {src}"),
2311            }
2312        }
2313    }
2314
2315    #[test]
2316    fn test_parse_count_leaves_projection_alone() {
2317        // count() doesn't need a target field, so the projection (if any)
2318        // stays intact. It's silly to project inside a count, but it's legal.
2319        let stmt = parse("count(User { .age })").unwrap();
2320        match stmt {
2321            Statement::Query(q) => {
2322                let agg = q.aggregation.unwrap();
2323                assert_eq!(agg.function, AggFunc::Count);
2324                assert!(agg.field.is_none());
2325                assert!(q.projection.is_some(), "count must not eat projection");
2326            }
2327            _ => panic!("expected query"),
2328        }
2329    }
2330
2331    // ---- Mission E1.1: JOIN parser tests ----------------------------------
2332    // Parser-level only. The planner rejects joins with a clean error until
2333    // E1.2 wires up execution.
2334
2335    #[test]
2336    fn test_parse_source_alias() {
2337        let stmt = parse("User as u filter u.age > 30").unwrap();
2338        match stmt {
2339            Statement::Query(q) => {
2340                assert_eq!(q.source, "User");
2341                assert_eq!(q.alias.as_deref(), Some("u"));
2342                assert!(q.joins.is_empty());
2343                match q.filter.unwrap() {
2344                    Expr::BinaryOp(l, BinOp::Gt, _) => match *l {
2345                        Expr::QualifiedField { qualifier, field } => {
2346                            assert_eq!(qualifier, "u");
2347                            assert_eq!(field, "age");
2348                        }
2349                        other => panic!("expected qualified field, got {other:?}"),
2350                    },
2351                    other => panic!("expected >, got {other:?}"),
2352                }
2353            }
2354            _ => panic!("expected query"),
2355        }
2356    }
2357
2358    #[test]
2359    fn test_parse_inner_join_on() {
2360        let stmt = parse("User as u inner join Order as o on u.id = o.user_id").unwrap();
2361        match stmt {
2362            Statement::Query(q) => {
2363                assert_eq!(q.source, "User");
2364                assert_eq!(q.alias.as_deref(), Some("u"));
2365                assert_eq!(q.joins.len(), 1);
2366                let j = &q.joins[0];
2367                assert_eq!(j.kind, JoinKind::Inner);
2368                assert_eq!(j.source, "Order");
2369                assert_eq!(j.alias.as_deref(), Some("o"));
2370                let on = j.on.as_ref().expect("on clause");
2371                match on {
2372                    Expr::BinaryOp(l, BinOp::Eq, r) => {
2373                        assert!(matches!(**l, Expr::QualifiedField { .. }));
2374                        assert!(matches!(**r, Expr::QualifiedField { .. }));
2375                    }
2376                    other => panic!("expected eq, got {other:?}"),
2377                }
2378            }
2379            _ => panic!("expected query"),
2380        }
2381    }
2382
2383    #[test]
2384    fn test_parse_bare_join_defaults_to_inner() {
2385        let stmt = parse("User join Order on User.id = Order.user_id").unwrap();
2386        match stmt {
2387            Statement::Query(q) => {
2388                assert_eq!(q.joins.len(), 1);
2389                assert_eq!(q.joins[0].kind, JoinKind::Inner);
2390            }
2391            _ => panic!("expected query"),
2392        }
2393    }
2394
2395    #[test]
2396    fn test_parse_left_outer_join() {
2397        let stmt = parse("User as u left outer join Order as o on u.id = o.user_id").unwrap();
2398        match stmt {
2399            Statement::Query(q) => {
2400                assert_eq!(q.joins.len(), 1);
2401                assert_eq!(q.joins[0].kind, JoinKind::LeftOuter);
2402            }
2403            _ => panic!("expected query"),
2404        }
2405    }
2406
2407    #[test]
2408    fn test_parse_left_join_without_outer_keyword() {
2409        // `left join` is shorthand for `left outer join` in SQL — we accept it.
2410        let stmt = parse("User as u left join Order as o on u.id = o.user_id").unwrap();
2411        match stmt {
2412            Statement::Query(q) => {
2413                assert_eq!(q.joins[0].kind, JoinKind::LeftOuter);
2414            }
2415            _ => panic!("expected query"),
2416        }
2417    }
2418
2419    #[test]
2420    fn test_parse_right_join() {
2421        let stmt = parse("User as u right join Order as o on u.id = o.user_id").unwrap();
2422        match stmt {
2423            Statement::Query(q) => {
2424                assert_eq!(q.joins[0].kind, JoinKind::RightOuter);
2425            }
2426            _ => panic!("expected query"),
2427        }
2428    }
2429
2430    #[test]
2431    fn test_parse_cross_join_has_no_on() {
2432        let stmt = parse("User cross join Order").unwrap();
2433        match stmt {
2434            Statement::Query(q) => {
2435                assert_eq!(q.joins[0].kind, JoinKind::Cross);
2436                assert!(q.joins[0].on.is_none());
2437            }
2438            _ => panic!("expected query"),
2439        }
2440    }
2441
2442    #[test]
2443    fn test_parse_multi_join_chain() {
2444        let stmt = parse(
2445            "User as u join Order as o on u.id = o.user_id \
2446             join Product as p on o.product_id = p.id",
2447        )
2448        .unwrap();
2449        match stmt {
2450            Statement::Query(q) => {
2451                assert_eq!(q.joins.len(), 2);
2452                assert_eq!(q.joins[0].source, "Order");
2453                assert_eq!(q.joins[1].source, "Product");
2454            }
2455            _ => panic!("expected query"),
2456        }
2457    }
2458
2459    #[test]
2460    fn test_parse_join_with_filter_tail() {
2461        // Filter/order/limit still work after a join clause.
2462        let stmt = parse(
2463            "User as u join Order as o on u.id = o.user_id \
2464             filter o.total > 100 order .name limit 10",
2465        )
2466        .unwrap();
2467        match stmt {
2468            Statement::Query(q) => {
2469                assert_eq!(q.joins.len(), 1);
2470                assert!(q.filter.is_some());
2471                assert!(q.order.is_some());
2472                assert!(q.limit.is_some());
2473            }
2474            _ => panic!("expected query"),
2475        }
2476    }
2477
2478    #[test]
2479    fn test_parse_join_requires_on_for_inner() {
2480        // Non-cross joins require `on <expr>`. Missing `on` is a parse error.
2481        let err = parse("User join Order").unwrap_err();
2482        assert!(
2483            err.message().contains("on"),
2484            "expected on-clause error, got {:?}",
2485            err.message()
2486        );
2487    }
2488
2489    #[test]
2490    fn test_parse_update_on_joined_query_errors() {
2491        // E1.1 explicitly rejects update/delete on joined queries — SQL
2492        // semantics here are messy and we're not implementing them yet.
2493        let err =
2494            parse("User as u join Order as o on u.id = o.user_id update { age := 1 }").unwrap_err();
2495        assert!(err.message().contains("update"));
2496    }
2497
2498    #[test]
2499    fn test_parse_delete_on_joined_query_errors() {
2500        let err = parse("User as u join Order as o on u.id = o.user_id delete").unwrap_err();
2501        assert!(err.message().contains("delete"));
2502    }
2503
2504    // ---- Mission E2a: DISTINCT + IN-list + BETWEEN + LIKE -----------------
2505
2506    #[test]
2507    fn test_parse_distinct() {
2508        let stmt = parse("User distinct { .name }").unwrap();
2509        match stmt {
2510            Statement::Query(q) => {
2511                assert!(q.distinct);
2512                assert!(q.projection.is_some());
2513            }
2514            _ => panic!("expected query"),
2515        }
2516    }
2517
2518    #[test]
2519    fn test_parse_in_list() {
2520        let stmt = parse(r#"User filter .name in ("Alice", "Bob")"#).unwrap();
2521        match stmt {
2522            Statement::Query(q) => match q.filter.unwrap() {
2523                Expr::InList {
2524                    expr,
2525                    list,
2526                    negated,
2527                } => {
2528                    assert!(!negated);
2529                    assert!(matches!(*expr, Expr::Field(f) if f == "name"));
2530                    assert_eq!(list.len(), 2);
2531                }
2532                other => panic!("expected InList, got {other:?}"),
2533            },
2534            _ => panic!("expected query"),
2535        }
2536    }
2537
2538    #[test]
2539    fn test_parse_not_in_list() {
2540        let stmt = parse("User filter .age not in (1, 2, 3)").unwrap();
2541        match stmt {
2542            Statement::Query(q) => match q.filter.unwrap() {
2543                Expr::InList { negated, list, .. } => {
2544                    assert!(negated);
2545                    assert_eq!(list.len(), 3);
2546                }
2547                other => panic!("expected InList, got {other:?}"),
2548            },
2549            _ => panic!("expected query"),
2550        }
2551    }
2552
2553    #[test]
2554    fn test_parse_between() {
2555        // BETWEEN desugars into >= AND <=.
2556        let stmt = parse("User filter .age between 10 and 20").unwrap();
2557        match stmt {
2558            Statement::Query(q) => {
2559                match q.filter.unwrap() {
2560                    Expr::BinaryOp(_, BinOp::And, _) => {} // desugared
2561                    other => panic!("expected And (desugared between), got {other:?}"),
2562                }
2563            }
2564            _ => panic!("expected query"),
2565        }
2566    }
2567
2568    #[test]
2569    fn test_parse_not_between() {
2570        // NOT BETWEEN desugars into < OR >.
2571        let stmt = parse("User filter .age not between 10 and 20").unwrap();
2572        match stmt {
2573            Statement::Query(q) => {
2574                match q.filter.unwrap() {
2575                    Expr::BinaryOp(_, BinOp::Or, _) => {} // desugared
2576                    other => panic!("expected Or (desugared not between), got {other:?}"),
2577                }
2578            }
2579            _ => panic!("expected query"),
2580        }
2581    }
2582
2583    #[test]
2584    fn test_parse_like() {
2585        let stmt = parse(r#"User filter .name like "A%""#).unwrap();
2586        match stmt {
2587            Statement::Query(q) => match q.filter.unwrap() {
2588                Expr::BinaryOp(l, BinOp::Like, r) => {
2589                    assert!(matches!(*l, Expr::Field(f) if f == "name"));
2590                    assert!(matches!(*r, Expr::Literal(Literal::String(s)) if s == "A%"));
2591                }
2592                other => panic!("expected Like, got {other:?}"),
2593            },
2594            _ => panic!("expected query"),
2595        }
2596    }
2597
2598    #[test]
2599    fn test_parse_not_like() {
2600        let stmt = parse(r#"User filter .name not like "A%""#).unwrap();
2601        match stmt {
2602            Statement::Query(q) => match q.filter.unwrap() {
2603                Expr::UnaryOp(UnaryOp::Not, inner) => {
2604                    assert!(matches!(*inner, Expr::BinaryOp(_, BinOp::Like, _)));
2605                }
2606                other => panic!("expected Not(Like), got {other:?}"),
2607            },
2608            _ => panic!("expected query"),
2609        }
2610    }
2611
2612    // ---- Mission E2b: GROUP BY + HAVING ------------------------------------
2613
2614    #[test]
2615    fn test_parse_group_by_single_key() {
2616        let stmt = parse("User group .status { .status, n: count(.name) }").unwrap();
2617        match stmt {
2618            Statement::Query(q) => {
2619                let gb = q.group_by.unwrap();
2620                assert_eq!(gb.keys, vec![GroupKey::Unqualified("status".into())]);
2621                assert!(gb.having.is_none());
2622                let proj = q.projection.unwrap();
2623                assert_eq!(proj.len(), 2);
2624                assert!(matches!(
2625                    &proj[1].expr,
2626                    Expr::FunctionCall(AggFunc::Count, _)
2627                ));
2628                assert_eq!(proj[1].alias.as_deref(), Some("n"));
2629            }
2630            _ => panic!("expected query"),
2631        }
2632    }
2633
2634    #[test]
2635    fn test_parse_group_by_multi_key() {
2636        let stmt = parse("User group .status, .age { .status, .age }").unwrap();
2637        match stmt {
2638            Statement::Query(q) => {
2639                let gb = q.group_by.unwrap();
2640                assert_eq!(
2641                    gb.keys,
2642                    vec![
2643                        GroupKey::Unqualified("status".into()),
2644                        GroupKey::Unqualified("age".into())
2645                    ]
2646                );
2647            }
2648            _ => panic!("expected query"),
2649        }
2650    }
2651
2652    #[test]
2653    fn test_parse_group_by_having() {
2654        let stmt = parse("User group .status having count(.name) > 1 { .status }").unwrap();
2655        match stmt {
2656            Statement::Query(q) => {
2657                let gb = q.group_by.unwrap();
2658                assert_eq!(gb.keys, vec![GroupKey::Unqualified("status".into())]);
2659                assert!(gb.having.is_some());
2660                // HAVING is `count(.name) > 1` — BinaryOp(FunctionCall, Gt, Literal)
2661                match gb.having.unwrap() {
2662                    Expr::BinaryOp(l, BinOp::Gt, _) => {
2663                        assert!(matches!(*l, Expr::FunctionCall(AggFunc::Count, _)));
2664                    }
2665                    other => panic!("expected BinaryOp, got {other:?}"),
2666                }
2667            }
2668            _ => panic!("expected query"),
2669        }
2670    }
2671
2672    #[test]
2673    fn test_parse_aggregate_in_projection() {
2674        // Unaliased aggregate function calls in projection.
2675        let stmt = parse("User group .status { .status, count(.name), sum(.age) }").unwrap();
2676        match stmt {
2677            Statement::Query(q) => {
2678                let proj = q.projection.unwrap();
2679                assert_eq!(proj.len(), 3);
2680                assert!(matches!(
2681                    &proj[1].expr,
2682                    Expr::FunctionCall(AggFunc::Count, _)
2683                ));
2684                assert!(matches!(&proj[2].expr, Expr::FunctionCall(AggFunc::Sum, _)));
2685            }
2686            _ => panic!("expected query"),
2687        }
2688    }
2689
2690    #[test]
2691    fn test_parse_aggregate_in_aliased_projection() {
2692        let stmt = parse("User group .status { .status, total: count(.name), average: avg(.age) }")
2693            .unwrap();
2694        match stmt {
2695            Statement::Query(q) => {
2696                let proj = q.projection.unwrap();
2697                assert_eq!(proj[1].alias.as_deref(), Some("total"));
2698                assert!(matches!(
2699                    &proj[1].expr,
2700                    Expr::FunctionCall(AggFunc::Count, _)
2701                ));
2702                assert_eq!(proj[2].alias.as_deref(), Some("average"));
2703                assert!(matches!(&proj[2].expr, Expr::FunctionCall(AggFunc::Avg, _)));
2704            }
2705            _ => panic!("expected query"),
2706        }
2707    }
2708
2709    // ─── IS NULL / IS NOT NULL parser tests ────────────────────────────
2710
2711    #[test]
2712    fn test_parse_is_null() {
2713        let stmt = parse("User filter .age is null").unwrap();
2714        match stmt {
2715            Statement::Query(q) => {
2716                let filter = q.filter.unwrap();
2717                assert_eq!(
2718                    filter,
2719                    Expr::UnaryOp(UnaryOp::IsNull, Box::new(Expr::Field("age".into())))
2720                );
2721            }
2722            _ => panic!("expected query"),
2723        }
2724    }
2725
2726    #[test]
2727    fn test_parse_is_not_null() {
2728        let stmt = parse("User filter .age is not null").unwrap();
2729        match stmt {
2730            Statement::Query(q) => {
2731                let filter = q.filter.unwrap();
2732                assert_eq!(
2733                    filter,
2734                    Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(Expr::Field("age".into())))
2735                );
2736            }
2737            _ => panic!("expected query"),
2738        }
2739    }
2740
2741    #[test]
2742    fn test_parse_eq_null_desugars_to_is_null() {
2743        let stmt = parse("User filter .age = null").unwrap();
2744        match stmt {
2745            Statement::Query(q) => {
2746                let filter = q.filter.unwrap();
2747                assert_eq!(
2748                    filter,
2749                    Expr::UnaryOp(UnaryOp::IsNull, Box::new(Expr::Field("age".into())))
2750                );
2751            }
2752            _ => panic!("expected query"),
2753        }
2754    }
2755
2756    #[test]
2757    fn test_parse_neq_null_desugars_to_is_not_null() {
2758        let stmt = parse("User filter .age != null").unwrap();
2759        match stmt {
2760            Statement::Query(q) => {
2761                let filter = q.filter.unwrap();
2762                assert_eq!(
2763                    filter,
2764                    Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(Expr::Field("age".into())))
2765                );
2766            }
2767            _ => panic!("expected query"),
2768        }
2769    }
2770
2771    #[test]
2772    fn test_parse_null_comparisons_parse_ok() {
2773        // `< null`, `>= null` etc. parse successfully now that `null` is a
2774        // valid expression. At runtime they evaluate to Empty (no match),
2775        // which is correct null-propagation semantics.
2776        assert!(parse("User filter .age < null").is_ok());
2777        assert!(parse("User filter .age >= null").is_ok());
2778    }
2779
2780    #[test]
2781    fn test_parse_count_star_expr() {
2782        let stmt = parse("User filter count(*) > 0").unwrap();
2783        match stmt {
2784            Statement::Query(q) => {
2785                let filter = q.filter.unwrap();
2786                match filter {
2787                    Expr::BinaryOp(left, BinOp::Gt, _) => {
2788                        assert_eq!(
2789                            *left,
2790                            Expr::FunctionCall(AggFunc::Count, Box::new(Expr::Field("*".into())))
2791                        );
2792                    }
2793                    _ => panic!("expected comparison"),
2794                }
2795            }
2796            _ => panic!("expected query"),
2797        }
2798    }
2799
2800    // ─── String function parser tests ──────────────────────────────────
2801
2802    #[test]
2803    fn test_parse_upper_in_filter() {
2804        let stmt = parse(r#"User filter upper(.name) = "ALICE""#).unwrap();
2805        match stmt {
2806            Statement::Query(q) => {
2807                let f = q.filter.unwrap();
2808                match f {
2809                    Expr::BinaryOp(left, BinOp::Eq, _right) => {
2810                        assert!(matches!(*left, Expr::ScalarFunc(ScalarFn::Upper, _)));
2811                    }
2812                    _ => panic!("expected binary op with upper"),
2813                }
2814            }
2815            _ => panic!("expected query"),
2816        }
2817    }
2818
2819    #[test]
2820    fn test_parse_substring() {
2821        let stmt = parse("User { sub: substring(.name, 1, 3) }").unwrap();
2822        match stmt {
2823            Statement::Query(q) => {
2824                let proj = q.projection.unwrap();
2825                match &proj[0].expr {
2826                    Expr::ScalarFunc(ScalarFn::Substring, args) => {
2827                        assert_eq!(args.len(), 3);
2828                    }
2829                    other => panic!("expected ScalarFunc Substring, got {other:?}"),
2830                }
2831            }
2832            _ => panic!("expected query"),
2833        }
2834    }
2835
2836    #[test]
2837    fn test_parse_concat() {
2838        let stmt = parse(r#"User { full: concat(.name, " - ", .email) }"#).unwrap();
2839        match stmt {
2840            Statement::Query(q) => {
2841                let proj = q.projection.unwrap();
2842                match &proj[0].expr {
2843                    Expr::ScalarFunc(ScalarFn::Concat, args) => {
2844                        assert_eq!(args.len(), 3);
2845                    }
2846                    other => panic!("expected ScalarFunc Concat, got {other:?}"),
2847                }
2848            }
2849            _ => panic!("expected query"),
2850        }
2851    }
2852
2853    // ─── CASE WHEN parser tests ────────────────────────────────────────
2854
2855    #[test]
2856    fn test_parse_case_single_when() {
2857        let stmt = parse(r#"User filter case when .age > 30 then true else false end"#).unwrap();
2858        match stmt {
2859            Statement::Query(q) => {
2860                let filter = q.filter.unwrap();
2861                match filter {
2862                    Expr::Case { whens, else_expr } => {
2863                        assert_eq!(whens.len(), 1);
2864                        assert!(else_expr.is_some());
2865                    }
2866                    other => panic!("expected Case expr, got {other:?}"),
2867                }
2868            }
2869            _ => panic!("expected query"),
2870        }
2871    }
2872
2873    #[test]
2874    fn test_parse_case_multiple_whens() {
2875        let stmt = parse(
2876            r#"User { label: case when .age > 30 then "senior" when .age > 20 then "adult" else "young" end }"#
2877        ).unwrap();
2878        match stmt {
2879            Statement::Query(q) => {
2880                let proj = q.projection.unwrap();
2881                match &proj[0].expr {
2882                    Expr::Case { whens, else_expr } => {
2883                        assert_eq!(whens.len(), 2);
2884                        assert!(else_expr.is_some());
2885                    }
2886                    other => panic!("expected Case expr, got {other:?}"),
2887                }
2888            }
2889            _ => panic!("expected query"),
2890        }
2891    }
2892
2893    #[test]
2894    fn test_parse_case_without_else() {
2895        let stmt = parse(r#"User filter case when .age > 30 then true end"#).unwrap();
2896        match stmt {
2897            Statement::Query(q) => {
2898                let filter = q.filter.unwrap();
2899                match filter {
2900                    Expr::Case { whens, else_expr } => {
2901                        assert_eq!(whens.len(), 1);
2902                        assert!(else_expr.is_none());
2903                    }
2904                    other => panic!("expected Case expr, got {other:?}"),
2905                }
2906            }
2907            _ => panic!("expected query"),
2908        }
2909    }
2910
2911    // ─── Mul/Div expression tests (E2f) ───────────────────────────────
2912
2913    #[test]
2914    fn test_parse_mul_expr() {
2915        let stmt = parse("User filter .price * .quantity > 100").unwrap();
2916        match stmt {
2917            Statement::Query(q) => {
2918                let filter = q.filter.unwrap();
2919                match filter {
2920                    Expr::BinaryOp(left, BinOp::Gt, _) => match *left {
2921                        Expr::BinaryOp(_, BinOp::Mul, _) => {}
2922                        other => panic!("expected Mul, got {other:?}"),
2923                    },
2924                    other => panic!("expected BinaryOp Gt, got {other:?}"),
2925                }
2926            }
2927            _ => panic!("expected query"),
2928        }
2929    }
2930
2931    #[test]
2932    fn test_parse_div_expr() {
2933        let stmt = parse("User { ratio: .total / .count }").unwrap();
2934        match stmt {
2935            Statement::Query(q) => {
2936                let proj = q.projection.unwrap();
2937                assert_eq!(proj[0].alias.as_deref(), Some("ratio"));
2938                match &proj[0].expr {
2939                    Expr::BinaryOp(_, BinOp::Div, _) => {}
2940                    other => panic!("expected Div, got {other:?}"),
2941                }
2942            }
2943            _ => panic!("expected query"),
2944        }
2945    }
2946
2947    #[test]
2948    fn test_parse_mul_div_precedence() {
2949        // .a + .b * .c should parse as .a + (.b * .c)
2950        let stmt = parse("User filter .a + .b * .c > 0").unwrap();
2951        match stmt {
2952            Statement::Query(q) => {
2953                let filter = q.filter.unwrap();
2954                match filter {
2955                    Expr::BinaryOp(left, BinOp::Gt, _) => match *left {
2956                        Expr::BinaryOp(_, BinOp::Add, right) => {
2957                            assert!(matches!(*right, Expr::BinaryOp(_, BinOp::Mul, _)));
2958                        }
2959                        other => panic!("expected Add, got {other:?}"),
2960                    },
2961                    other => panic!("expected Gt, got {other:?}"),
2962                }
2963            }
2964            _ => panic!("expected query"),
2965        }
2966    }
2967
2968    // ─── Multi-column ORDER BY tests (E2f) ────────────────────────────
2969
2970    #[test]
2971    fn test_parse_multi_order() {
2972        let stmt = parse("User order .name asc, .age desc").unwrap();
2973        match stmt {
2974            Statement::Query(q) => {
2975                let order = q.order.unwrap();
2976                assert_eq!(order.keys.len(), 2);
2977                assert_eq!(order.keys[0].field, "name");
2978                assert!(!order.keys[0].descending);
2979                assert_eq!(order.keys[1].field, "age");
2980                assert!(order.keys[1].descending);
2981            }
2982            _ => panic!("expected query"),
2983        }
2984    }
2985
2986    #[test]
2987    fn test_parse_order_default_asc() {
2988        let stmt = parse("User order .name").unwrap();
2989        match stmt {
2990            Statement::Query(q) => {
2991                let order = q.order.unwrap();
2992                assert_eq!(order.keys.len(), 1);
2993                assert!(!order.keys[0].descending);
2994            }
2995            _ => panic!("expected query"),
2996        }
2997    }
2998
2999    // ─── ALTER TABLE / DROP TABLE parser tests (E2g) ──────────────────
3000
3001    #[test]
3002    fn test_parse_alter_add_column() {
3003        let stmt = parse("alter User add column status: str").unwrap();
3004        match stmt {
3005            Statement::AlterTable(at) => {
3006                assert_eq!(at.table, "User");
3007                match at.action {
3008                    AlterAction::AddColumn {
3009                        name,
3010                        type_name,
3011                        required,
3012                    } => {
3013                        assert_eq!(name, "status");
3014                        assert_eq!(type_name, "str");
3015                        assert!(!required);
3016                    }
3017                    other => panic!("expected AddColumn, got {other:?}"),
3018                }
3019            }
3020            other => panic!("expected AlterTable, got {other:?}"),
3021        }
3022    }
3023
3024    #[test]
3025    fn test_parse_alter_add_required_column() {
3026        let stmt = parse("alter User add required status: str").unwrap();
3027        match stmt {
3028            Statement::AlterTable(at) => match at.action {
3029                AlterAction::AddColumn { required, .. } => assert!(required),
3030                other => panic!("expected AddColumn, got {other:?}"),
3031            },
3032            other => panic!("expected AlterTable, got {other:?}"),
3033        }
3034    }
3035
3036    #[test]
3037    fn test_parse_type_with_unique_modifier() {
3038        let stmt = parse("type User { required unique email: str, age: int }").unwrap();
3039        match stmt {
3040            Statement::CreateType(ct) => {
3041                assert!(ct.fields[0].required && ct.fields[0].unique);
3042                assert!(!ct.fields[1].unique);
3043            }
3044            other => panic!("expected CreateType, got {other:?}"),
3045        }
3046    }
3047
3048    #[test]
3049    fn test_parse_type_unique_before_required() {
3050        // Modifiers accepted in either order.
3051        let stmt = parse("type User { unique required email: str }").unwrap();
3052        match stmt {
3053            Statement::CreateType(ct) => {
3054                assert!(ct.fields[0].required && ct.fields[0].unique);
3055            }
3056            other => panic!("expected CreateType, got {other:?}"),
3057        }
3058    }
3059
3060    #[test]
3061    fn test_parse_alter_add_unique() {
3062        let stmt = parse("alter User add unique .email").unwrap();
3063        match stmt {
3064            Statement::AlterTable(at) => assert!(matches!(
3065                at.action,
3066                AlterAction::AddUnique { ref column, .. } if column == "email"
3067            )),
3068            other => panic!("expected AlterTable, got {other:?}"),
3069        }
3070    }
3071
3072    #[test]
3073    fn test_parse_alter_drop_column() {
3074        let stmt = parse("alter User drop column status").unwrap();
3075        match stmt {
3076            Statement::AlterTable(at) => {
3077                assert_eq!(at.table, "User");
3078                match at.action {
3079                    AlterAction::DropColumn { name, .. } => assert_eq!(name, "status"),
3080                    other => panic!("expected DropColumn, got {other:?}"),
3081                }
3082            }
3083            other => panic!("expected AlterTable, got {other:?}"),
3084        }
3085    }
3086
3087    #[test]
3088    fn test_parse_alter_drop_without_column_keyword() {
3089        let stmt = parse("alter User drop status").unwrap();
3090        match stmt {
3091            Statement::AlterTable(at) => match at.action {
3092                AlterAction::DropColumn { name, .. } => assert_eq!(name, "status"),
3093                other => panic!("expected DropColumn, got {other:?}"),
3094            },
3095            other => panic!("expected AlterTable, got {other:?}"),
3096        }
3097    }
3098
3099    #[test]
3100    fn test_parse_drop_table() {
3101        let stmt = parse("drop User").unwrap();
3102        match stmt {
3103            Statement::DropTable(dt) => assert_eq!(dt.table, "User"),
3104            other => panic!("expected DropTable, got {other:?}"),
3105        }
3106    }
3107
3108    // ─── IN subquery parser tests (E2h) ───────────────────────────────
3109
3110    #[test]
3111    fn test_parse_in_subquery() {
3112        let stmt = parse("User filter .name in (VIP { .name })").unwrap();
3113        match stmt {
3114            Statement::Query(q) => {
3115                let filter = q.filter.unwrap();
3116                match filter {
3117                    Expr::InSubquery {
3118                        expr,
3119                        subquery,
3120                        negated,
3121                    } => {
3122                        assert!(!negated);
3123                        assert!(matches!(*expr, Expr::Field(ref f) if f == "name"));
3124                        assert_eq!(subquery.source, "VIP");
3125                    }
3126                    other => panic!("expected InSubquery, got {other:?}"),
3127                }
3128            }
3129            _ => panic!("expected query"),
3130        }
3131    }
3132
3133    #[test]
3134    fn test_parse_not_in_subquery() {
3135        let stmt = parse("User filter .id not in (Order { .user_id })").unwrap();
3136        match stmt {
3137            Statement::Query(q) => match q.filter.unwrap() {
3138                Expr::InSubquery { negated, .. } => assert!(negated),
3139                other => panic!("expected InSubquery, got {other:?}"),
3140            },
3141            _ => panic!("expected query"),
3142        }
3143    }
3144
3145    #[test]
3146    fn test_parse_in_literal_list_still_works() {
3147        // Ensure existing IN (literal) parsing isn't broken
3148        let stmt = parse("User filter .age in (25, 30, 35)").unwrap();
3149        match stmt {
3150            Statement::Query(q) => match q.filter.unwrap() {
3151                Expr::InList { list, negated, .. } => {
3152                    assert!(!negated);
3153                    assert_eq!(list.len(), 3);
3154                }
3155                other => panic!("expected InList, got {other:?}"),
3156            },
3157            _ => panic!("expected query"),
3158        }
3159    }
3160
3161    // ---- Materialized view parser tests ------------------------------------
3162
3163    #[test]
3164    fn test_parse_create_view() {
3165        let stmt = parse("materialize OldUsers as User filter .age > 28").unwrap();
3166        match stmt {
3167            Statement::CreateView(cv) => {
3168                assert_eq!(cv.name, "OldUsers");
3169                assert_eq!(cv.query.source, "User");
3170                assert!(cv.query.filter.is_some());
3171                assert!(!cv.query_text.is_empty());
3172            }
3173            _ => panic!("expected CreateView"),
3174        }
3175    }
3176
3177    #[test]
3178    fn test_parse_create_view_with_projection() {
3179        let stmt = parse("materialize UserNames as User { .name }").unwrap();
3180        match stmt {
3181            Statement::CreateView(cv) => {
3182                assert_eq!(cv.name, "UserNames");
3183                assert!(cv.query.projection.is_some());
3184            }
3185            _ => panic!("expected CreateView"),
3186        }
3187    }
3188
3189    #[test]
3190    fn test_parse_refresh_view() {
3191        let stmt = parse("refresh OldUsers").unwrap();
3192        match stmt {
3193            Statement::RefreshView(rv) => {
3194                assert_eq!(rv.name, "OldUsers");
3195            }
3196            _ => panic!("expected RefreshView"),
3197        }
3198    }
3199
3200    #[test]
3201    fn test_parse_drop_view() {
3202        let stmt = parse("drop view OldUsers").unwrap();
3203        match stmt {
3204            Statement::DropView(dv) => {
3205                assert_eq!(dv.name, "OldUsers");
3206            }
3207            _ => panic!("expected DropView"),
3208        }
3209    }
3210
3211    #[test]
3212    fn test_parse_drop_table_still_works() {
3213        let stmt = parse("drop Users").unwrap();
3214        match stmt {
3215            Statement::DropTable(dt) => {
3216                assert_eq!(dt.table, "Users");
3217            }
3218            _ => panic!("expected DropTable"),
3219        }
3220    }
3221
3222    #[test]
3223    fn test_parse_union() {
3224        let stmt = parse("User union Order").unwrap();
3225        match stmt {
3226            Statement::Union(u) => {
3227                assert!(!u.all);
3228                match *u.left {
3229                    Statement::Query(_) => {}
3230                    _ => panic!("expected Query on left"),
3231                }
3232                match *u.right {
3233                    Statement::Query(_) => {}
3234                    _ => panic!("expected Query on right"),
3235                }
3236            }
3237            _ => panic!("expected Union"),
3238        }
3239    }
3240
3241    #[test]
3242    fn test_parse_union_all() {
3243        let stmt = parse("User union all Order").unwrap();
3244        match stmt {
3245            Statement::Union(u) => {
3246                assert!(u.all, "expected UNION ALL");
3247                match *u.left {
3248                    Statement::Query(_) => {}
3249                    _ => panic!("expected Query on left"),
3250                }
3251                match *u.right {
3252                    Statement::Query(_) => {}
3253                    _ => panic!("expected Query on right"),
3254                }
3255            }
3256            _ => panic!("expected Union"),
3257        }
3258    }
3259
3260    #[test]
3261    fn test_parse_union_chain() {
3262        // Left-associative: A union B union C => Union(Union(A, B), C)
3263        let stmt = parse("User union Order union Product").unwrap();
3264        match stmt {
3265            Statement::Union(outer) => {
3266                assert!(!outer.all);
3267                // Right side is Product
3268                match *outer.right {
3269                    Statement::Query(q) => assert_eq!(q.source, "Product"),
3270                    _ => panic!("expected Query(Product) on right"),
3271                }
3272                // Left side is Union(User, Order)
3273                match *outer.left {
3274                    Statement::Union(inner) => {
3275                        assert!(!inner.all);
3276                        match *inner.left {
3277                            Statement::Query(q) => assert_eq!(q.source, "User"),
3278                            _ => panic!("expected Query(User)"),
3279                        }
3280                        match *inner.right {
3281                            Statement::Query(q) => assert_eq!(q.source, "Order"),
3282                            _ => panic!("expected Query(Order)"),
3283                        }
3284                    }
3285                    _ => panic!("expected inner Union"),
3286                }
3287            }
3288            _ => panic!("expected Union"),
3289        }
3290    }
3291
3292    #[test]
3293    fn test_parse_union_with_filter() {
3294        let stmt = parse("User filter .age > 10 union Order filter .total > 50").unwrap();
3295        match stmt {
3296            Statement::Union(u) => {
3297                assert!(!u.all);
3298                // Both sides should be queries (the filter is part of each query)
3299                match *u.left {
3300                    Statement::Query(q) => {
3301                        assert_eq!(q.source, "User");
3302                        assert!(q.filter.is_some());
3303                    }
3304                    _ => panic!("expected Query on left"),
3305                }
3306                match *u.right {
3307                    Statement::Query(q) => {
3308                        assert_eq!(q.source, "Order");
3309                        assert!(q.filter.is_some());
3310                    }
3311                    _ => panic!("expected Query on right"),
3312                }
3313            }
3314            _ => panic!("expected Union"),
3315        }
3316    }
3317
3318    #[test]
3319    fn test_parse_count_distinct_standalone() {
3320        let stmt = parse("count(distinct User { .name })").unwrap();
3321        match stmt {
3322            Statement::Query(q) => {
3323                let agg = q.aggregation.unwrap();
3324                assert_eq!(agg.function, AggFunc::CountDistinct);
3325                assert_eq!(agg.field.as_deref(), Some("name"));
3326            }
3327            _ => panic!("expected Query"),
3328        }
3329    }
3330
3331    #[test]
3332    fn test_parse_count_distinct_in_projection() {
3333        let stmt = parse("User group .dept { .dept, count(distinct .name) }").unwrap();
3334        match stmt {
3335            Statement::Query(q) => {
3336                let proj = q.projection.unwrap();
3337                assert_eq!(proj.len(), 2);
3338                match &proj[1].expr {
3339                    Expr::FunctionCall(func, _) => {
3340                        assert_eq!(*func, AggFunc::CountDistinct);
3341                    }
3342                    _ => panic!("expected FunctionCall"),
3343                }
3344            }
3345            _ => panic!("expected Query"),
3346        }
3347    }
3348
3349    // ---- Window function parser tests ----------------------------------------
3350
3351    #[test]
3352    fn test_parse_window_row_number_order() {
3353        let stmt = parse("User { .name, rn: row_number() over (order .age) }").unwrap();
3354        match stmt {
3355            Statement::Query(q) => {
3356                let proj = q.projection.unwrap();
3357                assert_eq!(proj.len(), 2);
3358                assert_eq!(proj[1].alias.as_deref(), Some("rn"));
3359                match &proj[1].expr {
3360                    Expr::Window {
3361                        function,
3362                        args,
3363                        partition_by,
3364                        order_by,
3365                    } => {
3366                        assert_eq!(*function, WindowFunc::RowNumber);
3367                        assert!(args.is_empty());
3368                        assert!(partition_by.is_empty());
3369                        assert_eq!(order_by.len(), 1);
3370                        assert_eq!(order_by[0].field, "age");
3371                        assert!(!order_by[0].descending);
3372                    }
3373                    other => panic!("expected Window, got {other:?}"),
3374                }
3375            }
3376            _ => panic!("expected query"),
3377        }
3378    }
3379
3380    #[test]
3381    fn test_parse_window_sum_partition_order() {
3382        let stmt =
3383            parse("User { .name, s: sum(.salary) over (partition .dept order .salary) }").unwrap();
3384        match stmt {
3385            Statement::Query(q) => {
3386                let proj = q.projection.unwrap();
3387                assert_eq!(proj.len(), 2);
3388                assert_eq!(proj[1].alias.as_deref(), Some("s"));
3389                match &proj[1].expr {
3390                    Expr::Window {
3391                        function,
3392                        args,
3393                        partition_by,
3394                        order_by,
3395                    } => {
3396                        assert_eq!(*function, WindowFunc::Sum);
3397                        assert_eq!(args.len(), 1);
3398                        assert!(matches!(&args[0], Expr::Field(f) if f == "salary"));
3399                        assert_eq!(partition_by, &["dept"]);
3400                        assert_eq!(order_by.len(), 1);
3401                        assert_eq!(order_by[0].field, "salary");
3402                        assert!(!order_by[0].descending);
3403                    }
3404                    other => panic!("expected Window, got {other:?}"),
3405                }
3406            }
3407            _ => panic!("expected query"),
3408        }
3409    }
3410
3411    #[test]
3412    fn test_parse_window_rank_desc() {
3413        let stmt =
3414            parse("User { .dept, .salary, r: rank() over (partition .dept order .salary desc) }")
3415                .unwrap();
3416        match stmt {
3417            Statement::Query(q) => {
3418                let proj = q.projection.unwrap();
3419                assert_eq!(proj.len(), 3);
3420                match &proj[2].expr {
3421                    Expr::Window {
3422                        function,
3423                        partition_by,
3424                        order_by,
3425                        ..
3426                    } => {
3427                        assert_eq!(*function, WindowFunc::Rank);
3428                        assert_eq!(partition_by, &["dept"]);
3429                        assert_eq!(order_by.len(), 1);
3430                        assert!(order_by[0].descending);
3431                    }
3432                    other => panic!("expected Window, got {other:?}"),
3433                }
3434            }
3435            _ => panic!("expected query"),
3436        }
3437    }
3438
3439    #[test]
3440    fn test_parse_window_dense_rank() {
3441        let stmt = parse("User { .name, dr: dense_rank() over (order .score desc) }").unwrap();
3442        match stmt {
3443            Statement::Query(q) => {
3444                let proj = q.projection.unwrap();
3445                assert_eq!(proj.len(), 2);
3446                match &proj[1].expr {
3447                    Expr::Window { function, .. } => {
3448                        assert_eq!(*function, WindowFunc::DenseRank);
3449                    }
3450                    other => panic!("expected Window, got {other:?}"),
3451                }
3452            }
3453            _ => panic!("expected query"),
3454        }
3455    }
3456
3457    #[test]
3458    fn test_parse_sum_without_over_is_aggregate() {
3459        // sum(.salary) alone (no `over`) stays as FunctionCall, not Window.
3460        let stmt = parse("User group .dept { .dept, total: sum(.salary) }").unwrap();
3461        match stmt {
3462            Statement::Query(q) => {
3463                let proj = q.projection.unwrap();
3464                assert_eq!(proj.len(), 2);
3465                match &proj[1].expr {
3466                    Expr::FunctionCall(AggFunc::Sum, _) => {} // correct
3467                    other => panic!("expected FunctionCall(Sum), got {other:?}"),
3468                }
3469            }
3470            _ => panic!("expected query"),
3471        }
3472    }
3473
3474    #[test]
3475    fn test_nesting_depth_limit() {
3476        // Build a deeply nested parenthesized expression that exceeds MAX_NESTING_DEPTH.
3477        let mut query = String::from("User filter ");
3478        for _ in 0..70 {
3479            query.push('(');
3480        }
3481        query.push_str(".age > 1");
3482        for _ in 0..70 {
3483            query.push(')');
3484        }
3485        let result = parse(&query);
3486        assert!(result.is_err());
3487        let err = result.unwrap_err();
3488        assert!(
3489            err.message().contains("nesting depth"),
3490            "expected nesting depth error, got: {}",
3491            err.message()
3492        );
3493    }
3494
3495    #[test]
3496    fn test_unary_prefix_nesting_depth_limit() {
3497        // A long chain of `not` prefixes recurses through parse_primary
3498        // without passing through parse_expr's guard. It must error cleanly
3499        // at the depth limit instead of overflowing the stack.
3500        let query = String::from("User filter ") + &"not ".repeat(5000) + ".active";
3501        let result = parse(&query);
3502        assert!(result.is_err());
3503        let err = result.unwrap_err();
3504        assert!(
3505            err.message().contains("nesting depth"),
3506            "expected nesting depth error, got: {}",
3507            err.message()
3508        );
3509    }
3510
3511    #[test]
3512    fn test_moderate_nesting_succeeds() {
3513        // 10 levels of nesting should be fine.
3514        let mut query = String::from("User filter ");
3515        for _ in 0..10 {
3516            query.push('(');
3517        }
3518        query.push_str(".age > 1");
3519        for _ in 0..10 {
3520            query.push(')');
3521        }
3522        assert!(parse(&query).is_ok());
3523    }
3524
3525    /// Regression for issue #26: `fuzz_parser` crashed on the 3-byte input
3526    /// `nn{` — the projection loop consumed the Eof token and then indexed
3527    /// past the end of `tokens`. Must return an error instead.
3528    #[test]
3529    fn test_parse_fuzz_repro_projection_eof() {
3530        let err = parse("nn{").expect_err("unterminated projection must error, not panic");
3531        let _ = err.message();
3532    }
3533
3534    /// Regression for issue #26: `fuzz_roundtrip` tripped the same bug with
3535    /// the 2-byte input `z{`.
3536    #[test]
3537    fn test_parse_fuzz_repro_short_projection_eof() {
3538        let err = parse("z{").expect_err("unterminated projection must error, not panic");
3539        let _ = err.message();
3540    }
3541
3542    #[test]
3543    fn test_update_at_statement_start_gives_helpful_error() {
3544        let err =
3545            parse(r#"update User filter .name = "Alice" { age := 31 }"#).expect_err("should fail");
3546        let msg = err.message();
3547        assert!(
3548            msg.contains("pipeline syntax"),
3549            "error should mention pipeline syntax, got: {msg}"
3550        );
3551        assert!(
3552            msg.contains("update"),
3553            "error should mention 'update', got: {msg}"
3554        );
3555    }
3556
3557    #[test]
3558    fn test_delete_at_statement_start_gives_helpful_error() {
3559        let err = parse("delete User filter .age < 18").expect_err("should fail");
3560        let msg = err.message();
3561        assert!(
3562            msg.contains("pipeline syntax"),
3563            "error should mention pipeline syntax, got: {msg}"
3564        );
3565        assert!(
3566            msg.contains("delete"),
3567            "error should mention 'delete', got: {msg}"
3568        );
3569    }
3570}
3571
3572#[cfg(test)]
3573mod cleanup_parser_dx_tests {
3574    use super::*;
3575
3576    #[test]
3577    fn typoed_statement_keyword_gets_suggestion() {
3578        let err = parse("updat User set age = 1").unwrap_err();
3579        let msg = err.to_string();
3580        assert!(msg.contains("near token"), "{msg}");
3581        assert!(msg.contains("did you mean `update`"), "{msg}");
3582    }
3583}
3584
3585#[cfg(test)]
3586mod dogfood_dx_tests {
3587    use super::*;
3588
3589    // ── P-6: reserved words as column names ────────────────────────────
3590
3591    #[test]
3592    fn reserved_word_field_name_gives_actionable_error() {
3593        let err = parse("type Post { type: str }").unwrap_err();
3594        let msg = err.to_string();
3595        assert!(
3596            msg.contains("'type' is a reserved word")
3597                && msg.contains("field name")
3598                && msg.contains("quote it as `type`"),
3599            "unhelpful message: {msg}"
3600        );
3601    }
3602
3603    #[test]
3604    fn reserved_modifier_word_as_field_name_gives_actionable_error() {
3605        // `required` is a modifier keyword; followed directly by `:` it is
3606        // instead the field's (reserved) name — the old error was the opaque
3607        // "expected field name, got ':'".
3608        let err = parse("type Post { required: bool }").unwrap_err();
3609        let msg = err.to_string();
3610        assert!(
3611            msg.contains("'required' is a reserved word") && msg.contains("quote it as `required`"),
3612            "unhelpful message: {msg}"
3613        );
3614    }
3615
3616    #[test]
3617    fn reserved_word_in_insert_assignment_gives_actionable_error() {
3618        let err = parse(r#"insert Post { type := "x" }"#).unwrap_err();
3619        let msg = err.to_string();
3620        assert!(msg.contains("'type' is a reserved word"), "{msg}");
3621    }
3622
3623    #[test]
3624    fn reserved_word_in_alter_column_gives_actionable_error() {
3625        let err = parse("alter Post add column order: int").unwrap_err();
3626        let msg = err.to_string();
3627        assert!(msg.contains("'order' is a reserved word"), "{msg}");
3628    }
3629
3630    #[test]
3631    fn backtick_field_name_parses_as_identifier() {
3632        let stmt = parse("type Post { `type`: str, `order`: int }").unwrap();
3633        match stmt {
3634            Statement::CreateType(ct) => {
3635                assert_eq!(ct.fields[0].name, "type");
3636                assert_eq!(ct.fields[1].name, "order");
3637            }
3638            other => panic!("expected CreateType, got {other:?}"),
3639        }
3640    }
3641
3642    #[test]
3643    fn backtick_field_still_honors_modifiers() {
3644        let stmt = parse("type Post { required `type`: str }").unwrap();
3645        match stmt {
3646            Statement::CreateType(ct) => {
3647                assert_eq!(ct.fields[0].name, "type");
3648                assert!(ct.fields[0].required);
3649            }
3650            other => panic!("expected CreateType, got {other:?}"),
3651        }
3652    }
3653
3654    // ── P-7: DDL idempotency ───────────────────────────────────────────
3655
3656    #[test]
3657    fn create_type_if_not_exists_parses() {
3658        let stmt = parse("type Post if not exists { id: int }").unwrap();
3659        match stmt {
3660            Statement::CreateType(ct) => assert!(ct.if_not_exists),
3661            other => panic!("expected CreateType, got {other:?}"),
3662        }
3663    }
3664
3665    #[test]
3666    fn create_type_without_clause_defaults_false() {
3667        let stmt = parse("type Post { id: int }").unwrap();
3668        match stmt {
3669            Statement::CreateType(ct) => assert!(!ct.if_not_exists),
3670            other => panic!("expected CreateType, got {other:?}"),
3671        }
3672    }
3673
3674    #[test]
3675    fn drop_if_exists_parses() {
3676        match parse("drop if exists Post").unwrap() {
3677            Statement::DropTable(dt) => assert!(dt.if_exists),
3678            other => panic!("expected DropTable, got {other:?}"),
3679        }
3680        match parse("drop Post").unwrap() {
3681            Statement::DropTable(dt) => assert!(!dt.if_exists),
3682            other => panic!("expected DropTable, got {other:?}"),
3683        }
3684    }
3685
3686    #[test]
3687    fn drop_view_if_exists_parses() {
3688        match parse("drop view if exists ActiveUsers").unwrap() {
3689            Statement::DropView(dv) => {
3690                assert!(dv.if_exists);
3691                assert_eq!(dv.name, "ActiveUsers");
3692            }
3693            other => panic!("expected DropView, got {other:?}"),
3694        }
3695    }
3696
3697    #[test]
3698    fn add_index_and_unique_if_not_exists_parse() {
3699        match parse("alter Post add index if not exists .slug").unwrap() {
3700            Statement::AlterTable(at) => {
3701                assert!(matches!(
3702                    at.action,
3703                    AlterAction::AddIndex {
3704                        if_not_exists: true,
3705                        ..
3706                    }
3707                ));
3708            }
3709            other => panic!("expected AlterTable, got {other:?}"),
3710        }
3711        match parse("alter Post add unique if not exists .slug").unwrap() {
3712            Statement::AlterTable(at) => {
3713                assert!(matches!(
3714                    at.action,
3715                    AlterAction::AddUnique {
3716                        if_not_exists: true,
3717                        ..
3718                    }
3719                ));
3720            }
3721            other => panic!("expected AlterTable, got {other:?}"),
3722        }
3723    }
3724
3725    #[test]
3726    fn alter_drop_column_if_exists_parses() {
3727        match parse("alter Post drop column if exists status").unwrap() {
3728            Statement::AlterTable(at) => {
3729                assert!(matches!(
3730                    at.action,
3731                    AlterAction::DropColumn {
3732                        if_exists: true,
3733                        ..
3734                    }
3735                ));
3736            }
3737            other => panic!("expected AlterTable, got {other:?}"),
3738        }
3739    }
3740
3741    // ── P-8: introspection ─────────────────────────────────────────────
3742
3743    #[test]
3744    fn schema_parses_to_list_types() {
3745        assert_eq!(parse("schema").unwrap(), Statement::ListTypes);
3746    }
3747
3748    #[test]
3749    fn describe_parses_to_describe() {
3750        assert_eq!(
3751            parse("describe Post").unwrap(),
3752            Statement::Describe("Post".to_string())
3753        );
3754    }
3755
3756    #[test]
3757    fn schema_with_type_aliases_describe() {
3758        assert_eq!(
3759            parse("schema Post").unwrap(),
3760            Statement::Describe("Post".to_string())
3761        );
3762    }
3763}