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