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