Skip to main content

core_query/cypher/
parser.rs

1//! Recursive-descent parser for the Cypher subset. Never panics on any token sequence.
2
3use super::ast::{
4    AggArg, AggFunc, ArithOp, CreateEdge, CreateNode, CreateStmt, EdgeDelete, Expr, HopRange,
5    LimitSkip, MatchDeleteNodeStmt, MatchDeleteStmt, MatchSetStmt, MergeStmt, NodePat, Operand,
6    OptionalClause, OrderItem, OrderTarget, Pattern, Query, RelDir, RelPat, RetItem, RetVal,
7    SetClause, UnwindClause, UnwindExpr, WithStage, WriteStatement,
8};
9use super::Tok;
10use crate::filter::CmpOp;
11use core_storage::{Value, NS_PROP};
12
13/// Max parenthesized-expression nesting. Deeper input is `Err`, not a stack overflow.
14const MAX_PAREN_DEPTH: usize = 64;
15
16/// Parse a tokenized Cypher subset query. Every failure is `Err(String)`; this
17/// function never panics on a well-formed `&[Tok]` (including empty / garbage).
18pub fn parse(tokens: &[Tok]) -> Result<Query, String> {
19    let mut p = Parser {
20        toks: tokens,
21        pos: 0,
22    };
23    let q = p.query()?;
24    if p.pos < p.toks.len() {
25        // A single-query parse: a trailing `UNION` (or anything else) is an
26        // error here. Use `parse_read` to accept a `UNION` chain.
27        return Err(p.unsupported_or_unexpected("unexpected tokens after query"));
28    }
29    Ok(q)
30}
31
32/// A read query that may be a single query or a `UNION` / `UNION ALL` chain.
33/// `all_flags[i]` is `true` when the boundary before `parts[i + 1]` was
34/// `UNION ALL` (bag union); `false` for `UNION` (distinct). Length is
35/// `parts.len() - 1`.
36#[derive(Debug, Clone, PartialEq)]
37pub struct UnionQuery {
38    pub parts: Vec<Query>,
39    pub all_flags: Vec<bool>,
40}
41
42/// Parse a read query, accepting a `UNION` / `UNION ALL` chain.
43pub fn parse_read(tokens: &[Tok]) -> Result<UnionQuery, String> {
44    let mut p = Parser {
45        toks: tokens,
46        pos: 0,
47    };
48    let mut parts = vec![p.query()?];
49    let mut all_flags = Vec::new();
50    while p.peek_is_union() {
51        p.pos += 1; // consume UNION
52        let all = matches!(p.peek(), Some(Tok::Ident(s)) if s.eq_ignore_ascii_case("all"));
53        if all {
54            p.pos += 1; // consume ALL
55        }
56        all_flags.push(all);
57        parts.push(p.query()?);
58    }
59    if p.pos < p.toks.len() {
60        return Err(p.err("unexpected tokens after UNION query"));
61    }
62    Ok(UnionQuery { parts, all_flags })
63}
64
65/// Parse a tokenized write statement (CREATE / MATCH…SET / MATCH…DELETE / MERGE).
66/// Returns `Err` for read queries or malformed write statements.
67pub fn parse_write(tokens: &[Tok]) -> Result<WriteStatement, String> {
68    let mut p = Parser {
69        toks: tokens,
70        pos: 0,
71    };
72    p.write_statement()
73}
74
75/// Return true if the token stream starts with a write keyword, or is a MATCH
76/// statement followed by SET or DELETE.  Used for fast server-side dispatch
77/// without a full parse.
78pub fn is_write_tokens(tokens: &[Tok]) -> bool {
79    match tokens.first() {
80        Some(Tok::Create) | Some(Tok::Merge) => true,
81        Some(Tok::Match) => tokens.iter().any(|t| matches!(t, Tok::Set | Tok::Delete)),
82        _ => false,
83    }
84}
85
86/// `true` or `false` in any case, as the literal it spells; `None` for any
87/// other identifier. A name followed by `.` or `(` is a property reference or
88/// a function call and never reaches this.
89fn bool_literal(name: &str) -> Option<bool> {
90    if name.eq_ignore_ascii_case("true") {
91        Some(true)
92    } else if name.eq_ignore_ascii_case("false") {
93        Some(false)
94    } else {
95        None
96    }
97}
98
99struct Parser<'a> {
100    toks: &'a [Tok],
101    pos: usize,
102}
103
104impl<'a> Parser<'a> {
105    fn peek(&self) -> Option<&'a Tok> {
106        self.toks.get(self.pos)
107    }
108
109    fn eat(&mut self, want: &Tok) -> bool {
110        if self.peek() == Some(want) {
111            self.pos += 1;
112            true
113        } else {
114            false
115        }
116    }
117
118    fn err(&self, msg: &str) -> String {
119        match self.peek() {
120            Some(tok) => format!("parse error at token {}: {msg} (found {tok:?})", self.pos),
121            None => format!(
122                "parse error at token {}: {msg} (found end of input)",
123                self.pos
124            ),
125        }
126    }
127
128    fn expect(&mut self, want: &Tok, what: &str) -> Result<(), String> {
129        if self.eat(want) {
130            Ok(())
131        } else {
132            Err(self.err(what))
133        }
134    }
135
136    fn ident(&mut self, what: &str) -> Result<String, String> {
137        match self.peek() {
138            Some(Tok::Ident(s)) => {
139                let s = s.clone();
140                self.pos += 1;
141                Ok(s)
142            }
143            _ => Err(self.err(what)),
144        }
145    }
146
147    /// `true` and `false` are literals, so neither can name a variable or an
148    /// alias: an expression that mentions the name would read the literal.
149    fn not_reserved(&self, name: &str) -> Result<(), String> {
150        if bool_literal(name).is_some() {
151            return Err(self.err(&format!(
152                "`{name}` is a boolean literal and cannot name a variable or an alias"
153            )));
154        }
155        Ok(())
156    }
157
158    /// An identifier that binds a name: an alias after `AS`.
159    fn binding(&mut self, what: &str) -> Result<String, String> {
160        if let Some(Tok::Ident(s)) = self.peek() {
161            self.not_reserved(s)?;
162        }
163        self.ident(what)
164    }
165
166    fn query(&mut self) -> Result<Query, String> {
167        let mut matches = Vec::new();
168        while self.peek() == Some(&Tok::Match) {
169            matches.extend(self.match_clause()?);
170        }
171        if matches.is_empty() {
172            return Err(self.err("expected MATCH"));
173        }
174        // Optional WHERE clause that follows the required MATCH(es) — may come
175        // before or after OPTIONAL MATCH.  In standard Cypher, WHERE applies to
176        // the preceding MATCH, so `MATCH (a) WHERE … OPTIONAL MATCH … RETURN`
177        // is legal.  Parse it here, then parse OPTIONAL MATCHes, then check
178        // again in case WHERE follows the OPTIONAL MATCHes instead.
179        let where_expr_pre = if self.eat(&Tok::Where) {
180            Some(self.expr(0)?)
181        } else {
182            None
183        };
184        // Optional MATCH clauses (zero or more, after required MATCHes + WHERE).
185        let mut optional_clauses = Vec::new();
186        while self.peek() == Some(&Tok::Optional) {
187            optional_clauses.push(self.optional_match_clause()?);
188        }
189        // WHERE may also appear *after* OPTIONAL MATCHes (if not already seen).
190        let where_expr = if where_expr_pre.is_some() {
191            where_expr_pre
192        } else if self.eat(&Tok::Where) {
193            Some(self.expr(0)?)
194        } else {
195            None
196        };
197        // Optional top-level UNWIND clauses (after WHERE, before WITH).
198        let mut unwinds = Vec::new();
199        while self.peek() == Some(&Tok::Unwind) {
200            unwinds.push(self.unwind_clause()?);
201        }
202        // Optional WHERE that follows UNWIND (references UNWIND aliases).
203        let post_unwind_where = if !unwinds.is_empty() && self.eat(&Tok::Where) {
204            Some(self.expr(0)?)
205        } else {
206            None
207        };
208        // WITH pipeline stages (zero or more).
209        let mut stages = Vec::new();
210        while self.peek() == Some(&Tok::With) {
211            stages.push(self.with_stage()?);
212        }
213        let (distinct, returns) = self.return_clause()?;
214        let aliases: Vec<&str> = returns.iter().filter_map(|r| r.alias.as_deref()).collect();
215        let order_by = if self.peek() == Some(&Tok::Order) {
216            self.order_clause(&aliases)?
217        } else {
218            Vec::new()
219        };
220        let skip = if self.eat(&Tok::Skip) {
221            Some(self.uint("SKIP")?)
222        } else {
223            None
224        };
225        let limit = if self.eat(&Tok::Limit) {
226            Some(self.uint("LIMIT")?)
227        } else {
228            None
229        };
230        // NOTE: the leftover-token check lives in `parse` / `parse_read`, not
231        // here, so `query()` can stop cleanly at a `UNION` boundary.
232        Ok(Query {
233            matches,
234            optional_clauses,
235            where_expr,
236            unwinds,
237            post_unwind_where,
238            stages,
239            returns,
240            distinct,
241            order_by,
242            skip,
243            limit,
244        })
245    }
246
247    /// True if the next token is the `UNION` keyword (an ordinary identifier).
248    fn peek_is_union(&self) -> bool {
249        matches!(self.peek(), Some(Tok::Ident(s)) if s.eq_ignore_ascii_case("union"))
250    }
251
252    /// Named errors for still-unsupported Cypher forms (`UNION`, `CASE`).
253    fn unsupported_or_unexpected(&self, msg: &str) -> String {
254        match self.peek() {
255            Some(Tok::Ident(s)) if s.eq_ignore_ascii_case("union") => {
256                "UNION is not supported".to_string()
257            }
258            Some(Tok::Ident(s)) if s.eq_ignore_ascii_case("case") => {
259                "CASE is not supported".to_string()
260            }
261            _ => self.err(msg),
262        }
263    }
264
265    /// Consume an identifier keyword (case-insensitive). Keywords that are
266    /// not lexer tokens (`IN`, `DISTINCT`, `ON`) stay `Ident` so they can
267    /// still be used as variable names in other positions.
268    fn eat_ident_kw(&mut self, kw: &str) -> bool {
269        if let Some(Tok::Ident(s)) = self.peek() {
270            if s.eq_ignore_ascii_case(kw) {
271                self.pos += 1;
272                return true;
273            }
274        }
275        false
276    }
277
278    /// Parse one `OPTIONAL MATCH pattern [WHERE expr]` clause.
279    ///
280    /// Standard openCypher allows exactly one MATCH per OPTIONAL MATCH.
281    fn optional_match_clause(&mut self) -> Result<OptionalClause, String> {
282        self.expect(&Tok::Optional, "expected OPTIONAL")?;
283        self.expect(&Tok::Match, "expected MATCH after OPTIONAL")?;
284        let patterns = vec![self.pattern()?];
285        // Optional WHERE inside the optional scope.
286        let where_expr = if self.eat(&Tok::Where) {
287            Some(self.expr(0)?)
288        } else {
289            None
290        };
291        Ok(OptionalClause {
292            patterns,
293            where_expr,
294        })
295    }
296
297    /// Parse one `WITH <items> [WHERE] [ORDER BY] [SKIP] [LIMIT] [MATCH]* [UNWIND]* [WHERE]`
298    /// stage and return a `WithStage`.
299    fn with_stage(&mut self) -> Result<WithStage, String> {
300        self.expect(&Tok::With, "expected WITH")?;
301        let mut items = vec![self.ret_item()?];
302        while self.eat(&Tok::Comma) {
303            items.push(self.ret_item()?);
304        }
305        // Optional WHERE / HAVING immediately after WITH items.
306        let where_expr = if self.eat(&Tok::Where) {
307            Some(self.expr(0)?)
308        } else {
309            None
310        };
311        // Optional ORDER BY inside WITH.
312        let aliases: Vec<&str> = items.iter().filter_map(|r| r.alias.as_deref()).collect();
313        let order_by = if self.peek() == Some(&Tok::Order) {
314            self.order_clause(&aliases)?
315        } else {
316            Vec::new()
317        };
318        let skip = if self.eat(&Tok::Skip) {
319            Some(self.uint("SKIP")?)
320        } else {
321            None
322        };
323        let limit = if self.eat(&Tok::Limit) {
324            Some(self.uint("LIMIT")?)
325        } else {
326            None
327        };
328        // Optional MATCH clauses that follow this WITH.
329        let mut matches = Vec::new();
330        while self.peek() == Some(&Tok::Match) {
331            matches.extend(self.match_clause()?);
332        }
333        // Optional OPTIONAL MATCH clauses that follow those MATCHes.
334        let mut optional_clauses = Vec::new();
335        while self.peek() == Some(&Tok::Optional) {
336            optional_clauses.push(self.optional_match_clause()?);
337        }
338        // Optional UNWIND clauses that follow those MATCHes.
339        let mut stage_unwinds = Vec::new();
340        while self.peek() == Some(&Tok::Unwind) {
341            stage_unwinds.push(self.unwind_clause()?);
342        }
343        // Optional WHERE that follows those MATCHes / UNWINDs.
344        let post_where = if self.peek() == Some(&Tok::Where)
345            && (!matches.is_empty() || !optional_clauses.is_empty() || !stage_unwinds.is_empty())
346        {
347            self.pos += 1; // consume WHERE
348            Some(self.expr(0)?)
349        } else {
350            None
351        };
352        Ok(WithStage {
353            items,
354            where_expr,
355            order_by,
356            skip,
357            limit,
358            matches,
359            optional_clauses,
360            unwinds: stage_unwinds,
361            post_where,
362        })
363    }
364
365    /// Parse `UNWIND <expr> AS <alias>`.
366    fn unwind_clause(&mut self) -> Result<UnwindClause, String> {
367        self.expect(&Tok::Unwind, "expected UNWIND")?;
368        let list = self.unwind_expr()?;
369        self.expect(&Tok::As, "expected AS after UNWIND expression")?;
370        let alias = self.binding("expected alias identifier after AS")?;
371        Ok(UnwindClause { list, alias })
372    }
373
374    /// Parse the list expression in an UNWIND clause:
375    /// - `[v1, v2, …]` — literal list.
376    /// - `var.field`   — property reference.
377    /// - `var`         — bare variable (alias from a prior WITH).
378    fn unwind_expr(&mut self) -> Result<UnwindExpr, String> {
379        match self.peek() {
380            Some(Tok::LBracket) => {
381                self.pos += 1; // consume '['
382                let mut vals = Vec::new();
383                if !self.eat(&Tok::RBracket) {
384                    loop {
385                        vals.push(self.literal_value("UNWIND list element")?);
386                        if self.eat(&Tok::Comma) {
387                            continue;
388                        }
389                        self.expect(&Tok::RBracket, "expected ']' to close UNWIND list")?;
390                        break;
391                    }
392                }
393                Ok(UnwindExpr::Lit(vals))
394            }
395            Some(Tok::Ident(_)) => {
396                let name = self.ident("expected variable or property in UNWIND")?;
397                if self.eat(&Tok::Dot) {
398                    let field = self.ident("expected field name after '.' in UNWIND")?;
399                    Ok(UnwindExpr::Prop { var: name, field })
400                } else {
401                    Ok(UnwindExpr::Var(name))
402                }
403            }
404            _ => Err(self.err("expected list literal, property, or variable in UNWIND")),
405        }
406    }
407
408    /// Parse one `MATCH p1 [, p2, …]` clause.
409    ///
410    /// Comma-separated patterns in a single MATCH are the openCypher way to
411    /// write an intersection — `MATCH (t)-[:A]->(c), (t)-[:B]->(c)` binds the
412    /// *same* `t` and `c` across both patterns — and every multihop question
413    /// an assistant asks is that shape. The returned patterns are appended to
414    /// the same `Vec<Pattern>` a run of separate MATCH clauses produces, so
415    /// the two spellings plan and execute identically.
416    fn match_clause(&mut self) -> Result<Vec<Pattern>, String> {
417        self.expect(&Tok::Match, "expected MATCH")?;
418        let mut out = vec![self.match_pattern()?];
419        while self.eat(&Tok::Comma) {
420            out.push(self.match_pattern()?);
421        }
422        Ok(out)
423    }
424
425    /// One pattern inside a MATCH clause: `shortestPath(…)` or a plain chain.
426    fn match_pattern(&mut self) -> Result<Pattern, String> {
427        // Detect `shortestPath(...)`.
428        if let Some(Tok::Ident(s)) = self.peek() {
429            if s.eq_ignore_ascii_case("shortestpath") {
430                return self.shortest_path_clause();
431            }
432        }
433        self.pattern()
434    }
435
436    fn shortest_path_clause(&mut self) -> Result<Pattern, String> {
437        self.pos += 1; // consume "shortestPath" identifier
438        self.expect(&Tok::LParen, "expected '(' after shortestPath")?;
439        let start = self.node()?;
440        let rel = self.rel()?;
441        if rel.hops.is_none() {
442            return Err(
443                self.err("shortestPath requires a variable-length relationship (e.g. [*..5])")
444            );
445        }
446        let dest = self.node()?;
447        self.expect(&Tok::RParen, "expected ')' to close shortestPath")?;
448        Ok(Pattern {
449            start,
450            chain: vec![(rel, dest)],
451            shortest: true,
452        })
453    }
454
455    fn pattern(&mut self) -> Result<Pattern, String> {
456        let start = self.node()?;
457        let mut chain = Vec::new();
458        while matches!(self.peek(), Some(Tok::Dash) | Some(Tok::Lt)) {
459            let rel = self.rel()?;
460            let dest = self.node()?;
461            chain.push((rel, dest));
462        }
463        Ok(Pattern {
464            start,
465            chain,
466            shortest: false,
467        })
468    }
469
470    fn node(&mut self) -> Result<NodePat, String> {
471        self.expect(&Tok::LParen, "expected '(' to start a node pattern")?;
472        let var = match self.peek() {
473            Some(Tok::Ident(s)) => {
474                let s = s.clone();
475                self.not_reserved(&s)?;
476                self.pos += 1;
477                Some(s)
478            }
479            _ => None,
480        };
481        let label = if self.eat(&Tok::Colon) {
482            Some(self.ident("expected label identifier after ':'")?)
483        } else {
484            None
485        };
486        let props = if self.peek() == Some(&Tok::LBrace) {
487            self.props()?
488        } else {
489            Vec::new()
490        };
491        self.expect(&Tok::RParen, "expected ')' to close a node pattern")?;
492        Ok(NodePat { var, label, props })
493    }
494
495    fn props(&mut self) -> Result<Vec<(String, Operand)>, String> {
496        self.expect(&Tok::LBrace, "expected '{'")?;
497        let mut out = Vec::new();
498        loop {
499            let key = self.ident("expected property key")?;
500            self.expect(&Tok::Colon, "expected ':' after property key")?;
501            let val = self.operand()?;
502            out.push((key, val));
503            if self.eat(&Tok::Comma) {
504                continue;
505            }
506            break;
507        }
508        self.expect(&Tok::RBrace, "expected '}' to close property map")?;
509        Ok(out)
510    }
511
512    fn rel(&mut self) -> Result<RelPat, String> {
513        if self.eat(&Tok::Lt) {
514            self.expect(
515                &Tok::Dash,
516                "expected '-' after '<' in a left-directed relationship",
517            )?;
518            let (var, etypes, hops) = self.rel_body()?;
519            self.expect(
520                &Tok::Dash,
521                "expected '-' to close a left-directed relationship",
522            )?;
523            return Ok(RelPat {
524                var,
525                etypes,
526                dir: RelDir::Left,
527                hops,
528            });
529        }
530        self.expect(&Tok::Dash, "expected '-' to start a relationship")?;
531        let (var, etypes, hops) = self.rel_body()?;
532        self.expect(&Tok::Dash, "expected '-' after ']'")?;
533        let dir = if self.eat(&Tok::Gt) {
534            RelDir::Right
535        } else {
536            RelDir::Undirected
537        };
538        Ok(RelPat {
539            var,
540            etypes,
541            dir,
542            hops,
543        })
544    }
545
546    #[allow(clippy::type_complexity)]
547    fn rel_body(&mut self) -> Result<(Option<String>, Vec<String>, Option<HopRange>), String> {
548        self.expect(&Tok::LBracket, "expected '[' in a relationship pattern")?;
549        let var = match self.peek() {
550            Some(Tok::Ident(s)) => {
551                let s = s.clone();
552                self.not_reserved(&s)?;
553                self.pos += 1;
554                Some(s)
555            }
556            _ => None,
557        };
558        // `:A`, or `:A|:B|:C` alternation. Empty = any type.
559        let mut etypes = Vec::new();
560        if self.eat(&Tok::Colon) {
561            etypes.push(self.ident("expected relationship type identifier after ':'")?);
562            while self.eat(&Tok::Pipe) {
563                // Both `|:B` (openCypher) and the lenient `|B` are accepted.
564                self.eat(&Tok::Colon);
565                etypes.push(self.ident("expected relationship type identifier after '|'")?);
566            }
567        }
568        let hops = if self.eat(&Tok::Star) {
569            Some(self.parse_hop_range()?)
570        } else {
571            None
572        };
573        self.expect(
574            &Tok::RBracket,
575            "expected ']' to close a relationship pattern",
576        )?;
577        Ok((var, etypes, hops))
578    }
579
580    /// Parse the hop-count range that follows `*` inside a relationship bracket.
581    ///
582    /// Recognised forms (after `*` is already consumed):
583    /// - `]`        → bare `*`, treated as `1..10`
584    /// - `n`        → exactly n hops (`n..n`)
585    /// - `n..m`     → n..m hops
586    /// - `..m`      → 1..m hops
587    /// - `n..`      → unbounded → hard-cap error
588    ///
589    /// Hard cap: max > 10 or unbounded → Err("variable-length paths are capped at 10 hops").
590    fn parse_hop_range(&mut self) -> Result<HopRange, String> {
591        const CAP_ERR: &str = "variable-length paths are capped at 10 hops";
592
593        match self.peek() {
594            // bare `*`  →  min=1, max=10
595            Some(Tok::RBracket) => Ok(HopRange { min: 1, max: 10 }),
596
597            // `*n`  or  `*n..`  or  `*n..m`
598            Some(Tok::Int(n)) => {
599                let n = *n;
600                self.pos += 1;
601                if self.eat(&Tok::Dot) {
602                    self.expect(&Tok::Dot, "expected '..' separator in hop range")?;
603                    match self.peek() {
604                        Some(Tok::Int(m)) => {
605                            let m = *m;
606                            self.pos += 1;
607                            // `*n..m`
608                            self.validate_hop_range(n, m, CAP_ERR)
609                        }
610                        _ => {
611                            // `*n..` — unbounded
612                            Err(CAP_ERR.to_string())
613                        }
614                    }
615                } else {
616                    // `*n` — exact hops
617                    self.validate_hop_range(n, n, CAP_ERR)
618                }
619            }
620
621            // `*..m`
622            Some(Tok::Dot) => {
623                self.pos += 1; // consume first '.'
624                self.expect(&Tok::Dot, "expected '..' range separator after '*'")?;
625                match self.peek() {
626                    Some(Tok::Int(m)) => {
627                        let m = *m;
628                        self.pos += 1;
629                        self.validate_hop_range(1, m, CAP_ERR)
630                    }
631                    _ => Err(self.err("expected max-hop integer after '*..'")),
632                }
633            }
634
635            // Anything else after `*` — treat as bare `*`
636            _ => Ok(HopRange { min: 1, max: 10 }),
637        }
638    }
639
640    fn validate_hop_range(
641        &self,
642        min_n: i64,
643        max_n: i64,
644        cap_err: &str,
645    ) -> Result<HopRange, String> {
646        if min_n < 0 || max_n < 0 {
647            return Err(self.err("hop counts must be non-negative"));
648        }
649        if min_n == 0 {
650            return Err(self.err(
651                "zero-length variable-length paths are not supported; minimum hop count is 1",
652            ));
653        }
654        if max_n > 10 {
655            return Err(cap_err.to_string());
656        }
657        let min = min_n as u8;
658        let max = max_n as u8;
659        if min > max {
660            return Err(self.err(&format!(
661                "variable-length path min ({min}) must not exceed max ({max})"
662            )));
663        }
664        Ok(HopRange { min, max })
665    }
666
667    fn expr(&mut self, paren_depth: usize) -> Result<Expr, String> {
668        let mut left = self.term(paren_depth)?;
669        while self.eat(&Tok::Or) {
670            let right = self.term(paren_depth)?;
671            left = Expr::Or(Box::new(left), Box::new(right));
672        }
673        Ok(left)
674    }
675
676    fn term(&mut self, paren_depth: usize) -> Result<Expr, String> {
677        let mut left = self.factor(paren_depth)?;
678        while self.eat(&Tok::And) {
679            let right = self.factor(paren_depth)?;
680            left = Expr::And(Box::new(left), Box::new(right));
681        }
682        Ok(left)
683    }
684
685    fn factor(&mut self, paren_depth: usize) -> Result<Expr, String> {
686        let negated = self.eat(&Tok::Not);
687        let inner = if self.eat(&Tok::LParen) {
688            if paren_depth >= MAX_PAREN_DEPTH {
689                return Err(self.err("expression nesting too deep"));
690            }
691            let e = self.expr(paren_depth + 1)?;
692            self.expect(
693                &Tok::RParen,
694                "expected ')' to close parenthesized expression",
695            )?;
696            e
697        } else {
698            self.cmp()?
699        };
700        if negated {
701            Ok(Expr::Not(Box::new(inner)))
702        } else {
703            Ok(inner)
704        }
705    }
706
707    fn cmp(&mut self) -> Result<Expr, String> {
708        let lhs = self.arith_expr()?;
709        // Check for IS NULL / IS NOT NULL postfix.
710        if let Some(Tok::Ident(s)) = self.peek() {
711            if s.eq_ignore_ascii_case("is") {
712                self.pos += 1; // consume "IS"
713                               // Optional NOT.
714                let negated = self.eat(&Tok::Not);
715                // Expect NULL identifier.
716                match self.peek() {
717                    Some(Tok::Ident(n)) if n.eq_ignore_ascii_case("null") => {
718                        self.pos += 1; // consume "NULL"
719                        return Ok(if negated {
720                            Expr::IsNotNull(lhs)
721                        } else {
722                            Expr::IsNull(lhs)
723                        });
724                    }
725                    _ => {
726                        return Err(self.err(if negated {
727                            "expected NULL after IS NOT"
728                        } else {
729                            "expected NULL after IS"
730                        }));
731                    }
732                }
733            }
734        }
735        // `IN [a, b, $p]` or `IN $list`.
736        if self.eat_ident_kw("in") {
737            let list = self.in_list()?;
738            return Ok(Expr::In { expr: lhs, list });
739        }
740        // Infix string predicates: `STARTS WITH`, `ENDS WITH`, `CONTAINS`.
741        // These desugar to the scalar functions of the same name that the
742        // executor already implements, so null- and non-string handling is
743        // shared with the `startsWith(a, b)` call spelling. Only the infix
744        // form is what an assistant writes, and it used to be a parse error.
745        if let Some(name) = self.eat_infix_string_op()? {
746            let rhs = self.arith_expr()?;
747            return Ok(Expr::Truthy(Operand::FuncCall {
748                name: name.to_string(),
749                args: vec![lhs, rhs],
750            }));
751        }
752        // If no comparison operator follows, treat the operand as a standalone
753        // boolean predicate (Expr::Truthy).  This enables:
754        //   WHERE textMatches(n.bio, 'query')
755        // without requiring an explicit `= true` or similar.
756        match self.cmp_op() {
757            Ok(op) => {
758                let rhs = self.arith_expr()?;
759                Ok(Expr::Cmp { lhs, op, rhs })
760            }
761            Err(_) => Ok(Expr::Truthy(lhs)),
762        }
763    }
764
765    /// Consume `STARTS WITH` / `ENDS WITH` / `CONTAINS` if one follows, and
766    /// return the scalar function it desugars to.
767    ///
768    /// `STARTS` and `ENDS` are ordinary identifiers to the lexer, so a bare
769    /// `STARTS` not followed by `WITH` is a named error rather than a silent
770    /// reinterpretation of the token as a variable.
771    fn eat_infix_string_op(&mut self) -> Result<Option<&'static str>, String> {
772        let Some(Tok::Ident(s)) = self.peek() else {
773            return Ok(None);
774        };
775        let word = s.to_ascii_lowercase();
776        match word.as_str() {
777            "contains" => {
778                self.pos += 1;
779                Ok(Some("contains"))
780            }
781            "starts" | "ends" => {
782                self.pos += 1;
783                // `WITH` is a lexer keyword token here, not an identifier.
784                if !self.eat(&Tok::With) {
785                    let kw = if word == "starts" { "STARTS" } else { "ENDS" };
786                    return Err(self.err(&format!("expected WITH after {kw}")));
787                }
788                Ok(Some(if word == "starts" {
789                    "startsWith"
790                } else {
791                    "endsWith"
792                }))
793            }
794            _ => Ok(None),
795        }
796    }
797
798    /// Parse the list operand of `IN`: `[a, b, $p]` or a single operand (`$cities`).
799    fn in_list(&mut self) -> Result<Vec<Operand>, String> {
800        if self.eat(&Tok::LBracket) {
801            let mut items = Vec::new();
802            if !self.eat(&Tok::RBracket) {
803                loop {
804                    items.push(self.arith_expr()?);
805                    if self.eat(&Tok::Comma) {
806                        continue;
807                    }
808                    self.expect(&Tok::RBracket, "expected ']' to close IN list")?;
809                    break;
810                }
811            }
812            Ok(items)
813        } else {
814            Ok(vec![self.arith_expr()?])
815        }
816    }
817
818    // ── Arithmetic expression parsing (precedence: * / > + -) ──────────────
819    //
820    // Grammar:
821    //   arith_expr  = arith_add
822    //   arith_add   = arith_mul ((+ | -) arith_mul)*
823    //   arith_mul   = arith_unary ((* | /) arith_unary)*
824    //   arith_unary = - arith_atom | arith_atom
825    //   arith_atom  = literal | param | ident | ident.field | ident(args…) | (arith_add)
826
827    /// Parse a full arithmetic expression (additive level).
828    fn arith_expr(&mut self) -> Result<Operand, String> {
829        let mut left = self.arith_mul()?;
830        loop {
831            let op = if self.eat(&Tok::Plus) {
832                ArithOp::Add
833            } else if self.eat(&Tok::Dash) {
834                // Dash is `-`; but we must not consume a Dash that starts a
835                // relationship pattern (those appear at the top-level pattern
836                // parser, not inside an expression). Inside expressions `-`
837                // is always subtraction.
838                ArithOp::Sub
839            } else {
840                break;
841            };
842            let right = self.arith_mul()?;
843            left = Operand::BinArith {
844                op,
845                left: Box::new(left),
846                right: Box::new(right),
847            };
848        }
849        Ok(left)
850    }
851
852    /// Parse a multiplicative-level arithmetic expression.
853    fn arith_mul(&mut self) -> Result<Operand, String> {
854        let mut left = self.arith_unary()?;
855        loop {
856            let op = if self.eat(&Tok::Star) {
857                ArithOp::Mul
858            } else if self.eat(&Tok::Slash) {
859                ArithOp::Div
860            } else {
861                break;
862            };
863            let right = self.arith_unary()?;
864            left = Operand::BinArith {
865                op,
866                left: Box::new(left),
867                right: Box::new(right),
868            };
869        }
870        Ok(left)
871    }
872
873    /// Parse a unary-level arithmetic expression (handles unary `-`).
874    fn arith_unary(&mut self) -> Result<Operand, String> {
875        if self.eat(&Tok::Dash) {
876            // Unary minus: fold into the next atom.
877            return match self.peek() {
878                Some(Tok::Int(n)) => {
879                    let n = *n;
880                    self.pos += 1;
881                    let neg = n
882                        .checked_neg()
883                        .ok_or_else(|| self.err("integer negation overflow"))?;
884                    Ok(Operand::Lit(Value::Int(neg)))
885                }
886                Some(Tok::Float(x)) => {
887                    let x = *x;
888                    self.pos += 1;
889                    Ok(Operand::Lit(Value::Float(-x)))
890                }
891                _ => Err(self.err("unary minus only applies to numeric literals")),
892            };
893        }
894        self.arith_atom()
895    }
896
897    /// Parse an atomic operand (leaf of the arithmetic expression tree).
898    fn arith_atom(&mut self) -> Result<Operand, String> {
899        // Parenthesized arithmetic expression.
900        if self.eat(&Tok::LParen) {
901            let inner = self.arith_expr()?;
902            self.expect(&Tok::RParen, "expected ')' to close arithmetic expression")?;
903            return Ok(inner);
904        }
905        // Delegate to the existing atom parser (no unary minus here — handled above).
906        let atom = self.operand_atom()?;
907        self.subscripts(atom)
908    }
909
910    /// Apply any postfix `[index]` subscripts to an already-parsed operand.
911    ///
912    /// `n.location[0]` is how an assistant reads one end of a `[lat, lon]`
913    /// pair, and it used to stop the parse dead at the `[`. Chained
914    /// subscripts (`n.grid[0][1]`) parse; only the first level is ever
915    /// non-null for the list shapes the graph stores.
916    fn subscripts(&mut self, mut base: Operand) -> Result<Operand, String> {
917        while self.eat(&Tok::LBracket) {
918            let index = self.arith_expr()?;
919            self.expect(&Tok::RBracket, "expected ']' to close a list subscript")?;
920            base = Operand::Index {
921                base: Box::new(base),
922                index: Box::new(index),
923            };
924        }
925        Ok(base)
926    }
927
928    fn cmp_op(&mut self) -> Result<CmpOp, String> {
929        let op = match self.peek() {
930            Some(Tok::Eq) => CmpOp::Eq,
931            Some(Tok::Ne) => CmpOp::Ne,
932            Some(Tok::Lt) => CmpOp::Lt,
933            Some(Tok::Le) => CmpOp::Le,
934            Some(Tok::Gt) => CmpOp::Gt,
935            Some(Tok::Ge) => CmpOp::Ge,
936            _ => return Err(self.err("expected comparison operator")),
937        };
938        self.pos += 1;
939        Ok(op)
940    }
941
942    /// Parse a single atomic operand (no arithmetic wrapping — use `arith_expr`
943    /// for full expression support).  This is the leaf parser for literals,
944    /// parameters, property references, variable references, and function calls.
945    fn operand_atom(&mut self) -> Result<Operand, String> {
946        match self.peek() {
947            Some(Tok::Int(n)) => {
948                let n = *n;
949                self.pos += 1;
950                Ok(Operand::Lit(Value::Int(n)))
951            }
952            Some(Tok::Float(x)) => {
953                let x = *x;
954                self.pos += 1;
955                Ok(Operand::Lit(Value::Float(x)))
956            }
957            Some(Tok::Str(s)) => {
958                let s = s.clone();
959                self.pos += 1;
960                Ok(Operand::Lit(Value::Str(s)))
961            }
962            Some(Tok::Param(s)) => {
963                let s = s.clone();
964                self.pos += 1;
965                Ok(Operand::Param(s))
966            }
967            Some(Tok::Ident(_)) => {
968                let name = self.ident("expected identifier")?;
969                if name.eq_ignore_ascii_case("case") {
970                    return self.parse_case();
971                }
972                if name.eq_ignore_ascii_case("collect") && self.peek() == Some(&Tok::LParen) {
973                    // collect() is a top-level RETURN/WITH aggregate (handled in
974                    // ret_item); it is not valid inside a larger expression.
975                    return Err(
976                        "collect() is only supported as a top-level RETURN/WITH aggregate"
977                            .to_string(),
978                    );
979                }
980                if self.peek() == Some(&Tok::LParen) {
981                    // Scalar function call: name(arg, ...)
982                    // Function arguments may be arbitrary arithmetic expressions.
983                    self.pos += 1; // consume '('
984                    let mut args = Vec::new();
985                    if self.peek() != Some(&Tok::RParen) {
986                        args.push(self.arith_expr()?);
987                        while self.eat(&Tok::Comma) {
988                            args.push(self.arith_expr()?);
989                        }
990                    }
991                    self.expect(&Tok::RParen, "expected ')' to close function call")?;
992                    Ok(Operand::FuncCall { name, args })
993                } else if self.eat(&Tok::Dot) {
994                    let field = self.ident("expected field name after '.'")?;
995                    Ok(Operand::Prop { var: name, field })
996                } else if let Some(b) = bool_literal(&name) {
997                    Ok(Operand::Lit(Value::Bool(b)))
998                } else {
999                    // Bare variable reference (e.g. alias name in WITH … WHERE c > 2).
1000                    Ok(Operand::Var(name))
1001                }
1002            }
1003            _ => Err(self.err("expected operand (property, literal, or parameter)")),
1004        }
1005    }
1006
1007    /// Parse a generic `CASE WHEN <cond> THEN <value> … [ELSE <value>] END`.
1008    /// The `CASE` keyword has already been consumed. `WHEN`/`THEN`/`ELSE`/`END`
1009    /// are ordinary identifiers to the lexer, matched case-insensitively.
1010    fn parse_case(&mut self) -> Result<Operand, String> {
1011        let is_kw = |tok: Option<&Tok>, kw: &str| matches!(tok, Some(Tok::Ident(s)) if s.eq_ignore_ascii_case(kw));
1012        let mut branches = Vec::new();
1013        while is_kw(self.peek(), "when") {
1014            self.pos += 1; // consume WHEN
1015            let cond = self.expr(0)?;
1016            if !is_kw(self.peek(), "then") {
1017                return Err(self.err("expected THEN in a CASE branch"));
1018            }
1019            self.pos += 1; // consume THEN
1020            let value = self.arith_expr()?;
1021            branches.push((cond, value));
1022        }
1023        if branches.is_empty() {
1024            return Err(self.err("CASE requires at least one WHEN ... THEN branch"));
1025        }
1026        let default = if is_kw(self.peek(), "else") {
1027            self.pos += 1; // consume ELSE
1028            Some(Box::new(self.arith_expr()?))
1029        } else {
1030            None
1031        };
1032        if !is_kw(self.peek(), "end") {
1033            return Err(self.err("expected END to close CASE"));
1034        }
1035        self.pos += 1; // consume END
1036        Ok(Operand::Case { branches, default })
1037    }
1038
1039    /// Parse a full arithmetic expression (additive + multiplicative + unary +
1040    /// atom).  This is the primary operand entry point for WHERE, RETURN, SET,
1041    /// and function arguments.  Use `operand_atom` for contexts that truly
1042    /// require a single atom (e.g., MATCH property map values where arithmetic
1043    /// would be syntactically ambiguous with the `}` delimiter).
1044    fn operand(&mut self) -> Result<Operand, String> {
1045        // In contexts where `operand` is called for MATCH props, the parser
1046        // never sees arithmetic operators (`+`/`/`) because they can't appear
1047        // inside `{key: val}` maps.  Delegating to `arith_expr` is safe here.
1048        self.arith_expr()
1049    }
1050
1051    fn return_clause(&mut self) -> Result<(bool, Vec<RetItem>), String> {
1052        if !self.eat(&Tok::Return) {
1053            return Err(self.unsupported_or_unexpected("expected RETURN"));
1054        }
1055        let distinct = self.eat_ident_kw("distinct");
1056        let mut items = vec![self.ret_item()?];
1057        while self.eat(&Tok::Comma) {
1058            items.push(self.ret_item()?);
1059        }
1060        Ok((distinct, items))
1061    }
1062
1063    fn return_items(&mut self) -> Result<Vec<RetItem>, String> {
1064        let mut items = vec![self.ret_item()?];
1065        while self.eat(&Tok::Comma) {
1066            items.push(self.ret_item()?);
1067        }
1068        Ok(items)
1069    }
1070
1071    fn ret_item(&mut self) -> Result<RetItem, String> {
1072        // Check for an aggregate function name (COUNT/SUM/AVG/MIN/MAX).
1073        // These are ordinary identifiers in the lexer, so we peek and check
1074        // the lowercased string before deciding the parse branch.
1075        if let Some(Tok::Ident(s)) = self.peek() {
1076            let func = match s.to_ascii_lowercase().as_str() {
1077                "count" => Some(AggFunc::Count),
1078                "sum" => Some(AggFunc::Sum),
1079                "avg" => Some(AggFunc::Avg),
1080                "min" => Some(AggFunc::Min),
1081                "max" => Some(AggFunc::Max),
1082                "collect" => Some(AggFunc::Collect),
1083                _ => None,
1084            };
1085            if let Some(func) = func {
1086                self.pos += 1; // consume the function name
1087                self.expect(&Tok::LParen, "expected '(' after aggregate function name")?;
1088                // `count(DISTINCT t)` — DISTINCT is an ordinary identifier to
1089                // the lexer, so it is only a modifier here when something
1090                // follows it inside the parens.
1091                let distinct = matches!(self.peek(), Some(Tok::Ident(s)) if s.eq_ignore_ascii_case("distinct"))
1092                    && !matches!(self.toks.get(self.pos + 1), Some(Tok::RParen));
1093                if distinct {
1094                    self.pos += 1;
1095                    if self.peek() == Some(&Tok::Star) {
1096                        return Err(self.err("DISTINCT * is not a valid aggregate argument"));
1097                    }
1098                }
1099                let arg = if self.eat(&Tok::Star) {
1100                    AggArg::Star
1101                } else {
1102                    let var = self.ident("expected variable or '*' in aggregate argument")?;
1103                    if self.eat(&Tok::Dot) {
1104                        let field =
1105                            self.ident("expected field name after '.' in aggregate argument")?;
1106                        AggArg::Prop { var, field }
1107                    } else {
1108                        AggArg::Var(var)
1109                    }
1110                };
1111                let arg = if distinct {
1112                    AggArg::Distinct(Box::new(arg))
1113                } else {
1114                    arg
1115                };
1116                self.expect(&Tok::RParen, "expected ')' to close aggregate function")?;
1117                let alias = if self.eat(&Tok::As) {
1118                    Some(self.binding("expected alias identifier after AS")?)
1119                } else {
1120                    None
1121                };
1122                return Ok(RetItem {
1123                    value: RetVal::Agg { func, arg },
1124                    alias,
1125                });
1126            }
1127        }
1128
1129        // Non-aggregate RETURN item: parse as a full arithmetic expression,
1130        // then convert the resulting Operand to the appropriate RetVal variant.
1131        // This unified path handles:
1132        //   n          → RetVal::Var
1133        //   n.prop     → RetVal::Prop
1134        //   f(...)     → RetVal::FuncCall
1135        //   n.age + 1  → RetVal::ScalarExpr(BinArith)
1136        //   42         → RetVal::ScalarExpr(Lit)
1137        let op = self.arith_expr()?;
1138        let value = match op {
1139            Operand::Var(name) => RetVal::Var(name),
1140            Operand::Prop { var, field } => RetVal::Prop { var, field },
1141            Operand::FuncCall { name, args } => RetVal::FuncCall { name, args },
1142            other => RetVal::ScalarExpr(other),
1143        };
1144        let alias = if self.eat(&Tok::As) {
1145            Some(self.binding("expected alias identifier after AS")?)
1146        } else {
1147            None
1148        };
1149        Ok(RetItem { value, alias })
1150    }
1151
1152    fn order_clause(&mut self, aliases: &[&str]) -> Result<Vec<OrderItem>, String> {
1153        self.expect(&Tok::Order, "expected ORDER")?;
1154        self.expect(&Tok::By, "expected BY after ORDER")?;
1155        let mut items = vec![self.order_item(aliases)?];
1156        while self.eat(&Tok::Comma) {
1157            items.push(self.order_item(aliases)?);
1158        }
1159        Ok(items)
1160    }
1161
1162    fn order_item(&mut self, aliases: &[&str]) -> Result<OrderItem, String> {
1163        let name = self.ident("expected ORDER BY target")?;
1164        let target = if self.eat(&Tok::Dot) {
1165            let field = self.ident("expected field name after '.'")?;
1166            OrderTarget::Prop { var: name, field }
1167        } else if aliases.contains(&name.as_str()) {
1168            OrderTarget::Alias(name)
1169        } else {
1170            OrderTarget::Var(name)
1171        };
1172        let descending = if self.eat(&Tok::Desc) {
1173            true
1174        } else {
1175            let _ = self.eat(&Tok::Asc);
1176            false
1177        };
1178        Ok(OrderItem { target, descending })
1179    }
1180
1181    fn uint(&mut self, what: &str) -> Result<LimitSkip, String> {
1182        match self.peek() {
1183            Some(Tok::Int(n)) if *n >= 0 => {
1184                let n = *n as u64;
1185                self.pos += 1;
1186                Ok(LimitSkip::Exact(n))
1187            }
1188            Some(Tok::Int(_)) => Err(self.err(&format!("{what} must be a non-negative integer"))),
1189            Some(Tok::Param(_)) => {
1190                let name = match self.toks.get(self.pos) {
1191                    Some(Tok::Param(s)) => s.clone(),
1192                    _ => unreachable!(),
1193                };
1194                self.pos += 1;
1195                Ok(LimitSkip::Param(name))
1196            }
1197            _ => Err(self.err(&format!("expected integer or $parameter after {what}"))),
1198        }
1199    }
1200
1201    // ── Write statement parsing ───────────────────────────────────────────────
1202
1203    fn write_statement(&mut self) -> Result<WriteStatement, String> {
1204        match self.peek() {
1205            Some(Tok::Create) => self.create_stmt(),
1206            Some(Tok::Merge) => self.merge_stmt(),
1207            Some(Tok::Match) => self.match_write_stmt(),
1208            _ => Err(self
1209                .err("expected CREATE, MERGE, or MATCH … SET/DELETE (write statement required)")),
1210        }
1211    }
1212
1213    // ── CREATE ────────────────────────────────────────────────────────────────
1214
1215    fn create_stmt(&mut self) -> Result<WriteStatement, String> {
1216        self.expect(&Tok::Create, "expected CREATE")?;
1217        let mut stmt = self.create_pattern()?;
1218        // Optional RETURN clause: `CREATE (n:L {…}) RETURN n` or `RETURN n.id AS id`.
1219        if self.eat(&Tok::Return) {
1220            stmt.returns = Some(self.return_items()?);
1221        }
1222        if self.pos < self.toks.len() {
1223            return Err(self.err("unexpected tokens after CREATE"));
1224        }
1225        Ok(WriteStatement::Create(stmt))
1226    }
1227
1228    fn create_pattern(&mut self) -> Result<CreateStmt, String> {
1229        // Parse the first (possibly only) node.
1230        let first = self.create_node(0)?;
1231        let first_var = first.var.clone().unwrap_or_else(|| format!("_cn{}", 0));
1232        let mut nodes: Vec<CreateNode> = vec![first];
1233        let mut edges: Vec<CreateEdge> = Vec::new();
1234
1235        // Chain: (-[:T]-> | <-[:T]-) followed by another node.
1236        while matches!(self.peek(), Some(Tok::Dash) | Some(Tok::Lt)) {
1237            let (etype, src_is_left) = self.create_rel()?;
1238            let idx = nodes.len();
1239            let next = self.create_node(idx)?;
1240            let next_var = next.var.clone().unwrap_or_else(|| format!("_cn{idx}"));
1241            let prev_var = nodes.last().unwrap().var.clone().unwrap_or_else(|| {
1242                if nodes.len() == 1 {
1243                    first_var.clone()
1244                } else {
1245                    format!("_cn{}", nodes.len() - 1)
1246                }
1247            });
1248            let (src_var, dst_var) = if src_is_left {
1249                // <-[:T]- means next→prev i.e. next is src
1250                (next_var.clone(), prev_var)
1251            } else {
1252                // -[:T]-> means prev→next
1253                (prev_var, next_var.clone())
1254            };
1255            edges.push(CreateEdge {
1256                src_var,
1257                etype,
1258                dst_var,
1259            });
1260            nodes.push(next);
1261        }
1262        Ok(CreateStmt {
1263            nodes,
1264            edges,
1265            returns: None,
1266        })
1267    }
1268
1269    fn create_node(&mut self, idx: usize) -> Result<CreateNode, String> {
1270        self.expect(&Tok::LParen, "expected '(' in CREATE node pattern")?;
1271        let var = match self.peek() {
1272            Some(Tok::Ident(s)) => {
1273                let s = s.clone();
1274                self.not_reserved(&s)?;
1275                self.pos += 1;
1276                Some(s)
1277            }
1278            _ => None,
1279        };
1280        if !self.eat(&Tok::Colon) {
1281            return Err(self.err("CREATE node requires a label (e.g., (n:Label {…}))"));
1282        }
1283        let label = self.ident("expected label identifier after ':'")?;
1284        let props = if self.peek() == Some(&Tok::LBrace) {
1285            self.literal_props()?
1286        } else {
1287            Vec::new()
1288        };
1289        self.expect(&Tok::RParen, "expected ')' to close CREATE node pattern")?;
1290        let var = Some(var.unwrap_or_else(|| format!("_cn{idx}")));
1291        Ok(CreateNode { var, label, props })
1292    }
1293
1294    /// Parse `{key: literal, …}` where all values must be literals (no params, no props).
1295    fn literal_props(&mut self) -> Result<Vec<(String, Value)>, String> {
1296        self.expect(&Tok::LBrace, "expected '{'")?;
1297        let mut out = Vec::new();
1298        if self.eat(&Tok::RBrace) {
1299            return Ok(out);
1300        }
1301        loop {
1302            let key = self.ident("expected property key")?;
1303            self.expect(&Tok::Colon, "expected ':' after property key")?;
1304            let val = self.literal_value("property value")?;
1305            out.push((key, val));
1306            if self.eat(&Tok::Comma) {
1307                continue;
1308            }
1309            break;
1310        }
1311        self.expect(&Tok::RBrace, "expected '}' to close property map")?;
1312        Ok(out)
1313    }
1314
1315    /// Parse a literal value (int, float, string, boolean or list). Parameters and property
1316    /// references are not accepted in write statements (v1 limitation).
1317    fn literal_value(&mut self, what: &str) -> Result<Value, String> {
1318        if self.eat(&Tok::Dash) {
1319            return match self.peek() {
1320                Some(Tok::Int(n)) => {
1321                    let n = *n;
1322                    self.pos += 1;
1323                    Ok(Value::Int(-n))
1324                }
1325                Some(Tok::Float(x)) => {
1326                    let x = *x;
1327                    self.pos += 1;
1328                    Ok(Value::Float(-x))
1329                }
1330                _ => Err(self.err("unary minus only applies to numeric literals")),
1331            };
1332        }
1333        match self.peek() {
1334            Some(Tok::Int(n)) => {
1335                let n = *n;
1336                self.pos += 1;
1337                Ok(Value::Int(n))
1338            }
1339            Some(Tok::Float(x)) => {
1340                let x = *x;
1341                self.pos += 1;
1342                Ok(Value::Float(x))
1343            }
1344            Some(Tok::Str(s)) => {
1345                let s = s.clone();
1346                self.pos += 1;
1347                Ok(Value::Str(s))
1348            }
1349            // List literal: `[]`, `[v1, v2, …]`, nesting allowed. Elements are
1350            // themselves literal values (recursion), matching the UNWIND list
1351            // form and downstream `Value::List` support in the store.
1352            Some(Tok::LBracket) => {
1353                self.pos += 1; // consume '['
1354                let mut items = Vec::new();
1355                if !self.eat(&Tok::RBracket) {
1356                    loop {
1357                        items.push(self.literal_value(what)?);
1358                        if self.eat(&Tok::Comma) {
1359                            continue;
1360                        }
1361                        self.expect(&Tok::RBracket, "expected ']' to close list literal")?;
1362                        break;
1363                    }
1364                }
1365                Ok(Value::List(items))
1366            }
1367            Some(Tok::Ident(name)) if bool_literal(name).is_some() => {
1368                let b = bool_literal(name).unwrap_or_default();
1369                self.pos += 1;
1370                Ok(Value::Bool(b))
1371            }
1372            Some(Tok::Param(_)) => Err(self.err(&format!(
1373                "parameter references are not supported in {what} (v1 limitation: use literals only)"
1374            ))),
1375            Some(Tok::Ident(_)) => Err(self.err(&format!(
1376                "expression RHS not supported in {what} (v1 limitation: use literals only)"
1377            ))),
1378            _ => Err(self.err(&format!("expected literal value for {what}"))),
1379        }
1380    }
1381
1382    /// Parse `-[:TYPE]->` or `<-[:TYPE]-`.  Returns `(etype, src_is_left)` where
1383    /// `src_is_left = true` means left node is dst (i.e., next node is src).
1384    fn create_rel(&mut self) -> Result<(String, bool), String> {
1385        if self.eat(&Tok::Lt) {
1386            // <-[:TYPE]-
1387            self.expect(&Tok::Dash, "expected '-' after '<' in relationship")?;
1388            self.expect(&Tok::LBracket, "expected '[' in relationship pattern")?;
1389            self.expect(&Tok::Colon, "expected ':TYPE' in CREATE relationship")?;
1390            let etype = self.ident("expected relationship type")?;
1391            self.expect(&Tok::RBracket, "expected ']'")?;
1392            self.expect(&Tok::Dash, "expected '-'")?;
1393            return Ok((etype, true));
1394        }
1395        // -[:TYPE]->
1396        self.expect(&Tok::Dash, "expected '-' to start relationship")?;
1397        self.expect(&Tok::LBracket, "expected '[' in relationship pattern")?;
1398        self.expect(&Tok::Colon, "expected ':TYPE' in CREATE relationship")?;
1399        let etype = self.ident("expected relationship type")?;
1400        self.expect(&Tok::RBracket, "expected ']'")?;
1401        self.expect(&Tok::Dash, "expected '-'")?;
1402        self.expect(
1403            &Tok::Gt,
1404            "expected '>' — CREATE requires directed relationships",
1405        )?;
1406        Ok((etype, false))
1407    }
1408
1409    // ── MERGE ─────────────────────────────────────────────────────────────────
1410
1411    fn merge_stmt(&mut self) -> Result<WriteStatement, String> {
1412        self.expect(&Tok::Merge, "expected MERGE")?;
1413        self.expect(&Tok::LParen, "expected '(' after MERGE")?;
1414        // Optional var: `MERGE (n:Label {…})` — capture n for RETURN projection.
1415        let var = match self.peek() {
1416            Some(Tok::Ident(_)) => {
1417                let s = match self.toks.get(self.pos) {
1418                    Some(Tok::Ident(s)) => s.clone(),
1419                    _ => unreachable!(),
1420                };
1421                self.not_reserved(&s)?;
1422                self.pos += 1; // consume var name
1423                Some(s)
1424            }
1425            _ => None,
1426        };
1427        if !self.eat(&Tok::Colon) {
1428            return Err(self.err("MERGE requires a label (e.g., MERGE (n:Label {key: 'x'}))"));
1429        }
1430        let label = self.ident("expected label identifier after ':'")?;
1431        if self.peek() != Some(&Tok::LBrace) {
1432            return Err(self.err(
1433                "MERGE requires a property map with exactly one key (e.g., MERGE (n:Label {id: 'x'}))",
1434            ));
1435        }
1436        let props = self.literal_props()?;
1437        let mut ns = None;
1438        let mut rest = Vec::new();
1439        for (k, v) in props {
1440            if k == NS_PROP {
1441                if ns.is_some() {
1442                    return Err(
1443                        self.err("ns is given more than once; a node has exactly one namespace")
1444                    );
1445                }
1446                ns = Some(v);
1447            } else {
1448                rest.push((k, v));
1449            }
1450        }
1451        if rest.len() != 1 {
1452            // A lone `ns` is the identifying key, not a namespace stamp.
1453            if rest.is_empty() {
1454                if let Some(v) = ns.take() {
1455                    rest.push((NS_PROP.to_string(), v));
1456                }
1457            }
1458            if rest.len() != 1 {
1459                return Err(format!(
1460                    "MERGE supports exactly one key property (got {}); use CREATE for multi-prop nodes",
1461                    rest.len() + usize::from(ns.is_some()),
1462                ));
1463            }
1464        }
1465        self.expect(&Tok::RParen, "expected ')' to close MERGE pattern")?;
1466        let mut on_create = Vec::new();
1467        let mut on_match = Vec::new();
1468        while self.eat_ident_kw("on") {
1469            if self.eat(&Tok::Create) {
1470                self.expect(&Tok::Set, "expected SET after ON CREATE")?;
1471                on_create.extend(self.set_clauses()?);
1472            } else if self.eat(&Tok::Match) {
1473                self.expect(&Tok::Set, "expected SET after ON MATCH")?;
1474                on_match.extend(self.set_clauses()?);
1475            } else {
1476                return Err(self.err("expected CREATE or MATCH after ON"));
1477            }
1478        }
1479        // Optional RETURN clause.
1480        let returns = if self.eat(&Tok::Return) {
1481            Some(self.return_items()?)
1482        } else {
1483            None
1484        };
1485        if self.pos < self.toks.len() {
1486            return Err(self.unsupported_or_unexpected("unexpected tokens after MERGE"));
1487        }
1488        let (key_field, key_value) = rest.remove(0);
1489        Ok(WriteStatement::Merge(MergeStmt {
1490            label,
1491            key_field,
1492            key_value,
1493            ns,
1494            var,
1495            on_create,
1496            on_match,
1497            returns,
1498        }))
1499    }
1500
1501    // ── MATCH … SET / MATCH … DELETE ─────────────────────────────────────────
1502
1503    fn match_write_stmt(&mut self) -> Result<WriteStatement, String> {
1504        // Parse MATCH clauses (same as read query).
1505        let mut matches = Vec::new();
1506        while self.peek() == Some(&Tok::Match) {
1507            matches.extend(self.match_clause()?);
1508        }
1509        if matches.is_empty() {
1510            return Err(self.err("expected MATCH"));
1511        }
1512        // Optional WHERE.
1513        let where_expr = if self.eat(&Tok::Where) {
1514            Some(self.expr(0)?)
1515        } else {
1516            None
1517        };
1518        // Dispatch on SET, DETACH DELETE, or DELETE.
1519        match self.peek() {
1520            Some(Tok::Set) => {
1521                self.pos += 1; // consume SET
1522                let sets = self.set_clauses()?;
1523                let returns = if self.eat(&Tok::Return) {
1524                    Some(self.return_items()?)
1525                } else {
1526                    None
1527                };
1528                if self.pos < self.toks.len() {
1529                    return Err(self.unsupported_or_unexpected("unexpected tokens after SET"));
1530                }
1531                Ok(WriteStatement::MatchSet(MatchSetStmt {
1532                    matches,
1533                    where_expr,
1534                    sets,
1535                    returns,
1536                }))
1537            }
1538            Some(Tok::Detach) => {
1539                // DETACH DELETE <node_var> [, …]
1540                self.pos += 1; // consume DETACH
1541                self.expect(&Tok::Delete, "expected DELETE after DETACH")?;
1542                let node_vars = self.node_delete_targets(&matches)?;
1543                if self.pos < self.toks.len() {
1544                    return Err(self.err("unexpected tokens after DETACH DELETE"));
1545                }
1546                Ok(WriteStatement::MatchDeleteNode(MatchDeleteNodeStmt {
1547                    matches,
1548                    where_expr,
1549                    node_vars,
1550                    detach: true,
1551                }))
1552            }
1553            Some(Tok::Delete) => {
1554                self.pos += 1; // consume DELETE
1555                               // Try to resolve all targets as edge vars first. If the first
1556                               // target is a node var (not an edge var), fall through to node delete.
1557                match self.delete_targets_or_node(&matches)? {
1558                    DeleteTargetResult::Edges(deletes) => {
1559                        if self.pos < self.toks.len() {
1560                            return Err(self.err("unexpected tokens after DELETE"));
1561                        }
1562                        Ok(WriteStatement::MatchDelete(MatchDeleteStmt {
1563                            matches,
1564                            where_expr,
1565                            deletes,
1566                        }))
1567                    }
1568                    DeleteTargetResult::Nodes(node_vars) => {
1569                        if self.pos < self.toks.len() {
1570                            return Err(self.err("unexpected tokens after DELETE"));
1571                        }
1572                        Ok(WriteStatement::MatchDeleteNode(MatchDeleteNodeStmt {
1573                            matches,
1574                            where_expr,
1575                            node_vars,
1576                            detach: false,
1577                        }))
1578                    }
1579                }
1580            }
1581            _ => Err(self.err(
1582                "expected SET or DELETE after MATCH [WHERE]; \
1583                 combined MATCH…RETURN is a read query, not a write statement",
1584            )),
1585        }
1586    }
1587
1588    fn set_clauses(&mut self) -> Result<Vec<SetClause>, String> {
1589        let mut sets = vec![self.set_clause()?];
1590        while self.eat(&Tok::Comma) {
1591            sets.push(self.set_clause()?);
1592        }
1593        Ok(sets)
1594    }
1595
1596    fn set_clause(&mut self) -> Result<SetClause, String> {
1597        let var = self.ident("expected variable in SET clause")?;
1598        self.expect(&Tok::Dot, "expected '.' after variable in SET")?;
1599        let field = self.ident("expected field name after '.'")?;
1600        self.expect(&Tok::Eq, "expected '=' in SET clause")?;
1601        // Accept Lit, Param, BinArith (arithmetic expression), and FuncCall on
1602        // the RHS. Bare Prop/Var (e.g. `SET n.x = m.y`) remain a named error
1603        // because that form requires join semantics not supported in v1; use
1604        // an arithmetic expression like `m.y + 0` if needed.
1605        let value = match self.peek() {
1606            Some(
1607                Tok::Int(_)
1608                | Tok::Float(_)
1609                | Tok::Str(_)
1610                | Tok::Param(_)
1611                | Tok::Dash
1612                | Tok::Ident(_),
1613            ) => {
1614                let op = self.arith_expr()?;
1615                match &op {
1616                    Operand::Lit(_)
1617                    | Operand::Param(_)
1618                    | Operand::BinArith { .. }
1619                    | Operand::FuncCall { .. }
1620                    | Operand::Index { .. }
1621                    | Operand::Case { .. } => op,
1622                    Operand::Prop { .. } | Operand::Var(_) => {
1623                        return Err(self.err(
1624                            "SET RHS: bare property/variable reference is not supported; \
1625                             use a literal, $parameter, or arithmetic expression (e.g. n.x + 1)",
1626                        ));
1627                    }
1628                }
1629            }
1630            // List-literal RHS: `SET n.tags = ['a', 'b']`. Lists are pure
1631            // literals (no arithmetic), so parse directly into `Operand::Lit`.
1632            Some(Tok::LBracket) => Operand::Lit(self.literal_value("SET value")?),
1633            _ => {
1634                return Err(
1635                    self.err("expected literal, $parameter, or arithmetic expression as SET value")
1636                )
1637            }
1638        };
1639        Ok(SetClause { var, field, value })
1640    }
1641
1642    /// Find the rel-var `var` in `patterns` and return its etype, src node var,
1643    /// and dst node var.  Returns Err if the var is not a rel var or has no type.
1644    fn resolve_edge_var(&self, var: &str, patterns: &[Pattern]) -> Result<EdgeDelete, String> {
1645        for pat in patterns {
1646            let start_var = pat.start.var.as_deref().unwrap_or("_unknown");
1647            let mut from_var = start_var;
1648            for (rel, dest) in &pat.chain {
1649                let to_var = dest.var.as_deref().unwrap_or("_unknown");
1650                if rel.var.as_deref() == Some(var) {
1651                    let etype = match rel.etypes.as_slice() {
1652                        [t] => t.clone(),
1653                        [] => {
1654                            return Err(format!(
1655                                "DELETE `{var}`: relationship has no type; \
1656                                 DELETE requires an explicit edge type (e.g., [r:TYPE])"
1657                            ))
1658                        }
1659                        _ => {
1660                            return Err(format!(
1661                                "DELETE `{var}`: relationship has multiple types; \
1662                                 DELETE requires a single explicit edge type (e.g., [r:TYPE])"
1663                            ))
1664                        }
1665                    };
1666                    let (src_var, dst_var) = match rel.dir {
1667                        RelDir::Right => (from_var.to_string(), to_var.to_string()),
1668                        RelDir::Left => (to_var.to_string(), from_var.to_string()),
1669                        RelDir::Undirected => {
1670                            return Err(format!(
1671                                "DELETE `{var}`: undirected relationship DELETE is not supported; \
1672                                 use a directed pattern (e.g., -[r:TYPE]->)"
1673                            ))
1674                        }
1675                    };
1676                    return Ok(EdgeDelete {
1677                        rel_var: var.to_string(),
1678                        etype,
1679                        src_var,
1680                        dst_var,
1681                    });
1682                }
1683                from_var = to_var;
1684            }
1685        }
1686        Err(format!(
1687            "DELETE `{var}`: variable is not bound as a relationship in any MATCH pattern; \
1688             only relationship variables can be deleted (DELETE edge vars, not node vars)"
1689        ))
1690    }
1691
1692    /// Return `true` if `var` is bound as a node variable in `patterns`.
1693    fn is_node_var(&self, var: &str, patterns: &[Pattern]) -> bool {
1694        for pat in patterns {
1695            if pat.start.var.as_deref() == Some(var) {
1696                return true;
1697            }
1698            for (_, dest) in &pat.chain {
1699                if dest.var.as_deref() == Some(var) {
1700                    return true;
1701                }
1702            }
1703        }
1704        false
1705    }
1706
1707    /// Parse a comma-separated list of node-variable targets for
1708    /// `[DETACH] DELETE`.  All targets must be node variables bound in `patterns`.
1709    fn node_delete_targets(&mut self, patterns: &[Pattern]) -> Result<Vec<String>, String> {
1710        let mut vars = Vec::new();
1711        loop {
1712            let var = self.ident("expected node variable to DELETE")?;
1713            if !self.is_node_var(&var, patterns) {
1714                return Err(format!(
1715                    "DELETE `{var}`: variable is not bound as a node in any MATCH pattern"
1716                ));
1717            }
1718            vars.push(var);
1719            if !self.eat(&Tok::Comma) {
1720                break;
1721            }
1722        }
1723        Ok(vars)
1724    }
1725
1726    /// Try to parse DELETE targets as edge vars; if the first target is a node
1727    /// var, fall back to parsing all targets as node vars.
1728    fn delete_targets_or_node(
1729        &mut self,
1730        patterns: &[Pattern],
1731    ) -> Result<DeleteTargetResult, String> {
1732        // Peek at the identifier to decide which path to take.
1733        let var = self.ident("expected variable to DELETE")?;
1734        // Try edge var first.
1735        match self.resolve_edge_var(&var, patterns) {
1736            Ok(edge_del) => {
1737                // At least the first target is an edge var; parse the rest as
1738                // edge vars too.
1739                let mut targets = vec![edge_del];
1740                while self.eat(&Tok::Comma) {
1741                    let v = self.ident("expected variable to DELETE")?;
1742                    targets.push(self.resolve_edge_var(&v, patterns)?);
1743                }
1744                Ok(DeleteTargetResult::Edges(targets))
1745            }
1746            Err(_) => {
1747                // Not an edge var; try as node var.
1748                if self.is_node_var(&var, patterns) {
1749                    let mut node_vars = vec![var];
1750                    while self.eat(&Tok::Comma) {
1751                        let v = self.ident("expected variable to DELETE")?;
1752                        if !self.is_node_var(&v, patterns) {
1753                            return Err(format!(
1754                                "DELETE `{v}`: variable is not bound as a node in any MATCH pattern"
1755                            ));
1756                        }
1757                        node_vars.push(v);
1758                    }
1759                    Ok(DeleteTargetResult::Nodes(node_vars))
1760                } else {
1761                    Err(format!(
1762                        "DELETE `{var}`: variable is not bound as a relationship or node \
1763                         in any MATCH pattern"
1764                    ))
1765                }
1766            }
1767        }
1768    }
1769}
1770
1771/// Used internally by `delete_targets_or_node` to signal whether the targets
1772/// resolved to edge variables or node variables.
1773enum DeleteTargetResult {
1774    Edges(Vec<EdgeDelete>),
1775    Nodes(Vec<String>),
1776}
1777
1778#[cfg(test)]
1779mod tests {
1780    use super::parse;
1781    use crate::cypher::ast::{
1782        AggFunc, Expr, HopRange, LimitSkip, NodePat, Operand, OrderItem, OrderTarget, Pattern,
1783        Query, RelDir, RelPat, RetItem, RetVal,
1784    };
1785    use crate::cypher::{lex, Tok};
1786    use crate::filter::CmpOp;
1787    use core_storage::Value;
1788
1789    fn parse_src(src: &str) -> Result<Query, String> {
1790        parse(&lex(src)?)
1791    }
1792
1793    fn prop(var: &str, field: &str) -> Operand {
1794        Operand::Prop {
1795            var: var.into(),
1796            field: field.into(),
1797        }
1798    }
1799
1800    fn cmp(lhs: Operand, op: CmpOp, rhs: Operand) -> Expr {
1801        Expr::Cmp { lhs, op, rhs }
1802    }
1803
1804    fn node(var: Option<&str>, label: Option<&str>, props: Vec<(String, Operand)>) -> NodePat {
1805        NodePat {
1806            var: var.map(str::to_string),
1807            label: label.map(str::to_string),
1808            props,
1809        }
1810    }
1811
1812    #[test]
1813    fn full_feature_query_exact_ast() {
1814        let src = "\
1815MATCH (a:Person {name: $n, age: 30})-[r:KNOWS]->(b)-[u:TEAM]-(c)<-[s:LIKES]-(d) \
1816WHERE NOT a.age < 18 AND b.name = 'x' OR c.score >= 2.5 \
1817RETURN a, r.since AS since, b.name \
1818ORDER BY since DESC, b.name ASC \
1819SKIP 1 LIMIT 5";
1820        let got = parse_src(src).expect("full-feature query must parse");
1821        let expected = Query {
1822            matches: vec![Pattern {
1823                start: node(
1824                    Some("a"),
1825                    Some("Person"),
1826                    vec![
1827                        ("name".into(), Operand::Param("n".into())),
1828                        ("age".into(), Operand::Lit(Value::Int(30))),
1829                    ],
1830                ),
1831                chain: vec![
1832                    (
1833                        RelPat {
1834                            var: Some("r".into()),
1835                            etypes: vec!["KNOWS".into()],
1836                            dir: RelDir::Right,
1837                            hops: None,
1838                        },
1839                        node(Some("b"), None, vec![]),
1840                    ),
1841                    (
1842                        RelPat {
1843                            var: Some("u".into()),
1844                            etypes: vec!["TEAM".into()],
1845                            dir: RelDir::Undirected,
1846                            hops: None,
1847                        },
1848                        node(Some("c"), None, vec![]),
1849                    ),
1850                    (
1851                        RelPat {
1852                            var: Some("s".into()),
1853                            etypes: vec!["LIKES".into()],
1854                            dir: RelDir::Left,
1855                            hops: None,
1856                        },
1857                        node(Some("d"), None, vec![]),
1858                    ),
1859                ],
1860                shortest: false,
1861            }],
1862            optional_clauses: vec![],
1863            unwinds: vec![],
1864            post_unwind_where: None,
1865            where_expr: Some(Expr::Or(
1866                Box::new(Expr::And(
1867                    Box::new(Expr::Not(Box::new(cmp(
1868                        prop("a", "age"),
1869                        CmpOp::Lt,
1870                        Operand::Lit(Value::Int(18)),
1871                    )))),
1872                    Box::new(cmp(
1873                        prop("b", "name"),
1874                        CmpOp::Eq,
1875                        Operand::Lit(Value::Str("x".into())),
1876                    )),
1877                )),
1878                Box::new(cmp(
1879                    prop("c", "score"),
1880                    CmpOp::Ge,
1881                    Operand::Lit(Value::Float(2.5)),
1882                )),
1883            )),
1884            stages: vec![],
1885            returns: vec![
1886                RetItem {
1887                    value: RetVal::Var("a".into()),
1888                    alias: None,
1889                },
1890                RetItem {
1891                    value: RetVal::Prop {
1892                        var: "r".into(),
1893                        field: "since".into(),
1894                    },
1895                    alias: Some("since".into()),
1896                },
1897                RetItem {
1898                    value: RetVal::Prop {
1899                        var: "b".into(),
1900                        field: "name".into(),
1901                    },
1902                    alias: None,
1903                },
1904            ],
1905            order_by: vec![
1906                OrderItem {
1907                    target: OrderTarget::Alias("since".into()),
1908                    descending: true,
1909                },
1910                OrderItem {
1911                    target: OrderTarget::Prop {
1912                        var: "b".into(),
1913                        field: "name".into(),
1914                    },
1915                    descending: false,
1916                },
1917            ],
1918            distinct: false,
1919            skip: Some(LimitSkip::Exact(1)),
1920            limit: Some(LimitSkip::Exact(5)),
1921        };
1922        assert_eq!(got, expected);
1923    }
1924
1925    #[test]
1926    fn rel_direction_right() {
1927        let q = parse_src("MATCH (a)-[r:T]->(b) RETURN a").unwrap();
1928        assert_eq!(q.matches[0].chain[0].0.dir, RelDir::Right);
1929        assert_eq!(q.matches[0].chain[0].0.var.as_deref(), Some("r"));
1930        assert_eq!(q.matches[0].chain[0].0.etypes, vec!["T".to_string()]);
1931    }
1932
1933    #[test]
1934    fn rel_direction_left() {
1935        let q = parse_src("MATCH (a)<-[r:T]-(b) RETURN a").unwrap();
1936        assert_eq!(q.matches[0].chain[0].0.dir, RelDir::Left);
1937    }
1938
1939    #[test]
1940    fn rel_direction_undirected() {
1941        let q = parse_src("MATCH (a)-[r:T]-(b) RETURN a").unwrap();
1942        assert_eq!(q.matches[0].chain[0].0.dir, RelDir::Undirected);
1943    }
1944
1945    #[test]
1946    fn node_props_map_with_param() {
1947        let q = parse_src("MATCH (t:Talent {id: $tid, n: 1, s: 'x'}) RETURN t").unwrap();
1948        assert_eq!(
1949            q.matches[0].start.props,
1950            vec![
1951                ("id".into(), Operand::Param("tid".into())),
1952                ("n".into(), Operand::Lit(Value::Int(1))),
1953                ("s".into(), Operand::Lit(Value::Str("x".into()))),
1954            ]
1955        );
1956        assert_eq!(q.matches[0].start.var.as_deref(), Some("t"));
1957        assert_eq!(q.matches[0].start.label.as_deref(), Some("Talent"));
1958    }
1959
1960    #[test]
1961    fn operator_precedence_or_and_not() {
1962        let q = parse_src("MATCH (a) WHERE a.x = 1 OR b.y = 2 AND NOT c.z = 3 RETURN a").unwrap();
1963        let expected = Expr::Or(
1964            Box::new(cmp(prop("a", "x"), CmpOp::Eq, Operand::Lit(Value::Int(1)))),
1965            Box::new(Expr::And(
1966                Box::new(cmp(prop("b", "y"), CmpOp::Eq, Operand::Lit(Value::Int(2)))),
1967                Box::new(Expr::Not(Box::new(cmp(
1968                    prop("c", "z"),
1969                    CmpOp::Eq,
1970                    Operand::Lit(Value::Int(3)),
1971                )))),
1972            )),
1973        );
1974        assert_eq!(q.where_expr, Some(expected));
1975    }
1976
1977    #[test]
1978    fn unary_minus_folds_numeric_literals() {
1979        let q = parse_src("MATCH (a) WHERE a.x > -5 AND a.y < -1.5 RETURN a").unwrap();
1980        let expected = Expr::And(
1981            Box::new(cmp(prop("a", "x"), CmpOp::Gt, Operand::Lit(Value::Int(-5)))),
1982            Box::new(cmp(
1983                prop("a", "y"),
1984                CmpOp::Lt,
1985                Operand::Lit(Value::Float(-1.5)),
1986            )),
1987        );
1988        assert_eq!(q.where_expr, Some(expected));
1989    }
1990
1991    fn assert_parse_err(src: &str) {
1992        let result = std::panic::catch_unwind(|| parse_src(src));
1993        assert!(result.is_ok(), "parse({src:?}) panicked");
1994        let err = result
1995            .unwrap()
1996            .expect_err(&format!("parse({src:?}) must be Err"));
1997        // Parse errors say "token"; lex errors say "position". Either is a valid reject.
1998        assert!(
1999            err.contains("token") || err.contains("position"),
2000            "error must include token or lex position, got: {err}"
2001        );
2002    }
2003
2004    #[test]
2005    fn malformed_match_alone_is_err() {
2006        assert_parse_err("MATCH");
2007    }
2008
2009    #[test]
2010    fn malformed_missing_return_is_err() {
2011        assert_parse_err("MATCH (n)");
2012    }
2013
2014    #[test]
2015    fn malformed_rel_colon_without_type_is_err() {
2016        assert_parse_err("MATCH (a)-[x:]->(b) RETURN a");
2017    }
2018
2019    #[test]
2020    fn malformed_dangling_comma_in_return_is_err() {
2021        assert_parse_err("MATCH (n) RETURN n,");
2022    }
2023
2024    #[test]
2025    fn malformed_unclosed_paren_is_err() {
2026        assert_parse_err("MATCH (n RETURN n");
2027        assert_parse_err("MATCH (n) WHERE (a.x = 1 RETURN n");
2028    }
2029
2030    #[test]
2031    fn malformed_order_by_before_return_is_err() {
2032        assert_parse_err("MATCH (n) ORDER BY n RETURN n");
2033    }
2034
2035    #[test]
2036    fn malformed_garbage_after_limit_is_err() {
2037        assert_parse_err("MATCH (n) RETURN n LIMIT 1 extra");
2038    }
2039
2040    #[test]
2041    fn dogfood_query_exact_ast() {
2042        let src = "\
2043MATCH (t:Talent {id: $tid}) \
2044MATCH (c:Company)-[i:INDUSTRY_ALIGNMENT]->(t) \
2045MATCH (c)-[s:SPECIALTY_MATCH]->(t) \
2046WHERE i.score >= 0.5 AND s.score >= 0.5 \
2047RETURN c, i.score AS industry, s.score AS specialty \
2048ORDER BY industry DESC, specialty DESC \
2049LIMIT 10";
2050        let got = parse_src(src).expect("dogfood query must parse");
2051        let expected = Query {
2052            matches: vec![
2053                Pattern {
2054                    start: node(
2055                        Some("t"),
2056                        Some("Talent"),
2057                        vec![("id".into(), Operand::Param("tid".into()))],
2058                    ),
2059                    chain: vec![],
2060                    shortest: false,
2061                },
2062                Pattern {
2063                    start: node(Some("c"), Some("Company"), vec![]),
2064                    chain: vec![(
2065                        RelPat {
2066                            var: Some("i".into()),
2067                            etypes: vec!["INDUSTRY_ALIGNMENT".into()],
2068                            dir: RelDir::Right,
2069                            hops: None,
2070                        },
2071                        node(Some("t"), None, vec![]),
2072                    )],
2073                    shortest: false,
2074                },
2075                Pattern {
2076                    start: node(Some("c"), None, vec![]),
2077                    chain: vec![(
2078                        RelPat {
2079                            var: Some("s".into()),
2080                            etypes: vec!["SPECIALTY_MATCH".into()],
2081                            dir: RelDir::Right,
2082                            hops: None,
2083                        },
2084                        node(Some("t"), None, vec![]),
2085                    )],
2086                    shortest: false,
2087                },
2088            ],
2089            optional_clauses: vec![],
2090            unwinds: vec![],
2091            post_unwind_where: None,
2092            where_expr: Some(Expr::And(
2093                Box::new(cmp(
2094                    prop("i", "score"),
2095                    CmpOp::Ge,
2096                    Operand::Lit(Value::Float(0.5)),
2097                )),
2098                Box::new(cmp(
2099                    prop("s", "score"),
2100                    CmpOp::Ge,
2101                    Operand::Lit(Value::Float(0.5)),
2102                )),
2103            )),
2104            stages: vec![],
2105            returns: vec![
2106                RetItem {
2107                    value: RetVal::Var("c".into()),
2108                    alias: None,
2109                },
2110                RetItem {
2111                    value: RetVal::Prop {
2112                        var: "i".into(),
2113                        field: "score".into(),
2114                    },
2115                    alias: Some("industry".into()),
2116                },
2117                RetItem {
2118                    value: RetVal::Prop {
2119                        var: "s".into(),
2120                        field: "score".into(),
2121                    },
2122                    alias: Some("specialty".into()),
2123                },
2124            ],
2125            order_by: vec![
2126                OrderItem {
2127                    target: OrderTarget::Alias("industry".into()),
2128                    descending: true,
2129                },
2130                OrderItem {
2131                    target: OrderTarget::Alias("specialty".into()),
2132                    descending: true,
2133                },
2134            ],
2135            distinct: false,
2136            skip: None,
2137            limit: Some(LimitSkip::Exact(10)),
2138        };
2139        assert_eq!(got, expected);
2140    }
2141
2142    #[test]
2143    fn unary_minus_in_props_and_dash_elsewhere_is_err() {
2144        let q = parse_src("MATCH (a {x: -5, y: -1.5}) RETURN a").unwrap();
2145        assert_eq!(
2146            q.matches[0].start.props,
2147            vec![
2148                ("x".into(), Operand::Lit(Value::Int(-5))),
2149                ("y".into(), Operand::Lit(Value::Float(-1.5))),
2150            ]
2151        );
2152        // `1 - 2` is now valid arithmetic — no longer a parse error.
2153        let q2 = parse_src("MATCH (a) WHERE a.x = 1 - 2 RETURN a").unwrap();
2154        assert!(q2.where_expr.is_some());
2155        // Unary minus on non-literal remains an error.
2156        assert_parse_err("MATCH (a) WHERE a.x > -b.y RETURN a");
2157        assert_parse_err("MATCH (a) RETURN a SKIP -1");
2158    }
2159
2160    // ── IS NULL / IS NOT NULL ──────────────────────────────────────────────────
2161
2162    #[test]
2163    fn is_null_parses_on_prop() {
2164        let q = parse_src("MATCH (a) WHERE a.x IS NULL RETURN a").unwrap();
2165        assert_eq!(q.where_expr, Some(Expr::IsNull(prop("a", "x"))),);
2166    }
2167
2168    #[test]
2169    fn is_not_null_parses_on_prop() {
2170        let q = parse_src("MATCH (a) WHERE a.x IS NOT NULL RETURN a").unwrap();
2171        assert_eq!(q.where_expr, Some(Expr::IsNotNull(prop("a", "x"))),);
2172    }
2173
2174    #[test]
2175    fn is_null_on_var() {
2176        let q =
2177            parse_src("MATCH (a) OPTIONAL MATCH (a)-[:T]->(b) WITH a, b WHERE b IS NULL RETURN a")
2178                .unwrap();
2179        // The IS NULL filter lives on the first WITH stage's where_expr.
2180        let stage = &q.stages[0];
2181        assert_eq!(
2182            stage.where_expr,
2183            Some(Expr::IsNull(Operand::Var("b".into()))),
2184        );
2185    }
2186
2187    #[test]
2188    fn is_null_case_insensitive() {
2189        let q = parse_src("MATCH (a) WHERE a.x is null RETURN a").unwrap();
2190        assert_eq!(q.where_expr, Some(Expr::IsNull(prop("a", "x"))));
2191        let q2 = parse_src("MATCH (a) WHERE a.x IS NOT NULL RETURN a").unwrap();
2192        assert_eq!(q2.where_expr, Some(Expr::IsNotNull(prop("a", "x"))));
2193    }
2194
2195    #[test]
2196    fn is_null_combined_with_and() {
2197        let q = parse_src("MATCH (a) WHERE a.x IS NULL AND a.y > 5 RETURN a").unwrap();
2198        assert!(matches!(q.where_expr, Some(Expr::And(_, _))));
2199    }
2200
2201    // ── Arithmetic expression parsing ──────────────────────────────────────────
2202
2203    #[test]
2204    fn arith_add_in_where() {
2205        use crate::cypher::ast::ArithOp;
2206        let q = parse_src("MATCH (n) WHERE n.age + 1 > 5 RETURN n").unwrap();
2207        let expected_lhs = Operand::BinArith {
2208            op: ArithOp::Add,
2209            left: Box::new(prop("n", "age")),
2210            right: Box::new(Operand::Lit(Value::Int(1))),
2211        };
2212        assert_eq!(
2213            q.where_expr,
2214            Some(Expr::Cmp {
2215                lhs: expected_lhs,
2216                op: CmpOp::Gt,
2217                rhs: Operand::Lit(Value::Int(5)),
2218            })
2219        );
2220    }
2221
2222    #[test]
2223    fn arith_precedence_mul_over_add() {
2224        use crate::cypher::ast::ArithOp;
2225        // 1 + 2 * 3  should parse as  1 + (2 * 3)
2226        let q = parse_src("MATCH (n) WHERE n.x = 1 + 2 * 3 RETURN n").unwrap();
2227        let expected_rhs = Operand::BinArith {
2228            op: ArithOp::Add,
2229            left: Box::new(Operand::Lit(Value::Int(1))),
2230            right: Box::new(Operand::BinArith {
2231                op: ArithOp::Mul,
2232                left: Box::new(Operand::Lit(Value::Int(2))),
2233                right: Box::new(Operand::Lit(Value::Int(3))),
2234            }),
2235        };
2236        assert_eq!(
2237            q.where_expr,
2238            Some(Expr::Cmp {
2239                lhs: prop("n", "x"),
2240                op: CmpOp::Eq,
2241                rhs: expected_rhs,
2242            })
2243        );
2244    }
2245
2246    #[test]
2247    fn arith_parens_override_precedence() {
2248        use crate::cypher::ast::ArithOp;
2249        // In RETURN position: (1+2)*3 should parse as (1+2)*3
2250        let q = parse_src("MATCH (n) RETURN (1 + 2) * 3 AS r").unwrap();
2251        let expected = RetVal::ScalarExpr(Operand::BinArith {
2252            op: ArithOp::Mul,
2253            left: Box::new(Operand::BinArith {
2254                op: ArithOp::Add,
2255                left: Box::new(Operand::Lit(Value::Int(1))),
2256                right: Box::new(Operand::Lit(Value::Int(2))),
2257            }),
2258            right: Box::new(Operand::Lit(Value::Int(3))),
2259        });
2260        assert_eq!(q.returns[0].value, expected);
2261        assert_eq!(q.returns[0].alias, Some("r".into()));
2262    }
2263
2264    #[test]
2265    fn arith_scalar_expr_in_return() {
2266        use crate::cypher::ast::ArithOp;
2267        let q = parse_src("MATCH (n) RETURN n.age + 1 AS adjusted").unwrap();
2268        let expected = RetVal::ScalarExpr(Operand::BinArith {
2269            op: ArithOp::Add,
2270            left: Box::new(prop("n", "age")),
2271            right: Box::new(Operand::Lit(Value::Int(1))),
2272        });
2273        assert_eq!(q.returns[0].value, expected);
2274        assert_eq!(q.returns[0].alias, Some("adjusted".into()));
2275    }
2276
2277    #[test]
2278    fn arith_div_in_where() {
2279        use crate::cypher::ast::ArithOp;
2280        let q = parse_src("MATCH (n) WHERE n.x / 2 > 3 RETURN n").unwrap();
2281        assert!(matches!(
2282            q.where_expr,
2283            Some(Expr::Cmp {
2284                lhs: Operand::BinArith {
2285                    op: ArithOp::Div,
2286                    ..
2287                },
2288                ..
2289            })
2290        ));
2291    }
2292
2293    // ── CREATE...RETURN and MERGE...RETURN parser tests ────────────────────────
2294
2295    #[test]
2296    fn create_return_parses_node_var() {
2297        use super::parse_write;
2298        use crate::cypher::ast::{RetVal, WriteStatement};
2299
2300        let toks = crate::cypher::lex("CREATE (n:Thing {id: 'x'}) RETURN n").unwrap();
2301        let stmt = parse_write(&toks).unwrap();
2302        match stmt {
2303            WriteStatement::Create(s) => {
2304                assert_eq!(s.nodes.len(), 1);
2305                let returns = s.returns.expect("expected RETURN clause");
2306                assert_eq!(returns.len(), 1);
2307                assert_eq!(returns[0].value, RetVal::Var("n".into()));
2308            }
2309            _ => panic!("expected Create"),
2310        }
2311    }
2312
2313    #[test]
2314    fn create_return_prop_with_alias() {
2315        use super::parse_write;
2316        use crate::cypher::ast::{RetVal, WriteStatement};
2317
2318        let toks = crate::cypher::lex("CREATE (n:Thing {id: 'x'}) RETURN n.id AS node_id").unwrap();
2319        let stmt = parse_write(&toks).unwrap();
2320        match stmt {
2321            WriteStatement::Create(s) => {
2322                let returns = s.returns.expect("RETURN required");
2323                assert_eq!(
2324                    returns[0].value,
2325                    RetVal::Prop {
2326                        var: "n".into(),
2327                        field: "id".into()
2328                    }
2329                );
2330                assert_eq!(returns[0].alias, Some("node_id".into()));
2331            }
2332            _ => panic!("expected Create"),
2333        }
2334    }
2335
2336    #[test]
2337    fn merge_return_parses_node_var() {
2338        use super::parse_write;
2339        use crate::cypher::ast::{RetVal, WriteStatement};
2340
2341        let toks = crate::cypher::lex("MERGE (n:Thing {id: 'x'}) RETURN n").unwrap();
2342        let stmt = parse_write(&toks).unwrap();
2343        match stmt {
2344            WriteStatement::Merge(s) => {
2345                assert_eq!(s.var, Some("n".into()));
2346                let returns = s.returns.expect("RETURN required");
2347                assert_eq!(returns[0].value, RetVal::Var("n".into()));
2348            }
2349            _ => panic!("expected Merge"),
2350        }
2351    }
2352
2353    #[test]
2354    fn is_write_tokens_still_true_for_create_return() {
2355        use super::is_write_tokens;
2356        use crate::cypher::lex;
2357
2358        let toks = lex("CREATE (n:T {id: 'x'}) RETURN n").unwrap();
2359        assert!(
2360            is_write_tokens(&toks),
2361            "CREATE...RETURN must still be classified as write"
2362        );
2363    }
2364
2365    #[test]
2366    fn where_in_list_and_param_parses() {
2367        let q = parse_src("MATCH (n) WHERE n.city IN ['Austin', $c] RETURN n").unwrap();
2368        match q.where_expr {
2369            Some(Expr::In { expr, list }) => {
2370                assert_eq!(
2371                    expr,
2372                    Operand::Prop {
2373                        var: "n".into(),
2374                        field: "city".into()
2375                    }
2376                );
2377                assert_eq!(list.len(), 2);
2378                assert_eq!(list[0], Operand::Lit(Value::Str("Austin".into())));
2379                assert_eq!(list[1], Operand::Param("c".into()));
2380            }
2381            other => panic!("expected Expr::In, got {other:?}"),
2382        }
2383        let q2 = parse_src("MATCH (n) WHERE n.city IN $cities RETURN n").unwrap();
2384        match q2.where_expr {
2385            Some(Expr::In { list, .. }) => {
2386                assert_eq!(list, vec![Operand::Param("cities".into())]);
2387            }
2388            other => panic!("expected Expr::In, got {other:?}"),
2389        }
2390    }
2391
2392    #[test]
2393    fn return_distinct_parses() {
2394        let q = parse_src("MATCH (n) RETURN DISTINCT n.city").unwrap();
2395        assert!(q.distinct);
2396        assert_eq!(q.returns.len(), 1);
2397    }
2398
2399    #[test]
2400    fn union_query_parses_into_parts() {
2401        use super::parse_read;
2402        let u = parse_read(&lex("MATCH (n) RETURN n UNION ALL MATCH (m) RETURN m").unwrap())
2403            .expect("UNION parses");
2404        assert_eq!(u.parts.len(), 2);
2405        assert_eq!(u.all_flags, vec![true]);
2406        // A single query yields one part, no boundaries.
2407        let single = parse_read(&lex("MATCH (n) RETURN n").unwrap()).unwrap();
2408        assert_eq!(single.parts.len(), 1);
2409        assert!(single.all_flags.is_empty());
2410    }
2411
2412    #[test]
2413    fn case_when_expression_parses() {
2414        let q = parse_src("MATCH (n) RETURN CASE WHEN n.x = 1 THEN 2 ELSE 3 END AS c")
2415            .expect("CASE parses");
2416        assert!(matches!(
2417            q.returns[0].value,
2418            RetVal::ScalarExpr(Operand::Case { .. })
2419        ));
2420    }
2421
2422    #[test]
2423    fn collect_is_a_supported_aggregate() {
2424        let q = parse_src("MATCH (n) RETURN collect(n.name) AS names").expect("collect parses");
2425        assert!(matches!(
2426            q.returns[0].value,
2427            RetVal::Agg {
2428                func: AggFunc::Collect,
2429                ..
2430            }
2431        ));
2432    }
2433
2434    #[test]
2435    fn match_set_return_parses() {
2436        use super::parse_write;
2437        use crate::cypher::ast::{RetVal, WriteStatement};
2438
2439        let toks = crate::cypher::lex("MATCH (n {id:'a'}) SET n.x = 2 RETURN n.x").unwrap();
2440        let stmt = parse_write(&toks).unwrap();
2441        match stmt {
2442            WriteStatement::MatchSet(s) => {
2443                let returns = s.returns.expect("RETURN required");
2444                assert_eq!(
2445                    returns[0].value,
2446                    RetVal::Prop {
2447                        var: "n".into(),
2448                        field: "x".into()
2449                    }
2450                );
2451            }
2452            other => panic!("expected MatchSet, got {other:?}"),
2453        }
2454    }
2455
2456    #[test]
2457    fn merge_on_create_and_on_match_parse() {
2458        use super::parse_write;
2459        use crate::cypher::ast::WriteStatement;
2460
2461        let toks = crate::cypher::lex(
2462            "MERGE (n:L {id:'new'}) ON CREATE SET n.born = 1 ON MATCH SET n.hit = 1 RETURN n",
2463        )
2464        .unwrap();
2465        let stmt = parse_write(&toks).unwrap();
2466        match stmt {
2467            WriteStatement::Merge(s) => {
2468                assert_eq!(s.on_create.len(), 1);
2469                assert_eq!(s.on_create[0].field, "born");
2470                assert_eq!(s.on_match.len(), 1);
2471                assert_eq!(s.on_match[0].field, "hit");
2472                assert!(s.returns.is_some());
2473            }
2474            other => panic!("expected Merge, got {other:?}"),
2475        }
2476    }
2477
2478    #[test]
2479    fn merge_ns_property_parses_beside_the_key() {
2480        use super::parse_write;
2481        use crate::cypher::ast::WriteStatement;
2482
2483        let toks = crate::cypher::lex("MERGE (n:Doc {id: 'x', ns: 'other'})").unwrap();
2484        let stmt = parse_write(&toks).expect("MERGE may name ns beside the identifying key");
2485        match stmt {
2486            WriteStatement::Merge(s) => {
2487                assert_eq!(s.key_field, "id");
2488                assert_eq!(s.key_value, Value::Str("x".into()));
2489                assert_eq!(s.ns, Some(Value::Str("other".into())));
2490            }
2491            other => panic!("expected Merge, got {other:?}"),
2492        }
2493
2494        let toks = crate::cypher::lex("MERGE (n:Doc {id: 'x', name: 'Alice'})").unwrap();
2495        let err = parse_write(&toks).expect_err("a non-ns extra property is still refused");
2496        assert!(
2497            err.contains("one key property") || err.contains("exactly one"),
2498            "error must mention the single-key constraint, got: {err}"
2499        );
2500    }
2501
2502    #[test]
2503    fn paren_grouping_and_and_left_assoc() {
2504        let q = parse_src("MATCH (a) WHERE (a.x = 1 OR a.y = 2) AND a.z = 3 RETURN a").unwrap();
2505        let expected = Expr::And(
2506            Box::new(Expr::Or(
2507                Box::new(cmp(prop("a", "x"), CmpOp::Eq, Operand::Lit(Value::Int(1)))),
2508                Box::new(cmp(prop("a", "y"), CmpOp::Eq, Operand::Lit(Value::Int(2)))),
2509            )),
2510            Box::new(cmp(prop("a", "z"), CmpOp::Eq, Operand::Lit(Value::Int(3)))),
2511        );
2512        assert_eq!(q.where_expr, Some(expected));
2513
2514        let q = parse_src("MATCH (a) WHERE a.x = 1 AND a.y = 2 AND a.z = 3 RETURN a").unwrap();
2515        let expected = Expr::And(
2516            Box::new(Expr::And(
2517                Box::new(cmp(prop("a", "x"), CmpOp::Eq, Operand::Lit(Value::Int(1)))),
2518                Box::new(cmp(prop("a", "y"), CmpOp::Eq, Operand::Lit(Value::Int(2)))),
2519            )),
2520            Box::new(cmp(prop("a", "z"), CmpOp::Eq, Operand::Lit(Value::Int(3)))),
2521        );
2522        assert_eq!(q.where_expr, Some(expected));
2523    }
2524
2525    #[test]
2526    fn order_by_bare_ident_is_var_when_not_an_alias() {
2527        let q = parse_src("MATCH (a) RETURN a, b.name ORDER BY a, b.name").unwrap();
2528        assert_eq!(
2529            q.order_by,
2530            vec![
2531                OrderItem {
2532                    target: OrderTarget::Var("a".into()),
2533                    descending: false,
2534                },
2535                OrderItem {
2536                    target: OrderTarget::Prop {
2537                        var: "b".into(),
2538                        field: "name".into(),
2539                    },
2540                    descending: false,
2541                },
2542            ]
2543        );
2544    }
2545
2546    #[test]
2547    fn parse_never_panics_on_token_sequences() {
2548        let sequences: Vec<Vec<Tok>> = vec![
2549            vec![],
2550            vec![Tok::Match],
2551            vec![Tok::Return],
2552            vec![Tok::Dash, Tok::Dash, Tok::Dash],
2553            vec![Tok::Lt, Tok::Gt, Tok::Eq],
2554            vec![Tok::LParen, Tok::RParen, Tok::RParen],
2555            vec![Tok::Int(1), Tok::Float(2.0), Tok::Str("x".into())],
2556            vec![Tok::Where, Tok::Not, Tok::And, Tok::Or],
2557            vec![Tok::Order, Tok::By, Tok::Asc, Tok::Desc],
2558            vec![Tok::Skip, Tok::Limit, Tok::As],
2559            vec![Tok::Ident("n".into()), Tok::Dot, Tok::Ident("x".into())],
2560            vec![Tok::Param("p".into()), Tok::Colon, Tok::Comma],
2561            vec![Tok::LBracket, Tok::RBracket, Tok::LBrace, Tok::RBrace],
2562            lex("MATCH (a)-[x:]->(b) RETURN a ORDER BY a LIMIT 1 extra").unwrap(),
2563            // Aggregate tokens: COUNT(*), SUM/AVG/MIN/MAX in various positions.
2564            vec![
2565                Tok::Ident("COUNT".into()),
2566                Tok::LParen,
2567                Tok::Star,
2568                Tok::RParen,
2569            ],
2570            vec![
2571                Tok::Ident("sum".into()),
2572                Tok::LParen,
2573                Tok::Ident("n".into()),
2574                Tok::Dot,
2575                Tok::Ident("x".into()),
2576                Tok::RParen,
2577            ],
2578            vec![Tok::Star],
2579            vec![Tok::Star, Tok::LParen, Tok::RParen, Tok::Star],
2580            vec![
2581                Tok::Ident("avg".into()),
2582                Tok::LParen,
2583                Tok::Star,
2584                Tok::RParen,
2585            ],
2586            vec![Tok::Ident("min".into()), Tok::LParen, Tok::RParen],
2587            vec![
2588                Tok::Ident("max".into()),
2589                Tok::LParen,
2590                Tok::Star,
2591                Tok::RParen,
2592            ],
2593        ];
2594        for toks in sequences {
2595            let result = std::panic::catch_unwind(|| parse(&toks));
2596            assert!(result.is_ok(), "parse panicked on token sequence {toks:?}");
2597            let parsed = result.unwrap();
2598            if let Err(err) = parsed {
2599                assert!(
2600                    err.contains("token"),
2601                    "error must include token position, got: {err}"
2602                );
2603            }
2604        }
2605    }
2606
2607    #[test]
2608    fn aggregate_functions_parse_to_agg_retval() {
2609        use crate::cypher::ast::{AggArg, AggFunc, RetVal};
2610
2611        // COUNT(*) → AggFunc::Count, AggArg::Star
2612        let q = parse_src("MATCH (n) RETURN COUNT(*)").unwrap();
2613        assert_eq!(q.returns.len(), 1);
2614        assert_eq!(
2615            q.returns[0].value,
2616            RetVal::Agg {
2617                func: AggFunc::Count,
2618                arg: AggArg::Star,
2619            }
2620        );
2621        assert_eq!(q.returns[0].alias, None);
2622
2623        // COUNT(n) → AggFunc::Count, AggArg::Var
2624        let q = parse_src("MATCH (n) RETURN COUNT(n)").unwrap();
2625        assert_eq!(
2626            q.returns[0].value,
2627            RetVal::Agg {
2628                func: AggFunc::Count,
2629                arg: AggArg::Var("n".into()),
2630            }
2631        );
2632
2633        // SUM(n.age) → AggFunc::Sum, AggArg::Prop
2634        let q = parse_src("MATCH (n) RETURN SUM(n.age) AS total").unwrap();
2635        assert_eq!(
2636            q.returns[0].value,
2637            RetVal::Agg {
2638                func: AggFunc::Sum,
2639                arg: AggArg::Prop {
2640                    var: "n".into(),
2641                    field: "age".into()
2642                },
2643            }
2644        );
2645        assert_eq!(q.returns[0].alias, Some("total".into()));
2646
2647        // AVG, MIN, MAX case-insensitive
2648        let q = parse_src("MATCH (n) RETURN avg(n.score)").unwrap();
2649        assert!(matches!(
2650            q.returns[0].value,
2651            RetVal::Agg {
2652                func: AggFunc::Avg,
2653                ..
2654            }
2655        ));
2656        let q = parse_src("MATCH (n) RETURN Min(n.x)").unwrap();
2657        assert!(matches!(
2658            q.returns[0].value,
2659            RetVal::Agg {
2660                func: AggFunc::Min,
2661                ..
2662            }
2663        ));
2664        let q = parse_src("MATCH (n) RETURN MAX(n.x)").unwrap();
2665        assert!(matches!(
2666            q.returns[0].value,
2667            RetVal::Agg {
2668                func: AggFunc::Max,
2669                ..
2670            }
2671        ));
2672    }
2673
2674    #[test]
2675    fn nested_parens_beyond_limit_is_err_not_panic() {
2676        let mut src = String::from("MATCH (a) WHERE ");
2677        for _ in 0..80 {
2678            src.push('(');
2679        }
2680        src.push_str("a.x = 1");
2681        for _ in 0..80 {
2682            src.push(')');
2683        }
2684        src.push_str(" RETURN a");
2685        assert_parse_err(&src);
2686    }
2687
2688    // ── Variable-length path parser tests ─────────────────────────────────────
2689
2690    fn hop_range_of(src: &str) -> HopRange {
2691        let q = parse_src(src).expect(src);
2692        let (rel, _) = &q.matches[0].chain[0];
2693        rel.hops.expect("expected hop range")
2694    }
2695
2696    fn assert_hop_err(src: &str, needle: &str) {
2697        let result = std::panic::catch_unwind(|| parse_src(src));
2698        assert!(result.is_ok(), "parse panicked on {src:?}");
2699        let err = result
2700            .unwrap()
2701            .expect_err(&format!("parse({src:?}) must Err"));
2702        assert!(
2703            err.contains(needle),
2704            "error must contain {needle:?}, got: {err}"
2705        );
2706    }
2707
2708    #[test]
2709    fn var_length_bare_star_is_one_to_ten() {
2710        let r = hop_range_of("MATCH (a)-[r:T*]->(b) RETURN a");
2711        assert_eq!(r, HopRange { min: 1, max: 10 });
2712    }
2713
2714    #[test]
2715    fn var_length_exact_n_hops() {
2716        let r = hop_range_of("MATCH (a)-[r:T*3]->(b) RETURN a");
2717        assert_eq!(r, HopRange { min: 3, max: 3 });
2718    }
2719
2720    #[test]
2721    fn var_length_min_max_range() {
2722        let r = hop_range_of("MATCH (a)-[r:T*2..5]->(b) RETURN a");
2723        assert_eq!(r, HopRange { min: 2, max: 5 });
2724    }
2725
2726    #[test]
2727    fn var_length_dotdot_max() {
2728        let r = hop_range_of("MATCH (a)-[r:T*..4]->(b) RETURN a");
2729        assert_eq!(r, HopRange { min: 1, max: 4 });
2730    }
2731
2732    #[test]
2733    fn var_length_cap_at_ten_is_ok() {
2734        let r = hop_range_of("MATCH (a)-[r:T*10]->(b) RETURN a");
2735        assert_eq!(r, HopRange { min: 10, max: 10 });
2736        let r2 = hop_range_of("MATCH (a)-[r:T*1..10]->(b) RETURN a");
2737        assert_eq!(r2, HopRange { min: 1, max: 10 });
2738    }
2739
2740    #[test]
2741    fn var_length_cap_exceeded_is_err() {
2742        assert_hop_err(
2743            "MATCH (a)-[r:T*11]->(b) RETURN a",
2744            "variable-length paths are capped at 10 hops",
2745        );
2746        assert_hop_err(
2747            "MATCH (a)-[r:T*1..11]->(b) RETURN a",
2748            "variable-length paths are capped at 10 hops",
2749        );
2750    }
2751
2752    #[test]
2753    fn var_length_unbounded_min_dot_dot_is_err() {
2754        assert_hop_err(
2755            "MATCH (a)-[r:T*2..]->(b) RETURN a",
2756            "variable-length paths are capped at 10 hops",
2757        );
2758    }
2759
2760    #[test]
2761    fn var_length_shortest_path_parses() {
2762        let q =
2763            parse_src("MATCH (a:N) MATCH (b:N) MATCH shortestPath((a)-[r:T*..5]->(b)) RETURN a")
2764                .expect("shortestPath must parse");
2765        assert!(q.matches[2].shortest, "third match must be shortest=true");
2766        let (rel, _) = &q.matches[2].chain[0];
2767        assert_eq!(rel.hops, Some(HopRange { min: 1, max: 5 }));
2768        assert_eq!(rel.etypes, vec!["T".to_string()]);
2769    }
2770
2771    #[test]
2772    fn var_length_no_type_is_ok() {
2773        // Bare `*` with no type filter
2774        let r = hop_range_of("MATCH (a)-[r*1..3]->(b) RETURN a");
2775        assert_eq!(r, HopRange { min: 1, max: 3 });
2776    }
2777
2778    #[test]
2779    fn var_length_rel_appears_in_chain() {
2780        let q = parse_src("MATCH (a)-[r:T*2..4]->(b) RETURN a").unwrap();
2781        let (rel, dest) = &q.matches[0].chain[0];
2782        assert_eq!(rel.var.as_deref(), Some("r"));
2783        assert_eq!(rel.etypes, vec!["T".to_string()]);
2784        assert_eq!(rel.dir, RelDir::Right);
2785        assert_eq!(rel.hops, Some(HopRange { min: 2, max: 4 }));
2786        assert_eq!(dest.var.as_deref(), Some("b"));
2787    }
2788
2789    #[test]
2790    fn var_length_zero_hop_minimum_is_err() {
2791        // `*0` — exact form with min=0
2792        assert_hop_err(
2793            "MATCH (a)-[r:T*0]->(b) RETURN a",
2794            "zero-length variable-length paths are not supported",
2795        );
2796        // `*0..3` — range form with min=0
2797        assert_hop_err(
2798            "MATCH (a)-[r:T*0..3]->(b) RETURN a",
2799            "zero-length variable-length paths are not supported",
2800        );
2801    }
2802
2803    #[test]
2804    fn create_accepts_list_literal_property() {
2805        use super::parse_write;
2806        use crate::cypher::ast::WriteStatement;
2807        let src = "CREATE (n:Person {id: 'p1', tags: ['a', 'b']})";
2808        let stmt = parse_write(&lex(src).unwrap())
2809            .expect("CREATE with a list-literal property must parse");
2810        let WriteStatement::Create(c) = stmt else {
2811            panic!("expected a Create statement");
2812        };
2813        let tags = &c.nodes[0]
2814            .props
2815            .iter()
2816            .find(|(k, _)| k == "tags")
2817            .expect("tags property present")
2818            .1;
2819        assert_eq!(
2820            *tags,
2821            Value::List(vec![Value::Str("a".into()), Value::Str("b".into())])
2822        );
2823    }
2824
2825    #[test]
2826    fn create_accepts_empty_and_nested_list_literals() {
2827        use super::parse_write;
2828        use crate::cypher::ast::WriteStatement;
2829        let src = "CREATE (n:L {id: 'p1', empty: [], nested: [[1, 2], [3]]})";
2830        let stmt = parse_write(&lex(src).unwrap()).expect("empty and nested lists must parse");
2831        let WriteStatement::Create(c) = stmt else {
2832            panic!("expected a Create statement");
2833        };
2834        let get = |k: &str| {
2835            c.nodes[0]
2836                .props
2837                .iter()
2838                .find(|(name, _)| name == k)
2839                .expect("property present")
2840                .1
2841                .clone()
2842        };
2843        assert_eq!(get("empty"), Value::List(vec![]));
2844        assert_eq!(
2845            get("nested"),
2846            Value::List(vec![
2847                Value::List(vec![Value::Int(1), Value::Int(2)]),
2848                Value::List(vec![Value::Int(3)]),
2849            ])
2850        );
2851    }
2852
2853    #[test]
2854    fn set_accepts_list_literal_rhs() {
2855        use super::parse_write;
2856        use crate::cypher::ast::{Operand, WriteStatement};
2857        let src = "MATCH (n:Person {id: 'p1'}) SET n.tags = ['x', 'y']";
2858        let stmt = parse_write(&lex(src).unwrap()).expect("SET with a list-literal RHS must parse");
2859        let WriteStatement::MatchSet(m) = stmt else {
2860            panic!("expected a MatchSet statement");
2861        };
2862        let set = &m.sets[0];
2863        assert_eq!(set.field, "tags");
2864        assert_eq!(
2865            set.value,
2866            Operand::Lit(Value::List(vec![
2867                Value::Str("x".into()),
2868                Value::Str("y".into())
2869            ]))
2870        );
2871    }
2872}