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