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