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