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