Skip to main content

sparrowdb_cypher/
parser.rs

1//! Recursive-descent Cypher parser.
2//!
3//! Entry point: [`parse`].  Returns `Err(...)` — never panics — for
4//! unsupported syntax.
5
6use sparrowdb_common::{Error, Result};
7
8use crate::ast::{
9    BinOpKind, CallStatement, CreateStatement, EdgeDir, ExistsPattern, Expr, ListPredicateKind,
10    Literal, MatchCreateStatement, MatchMergeRelStatement, MatchMutateStatement,
11    MatchOptionalMatchStatement, MatchStatement, MergeStatement, Mutation, NodePattern,
12    OptionalMatchStatement, PathPattern, PipelineStage, PipelineStatement, PropEntry, RelPattern,
13    ReturnClause, ReturnItem, ShortestPathExpr, SortDir, Statement, UnionStatement,
14    UnwindStatement, WithClause, WithItem,
15};
16use crate::lexer::{tokenize, Token};
17
18/// Map a keyword `Token` variant to its canonical string representation.
19///
20/// Returns `None` for non-keyword tokens (e.g. `Ident`, `Integer`, punctuation).
21fn token_keyword_name(tok: &Token) -> Option<&'static str> {
22    match tok {
23        Token::Match => Some("MATCH"),
24        Token::Create => Some("CREATE"),
25        Token::Return => Some("RETURN"),
26        Token::Where => Some("WHERE"),
27        Token::Not => Some("NOT"),
28        Token::And => Some("AND"),
29        Token::Or => Some("OR"),
30        Token::Order => Some("ORDER"),
31        Token::By => Some("BY"),
32        Token::Asc => Some("ASC"),
33        Token::Desc => Some("DESC"),
34        Token::Limit => Some("LIMIT"),
35        Token::Skip => Some("SKIP"),
36        Token::Distinct => Some("DISTINCT"),
37        Token::Optional => Some("OPTIONAL"),
38        Token::Union => Some("UNION"),
39        Token::Unwind => Some("UNWIND"),
40        Token::Delete => Some("DELETE"),
41        Token::Detach => Some("DETACH"),
42        Token::Set => Some("SET"),
43        Token::Merge => Some("MERGE"),
44        Token::Checkpoint => Some("CHECKPOINT"),
45        Token::Optimize => Some("OPTIMIZE"),
46        Token::Contains => Some("CONTAINS"),
47        Token::StartsWith => Some("STARTS"),
48        Token::EndsWith => Some("ENDS"),
49        Token::Count => Some("COUNT"),
50        Token::Null => Some("NULL"),
51        Token::True => Some("TRUE"),
52        Token::False => Some("FALSE"),
53        Token::As => Some("AS"),
54        Token::With => Some("WITH"),
55        Token::Exists => Some("EXISTS"),
56        Token::In => Some("IN"),
57        Token::Any => Some("ANY"),
58        Token::All => Some("ALL"),
59        Token::NoneKw => Some("NONE"),
60        Token::Single => Some("SINGLE"),
61        Token::Is => Some("IS"),
62        Token::Call => Some("CALL"),
63        Token::Yield => Some("YIELD"),
64        Token::Case => Some("CASE"),
65        Token::When => Some("WHEN"),
66        Token::Then => Some("THEN"),
67        Token::Else => Some("ELSE"),
68        Token::End => Some("END"),
69        Token::Index => Some("INDEX"),
70        Token::On => Some("ON"),
71        Token::Constraint => Some("CONSTRAINT"),
72        Token::Assert => Some("ASSERT"),
73        _ => None,
74    }
75}
76
77/// Parse a Cypher statement string.  Returns `Err` for any unsupported or
78/// malformed input; never panics.
79pub fn parse(input: &str) -> Result<Statement> {
80    if input.trim().is_empty() {
81        return Err(Error::InvalidArgument("empty input".into()));
82    }
83    let tokens = tokenize(input)?;
84    let mut p = Parser::new(tokens);
85    let stmt = p.parse_statement()?;
86
87    // Check for UNION / UNION ALL between two statements.
88    let stmt = if matches!(p.peek(), Token::Union) {
89        p.advance();
90        let all = if matches!(p.peek(), Token::All)
91            || matches!(p.peek(), Token::Ident(ref s) if s.to_uppercase() == "ALL")
92        {
93            p.advance();
94            true
95        } else {
96            false
97        };
98        let right = p.parse_statement()?;
99        Statement::Union(UnionStatement {
100            left: Box::new(stmt),
101            right: Box::new(right),
102            all,
103        })
104    } else {
105        stmt
106    };
107
108    // Consume optional trailing semicolon.
109    if matches!(p.peek(), Token::Semicolon) {
110        p.advance();
111    }
112    // All tokens must now be consumed.
113    if !matches!(p.peek(), Token::Eof) {
114        return Err(Error::InvalidArgument(format!(
115            "unexpected trailing token: {:?}",
116            p.peek()
117        )));
118    }
119    Ok(stmt)
120}
121
122// ── Parser cursor ─────────────────────────────────────────────────────────────
123
124struct Parser {
125    tokens: Vec<Token>,
126    pos: usize,
127}
128
129impl Parser {
130    fn new(tokens: Vec<Token>) -> Self {
131        Parser { tokens, pos: 0 }
132    }
133
134    fn peek(&self) -> &Token {
135        &self.tokens[self.pos]
136    }
137
138    fn peek2(&self) -> &Token {
139        if self.pos + 1 < self.tokens.len() {
140            &self.tokens[self.pos + 1]
141        } else {
142            &Token::Eof
143        }
144    }
145
146    fn advance(&mut self) -> &Token {
147        let tok = &self.tokens[self.pos];
148        if self.pos + 1 < self.tokens.len() {
149            self.pos += 1;
150        }
151        tok
152    }
153
154    fn expect_ident(&mut self) -> Result<String> {
155        match self.advance().clone() {
156            Token::Ident(s) => Ok(s),
157            other => Err(Error::InvalidArgument(format!(
158                "expected identifier, got {:?}",
159                other
160            ))),
161        }
162    }
163
164    /// Accept the next token as an identifier for a label or relationship type.
165    ///
166    /// In standard Cypher, backtick-escaped identifiers allow reserved words to
167    /// be used as labels or relationship types.  After lexing, backtick-quoted
168    /// words are already `Token::Ident`, but *unquoted* reserved words (e.g.
169    /// `CONTAINS`, `ORDER`) are lexed as keyword tokens.  This helper accepts
170    /// both `Token::Ident` and any keyword token and returns the string form,
171    /// so that `:CONTAINS` and `` :`CONTAINS` `` both work.
172    fn expect_label_or_type(&mut self) -> Result<String> {
173        let tok = self.advance().clone();
174        match tok {
175            Token::Ident(s) => Ok(s),
176            ref kw => token_keyword_name(kw)
177                .map(|s| s.to_string())
178                .ok_or_else(|| {
179                    Error::InvalidArgument(format!("expected label/type name, got {:?}", tok))
180                }),
181        }
182    }
183
184    fn expect_tok(&mut self, expected: &Token) -> Result<()> {
185        let got = self.advance().clone();
186        if &got == expected {
187            Ok(())
188        } else {
189            Err(Error::InvalidArgument(format!(
190                "expected {:?}, got {:?}",
191                expected, got
192            )))
193        }
194    }
195
196    /// Consume the next token and return it as a property-name string.
197    ///
198    /// Cypher allows keywords to appear as property names when they follow a
199    /// dot (`n.count`) or appear as map keys (`{count: 42}`).  This helper
200    /// accepts any keyword token that is syntactically unambiguous in a
201    /// property-name position and converts it to its lowercase string
202    /// equivalent (SPA-265).
203    fn advance_as_prop_name(&mut self) -> Result<String> {
204        match self.advance().clone() {
205            Token::Ident(s) => Ok(s),
206            // Aggregate and common keywords that users legitimately use as
207            // property names.
208            Token::Count => Ok("count".into()),
209            Token::With => Ok("with".into()),
210            Token::As => Ok("as".into()),
211            Token::In => Ok("in".into()),
212            Token::Is => Ok("is".into()),
213            Token::Not => Ok("not".into()),
214            Token::And => Ok("and".into()),
215            Token::Or => Ok("or".into()),
216            Token::Contains => Ok("contains".into()),
217            Token::StartsWith => Ok("starts".into()),
218            Token::EndsWith => Ok("ends".into()),
219            Token::Null => Ok("null".into()),
220            Token::True => Ok("true".into()),
221            Token::False => Ok("false".into()),
222            Token::Set => Ok("set".into()),
223            Token::Delete => Ok("delete".into()),
224            Token::Detach => Ok("detach".into()),
225            Token::Merge => Ok("merge".into()),
226            Token::Match => Ok("match".into()),
227            Token::Where => Ok("where".into()),
228            Token::Return => Ok("return".into()),
229            Token::Order => Ok("order".into()),
230            Token::By => Ok("by".into()),
231            Token::Asc => Ok("asc".into()),
232            Token::Desc => Ok("desc".into()),
233            Token::Limit => Ok("limit".into()),
234            Token::Skip => Ok("skip".into()),
235            Token::Distinct => Ok("distinct".into()),
236            Token::Optional => Ok("optional".into()),
237            Token::Union => Ok("union".into()),
238            Token::Unwind => Ok("unwind".into()),
239            Token::Create => Ok("create".into()),
240            Token::Exists => Ok("exists".into()),
241            Token::Any => Ok("any".into()),
242            Token::All => Ok("all".into()),
243            Token::NoneKw => Ok("none".into()),
244            Token::Single => Ok("single".into()),
245            Token::Call => Ok("call".into()),
246            Token::Yield => Ok("yield".into()),
247            Token::Case => Ok("case".into()),
248            Token::When => Ok("when".into()),
249            Token::Then => Ok("then".into()),
250            Token::Else => Ok("else".into()),
251            Token::End => Ok("end".into()),
252            Token::Index => Ok("index".into()),
253            Token::On => Ok("on".into()),
254            Token::Constraint => Ok("constraint".into()),
255            Token::Assert => Ok("assert".into()),
256            other => Err(Error::InvalidArgument(format!(
257                "expected property name, got {:?}",
258                other
259            ))),
260        }
261    }
262}
263
264// ── Statement dispatch ────────────────────────────────────────────────────────
265
266impl Parser {
267    fn parse_statement(&mut self) -> Result<Statement> {
268        match self.peek().clone() {
269            Token::Match => self.parse_match_or_match_mutate(),
270            Token::Create => self.parse_create(),
271            Token::Merge => self.parse_merge(),
272            Token::Checkpoint => {
273                self.advance();
274                Ok(Statement::Checkpoint)
275            }
276            Token::Optimize => {
277                self.advance();
278                Ok(Statement::Optimize)
279            }
280            Token::Optional => self.parse_optional_match(),
281            Token::Union => Err(Error::InvalidArgument(
282                "unexpected UNION: use 'MATCH ... RETURN ... UNION MATCH ... RETURN ...'".into(),
283            )),
284            Token::Unwind => self.parse_unwind(),
285            // Standalone RETURN (no MATCH): `RETURN expr [AS alias], ...`
286            Token::Return => self.parse_standalone_return(),
287            // CALL procedure(args) YIELD col [RETURN ...]
288            Token::Call => self.parse_call(),
289            other => Err(Error::InvalidArgument(format!(
290                "unexpected token at statement start: {:?}",
291                other
292            ))),
293        }
294    }
295
296    /// Parse `RETURN expr [AS alias], ...` with no preceding MATCH clause.
297    ///
298    /// Emits a `Statement::Match` with an empty pattern list.  The execution
299    /// engine detects the empty pattern and evaluates the RETURN items as
300    /// pure scalar expressions (functions, literals, etc.).
301    fn parse_standalone_return(&mut self) -> Result<Statement> {
302        self.expect_tok(&Token::Return)?;
303        let distinct = if matches!(self.peek(), Token::Distinct) {
304            self.advance();
305            true
306        } else {
307            false
308        };
309        let items = self.parse_return_items()?;
310        Ok(Statement::Match(MatchStatement {
311            pattern: vec![],
312            where_clause: None,
313            return_clause: ReturnClause { items },
314            order_by: vec![],
315            skip: None,
316            limit: None,
317            distinct,
318        }))
319    }
320
321    // ── MATCH (or MATCH ... CREATE / SET / DELETE) ────────────────────────────
322
323    fn parse_match_or_match_mutate(&mut self) -> Result<Statement> {
324        // Parse the MATCH clause first, then dispatch on the following keyword.
325        self.expect_tok(&Token::Match)?;
326
327        let patterns = self.parse_pattern_list()?;
328
329        match self.peek().clone() {
330            Token::Create => {
331                // MATCH ... CREATE — used to add edges
332                self.advance();
333                let create = self.parse_create_body()?;
334                Ok(Statement::MatchCreate(MatchCreateStatement {
335                    match_patterns: patterns,
336                    match_props: vec![],
337                    create,
338                }))
339            }
340            Token::Set => {
341                // MATCH ... SET var.prop = expr
342                self.advance();
343                let var = self.expect_ident()?;
344                self.expect_tok(&Token::Dot)?;
345                let prop = self.expect_ident()?;
346                self.expect_tok(&Token::Eq)?;
347                let value = self.parse_expr()?;
348                Ok(Statement::MatchMutate(MatchMutateStatement {
349                    match_patterns: patterns,
350                    where_clause: None,
351                    mutation: Mutation::Set { var, prop, value },
352                }))
353            }
354            Token::Delete => {
355                // MATCH ... DELETE var
356                self.advance();
357                let var = self.expect_ident()?;
358                Ok(Statement::MatchMutate(MatchMutateStatement {
359                    match_patterns: patterns,
360                    where_clause: None,
361                    mutation: Mutation::Delete { var },
362                }))
363            }
364            Token::Where => {
365                // MATCH ... WHERE expr (SET|DELETE|RETURN)
366                self.advance();
367                let where_expr = self.parse_expr()?;
368                match self.peek().clone() {
369                    Token::Set => {
370                        self.advance();
371                        let var = self.expect_ident()?;
372                        self.expect_tok(&Token::Dot)?;
373                        let prop = self.expect_ident()?;
374                        self.expect_tok(&Token::Eq)?;
375                        let value = self.parse_expr()?;
376                        Ok(Statement::MatchMutate(MatchMutateStatement {
377                            match_patterns: patterns,
378                            where_clause: Some(where_expr),
379                            mutation: Mutation::Set { var, prop, value },
380                        }))
381                    }
382                    Token::Delete => {
383                        self.advance();
384                        let var = self.expect_ident()?;
385                        Ok(Statement::MatchMutate(MatchMutateStatement {
386                            match_patterns: patterns,
387                            where_clause: Some(where_expr),
388                            mutation: Mutation::Delete { var },
389                        }))
390                    }
391                    Token::With => {
392                        // MATCH … WHERE … WITH … RETURN
393                        self.parse_with_pipeline(patterns, Some(where_expr))
394                    }
395                    Token::Merge => {
396                        // MATCH … WHERE … MERGE (a)-[r:TYPE]->(b)
397                        self.parse_match_merge_rel_tail(patterns, Some(where_expr))
398                    }
399                    _ => {
400                        // Fall through to RETURN parsing with the parsed WHERE expr.
401                        self.finish_match_return(patterns, Some(where_expr))
402                    }
403                }
404            }
405            Token::With => {
406                // MATCH … WITH … RETURN pipeline
407                self.parse_with_pipeline(patterns, None)
408            }
409            Token::Merge => {
410                // MATCH … MERGE (a)-[r:TYPE]->(b) — find-or-create relationship (SPA-233)
411                self.parse_match_merge_rel_tail(patterns, None)
412            }
413            Token::Return
414            | Token::Order
415            | Token::Skip
416            | Token::Limit
417            | Token::Eof
418            | Token::Semicolon => self.finish_match_return(patterns, None),
419            Token::Optional => {
420                // MATCH … OPTIONAL MATCH … RETURN
421                self.parse_match_optional_match_tail(patterns, None)
422            }
423            other => Err(Error::InvalidArgument(format!(
424                "unexpected token after MATCH pattern: {:?}",
425                other
426            ))),
427        }
428    }
429
430    // ── OPTIONAL MATCH (standalone) ───────────────────────────────────────────
431
432    /// Parse `OPTIONAL MATCH pattern [WHERE expr] RETURN …`
433    fn parse_optional_match(&mut self) -> Result<Statement> {
434        self.expect_tok(&Token::Optional)?;
435        self.expect_tok(&Token::Match)?;
436
437        let patterns = self.parse_pattern_list()?;
438
439        // Optional WHERE clause.
440        let where_clause = if matches!(self.peek(), Token::Where) {
441            self.advance();
442            Some(self.parse_expr()?)
443        } else {
444            None
445        };
446
447        // RETURN clause
448        self.expect_tok(&Token::Return)?;
449        let distinct = if matches!(self.peek(), Token::Distinct) {
450            self.advance();
451            true
452        } else {
453            false
454        };
455        let items = self.parse_return_items()?;
456        let return_clause = ReturnClause { items };
457
458        // ORDER BY
459        let order_by = if matches!(self.peek(), Token::Order) {
460            self.advance();
461            self.expect_tok(&Token::By)?;
462            self.parse_order_by_items()?
463        } else {
464            vec![]
465        };
466
467        // SKIP
468        let skip = if matches!(self.peek(), Token::Skip) {
469            self.advance();
470            match self.advance().clone() {
471                Token::Integer(n) => {
472                    if n < 0 {
473                        return Err(Error::InvalidArgument("SKIP must be non-negative".into()));
474                    }
475                    Some(n as u64)
476                }
477                other => {
478                    return Err(Error::InvalidArgument(format!(
479                        "expected integer after SKIP, got {:?}",
480                        other
481                    )))
482                }
483            }
484        } else {
485            None
486        };
487
488        // LIMIT
489        let limit = if matches!(self.peek(), Token::Limit) {
490            self.advance();
491            match self.advance().clone() {
492                Token::Integer(n) => {
493                    if n < 0 {
494                        return Err(Error::InvalidArgument("LIMIT must be non-negative".into()));
495                    }
496                    Some(n as u64)
497                }
498                other => {
499                    return Err(Error::InvalidArgument(format!(
500                        "expected integer after LIMIT, got {:?}",
501                        other
502                    )))
503                }
504            }
505        } else {
506            None
507        };
508
509        Ok(Statement::OptionalMatch(OptionalMatchStatement {
510            pattern: patterns,
511            where_clause,
512            return_clause,
513            order_by,
514            skip,
515            limit,
516            distinct,
517        }))
518    }
519
520    // ── MATCH … OPTIONAL MATCH … RETURN ───────────────────────────────────────
521
522    /// Parse the `OPTIONAL MATCH … RETURN` tail after `MATCH patterns` has been
523    /// consumed.  `match_patterns` is already parsed; `match_where` is the
524    /// WHERE predicate from the leading MATCH (if any).
525    fn parse_match_optional_match_tail(
526        &mut self,
527        match_patterns: Vec<PathPattern>,
528        match_where: Option<Expr>,
529    ) -> Result<Statement> {
530        // Consume OPTIONAL MATCH.
531        self.expect_tok(&Token::Optional)?;
532        self.expect_tok(&Token::Match)?;
533
534        let optional_patterns = self.parse_pattern_list()?;
535
536        // Optional WHERE clause on the OPTIONAL MATCH.
537        let optional_where = if matches!(self.peek(), Token::Where) {
538            self.advance();
539            Some(self.parse_expr()?)
540        } else {
541            None
542        };
543
544        // RETURN clause.
545        self.expect_tok(&Token::Return)?;
546        let distinct = if matches!(self.peek(), Token::Distinct) {
547            self.advance();
548            true
549        } else {
550            false
551        };
552        let items = self.parse_return_items()?;
553        let return_clause = ReturnClause { items };
554
555        // ORDER BY
556        let order_by = if matches!(self.peek(), Token::Order) {
557            self.advance();
558            self.expect_tok(&Token::By)?;
559            self.parse_order_by_items()?
560        } else {
561            vec![]
562        };
563
564        // SKIP
565        let skip = if matches!(self.peek(), Token::Skip) {
566            self.advance();
567            match self.advance().clone() {
568                Token::Integer(n) => {
569                    if n < 0 {
570                        return Err(Error::InvalidArgument("SKIP must be non-negative".into()));
571                    }
572                    Some(n as u64)
573                }
574                other => {
575                    return Err(Error::InvalidArgument(format!(
576                        "expected integer after SKIP, got {:?}",
577                        other
578                    )))
579                }
580            }
581        } else {
582            None
583        };
584
585        // LIMIT
586        let limit = if matches!(self.peek(), Token::Limit) {
587            self.advance();
588            match self.advance().clone() {
589                Token::Integer(n) => {
590                    if n < 0 {
591                        return Err(Error::InvalidArgument("LIMIT must be non-negative".into()));
592                    }
593                    Some(n as u64)
594                }
595                other => {
596                    return Err(Error::InvalidArgument(format!(
597                        "expected integer after LIMIT, got {:?}",
598                        other
599                    )))
600                }
601            }
602        } else {
603            None
604        };
605
606        Ok(Statement::MatchOptionalMatch(MatchOptionalMatchStatement {
607            match_patterns,
608            match_where,
609            optional_patterns,
610            optional_where,
611            return_clause,
612            order_by,
613            skip,
614            limit,
615            distinct,
616        }))
617    }
618
619    /// Shared helper: finish parsing a MATCH … RETURN statement after the
620    /// pattern list (and optional WHERE expr) have already been consumed.
621    fn finish_match_return(
622        &mut self,
623        patterns: Vec<PathPattern>,
624        pre_where: Option<Expr>,
625    ) -> Result<Statement> {
626        // If caller already parsed WHERE, use it; otherwise try to parse it now.
627        let where_clause = if pre_where.is_some() { pre_where } else { None };
628
629        // RETURN clause
630        let (distinct, return_clause) = if matches!(self.peek(), Token::Return) {
631            self.advance();
632            let distinct = if matches!(self.peek(), Token::Distinct) {
633                self.advance();
634                true
635            } else {
636                false
637            };
638            let items = self.parse_return_items()?;
639            (distinct, ReturnClause { items })
640        } else {
641            return Err(Error::InvalidArgument("expected RETURN clause".into()));
642        };
643
644        // ORDER BY
645        let order_by = if matches!(self.peek(), Token::Order) {
646            self.advance();
647            self.expect_tok(&Token::By)?;
648            self.parse_order_by_items()?
649        } else {
650            vec![]
651        };
652
653        // SKIP
654        let skip = if matches!(self.peek(), Token::Skip) {
655            self.advance();
656            match self.advance().clone() {
657                Token::Integer(n) => {
658                    if n < 0 {
659                        return Err(Error::InvalidArgument("SKIP must be non-negative".into()));
660                    }
661                    Some(n as u64)
662                }
663                other => {
664                    return Err(Error::InvalidArgument(format!(
665                        "expected integer after SKIP, got {:?}",
666                        other
667                    )))
668                }
669            }
670        } else {
671            None
672        };
673
674        // LIMIT
675        let limit = if matches!(self.peek(), Token::Limit) {
676            self.advance();
677            match self.advance().clone() {
678                Token::Integer(n) => {
679                    if n < 0 {
680                        return Err(Error::InvalidArgument("LIMIT must be non-negative".into()));
681                    }
682                    Some(n as u64)
683                }
684                other => {
685                    return Err(Error::InvalidArgument(format!(
686                        "expected integer after LIMIT, got {:?}",
687                        other
688                    )))
689                }
690            }
691        } else {
692            None
693        };
694
695        Ok(Statement::Match(MatchStatement {
696            pattern: patterns,
697            where_clause,
698            return_clause,
699            order_by,
700            skip,
701            limit,
702            distinct,
703        }))
704    }
705
706    // ── MATCH … WITH … RETURN pipeline ────────────────────────────────────────
707
708    /// Parse `MATCH pattern [WHERE pred] WITH expr AS alias [, …] [WHERE pred] RETURN …`.
709    fn parse_with_pipeline(
710        &mut self,
711        patterns: Vec<PathPattern>,
712        match_where: Option<Expr>,
713    ) -> Result<Statement> {
714        use crate::ast::MatchWithStatement;
715
716        // Consume WITH token.
717        self.expect_tok(&Token::With)?;
718
719        // Parse one or more `expr AS alias` items separated by commas.
720        let mut items: Vec<WithItem> = Vec::new();
721        loop {
722            let expr = self.parse_expr()?;
723            self.expect_tok(&Token::As)?;
724            let alias = self.expect_ident()?;
725            items.push(WithItem { expr, alias });
726            if matches!(self.peek(), Token::Comma) {
727                self.advance();
728            } else {
729                break;
730            }
731        }
732
733        // Optional WHERE clause on the WITH stage.
734        let with_where = if matches!(self.peek(), Token::Where) {
735            self.advance();
736            Some(self.parse_expr()?)
737        } else {
738            None
739        };
740
741        let with_clause = WithClause {
742            items,
743            where_clause: with_where,
744        };
745
746        // Optional ORDER BY / SKIP / LIMIT that belong to this WITH stage.
747        // These are consumed before checking for a continuation clause (MATCH/WITH/UNWIND).
748        let with_order_by = if matches!(self.peek(), Token::Order) {
749            self.advance();
750            self.expect_tok(&Token::By)?;
751            self.parse_order_by_items()?
752        } else {
753            vec![]
754        };
755
756        let with_skip = if matches!(self.peek(), Token::Skip) {
757            self.advance();
758            match self.advance().clone() {
759                Token::Integer(n) => {
760                    if n < 0 {
761                        return Err(Error::InvalidArgument("SKIP must be non-negative".into()));
762                    }
763                    Some(n as u64)
764                }
765                other => {
766                    return Err(Error::InvalidArgument(format!(
767                        "expected integer after SKIP, got {:?}",
768                        other
769                    )))
770                }
771            }
772        } else {
773            None
774        };
775
776        let with_limit = if matches!(self.peek(), Token::Limit) {
777            self.advance();
778            match self.advance().clone() {
779                Token::Integer(n) => {
780                    if n < 0 {
781                        return Err(Error::InvalidArgument("LIMIT must be non-negative".into()));
782                    }
783                    Some(n as u64)
784                }
785                other => {
786                    return Err(Error::InvalidArgument(format!(
787                        "expected integer after LIMIT, got {:?}",
788                        other
789                    )))
790                }
791            }
792        } else {
793            None
794        };
795
796        // Peek at the next token to decide: RETURN → simple MatchWith,
797        // MATCH/WITH/UNWIND → multi-stage Pipeline (SPA-134).
798        match self.peek().clone() {
799            Token::Return => {
800                // Simple single-WITH pipeline: MATCH … WITH … RETURN …
801                self.advance(); // consume RETURN
802                let distinct = if matches!(self.peek(), Token::Distinct) {
803                    self.advance();
804                    true
805                } else {
806                    false
807                };
808                let return_items = self.parse_return_items()?;
809                let return_clause = ReturnClause {
810                    items: return_items,
811                };
812
813                // ORDER BY / SKIP / LIMIT on the RETURN (i.e. not on the WITH).
814                let order_by = if matches!(self.peek(), Token::Order) {
815                    self.advance();
816                    self.expect_tok(&Token::By)?;
817                    self.parse_order_by_items()?
818                } else {
819                    with_order_by
820                };
821
822                let skip = if matches!(self.peek(), Token::Skip) {
823                    self.advance();
824                    match self.advance().clone() {
825                        Token::Integer(n) => {
826                            if n < 0 {
827                                return Err(Error::InvalidArgument(
828                                    "SKIP must be non-negative".into(),
829                                ));
830                            }
831                            Some(n as u64)
832                        }
833                        other => {
834                            return Err(Error::InvalidArgument(format!(
835                                "expected integer after SKIP, got {:?}",
836                                other
837                            )))
838                        }
839                    }
840                } else {
841                    with_skip
842                };
843
844                let limit = if matches!(self.peek(), Token::Limit) {
845                    self.advance();
846                    match self.advance().clone() {
847                        Token::Integer(n) => {
848                            if n < 0 {
849                                return Err(Error::InvalidArgument(
850                                    "LIMIT must be non-negative".into(),
851                                ));
852                            }
853                            Some(n as u64)
854                        }
855                        other => {
856                            return Err(Error::InvalidArgument(format!(
857                                "expected integer after LIMIT, got {:?}",
858                                other
859                            )))
860                        }
861                    }
862                } else {
863                    with_limit
864                };
865
866                Ok(Statement::MatchWith(MatchWithStatement {
867                    match_patterns: patterns,
868                    match_where,
869                    with_clause,
870                    return_clause,
871                    order_by,
872                    skip,
873                    limit,
874                    distinct,
875                }))
876            }
877            // Continuation clause: MATCH, WITH, or UNWIND → build Pipeline.
878            Token::Match | Token::With | Token::Unwind => {
879                let first_with_stage = PipelineStage::With {
880                    clause: with_clause,
881                    order_by: with_order_by,
882                    skip: with_skip,
883                    limit: with_limit,
884                };
885                self.parse_pipeline_continuation(
886                    Some(patterns),
887                    match_where,
888                    None,
889                    vec![first_with_stage],
890                )
891            }
892            other => Err(Error::InvalidArgument(format!(
893                "expected RETURN, MATCH, WITH, or UNWIND after WITH clause, got {:?}",
894                other
895            ))),
896        }
897    }
898
899    /// Parse the remainder of a multi-clause pipeline after the initial stages have
900    /// been set up.  Accumulates additional MATCH / WITH / UNWIND stages until a
901    /// RETURN clause terminates the pipeline.
902    fn parse_pipeline_continuation(
903        &mut self,
904        leading_match: Option<Vec<PathPattern>>,
905        leading_where: Option<Expr>,
906        leading_unwind: Option<(crate::ast::Expr, String)>,
907        mut stages: Vec<PipelineStage>,
908    ) -> Result<Statement> {
909        loop {
910            match self.peek().clone() {
911                Token::Match => {
912                    // Parse a MATCH stage.
913                    self.advance(); // consume MATCH
914                    let patterns = self.parse_pattern_list()?;
915                    let where_clause = if matches!(self.peek(), Token::Where) {
916                        self.advance();
917                        Some(self.parse_expr()?)
918                    } else {
919                        None
920                    };
921                    stages.push(PipelineStage::Match {
922                        patterns,
923                        where_clause,
924                    });
925                }
926                Token::With => {
927                    // Parse a WITH stage.
928                    self.advance(); // consume WITH
929                    let mut items: Vec<WithItem> = Vec::new();
930                    loop {
931                        let expr = self.parse_expr()?;
932                        self.expect_tok(&Token::As)?;
933                        let alias = self.expect_ident()?;
934                        items.push(WithItem { expr, alias });
935                        if matches!(self.peek(), Token::Comma) {
936                            self.advance();
937                        } else {
938                            break;
939                        }
940                    }
941                    let where_clause = if matches!(self.peek(), Token::Where) {
942                        self.advance();
943                        Some(self.parse_expr()?)
944                    } else {
945                        None
946                    };
947                    let clause = WithClause {
948                        items,
949                        where_clause,
950                    };
951                    // ORDER BY / SKIP / LIMIT on this intermediate WITH.
952                    let order_by = if matches!(self.peek(), Token::Order) {
953                        self.advance();
954                        self.expect_tok(&Token::By)?;
955                        self.parse_order_by_items()?
956                    } else {
957                        vec![]
958                    };
959                    let skip = if matches!(self.peek(), Token::Skip) {
960                        self.advance();
961                        match self.advance().clone() {
962                            Token::Integer(n) => {
963                                if n < 0 {
964                                    return Err(Error::InvalidArgument(
965                                        "SKIP must be non-negative".into(),
966                                    ));
967                                }
968                                Some(n as u64)
969                            }
970                            other => {
971                                return Err(Error::InvalidArgument(format!(
972                                    "expected integer after SKIP, got {:?}",
973                                    other
974                                )))
975                            }
976                        }
977                    } else {
978                        None
979                    };
980                    let limit = if matches!(self.peek(), Token::Limit) {
981                        self.advance();
982                        match self.advance().clone() {
983                            Token::Integer(n) => {
984                                if n < 0 {
985                                    return Err(Error::InvalidArgument(
986                                        "LIMIT must be non-negative".into(),
987                                    ));
988                                }
989                                Some(n as u64)
990                            }
991                            other => {
992                                return Err(Error::InvalidArgument(format!(
993                                    "expected integer after LIMIT, got {:?}",
994                                    other
995                                )))
996                            }
997                        }
998                    } else {
999                        None
1000                    };
1001                    stages.push(PipelineStage::With {
1002                        clause,
1003                        order_by,
1004                        skip,
1005                        limit,
1006                    });
1007                }
1008                Token::Unwind => {
1009                    // Parse an UNWIND stage: UNWIND alias_var AS new_alias.
1010                    self.advance(); // consume UNWIND
1011                                    // In pipeline context the "list" to unwind is a variable name.
1012                    let alias = self.expect_ident()?;
1013                    self.expect_tok(&Token::As)?;
1014                    let new_alias = self.expect_ident()?;
1015                    stages.push(PipelineStage::Unwind { alias, new_alias });
1016                }
1017                Token::Return => {
1018                    // Terminal clause.
1019                    self.advance(); // consume RETURN
1020                    let distinct = if matches!(self.peek(), Token::Distinct) {
1021                        self.advance();
1022                        true
1023                    } else {
1024                        false
1025                    };
1026                    let return_items = self.parse_return_items()?;
1027                    let return_clause = ReturnClause {
1028                        items: return_items,
1029                    };
1030                    let return_order_by = if matches!(self.peek(), Token::Order) {
1031                        self.advance();
1032                        self.expect_tok(&Token::By)?;
1033                        self.parse_order_by_items()?
1034                    } else {
1035                        vec![]
1036                    };
1037                    let return_skip = if matches!(self.peek(), Token::Skip) {
1038                        self.advance();
1039                        match self.advance().clone() {
1040                            Token::Integer(n) => {
1041                                if n < 0 {
1042                                    return Err(Error::InvalidArgument(
1043                                        "SKIP must be non-negative".into(),
1044                                    ));
1045                                }
1046                                Some(n as u64)
1047                            }
1048                            other => {
1049                                return Err(Error::InvalidArgument(format!(
1050                                    "expected integer after SKIP, got {:?}",
1051                                    other
1052                                )))
1053                            }
1054                        }
1055                    } else {
1056                        None
1057                    };
1058                    let return_limit = if matches!(self.peek(), Token::Limit) {
1059                        self.advance();
1060                        match self.advance().clone() {
1061                            Token::Integer(n) => {
1062                                if n < 0 {
1063                                    return Err(Error::InvalidArgument(
1064                                        "LIMIT must be non-negative".into(),
1065                                    ));
1066                                }
1067                                Some(n as u64)
1068                            }
1069                            other => {
1070                                return Err(Error::InvalidArgument(format!(
1071                                    "expected integer after LIMIT, got {:?}",
1072                                    other
1073                                )))
1074                            }
1075                        }
1076                    } else {
1077                        None
1078                    };
1079                    return Ok(Statement::Pipeline(PipelineStatement {
1080                        leading_match,
1081                        leading_where,
1082                        leading_unwind,
1083                        stages,
1084                        return_clause,
1085                        return_order_by,
1086                        return_skip,
1087                        return_limit,
1088                        distinct,
1089                    }));
1090                }
1091                other => {
1092                    return Err(Error::InvalidArgument(format!(
1093                        "expected MATCH, WITH, UNWIND, or RETURN in pipeline, got {:?}",
1094                        other
1095                    )))
1096                }
1097            }
1098        }
1099    }
1100
1101    // ── MERGE ─────────────────────────────────────────────────────────────────
1102
1103    /// Parse `MERGE (:Label {prop: val, ...})` and `MATCH...MERGE (a)-[:R]->(b)` patterns.
1104    ///
1105    /// Supports single-node MERGE and relationship MERGE via the match-merge path.
1106    fn parse_merge(&mut self) -> Result<Statement> {
1107        self.expect_tok(&Token::Merge)?;
1108        self.expect_tok(&Token::LParen)?;
1109
1110        // Optional variable name — capture it so RETURN can reference it.
1111        let var = if let Token::Ident(_) = self.peek().clone() {
1112            if matches!(self.peek2(), Token::Colon) {
1113                // `n:Label` — capture variable name.
1114                let name = match self.advance().clone() {
1115                    Token::Ident(s) => s,
1116                    _ => unreachable!(),
1117                };
1118                name
1119            } else {
1120                // ambiguous or anonymous — treat as anonymous
1121                String::new()
1122            }
1123        } else {
1124            String::new()
1125        };
1126
1127        // Label(s) — at least one required for MERGE.
1128        if !matches!(self.peek(), Token::Colon) {
1129            return Err(Error::InvalidArgument(
1130                "MERGE requires a label (e.g. MERGE (:Person {...}))".into(),
1131            ));
1132        }
1133        self.advance(); // consume ':'
1134        let label = self.expect_label_or_type()?;
1135
1136        // Property map (optional but typical for MERGE).
1137        let props = if matches!(self.peek(), Token::LBrace) {
1138            self.parse_prop_map()?
1139        } else {
1140            vec![]
1141        };
1142
1143        self.expect_tok(&Token::RParen)?;
1144
1145        // Optional RETURN clause.
1146        let return_clause = if matches!(self.peek(), Token::Return) {
1147            self.advance(); // consume RETURN
1148            let items = self.parse_return_items()?;
1149            Some(ReturnClause { items })
1150        } else {
1151            None
1152        };
1153
1154        Ok(Statement::Merge(MergeStatement {
1155            var,
1156            label,
1157            props,
1158            return_clause,
1159        }))
1160    }
1161
1162    // ── MATCH … MERGE relationship ────────────────────────────────────────────
1163
1164    /// Parse the `MERGE (a)-[r:TYPE]->(b)` tail after a MATCH clause (SPA-233).
1165    fn parse_match_merge_rel_tail(
1166        &mut self,
1167        match_patterns: Vec<PathPattern>,
1168        where_clause: Option<Expr>,
1169    ) -> Result<Statement> {
1170        self.expect_tok(&Token::Merge)?;
1171
1172        let src_node = self.parse_node_pattern()?;
1173        let src_var = src_node.var;
1174
1175        if !matches!(self.peek(), Token::Dash | Token::Arrow | Token::LeftArrow) {
1176            return Err(Error::InvalidArgument(format!(
1177                "expected relationship pattern after node in MERGE, got {:?}",
1178                self.peek()
1179            )));
1180        }
1181        let rel_pat = self.parse_rel_pattern()?;
1182        if rel_pat.dir != EdgeDir::Outgoing {
1183            return Err(Error::InvalidArgument(
1184                "MERGE relationship pattern must use outgoing direction: (a)-[r:TYPE]->(b)".into(),
1185            ));
1186        }
1187
1188        let dst_node = self.parse_node_pattern()?;
1189        let dst_var = dst_node.var;
1190
1191        // Optional `ON CREATE SET …` / `ON MATCH SET …` clauses — parse and discard.
1192        while matches!(self.peek(), Token::On) {
1193            self.advance(); // consume ON
1194            self.skip_on_clause()?;
1195        }
1196
1197        Ok(Statement::MatchMergeRel(MatchMergeRelStatement {
1198            match_patterns,
1199            where_clause,
1200            src_var,
1201            rel_var: rel_pat.var,
1202            rel_type: rel_pat.rel_type,
1203            dst_var,
1204        }))
1205    }
1206
1207    /// Skip an `ON CREATE SET …` or `ON MATCH SET …` clause after a MERGE.
1208    fn skip_on_clause(&mut self) -> Result<()> {
1209        match self.peek() {
1210            Token::Create | Token::Match => {
1211                self.advance();
1212            }
1213            other => {
1214                return Err(Error::InvalidArgument(format!(
1215                    "expected CREATE or MATCH after ON, got {:?}",
1216                    other
1217                )));
1218            }
1219        }
1220        loop {
1221            match self.peek() {
1222                Token::Eof
1223                | Token::Semicolon
1224                | Token::Return
1225                | Token::With
1226                | Token::Merge
1227                | Token::On => break,
1228                _ => {
1229                    self.advance();
1230                }
1231            }
1232        }
1233        Ok(())
1234    }
1235
1236    // ── CREATE ────────────────────────────────────────────────────────────────
1237
1238    fn parse_create(&mut self) -> Result<Statement> {
1239        self.expect_tok(&Token::Create)?;
1240        if matches!(self.peek(), Token::Index) {
1241            self.advance();
1242            return self.parse_create_index();
1243        }
1244        if matches!(self.peek(), Token::Constraint) {
1245            self.advance();
1246            return self.parse_create_constraint();
1247        }
1248        let body = self.parse_create_body()?;
1249        Ok(Statement::Create(body))
1250    }
1251    fn parse_create_index(&mut self) -> Result<Statement> {
1252        self.expect_tok(&Token::On)?;
1253        self.expect_tok(&Token::Colon)?;
1254        let label = self.expect_label_or_type()?;
1255        self.expect_tok(&Token::LParen)?;
1256        let property = self.expect_ident()?;
1257        self.expect_tok(&Token::RParen)?;
1258        Ok(Statement::CreateIndex { label, property })
1259    }
1260    fn parse_create_constraint(&mut self) -> Result<Statement> {
1261        self.expect_tok(&Token::On)?;
1262        self.expect_tok(&Token::LParen)?;
1263        let _var = self.expect_ident()?;
1264        self.expect_tok(&Token::Colon)?;
1265        let label = self.expect_label_or_type()?;
1266        self.expect_tok(&Token::RParen)?;
1267        match self.advance().clone() {
1268            Token::Assert => {}
1269            other => {
1270                return Err(Error::InvalidArgument(format!(
1271                    "expected ASSERT, got {:?}",
1272                    other
1273                )))
1274            }
1275        }
1276        let _prop_var = self.expect_ident()?;
1277        self.expect_tok(&Token::Dot)?;
1278        let property = self.expect_ident()?;
1279        self.expect_tok(&Token::Is)?;
1280        match self.advance().clone() {
1281            Token::Ident(ref s) if s.eq_ignore_ascii_case("UNIQUE") => {}
1282            other => {
1283                return Err(Error::InvalidArgument(format!(
1284                    "expected UNIQUE, got {:?}",
1285                    other
1286                )))
1287            }
1288        }
1289        Ok(Statement::CreateConstraint { label, property })
1290    }
1291
1292    // ── UNWIND ────────────────────────────────────────────────────────────────
1293
1294    /// Parse `UNWIND <expr> AS <var> RETURN <items>`.
1295    ///
1296    /// The list expression may be:
1297    /// - A list literal:    `[1, 2, 3]`
1298    /// - A parameter ref:   `$items`
1299    ///
1300    /// NOTE: `range(start, end)` function support is a TODO — it will be added
1301    /// when the function-call execution layer is extended.
1302    fn parse_unwind(&mut self) -> Result<Statement> {
1303        self.expect_tok(&Token::Unwind)?;
1304        let expr = self.parse_unwind_expr()?;
1305        self.expect_tok(&Token::As)?;
1306        let alias = self.expect_ident()?;
1307
1308        // If the next token is WITH or MATCH, this is a pipeline:
1309        //   UNWIND … WITH … RETURN  (SPA-134)
1310        //   UNWIND … MATCH … RETURN (SPA-237)
1311        if matches!(self.peek(), Token::With | Token::Match) {
1312            return self.parse_pipeline_continuation(None, None, Some((expr, alias)), vec![]);
1313        }
1314
1315        self.expect_tok(&Token::Return)?;
1316        let items = self.parse_return_items()?;
1317        Ok(Statement::Unwind(UnwindStatement {
1318            expr,
1319            alias,
1320            return_clause: ReturnClause { items },
1321        }))
1322    }
1323
1324    /// Parse the list-producing expression for UNWIND.
1325    ///
1326    /// Accepts:
1327    /// - `[elem, ...]`  — list literal
1328    /// - `$param`       — parameter (evaluated at runtime to a list)
1329    /// - `fn(args)`     — function call that returns a list (e.g. `range(1, 5)`)
1330    fn parse_unwind_expr(&mut self) -> Result<Expr> {
1331        match self.peek().clone() {
1332            Token::LBracket => self.parse_list_literal(),
1333            Token::Param(p) => {
1334                self.advance();
1335                Ok(Expr::Literal(Literal::Param(p)))
1336            }
1337            Token::Ident(_) => {
1338                // May be a function call like range(1, 5).
1339                self.parse_atom()
1340            }
1341            other => Err(Error::InvalidArgument(format!(
1342                "UNWIND expects a list literal [..], $param, or a function call, got {:?}",
1343                other
1344            ))),
1345        }
1346    }
1347
1348    /// Parse `[expr, expr, ...]` into `Expr::List`.
1349    fn parse_list_literal(&mut self) -> Result<Expr> {
1350        self.expect_tok(&Token::LBracket)?;
1351        let mut elems = Vec::new();
1352        if !matches!(self.peek(), Token::RBracket) {
1353            loop {
1354                elems.push(self.parse_expr()?);
1355                if matches!(self.peek(), Token::Comma) {
1356                    self.advance();
1357                } else {
1358                    break;
1359                }
1360            }
1361        }
1362        self.expect_tok(&Token::RBracket)?;
1363        Ok(Expr::List(elems))
1364    }
1365
1366    fn parse_create_body(&mut self) -> Result<CreateStatement> {
1367        let mut nodes = Vec::new();
1368        let mut edges: Vec<(String, RelPattern, String)> = Vec::new();
1369
1370        loop {
1371            if !matches!(self.peek(), Token::LParen) {
1372                break;
1373            }
1374            // Could be a node or the start of an edge
1375            let node = self.parse_node_pattern()?;
1376            let node_var = node.var.clone();
1377
1378            if matches!(self.peek(), Token::Dash | Token::Arrow | Token::LeftArrow) {
1379                // Edge pattern: (a)-[:R]->(b)
1380                // Both endpoint nodes are always emitted so that the executor
1381                // can create them and resolve variable bindings for the edge.
1382                // Nodes without a variable name are anonymous and need not be
1383                // tracked, but they still get created.
1384                nodes.push(node);
1385                let rel = self.parse_rel_pattern()?;
1386                let dst_node = self.parse_node_pattern()?;
1387                let dst_var = dst_node.var.clone();
1388                edges.push((node_var, rel, dst_var));
1389                nodes.push(dst_node);
1390            } else {
1391                nodes.push(node);
1392            }
1393
1394            if matches!(self.peek(), Token::Comma) {
1395                self.advance();
1396            } else {
1397                break;
1398            }
1399        }
1400
1401        if nodes.is_empty() && edges.is_empty() {
1402            return Err(Error::InvalidArgument(
1403                "CREATE body must contain at least one node or edge pattern".into(),
1404            ));
1405        }
1406
1407        Ok(CreateStatement { nodes, edges })
1408    }
1409
1410    // ── Pattern list ──────────────────────────────────────────────────────────
1411
1412    fn parse_pattern_list(&mut self) -> Result<Vec<PathPattern>> {
1413        let mut patterns = Vec::new();
1414        patterns.push(self.parse_path_pattern()?);
1415        while matches!(self.peek(), Token::Comma) {
1416            self.advance();
1417            patterns.push(self.parse_path_pattern()?);
1418        }
1419        Ok(patterns)
1420    }
1421
1422    fn parse_path_pattern(&mut self) -> Result<PathPattern> {
1423        let mut nodes = Vec::new();
1424        let mut rels = Vec::new();
1425
1426        nodes.push(self.parse_node_pattern()?);
1427
1428        while matches!(self.peek(), Token::Dash | Token::Arrow | Token::LeftArrow) {
1429            // Check for variable-length paths: -[:R*n..m]->
1430            let rel = self.parse_rel_pattern()?;
1431            nodes.push(self.parse_node_pattern()?);
1432            rels.push(rel);
1433        }
1434
1435        Ok(PathPattern { nodes, rels })
1436    }
1437
1438    // ── Node pattern ──────────────────────────────────────────────────────────
1439
1440    fn parse_node_pattern(&mut self) -> Result<NodePattern> {
1441        self.expect_tok(&Token::LParen)?;
1442
1443        let var = match self.peek().clone() {
1444            Token::Ident(s) if !matches!(self.peek2(), Token::LParen) => {
1445                self.advance();
1446                s
1447            }
1448            _ => String::new(),
1449        };
1450
1451        let mut labels = Vec::new();
1452        while matches!(self.peek(), Token::Colon) {
1453            self.advance();
1454            let label = self.expect_label_or_type()?;
1455            labels.push(label);
1456        }
1457
1458        let props = if matches!(self.peek(), Token::LBrace) {
1459            self.parse_prop_map()?
1460        } else {
1461            vec![]
1462        };
1463
1464        self.expect_tok(&Token::RParen)?;
1465        Ok(NodePattern { var, labels, props })
1466    }
1467
1468    // ── Relationship pattern ──────────────────────────────────────────────────
1469
1470    fn parse_rel_pattern(&mut self) -> Result<RelPattern> {
1471        // Syntax: -[:REL]-> or <-[:REL]- or -[:REL]-
1472        let incoming = if matches!(self.peek(), Token::LeftArrow) {
1473            self.advance();
1474            true
1475        } else if matches!(self.peek(), Token::Dash) {
1476            self.advance();
1477            false
1478        } else {
1479            return Err(Error::InvalidArgument(format!(
1480                "expected - or <- for relationship, got {:?}",
1481                self.peek()
1482            )));
1483        };
1484
1485        // Supports: [r:REL_TYPE], [:REL_TYPE], [r], []
1486        self.expect_tok(&Token::LBracket)?;
1487
1488        // SPA-195: parse optional variable name and optional rel type.
1489        //
1490        // Supported forms inside `[...]`:
1491        //   [r]          → var="r", rel_type=""      (variable only, no type filter)
1492        //   [:TYPE]      → var="",  rel_type="TYPE"  (type filter, no variable)
1493        //   [r:TYPE]     → var="r", rel_type="TYPE"  (both)
1494        //   [:TYPE*M..N] → variable-length with type
1495        //   [*]          → error (variable-length requires a rel type)
1496        let var = match self.peek().clone() {
1497            Token::Ident(s) if matches!(self.peek2(), Token::Colon) => {
1498                // `r:TYPE` — consume the variable identifier; colon handled below.
1499                self.advance();
1500                s
1501            }
1502            Token::Ident(s) if matches!(self.peek2(), Token::RBracket) => {
1503                // `[r]` — variable only, no rel-type constraint (SPA-198).
1504                self.advance();
1505                s
1506            }
1507            Token::Ident(s) if matches!(self.peek2(), Token::RBracket | Token::Star) => {
1508                // `r]` or `r*` — variable only, no rel type.
1509                // Variable-length without type (`[r*]`) is not valid Cypher;
1510                // emit a clear error rather than a confusing type-not-found one.
1511                self.advance();
1512                if matches!(self.peek(), Token::Star) {
1513                    return Err(Error::InvalidArgument(
1514                        "variable-length paths require a relationship type: \
1515                         use [r:R*] not [r*]"
1516                            .into(),
1517                    ));
1518                }
1519                self.expect_tok(&Token::RBracket)?;
1520                // Parse direction after `]`.
1521                let dir = if incoming {
1522                    if matches!(self.peek(), Token::Dash) {
1523                        self.advance();
1524                    } else {
1525                        return Err(Error::InvalidArgument(format!(
1526                            "expected '-' after ']' for incoming relationship, got {:?}",
1527                            self.peek()
1528                        )));
1529                    }
1530                    EdgeDir::Incoming
1531                } else if matches!(self.peek(), Token::Arrow) {
1532                    self.advance();
1533                    EdgeDir::Outgoing
1534                } else if matches!(self.peek(), Token::Dash) {
1535                    self.advance();
1536                    EdgeDir::Both
1537                } else {
1538                    return Err(Error::InvalidArgument(format!(
1539                        "expected '->' or '-' after ']' for outgoing/undirected \
1540                         relationship, got {:?}",
1541                        self.peek()
1542                    )));
1543                };
1544                return Ok(RelPattern {
1545                    var: s,
1546                    rel_type: String::new(),
1547                    dir,
1548                    min_hops: None,
1549                    max_hops: None,
1550                    props: vec![],
1551                });
1552            }
1553            _ => String::new(),
1554        };
1555
1556        // Parse optional colon + rel type, or detect illegal bare star.
1557        let rel_type = if matches!(self.peek(), Token::Colon) {
1558            self.advance(); // consume ':'
1559            self.expect_label_or_type()?
1560        } else if matches!(self.peek(), Token::Star) {
1561            return Err(Error::InvalidArgument(
1562                "variable-length paths require a relationship type: use [:R*] not [*]".into(),
1563            ));
1564        } else if matches!(self.peek(), Token::RBracket) {
1565            // `[r]` or `[]` — no rel-type at all; leave rel_type empty.
1566            let rel_type = String::new();
1567            // Parse direction and return early.
1568            self.expect_tok(&Token::RBracket)?;
1569            let dir = if incoming {
1570                if matches!(self.peek(), Token::Dash) {
1571                    self.advance();
1572                } else {
1573                    return Err(Error::InvalidArgument(format!(
1574                        "expected '-' after ']' for incoming relationship, got {:?}",
1575                        self.peek()
1576                    )));
1577                }
1578                EdgeDir::Incoming
1579            } else if matches!(self.peek(), Token::Arrow) {
1580                self.advance();
1581                EdgeDir::Outgoing
1582            } else if matches!(self.peek(), Token::Dash) {
1583                self.advance();
1584                EdgeDir::Both
1585            } else {
1586                return Err(Error::InvalidArgument(format!(
1587                    "expected '->' or '-' after ']' for outgoing/undirected relationship, got {:?}",
1588                    self.peek()
1589                )));
1590            };
1591            return Ok(RelPattern {
1592                var,
1593                rel_type,
1594                dir,
1595                min_hops: None,
1596                max_hops: None,
1597                props: vec![],
1598            });
1599        } else {
1600            // No colon — rel type is unspecified (matches any relationship type).
1601            String::new()
1602        };
1603
1604        // Parse optional inline property map: `[:R {key: val, ...}]`.
1605        // Props come after the rel type and before any hop spec or closing `]`.
1606        let rel_props: Vec<PropEntry> = if matches!(self.peek(), Token::LBrace) {
1607            self.parse_prop_map()?
1608        } else {
1609            vec![]
1610        };
1611
1612        // Parse optional variable-length hop spec after rel type:
1613        //   [:R*]      -> min=1, max=unbounded (capped at 10 in engine)
1614        //   [:R*N]     -> min=N, max=N
1615        //   [:R*M..N]  -> min=M, max=N
1616        //   [:R*M..]   -> min=M, max=unbounded
1617        //   [:R*..N]   -> min=1, max=N
1618        let (min_hops, max_hops) = if matches!(self.peek(), Token::Star) {
1619            self.advance(); // consume '*'
1620            if matches!(self.peek(), Token::DotDot) {
1621                // [:R*..N]
1622                self.advance(); // consume '..'
1623                let max = match self.advance().clone() {
1624                    Token::Integer(n) if n >= 0 => n as u32,
1625                    other => {
1626                        return Err(Error::InvalidArgument(format!(
1627                            "expected integer after '..', got {:?}",
1628                            other
1629                        )))
1630                    }
1631                };
1632                (Some(1u32), Some(max))
1633            } else if let Token::Integer(n) = self.peek().clone() {
1634                let first = n as u32;
1635                self.advance(); // consume first integer
1636                if matches!(self.peek(), Token::DotDot) {
1637                    self.advance(); // consume '..'
1638                    if let Token::Integer(m) = self.peek().clone() {
1639                        let second = m as u32;
1640                        self.advance(); // consume second integer
1641                                        // [:R*M..N]
1642                        (Some(first), Some(second))
1643                    } else {
1644                        // [:R*M..] -> min=M, max=unbounded
1645                        (Some(first), None)
1646                    }
1647                } else {
1648                    // [:R*N] -> min=N, max=N
1649                    (Some(first), Some(first))
1650                }
1651            } else {
1652                // [:R*] -> min=1, max=unbounded
1653                (Some(1u32), None)
1654            }
1655        } else {
1656            (None, None)
1657        };
1658
1659        self.expect_tok(&Token::RBracket)?;
1660
1661        // -> or - (outgoing/undirected) or -
1662        let dir = if incoming {
1663            // <-[:R]- means incoming; the trailing '-' is required.
1664            if matches!(self.peek(), Token::Dash) {
1665                self.advance();
1666            } else {
1667                return Err(Error::InvalidArgument(format!(
1668                    "expected '-' after ']' for incoming relationship, got {:?}",
1669                    self.peek()
1670                )));
1671            }
1672            EdgeDir::Incoming
1673        } else {
1674            // -[:R]-> or -[:R]-; an arrow or dash is required.
1675            if matches!(self.peek(), Token::Arrow) {
1676                self.advance();
1677                EdgeDir::Outgoing
1678            } else if matches!(self.peek(), Token::Dash) {
1679                self.advance();
1680                EdgeDir::Both
1681            } else {
1682                return Err(Error::InvalidArgument(format!(
1683                    "expected '->' or '-' after ']' for outgoing/undirected relationship, got {:?}",
1684                    self.peek()
1685                )));
1686            }
1687        };
1688
1689        Ok(RelPattern {
1690            var,
1691            rel_type,
1692            dir,
1693            min_hops,
1694            max_hops,
1695            props: rel_props,
1696        })
1697    }
1698
1699    // ── Property map ──────────────────────────────────────────────────────────
1700
1701    fn parse_prop_map(&mut self) -> Result<Vec<PropEntry>> {
1702        self.expect_tok(&Token::LBrace)?;
1703        let mut entries = Vec::new();
1704
1705        if matches!(self.peek(), Token::RBrace) {
1706            self.advance();
1707            return Ok(entries);
1708        }
1709
1710        loop {
1711            // SPA-265: property keys in map literals may be keyword tokens
1712            // (e.g. `{count: 42}`).
1713            let key = self.advance_as_prop_name()?;
1714            self.expect_tok(&Token::Colon)?;
1715            let value = self.parse_expr()?;
1716            entries.push(PropEntry { key, value });
1717
1718            if matches!(self.peek(), Token::Comma) {
1719                self.advance();
1720            } else {
1721                break;
1722            }
1723        }
1724
1725        self.expect_tok(&Token::RBrace)?;
1726        Ok(entries)
1727    }
1728
1729    // ── Literals ──────────────────────────────────────────────────────────────
1730
1731    fn parse_literal(&mut self) -> Result<Literal> {
1732        match self.advance().clone() {
1733            Token::Integer(n) => Ok(Literal::Int(n)),
1734            Token::Float(f) => Ok(Literal::Float(f)),
1735            Token::Str(s) => Ok(Literal::String(s)),
1736            Token::Param(p) => Ok(Literal::Param(p)),
1737            Token::Null => Ok(Literal::Null),
1738            Token::True => Ok(Literal::Bool(true)),
1739            Token::False => Ok(Literal::Bool(false)),
1740            other => Err(Error::InvalidArgument(format!(
1741                "expected literal, got {:?}",
1742                other
1743            ))),
1744        }
1745    }
1746
1747    // ── Expressions ───────────────────────────────────────────────────────────
1748
1749    fn parse_expr(&mut self) -> Result<Expr> {
1750        self.parse_or_expr()
1751    }
1752
1753    fn parse_or_expr(&mut self) -> Result<Expr> {
1754        let mut left = self.parse_and_expr()?;
1755        while matches!(self.peek(), Token::Or) {
1756            self.advance();
1757            let right = self.parse_and_expr()?;
1758            left = Expr::Or(Box::new(left), Box::new(right));
1759        }
1760        Ok(left)
1761    }
1762
1763    fn parse_and_expr(&mut self) -> Result<Expr> {
1764        let mut left = self.parse_not_expr()?;
1765        while matches!(self.peek(), Token::And) {
1766            self.advance();
1767            let right = self.parse_not_expr()?;
1768            left = Expr::And(Box::new(left), Box::new(right));
1769        }
1770        Ok(left)
1771    }
1772
1773    fn parse_not_expr(&mut self) -> Result<Expr> {
1774        if matches!(self.peek(), Token::Not) {
1775            self.advance();
1776            // NOT (a)-[:R]->(b) — existence predicate, or NOT (expr) — parenthesized expr
1777            if matches!(self.peek(), Token::LParen) {
1778                // Try existence pattern first; fall back to parenthesized expr
1779                let saved_pos = self.pos;
1780                match self.parse_path_pattern() {
1781                    Ok(path) => return Ok(Expr::NotExists(Box::new(ExistsPattern { path }))),
1782                    Err(_) => {
1783                        self.pos = saved_pos; // restore position
1784                                              // fall through to parse as normal NOT expr
1785                    }
1786                }
1787            }
1788            let inner = self.parse_comparison()?;
1789            return Ok(Expr::Not(Box::new(inner)));
1790        }
1791        self.parse_comparison()
1792    }
1793
1794    fn parse_comparison(&mut self) -> Result<Expr> {
1795        let left = self.parse_additive()?;
1796
1797        // Handle `expr IS NULL` / `expr IS NOT NULL`
1798        if matches!(self.peek(), Token::Is) {
1799            self.advance(); // consume IS
1800            if matches!(self.peek(), Token::Not) {
1801                self.advance(); // consume NOT
1802                                // Expect NULL
1803                match self.peek().clone() {
1804                    Token::Null => {
1805                        self.advance();
1806                        return Ok(Expr::IsNotNull(Box::new(left)));
1807                    }
1808                    other => {
1809                        return Err(Error::InvalidArgument(format!(
1810                            "expected NULL after IS NOT, got {:?}",
1811                            other
1812                        )));
1813                    }
1814                }
1815            } else {
1816                // Expect NULL
1817                match self.peek().clone() {
1818                    Token::Null => {
1819                        self.advance();
1820                        return Ok(Expr::IsNull(Box::new(left)));
1821                    }
1822                    other => {
1823                        return Err(Error::InvalidArgument(format!(
1824                            "expected NULL after IS, got {:?}",
1825                            other
1826                        )));
1827                    }
1828                }
1829            }
1830        }
1831
1832        // Handle `expr IN [...]`
1833        if matches!(self.peek(), Token::In) {
1834            self.advance(); // consume IN
1835            self.expect_tok(&Token::LBracket)?;
1836            let list = self.parse_in_list()?;
1837            return Ok(Expr::InList {
1838                expr: Box::new(left),
1839                list,
1840                negated: false,
1841            });
1842        }
1843
1844        let op = match self.peek().clone() {
1845            Token::Eq => BinOpKind::Eq,
1846            Token::Neq => BinOpKind::Neq,
1847            Token::Lt => BinOpKind::Lt,
1848            Token::Le => BinOpKind::Le,
1849            Token::Gt => BinOpKind::Gt,
1850            Token::Ge => BinOpKind::Ge,
1851            Token::Contains => BinOpKind::Contains,
1852            Token::StartsWith => {
1853                self.advance();
1854                // STARTS WITH — the WITH keyword is mandatory.
1855                match self.peek().clone() {
1856                    Token::With | Token::Ident(_) => {
1857                        self.advance();
1858                    }
1859                    other => {
1860                        return Err(Error::InvalidArgument(format!(
1861                            "expected WITH after STARTS, got {:?}",
1862                            other
1863                        )));
1864                    }
1865                }
1866                let right = self.parse_atom()?;
1867                return Ok(Expr::BinOp {
1868                    left: Box::new(left),
1869                    op: BinOpKind::StartsWith,
1870                    right: Box::new(right),
1871                });
1872            }
1873            Token::EndsWith => {
1874                self.advance();
1875                // ENDS WITH — the WITH keyword is mandatory.
1876                match self.peek().clone() {
1877                    Token::With | Token::Ident(_) => {
1878                        self.advance();
1879                    }
1880                    other => {
1881                        return Err(Error::InvalidArgument(format!(
1882                            "expected WITH after ENDS, got {:?}",
1883                            other
1884                        )));
1885                    }
1886                }
1887                let right = self.parse_atom()?;
1888                return Ok(Expr::BinOp {
1889                    left: Box::new(left),
1890                    op: BinOpKind::EndsWith,
1891                    right: Box::new(right),
1892                });
1893            }
1894            _ => return Ok(left),
1895        };
1896        self.advance();
1897        let right = self.parse_additive()?;
1898        Ok(Expr::BinOp {
1899            left: Box::new(left),
1900            op,
1901            right: Box::new(right),
1902        })
1903    }
1904
1905    /// Parse additive expressions: `a + b`, `a - b`.
1906    fn parse_additive(&mut self) -> Result<Expr> {
1907        let mut left = self.parse_multiplicative()?;
1908        loop {
1909            let op = match self.peek() {
1910                Token::Plus => BinOpKind::Add,
1911                Token::Dash => BinOpKind::Sub,
1912                _ => break,
1913            };
1914            self.advance();
1915            let right = self.parse_multiplicative()?;
1916            left = Expr::BinOp {
1917                left: Box::new(left),
1918                op,
1919                right: Box::new(right),
1920            };
1921        }
1922        Ok(left)
1923    }
1924
1925    /// Parse multiplicative expressions: `a * b`, `a / b`, `a % b`.
1926    fn parse_multiplicative(&mut self) -> Result<Expr> {
1927        let mut left = self.parse_atom()?;
1928        loop {
1929            let op = match self.peek() {
1930                Token::Star => BinOpKind::Mul,
1931                Token::Slash => BinOpKind::Div,
1932                Token::Percent => BinOpKind::Mod,
1933                _ => break,
1934            };
1935            self.advance();
1936            let right = self.parse_atom()?;
1937            left = Expr::BinOp {
1938                left: Box::new(left),
1939                op,
1940                right: Box::new(right),
1941            };
1942        }
1943        Ok(left)
1944    }
1945
1946    /// Parse a comma-separated list of atom expressions up to `]`.
1947    /// Assumes `[` has already been consumed.  Returns the list and consumes `]`.
1948    fn parse_in_list(&mut self) -> Result<Vec<Expr>> {
1949        let mut items = Vec::new();
1950        if matches!(self.peek(), Token::RBracket) {
1951            self.advance(); // consume `]`
1952            return Ok(items); // empty list
1953        }
1954        loop {
1955            items.push(self.parse_atom()?);
1956            match self.peek().clone() {
1957                Token::Comma => {
1958                    self.advance();
1959                }
1960                Token::RBracket => {
1961                    self.advance();
1962                    break;
1963                }
1964                other => {
1965                    return Err(Error::InvalidArgument(format!(
1966                        "expected ',' or ']' in IN list, got {:?}",
1967                        other
1968                    )));
1969                }
1970            }
1971        }
1972        Ok(items)
1973    }
1974
1975    fn parse_atom(&mut self) -> Result<Expr> {
1976        match self.peek().clone() {
1977            Token::Ident(var) => {
1978                // Could be var.prop, a function call fn(args), or just var.
1979                let next2 = self.peek2().clone();
1980                if matches!(next2, Token::Dot) {
1981                    self.advance(); // var
1982                    self.advance(); // .
1983                                    // SPA-265: property names may be keyword tokens (e.g. `n.count`).
1984                    let prop = self.advance_as_prop_name()?;
1985                    Ok(Expr::PropAccess { var, prop })
1986                } else if matches!(next2, Token::LParen) {
1987                    // Special-case shortestPath(…) and allShortestPaths(…) — SPA-136.
1988                    if var.to_lowercase() == "shortestpath"
1989                        || var.to_lowercase() == "allshortestpaths"
1990                    {
1991                        self.advance(); // consume function name
1992                        return self.parse_shortest_path_fn();
1993                    }
1994                    // Function call: name(arg, arg, ...)
1995                    self.advance(); // consume function name
1996                    self.advance(); // consume '('
1997                    let mut args = Vec::new();
1998                    if !matches!(self.peek(), Token::RParen) {
1999                        loop {
2000                            args.push(self.parse_expr()?);
2001                            if matches!(self.peek(), Token::Comma) {
2002                                self.advance();
2003                            } else {
2004                                break;
2005                            }
2006                        }
2007                    }
2008                    self.expect_tok(&Token::RParen)?;
2009                    Ok(Expr::FnCall { name: var, args })
2010                } else {
2011                    self.advance();
2012                    Ok(Expr::Var(var))
2013                }
2014            }
2015            Token::Count => {
2016                self.advance();
2017                self.expect_tok(&Token::LParen)?;
2018                if self.peek() == &Token::Star {
2019                    // COUNT(*) — the well-known star form.
2020                    self.advance();
2021                    self.expect_tok(&Token::RParen)?;
2022                    Ok(Expr::CountStar)
2023                } else {
2024                    // COUNT(expr) — treat as a named aggregate FnCall.
2025                    let arg = self.parse_expr()?;
2026                    self.expect_tok(&Token::RParen)?;
2027                    Ok(Expr::FnCall {
2028                        name: "count".to_string(),
2029                        args: vec![arg],
2030                    })
2031                }
2032            }
2033            Token::Integer(_)
2034            | Token::Float(_)
2035            | Token::Str(_)
2036            | Token::Param(_)
2037            | Token::Null
2038            | Token::True
2039            | Token::False => {
2040                let lit = self.parse_literal()?;
2041                Ok(Expr::Literal(lit))
2042            }
2043            Token::LParen => {
2044                self.advance();
2045                let e = self.parse_expr()?;
2046                self.expect_tok(&Token::RParen)?;
2047                Ok(e)
2048            }
2049            // Inline list literal: [expr, expr, ...]
2050            Token::LBracket => self.parse_list_literal(),
2051            // List predicate: ANY(x IN list_expr WHERE predicate)
2052            Token::Any | Token::All | Token::NoneKw | Token::Single => {
2053                let kind = match self.advance().clone() {
2054                    Token::Any => ListPredicateKind::Any,
2055                    Token::All => ListPredicateKind::All,
2056                    Token::NoneKw => ListPredicateKind::None,
2057                    Token::Single => ListPredicateKind::Single,
2058                    _ => unreachable!(),
2059                };
2060                self.expect_tok(&Token::LParen)?;
2061                let variable = self.expect_ident()?;
2062                self.expect_tok(&Token::In)?;
2063                let list_expr = self.parse_expr()?;
2064                self.expect_tok(&Token::Where)?;
2065                let predicate = self.parse_expr()?;
2066                self.expect_tok(&Token::RParen)?;
2067                Ok(Expr::ListPredicate {
2068                    kind,
2069                    variable,
2070                    list_expr: Box::new(list_expr),
2071                    predicate: Box::new(predicate),
2072                })
2073            }
2074            // EXISTS { pattern } — positive existence subquery (SPA-137).
2075            Token::Exists => {
2076                self.advance(); // consume EXISTS
2077                self.expect_tok(&Token::LBrace)?;
2078                let path = self.parse_path_pattern()?;
2079                self.expect_tok(&Token::RBrace)?;
2080                Ok(Expr::ExistsSubquery(Box::new(ExistsPattern { path })))
2081            }
2082            // CASE WHEN cond THEN val [WHEN cond THEN val]* [ELSE val] END (SPA-138).
2083            Token::Case => {
2084                self.advance(); // consume CASE
2085                let mut branches: Vec<(Expr, Expr)> = Vec::new();
2086                let mut else_expr: Option<Box<Expr>> = None;
2087                let mut seen_when = false;
2088                let mut seen_else = false;
2089                loop {
2090                    match self.peek().clone() {
2091                        Token::When => {
2092                            if seen_else {
2093                                return Err(Error::InvalidArgument(
2094                                    "WHEN cannot follow ELSE in CASE expression".to_string(),
2095                                ));
2096                            }
2097                            self.advance(); // consume WHEN
2098                            let cond = self.parse_expr()?;
2099                            self.expect_tok(&Token::Then)?;
2100                            let val = self.parse_expr()?;
2101                            branches.push((cond, val));
2102                            seen_when = true;
2103                        }
2104                        Token::Else => {
2105                            if !seen_when {
2106                                return Err(Error::InvalidArgument(
2107                                    "ELSE requires at least one WHEN branch in CASE expression"
2108                                        .to_string(),
2109                                ));
2110                            }
2111                            if seen_else {
2112                                return Err(Error::InvalidArgument(
2113                                    "duplicate ELSE in CASE expression".to_string(),
2114                                ));
2115                            }
2116                            self.advance(); // consume ELSE
2117                            else_expr = Some(Box::new(self.parse_expr()?));
2118                            seen_else = true;
2119                        }
2120                        Token::End => {
2121                            if !seen_when {
2122                                return Err(Error::InvalidArgument(
2123                                    "CASE expression requires at least one WHEN branch".to_string(),
2124                                ));
2125                            }
2126                            self.advance(); // consume END
2127                            break;
2128                        }
2129                        other => {
2130                            return Err(Error::InvalidArgument(format!(
2131                                "expected WHEN, ELSE, or END in CASE expression, got {:?}",
2132                                other
2133                            )));
2134                        }
2135                    }
2136                }
2137                Ok(Expr::CaseWhen {
2138                    branches,
2139                    else_expr,
2140                })
2141            }
2142            // Unary minus: -expr (negates a numeric literal or sub-expression).
2143            Token::Dash => {
2144                self.advance();
2145                let inner = self.parse_atom()?;
2146                match inner {
2147                    Expr::Literal(Literal::Int(n)) => Ok(Expr::Literal(Literal::Int(-n))),
2148                    Expr::Literal(Literal::Float(f)) => Ok(Expr::Literal(Literal::Float(-f))),
2149                    // Wrap in a FnCall to negate: abs(0 - x) is wrong, use unary-minus fn.
2150                    // Instead, emit FnCall("_neg", [inner]) — handled by dispatch as negation.
2151                    // For now call a built-in no-op negation: use the integer math path.
2152                    other => Ok(Expr::FnCall {
2153                        name: "_neg".into(),
2154                        args: vec![other],
2155                    }),
2156                }
2157            }
2158            other => Err(Error::InvalidArgument(format!(
2159                "unexpected token in expression: {:?}",
2160                other
2161            ))),
2162        }
2163    }
2164
2165    /// Parse `shortestPath((src)-[:REL*]->(dst))` — invoked from the Ident branch (SPA-136).
2166    fn parse_shortest_path_fn(&mut self) -> Result<Expr> {
2167        self.expect_tok(&Token::LParen)?;
2168        let path = self.parse_path_pattern()?;
2169        self.expect_tok(&Token::RParen)?;
2170
2171        if path.nodes.len() != 2 || path.rels.len() != 1 {
2172            return Err(Error::InvalidArgument(
2173                "shortestPath() requires exactly one relationship pattern".into(),
2174            ));
2175        }
2176        let src_node = &path.nodes[0];
2177        let dst_node = &path.nodes[1];
2178        let rel = &path.rels[0];
2179
2180        Ok(Expr::ShortestPath(Box::new(ShortestPathExpr {
2181            src_var: src_node.var.clone(),
2182            src_label: src_node.labels.first().cloned().unwrap_or_default(),
2183            src_props: src_node.props.clone(),
2184            dst_var: dst_node.var.clone(),
2185            dst_label: dst_node.labels.first().cloned().unwrap_or_default(),
2186            dst_props: dst_node.props.clone(),
2187            rel_type: rel.rel_type.clone(),
2188        })))
2189    }
2190
2191    // ── RETURN items ──────────────────────────────────────────────────────────
2192
2193    fn parse_return_items(&mut self) -> Result<Vec<ReturnItem>> {
2194        if matches!(self.peek(), Token::Star) {
2195            self.advance();
2196            return Ok(vec![ReturnItem {
2197                expr: Expr::Var("*".into()),
2198                alias: None,
2199            }]);
2200        }
2201
2202        let mut items = Vec::new();
2203        loop {
2204            let expr = self.parse_expr()?;
2205            let alias = if matches!(self.peek(), Token::As) {
2206                self.advance();
2207                Some(self.expect_ident()?)
2208            } else {
2209                None
2210            };
2211            items.push(ReturnItem { expr, alias });
2212            if matches!(self.peek(), Token::Comma) {
2213                self.advance();
2214            } else {
2215                break;
2216            }
2217        }
2218        Ok(items)
2219    }
2220
2221    // ── ORDER BY items ────────────────────────────────────────────────────────
2222
2223    fn parse_order_by_items(&mut self) -> Result<Vec<(Expr, SortDir)>> {
2224        let mut items = Vec::new();
2225        loop {
2226            let expr = self.parse_expr()?;
2227            let dir = match self.peek().clone() {
2228                Token::Desc => {
2229                    self.advance();
2230                    SortDir::Desc
2231                }
2232                Token::Asc => {
2233                    self.advance();
2234                    SortDir::Asc
2235                }
2236                _ => SortDir::Asc,
2237            };
2238            items.push((expr, dir));
2239            if matches!(self.peek(), Token::Comma) {
2240                self.advance();
2241            } else {
2242                break;
2243            }
2244        }
2245        Ok(items)
2246    }
2247
2248    // ── CALL procedure(args) YIELD col [RETURN ...] ───────────────────────────
2249
2250    /// Parse `CALL proc.name(args) YIELD col1, col2 [RETURN ...]`.
2251    ///
2252    /// The procedure name is a dotted identifier sequence, e.g.
2253    /// `db.index.fulltext.queryNodes`.  Arguments are a comma-separated list
2254    /// of expressions (string literals, parameters, etc.).  The `YIELD` clause
2255    /// names the columns the procedure produces; an optional `RETURN` clause
2256    /// projects them further.
2257    fn parse_call(&mut self) -> Result<Statement> {
2258        self.expect_tok(&Token::Call)?;
2259
2260        // Parse dotted procedure name: ident (. ident)*
2261        // Use advance_as_prop_name for all segments so that keyword tokens like
2262        // `index` (Token::Index) are accepted within the dotted path
2263        // (e.g. `db.index.fulltext.queryNodes`).
2264        let mut proc_name = self.advance_as_prop_name()?;
2265        while matches!(self.peek(), Token::Dot) {
2266            self.advance(); // consume '.'
2267            let part = self.advance_as_prop_name()?;
2268            proc_name.push('.');
2269            proc_name.push_str(&part);
2270        }
2271
2272        // Parse argument list: ( expr, expr, ... )
2273        self.expect_tok(&Token::LParen)?;
2274        let mut args = Vec::new();
2275        if !matches!(self.peek(), Token::RParen) {
2276            loop {
2277                args.push(self.parse_atom()?);
2278                if matches!(self.peek(), Token::Comma) {
2279                    self.advance();
2280                } else {
2281                    break;
2282                }
2283            }
2284        }
2285        self.expect_tok(&Token::RParen)?;
2286
2287        // Parse YIELD col1, col2, ...
2288        let yield_columns = if matches!(self.peek(), Token::Yield) {
2289            self.advance(); // consume YIELD
2290            let mut cols = Vec::new();
2291            loop {
2292                cols.push(self.expect_ident()?);
2293                if matches!(self.peek(), Token::Comma) {
2294                    self.advance();
2295                } else {
2296                    break;
2297                }
2298            }
2299            cols
2300        } else {
2301            vec![]
2302        };
2303
2304        // Optional trailing RETURN clause.
2305        let return_clause = if matches!(self.peek(), Token::Return) {
2306            self.advance(); // consume RETURN
2307            let distinct = if matches!(self.peek(), Token::Distinct) {
2308                self.advance();
2309                true
2310            } else {
2311                false
2312            };
2313            let _ = distinct; // not threaded through CallStatement yet — ignored
2314            let items = self.parse_return_items()?;
2315            Some(ReturnClause { items })
2316        } else {
2317            None
2318        };
2319
2320        Ok(Statement::Call(CallStatement {
2321            procedure: proc_name,
2322            args,
2323            yield_columns,
2324            return_clause,
2325        }))
2326    }
2327}
2328
2329#[cfg(test)]
2330mod tests {
2331    use super::*;
2332    use crate::ast::Statement;
2333
2334    #[test]
2335    fn parse_checkpoint_smoke() {
2336        assert!(matches!(parse("CHECKPOINT"), Ok(Statement::Checkpoint)));
2337    }
2338
2339    #[test]
2340    fn parse_optimize_smoke() {
2341        assert!(matches!(parse("OPTIMIZE"), Ok(Statement::Optimize)));
2342    }
2343
2344    #[test]
2345    fn parse_empty_fails() {
2346        assert!(parse("").is_err());
2347    }
2348
2349    #[test]
2350    fn parse_optional_match_ok() {
2351        // OPTIONAL MATCH standalone is supported (SPA-131).
2352        let stmt = parse("OPTIONAL MATCH (n:Person) RETURN n.name").unwrap();
2353        assert!(matches!(stmt, Statement::OptionalMatch(_)));
2354    }
2355
2356    #[test]
2357    fn parse_optional_match_missing_return_fails() {
2358        assert!(parse("OPTIONAL MATCH (n:Person)").is_err());
2359    }
2360
2361    #[test]
2362    fn parse_create_node() {
2363        let stmt = parse("CREATE (n:Person {name: \"Alice\"})").unwrap();
2364        assert!(matches!(stmt, Statement::Create(_)));
2365    }
2366
2367    #[test]
2368    fn parse_match_return() {
2369        let stmt = parse("MATCH (n:Person) RETURN n.name").unwrap();
2370        assert!(matches!(stmt, Statement::Match(_)));
2371    }
2372}