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