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