Skip to main content

powdb_query/
parser.rs

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