Skip to main content

powdb_query/
parser.rs

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