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;
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.push(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.push(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<Pattern, String> {
377 self.expect(&Tok::Match, "expected MATCH")?;
378 if let Some(Tok::Ident(s)) = self.peek() {
380 if s.eq_ignore_ascii_case("shortestpath") {
381 return self.shortest_path_clause();
382 }
383 }
384 self.pattern()
385 }
386
387 fn shortest_path_clause(&mut self) -> Result<Pattern, String> {
388 self.pos += 1; self.expect(&Tok::LParen, "expected '(' after shortestPath")?;
390 let start = self.node()?;
391 let rel = self.rel()?;
392 if rel.hops.is_none() {
393 return Err(
394 self.err("shortestPath requires a variable-length relationship (e.g. [*..5])")
395 );
396 }
397 let dest = self.node()?;
398 self.expect(&Tok::RParen, "expected ')' to close shortestPath")?;
399 Ok(Pattern {
400 start,
401 chain: vec![(rel, dest)],
402 shortest: true,
403 })
404 }
405
406 fn pattern(&mut self) -> Result<Pattern, String> {
407 let start = self.node()?;
408 let mut chain = Vec::new();
409 while matches!(self.peek(), Some(Tok::Dash) | Some(Tok::Lt)) {
410 let rel = self.rel()?;
411 let dest = self.node()?;
412 chain.push((rel, dest));
413 }
414 Ok(Pattern {
415 start,
416 chain,
417 shortest: false,
418 })
419 }
420
421 fn node(&mut self) -> Result<NodePat, String> {
422 self.expect(&Tok::LParen, "expected '(' to start a node pattern")?;
423 let var = match self.peek() {
424 Some(Tok::Ident(s)) => {
425 let s = s.clone();
426 self.pos += 1;
427 Some(s)
428 }
429 _ => None,
430 };
431 let label = if self.eat(&Tok::Colon) {
432 Some(self.ident("expected label identifier after ':'")?)
433 } else {
434 None
435 };
436 let props = if self.peek() == Some(&Tok::LBrace) {
437 self.props()?
438 } else {
439 Vec::new()
440 };
441 self.expect(&Tok::RParen, "expected ')' to close a node pattern")?;
442 Ok(NodePat { var, label, props })
443 }
444
445 fn props(&mut self) -> Result<Vec<(String, Operand)>, String> {
446 self.expect(&Tok::LBrace, "expected '{'")?;
447 let mut out = Vec::new();
448 loop {
449 let key = self.ident("expected property key")?;
450 self.expect(&Tok::Colon, "expected ':' after property key")?;
451 let val = self.operand()?;
452 out.push((key, val));
453 if self.eat(&Tok::Comma) {
454 continue;
455 }
456 break;
457 }
458 self.expect(&Tok::RBrace, "expected '}' to close property map")?;
459 Ok(out)
460 }
461
462 fn rel(&mut self) -> Result<RelPat, String> {
463 if self.eat(&Tok::Lt) {
464 self.expect(
465 &Tok::Dash,
466 "expected '-' after '<' in a left-directed relationship",
467 )?;
468 let (var, etypes, hops) = self.rel_body()?;
469 self.expect(
470 &Tok::Dash,
471 "expected '-' to close a left-directed relationship",
472 )?;
473 return Ok(RelPat {
474 var,
475 etypes,
476 dir: RelDir::Left,
477 hops,
478 });
479 }
480 self.expect(&Tok::Dash, "expected '-' to start a relationship")?;
481 let (var, etypes, hops) = self.rel_body()?;
482 self.expect(&Tok::Dash, "expected '-' after ']'")?;
483 let dir = if self.eat(&Tok::Gt) {
484 RelDir::Right
485 } else {
486 RelDir::Undirected
487 };
488 Ok(RelPat {
489 var,
490 etypes,
491 dir,
492 hops,
493 })
494 }
495
496 #[allow(clippy::type_complexity)]
497 fn rel_body(&mut self) -> Result<(Option<String>, Vec<String>, Option<HopRange>), String> {
498 self.expect(&Tok::LBracket, "expected '[' in a relationship pattern")?;
499 let var = match self.peek() {
500 Some(Tok::Ident(s)) => {
501 let s = s.clone();
502 self.pos += 1;
503 Some(s)
504 }
505 _ => None,
506 };
507 let mut etypes = Vec::new();
509 if self.eat(&Tok::Colon) {
510 etypes.push(self.ident("expected relationship type identifier after ':'")?);
511 while self.eat(&Tok::Pipe) {
512 self.eat(&Tok::Colon);
514 etypes.push(self.ident("expected relationship type identifier after '|'")?);
515 }
516 }
517 let hops = if self.eat(&Tok::Star) {
518 Some(self.parse_hop_range()?)
519 } else {
520 None
521 };
522 self.expect(
523 &Tok::RBracket,
524 "expected ']' to close a relationship pattern",
525 )?;
526 Ok((var, etypes, hops))
527 }
528
529 fn parse_hop_range(&mut self) -> Result<HopRange, String> {
540 const CAP_ERR: &str = "variable-length paths are capped at 10 hops";
541
542 match self.peek() {
543 Some(Tok::RBracket) => Ok(HopRange { min: 1, max: 10 }),
545
546 Some(Tok::Int(n)) => {
548 let n = *n;
549 self.pos += 1;
550 if self.eat(&Tok::Dot) {
551 self.expect(&Tok::Dot, "expected '..' separator in hop range")?;
552 match self.peek() {
553 Some(Tok::Int(m)) => {
554 let m = *m;
555 self.pos += 1;
556 self.validate_hop_range(n, m, CAP_ERR)
558 }
559 _ => {
560 Err(CAP_ERR.to_string())
562 }
563 }
564 } else {
565 self.validate_hop_range(n, n, CAP_ERR)
567 }
568 }
569
570 Some(Tok::Dot) => {
572 self.pos += 1; self.expect(&Tok::Dot, "expected '..' range separator after '*'")?;
574 match self.peek() {
575 Some(Tok::Int(m)) => {
576 let m = *m;
577 self.pos += 1;
578 self.validate_hop_range(1, m, CAP_ERR)
579 }
580 _ => Err(self.err("expected max-hop integer after '*..'")),
581 }
582 }
583
584 _ => Ok(HopRange { min: 1, max: 10 }),
586 }
587 }
588
589 fn validate_hop_range(
590 &self,
591 min_n: i64,
592 max_n: i64,
593 cap_err: &str,
594 ) -> Result<HopRange, String> {
595 if min_n < 0 || max_n < 0 {
596 return Err(self.err("hop counts must be non-negative"));
597 }
598 if min_n == 0 {
599 return Err(self.err(
600 "zero-length variable-length paths are not supported; minimum hop count is 1",
601 ));
602 }
603 if max_n > 10 {
604 return Err(cap_err.to_string());
605 }
606 let min = min_n as u8;
607 let max = max_n as u8;
608 if min > max {
609 return Err(self.err(&format!(
610 "variable-length path min ({min}) must not exceed max ({max})"
611 )));
612 }
613 Ok(HopRange { min, max })
614 }
615
616 fn expr(&mut self, paren_depth: usize) -> Result<Expr, String> {
617 let mut left = self.term(paren_depth)?;
618 while self.eat(&Tok::Or) {
619 let right = self.term(paren_depth)?;
620 left = Expr::Or(Box::new(left), Box::new(right));
621 }
622 Ok(left)
623 }
624
625 fn term(&mut self, paren_depth: usize) -> Result<Expr, String> {
626 let mut left = self.factor(paren_depth)?;
627 while self.eat(&Tok::And) {
628 let right = self.factor(paren_depth)?;
629 left = Expr::And(Box::new(left), Box::new(right));
630 }
631 Ok(left)
632 }
633
634 fn factor(&mut self, paren_depth: usize) -> Result<Expr, String> {
635 let negated = self.eat(&Tok::Not);
636 let inner = if self.eat(&Tok::LParen) {
637 if paren_depth >= MAX_PAREN_DEPTH {
638 return Err(self.err("expression nesting too deep"));
639 }
640 let e = self.expr(paren_depth + 1)?;
641 self.expect(
642 &Tok::RParen,
643 "expected ')' to close parenthesized expression",
644 )?;
645 e
646 } else {
647 self.cmp()?
648 };
649 if negated {
650 Ok(Expr::Not(Box::new(inner)))
651 } else {
652 Ok(inner)
653 }
654 }
655
656 fn cmp(&mut self) -> Result<Expr, String> {
657 let lhs = self.arith_expr()?;
658 if let Some(Tok::Ident(s)) = self.peek() {
660 if s.eq_ignore_ascii_case("is") {
661 self.pos += 1; let negated = self.eat(&Tok::Not);
664 match self.peek() {
666 Some(Tok::Ident(n)) if n.eq_ignore_ascii_case("null") => {
667 self.pos += 1; return Ok(if negated {
669 Expr::IsNotNull(lhs)
670 } else {
671 Expr::IsNull(lhs)
672 });
673 }
674 _ => {
675 return Err(self.err(if negated {
676 "expected NULL after IS NOT"
677 } else {
678 "expected NULL after IS"
679 }));
680 }
681 }
682 }
683 }
684 if self.eat_ident_kw("in") {
686 let list = self.in_list()?;
687 return Ok(Expr::In { expr: lhs, list });
688 }
689 match self.cmp_op() {
694 Ok(op) => {
695 let rhs = self.arith_expr()?;
696 Ok(Expr::Cmp { lhs, op, rhs })
697 }
698 Err(_) => Ok(Expr::Truthy(lhs)),
699 }
700 }
701
702 fn in_list(&mut self) -> Result<Vec<Operand>, String> {
704 if self.eat(&Tok::LBracket) {
705 let mut items = Vec::new();
706 if !self.eat(&Tok::RBracket) {
707 loop {
708 items.push(self.arith_expr()?);
709 if self.eat(&Tok::Comma) {
710 continue;
711 }
712 self.expect(&Tok::RBracket, "expected ']' to close IN list")?;
713 break;
714 }
715 }
716 Ok(items)
717 } else {
718 Ok(vec![self.arith_expr()?])
719 }
720 }
721
722 fn arith_expr(&mut self) -> Result<Operand, String> {
733 let mut left = self.arith_mul()?;
734 loop {
735 let op = if self.eat(&Tok::Plus) {
736 ArithOp::Add
737 } else if self.eat(&Tok::Dash) {
738 ArithOp::Sub
743 } else {
744 break;
745 };
746 let right = self.arith_mul()?;
747 left = Operand::BinArith {
748 op,
749 left: Box::new(left),
750 right: Box::new(right),
751 };
752 }
753 Ok(left)
754 }
755
756 fn arith_mul(&mut self) -> Result<Operand, String> {
758 let mut left = self.arith_unary()?;
759 loop {
760 let op = if self.eat(&Tok::Star) {
761 ArithOp::Mul
762 } else if self.eat(&Tok::Slash) {
763 ArithOp::Div
764 } else {
765 break;
766 };
767 let right = self.arith_unary()?;
768 left = Operand::BinArith {
769 op,
770 left: Box::new(left),
771 right: Box::new(right),
772 };
773 }
774 Ok(left)
775 }
776
777 fn arith_unary(&mut self) -> Result<Operand, String> {
779 if self.eat(&Tok::Dash) {
780 return match self.peek() {
782 Some(Tok::Int(n)) => {
783 let n = *n;
784 self.pos += 1;
785 let neg = n
786 .checked_neg()
787 .ok_or_else(|| self.err("integer negation overflow"))?;
788 Ok(Operand::Lit(Value::Int(neg)))
789 }
790 Some(Tok::Float(x)) => {
791 let x = *x;
792 self.pos += 1;
793 Ok(Operand::Lit(Value::Float(-x)))
794 }
795 _ => Err(self.err("unary minus only applies to numeric literals")),
796 };
797 }
798 self.arith_atom()
799 }
800
801 fn arith_atom(&mut self) -> Result<Operand, String> {
803 if self.eat(&Tok::LParen) {
805 let inner = self.arith_expr()?;
806 self.expect(&Tok::RParen, "expected ')' to close arithmetic expression")?;
807 return Ok(inner);
808 }
809 self.operand_atom()
811 }
812
813 fn cmp_op(&mut self) -> Result<CmpOp, String> {
814 let op = match self.peek() {
815 Some(Tok::Eq) => CmpOp::Eq,
816 Some(Tok::Ne) => CmpOp::Ne,
817 Some(Tok::Lt) => CmpOp::Lt,
818 Some(Tok::Le) => CmpOp::Le,
819 Some(Tok::Gt) => CmpOp::Gt,
820 Some(Tok::Ge) => CmpOp::Ge,
821 _ => return Err(self.err("expected comparison operator")),
822 };
823 self.pos += 1;
824 Ok(op)
825 }
826
827 fn operand_atom(&mut self) -> Result<Operand, String> {
831 match self.peek() {
832 Some(Tok::Int(n)) => {
833 let n = *n;
834 self.pos += 1;
835 Ok(Operand::Lit(Value::Int(n)))
836 }
837 Some(Tok::Float(x)) => {
838 let x = *x;
839 self.pos += 1;
840 Ok(Operand::Lit(Value::Float(x)))
841 }
842 Some(Tok::Str(s)) => {
843 let s = s.clone();
844 self.pos += 1;
845 Ok(Operand::Lit(Value::Str(s)))
846 }
847 Some(Tok::Param(s)) => {
848 let s = s.clone();
849 self.pos += 1;
850 Ok(Operand::Param(s))
851 }
852 Some(Tok::Ident(_)) => {
853 let name = self.ident("expected identifier")?;
854 if name.eq_ignore_ascii_case("case") {
855 return self.parse_case();
856 }
857 if name.eq_ignore_ascii_case("collect") && self.peek() == Some(&Tok::LParen) {
858 return Err(
861 "collect() is only supported as a top-level RETURN/WITH aggregate"
862 .to_string(),
863 );
864 }
865 if self.peek() == Some(&Tok::LParen) {
866 self.pos += 1; let mut args = Vec::new();
870 if self.peek() != Some(&Tok::RParen) {
871 args.push(self.arith_expr()?);
872 while self.eat(&Tok::Comma) {
873 args.push(self.arith_expr()?);
874 }
875 }
876 self.expect(&Tok::RParen, "expected ')' to close function call")?;
877 Ok(Operand::FuncCall { name, args })
878 } else if self.eat(&Tok::Dot) {
879 let field = self.ident("expected field name after '.'")?;
880 Ok(Operand::Prop { var: name, field })
881 } else {
882 Ok(Operand::Var(name))
884 }
885 }
886 _ => Err(self.err("expected operand (property, literal, or parameter)")),
887 }
888 }
889
890 fn parse_case(&mut self) -> Result<Operand, String> {
894 let is_kw = |tok: Option<&Tok>, kw: &str| matches!(tok, Some(Tok::Ident(s)) if s.eq_ignore_ascii_case(kw));
895 let mut branches = Vec::new();
896 while is_kw(self.peek(), "when") {
897 self.pos += 1; let cond = self.expr(0)?;
899 if !is_kw(self.peek(), "then") {
900 return Err(self.err("expected THEN in a CASE branch"));
901 }
902 self.pos += 1; let value = self.arith_expr()?;
904 branches.push((cond, value));
905 }
906 if branches.is_empty() {
907 return Err(self.err("CASE requires at least one WHEN ... THEN branch"));
908 }
909 let default = if is_kw(self.peek(), "else") {
910 self.pos += 1; Some(Box::new(self.arith_expr()?))
912 } else {
913 None
914 };
915 if !is_kw(self.peek(), "end") {
916 return Err(self.err("expected END to close CASE"));
917 }
918 self.pos += 1; Ok(Operand::Case { branches, default })
920 }
921
922 fn operand(&mut self) -> Result<Operand, String> {
928 self.arith_expr()
932 }
933
934 fn return_clause(&mut self) -> Result<(bool, Vec<RetItem>), String> {
935 if !self.eat(&Tok::Return) {
936 return Err(self.unsupported_or_unexpected("expected RETURN"));
937 }
938 let distinct = self.eat_ident_kw("distinct");
939 let mut items = vec![self.ret_item()?];
940 while self.eat(&Tok::Comma) {
941 items.push(self.ret_item()?);
942 }
943 Ok((distinct, items))
944 }
945
946 fn return_items(&mut self) -> Result<Vec<RetItem>, String> {
947 let mut items = vec![self.ret_item()?];
948 while self.eat(&Tok::Comma) {
949 items.push(self.ret_item()?);
950 }
951 Ok(items)
952 }
953
954 fn ret_item(&mut self) -> Result<RetItem, String> {
955 if let Some(Tok::Ident(s)) = self.peek() {
959 let func = match s.to_ascii_lowercase().as_str() {
960 "count" => Some(AggFunc::Count),
961 "sum" => Some(AggFunc::Sum),
962 "avg" => Some(AggFunc::Avg),
963 "min" => Some(AggFunc::Min),
964 "max" => Some(AggFunc::Max),
965 "collect" => Some(AggFunc::Collect),
966 _ => None,
967 };
968 if let Some(func) = func {
969 self.pos += 1; self.expect(&Tok::LParen, "expected '(' after aggregate function name")?;
971 let arg = if self.eat(&Tok::Star) {
972 AggArg::Star
973 } else {
974 let var = self.ident("expected variable or '*' in aggregate argument")?;
975 if self.eat(&Tok::Dot) {
976 let field =
977 self.ident("expected field name after '.' in aggregate argument")?;
978 AggArg::Prop { var, field }
979 } else {
980 AggArg::Var(var)
981 }
982 };
983 self.expect(&Tok::RParen, "expected ')' to close aggregate function")?;
984 let alias = if self.eat(&Tok::As) {
985 Some(self.ident("expected alias identifier after AS")?)
986 } else {
987 None
988 };
989 return Ok(RetItem {
990 value: RetVal::Agg { func, arg },
991 alias,
992 });
993 }
994 }
995
996 let op = self.arith_expr()?;
1005 let value = match op {
1006 Operand::Var(name) => RetVal::Var(name),
1007 Operand::Prop { var, field } => RetVal::Prop { var, field },
1008 Operand::FuncCall { name, args } => RetVal::FuncCall { name, args },
1009 other => RetVal::ScalarExpr(other),
1010 };
1011 let alias = if self.eat(&Tok::As) {
1012 Some(self.ident("expected alias identifier after AS")?)
1013 } else {
1014 None
1015 };
1016 Ok(RetItem { value, alias })
1017 }
1018
1019 fn order_clause(&mut self, aliases: &[&str]) -> Result<Vec<OrderItem>, String> {
1020 self.expect(&Tok::Order, "expected ORDER")?;
1021 self.expect(&Tok::By, "expected BY after ORDER")?;
1022 let mut items = vec![self.order_item(aliases)?];
1023 while self.eat(&Tok::Comma) {
1024 items.push(self.order_item(aliases)?);
1025 }
1026 Ok(items)
1027 }
1028
1029 fn order_item(&mut self, aliases: &[&str]) -> Result<OrderItem, String> {
1030 let name = self.ident("expected ORDER BY target")?;
1031 let target = if self.eat(&Tok::Dot) {
1032 let field = self.ident("expected field name after '.'")?;
1033 OrderTarget::Prop { var: name, field }
1034 } else if aliases.contains(&name.as_str()) {
1035 OrderTarget::Alias(name)
1036 } else {
1037 OrderTarget::Var(name)
1038 };
1039 let descending = if self.eat(&Tok::Desc) {
1040 true
1041 } else {
1042 let _ = self.eat(&Tok::Asc);
1043 false
1044 };
1045 Ok(OrderItem { target, descending })
1046 }
1047
1048 fn uint(&mut self, what: &str) -> Result<LimitSkip, String> {
1049 match self.peek() {
1050 Some(Tok::Int(n)) if *n >= 0 => {
1051 let n = *n as u64;
1052 self.pos += 1;
1053 Ok(LimitSkip::Exact(n))
1054 }
1055 Some(Tok::Int(_)) => Err(self.err(&format!("{what} must be a non-negative integer"))),
1056 Some(Tok::Param(_)) => {
1057 let name = match self.toks.get(self.pos) {
1058 Some(Tok::Param(s)) => s.clone(),
1059 _ => unreachable!(),
1060 };
1061 self.pos += 1;
1062 Ok(LimitSkip::Param(name))
1063 }
1064 _ => Err(self.err(&format!("expected integer or $parameter after {what}"))),
1065 }
1066 }
1067
1068 fn write_statement(&mut self) -> Result<WriteStatement, String> {
1071 match self.peek() {
1072 Some(Tok::Create) => self.create_stmt(),
1073 Some(Tok::Merge) => self.merge_stmt(),
1074 Some(Tok::Match) => self.match_write_stmt(),
1075 _ => Err(self
1076 .err("expected CREATE, MERGE, or MATCH … SET/DELETE (write statement required)")),
1077 }
1078 }
1079
1080 fn create_stmt(&mut self) -> Result<WriteStatement, String> {
1083 self.expect(&Tok::Create, "expected CREATE")?;
1084 let mut stmt = self.create_pattern()?;
1085 if self.eat(&Tok::Return) {
1087 stmt.returns = Some(self.return_items()?);
1088 }
1089 if self.pos < self.toks.len() {
1090 return Err(self.err("unexpected tokens after CREATE"));
1091 }
1092 Ok(WriteStatement::Create(stmt))
1093 }
1094
1095 fn create_pattern(&mut self) -> Result<CreateStmt, String> {
1096 let first = self.create_node(0)?;
1098 let first_var = first.var.clone().unwrap_or_else(|| format!("_cn{}", 0));
1099 let mut nodes: Vec<CreateNode> = vec![first];
1100 let mut edges: Vec<CreateEdge> = Vec::new();
1101
1102 while matches!(self.peek(), Some(Tok::Dash) | Some(Tok::Lt)) {
1104 let (etype, src_is_left) = self.create_rel()?;
1105 let idx = nodes.len();
1106 let next = self.create_node(idx)?;
1107 let next_var = next.var.clone().unwrap_or_else(|| format!("_cn{idx}"));
1108 let prev_var = nodes.last().unwrap().var.clone().unwrap_or_else(|| {
1109 if nodes.len() == 1 {
1110 first_var.clone()
1111 } else {
1112 format!("_cn{}", nodes.len() - 1)
1113 }
1114 });
1115 let (src_var, dst_var) = if src_is_left {
1116 (next_var.clone(), prev_var)
1118 } else {
1119 (prev_var, next_var.clone())
1121 };
1122 edges.push(CreateEdge {
1123 src_var,
1124 etype,
1125 dst_var,
1126 });
1127 nodes.push(next);
1128 }
1129 Ok(CreateStmt {
1130 nodes,
1131 edges,
1132 returns: None,
1133 })
1134 }
1135
1136 fn create_node(&mut self, idx: usize) -> Result<CreateNode, String> {
1137 self.expect(&Tok::LParen, "expected '(' in CREATE node pattern")?;
1138 let var = match self.peek() {
1139 Some(Tok::Ident(s)) => {
1140 let s = s.clone();
1141 self.pos += 1;
1142 Some(s)
1143 }
1144 _ => None,
1145 };
1146 if !self.eat(&Tok::Colon) {
1147 return Err(self.err("CREATE node requires a label (e.g., (n:Label {…}))"));
1148 }
1149 let label = self.ident("expected label identifier after ':'")?;
1150 let props = if self.peek() == Some(&Tok::LBrace) {
1151 self.literal_props()?
1152 } else {
1153 Vec::new()
1154 };
1155 self.expect(&Tok::RParen, "expected ')' to close CREATE node pattern")?;
1156 let var = Some(var.unwrap_or_else(|| format!("_cn{idx}")));
1157 Ok(CreateNode { var, label, props })
1158 }
1159
1160 fn literal_props(&mut self) -> Result<Vec<(String, Value)>, String> {
1162 self.expect(&Tok::LBrace, "expected '{'")?;
1163 let mut out = Vec::new();
1164 if self.eat(&Tok::RBrace) {
1165 return Ok(out);
1166 }
1167 loop {
1168 let key = self.ident("expected property key")?;
1169 self.expect(&Tok::Colon, "expected ':' after property key")?;
1170 let val = self.literal_value("property value")?;
1171 out.push((key, val));
1172 if self.eat(&Tok::Comma) {
1173 continue;
1174 }
1175 break;
1176 }
1177 self.expect(&Tok::RBrace, "expected '}' to close property map")?;
1178 Ok(out)
1179 }
1180
1181 fn literal_value(&mut self, what: &str) -> Result<Value, String> {
1184 if self.eat(&Tok::Dash) {
1185 return match self.peek() {
1186 Some(Tok::Int(n)) => {
1187 let n = *n;
1188 self.pos += 1;
1189 Ok(Value::Int(-n))
1190 }
1191 Some(Tok::Float(x)) => {
1192 let x = *x;
1193 self.pos += 1;
1194 Ok(Value::Float(-x))
1195 }
1196 _ => Err(self.err("unary minus only applies to numeric literals")),
1197 };
1198 }
1199 match self.peek() {
1200 Some(Tok::Int(n)) => {
1201 let n = *n;
1202 self.pos += 1;
1203 Ok(Value::Int(n))
1204 }
1205 Some(Tok::Float(x)) => {
1206 let x = *x;
1207 self.pos += 1;
1208 Ok(Value::Float(x))
1209 }
1210 Some(Tok::Str(s)) => {
1211 let s = s.clone();
1212 self.pos += 1;
1213 Ok(Value::Str(s))
1214 }
1215 Some(Tok::LBracket) => {
1219 self.pos += 1; let mut items = Vec::new();
1221 if !self.eat(&Tok::RBracket) {
1222 loop {
1223 items.push(self.literal_value(what)?);
1224 if self.eat(&Tok::Comma) {
1225 continue;
1226 }
1227 self.expect(&Tok::RBracket, "expected ']' to close list literal")?;
1228 break;
1229 }
1230 }
1231 Ok(Value::List(items))
1232 }
1233 Some(Tok::Param(_)) => Err(self.err(&format!(
1234 "parameter references are not supported in {what} (v1 limitation: use literals only)"
1235 ))),
1236 Some(Tok::Ident(_)) => Err(self.err(&format!(
1237 "expression RHS not supported in {what} (v1 limitation: use literals only)"
1238 ))),
1239 _ => Err(self.err(&format!("expected literal value for {what}"))),
1240 }
1241 }
1242
1243 fn create_rel(&mut self) -> Result<(String, bool), String> {
1246 if self.eat(&Tok::Lt) {
1247 self.expect(&Tok::Dash, "expected '-' after '<' in relationship")?;
1249 self.expect(&Tok::LBracket, "expected '[' in relationship pattern")?;
1250 self.expect(&Tok::Colon, "expected ':TYPE' in CREATE relationship")?;
1251 let etype = self.ident("expected relationship type")?;
1252 self.expect(&Tok::RBracket, "expected ']'")?;
1253 self.expect(&Tok::Dash, "expected '-'")?;
1254 return Ok((etype, true));
1255 }
1256 self.expect(&Tok::Dash, "expected '-' to start relationship")?;
1258 self.expect(&Tok::LBracket, "expected '[' in relationship pattern")?;
1259 self.expect(&Tok::Colon, "expected ':TYPE' in CREATE relationship")?;
1260 let etype = self.ident("expected relationship type")?;
1261 self.expect(&Tok::RBracket, "expected ']'")?;
1262 self.expect(&Tok::Dash, "expected '-'")?;
1263 self.expect(
1264 &Tok::Gt,
1265 "expected '>' — CREATE requires directed relationships",
1266 )?;
1267 Ok((etype, false))
1268 }
1269
1270 fn merge_stmt(&mut self) -> Result<WriteStatement, String> {
1273 self.expect(&Tok::Merge, "expected MERGE")?;
1274 self.expect(&Tok::LParen, "expected '(' after MERGE")?;
1275 let var = match self.peek() {
1277 Some(Tok::Ident(_)) => {
1278 let s = match self.toks.get(self.pos) {
1279 Some(Tok::Ident(s)) => s.clone(),
1280 _ => unreachable!(),
1281 };
1282 self.pos += 1; Some(s)
1284 }
1285 _ => None,
1286 };
1287 if !self.eat(&Tok::Colon) {
1288 return Err(self.err("MERGE requires a label (e.g., MERGE (n:Label {key: 'x'}))"));
1289 }
1290 let label = self.ident("expected label identifier after ':'")?;
1291 if self.peek() != Some(&Tok::LBrace) {
1292 return Err(self.err(
1293 "MERGE requires a property map with exactly one key (e.g., MERGE (n:Label {id: 'x'}))",
1294 ));
1295 }
1296 let mut props = self.literal_props()?;
1297 if props.len() != 1 {
1298 return Err(format!(
1299 "MERGE supports exactly one key property (got {}); use CREATE for multi-prop nodes",
1300 props.len()
1301 ));
1302 }
1303 self.expect(&Tok::RParen, "expected ')' to close MERGE pattern")?;
1304 let mut on_create = Vec::new();
1305 let mut on_match = Vec::new();
1306 while self.eat_ident_kw("on") {
1307 if self.eat(&Tok::Create) {
1308 self.expect(&Tok::Set, "expected SET after ON CREATE")?;
1309 on_create.extend(self.set_clauses()?);
1310 } else if self.eat(&Tok::Match) {
1311 self.expect(&Tok::Set, "expected SET after ON MATCH")?;
1312 on_match.extend(self.set_clauses()?);
1313 } else {
1314 return Err(self.err("expected CREATE or MATCH after ON"));
1315 }
1316 }
1317 let returns = if self.eat(&Tok::Return) {
1319 Some(self.return_items()?)
1320 } else {
1321 None
1322 };
1323 if self.pos < self.toks.len() {
1324 return Err(self.unsupported_or_unexpected("unexpected tokens after MERGE"));
1325 }
1326 let (key_field, key_value) = props.remove(0);
1327 Ok(WriteStatement::Merge(MergeStmt {
1328 label,
1329 key_field,
1330 key_value,
1331 var,
1332 on_create,
1333 on_match,
1334 returns,
1335 }))
1336 }
1337
1338 fn match_write_stmt(&mut self) -> Result<WriteStatement, String> {
1341 let mut matches = Vec::new();
1343 while self.peek() == Some(&Tok::Match) {
1344 matches.push(self.match_clause()?);
1345 }
1346 if matches.is_empty() {
1347 return Err(self.err("expected MATCH"));
1348 }
1349 let where_expr = if self.eat(&Tok::Where) {
1351 Some(self.expr(0)?)
1352 } else {
1353 None
1354 };
1355 match self.peek() {
1357 Some(Tok::Set) => {
1358 self.pos += 1; let sets = self.set_clauses()?;
1360 let returns = if self.eat(&Tok::Return) {
1361 Some(self.return_items()?)
1362 } else {
1363 None
1364 };
1365 if self.pos < self.toks.len() {
1366 return Err(self.unsupported_or_unexpected("unexpected tokens after SET"));
1367 }
1368 Ok(WriteStatement::MatchSet(MatchSetStmt {
1369 matches,
1370 where_expr,
1371 sets,
1372 returns,
1373 }))
1374 }
1375 Some(Tok::Detach) => {
1376 self.pos += 1; self.expect(&Tok::Delete, "expected DELETE after DETACH")?;
1379 let node_vars = self.node_delete_targets(&matches)?;
1380 if self.pos < self.toks.len() {
1381 return Err(self.err("unexpected tokens after DETACH DELETE"));
1382 }
1383 Ok(WriteStatement::MatchDeleteNode(MatchDeleteNodeStmt {
1384 matches,
1385 where_expr,
1386 node_vars,
1387 detach: true,
1388 }))
1389 }
1390 Some(Tok::Delete) => {
1391 self.pos += 1; match self.delete_targets_or_node(&matches)? {
1395 DeleteTargetResult::Edges(deletes) => {
1396 if self.pos < self.toks.len() {
1397 return Err(self.err("unexpected tokens after DELETE"));
1398 }
1399 Ok(WriteStatement::MatchDelete(MatchDeleteStmt {
1400 matches,
1401 where_expr,
1402 deletes,
1403 }))
1404 }
1405 DeleteTargetResult::Nodes(node_vars) => {
1406 if self.pos < self.toks.len() {
1407 return Err(self.err("unexpected tokens after DELETE"));
1408 }
1409 Ok(WriteStatement::MatchDeleteNode(MatchDeleteNodeStmt {
1410 matches,
1411 where_expr,
1412 node_vars,
1413 detach: false,
1414 }))
1415 }
1416 }
1417 }
1418 _ => Err(self.err(
1419 "expected SET or DELETE after MATCH [WHERE]; \
1420 combined MATCH…RETURN is a read query, not a write statement",
1421 )),
1422 }
1423 }
1424
1425 fn set_clauses(&mut self) -> Result<Vec<SetClause>, String> {
1426 let mut sets = vec![self.set_clause()?];
1427 while self.eat(&Tok::Comma) {
1428 sets.push(self.set_clause()?);
1429 }
1430 Ok(sets)
1431 }
1432
1433 fn set_clause(&mut self) -> Result<SetClause, String> {
1434 let var = self.ident("expected variable in SET clause")?;
1435 self.expect(&Tok::Dot, "expected '.' after variable in SET")?;
1436 let field = self.ident("expected field name after '.'")?;
1437 self.expect(&Tok::Eq, "expected '=' in SET clause")?;
1438 let value = match self.peek() {
1443 Some(
1444 Tok::Int(_)
1445 | Tok::Float(_)
1446 | Tok::Str(_)
1447 | Tok::Param(_)
1448 | Tok::Dash
1449 | Tok::Ident(_),
1450 ) => {
1451 let op = self.arith_expr()?;
1452 match &op {
1453 Operand::Lit(_)
1454 | Operand::Param(_)
1455 | Operand::BinArith { .. }
1456 | Operand::FuncCall { .. }
1457 | Operand::Case { .. } => op,
1458 Operand::Prop { .. } | Operand::Var(_) => {
1459 return Err(self.err(
1460 "SET RHS: bare property/variable reference is not supported; \
1461 use a literal, $parameter, or arithmetic expression (e.g. n.x + 1)",
1462 ));
1463 }
1464 }
1465 }
1466 Some(Tok::LBracket) => Operand::Lit(self.literal_value("SET value")?),
1469 _ => {
1470 return Err(
1471 self.err("expected literal, $parameter, or arithmetic expression as SET value")
1472 )
1473 }
1474 };
1475 Ok(SetClause { var, field, value })
1476 }
1477
1478 fn resolve_edge_var(&self, var: &str, patterns: &[Pattern]) -> Result<EdgeDelete, String> {
1481 for pat in patterns {
1482 let start_var = pat.start.var.as_deref().unwrap_or("_unknown");
1483 let mut from_var = start_var;
1484 for (rel, dest) in &pat.chain {
1485 let to_var = dest.var.as_deref().unwrap_or("_unknown");
1486 if rel.var.as_deref() == Some(var) {
1487 let etype = match rel.etypes.as_slice() {
1488 [t] => t.clone(),
1489 [] => {
1490 return Err(format!(
1491 "DELETE `{var}`: relationship has no type; \
1492 DELETE requires an explicit edge type (e.g., [r:TYPE])"
1493 ))
1494 }
1495 _ => {
1496 return Err(format!(
1497 "DELETE `{var}`: relationship has multiple types; \
1498 DELETE requires a single explicit edge type (e.g., [r:TYPE])"
1499 ))
1500 }
1501 };
1502 let (src_var, dst_var) = match rel.dir {
1503 RelDir::Right => (from_var.to_string(), to_var.to_string()),
1504 RelDir::Left => (to_var.to_string(), from_var.to_string()),
1505 RelDir::Undirected => {
1506 return Err(format!(
1507 "DELETE `{var}`: undirected relationship DELETE is not supported; \
1508 use a directed pattern (e.g., -[r:TYPE]->)"
1509 ))
1510 }
1511 };
1512 return Ok(EdgeDelete {
1513 rel_var: var.to_string(),
1514 etype,
1515 src_var,
1516 dst_var,
1517 });
1518 }
1519 from_var = to_var;
1520 }
1521 }
1522 Err(format!(
1523 "DELETE `{var}`: variable is not bound as a relationship in any MATCH pattern; \
1524 only relationship variables can be deleted (DELETE edge vars, not node vars)"
1525 ))
1526 }
1527
1528 fn is_node_var(&self, var: &str, patterns: &[Pattern]) -> bool {
1530 for pat in patterns {
1531 if pat.start.var.as_deref() == Some(var) {
1532 return true;
1533 }
1534 for (_, dest) in &pat.chain {
1535 if dest.var.as_deref() == Some(var) {
1536 return true;
1537 }
1538 }
1539 }
1540 false
1541 }
1542
1543 fn node_delete_targets(&mut self, patterns: &[Pattern]) -> Result<Vec<String>, String> {
1546 let mut vars = Vec::new();
1547 loop {
1548 let var = self.ident("expected node variable to DELETE")?;
1549 if !self.is_node_var(&var, patterns) {
1550 return Err(format!(
1551 "DELETE `{var}`: variable is not bound as a node in any MATCH pattern"
1552 ));
1553 }
1554 vars.push(var);
1555 if !self.eat(&Tok::Comma) {
1556 break;
1557 }
1558 }
1559 Ok(vars)
1560 }
1561
1562 fn delete_targets_or_node(
1565 &mut self,
1566 patterns: &[Pattern],
1567 ) -> Result<DeleteTargetResult, String> {
1568 let var = self.ident("expected variable to DELETE")?;
1570 match self.resolve_edge_var(&var, patterns) {
1572 Ok(edge_del) => {
1573 let mut targets = vec![edge_del];
1576 while self.eat(&Tok::Comma) {
1577 let v = self.ident("expected variable to DELETE")?;
1578 targets.push(self.resolve_edge_var(&v, patterns)?);
1579 }
1580 Ok(DeleteTargetResult::Edges(targets))
1581 }
1582 Err(_) => {
1583 if self.is_node_var(&var, patterns) {
1585 let mut node_vars = vec![var];
1586 while self.eat(&Tok::Comma) {
1587 let v = self.ident("expected variable to DELETE")?;
1588 if !self.is_node_var(&v, patterns) {
1589 return Err(format!(
1590 "DELETE `{v}`: variable is not bound as a node in any MATCH pattern"
1591 ));
1592 }
1593 node_vars.push(v);
1594 }
1595 Ok(DeleteTargetResult::Nodes(node_vars))
1596 } else {
1597 Err(format!(
1598 "DELETE `{var}`: variable is not bound as a relationship or node \
1599 in any MATCH pattern"
1600 ))
1601 }
1602 }
1603 }
1604 }
1605}
1606
1607enum DeleteTargetResult {
1610 Edges(Vec<EdgeDelete>),
1611 Nodes(Vec<String>),
1612}
1613
1614#[cfg(test)]
1615mod tests {
1616 use super::parse;
1617 use crate::cypher::ast::{
1618 AggFunc, Expr, HopRange, LimitSkip, NodePat, Operand, OrderItem, OrderTarget, Pattern,
1619 Query, RelDir, RelPat, RetItem, RetVal,
1620 };
1621 use crate::cypher::{lex, Tok};
1622 use crate::filter::CmpOp;
1623 use core_storage::Value;
1624
1625 fn parse_src(src: &str) -> Result<Query, String> {
1626 parse(&lex(src)?)
1627 }
1628
1629 fn prop(var: &str, field: &str) -> Operand {
1630 Operand::Prop {
1631 var: var.into(),
1632 field: field.into(),
1633 }
1634 }
1635
1636 fn cmp(lhs: Operand, op: CmpOp, rhs: Operand) -> Expr {
1637 Expr::Cmp { lhs, op, rhs }
1638 }
1639
1640 fn node(var: Option<&str>, label: Option<&str>, props: Vec<(String, Operand)>) -> NodePat {
1641 NodePat {
1642 var: var.map(str::to_string),
1643 label: label.map(str::to_string),
1644 props,
1645 }
1646 }
1647
1648 #[test]
1649 fn full_feature_query_exact_ast() {
1650 let src = "\
1651MATCH (a:Person {name: $n, age: 30})-[r:KNOWS]->(b)-[u:TEAM]-(c)<-[s:LIKES]-(d) \
1652WHERE NOT a.age < 18 AND b.name = 'x' OR c.score >= 2.5 \
1653RETURN a, r.since AS since, b.name \
1654ORDER BY since DESC, b.name ASC \
1655SKIP 1 LIMIT 5";
1656 let got = parse_src(src).expect("full-feature query must parse");
1657 let expected = Query {
1658 matches: vec![Pattern {
1659 start: node(
1660 Some("a"),
1661 Some("Person"),
1662 vec![
1663 ("name".into(), Operand::Param("n".into())),
1664 ("age".into(), Operand::Lit(Value::Int(30))),
1665 ],
1666 ),
1667 chain: vec![
1668 (
1669 RelPat {
1670 var: Some("r".into()),
1671 etypes: vec!["KNOWS".into()],
1672 dir: RelDir::Right,
1673 hops: None,
1674 },
1675 node(Some("b"), None, vec![]),
1676 ),
1677 (
1678 RelPat {
1679 var: Some("u".into()),
1680 etypes: vec!["TEAM".into()],
1681 dir: RelDir::Undirected,
1682 hops: None,
1683 },
1684 node(Some("c"), None, vec![]),
1685 ),
1686 (
1687 RelPat {
1688 var: Some("s".into()),
1689 etypes: vec!["LIKES".into()],
1690 dir: RelDir::Left,
1691 hops: None,
1692 },
1693 node(Some("d"), None, vec![]),
1694 ),
1695 ],
1696 shortest: false,
1697 }],
1698 optional_clauses: vec![],
1699 unwinds: vec![],
1700 post_unwind_where: None,
1701 where_expr: Some(Expr::Or(
1702 Box::new(Expr::And(
1703 Box::new(Expr::Not(Box::new(cmp(
1704 prop("a", "age"),
1705 CmpOp::Lt,
1706 Operand::Lit(Value::Int(18)),
1707 )))),
1708 Box::new(cmp(
1709 prop("b", "name"),
1710 CmpOp::Eq,
1711 Operand::Lit(Value::Str("x".into())),
1712 )),
1713 )),
1714 Box::new(cmp(
1715 prop("c", "score"),
1716 CmpOp::Ge,
1717 Operand::Lit(Value::Float(2.5)),
1718 )),
1719 )),
1720 stages: vec![],
1721 returns: vec![
1722 RetItem {
1723 value: RetVal::Var("a".into()),
1724 alias: None,
1725 },
1726 RetItem {
1727 value: RetVal::Prop {
1728 var: "r".into(),
1729 field: "since".into(),
1730 },
1731 alias: Some("since".into()),
1732 },
1733 RetItem {
1734 value: RetVal::Prop {
1735 var: "b".into(),
1736 field: "name".into(),
1737 },
1738 alias: None,
1739 },
1740 ],
1741 order_by: vec![
1742 OrderItem {
1743 target: OrderTarget::Alias("since".into()),
1744 descending: true,
1745 },
1746 OrderItem {
1747 target: OrderTarget::Prop {
1748 var: "b".into(),
1749 field: "name".into(),
1750 },
1751 descending: false,
1752 },
1753 ],
1754 distinct: false,
1755 skip: Some(LimitSkip::Exact(1)),
1756 limit: Some(LimitSkip::Exact(5)),
1757 };
1758 assert_eq!(got, expected);
1759 }
1760
1761 #[test]
1762 fn rel_direction_right() {
1763 let q = parse_src("MATCH (a)-[r:T]->(b) RETURN a").unwrap();
1764 assert_eq!(q.matches[0].chain[0].0.dir, RelDir::Right);
1765 assert_eq!(q.matches[0].chain[0].0.var.as_deref(), Some("r"));
1766 assert_eq!(q.matches[0].chain[0].0.etypes, vec!["T".to_string()]);
1767 }
1768
1769 #[test]
1770 fn rel_direction_left() {
1771 let q = parse_src("MATCH (a)<-[r:T]-(b) RETURN a").unwrap();
1772 assert_eq!(q.matches[0].chain[0].0.dir, RelDir::Left);
1773 }
1774
1775 #[test]
1776 fn rel_direction_undirected() {
1777 let q = parse_src("MATCH (a)-[r:T]-(b) RETURN a").unwrap();
1778 assert_eq!(q.matches[0].chain[0].0.dir, RelDir::Undirected);
1779 }
1780
1781 #[test]
1782 fn node_props_map_with_param() {
1783 let q = parse_src("MATCH (t:Talent {id: $tid, n: 1, s: 'x'}) RETURN t").unwrap();
1784 assert_eq!(
1785 q.matches[0].start.props,
1786 vec![
1787 ("id".into(), Operand::Param("tid".into())),
1788 ("n".into(), Operand::Lit(Value::Int(1))),
1789 ("s".into(), Operand::Lit(Value::Str("x".into()))),
1790 ]
1791 );
1792 assert_eq!(q.matches[0].start.var.as_deref(), Some("t"));
1793 assert_eq!(q.matches[0].start.label.as_deref(), Some("Talent"));
1794 }
1795
1796 #[test]
1797 fn operator_precedence_or_and_not() {
1798 let q = parse_src("MATCH (a) WHERE a.x = 1 OR b.y = 2 AND NOT c.z = 3 RETURN a").unwrap();
1799 let expected = Expr::Or(
1800 Box::new(cmp(prop("a", "x"), CmpOp::Eq, Operand::Lit(Value::Int(1)))),
1801 Box::new(Expr::And(
1802 Box::new(cmp(prop("b", "y"), CmpOp::Eq, Operand::Lit(Value::Int(2)))),
1803 Box::new(Expr::Not(Box::new(cmp(
1804 prop("c", "z"),
1805 CmpOp::Eq,
1806 Operand::Lit(Value::Int(3)),
1807 )))),
1808 )),
1809 );
1810 assert_eq!(q.where_expr, Some(expected));
1811 }
1812
1813 #[test]
1814 fn unary_minus_folds_numeric_literals() {
1815 let q = parse_src("MATCH (a) WHERE a.x > -5 AND a.y < -1.5 RETURN a").unwrap();
1816 let expected = Expr::And(
1817 Box::new(cmp(prop("a", "x"), CmpOp::Gt, Operand::Lit(Value::Int(-5)))),
1818 Box::new(cmp(
1819 prop("a", "y"),
1820 CmpOp::Lt,
1821 Operand::Lit(Value::Float(-1.5)),
1822 )),
1823 );
1824 assert_eq!(q.where_expr, Some(expected));
1825 }
1826
1827 fn assert_parse_err(src: &str) {
1828 let result = std::panic::catch_unwind(|| parse_src(src));
1829 assert!(result.is_ok(), "parse({src:?}) panicked");
1830 let err = result
1831 .unwrap()
1832 .expect_err(&format!("parse({src:?}) must be Err"));
1833 assert!(
1835 err.contains("token") || err.contains("position"),
1836 "error must include token or lex position, got: {err}"
1837 );
1838 }
1839
1840 #[test]
1841 fn malformed_match_alone_is_err() {
1842 assert_parse_err("MATCH");
1843 }
1844
1845 #[test]
1846 fn malformed_missing_return_is_err() {
1847 assert_parse_err("MATCH (n)");
1848 }
1849
1850 #[test]
1851 fn malformed_rel_colon_without_type_is_err() {
1852 assert_parse_err("MATCH (a)-[x:]->(b) RETURN a");
1853 }
1854
1855 #[test]
1856 fn malformed_dangling_comma_in_return_is_err() {
1857 assert_parse_err("MATCH (n) RETURN n,");
1858 }
1859
1860 #[test]
1861 fn malformed_unclosed_paren_is_err() {
1862 assert_parse_err("MATCH (n RETURN n");
1863 assert_parse_err("MATCH (n) WHERE (a.x = 1 RETURN n");
1864 }
1865
1866 #[test]
1867 fn malformed_order_by_before_return_is_err() {
1868 assert_parse_err("MATCH (n) ORDER BY n RETURN n");
1869 }
1870
1871 #[test]
1872 fn malformed_garbage_after_limit_is_err() {
1873 assert_parse_err("MATCH (n) RETURN n LIMIT 1 extra");
1874 }
1875
1876 #[test]
1877 fn dogfood_query_exact_ast() {
1878 let src = "\
1879MATCH (t:Talent {id: $tid}) \
1880MATCH (c:Company)-[i:INDUSTRY_ALIGNMENT]->(t) \
1881MATCH (c)-[s:SPECIALTY_MATCH]->(t) \
1882WHERE i.score >= 0.5 AND s.score >= 0.5 \
1883RETURN c, i.score AS industry, s.score AS specialty \
1884ORDER BY industry DESC, specialty DESC \
1885LIMIT 10";
1886 let got = parse_src(src).expect("dogfood query must parse");
1887 let expected = Query {
1888 matches: vec![
1889 Pattern {
1890 start: node(
1891 Some("t"),
1892 Some("Talent"),
1893 vec![("id".into(), Operand::Param("tid".into()))],
1894 ),
1895 chain: vec![],
1896 shortest: false,
1897 },
1898 Pattern {
1899 start: node(Some("c"), Some("Company"), vec![]),
1900 chain: vec![(
1901 RelPat {
1902 var: Some("i".into()),
1903 etypes: vec!["INDUSTRY_ALIGNMENT".into()],
1904 dir: RelDir::Right,
1905 hops: None,
1906 },
1907 node(Some("t"), None, vec![]),
1908 )],
1909 shortest: false,
1910 },
1911 Pattern {
1912 start: node(Some("c"), None, vec![]),
1913 chain: vec![(
1914 RelPat {
1915 var: Some("s".into()),
1916 etypes: vec!["SPECIALTY_MATCH".into()],
1917 dir: RelDir::Right,
1918 hops: None,
1919 },
1920 node(Some("t"), None, vec![]),
1921 )],
1922 shortest: false,
1923 },
1924 ],
1925 optional_clauses: vec![],
1926 unwinds: vec![],
1927 post_unwind_where: None,
1928 where_expr: Some(Expr::And(
1929 Box::new(cmp(
1930 prop("i", "score"),
1931 CmpOp::Ge,
1932 Operand::Lit(Value::Float(0.5)),
1933 )),
1934 Box::new(cmp(
1935 prop("s", "score"),
1936 CmpOp::Ge,
1937 Operand::Lit(Value::Float(0.5)),
1938 )),
1939 )),
1940 stages: vec![],
1941 returns: vec![
1942 RetItem {
1943 value: RetVal::Var("c".into()),
1944 alias: None,
1945 },
1946 RetItem {
1947 value: RetVal::Prop {
1948 var: "i".into(),
1949 field: "score".into(),
1950 },
1951 alias: Some("industry".into()),
1952 },
1953 RetItem {
1954 value: RetVal::Prop {
1955 var: "s".into(),
1956 field: "score".into(),
1957 },
1958 alias: Some("specialty".into()),
1959 },
1960 ],
1961 order_by: vec![
1962 OrderItem {
1963 target: OrderTarget::Alias("industry".into()),
1964 descending: true,
1965 },
1966 OrderItem {
1967 target: OrderTarget::Alias("specialty".into()),
1968 descending: true,
1969 },
1970 ],
1971 distinct: false,
1972 skip: None,
1973 limit: Some(LimitSkip::Exact(10)),
1974 };
1975 assert_eq!(got, expected);
1976 }
1977
1978 #[test]
1979 fn unary_minus_in_props_and_dash_elsewhere_is_err() {
1980 let q = parse_src("MATCH (a {x: -5, y: -1.5}) RETURN a").unwrap();
1981 assert_eq!(
1982 q.matches[0].start.props,
1983 vec![
1984 ("x".into(), Operand::Lit(Value::Int(-5))),
1985 ("y".into(), Operand::Lit(Value::Float(-1.5))),
1986 ]
1987 );
1988 let q2 = parse_src("MATCH (a) WHERE a.x = 1 - 2 RETURN a").unwrap();
1990 assert!(q2.where_expr.is_some());
1991 assert_parse_err("MATCH (a) WHERE a.x > -b.y RETURN a");
1993 assert_parse_err("MATCH (a) RETURN a SKIP -1");
1994 }
1995
1996 #[test]
1999 fn is_null_parses_on_prop() {
2000 let q = parse_src("MATCH (a) WHERE a.x IS NULL RETURN a").unwrap();
2001 assert_eq!(q.where_expr, Some(Expr::IsNull(prop("a", "x"))),);
2002 }
2003
2004 #[test]
2005 fn is_not_null_parses_on_prop() {
2006 let q = parse_src("MATCH (a) WHERE a.x IS NOT NULL RETURN a").unwrap();
2007 assert_eq!(q.where_expr, Some(Expr::IsNotNull(prop("a", "x"))),);
2008 }
2009
2010 #[test]
2011 fn is_null_on_var() {
2012 let q =
2013 parse_src("MATCH (a) OPTIONAL MATCH (a)-[:T]->(b) WITH a, b WHERE b IS NULL RETURN a")
2014 .unwrap();
2015 let stage = &q.stages[0];
2017 assert_eq!(
2018 stage.where_expr,
2019 Some(Expr::IsNull(Operand::Var("b".into()))),
2020 );
2021 }
2022
2023 #[test]
2024 fn is_null_case_insensitive() {
2025 let q = parse_src("MATCH (a) WHERE a.x is null RETURN a").unwrap();
2026 assert_eq!(q.where_expr, Some(Expr::IsNull(prop("a", "x"))));
2027 let q2 = parse_src("MATCH (a) WHERE a.x IS NOT NULL RETURN a").unwrap();
2028 assert_eq!(q2.where_expr, Some(Expr::IsNotNull(prop("a", "x"))));
2029 }
2030
2031 #[test]
2032 fn is_null_combined_with_and() {
2033 let q = parse_src("MATCH (a) WHERE a.x IS NULL AND a.y > 5 RETURN a").unwrap();
2034 assert!(matches!(q.where_expr, Some(Expr::And(_, _))));
2035 }
2036
2037 #[test]
2040 fn arith_add_in_where() {
2041 use crate::cypher::ast::ArithOp;
2042 let q = parse_src("MATCH (n) WHERE n.age + 1 > 5 RETURN n").unwrap();
2043 let expected_lhs = Operand::BinArith {
2044 op: ArithOp::Add,
2045 left: Box::new(prop("n", "age")),
2046 right: Box::new(Operand::Lit(Value::Int(1))),
2047 };
2048 assert_eq!(
2049 q.where_expr,
2050 Some(Expr::Cmp {
2051 lhs: expected_lhs,
2052 op: CmpOp::Gt,
2053 rhs: Operand::Lit(Value::Int(5)),
2054 })
2055 );
2056 }
2057
2058 #[test]
2059 fn arith_precedence_mul_over_add() {
2060 use crate::cypher::ast::ArithOp;
2061 let q = parse_src("MATCH (n) WHERE n.x = 1 + 2 * 3 RETURN n").unwrap();
2063 let expected_rhs = Operand::BinArith {
2064 op: ArithOp::Add,
2065 left: Box::new(Operand::Lit(Value::Int(1))),
2066 right: Box::new(Operand::BinArith {
2067 op: ArithOp::Mul,
2068 left: Box::new(Operand::Lit(Value::Int(2))),
2069 right: Box::new(Operand::Lit(Value::Int(3))),
2070 }),
2071 };
2072 assert_eq!(
2073 q.where_expr,
2074 Some(Expr::Cmp {
2075 lhs: prop("n", "x"),
2076 op: CmpOp::Eq,
2077 rhs: expected_rhs,
2078 })
2079 );
2080 }
2081
2082 #[test]
2083 fn arith_parens_override_precedence() {
2084 use crate::cypher::ast::ArithOp;
2085 let q = parse_src("MATCH (n) RETURN (1 + 2) * 3 AS r").unwrap();
2087 let expected = RetVal::ScalarExpr(Operand::BinArith {
2088 op: ArithOp::Mul,
2089 left: Box::new(Operand::BinArith {
2090 op: ArithOp::Add,
2091 left: Box::new(Operand::Lit(Value::Int(1))),
2092 right: Box::new(Operand::Lit(Value::Int(2))),
2093 }),
2094 right: Box::new(Operand::Lit(Value::Int(3))),
2095 });
2096 assert_eq!(q.returns[0].value, expected);
2097 assert_eq!(q.returns[0].alias, Some("r".into()));
2098 }
2099
2100 #[test]
2101 fn arith_scalar_expr_in_return() {
2102 use crate::cypher::ast::ArithOp;
2103 let q = parse_src("MATCH (n) RETURN n.age + 1 AS adjusted").unwrap();
2104 let expected = RetVal::ScalarExpr(Operand::BinArith {
2105 op: ArithOp::Add,
2106 left: Box::new(prop("n", "age")),
2107 right: Box::new(Operand::Lit(Value::Int(1))),
2108 });
2109 assert_eq!(q.returns[0].value, expected);
2110 assert_eq!(q.returns[0].alias, Some("adjusted".into()));
2111 }
2112
2113 #[test]
2114 fn arith_div_in_where() {
2115 use crate::cypher::ast::ArithOp;
2116 let q = parse_src("MATCH (n) WHERE n.x / 2 > 3 RETURN n").unwrap();
2117 assert!(matches!(
2118 q.where_expr,
2119 Some(Expr::Cmp {
2120 lhs: Operand::BinArith {
2121 op: ArithOp::Div,
2122 ..
2123 },
2124 ..
2125 })
2126 ));
2127 }
2128
2129 #[test]
2132 fn create_return_parses_node_var() {
2133 use super::parse_write;
2134 use crate::cypher::ast::{RetVal, WriteStatement};
2135
2136 let toks = crate::cypher::lex("CREATE (n:Thing {id: 'x'}) RETURN n").unwrap();
2137 let stmt = parse_write(&toks).unwrap();
2138 match stmt {
2139 WriteStatement::Create(s) => {
2140 assert_eq!(s.nodes.len(), 1);
2141 let returns = s.returns.expect("expected RETURN clause");
2142 assert_eq!(returns.len(), 1);
2143 assert_eq!(returns[0].value, RetVal::Var("n".into()));
2144 }
2145 _ => panic!("expected Create"),
2146 }
2147 }
2148
2149 #[test]
2150 fn create_return_prop_with_alias() {
2151 use super::parse_write;
2152 use crate::cypher::ast::{RetVal, WriteStatement};
2153
2154 let toks = crate::cypher::lex("CREATE (n:Thing {id: 'x'}) RETURN n.id AS node_id").unwrap();
2155 let stmt = parse_write(&toks).unwrap();
2156 match stmt {
2157 WriteStatement::Create(s) => {
2158 let returns = s.returns.expect("RETURN required");
2159 assert_eq!(
2160 returns[0].value,
2161 RetVal::Prop {
2162 var: "n".into(),
2163 field: "id".into()
2164 }
2165 );
2166 assert_eq!(returns[0].alias, Some("node_id".into()));
2167 }
2168 _ => panic!("expected Create"),
2169 }
2170 }
2171
2172 #[test]
2173 fn merge_return_parses_node_var() {
2174 use super::parse_write;
2175 use crate::cypher::ast::{RetVal, WriteStatement};
2176
2177 let toks = crate::cypher::lex("MERGE (n:Thing {id: 'x'}) RETURN n").unwrap();
2178 let stmt = parse_write(&toks).unwrap();
2179 match stmt {
2180 WriteStatement::Merge(s) => {
2181 assert_eq!(s.var, Some("n".into()));
2182 let returns = s.returns.expect("RETURN required");
2183 assert_eq!(returns[0].value, RetVal::Var("n".into()));
2184 }
2185 _ => panic!("expected Merge"),
2186 }
2187 }
2188
2189 #[test]
2190 fn is_write_tokens_still_true_for_create_return() {
2191 use super::is_write_tokens;
2192 use crate::cypher::lex;
2193
2194 let toks = lex("CREATE (n:T {id: 'x'}) RETURN n").unwrap();
2195 assert!(
2196 is_write_tokens(&toks),
2197 "CREATE...RETURN must still be classified as write"
2198 );
2199 }
2200
2201 #[test]
2202 fn where_in_list_and_param_parses() {
2203 let q = parse_src("MATCH (n) WHERE n.city IN ['Austin', $c] RETURN n").unwrap();
2204 match q.where_expr {
2205 Some(Expr::In { expr, list }) => {
2206 assert_eq!(
2207 expr,
2208 Operand::Prop {
2209 var: "n".into(),
2210 field: "city".into()
2211 }
2212 );
2213 assert_eq!(list.len(), 2);
2214 assert_eq!(list[0], Operand::Lit(Value::Str("Austin".into())));
2215 assert_eq!(list[1], Operand::Param("c".into()));
2216 }
2217 other => panic!("expected Expr::In, got {other:?}"),
2218 }
2219 let q2 = parse_src("MATCH (n) WHERE n.city IN $cities RETURN n").unwrap();
2220 match q2.where_expr {
2221 Some(Expr::In { list, .. }) => {
2222 assert_eq!(list, vec![Operand::Param("cities".into())]);
2223 }
2224 other => panic!("expected Expr::In, got {other:?}"),
2225 }
2226 }
2227
2228 #[test]
2229 fn return_distinct_parses() {
2230 let q = parse_src("MATCH (n) RETURN DISTINCT n.city").unwrap();
2231 assert!(q.distinct);
2232 assert_eq!(q.returns.len(), 1);
2233 }
2234
2235 #[test]
2236 fn union_query_parses_into_parts() {
2237 use super::parse_read;
2238 let u = parse_read(&lex("MATCH (n) RETURN n UNION ALL MATCH (m) RETURN m").unwrap())
2239 .expect("UNION parses");
2240 assert_eq!(u.parts.len(), 2);
2241 assert_eq!(u.all_flags, vec![true]);
2242 let single = parse_read(&lex("MATCH (n) RETURN n").unwrap()).unwrap();
2244 assert_eq!(single.parts.len(), 1);
2245 assert!(single.all_flags.is_empty());
2246 }
2247
2248 #[test]
2249 fn case_when_expression_parses() {
2250 let q = parse_src("MATCH (n) RETURN CASE WHEN n.x = 1 THEN 2 ELSE 3 END AS c")
2251 .expect("CASE parses");
2252 assert!(matches!(
2253 q.returns[0].value,
2254 RetVal::ScalarExpr(Operand::Case { .. })
2255 ));
2256 }
2257
2258 #[test]
2259 fn collect_is_a_supported_aggregate() {
2260 let q = parse_src("MATCH (n) RETURN collect(n.name) AS names").expect("collect parses");
2261 assert!(matches!(
2262 q.returns[0].value,
2263 RetVal::Agg {
2264 func: AggFunc::Collect,
2265 ..
2266 }
2267 ));
2268 }
2269
2270 #[test]
2271 fn match_set_return_parses() {
2272 use super::parse_write;
2273 use crate::cypher::ast::{RetVal, WriteStatement};
2274
2275 let toks = crate::cypher::lex("MATCH (n {id:'a'}) SET n.x = 2 RETURN n.x").unwrap();
2276 let stmt = parse_write(&toks).unwrap();
2277 match stmt {
2278 WriteStatement::MatchSet(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: "x".into()
2285 }
2286 );
2287 }
2288 other => panic!("expected MatchSet, got {other:?}"),
2289 }
2290 }
2291
2292 #[test]
2293 fn merge_on_create_and_on_match_parse() {
2294 use super::parse_write;
2295 use crate::cypher::ast::WriteStatement;
2296
2297 let toks = crate::cypher::lex(
2298 "MERGE (n:L {id:'new'}) ON CREATE SET n.born = 1 ON MATCH SET n.hit = 1 RETURN n",
2299 )
2300 .unwrap();
2301 let stmt = parse_write(&toks).unwrap();
2302 match stmt {
2303 WriteStatement::Merge(s) => {
2304 assert_eq!(s.on_create.len(), 1);
2305 assert_eq!(s.on_create[0].field, "born");
2306 assert_eq!(s.on_match.len(), 1);
2307 assert_eq!(s.on_match[0].field, "hit");
2308 assert!(s.returns.is_some());
2309 }
2310 other => panic!("expected Merge, got {other:?}"),
2311 }
2312 }
2313
2314 #[test]
2315 fn paren_grouping_and_and_left_assoc() {
2316 let q = parse_src("MATCH (a) WHERE (a.x = 1 OR a.y = 2) AND a.z = 3 RETURN a").unwrap();
2317 let expected = Expr::And(
2318 Box::new(Expr::Or(
2319 Box::new(cmp(prop("a", "x"), CmpOp::Eq, Operand::Lit(Value::Int(1)))),
2320 Box::new(cmp(prop("a", "y"), CmpOp::Eq, Operand::Lit(Value::Int(2)))),
2321 )),
2322 Box::new(cmp(prop("a", "z"), CmpOp::Eq, Operand::Lit(Value::Int(3)))),
2323 );
2324 assert_eq!(q.where_expr, Some(expected));
2325
2326 let q = parse_src("MATCH (a) WHERE a.x = 1 AND a.y = 2 AND a.z = 3 RETURN a").unwrap();
2327 let expected = Expr::And(
2328 Box::new(Expr::And(
2329 Box::new(cmp(prop("a", "x"), CmpOp::Eq, Operand::Lit(Value::Int(1)))),
2330 Box::new(cmp(prop("a", "y"), CmpOp::Eq, Operand::Lit(Value::Int(2)))),
2331 )),
2332 Box::new(cmp(prop("a", "z"), CmpOp::Eq, Operand::Lit(Value::Int(3)))),
2333 );
2334 assert_eq!(q.where_expr, Some(expected));
2335 }
2336
2337 #[test]
2338 fn order_by_bare_ident_is_var_when_not_an_alias() {
2339 let q = parse_src("MATCH (a) RETURN a, b.name ORDER BY a, b.name").unwrap();
2340 assert_eq!(
2341 q.order_by,
2342 vec![
2343 OrderItem {
2344 target: OrderTarget::Var("a".into()),
2345 descending: false,
2346 },
2347 OrderItem {
2348 target: OrderTarget::Prop {
2349 var: "b".into(),
2350 field: "name".into(),
2351 },
2352 descending: false,
2353 },
2354 ]
2355 );
2356 }
2357
2358 #[test]
2359 fn parse_never_panics_on_token_sequences() {
2360 let sequences: Vec<Vec<Tok>> = vec![
2361 vec![],
2362 vec![Tok::Match],
2363 vec![Tok::Return],
2364 vec![Tok::Dash, Tok::Dash, Tok::Dash],
2365 vec![Tok::Lt, Tok::Gt, Tok::Eq],
2366 vec![Tok::LParen, Tok::RParen, Tok::RParen],
2367 vec![Tok::Int(1), Tok::Float(2.0), Tok::Str("x".into())],
2368 vec![Tok::Where, Tok::Not, Tok::And, Tok::Or],
2369 vec![Tok::Order, Tok::By, Tok::Asc, Tok::Desc],
2370 vec![Tok::Skip, Tok::Limit, Tok::As],
2371 vec![Tok::Ident("n".into()), Tok::Dot, Tok::Ident("x".into())],
2372 vec![Tok::Param("p".into()), Tok::Colon, Tok::Comma],
2373 vec![Tok::LBracket, Tok::RBracket, Tok::LBrace, Tok::RBrace],
2374 lex("MATCH (a)-[x:]->(b) RETURN a ORDER BY a LIMIT 1 extra").unwrap(),
2375 vec![
2377 Tok::Ident("COUNT".into()),
2378 Tok::LParen,
2379 Tok::Star,
2380 Tok::RParen,
2381 ],
2382 vec![
2383 Tok::Ident("sum".into()),
2384 Tok::LParen,
2385 Tok::Ident("n".into()),
2386 Tok::Dot,
2387 Tok::Ident("x".into()),
2388 Tok::RParen,
2389 ],
2390 vec![Tok::Star],
2391 vec![Tok::Star, Tok::LParen, Tok::RParen, Tok::Star],
2392 vec![
2393 Tok::Ident("avg".into()),
2394 Tok::LParen,
2395 Tok::Star,
2396 Tok::RParen,
2397 ],
2398 vec![Tok::Ident("min".into()), Tok::LParen, Tok::RParen],
2399 vec![
2400 Tok::Ident("max".into()),
2401 Tok::LParen,
2402 Tok::Star,
2403 Tok::RParen,
2404 ],
2405 ];
2406 for toks in sequences {
2407 let result = std::panic::catch_unwind(|| parse(&toks));
2408 assert!(result.is_ok(), "parse panicked on token sequence {toks:?}");
2409 let parsed = result.unwrap();
2410 if let Err(err) = parsed {
2411 assert!(
2412 err.contains("token"),
2413 "error must include token position, got: {err}"
2414 );
2415 }
2416 }
2417 }
2418
2419 #[test]
2420 fn aggregate_functions_parse_to_agg_retval() {
2421 use crate::cypher::ast::{AggArg, AggFunc, RetVal};
2422
2423 let q = parse_src("MATCH (n) RETURN COUNT(*)").unwrap();
2425 assert_eq!(q.returns.len(), 1);
2426 assert_eq!(
2427 q.returns[0].value,
2428 RetVal::Agg {
2429 func: AggFunc::Count,
2430 arg: AggArg::Star,
2431 }
2432 );
2433 assert_eq!(q.returns[0].alias, None);
2434
2435 let q = parse_src("MATCH (n) RETURN COUNT(n)").unwrap();
2437 assert_eq!(
2438 q.returns[0].value,
2439 RetVal::Agg {
2440 func: AggFunc::Count,
2441 arg: AggArg::Var("n".into()),
2442 }
2443 );
2444
2445 let q = parse_src("MATCH (n) RETURN SUM(n.age) AS total").unwrap();
2447 assert_eq!(
2448 q.returns[0].value,
2449 RetVal::Agg {
2450 func: AggFunc::Sum,
2451 arg: AggArg::Prop {
2452 var: "n".into(),
2453 field: "age".into()
2454 },
2455 }
2456 );
2457 assert_eq!(q.returns[0].alias, Some("total".into()));
2458
2459 let q = parse_src("MATCH (n) RETURN avg(n.score)").unwrap();
2461 assert!(matches!(
2462 q.returns[0].value,
2463 RetVal::Agg {
2464 func: AggFunc::Avg,
2465 ..
2466 }
2467 ));
2468 let q = parse_src("MATCH (n) RETURN Min(n.x)").unwrap();
2469 assert!(matches!(
2470 q.returns[0].value,
2471 RetVal::Agg {
2472 func: AggFunc::Min,
2473 ..
2474 }
2475 ));
2476 let q = parse_src("MATCH (n) RETURN MAX(n.x)").unwrap();
2477 assert!(matches!(
2478 q.returns[0].value,
2479 RetVal::Agg {
2480 func: AggFunc::Max,
2481 ..
2482 }
2483 ));
2484 }
2485
2486 #[test]
2487 fn nested_parens_beyond_limit_is_err_not_panic() {
2488 let mut src = String::from("MATCH (a) WHERE ");
2489 for _ in 0..80 {
2490 src.push('(');
2491 }
2492 src.push_str("a.x = 1");
2493 for _ in 0..80 {
2494 src.push(')');
2495 }
2496 src.push_str(" RETURN a");
2497 assert_parse_err(&src);
2498 }
2499
2500 fn hop_range_of(src: &str) -> HopRange {
2503 let q = parse_src(src).expect(src);
2504 let (rel, _) = &q.matches[0].chain[0];
2505 rel.hops.expect("expected hop range")
2506 }
2507
2508 fn assert_hop_err(src: &str, needle: &str) {
2509 let result = std::panic::catch_unwind(|| parse_src(src));
2510 assert!(result.is_ok(), "parse panicked on {src:?}");
2511 let err = result
2512 .unwrap()
2513 .expect_err(&format!("parse({src:?}) must Err"));
2514 assert!(
2515 err.contains(needle),
2516 "error must contain {needle:?}, got: {err}"
2517 );
2518 }
2519
2520 #[test]
2521 fn var_length_bare_star_is_one_to_ten() {
2522 let r = hop_range_of("MATCH (a)-[r:T*]->(b) RETURN a");
2523 assert_eq!(r, HopRange { min: 1, max: 10 });
2524 }
2525
2526 #[test]
2527 fn var_length_exact_n_hops() {
2528 let r = hop_range_of("MATCH (a)-[r:T*3]->(b) RETURN a");
2529 assert_eq!(r, HopRange { min: 3, max: 3 });
2530 }
2531
2532 #[test]
2533 fn var_length_min_max_range() {
2534 let r = hop_range_of("MATCH (a)-[r:T*2..5]->(b) RETURN a");
2535 assert_eq!(r, HopRange { min: 2, max: 5 });
2536 }
2537
2538 #[test]
2539 fn var_length_dotdot_max() {
2540 let r = hop_range_of("MATCH (a)-[r:T*..4]->(b) RETURN a");
2541 assert_eq!(r, HopRange { min: 1, max: 4 });
2542 }
2543
2544 #[test]
2545 fn var_length_cap_at_ten_is_ok() {
2546 let r = hop_range_of("MATCH (a)-[r:T*10]->(b) RETURN a");
2547 assert_eq!(r, HopRange { min: 10, max: 10 });
2548 let r2 = hop_range_of("MATCH (a)-[r:T*1..10]->(b) RETURN a");
2549 assert_eq!(r2, HopRange { min: 1, max: 10 });
2550 }
2551
2552 #[test]
2553 fn var_length_cap_exceeded_is_err() {
2554 assert_hop_err(
2555 "MATCH (a)-[r:T*11]->(b) RETURN a",
2556 "variable-length paths are capped at 10 hops",
2557 );
2558 assert_hop_err(
2559 "MATCH (a)-[r:T*1..11]->(b) RETURN a",
2560 "variable-length paths are capped at 10 hops",
2561 );
2562 }
2563
2564 #[test]
2565 fn var_length_unbounded_min_dot_dot_is_err() {
2566 assert_hop_err(
2567 "MATCH (a)-[r:T*2..]->(b) RETURN a",
2568 "variable-length paths are capped at 10 hops",
2569 );
2570 }
2571
2572 #[test]
2573 fn var_length_shortest_path_parses() {
2574 let q =
2575 parse_src("MATCH (a:N) MATCH (b:N) MATCH shortestPath((a)-[r:T*..5]->(b)) RETURN a")
2576 .expect("shortestPath must parse");
2577 assert!(q.matches[2].shortest, "third match must be shortest=true");
2578 let (rel, _) = &q.matches[2].chain[0];
2579 assert_eq!(rel.hops, Some(HopRange { min: 1, max: 5 }));
2580 assert_eq!(rel.etypes, vec!["T".to_string()]);
2581 }
2582
2583 #[test]
2584 fn var_length_no_type_is_ok() {
2585 let r = hop_range_of("MATCH (a)-[r*1..3]->(b) RETURN a");
2587 assert_eq!(r, HopRange { min: 1, max: 3 });
2588 }
2589
2590 #[test]
2591 fn var_length_rel_appears_in_chain() {
2592 let q = parse_src("MATCH (a)-[r:T*2..4]->(b) RETURN a").unwrap();
2593 let (rel, dest) = &q.matches[0].chain[0];
2594 assert_eq!(rel.var.as_deref(), Some("r"));
2595 assert_eq!(rel.etypes, vec!["T".to_string()]);
2596 assert_eq!(rel.dir, RelDir::Right);
2597 assert_eq!(rel.hops, Some(HopRange { min: 2, max: 4 }));
2598 assert_eq!(dest.var.as_deref(), Some("b"));
2599 }
2600
2601 #[test]
2602 fn var_length_zero_hop_minimum_is_err() {
2603 assert_hop_err(
2605 "MATCH (a)-[r:T*0]->(b) RETURN a",
2606 "zero-length variable-length paths are not supported",
2607 );
2608 assert_hop_err(
2610 "MATCH (a)-[r:T*0..3]->(b) RETURN a",
2611 "zero-length variable-length paths are not supported",
2612 );
2613 }
2614
2615 #[test]
2616 fn create_accepts_list_literal_property() {
2617 use super::parse_write;
2618 use crate::cypher::ast::WriteStatement;
2619 let src = "CREATE (n:Person {id: 'p1', tags: ['a', 'b']})";
2620 let stmt = parse_write(&lex(src).unwrap())
2621 .expect("CREATE with a list-literal property must parse");
2622 let WriteStatement::Create(c) = stmt else {
2623 panic!("expected a Create statement");
2624 };
2625 let tags = &c.nodes[0]
2626 .props
2627 .iter()
2628 .find(|(k, _)| k == "tags")
2629 .expect("tags property present")
2630 .1;
2631 assert_eq!(
2632 *tags,
2633 Value::List(vec![Value::Str("a".into()), Value::Str("b".into())])
2634 );
2635 }
2636
2637 #[test]
2638 fn create_accepts_empty_and_nested_list_literals() {
2639 use super::parse_write;
2640 use crate::cypher::ast::WriteStatement;
2641 let src = "CREATE (n:L {id: 'p1', empty: [], nested: [[1, 2], [3]]})";
2642 let stmt = parse_write(&lex(src).unwrap()).expect("empty and nested lists must parse");
2643 let WriteStatement::Create(c) = stmt else {
2644 panic!("expected a Create statement");
2645 };
2646 let get = |k: &str| {
2647 c.nodes[0]
2648 .props
2649 .iter()
2650 .find(|(name, _)| name == k)
2651 .expect("property present")
2652 .1
2653 .clone()
2654 };
2655 assert_eq!(get("empty"), Value::List(vec![]));
2656 assert_eq!(
2657 get("nested"),
2658 Value::List(vec![
2659 Value::List(vec![Value::Int(1), Value::Int(2)]),
2660 Value::List(vec![Value::Int(3)]),
2661 ])
2662 );
2663 }
2664
2665 #[test]
2666 fn set_accepts_list_literal_rhs() {
2667 use super::parse_write;
2668 use crate::cypher::ast::{Operand, WriteStatement};
2669 let src = "MATCH (n:Person {id: 'p1'}) SET n.tags = ['x', 'y']";
2670 let stmt = parse_write(&lex(src).unwrap()).expect("SET with a list-literal RHS must parse");
2671 let WriteStatement::MatchSet(m) = stmt else {
2672 panic!("expected a MatchSet statement");
2673 };
2674 let set = &m.sets[0];
2675 assert_eq!(set.field, "tags");
2676 assert_eq!(
2677 set.value,
2678 Operand::Lit(Value::List(vec![
2679 Value::Str("x".into()),
2680 Value::Str("y".into())
2681 ]))
2682 );
2683 }
2684}