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