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, .field2 [having <expr>]`.
1238    fn parse_group_by(&mut self) -> Result<GroupByClause, ParseError> {
1239        let mut keys = Vec::new();
1240        while let Token::DotIdent(name) = self.peek() {
1241            let name = name.clone();
1242            self.advance();
1243            keys.push(name);
1244            if *self.peek() == Token::Comma {
1245                self.advance();
1246            } else {
1247                break;
1248            }
1249        }
1250        if keys.is_empty() {
1251            return Err(ParseError::Syntax {
1252                message: "expected at least one .field after group".into(),
1253            });
1254        }
1255        let having = if *self.peek() == Token::Having {
1256            self.advance();
1257            Some(self.parse_expr()?)
1258        } else {
1259            None
1260        };
1261        Ok(GroupByClause { keys, having })
1262    }
1263
1264    fn parse_additive(&mut self) -> Result<Expr, ParseError> {
1265        let mut left = self.parse_multiplicative()?;
1266        loop {
1267            let op = match self.peek() {
1268                Token::Plus => BinOp::Add,
1269                Token::Minus => BinOp::Sub,
1270                Token::Coalesce => {
1271                    self.advance();
1272                    let right = self.parse_multiplicative()?;
1273                    left = Expr::Coalesce(Box::new(left), Box::new(right));
1274                    continue;
1275                }
1276                _ => break,
1277            };
1278            self.advance();
1279            let right = self.parse_multiplicative()?;
1280            left = Expr::BinaryOp(Box::new(left), op, Box::new(right));
1281        }
1282        Ok(left)
1283    }
1284
1285    fn parse_multiplicative(&mut self) -> Result<Expr, ParseError> {
1286        let mut left = self.parse_primary()?;
1287        loop {
1288            let op = match self.peek() {
1289                Token::Star => BinOp::Mul,
1290                Token::Slash => BinOp::Div,
1291                _ => break,
1292            };
1293            self.advance();
1294            let right = self.parse_primary()?;
1295            left = Expr::BinaryOp(Box::new(left), op, Box::new(right));
1296        }
1297        Ok(left)
1298    }
1299
1300    fn parse_primary(&mut self) -> Result<Expr, ParseError> {
1301        // Guard recursion here too: unary prefixes (`not`, `exists`, `not exists`)
1302        // recurse straight back into parse_primary without going through parse_expr,
1303        // so a chain like `not not … .x` would otherwise overflow the stack (process
1304        // abort under panic=abort). See test_unary_prefix_nesting_depth_limit.
1305        self.depth += 1;
1306        if self.depth > MAX_NESTING_DEPTH {
1307            self.depth -= 1;
1308            return Err(ParseError::NestingDepthExceeded {
1309                max: MAX_NESTING_DEPTH,
1310            });
1311        }
1312        let result = self.parse_primary_inner();
1313        self.depth -= 1;
1314        result
1315    }
1316
1317    fn parse_primary_inner(&mut self) -> Result<Expr, ParseError> {
1318        match self.peek().clone() {
1319            Token::DotIdent(name) => {
1320                self.advance();
1321                Ok(Expr::Field(name))
1322            }
1323            Token::IntLit(v) => {
1324                self.advance();
1325                Ok(Expr::Literal(Literal::Int(v)))
1326            }
1327            Token::FloatLit(v) => {
1328                self.advance();
1329                Ok(Expr::Literal(Literal::Float(v)))
1330            }
1331            Token::StringLit(v) => {
1332                self.advance();
1333                Ok(Expr::Literal(Literal::String(v)))
1334            }
1335            Token::BoolLit(v) => {
1336                self.advance();
1337                Ok(Expr::Literal(Literal::Bool(v)))
1338            }
1339            // `$N` placeholders are only valid through
1340            // `parse_with_params`, which substitutes them for literal
1341            // tokens before this expression parser ever runs. Reaching a
1342            // raw `Token::Param` here means the caller used the plain
1343            // (no-params) path with a placeholder — surface the standard
1344            // unexpected-token error so the message names the parameter.
1345            Token::Null => {
1346                self.advance();
1347                Ok(Expr::Null)
1348            }
1349            Token::Not => {
1350                self.advance();
1351                if *self.peek() == Token::Exists {
1352                    self.advance();
1353                    // `not exists (Q)` → ExistsSubquery{ negated: true } when
1354                    // followed by `( Ident ...` (subquery form). Otherwise
1355                    // fall back to the scalar `is not null` unary op.
1356                    if let Some(sub) = self.try_parse_exists_subquery()? {
1357                        return Ok(Expr::ExistsSubquery {
1358                            subquery: Box::new(sub),
1359                            negated: true,
1360                        });
1361                    }
1362                    let expr = self.parse_primary()?;
1363                    Ok(Expr::UnaryOp(UnaryOp::NotExists, Box::new(expr)))
1364                } else {
1365                    let expr = self.parse_primary()?;
1366                    Ok(Expr::UnaryOp(UnaryOp::Not, Box::new(expr)))
1367                }
1368            }
1369            Token::Exists => {
1370                self.advance();
1371                // `exists (Q)` → ExistsSubquery when followed by a
1372                // parenthesised query. Scalar `exists .field` still parses
1373                // as UnaryOp::Exists for backwards compatibility.
1374                if let Some(sub) = self.try_parse_exists_subquery()? {
1375                    return Ok(Expr::ExistsSubquery {
1376                        subquery: Box::new(sub),
1377                        negated: false,
1378                    });
1379                }
1380                let expr = self.parse_primary()?;
1381                Ok(Expr::UnaryOp(UnaryOp::Exists, Box::new(expr)))
1382            }
1383            Token::LParen => {
1384                self.advance();
1385                let expr = self.parse_expr()?;
1386                self.expect(&Token::RParen)?;
1387                Ok(expr)
1388            }
1389            Token::Ident(name) => {
1390                self.advance();
1391                // `uuid("…")` / `bytes("…")` cast sugar. `uuid`/`bytes` are not
1392                // lexer keywords (so `type T { id: uuid }` and identifiers named
1393                // `uuid` are untouched); an `Ident` immediately followed by `(`
1394                // with a matching name is single-argument cast sugar.
1395                if *self.peek() == Token::LParen {
1396                    let cast_type = match name.as_str() {
1397                        "uuid" => Some(CastType::Uuid),
1398                        "bytes" => Some(CastType::Bytes),
1399                        _ => None,
1400                    };
1401                    if let Some(cast_type) = cast_type {
1402                        self.advance(); // consume `(`
1403                        let inner = self.parse_expr()?;
1404                        self.expect(&Token::RParen)?;
1405                        return Ok(Expr::Cast(Box::new(inner), cast_type));
1406                    }
1407                }
1408                // `alias.field` → QualifiedField. The lexer emits `t1.name` as
1409                // `Ident("t1")` + `DotIdent("name")` (see lexer.rs line 30),
1410                // so a trailing DotIdent here means a qualified reference.
1411                if let Token::DotIdent(field) = self.peek().clone() {
1412                    self.advance();
1413                    return Ok(Expr::QualifiedField {
1414                        qualifier: name,
1415                        field,
1416                    });
1417                }
1418                Ok(Expr::Field(name))
1419            }
1420            // Window-only functions: row_number(), rank(), dense_rank()
1421            Token::RowNumber | Token::Rank | Token::DenseRank => {
1422                let wfunc = match self.advance() {
1423                    Token::RowNumber => WindowFunc::RowNumber,
1424                    Token::Rank => WindowFunc::Rank,
1425                    Token::DenseRank => WindowFunc::DenseRank,
1426                    _ => {
1427                        return Err(ParseError::Syntax {
1428                            message: "unexpected window function token".into(),
1429                        })
1430                    }
1431                };
1432                self.expect(&Token::LParen)?;
1433                self.expect(&Token::RParen)?;
1434                let (partition_by, order_by) = self.parse_over_clause()?;
1435                Ok(Expr::Window {
1436                    function: wfunc,
1437                    args: vec![],
1438                    partition_by,
1439                    order_by,
1440                })
1441            }
1442            // Aggregate function calls inside expressions (projections, HAVING).
1443            // Top-level `count(User)` still routes through parse_aggregate_query
1444            // in parse_statement; this arm handles `count(.id)`, `sum(.age)`, etc.
1445            Token::Count | Token::Avg | Token::Sum | Token::Min | Token::Max => {
1446                let mut func = match self.advance() {
1447                    Token::Count => AggFunc::Count,
1448                    Token::Avg => AggFunc::Avg,
1449                    Token::Sum => AggFunc::Sum,
1450                    Token::Min => AggFunc::Min,
1451                    Token::Max => AggFunc::Max,
1452                    _ => {
1453                        return Err(ParseError::Syntax {
1454                            message: "unexpected aggregate token".into(),
1455                        })
1456                    }
1457                };
1458                self.expect(&Token::LParen)?;
1459                // count(*) — count all rows including nulls
1460                if func == AggFunc::Count && *self.peek() == Token::Star {
1461                    self.advance();
1462                    self.expect(&Token::RParen)?;
1463                    // Check for OVER — count(*) over (...)
1464                    if *self.peek() == Token::Over {
1465                        let (partition_by, order_by) = self.parse_over_clause()?;
1466                        return Ok(Expr::Window {
1467                            function: WindowFunc::Count,
1468                            args: vec![Expr::Field("*".into())],
1469                            partition_by,
1470                            order_by,
1471                        });
1472                    }
1473                    return Ok(Expr::FunctionCall(
1474                        AggFunc::Count,
1475                        Box::new(Expr::Field("*".into())),
1476                    ));
1477                }
1478                // count(distinct .field) → CountDistinct
1479                if func == AggFunc::Count && *self.peek() == Token::Distinct {
1480                    self.advance();
1481                    func = AggFunc::CountDistinct;
1482                }
1483                let inner = self.parse_expr()?;
1484                self.expect(&Token::RParen)?;
1485                // Check for OVER — e.g. sum(.salary) over (...)
1486                if *self.peek() == Token::Over {
1487                    let wfunc = match func {
1488                        AggFunc::Count => WindowFunc::Count,
1489                        AggFunc::Avg => WindowFunc::Avg,
1490                        AggFunc::Sum => WindowFunc::Sum,
1491                        AggFunc::Min => WindowFunc::Min,
1492                        AggFunc::Max => WindowFunc::Max,
1493                        _ => {
1494                            return Err(ParseError::Unsupported {
1495                                feature: "count(distinct ...) over (...) is not supported".into(),
1496                            })
1497                        }
1498                    };
1499                    let (partition_by, order_by) = self.parse_over_clause()?;
1500                    return Ok(Expr::Window {
1501                        function: wfunc,
1502                        args: vec![inner],
1503                        partition_by,
1504                        order_by,
1505                    });
1506                }
1507                Ok(Expr::FunctionCall(func, Box::new(inner)))
1508            }
1509            Token::Upper
1510            | Token::Lower
1511            | Token::Length
1512            | Token::Trim
1513            | Token::Substring
1514            | Token::Concat
1515            | Token::Abs
1516            | Token::Round
1517            | Token::Ceil
1518            | Token::Floor
1519            | Token::Sqrt
1520            | Token::Pow
1521            | Token::Now
1522            | Token::Extract
1523            | Token::DateAdd
1524            | Token::DateDiff => {
1525                let tok = self.advance();
1526                let func = token_to_scalar_fn(&tok);
1527                self.expect(&Token::LParen)?;
1528                let mut args = Vec::new();
1529                while !matches!(self.peek(), Token::RParen | Token::Eof) {
1530                    args.push(self.parse_expr()?);
1531                    if *self.peek() == Token::Comma {
1532                        self.advance();
1533                    }
1534                }
1535                self.expect(&Token::RParen)?;
1536                Ok(Expr::ScalarFunc(func, args))
1537            }
1538            Token::Cast => {
1539                self.advance();
1540                self.expect(&Token::LParen)?;
1541                let inner = self.parse_expr()?;
1542                self.expect(&Token::Comma)?;
1543                let cast_type = self.parse_cast_type()?;
1544                self.expect(&Token::RParen)?;
1545                Ok(Expr::Cast(Box::new(inner), cast_type))
1546            }
1547            Token::Case => {
1548                self.advance();
1549                let mut whens = Vec::new();
1550                while *self.peek() == Token::When {
1551                    self.advance();
1552                    let condition = self.parse_expr()?;
1553                    self.expect(&Token::Then)?;
1554                    let result = self.parse_expr()?;
1555                    whens.push((Box::new(condition), Box::new(result)));
1556                }
1557                let else_expr = if *self.peek() == Token::Else {
1558                    self.advance();
1559                    Some(Box::new(self.parse_expr()?))
1560                } else {
1561                    None
1562                };
1563                self.expect(&Token::End)?;
1564                Ok(Expr::Case { whens, else_expr })
1565            }
1566            t => Err(ParseError::Syntax {
1567                message: format!("unexpected token in expression: {}", t.display_name()),
1568            }),
1569        }
1570    }
1571
1572    /// `alter <Table> add [column] [required] <name>: <type>`
1573    /// `alter <Table> drop [column] <name>`
1574    fn parse_alter_table(&mut self) -> Result<Statement, ParseError> {
1575        self.expect(&Token::Alter)?;
1576        let table = match self.advance() {
1577            Token::Ident(name) => name,
1578            t => {
1579                return Err(ParseError::UnexpectedToken {
1580                    expected: "table name after alter".into(),
1581                    got: t.display_name(),
1582                })
1583            }
1584        };
1585        match self.peek() {
1586            Token::Add => {
1587                self.advance();
1588                // `alter <Table> add index [if not exists] .<column>`
1589                if *self.peek() == Token::Index {
1590                    self.advance();
1591                    let if_not_exists = self.parse_optional_if_not_exists();
1592                    let column = match self.advance() {
1593                        Token::DotIdent(n) => n,
1594                        t => {
1595                            return Err(ParseError::UnexpectedToken {
1596                                expected: ".<column> after add index".into(),
1597                                got: t.display_name(),
1598                            })
1599                        }
1600                    };
1601                    return Ok(Statement::AlterTable(AlterTableExpr {
1602                        table,
1603                        action: AlterAction::AddIndex {
1604                            column,
1605                            if_not_exists,
1606                        },
1607                    }));
1608                }
1609                // `alter <Table> add unique [if not exists] .<column>`
1610                if *self.peek() == Token::Unique {
1611                    self.advance();
1612                    let if_not_exists = self.parse_optional_if_not_exists();
1613                    let column = match self.advance() {
1614                        Token::DotIdent(n) => n,
1615                        t => {
1616                            return Err(ParseError::UnexpectedToken {
1617                                expected: ".<column> after add unique".into(),
1618                                got: t.display_name(),
1619                            })
1620                        }
1621                    };
1622                    return Ok(Statement::AlterTable(AlterTableExpr {
1623                        table,
1624                        action: AlterAction::AddUnique {
1625                            column,
1626                            if_not_exists,
1627                        },
1628                    }));
1629                }
1630                // optional `column` keyword
1631                if *self.peek() == Token::Column {
1632                    self.advance();
1633                }
1634                let required = if *self.peek() == Token::Required {
1635                    self.advance();
1636                    true
1637                } else {
1638                    false
1639                };
1640                let name = self.expect_named_ident("column name")?;
1641                self.expect(&Token::Colon)?;
1642                let type_name = match self.advance() {
1643                    Token::Ident(n) => n,
1644                    t => {
1645                        return Err(ParseError::UnexpectedToken {
1646                            expected: "type name".into(),
1647                            got: t.display_name(),
1648                        })
1649                    }
1650                };
1651                Ok(Statement::AlterTable(AlterTableExpr {
1652                    table,
1653                    action: AlterAction::AddColumn {
1654                        name,
1655                        type_name,
1656                        required,
1657                    },
1658                }))
1659            }
1660            Token::Drop => {
1661                self.advance();
1662                // optional `column` keyword
1663                if *self.peek() == Token::Column {
1664                    self.advance();
1665                }
1666                let if_exists = self.parse_optional_if_exists();
1667                let name = self.expect_named_ident("column name")?;
1668                Ok(Statement::AlterTable(AlterTableExpr {
1669                    table,
1670                    action: AlterAction::DropColumn { name, if_exists },
1671                }))
1672            }
1673            t => Err(ParseError::UnexpectedToken {
1674                expected: "add or drop after alter <table>".into(),
1675                got: t.display_name(),
1676            }),
1677        }
1678    }
1679
1680    /// `drop [if exists] <Table>` or `drop view [if exists] <ViewName>`
1681    fn parse_drop_or_drop_view(&mut self) -> Result<Statement, ParseError> {
1682        self.expect(&Token::Drop)?;
1683        if *self.peek() == Token::View {
1684            self.advance(); // consume `view`
1685            let if_exists = self.parse_optional_if_exists();
1686            let name = match self.advance() {
1687                Token::Ident(name) => name,
1688                t => {
1689                    return Err(ParseError::UnexpectedToken {
1690                        expected: "view name after drop view".into(),
1691                        got: t.display_name(),
1692                    })
1693                }
1694            };
1695            return Ok(Statement::DropView(DropViewExpr { name, if_exists }));
1696        }
1697        let if_exists = self.parse_optional_if_exists();
1698        let table = match self.advance() {
1699            Token::Ident(name) => name,
1700            t => {
1701                return Err(ParseError::UnexpectedToken {
1702                    expected: "table name after drop".into(),
1703                    got: t.display_name(),
1704                })
1705            }
1706        };
1707        Ok(Statement::DropTable(DropTableExpr { table, if_exists }))
1708    }
1709
1710    /// `materialize <ViewName> as <Query>`
1711    ///
1712    /// The source query text is captured by slicing the original token stream
1713    /// from the position after `as` to the end.
1714    fn parse_create_view(&mut self) -> Result<Statement, ParseError> {
1715        self.expect(&Token::Materialized)?;
1716        let name = match self.advance() {
1717            Token::Ident(name) => name,
1718            t => {
1719                return Err(ParseError::UnexpectedToken {
1720                    expected: "view name after materialize".into(),
1721                    got: t.display_name(),
1722                })
1723            }
1724        };
1725        self.expect(&Token::As)?;
1726        // Record position so we can reconstruct the query text for storage.
1727        let query_start = self.pos;
1728        let source = match self.advance() {
1729            Token::Ident(s) => s,
1730            t => {
1731                return Err(ParseError::UnexpectedToken {
1732                    expected: "source table name".into(),
1733                    got: t.display_name(),
1734                })
1735            }
1736        };
1737        let query = self.parse_query_tail(source)?;
1738        // Reconstruct query text from tokens for storage and re-execution.
1739        let query_text = tokens_to_text(&self.tokens[query_start..self.pos]);
1740        Ok(Statement::CreateView(CreateViewExpr {
1741            name,
1742            query,
1743            query_text,
1744        }))
1745    }
1746
1747    /// Check for `union [all]` after a query and build a left-associative
1748    /// chain if present.
1749    fn maybe_parse_union(&mut self, left: Statement) -> Result<Statement, ParseError> {
1750        if *self.peek() != Token::Union {
1751            return Ok(left);
1752        }
1753        if !matches!(left, Statement::Query(_) | Statement::Union(_)) {
1754            return Err(ParseError::Syntax {
1755                message: "UNION requires a query on the left side".into(),
1756            });
1757        }
1758        self.advance(); // consume `union`
1759        let all = if let Token::Ident(s) = self.peek() {
1760            if s == "all" {
1761                self.advance();
1762                true
1763            } else {
1764                false
1765            }
1766        } else {
1767            false
1768        };
1769        // Parse the RHS as a single query (not chained — we'll chain ourselves).
1770        let right = self.parse_single_query()?;
1771        let union = Statement::Union(UnionExpr {
1772            left: Box::new(left),
1773            right: Box::new(right),
1774            all,
1775        });
1776        // Recursively check for further chaining: `A union B union C`
1777        self.maybe_parse_union(union)
1778    }
1779
1780    /// Parse a single query statement (no UNION chaining). Used for UNION RHS.
1781    fn parse_single_query(&mut self) -> Result<Statement, ParseError> {
1782        match self.peek() {
1783            Token::Count | Token::Avg | Token::Sum | Token::Min | Token::Max => {
1784                self.parse_aggregate_query()
1785            }
1786            Token::Ident(_) => self.parse_query_or_mutation(),
1787            _ => Err(ParseError::Syntax {
1788                message: format!(
1789                    "expected query after UNION, got {}",
1790                    self.peek().display_name()
1791                ),
1792            }),
1793        }
1794    }
1795
1796    /// `refresh <ViewName>`
1797    fn parse_refresh_view(&mut self) -> Result<Statement, ParseError> {
1798        self.expect(&Token::Refresh)?;
1799        let name = match self.advance() {
1800            Token::Ident(name) => name,
1801            t => {
1802                return Err(ParseError::UnexpectedToken {
1803                    expected: "view name after refresh".into(),
1804                    got: t.display_name(),
1805                })
1806            }
1807        };
1808        Ok(Statement::RefreshView(RefreshViewExpr { name }))
1809    }
1810
1811    fn parse_create_type(&mut self) -> Result<Statement, ParseError> {
1812        self.expect(&Token::Type)?;
1813        let name = self.expect_named_ident("type name")?;
1814        let if_not_exists = self.parse_optional_if_not_exists();
1815        self.expect(&Token::LBrace)?;
1816        let mut fields = Vec::new();
1817        while !matches!(self.peek(), Token::RBrace | Token::Eof) {
1818            // Accept `required`, `unique`, and `auto` modifiers in any order.
1819            // A modifier keyword immediately followed by `:` is instead the
1820            // field's *name* (e.g. `required: int`) — leave it for
1821            // `expect_named_ident`, which emits the reserved-word guidance.
1822            let (mut required, mut unique, mut auto) = (false, false, false);
1823            loop {
1824                let is_modifier =
1825                    matches!(self.peek(), Token::Required | Token::Unique | Token::Auto)
1826                        && !matches!(self.tokens.get(self.pos + 1), Some(Token::Colon));
1827                if !is_modifier {
1828                    break;
1829                }
1830                match self.advance() {
1831                    Token::Required => required = true,
1832                    Token::Unique => unique = true,
1833                    Token::Auto => auto = true,
1834                    _ => unreachable!("guarded by is_modifier"),
1835                }
1836            }
1837            let field_name = self.expect_named_ident("field name")?;
1838            self.expect(&Token::Colon)?;
1839            let type_name = match self.advance() {
1840                Token::Ident(n) => n,
1841                t => {
1842                    return Err(ParseError::UnexpectedToken {
1843                        expected: "type name".into(),
1844                        got: t.display_name(),
1845                    })
1846                }
1847            };
1848            // Optional `default <literal>` — value applied when an insert
1849            // omits this column.
1850            let default = if *self.peek() == Token::Default {
1851                self.advance();
1852                Some(self.parse_default_literal()?)
1853            } else {
1854                None
1855            };
1856            fields.push(FieldDef {
1857                name: field_name,
1858                type_name,
1859                required,
1860                unique,
1861                default,
1862                auto,
1863            });
1864            if *self.peek() == Token::Comma {
1865                self.advance();
1866            }
1867        }
1868        self.expect(&Token::RBrace)?;
1869        Ok(Statement::CreateType(CreateTypeExpr {
1870            name,
1871            fields,
1872            if_not_exists,
1873        }))
1874    }
1875
1876    /// `schema` — list all types. `schema <Type>` is an alias for
1877    /// `describe <Type>`.
1878    fn parse_schema(&mut self) -> Result<Statement, ParseError> {
1879        self.expect(&Token::Schema)?;
1880        if let Token::Ident(_) = self.peek() {
1881            let table = self.expect_named_ident("type name")?;
1882            return Ok(Statement::Describe(table));
1883        }
1884        Ok(Statement::ListTypes)
1885    }
1886
1887    /// `describe <Type>` — the columns and indexes of one type.
1888    fn parse_describe(&mut self) -> Result<Statement, ParseError> {
1889        self.expect(&Token::Describe)?;
1890        let table = self.expect_named_ident("type name")?;
1891        Ok(Statement::Describe(table))
1892    }
1893
1894    /// Parse the literal following a `default` column modifier. Only scalar
1895    /// literals are allowed — expression defaults (e.g. `now()`) are not yet
1896    /// supported.
1897    fn parse_default_literal(&mut self) -> Result<Literal, ParseError> {
1898        match self.advance() {
1899            Token::IntLit(v) => Ok(Literal::Int(v)),
1900            Token::FloatLit(v) => Ok(Literal::Float(v)),
1901            Token::StringLit(v) => Ok(Literal::String(v)),
1902            Token::BoolLit(v) => Ok(Literal::Bool(v)),
1903            t => Err(ParseError::UnexpectedToken {
1904                expected: "literal default value".into(),
1905                got: t.display_name(),
1906            }),
1907        }
1908    }
1909}
1910
1911/// Reconstruct PowQL source text from a slice of tokens. Used to store the
1912/// view's source query for re-execution on refresh. Not perfectly
1913/// round-trippable (whitespace is normalised) but semantically identical.
1914fn tokens_to_text(tokens: &[Token]) -> String {
1915    let mut out = String::with_capacity(64);
1916    for tok in tokens {
1917        if !out.is_empty() && !matches!(tok, Token::Eof) {
1918            out.push(' ');
1919        }
1920        match tok {
1921            Token::Ident(s) => out.push_str(s),
1922            Token::DotIdent(s) => {
1923                out.push('.');
1924                out.push_str(s);
1925            }
1926            Token::IntLit(v) => out.push_str(&v.to_string()),
1927            Token::FloatLit(v) => out.push_str(&v.to_string()),
1928            Token::StringLit(s) => {
1929                out.push('"');
1930                out.push_str(s);
1931                out.push('"');
1932            }
1933            Token::BoolLit(v) => out.push_str(if *v { "true" } else { "false" }),
1934            Token::Param(s) => {
1935                out.push('$');
1936                out.push_str(s);
1937            }
1938            Token::Type => out.push_str("type"),
1939            Token::Filter => out.push_str("filter"),
1940            Token::Order => out.push_str("order"),
1941            Token::Limit => out.push_str("limit"),
1942            Token::Offset => out.push_str("offset"),
1943            Token::Insert => out.push_str("insert"),
1944            Token::Update => out.push_str("update"),
1945            Token::Delete => out.push_str("delete"),
1946            Token::Upsert => out.push_str("upsert"),
1947            Token::Returning => out.push_str("returning"),
1948            Token::Conflict => out.push_str("conflict"),
1949            Token::Select => out.push_str("select"),
1950            Token::Required => out.push_str("required"),
1951            Token::Default => out.push_str("default"),
1952            Token::Auto => out.push_str("auto"),
1953            Token::Multi => out.push_str("multi"),
1954            Token::Link => out.push_str("link"),
1955            Token::Index => out.push_str("index"),
1956            Token::Unique => out.push_str("unique"),
1957            Token::On => out.push_str("on"),
1958            Token::Asc => out.push_str("asc"),
1959            Token::Desc => out.push_str("desc"),
1960            Token::And => out.push_str("and"),
1961            Token::Or => out.push_str("or"),
1962            Token::Not => out.push_str("not"),
1963            Token::Exists => out.push_str("exists"),
1964            Token::Let => out.push_str("let"),
1965            Token::As => out.push_str("as"),
1966            Token::Match => out.push_str("match"),
1967            Token::Group => out.push_str("group"),
1968            Token::Join => out.push_str("join"),
1969            Token::Inner => out.push_str("inner"),
1970            Token::LeftKw => out.push_str("left"),
1971            Token::RightKw => out.push_str("right"),
1972            Token::Outer => out.push_str("outer"),
1973            Token::Cross => out.push_str("cross"),
1974            Token::Transaction => out.push_str("transaction"),
1975            Token::Begin => out.push_str("begin"),
1976            Token::Commit => out.push_str("commit"),
1977            Token::Rollback => out.push_str("rollback"),
1978            Token::View => out.push_str("view"),
1979            Token::Materialized => out.push_str("materialized"),
1980            Token::Refresh => out.push_str("refresh"),
1981            Token::Union => out.push_str("union"),
1982            Token::Having => out.push_str("having"),
1983            Token::Distinct => out.push_str("distinct"),
1984            Token::In => out.push_str("in"),
1985            Token::Between => out.push_str("between"),
1986            Token::Like => out.push_str("like"),
1987            Token::Count => out.push_str("count"),
1988            Token::Avg => out.push_str("avg"),
1989            Token::Sum => out.push_str("sum"),
1990            Token::Min => out.push_str("min"),
1991            Token::Max => out.push_str("max"),
1992            Token::Is => out.push_str("is"),
1993            Token::Null => out.push_str("null"),
1994            Token::Upper => out.push_str("upper"),
1995            Token::Lower => out.push_str("lower"),
1996            Token::Length => out.push_str("length"),
1997            Token::Trim => out.push_str("trim"),
1998            Token::Substring => out.push_str("substring"),
1999            Token::Concat => out.push_str("concat"),
2000            Token::Abs => out.push_str("abs"),
2001            Token::Round => out.push_str("round"),
2002            Token::Ceil => out.push_str("ceil"),
2003            Token::Floor => out.push_str("floor"),
2004            Token::Sqrt => out.push_str("sqrt"),
2005            Token::Pow => out.push_str("pow"),
2006            Token::Now => out.push_str("now"),
2007            Token::Extract => out.push_str("extract"),
2008            Token::DateAdd => out.push_str("date_add"),
2009            Token::DateDiff => out.push_str("date_diff"),
2010            Token::Cast => out.push_str("cast"),
2011            Token::Case => out.push_str("case"),
2012            Token::When => out.push_str("when"),
2013            Token::Then => out.push_str("then"),
2014            Token::Else => out.push_str("else"),
2015            Token::End => out.push_str("end"),
2016            Token::Over => out.push_str("over"),
2017            Token::Partition => out.push_str("partition"),
2018            Token::RowNumber => out.push_str("row_number"),
2019            Token::Rank => out.push_str("rank"),
2020            Token::DenseRank => out.push_str("dense_rank"),
2021            Token::Alter => out.push_str("alter"),
2022            Token::Drop => out.push_str("drop"),
2023            Token::Add => out.push_str("add"),
2024            Token::Column => out.push_str("column"),
2025            Token::Eq => out.push('='),
2026            Token::Neq => out.push_str("!="),
2027            Token::Lt => out.push('<'),
2028            Token::Gt => out.push('>'),
2029            Token::Lte => out.push_str("<="),
2030            Token::Gte => out.push_str(">="),
2031            Token::Assign => out.push_str(":="),
2032            Token::Arrow => out.push_str("->"),
2033            Token::Pipe => out.push('|'),
2034            Token::Coalesce => out.push_str("??"),
2035            Token::Plus => out.push('+'),
2036            Token::Minus => out.push('-'),
2037            Token::Star => out.push('*'),
2038            Token::Slash => out.push('/'),
2039            Token::LBrace => out.push('{'),
2040            Token::RBrace => out.push('}'),
2041            Token::LParen => out.push('('),
2042            Token::RParen => out.push(')'),
2043            Token::Comma => out.push(','),
2044            Token::Colon => out.push(':'),
2045            Token::Dot => out.push('.'),
2046            Token::Explain => out.push_str("explain"),
2047            Token::Schema => out.push_str("schema"),
2048            Token::Describe => out.push_str("describe"),
2049            Token::Eof => {}
2050        }
2051    }
2052    out
2053}
2054
2055#[cfg(test)]
2056mod tests {
2057    use super::*;
2058    #[test]
2059    fn test_parse_simple_query() {
2060        let stmt = parse("User").unwrap();
2061        match stmt {
2062            Statement::Query(q) => {
2063                assert_eq!(q.source, "User");
2064                assert!(q.filter.is_none());
2065                assert!(q.projection.is_none());
2066            }
2067            _ => panic!("expected query"),
2068        }
2069    }
2070
2071    #[test]
2072    fn test_parse_filter() {
2073        let stmt = parse("User filter .age > 30").unwrap();
2074        match stmt {
2075            Statement::Query(q) => {
2076                assert_eq!(q.source, "User");
2077                assert!(q.filter.is_some());
2078            }
2079            _ => panic!("expected query"),
2080        }
2081    }
2082
2083    #[test]
2084    fn test_parse_projection() {
2085        let stmt = parse("User { name, email }").unwrap();
2086        match stmt {
2087            Statement::Query(q) => {
2088                let proj = q.projection.unwrap();
2089                assert_eq!(proj.len(), 2);
2090            }
2091            _ => panic!("expected query"),
2092        }
2093    }
2094
2095    #[test]
2096    fn test_parse_filter_order_limit() {
2097        let stmt = parse("User filter .age > 30 order .name desc limit 10").unwrap();
2098        match stmt {
2099            Statement::Query(q) => {
2100                assert!(q.filter.is_some());
2101                let order = q.order.unwrap();
2102                assert_eq!(order.keys.len(), 1);
2103                assert_eq!(order.keys[0].field, "name");
2104                assert!(order.keys[0].descending);
2105                assert!(q.limit.is_some());
2106            }
2107            _ => panic!("expected query"),
2108        }
2109    }
2110
2111    #[test]
2112    fn test_parse_insert() {
2113        let stmt = parse(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
2114        match stmt {
2115            Statement::Insert(ins) => {
2116                assert_eq!(ins.target, "User");
2117                assert_eq!(ins.rows.len(), 1);
2118                assert_eq!(ins.rows[0].len(), 2);
2119                assert_eq!(ins.rows[0][0].field, "name");
2120                assert_eq!(ins.rows[0][1].field, "age");
2121            }
2122            _ => panic!("expected insert"),
2123        }
2124    }
2125
2126    #[test]
2127    fn test_parse_insert_multi_row() {
2128        let stmt =
2129            parse(r#"insert User { name := "Alice", age := 30 }, { name := "Bob", age := 25 }, { name := "Cy" }"#)
2130                .unwrap();
2131        match stmt {
2132            Statement::Insert(ins) => {
2133                assert_eq!(ins.target, "User");
2134                assert_eq!(ins.rows.len(), 3);
2135                assert_eq!(ins.rows[0].len(), 2);
2136                assert_eq!(ins.rows[1][0].field, "name");
2137                assert_eq!(ins.rows[2].len(), 1);
2138                assert_eq!(ins.rows[2][0].field, "name");
2139            }
2140            _ => panic!("expected insert"),
2141        }
2142    }
2143
2144    #[test]
2145    fn test_parse_update() {
2146        let stmt = parse(r#"User filter .email = "alice@ex.com" update { age := 31 }"#).unwrap();
2147        match stmt {
2148            Statement::UpdateQuery(upd) => {
2149                assert_eq!(upd.source, "User");
2150                assert!(upd.filter.is_some());
2151                assert_eq!(upd.assignments.len(), 1);
2152                assert!(!upd.returning);
2153            }
2154            _ => panic!("expected update"),
2155        }
2156    }
2157
2158    #[test]
2159    fn test_parse_update_returning() {
2160        let stmt = parse(r#"User filter .name = "Alice" update { age := 31 } returning"#).unwrap();
2161        match stmt {
2162            Statement::UpdateQuery(upd) => assert!(upd.returning),
2163            _ => panic!("expected update"),
2164        }
2165    }
2166
2167    #[test]
2168    fn test_parse_delete() {
2169        let stmt = parse("User filter .age < 18 delete").unwrap();
2170        match stmt {
2171            Statement::DeleteQuery(del) => {
2172                assert_eq!(del.source, "User");
2173                assert!(del.filter.is_some());
2174                assert!(!del.returning);
2175            }
2176            _ => panic!("expected delete"),
2177        }
2178    }
2179
2180    #[test]
2181    fn test_parse_delete_returning() {
2182        let stmt = parse("User filter .age < 18 delete returning").unwrap();
2183        match stmt {
2184            Statement::DeleteQuery(del) => assert!(del.returning),
2185            _ => panic!("expected delete"),
2186        }
2187    }
2188
2189    #[test]
2190    fn test_parse_count() {
2191        let stmt = parse("count(User)").unwrap();
2192        match stmt {
2193            Statement::Query(q) => {
2194                let agg = q.aggregation.unwrap();
2195                assert_eq!(agg.function, AggFunc::Count);
2196                assert!(q.filter.is_none());
2197            }
2198            _ => panic!("expected query with aggregation"),
2199        }
2200    }
2201
2202    #[test]
2203    fn test_parse_count_with_filter() {
2204        // Regression: previously returned "expected RParen, got Filter".
2205        // count(<query>) must accept a full read-pipeline tail.
2206        let stmt = parse("count(User filter .age > 30)").unwrap();
2207        match stmt {
2208            Statement::Query(q) => {
2209                assert_eq!(q.source, "User");
2210                let agg = q.aggregation.unwrap();
2211                assert_eq!(agg.function, AggFunc::Count);
2212                assert!(q.filter.is_some(), "filter should have been parsed");
2213            }
2214            _ => panic!("expected query with aggregation"),
2215        }
2216    }
2217
2218    #[test]
2219    fn test_parse_count_with_filter_and_limit() {
2220        let stmt = parse("count(User filter .age > 30 limit 100)").unwrap();
2221        match stmt {
2222            Statement::Query(q) => {
2223                assert_eq!(q.source, "User");
2224                assert!(q.filter.is_some());
2225                assert!(q.limit.is_some());
2226                assert_eq!(q.aggregation.unwrap().function, AggFunc::Count);
2227            }
2228            _ => panic!("expected query with aggregation"),
2229        }
2230    }
2231
2232    #[test]
2233    fn test_parse_create_type() {
2234        let stmt = parse("type User { required name: str, age: int }").unwrap();
2235        match stmt {
2236            Statement::CreateType(ct) => {
2237                assert_eq!(ct.name, "User");
2238                assert_eq!(ct.fields.len(), 2);
2239                assert!(ct.fields[0].required);
2240                assert!(!ct.fields[1].required);
2241            }
2242            _ => panic!("expected create type"),
2243        }
2244    }
2245
2246    #[test]
2247    fn test_parse_sum_with_field_projection() {
2248        // `sum(... { .age })` should lift `.age` into AggregateExpr.field and
2249        // clear the projection so the executor's aggregate fast path fires.
2250        let stmt = parse("sum(User filter .age > 30 { .age })").unwrap();
2251        match stmt {
2252            Statement::Query(q) => {
2253                let agg = q.aggregation.expect("aggregate");
2254                assert_eq!(agg.function, AggFunc::Sum);
2255                assert_eq!(agg.field.as_deref(), Some("age"));
2256                assert!(
2257                    q.projection.is_none(),
2258                    "projection should be lifted into agg.field"
2259                );
2260            }
2261            _ => panic!("expected query"),
2262        }
2263    }
2264
2265    #[test]
2266    fn test_parse_avg_min_max_with_field() {
2267        for (src, expected) in [
2268            ("avg(User { .age })", AggFunc::Avg),
2269            ("min(User { .age })", AggFunc::Min),
2270            ("max(User { .age })", AggFunc::Max),
2271        ] {
2272            let stmt = parse(src).unwrap();
2273            match stmt {
2274                Statement::Query(q) => {
2275                    let agg = q.aggregation.unwrap();
2276                    assert_eq!(agg.function, expected, "func mismatch for {src}");
2277                    assert_eq!(
2278                        agg.field.as_deref(),
2279                        Some("age"),
2280                        "field mismatch for {src}"
2281                    );
2282                    assert!(
2283                        q.projection.is_none(),
2284                        "projection should be cleared for {src}"
2285                    );
2286                }
2287                _ => panic!("expected query for {src}"),
2288            }
2289        }
2290    }
2291
2292    #[test]
2293    fn test_parse_count_leaves_projection_alone() {
2294        // count() doesn't need a target field, so the projection (if any)
2295        // stays intact. It's silly to project inside a count, but it's legal.
2296        let stmt = parse("count(User { .age })").unwrap();
2297        match stmt {
2298            Statement::Query(q) => {
2299                let agg = q.aggregation.unwrap();
2300                assert_eq!(agg.function, AggFunc::Count);
2301                assert!(agg.field.is_none());
2302                assert!(q.projection.is_some(), "count must not eat projection");
2303            }
2304            _ => panic!("expected query"),
2305        }
2306    }
2307
2308    // ---- Mission E1.1: JOIN parser tests ----------------------------------
2309    // Parser-level only. The planner rejects joins with a clean error until
2310    // E1.2 wires up execution.
2311
2312    #[test]
2313    fn test_parse_source_alias() {
2314        let stmt = parse("User as u filter u.age > 30").unwrap();
2315        match stmt {
2316            Statement::Query(q) => {
2317                assert_eq!(q.source, "User");
2318                assert_eq!(q.alias.as_deref(), Some("u"));
2319                assert!(q.joins.is_empty());
2320                match q.filter.unwrap() {
2321                    Expr::BinaryOp(l, BinOp::Gt, _) => match *l {
2322                        Expr::QualifiedField { qualifier, field } => {
2323                            assert_eq!(qualifier, "u");
2324                            assert_eq!(field, "age");
2325                        }
2326                        other => panic!("expected qualified field, got {other:?}"),
2327                    },
2328                    other => panic!("expected >, got {other:?}"),
2329                }
2330            }
2331            _ => panic!("expected query"),
2332        }
2333    }
2334
2335    #[test]
2336    fn test_parse_inner_join_on() {
2337        let stmt = parse("User as u inner join Order as o on u.id = o.user_id").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_eq!(q.joins.len(), 1);
2343                let j = &q.joins[0];
2344                assert_eq!(j.kind, JoinKind::Inner);
2345                assert_eq!(j.source, "Order");
2346                assert_eq!(j.alias.as_deref(), Some("o"));
2347                let on = j.on.as_ref().expect("on clause");
2348                match on {
2349                    Expr::BinaryOp(l, BinOp::Eq, r) => {
2350                        assert!(matches!(**l, Expr::QualifiedField { .. }));
2351                        assert!(matches!(**r, Expr::QualifiedField { .. }));
2352                    }
2353                    other => panic!("expected eq, got {other:?}"),
2354                }
2355            }
2356            _ => panic!("expected query"),
2357        }
2358    }
2359
2360    #[test]
2361    fn test_parse_bare_join_defaults_to_inner() {
2362        let stmt = parse("User join Order on User.id = Order.user_id").unwrap();
2363        match stmt {
2364            Statement::Query(q) => {
2365                assert_eq!(q.joins.len(), 1);
2366                assert_eq!(q.joins[0].kind, JoinKind::Inner);
2367            }
2368            _ => panic!("expected query"),
2369        }
2370    }
2371
2372    #[test]
2373    fn test_parse_left_outer_join() {
2374        let stmt = parse("User as u left outer join Order as o on u.id = o.user_id").unwrap();
2375        match stmt {
2376            Statement::Query(q) => {
2377                assert_eq!(q.joins.len(), 1);
2378                assert_eq!(q.joins[0].kind, JoinKind::LeftOuter);
2379            }
2380            _ => panic!("expected query"),
2381        }
2382    }
2383
2384    #[test]
2385    fn test_parse_left_join_without_outer_keyword() {
2386        // `left join` is shorthand for `left outer join` in SQL — we accept it.
2387        let stmt = parse("User as u left join Order as o on u.id = o.user_id").unwrap();
2388        match stmt {
2389            Statement::Query(q) => {
2390                assert_eq!(q.joins[0].kind, JoinKind::LeftOuter);
2391            }
2392            _ => panic!("expected query"),
2393        }
2394    }
2395
2396    #[test]
2397    fn test_parse_right_join() {
2398        let stmt = parse("User as u right join Order as o on u.id = o.user_id").unwrap();
2399        match stmt {
2400            Statement::Query(q) => {
2401                assert_eq!(q.joins[0].kind, JoinKind::RightOuter);
2402            }
2403            _ => panic!("expected query"),
2404        }
2405    }
2406
2407    #[test]
2408    fn test_parse_cross_join_has_no_on() {
2409        let stmt = parse("User cross join Order").unwrap();
2410        match stmt {
2411            Statement::Query(q) => {
2412                assert_eq!(q.joins[0].kind, JoinKind::Cross);
2413                assert!(q.joins[0].on.is_none());
2414            }
2415            _ => panic!("expected query"),
2416        }
2417    }
2418
2419    #[test]
2420    fn test_parse_multi_join_chain() {
2421        let stmt = parse(
2422            "User as u join Order as o on u.id = o.user_id \
2423             join Product as p on o.product_id = p.id",
2424        )
2425        .unwrap();
2426        match stmt {
2427            Statement::Query(q) => {
2428                assert_eq!(q.joins.len(), 2);
2429                assert_eq!(q.joins[0].source, "Order");
2430                assert_eq!(q.joins[1].source, "Product");
2431            }
2432            _ => panic!("expected query"),
2433        }
2434    }
2435
2436    #[test]
2437    fn test_parse_join_with_filter_tail() {
2438        // Filter/order/limit still work after a join clause.
2439        let stmt = parse(
2440            "User as u join Order as o on u.id = o.user_id \
2441             filter o.total > 100 order .name limit 10",
2442        )
2443        .unwrap();
2444        match stmt {
2445            Statement::Query(q) => {
2446                assert_eq!(q.joins.len(), 1);
2447                assert!(q.filter.is_some());
2448                assert!(q.order.is_some());
2449                assert!(q.limit.is_some());
2450            }
2451            _ => panic!("expected query"),
2452        }
2453    }
2454
2455    #[test]
2456    fn test_parse_join_requires_on_for_inner() {
2457        // Non-cross joins require `on <expr>`. Missing `on` is a parse error.
2458        let err = parse("User join Order").unwrap_err();
2459        assert!(
2460            err.message().contains("on"),
2461            "expected on-clause error, got {:?}",
2462            err.message()
2463        );
2464    }
2465
2466    #[test]
2467    fn test_parse_update_on_joined_query_errors() {
2468        // E1.1 explicitly rejects update/delete on joined queries — SQL
2469        // semantics here are messy and we're not implementing them yet.
2470        let err =
2471            parse("User as u join Order as o on u.id = o.user_id update { age := 1 }").unwrap_err();
2472        assert!(err.message().contains("update"));
2473    }
2474
2475    #[test]
2476    fn test_parse_delete_on_joined_query_errors() {
2477        let err = parse("User as u join Order as o on u.id = o.user_id delete").unwrap_err();
2478        assert!(err.message().contains("delete"));
2479    }
2480
2481    // ---- Mission E2a: DISTINCT + IN-list + BETWEEN + LIKE -----------------
2482
2483    #[test]
2484    fn test_parse_distinct() {
2485        let stmt = parse("User distinct { .name }").unwrap();
2486        match stmt {
2487            Statement::Query(q) => {
2488                assert!(q.distinct);
2489                assert!(q.projection.is_some());
2490            }
2491            _ => panic!("expected query"),
2492        }
2493    }
2494
2495    #[test]
2496    fn test_parse_in_list() {
2497        let stmt = parse(r#"User filter .name in ("Alice", "Bob")"#).unwrap();
2498        match stmt {
2499            Statement::Query(q) => match q.filter.unwrap() {
2500                Expr::InList {
2501                    expr,
2502                    list,
2503                    negated,
2504                } => {
2505                    assert!(!negated);
2506                    assert!(matches!(*expr, Expr::Field(f) if f == "name"));
2507                    assert_eq!(list.len(), 2);
2508                }
2509                other => panic!("expected InList, got {other:?}"),
2510            },
2511            _ => panic!("expected query"),
2512        }
2513    }
2514
2515    #[test]
2516    fn test_parse_not_in_list() {
2517        let stmt = parse("User filter .age not in (1, 2, 3)").unwrap();
2518        match stmt {
2519            Statement::Query(q) => match q.filter.unwrap() {
2520                Expr::InList { negated, list, .. } => {
2521                    assert!(negated);
2522                    assert_eq!(list.len(), 3);
2523                }
2524                other => panic!("expected InList, got {other:?}"),
2525            },
2526            _ => panic!("expected query"),
2527        }
2528    }
2529
2530    #[test]
2531    fn test_parse_between() {
2532        // BETWEEN desugars into >= AND <=.
2533        let stmt = parse("User filter .age between 10 and 20").unwrap();
2534        match stmt {
2535            Statement::Query(q) => {
2536                match q.filter.unwrap() {
2537                    Expr::BinaryOp(_, BinOp::And, _) => {} // desugared
2538                    other => panic!("expected And (desugared between), got {other:?}"),
2539                }
2540            }
2541            _ => panic!("expected query"),
2542        }
2543    }
2544
2545    #[test]
2546    fn test_parse_not_between() {
2547        // NOT BETWEEN desugars into < OR >.
2548        let stmt = parse("User filter .age not between 10 and 20").unwrap();
2549        match stmt {
2550            Statement::Query(q) => {
2551                match q.filter.unwrap() {
2552                    Expr::BinaryOp(_, BinOp::Or, _) => {} // desugared
2553                    other => panic!("expected Or (desugared not between), got {other:?}"),
2554                }
2555            }
2556            _ => panic!("expected query"),
2557        }
2558    }
2559
2560    #[test]
2561    fn test_parse_like() {
2562        let stmt = parse(r#"User filter .name like "A%""#).unwrap();
2563        match stmt {
2564            Statement::Query(q) => match q.filter.unwrap() {
2565                Expr::BinaryOp(l, BinOp::Like, r) => {
2566                    assert!(matches!(*l, Expr::Field(f) if f == "name"));
2567                    assert!(matches!(*r, Expr::Literal(Literal::String(s)) if s == "A%"));
2568                }
2569                other => panic!("expected Like, got {other:?}"),
2570            },
2571            _ => panic!("expected query"),
2572        }
2573    }
2574
2575    #[test]
2576    fn test_parse_not_like() {
2577        let stmt = parse(r#"User filter .name not like "A%""#).unwrap();
2578        match stmt {
2579            Statement::Query(q) => match q.filter.unwrap() {
2580                Expr::UnaryOp(UnaryOp::Not, inner) => {
2581                    assert!(matches!(*inner, Expr::BinaryOp(_, BinOp::Like, _)));
2582                }
2583                other => panic!("expected Not(Like), got {other:?}"),
2584            },
2585            _ => panic!("expected query"),
2586        }
2587    }
2588
2589    // ---- Mission E2b: GROUP BY + HAVING ------------------------------------
2590
2591    #[test]
2592    fn test_parse_group_by_single_key() {
2593        let stmt = parse("User group .status { .status, n: count(.name) }").unwrap();
2594        match stmt {
2595            Statement::Query(q) => {
2596                let gb = q.group_by.unwrap();
2597                assert_eq!(gb.keys, vec!["status"]);
2598                assert!(gb.having.is_none());
2599                let proj = q.projection.unwrap();
2600                assert_eq!(proj.len(), 2);
2601                assert!(matches!(
2602                    &proj[1].expr,
2603                    Expr::FunctionCall(AggFunc::Count, _)
2604                ));
2605                assert_eq!(proj[1].alias.as_deref(), Some("n"));
2606            }
2607            _ => panic!("expected query"),
2608        }
2609    }
2610
2611    #[test]
2612    fn test_parse_group_by_multi_key() {
2613        let stmt = parse("User group .status, .age { .status, .age }").unwrap();
2614        match stmt {
2615            Statement::Query(q) => {
2616                let gb = q.group_by.unwrap();
2617                assert_eq!(gb.keys, vec!["status", "age"]);
2618            }
2619            _ => panic!("expected query"),
2620        }
2621    }
2622
2623    #[test]
2624    fn test_parse_group_by_having() {
2625        let stmt = parse("User group .status having count(.name) > 1 { .status }").unwrap();
2626        match stmt {
2627            Statement::Query(q) => {
2628                let gb = q.group_by.unwrap();
2629                assert_eq!(gb.keys, vec!["status"]);
2630                assert!(gb.having.is_some());
2631                // HAVING is `count(.name) > 1` — BinaryOp(FunctionCall, Gt, Literal)
2632                match gb.having.unwrap() {
2633                    Expr::BinaryOp(l, BinOp::Gt, _) => {
2634                        assert!(matches!(*l, Expr::FunctionCall(AggFunc::Count, _)));
2635                    }
2636                    other => panic!("expected BinaryOp, got {other:?}"),
2637                }
2638            }
2639            _ => panic!("expected query"),
2640        }
2641    }
2642
2643    #[test]
2644    fn test_parse_aggregate_in_projection() {
2645        // Unaliased aggregate function calls in projection.
2646        let stmt = parse("User group .status { .status, count(.name), sum(.age) }").unwrap();
2647        match stmt {
2648            Statement::Query(q) => {
2649                let proj = q.projection.unwrap();
2650                assert_eq!(proj.len(), 3);
2651                assert!(matches!(
2652                    &proj[1].expr,
2653                    Expr::FunctionCall(AggFunc::Count, _)
2654                ));
2655                assert!(matches!(&proj[2].expr, Expr::FunctionCall(AggFunc::Sum, _)));
2656            }
2657            _ => panic!("expected query"),
2658        }
2659    }
2660
2661    #[test]
2662    fn test_parse_aggregate_in_aliased_projection() {
2663        let stmt = parse("User group .status { .status, total: count(.name), average: avg(.age) }")
2664            .unwrap();
2665        match stmt {
2666            Statement::Query(q) => {
2667                let proj = q.projection.unwrap();
2668                assert_eq!(proj[1].alias.as_deref(), Some("total"));
2669                assert!(matches!(
2670                    &proj[1].expr,
2671                    Expr::FunctionCall(AggFunc::Count, _)
2672                ));
2673                assert_eq!(proj[2].alias.as_deref(), Some("average"));
2674                assert!(matches!(&proj[2].expr, Expr::FunctionCall(AggFunc::Avg, _)));
2675            }
2676            _ => panic!("expected query"),
2677        }
2678    }
2679
2680    // ─── IS NULL / IS NOT NULL parser tests ────────────────────────────
2681
2682    #[test]
2683    fn test_parse_is_null() {
2684        let stmt = parse("User filter .age is null").unwrap();
2685        match stmt {
2686            Statement::Query(q) => {
2687                let filter = q.filter.unwrap();
2688                assert_eq!(
2689                    filter,
2690                    Expr::UnaryOp(UnaryOp::IsNull, Box::new(Expr::Field("age".into())))
2691                );
2692            }
2693            _ => panic!("expected query"),
2694        }
2695    }
2696
2697    #[test]
2698    fn test_parse_is_not_null() {
2699        let stmt = parse("User filter .age is not null").unwrap();
2700        match stmt {
2701            Statement::Query(q) => {
2702                let filter = q.filter.unwrap();
2703                assert_eq!(
2704                    filter,
2705                    Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(Expr::Field("age".into())))
2706                );
2707            }
2708            _ => panic!("expected query"),
2709        }
2710    }
2711
2712    #[test]
2713    fn test_parse_eq_null_desugars_to_is_null() {
2714        let stmt = parse("User filter .age = null").unwrap();
2715        match stmt {
2716            Statement::Query(q) => {
2717                let filter = q.filter.unwrap();
2718                assert_eq!(
2719                    filter,
2720                    Expr::UnaryOp(UnaryOp::IsNull, Box::new(Expr::Field("age".into())))
2721                );
2722            }
2723            _ => panic!("expected query"),
2724        }
2725    }
2726
2727    #[test]
2728    fn test_parse_neq_null_desugars_to_is_not_null() {
2729        let stmt = parse("User filter .age != null").unwrap();
2730        match stmt {
2731            Statement::Query(q) => {
2732                let filter = q.filter.unwrap();
2733                assert_eq!(
2734                    filter,
2735                    Expr::UnaryOp(UnaryOp::IsNotNull, Box::new(Expr::Field("age".into())))
2736                );
2737            }
2738            _ => panic!("expected query"),
2739        }
2740    }
2741
2742    #[test]
2743    fn test_parse_null_comparisons_parse_ok() {
2744        // `< null`, `>= null` etc. parse successfully now that `null` is a
2745        // valid expression. At runtime they evaluate to Empty (no match),
2746        // which is correct null-propagation semantics.
2747        assert!(parse("User filter .age < null").is_ok());
2748        assert!(parse("User filter .age >= null").is_ok());
2749    }
2750
2751    #[test]
2752    fn test_parse_count_star_expr() {
2753        let stmt = parse("User filter count(*) > 0").unwrap();
2754        match stmt {
2755            Statement::Query(q) => {
2756                let filter = q.filter.unwrap();
2757                match filter {
2758                    Expr::BinaryOp(left, BinOp::Gt, _) => {
2759                        assert_eq!(
2760                            *left,
2761                            Expr::FunctionCall(AggFunc::Count, Box::new(Expr::Field("*".into())))
2762                        );
2763                    }
2764                    _ => panic!("expected comparison"),
2765                }
2766            }
2767            _ => panic!("expected query"),
2768        }
2769    }
2770
2771    // ─── String function parser tests ──────────────────────────────────
2772
2773    #[test]
2774    fn test_parse_upper_in_filter() {
2775        let stmt = parse(r#"User filter upper(.name) = "ALICE""#).unwrap();
2776        match stmt {
2777            Statement::Query(q) => {
2778                let f = q.filter.unwrap();
2779                match f {
2780                    Expr::BinaryOp(left, BinOp::Eq, _right) => {
2781                        assert!(matches!(*left, Expr::ScalarFunc(ScalarFn::Upper, _)));
2782                    }
2783                    _ => panic!("expected binary op with upper"),
2784                }
2785            }
2786            _ => panic!("expected query"),
2787        }
2788    }
2789
2790    #[test]
2791    fn test_parse_substring() {
2792        let stmt = parse("User { sub: substring(.name, 1, 3) }").unwrap();
2793        match stmt {
2794            Statement::Query(q) => {
2795                let proj = q.projection.unwrap();
2796                match &proj[0].expr {
2797                    Expr::ScalarFunc(ScalarFn::Substring, args) => {
2798                        assert_eq!(args.len(), 3);
2799                    }
2800                    other => panic!("expected ScalarFunc Substring, got {other:?}"),
2801                }
2802            }
2803            _ => panic!("expected query"),
2804        }
2805    }
2806
2807    #[test]
2808    fn test_parse_concat() {
2809        let stmt = parse(r#"User { full: concat(.name, " - ", .email) }"#).unwrap();
2810        match stmt {
2811            Statement::Query(q) => {
2812                let proj = q.projection.unwrap();
2813                match &proj[0].expr {
2814                    Expr::ScalarFunc(ScalarFn::Concat, args) => {
2815                        assert_eq!(args.len(), 3);
2816                    }
2817                    other => panic!("expected ScalarFunc Concat, got {other:?}"),
2818                }
2819            }
2820            _ => panic!("expected query"),
2821        }
2822    }
2823
2824    // ─── CASE WHEN parser tests ────────────────────────────────────────
2825
2826    #[test]
2827    fn test_parse_case_single_when() {
2828        let stmt = parse(r#"User filter case when .age > 30 then true else false end"#).unwrap();
2829        match stmt {
2830            Statement::Query(q) => {
2831                let filter = q.filter.unwrap();
2832                match filter {
2833                    Expr::Case { whens, else_expr } => {
2834                        assert_eq!(whens.len(), 1);
2835                        assert!(else_expr.is_some());
2836                    }
2837                    other => panic!("expected Case expr, got {other:?}"),
2838                }
2839            }
2840            _ => panic!("expected query"),
2841        }
2842    }
2843
2844    #[test]
2845    fn test_parse_case_multiple_whens() {
2846        let stmt = parse(
2847            r#"User { label: case when .age > 30 then "senior" when .age > 20 then "adult" else "young" end }"#
2848        ).unwrap();
2849        match stmt {
2850            Statement::Query(q) => {
2851                let proj = q.projection.unwrap();
2852                match &proj[0].expr {
2853                    Expr::Case { whens, else_expr } => {
2854                        assert_eq!(whens.len(), 2);
2855                        assert!(else_expr.is_some());
2856                    }
2857                    other => panic!("expected Case expr, got {other:?}"),
2858                }
2859            }
2860            _ => panic!("expected query"),
2861        }
2862    }
2863
2864    #[test]
2865    fn test_parse_case_without_else() {
2866        let stmt = parse(r#"User filter case when .age > 30 then true end"#).unwrap();
2867        match stmt {
2868            Statement::Query(q) => {
2869                let filter = q.filter.unwrap();
2870                match filter {
2871                    Expr::Case { whens, else_expr } => {
2872                        assert_eq!(whens.len(), 1);
2873                        assert!(else_expr.is_none());
2874                    }
2875                    other => panic!("expected Case expr, got {other:?}"),
2876                }
2877            }
2878            _ => panic!("expected query"),
2879        }
2880    }
2881
2882    // ─── Mul/Div expression tests (E2f) ───────────────────────────────
2883
2884    #[test]
2885    fn test_parse_mul_expr() {
2886        let stmt = parse("User filter .price * .quantity > 100").unwrap();
2887        match stmt {
2888            Statement::Query(q) => {
2889                let filter = q.filter.unwrap();
2890                match filter {
2891                    Expr::BinaryOp(left, BinOp::Gt, _) => match *left {
2892                        Expr::BinaryOp(_, BinOp::Mul, _) => {}
2893                        other => panic!("expected Mul, got {other:?}"),
2894                    },
2895                    other => panic!("expected BinaryOp Gt, got {other:?}"),
2896                }
2897            }
2898            _ => panic!("expected query"),
2899        }
2900    }
2901
2902    #[test]
2903    fn test_parse_div_expr() {
2904        let stmt = parse("User { ratio: .total / .count }").unwrap();
2905        match stmt {
2906            Statement::Query(q) => {
2907                let proj = q.projection.unwrap();
2908                assert_eq!(proj[0].alias.as_deref(), Some("ratio"));
2909                match &proj[0].expr {
2910                    Expr::BinaryOp(_, BinOp::Div, _) => {}
2911                    other => panic!("expected Div, got {other:?}"),
2912                }
2913            }
2914            _ => panic!("expected query"),
2915        }
2916    }
2917
2918    #[test]
2919    fn test_parse_mul_div_precedence() {
2920        // .a + .b * .c should parse as .a + (.b * .c)
2921        let stmt = parse("User filter .a + .b * .c > 0").unwrap();
2922        match stmt {
2923            Statement::Query(q) => {
2924                let filter = q.filter.unwrap();
2925                match filter {
2926                    Expr::BinaryOp(left, BinOp::Gt, _) => match *left {
2927                        Expr::BinaryOp(_, BinOp::Add, right) => {
2928                            assert!(matches!(*right, Expr::BinaryOp(_, BinOp::Mul, _)));
2929                        }
2930                        other => panic!("expected Add, got {other:?}"),
2931                    },
2932                    other => panic!("expected Gt, got {other:?}"),
2933                }
2934            }
2935            _ => panic!("expected query"),
2936        }
2937    }
2938
2939    // ─── Multi-column ORDER BY tests (E2f) ────────────────────────────
2940
2941    #[test]
2942    fn test_parse_multi_order() {
2943        let stmt = parse("User order .name asc, .age desc").unwrap();
2944        match stmt {
2945            Statement::Query(q) => {
2946                let order = q.order.unwrap();
2947                assert_eq!(order.keys.len(), 2);
2948                assert_eq!(order.keys[0].field, "name");
2949                assert!(!order.keys[0].descending);
2950                assert_eq!(order.keys[1].field, "age");
2951                assert!(order.keys[1].descending);
2952            }
2953            _ => panic!("expected query"),
2954        }
2955    }
2956
2957    #[test]
2958    fn test_parse_order_default_asc() {
2959        let stmt = parse("User order .name").unwrap();
2960        match stmt {
2961            Statement::Query(q) => {
2962                let order = q.order.unwrap();
2963                assert_eq!(order.keys.len(), 1);
2964                assert!(!order.keys[0].descending);
2965            }
2966            _ => panic!("expected query"),
2967        }
2968    }
2969
2970    // ─── ALTER TABLE / DROP TABLE parser tests (E2g) ──────────────────
2971
2972    #[test]
2973    fn test_parse_alter_add_column() {
2974        let stmt = parse("alter User add column status: str").unwrap();
2975        match stmt {
2976            Statement::AlterTable(at) => {
2977                assert_eq!(at.table, "User");
2978                match at.action {
2979                    AlterAction::AddColumn {
2980                        name,
2981                        type_name,
2982                        required,
2983                    } => {
2984                        assert_eq!(name, "status");
2985                        assert_eq!(type_name, "str");
2986                        assert!(!required);
2987                    }
2988                    other => panic!("expected AddColumn, got {other:?}"),
2989                }
2990            }
2991            other => panic!("expected AlterTable, got {other:?}"),
2992        }
2993    }
2994
2995    #[test]
2996    fn test_parse_alter_add_required_column() {
2997        let stmt = parse("alter User add required status: str").unwrap();
2998        match stmt {
2999            Statement::AlterTable(at) => match at.action {
3000                AlterAction::AddColumn { required, .. } => assert!(required),
3001                other => panic!("expected AddColumn, got {other:?}"),
3002            },
3003            other => panic!("expected AlterTable, got {other:?}"),
3004        }
3005    }
3006
3007    #[test]
3008    fn test_parse_type_with_unique_modifier() {
3009        let stmt = parse("type User { required unique email: str, age: int }").unwrap();
3010        match stmt {
3011            Statement::CreateType(ct) => {
3012                assert!(ct.fields[0].required && ct.fields[0].unique);
3013                assert!(!ct.fields[1].unique);
3014            }
3015            other => panic!("expected CreateType, got {other:?}"),
3016        }
3017    }
3018
3019    #[test]
3020    fn test_parse_type_unique_before_required() {
3021        // Modifiers accepted in either order.
3022        let stmt = parse("type User { unique required email: str }").unwrap();
3023        match stmt {
3024            Statement::CreateType(ct) => {
3025                assert!(ct.fields[0].required && ct.fields[0].unique);
3026            }
3027            other => panic!("expected CreateType, got {other:?}"),
3028        }
3029    }
3030
3031    #[test]
3032    fn test_parse_alter_add_unique() {
3033        let stmt = parse("alter User add unique .email").unwrap();
3034        match stmt {
3035            Statement::AlterTable(at) => assert!(matches!(
3036                at.action,
3037                AlterAction::AddUnique { ref column, .. } if column == "email"
3038            )),
3039            other => panic!("expected AlterTable, got {other:?}"),
3040        }
3041    }
3042
3043    #[test]
3044    fn test_parse_alter_drop_column() {
3045        let stmt = parse("alter User drop column status").unwrap();
3046        match stmt {
3047            Statement::AlterTable(at) => {
3048                assert_eq!(at.table, "User");
3049                match at.action {
3050                    AlterAction::DropColumn { name, .. } => assert_eq!(name, "status"),
3051                    other => panic!("expected DropColumn, got {other:?}"),
3052                }
3053            }
3054            other => panic!("expected AlterTable, got {other:?}"),
3055        }
3056    }
3057
3058    #[test]
3059    fn test_parse_alter_drop_without_column_keyword() {
3060        let stmt = parse("alter User drop status").unwrap();
3061        match stmt {
3062            Statement::AlterTable(at) => match at.action {
3063                AlterAction::DropColumn { name, .. } => assert_eq!(name, "status"),
3064                other => panic!("expected DropColumn, got {other:?}"),
3065            },
3066            other => panic!("expected AlterTable, got {other:?}"),
3067        }
3068    }
3069
3070    #[test]
3071    fn test_parse_drop_table() {
3072        let stmt = parse("drop User").unwrap();
3073        match stmt {
3074            Statement::DropTable(dt) => assert_eq!(dt.table, "User"),
3075            other => panic!("expected DropTable, got {other:?}"),
3076        }
3077    }
3078
3079    // ─── IN subquery parser tests (E2h) ───────────────────────────────
3080
3081    #[test]
3082    fn test_parse_in_subquery() {
3083        let stmt = parse("User filter .name in (VIP { .name })").unwrap();
3084        match stmt {
3085            Statement::Query(q) => {
3086                let filter = q.filter.unwrap();
3087                match filter {
3088                    Expr::InSubquery {
3089                        expr,
3090                        subquery,
3091                        negated,
3092                    } => {
3093                        assert!(!negated);
3094                        assert!(matches!(*expr, Expr::Field(ref f) if f == "name"));
3095                        assert_eq!(subquery.source, "VIP");
3096                    }
3097                    other => panic!("expected InSubquery, got {other:?}"),
3098                }
3099            }
3100            _ => panic!("expected query"),
3101        }
3102    }
3103
3104    #[test]
3105    fn test_parse_not_in_subquery() {
3106        let stmt = parse("User filter .id not in (Order { .user_id })").unwrap();
3107        match stmt {
3108            Statement::Query(q) => match q.filter.unwrap() {
3109                Expr::InSubquery { negated, .. } => assert!(negated),
3110                other => panic!("expected InSubquery, got {other:?}"),
3111            },
3112            _ => panic!("expected query"),
3113        }
3114    }
3115
3116    #[test]
3117    fn test_parse_in_literal_list_still_works() {
3118        // Ensure existing IN (literal) parsing isn't broken
3119        let stmt = parse("User filter .age in (25, 30, 35)").unwrap();
3120        match stmt {
3121            Statement::Query(q) => match q.filter.unwrap() {
3122                Expr::InList { list, negated, .. } => {
3123                    assert!(!negated);
3124                    assert_eq!(list.len(), 3);
3125                }
3126                other => panic!("expected InList, got {other:?}"),
3127            },
3128            _ => panic!("expected query"),
3129        }
3130    }
3131
3132    // ---- Materialized view parser tests ------------------------------------
3133
3134    #[test]
3135    fn test_parse_create_view() {
3136        let stmt = parse("materialize OldUsers as User filter .age > 28").unwrap();
3137        match stmt {
3138            Statement::CreateView(cv) => {
3139                assert_eq!(cv.name, "OldUsers");
3140                assert_eq!(cv.query.source, "User");
3141                assert!(cv.query.filter.is_some());
3142                assert!(!cv.query_text.is_empty());
3143            }
3144            _ => panic!("expected CreateView"),
3145        }
3146    }
3147
3148    #[test]
3149    fn test_parse_create_view_with_projection() {
3150        let stmt = parse("materialize UserNames as User { .name }").unwrap();
3151        match stmt {
3152            Statement::CreateView(cv) => {
3153                assert_eq!(cv.name, "UserNames");
3154                assert!(cv.query.projection.is_some());
3155            }
3156            _ => panic!("expected CreateView"),
3157        }
3158    }
3159
3160    #[test]
3161    fn test_parse_refresh_view() {
3162        let stmt = parse("refresh OldUsers").unwrap();
3163        match stmt {
3164            Statement::RefreshView(rv) => {
3165                assert_eq!(rv.name, "OldUsers");
3166            }
3167            _ => panic!("expected RefreshView"),
3168        }
3169    }
3170
3171    #[test]
3172    fn test_parse_drop_view() {
3173        let stmt = parse("drop view OldUsers").unwrap();
3174        match stmt {
3175            Statement::DropView(dv) => {
3176                assert_eq!(dv.name, "OldUsers");
3177            }
3178            _ => panic!("expected DropView"),
3179        }
3180    }
3181
3182    #[test]
3183    fn test_parse_drop_table_still_works() {
3184        let stmt = parse("drop Users").unwrap();
3185        match stmt {
3186            Statement::DropTable(dt) => {
3187                assert_eq!(dt.table, "Users");
3188            }
3189            _ => panic!("expected DropTable"),
3190        }
3191    }
3192
3193    #[test]
3194    fn test_parse_union() {
3195        let stmt = parse("User union Order").unwrap();
3196        match stmt {
3197            Statement::Union(u) => {
3198                assert!(!u.all);
3199                match *u.left {
3200                    Statement::Query(_) => {}
3201                    _ => panic!("expected Query on left"),
3202                }
3203                match *u.right {
3204                    Statement::Query(_) => {}
3205                    _ => panic!("expected Query on right"),
3206                }
3207            }
3208            _ => panic!("expected Union"),
3209        }
3210    }
3211
3212    #[test]
3213    fn test_parse_union_all() {
3214        let stmt = parse("User union all Order").unwrap();
3215        match stmt {
3216            Statement::Union(u) => {
3217                assert!(u.all, "expected UNION ALL");
3218                match *u.left {
3219                    Statement::Query(_) => {}
3220                    _ => panic!("expected Query on left"),
3221                }
3222                match *u.right {
3223                    Statement::Query(_) => {}
3224                    _ => panic!("expected Query on right"),
3225                }
3226            }
3227            _ => panic!("expected Union"),
3228        }
3229    }
3230
3231    #[test]
3232    fn test_parse_union_chain() {
3233        // Left-associative: A union B union C => Union(Union(A, B), C)
3234        let stmt = parse("User union Order union Product").unwrap();
3235        match stmt {
3236            Statement::Union(outer) => {
3237                assert!(!outer.all);
3238                // Right side is Product
3239                match *outer.right {
3240                    Statement::Query(q) => assert_eq!(q.source, "Product"),
3241                    _ => panic!("expected Query(Product) on right"),
3242                }
3243                // Left side is Union(User, Order)
3244                match *outer.left {
3245                    Statement::Union(inner) => {
3246                        assert!(!inner.all);
3247                        match *inner.left {
3248                            Statement::Query(q) => assert_eq!(q.source, "User"),
3249                            _ => panic!("expected Query(User)"),
3250                        }
3251                        match *inner.right {
3252                            Statement::Query(q) => assert_eq!(q.source, "Order"),
3253                            _ => panic!("expected Query(Order)"),
3254                        }
3255                    }
3256                    _ => panic!("expected inner Union"),
3257                }
3258            }
3259            _ => panic!("expected Union"),
3260        }
3261    }
3262
3263    #[test]
3264    fn test_parse_union_with_filter() {
3265        let stmt = parse("User filter .age > 10 union Order filter .total > 50").unwrap();
3266        match stmt {
3267            Statement::Union(u) => {
3268                assert!(!u.all);
3269                // Both sides should be queries (the filter is part of each query)
3270                match *u.left {
3271                    Statement::Query(q) => {
3272                        assert_eq!(q.source, "User");
3273                        assert!(q.filter.is_some());
3274                    }
3275                    _ => panic!("expected Query on left"),
3276                }
3277                match *u.right {
3278                    Statement::Query(q) => {
3279                        assert_eq!(q.source, "Order");
3280                        assert!(q.filter.is_some());
3281                    }
3282                    _ => panic!("expected Query on right"),
3283                }
3284            }
3285            _ => panic!("expected Union"),
3286        }
3287    }
3288
3289    #[test]
3290    fn test_parse_count_distinct_standalone() {
3291        let stmt = parse("count(distinct User { .name })").unwrap();
3292        match stmt {
3293            Statement::Query(q) => {
3294                let agg = q.aggregation.unwrap();
3295                assert_eq!(agg.function, AggFunc::CountDistinct);
3296                assert_eq!(agg.field.as_deref(), Some("name"));
3297            }
3298            _ => panic!("expected Query"),
3299        }
3300    }
3301
3302    #[test]
3303    fn test_parse_count_distinct_in_projection() {
3304        let stmt = parse("User group .dept { .dept, count(distinct .name) }").unwrap();
3305        match stmt {
3306            Statement::Query(q) => {
3307                let proj = q.projection.unwrap();
3308                assert_eq!(proj.len(), 2);
3309                match &proj[1].expr {
3310                    Expr::FunctionCall(func, _) => {
3311                        assert_eq!(*func, AggFunc::CountDistinct);
3312                    }
3313                    _ => panic!("expected FunctionCall"),
3314                }
3315            }
3316            _ => panic!("expected Query"),
3317        }
3318    }
3319
3320    // ---- Window function parser tests ----------------------------------------
3321
3322    #[test]
3323    fn test_parse_window_row_number_order() {
3324        let stmt = parse("User { .name, rn: row_number() over (order .age) }").unwrap();
3325        match stmt {
3326            Statement::Query(q) => {
3327                let proj = q.projection.unwrap();
3328                assert_eq!(proj.len(), 2);
3329                assert_eq!(proj[1].alias.as_deref(), Some("rn"));
3330                match &proj[1].expr {
3331                    Expr::Window {
3332                        function,
3333                        args,
3334                        partition_by,
3335                        order_by,
3336                    } => {
3337                        assert_eq!(*function, WindowFunc::RowNumber);
3338                        assert!(args.is_empty());
3339                        assert!(partition_by.is_empty());
3340                        assert_eq!(order_by.len(), 1);
3341                        assert_eq!(order_by[0].field, "age");
3342                        assert!(!order_by[0].descending);
3343                    }
3344                    other => panic!("expected Window, got {other:?}"),
3345                }
3346            }
3347            _ => panic!("expected query"),
3348        }
3349    }
3350
3351    #[test]
3352    fn test_parse_window_sum_partition_order() {
3353        let stmt =
3354            parse("User { .name, s: sum(.salary) over (partition .dept order .salary) }").unwrap();
3355        match stmt {
3356            Statement::Query(q) => {
3357                let proj = q.projection.unwrap();
3358                assert_eq!(proj.len(), 2);
3359                assert_eq!(proj[1].alias.as_deref(), Some("s"));
3360                match &proj[1].expr {
3361                    Expr::Window {
3362                        function,
3363                        args,
3364                        partition_by,
3365                        order_by,
3366                    } => {
3367                        assert_eq!(*function, WindowFunc::Sum);
3368                        assert_eq!(args.len(), 1);
3369                        assert!(matches!(&args[0], Expr::Field(f) if f == "salary"));
3370                        assert_eq!(partition_by, &["dept"]);
3371                        assert_eq!(order_by.len(), 1);
3372                        assert_eq!(order_by[0].field, "salary");
3373                        assert!(!order_by[0].descending);
3374                    }
3375                    other => panic!("expected Window, got {other:?}"),
3376                }
3377            }
3378            _ => panic!("expected query"),
3379        }
3380    }
3381
3382    #[test]
3383    fn test_parse_window_rank_desc() {
3384        let stmt =
3385            parse("User { .dept, .salary, r: rank() over (partition .dept order .salary desc) }")
3386                .unwrap();
3387        match stmt {
3388            Statement::Query(q) => {
3389                let proj = q.projection.unwrap();
3390                assert_eq!(proj.len(), 3);
3391                match &proj[2].expr {
3392                    Expr::Window {
3393                        function,
3394                        partition_by,
3395                        order_by,
3396                        ..
3397                    } => {
3398                        assert_eq!(*function, WindowFunc::Rank);
3399                        assert_eq!(partition_by, &["dept"]);
3400                        assert_eq!(order_by.len(), 1);
3401                        assert!(order_by[0].descending);
3402                    }
3403                    other => panic!("expected Window, got {other:?}"),
3404                }
3405            }
3406            _ => panic!("expected query"),
3407        }
3408    }
3409
3410    #[test]
3411    fn test_parse_window_dense_rank() {
3412        let stmt = parse("User { .name, dr: dense_rank() over (order .score desc) }").unwrap();
3413        match stmt {
3414            Statement::Query(q) => {
3415                let proj = q.projection.unwrap();
3416                assert_eq!(proj.len(), 2);
3417                match &proj[1].expr {
3418                    Expr::Window { function, .. } => {
3419                        assert_eq!(*function, WindowFunc::DenseRank);
3420                    }
3421                    other => panic!("expected Window, got {other:?}"),
3422                }
3423            }
3424            _ => panic!("expected query"),
3425        }
3426    }
3427
3428    #[test]
3429    fn test_parse_sum_without_over_is_aggregate() {
3430        // sum(.salary) alone (no `over`) stays as FunctionCall, not Window.
3431        let stmt = parse("User group .dept { .dept, total: sum(.salary) }").unwrap();
3432        match stmt {
3433            Statement::Query(q) => {
3434                let proj = q.projection.unwrap();
3435                assert_eq!(proj.len(), 2);
3436                match &proj[1].expr {
3437                    Expr::FunctionCall(AggFunc::Sum, _) => {} // correct
3438                    other => panic!("expected FunctionCall(Sum), got {other:?}"),
3439                }
3440            }
3441            _ => panic!("expected query"),
3442        }
3443    }
3444
3445    #[test]
3446    fn test_nesting_depth_limit() {
3447        // Build a deeply nested parenthesized expression that exceeds MAX_NESTING_DEPTH.
3448        let mut query = String::from("User filter ");
3449        for _ in 0..70 {
3450            query.push('(');
3451        }
3452        query.push_str(".age > 1");
3453        for _ in 0..70 {
3454            query.push(')');
3455        }
3456        let result = parse(&query);
3457        assert!(result.is_err());
3458        let err = result.unwrap_err();
3459        assert!(
3460            err.message().contains("nesting depth"),
3461            "expected nesting depth error, got: {}",
3462            err.message()
3463        );
3464    }
3465
3466    #[test]
3467    fn test_unary_prefix_nesting_depth_limit() {
3468        // A long chain of `not` prefixes recurses through parse_primary
3469        // without passing through parse_expr's guard. It must error cleanly
3470        // at the depth limit instead of overflowing the stack.
3471        let query = String::from("User filter ") + &"not ".repeat(5000) + ".active";
3472        let result = parse(&query);
3473        assert!(result.is_err());
3474        let err = result.unwrap_err();
3475        assert!(
3476            err.message().contains("nesting depth"),
3477            "expected nesting depth error, got: {}",
3478            err.message()
3479        );
3480    }
3481
3482    #[test]
3483    fn test_moderate_nesting_succeeds() {
3484        // 10 levels of nesting should be fine.
3485        let mut query = String::from("User filter ");
3486        for _ in 0..10 {
3487            query.push('(');
3488        }
3489        query.push_str(".age > 1");
3490        for _ in 0..10 {
3491            query.push(')');
3492        }
3493        assert!(parse(&query).is_ok());
3494    }
3495
3496    /// Regression for issue #26: `fuzz_parser` crashed on the 3-byte input
3497    /// `nn{` — the projection loop consumed the Eof token and then indexed
3498    /// past the end of `tokens`. Must return an error instead.
3499    #[test]
3500    fn test_parse_fuzz_repro_projection_eof() {
3501        let err = parse("nn{").expect_err("unterminated projection must error, not panic");
3502        let _ = err.message();
3503    }
3504
3505    /// Regression for issue #26: `fuzz_roundtrip` tripped the same bug with
3506    /// the 2-byte input `z{`.
3507    #[test]
3508    fn test_parse_fuzz_repro_short_projection_eof() {
3509        let err = parse("z{").expect_err("unterminated projection must error, not panic");
3510        let _ = err.message();
3511    }
3512
3513    #[test]
3514    fn test_update_at_statement_start_gives_helpful_error() {
3515        let err =
3516            parse(r#"update User filter .name = "Alice" { age := 31 }"#).expect_err("should fail");
3517        let msg = err.message();
3518        assert!(
3519            msg.contains("pipeline syntax"),
3520            "error should mention pipeline syntax, got: {msg}"
3521        );
3522        assert!(
3523            msg.contains("update"),
3524            "error should mention 'update', got: {msg}"
3525        );
3526    }
3527
3528    #[test]
3529    fn test_delete_at_statement_start_gives_helpful_error() {
3530        let err = parse("delete User filter .age < 18").expect_err("should fail");
3531        let msg = err.message();
3532        assert!(
3533            msg.contains("pipeline syntax"),
3534            "error should mention pipeline syntax, got: {msg}"
3535        );
3536        assert!(
3537            msg.contains("delete"),
3538            "error should mention 'delete', got: {msg}"
3539        );
3540    }
3541}
3542
3543#[cfg(test)]
3544mod cleanup_parser_dx_tests {
3545    use super::*;
3546
3547    #[test]
3548    fn typoed_statement_keyword_gets_suggestion() {
3549        let err = parse("updat User set age = 1").unwrap_err();
3550        let msg = err.to_string();
3551        assert!(msg.contains("near token"), "{msg}");
3552        assert!(msg.contains("did you mean `update`"), "{msg}");
3553    }
3554}
3555
3556#[cfg(test)]
3557mod capa_dx_tests {
3558    use super::*;
3559
3560    // ── P-6: reserved words as column names ────────────────────────────
3561
3562    #[test]
3563    fn reserved_word_field_name_gives_actionable_error() {
3564        let err = parse("type Post { type: str }").unwrap_err();
3565        let msg = err.to_string();
3566        assert!(
3567            msg.contains("'type' is a reserved word")
3568                && msg.contains("field name")
3569                && msg.contains("quote it as `type`"),
3570            "unhelpful message: {msg}"
3571        );
3572    }
3573
3574    #[test]
3575    fn reserved_modifier_word_as_field_name_gives_actionable_error() {
3576        // `required` is a modifier keyword; followed directly by `:` it is
3577        // instead the field's (reserved) name — the old error was the opaque
3578        // "expected field name, got ':'".
3579        let err = parse("type Post { required: bool }").unwrap_err();
3580        let msg = err.to_string();
3581        assert!(
3582            msg.contains("'required' is a reserved word") && msg.contains("quote it as `required`"),
3583            "unhelpful message: {msg}"
3584        );
3585    }
3586
3587    #[test]
3588    fn reserved_word_in_insert_assignment_gives_actionable_error() {
3589        let err = parse(r#"insert Post { type := "x" }"#).unwrap_err();
3590        let msg = err.to_string();
3591        assert!(msg.contains("'type' is a reserved word"), "{msg}");
3592    }
3593
3594    #[test]
3595    fn reserved_word_in_alter_column_gives_actionable_error() {
3596        let err = parse("alter Post add column order: int").unwrap_err();
3597        let msg = err.to_string();
3598        assert!(msg.contains("'order' is a reserved word"), "{msg}");
3599    }
3600
3601    #[test]
3602    fn backtick_field_name_parses_as_identifier() {
3603        let stmt = parse("type Post { `type`: str, `order`: int }").unwrap();
3604        match stmt {
3605            Statement::CreateType(ct) => {
3606                assert_eq!(ct.fields[0].name, "type");
3607                assert_eq!(ct.fields[1].name, "order");
3608            }
3609            other => panic!("expected CreateType, got {other:?}"),
3610        }
3611    }
3612
3613    #[test]
3614    fn backtick_field_still_honors_modifiers() {
3615        let stmt = parse("type Post { required `type`: str }").unwrap();
3616        match stmt {
3617            Statement::CreateType(ct) => {
3618                assert_eq!(ct.fields[0].name, "type");
3619                assert!(ct.fields[0].required);
3620            }
3621            other => panic!("expected CreateType, got {other:?}"),
3622        }
3623    }
3624
3625    // ── P-7: DDL idempotency ───────────────────────────────────────────
3626
3627    #[test]
3628    fn create_type_if_not_exists_parses() {
3629        let stmt = parse("type Post if not exists { id: int }").unwrap();
3630        match stmt {
3631            Statement::CreateType(ct) => assert!(ct.if_not_exists),
3632            other => panic!("expected CreateType, got {other:?}"),
3633        }
3634    }
3635
3636    #[test]
3637    fn create_type_without_clause_defaults_false() {
3638        let stmt = parse("type Post { id: int }").unwrap();
3639        match stmt {
3640            Statement::CreateType(ct) => assert!(!ct.if_not_exists),
3641            other => panic!("expected CreateType, got {other:?}"),
3642        }
3643    }
3644
3645    #[test]
3646    fn drop_if_exists_parses() {
3647        match parse("drop if exists Post").unwrap() {
3648            Statement::DropTable(dt) => assert!(dt.if_exists),
3649            other => panic!("expected DropTable, got {other:?}"),
3650        }
3651        match parse("drop Post").unwrap() {
3652            Statement::DropTable(dt) => assert!(!dt.if_exists),
3653            other => panic!("expected DropTable, got {other:?}"),
3654        }
3655    }
3656
3657    #[test]
3658    fn drop_view_if_exists_parses() {
3659        match parse("drop view if exists ActiveUsers").unwrap() {
3660            Statement::DropView(dv) => {
3661                assert!(dv.if_exists);
3662                assert_eq!(dv.name, "ActiveUsers");
3663            }
3664            other => panic!("expected DropView, got {other:?}"),
3665        }
3666    }
3667
3668    #[test]
3669    fn add_index_and_unique_if_not_exists_parse() {
3670        match parse("alter Post add index if not exists .slug").unwrap() {
3671            Statement::AlterTable(at) => {
3672                assert!(matches!(
3673                    at.action,
3674                    AlterAction::AddIndex {
3675                        if_not_exists: true,
3676                        ..
3677                    }
3678                ));
3679            }
3680            other => panic!("expected AlterTable, got {other:?}"),
3681        }
3682        match parse("alter Post add unique if not exists .slug").unwrap() {
3683            Statement::AlterTable(at) => {
3684                assert!(matches!(
3685                    at.action,
3686                    AlterAction::AddUnique {
3687                        if_not_exists: true,
3688                        ..
3689                    }
3690                ));
3691            }
3692            other => panic!("expected AlterTable, got {other:?}"),
3693        }
3694    }
3695
3696    #[test]
3697    fn alter_drop_column_if_exists_parses() {
3698        match parse("alter Post drop column if exists status").unwrap() {
3699            Statement::AlterTable(at) => {
3700                assert!(matches!(
3701                    at.action,
3702                    AlterAction::DropColumn {
3703                        if_exists: true,
3704                        ..
3705                    }
3706                ));
3707            }
3708            other => panic!("expected AlterTable, got {other:?}"),
3709        }
3710    }
3711
3712    // ── P-8: introspection ─────────────────────────────────────────────
3713
3714    #[test]
3715    fn schema_parses_to_list_types() {
3716        assert_eq!(parse("schema").unwrap(), Statement::ListTypes);
3717    }
3718
3719    #[test]
3720    fn describe_parses_to_describe() {
3721        assert_eq!(
3722            parse("describe Post").unwrap(),
3723            Statement::Describe("Post".to_string())
3724        );
3725    }
3726
3727    #[test]
3728    fn schema_with_type_aliases_describe() {
3729        assert_eq!(
3730            parse("schema Post").unwrap(),
3731            Statement::Describe("Post".to_string())
3732        );
3733    }
3734}