1use indexmap::IndexMap;
39use smol_str::SmolStr;
40
41use cyrs_hir::{
42 Clause, Direction as HirDir, Expr as HirExpr, HirSpan, ListPredKind as HirListPredKind,
43 Pattern, PatternElement, PatternPart, Projection, RelLength as HirRelLen, RemoveItem, SetItem,
44 Statement, VarId as HirVarId,
45};
46
47use crate::{
48 AggExpr, BinOp, Direction, Expr, LabelSet, ListPredKind, NodeSpec, OpId, OrderKey,
49 PlanLowerError, Projection as PlanProj, ReadOp, RelLength, RelSpec, UnaryOp, UnionKind, VarId,
50 WriteOp,
51};
52
53#[derive(Debug, Clone)]
68pub struct PlanStatement {
69 pub ops: Vec<ReadOp>,
71 pub write_ops: Vec<WriteOp>,
73 pub var_map: IndexMap<VarId, HirVarId>,
76}
77
78impl PlanStatement {
79 fn new() -> Self {
80 Self::empty()
81 }
82
83 #[must_use]
87 pub fn empty() -> Self {
88 Self {
89 ops: Vec::new(),
90 write_ops: Vec::new(),
91 var_map: IndexMap::new(),
92 }
93 }
94
95 fn push(&mut self, op: ReadOp) -> OpId {
97 #[allow(clippy::cast_possible_truncation)]
98 let id = OpId(self.ops.len() as u32);
99 self.ops.push(op);
100 id
101 }
102}
103
104pub fn lower_statement(stmt: &Statement) -> Result<PlanStatement, PlanLowerError> {
134 precheck_statement(stmt)?;
135 let mut ctx = LowerCtx::new(stmt);
136 ctx.lower(stmt);
137 Ok(ctx.into_plan())
138}
139
140fn precheck_statement(stmt: &Statement) -> Result<(), PlanLowerError> {
145 for clause in &stmt.clauses {
146 let span = clause.span();
147 match clause {
148 Clause::Match { pattern, .. } | Clause::Create { pattern, .. } => {
149 check_pattern(pattern, span)?;
150 }
151 Clause::Where { predicate, .. } => check_expr(predicate, span)?,
152 Clause::With {
153 projections,
154 filter,
155 ..
156 } => {
157 for p in projections {
158 check_expr(&p.expr, span)?;
159 }
160 if let Some(f) = filter {
161 check_expr(f, span)?;
162 }
163 }
164 Clause::Return { projections, .. } => {
165 for p in projections {
166 check_expr(&p.expr, span)?;
167 }
168 }
169 Clause::Unwind { list, .. } => check_expr(list, span)?,
170 Clause::Merge {
171 pattern,
172 on_create,
173 on_match,
174 ..
175 } => {
176 check_pattern(pattern, span)?;
177 for item in on_create.iter().chain(on_match.iter()) {
178 check_set_item(item, span)?;
179 }
180 }
181 Clause::Set { items, .. } => {
182 for item in items {
183 check_set_item(item, span)?;
184 }
185 }
186 Clause::Remove { items, .. } => {
187 for item in items {
188 check_remove_item(item, span)?;
189 }
190 }
191 Clause::Delete { targets, .. } => {
192 for t in targets {
193 check_expr(t, span)?;
194 }
195 }
196 Clause::Call { args, .. } => {
197 for a in args {
198 check_expr(a, span)?;
199 }
200 }
201 }
202 }
203 Ok(())
204}
205
206fn check_pattern(pattern: &Pattern, clause_span: HirSpan) -> Result<(), PlanLowerError> {
207 for part in &pattern.parts {
208 match part.elements.first() {
215 None => return Err(PlanLowerError::EmptyPatternPart { span: clause_span }),
216 Some(PatternElement::Rel { .. }) => {
217 return Err(PlanLowerError::EmptyPatternPart { span: clause_span });
218 }
219 Some(PatternElement::Node { .. }) => {}
220 }
221 for elem in &part.elements {
222 let props = match elem {
223 PatternElement::Node { props, .. } | PatternElement::Rel { props, .. } => {
224 props.as_ref()
225 }
226 };
227 if let Some(p) = props {
228 check_expr(p, elem.span())?;
231 }
232 }
233 }
234 Ok(())
235}
236
237fn check_set_item(item: &SetItem, span: HirSpan) -> Result<(), PlanLowerError> {
238 match item {
239 SetItem::Property { target, value, .. } => {
240 check_expr(target, span)?;
241 check_expr(value, span)?;
242 }
243 SetItem::Labels { .. } => {}
244 SetItem::AssignMap { map, .. } => check_expr(map, span)?,
245 }
246 Ok(())
247}
248
249fn check_remove_item(item: &RemoveItem, span: HirSpan) -> Result<(), PlanLowerError> {
250 match item {
251 RemoveItem::Property { target, .. } => check_expr(target, span)?,
252 RemoveItem::Labels { .. } => {}
253 }
254 Ok(())
255}
256
257fn check_expr(expr: &HirExpr, span: HirSpan) -> Result<(), PlanLowerError> {
264 match expr {
265 HirExpr::Null
267 | HirExpr::Bool(_)
268 | HirExpr::Int(_)
269 | HirExpr::Float(_)
270 | HirExpr::String(_)
271 | HirExpr::Var(_)
272 | HirExpr::Param(_) => Ok(()),
273
274 HirExpr::PatternPredicate(pattern) => check_pattern(pattern, span),
284
285 HirExpr::Unresolved(name) => Err(PlanLowerError::UnresolvedName {
287 name: name.clone(),
288 span,
289 }),
290 HirExpr::ListComprehension { .. } => Err(PlanLowerError::UndesugaredExpr {
291 kind: "ListComprehension",
292 span,
293 }),
294 HirExpr::MapProjection { .. } => Err(PlanLowerError::UndesugaredExpr {
295 kind: "MapProjection",
296 span,
297 }),
298
299 HirExpr::Prop { target, .. } => check_expr(target, span),
301 HirExpr::Index { target, index } => {
302 check_expr(target, span)?;
303 check_expr(index, span)
304 }
305 HirExpr::Slice { target, start, end } => {
306 check_expr(target, span)?;
307 if let Some(s) = start {
308 check_expr(s, span)?;
309 }
310 if let Some(e) = end {
311 check_expr(e, span)?;
312 }
313 Ok(())
314 }
315 HirExpr::List(items) => {
316 for item in items {
317 check_expr(item, span)?;
318 }
319 Ok(())
320 }
321 HirExpr::Map(pairs) => {
322 for (_, v) in pairs {
323 check_expr(v, span)?;
324 }
325 Ok(())
326 }
327 HirExpr::Call { args, .. } => {
328 for a in args {
329 check_expr(a, span)?;
330 }
331 Ok(())
332 }
333 HirExpr::BinOp { lhs, rhs, .. } => {
334 check_expr(lhs, span)?;
335 check_expr(rhs, span)
336 }
337 HirExpr::UnaryOp { operand, .. } | HirExpr::IsNull { operand, .. } => {
338 check_expr(operand, span)
339 }
340 HirExpr::Case {
341 scrutinee,
342 arms,
343 otherwise,
344 } => {
345 if let Some(s) = scrutinee {
346 check_expr(s, span)?;
347 }
348 for (w, t) in arms {
349 check_expr(w, span)?;
350 check_expr(t, span)?;
351 }
352 if let Some(o) = otherwise {
353 check_expr(o, span)?;
354 }
355 Ok(())
356 }
357 HirExpr::InList { operand, list } => {
358 check_expr(operand, span)?;
359 check_expr(list, span)
360 }
361 HirExpr::ListPredicate {
362 iterable,
363 predicate,
364 ..
365 } => {
366 check_expr(iterable, span)?;
367 if let Some(p) = predicate {
368 check_expr(p, span)?;
369 }
370 Ok(())
371 }
372 }
373}
374
375struct LowerCtx<'s> {
378 plan: PlanStatement,
379 hir_to_plan: IndexMap<HirVarId, VarId>,
381 next_var: u32,
382 _stmt: &'s Statement,
383}
384
385impl<'s> LowerCtx<'s> {
386 fn new(stmt: &'s Statement) -> Self {
387 Self {
388 plan: PlanStatement::new(),
389 hir_to_plan: IndexMap::new(),
390 next_var: 0,
391 _stmt: stmt,
392 }
393 }
394
395 fn into_plan(self) -> PlanStatement {
396 self.plan
397 }
398
399 fn map_var(&mut self, hir_var: HirVarId) -> VarId {
402 if let Some(&plan_var) = self.hir_to_plan.get(&hir_var) {
403 return plan_var;
404 }
405 let plan_var = VarId(self.next_var);
406 self.next_var += 1;
407 self.hir_to_plan.insert(hir_var, plan_var);
408 self.plan.var_map.insert(plan_var, hir_var);
409 plan_var
410 }
411
412 fn lower(&mut self, stmt: &Statement) {
415 let mut current_op: Option<OpId> = None;
426
427 let mut i = 0;
428 while i < stmt.clauses.len() {
429 let clause = &stmt.clauses[i];
430 match clause {
431 Clause::Match {
432 pattern, optional, ..
433 } => {
434 let (new_op, _) = self.lower_match_pattern(pattern, current_op, *optional);
435 current_op = Some(new_op);
436 }
437 Clause::Where { predicate, .. } => {
438 let pred = self.lower_expr(predicate);
439 let input = current_op.unwrap_or_else(|| self.push_source_all());
440 let op = self.plan.push(ReadOp::Filter {
441 input,
442 predicate: pred,
443 });
444 current_op = Some(op);
445 }
446 Clause::With {
447 projections,
448 filter,
449 ..
450 } => {
451 let input = current_op.unwrap_or_else(|| self.push_source_all());
452 let items = self.lower_projections(projections);
453 let filter_expr = filter.as_ref().map(|f| self.lower_expr(f));
454 let op = self.plan.push(ReadOp::With {
455 input,
456 items,
457 filter: filter_expr,
458 });
459 current_op = Some(op);
460 }
461 Clause::Return {
462 projections,
463 distinct,
464 ..
465 } => {
466 let input = current_op.unwrap_or_else(|| self.push_source_all());
467 let (items, agg_items) = self.split_projections_agg(projections);
468 let op = if agg_items.is_empty() {
469 let proj_items = self.lower_projections(projections);
470 self.plan.push(ReadOp::Project {
471 input,
472 items: proj_items,
473 })
474 } else {
475 let keys: Vec<Expr> = items.iter().map(|p| p.expr.clone()).collect();
478 let agg_op = self.plan.push(ReadOp::Aggregate {
479 input,
480 keys,
481 aggs: agg_items,
482 });
483 let all_items = self.lower_projections(projections);
486 self.plan.push(ReadOp::Project {
487 input: agg_op,
488 items: all_items,
489 })
490 };
491 let op = if *distinct {
492 self.plan.push(ReadOp::Distinct { input: op })
493 } else {
494 op
495 };
496 current_op = Some(op);
497 }
498 Clause::Unwind { list, bind, .. } => {
499 let input = current_op.unwrap_or_else(|| self.push_source_all());
500 let list_expr = self.lower_expr(list);
501 let bind_var = self.map_var(*bind);
502 let op = self.plan.push(ReadOp::Unwind {
503 input,
504 list: list_expr,
505 bind: bind_var,
506 });
507 current_op = Some(op);
508 }
509 Clause::Create { pattern, .. } => {
510 let write_ops = self.lower_create_pattern(pattern);
511 self.plan.write_ops.extend(write_ops);
512 }
513 Clause::Merge {
514 pattern,
515 on_create,
516 on_match,
517 ..
518 } => {
519 let write_ops = self.lower_merge_pattern(pattern, on_create, on_match);
520 self.plan.write_ops.extend(write_ops);
521 }
522 Clause::Set { items, .. } => {
523 let write_ops = self.lower_set_items(items);
524 self.plan.write_ops.extend(write_ops);
525 }
526 Clause::Remove { items, .. } => {
527 let write_ops = self.lower_remove_items(items);
528 self.plan.write_ops.extend(write_ops);
529 }
530 Clause::Delete {
531 targets, detach, ..
532 } => {
533 let exprs: Vec<Expr> = targets.iter().map(|e| self.lower_expr(e)).collect();
534 self.plan.write_ops.push(WriteOp::Delete {
535 targets: exprs,
536 detach: *detach,
537 });
538 }
539 Clause::Call { .. } => {
540 }
543 }
544 i += 1;
545 }
546 }
547
548 fn push_source_all(&mut self) -> OpId {
551 self.plan.push(ReadOp::Source {
552 label: None,
553 bind: VarId(self.next_var),
554 })
555 }
558
559 fn lower_match_pattern(
567 &mut self,
568 pattern: &Pattern,
569 current_op: Option<OpId>,
570 optional: bool,
571 ) -> (OpId, Vec<VarId>) {
572 let mut vars = Vec::new();
573 let mut op: Option<OpId> = None;
574
575 for part in &pattern.parts {
576 let part_op = self.lower_pattern_part(part, &mut vars);
577 op = Some(match op {
578 None => part_op,
579 Some(left) => {
580 let _ = left;
586 part_op
587 }
588 });
589 }
590
591 let inner_op = op.unwrap_or_else(|| {
592 let bind = VarId(self.next_var);
594 self.next_var += 1;
595 self.plan.push(ReadOp::Source { label: None, bind })
596 });
597
598 let final_op = if optional {
599 if let Some(outer) = current_op {
600 let inner_root = self.plan.ops[inner_op.0 as usize].clone();
602 self.plan.push(ReadOp::OptionalJoin {
603 input: outer,
604 pattern: Box::new(inner_root),
605 })
606 } else {
607 inner_op
608 }
609 } else {
610 inner_op
611 };
612
613 (final_op, vars)
614 }
615
616 fn lower_pattern_part(&mut self, part: &PatternPart, vars: &mut Vec<VarId>) -> OpId {
617 let mut last_op: Option<OpId> = None;
626 let mut last_node_var: Option<VarId> = None;
627 let mut last_rel: Option<&PatternElement> = None;
628
629 for elem in &part.elements {
630 match elem {
631 PatternElement::Node {
632 bind,
633 labels,
634 props,
635 ..
636 } => {
637 let bind_var = bind.map(|v| {
638 let pv = self.map_var(v);
639 vars.push(pv);
640 pv
641 });
642
643 if let (Some(rel_elem), Some(from), Some(input)) =
644 (last_rel.take(), last_node_var, last_op)
645 {
646 let bind_var = bind_var.unwrap_or_else(|| {
649 let v = VarId(self.next_var);
650 self.next_var += 1;
651 v
652 });
653 let bind_to = bind_var;
654
655 let (rel_spec, bind_rel) = self.lower_rel_element(rel_elem, vars);
656
657 let node_spec = NodeSpec {
658 labels: LabelSet(labels.clone()),
659 properties: props.as_ref().map(|e| self.lower_expr(e)),
660 };
661
662 let op = self.plan.push(ReadOp::Expand {
663 input,
664 from,
665 rel: rel_spec,
666 to: node_spec,
667 bind_rel,
668 bind_to,
669 });
670 last_node_var = Some(bind_to);
671 last_op = Some(op);
672 } else {
673 let label_set = if labels.is_empty() {
676 None
677 } else {
678 Some(LabelSet(labels.clone()))
679 };
680 let bind_var = bind_var.unwrap_or_else(|| {
681 let v = VarId(self.next_var);
682 self.next_var += 1;
683 v
684 });
685 let op = self.plan.push(ReadOp::Source {
686 label: label_set,
687 bind: bind_var,
688 });
689 let op = if let Some(prop_expr) = props.as_ref() {
691 let predicate = self.lower_expr(prop_expr);
692 self.plan.push(ReadOp::Filter {
693 input: op,
694 predicate,
695 })
696 } else {
697 op
698 };
699 last_node_var = Some(bind_var);
700 last_op = Some(op);
701 }
702 }
703 PatternElement::Rel { .. } => {
704 last_rel = Some(elem);
706 }
707 }
708 }
709
710 last_op.unwrap_or_else(|| self.push_source_all())
715 }
716
717 fn lower_rel_element(
718 &mut self,
719 elem: &PatternElement,
720 vars: &mut Vec<VarId>,
721 ) -> (RelSpec, VarId) {
722 match elem {
723 PatternElement::Rel {
724 bind,
725 types,
726 direction,
727 length,
728 props,
729 ..
730 } => {
731 let bind_rel = bind
732 .map(|v| {
733 let pv = self.map_var(v);
734 vars.push(pv);
735 pv
736 })
737 .unwrap_or_else(|| {
738 let v = VarId(self.next_var);
739 self.next_var += 1;
740 v
741 });
742
743 let dir = match direction {
744 HirDir::Outgoing => Direction::Outgoing,
745 HirDir::Incoming => Direction::Incoming,
746 HirDir::Undirected => Direction::Undirected,
747 _ => unreachable!("cyrs-plan::lower: unhandled Direction variant"),
749 };
750
751 let rel_len = match length {
752 HirRelLen::Single => RelLength::Single,
753 HirRelLen::Variable { min, max } => RelLength::Variable {
754 min: *min,
755 max: *max,
756 },
757 _ => unreachable!("cyrs-plan::lower: unhandled RelLength variant"),
759 };
760
761 let rel_spec = RelSpec {
762 types: types.clone(),
763 direction: dir,
764 length: rel_len,
765 properties: props.as_ref().map(|e| self.lower_expr(e)),
766 };
767
768 (rel_spec, bind_rel)
769 }
770 PatternElement::Node { .. } => panic!("lower_rel_element called on a Node element"),
771 }
772 }
773
774 fn lower_projections(&mut self, projs: &[Projection]) -> Vec<PlanProj> {
777 projs
778 .iter()
779 .map(|p| {
780 let expr = self.lower_expr(&p.expr);
781 let alias = p.alias.clone().unwrap_or_else(|| synthesise_alias(&p.expr));
782 PlanProj { expr, alias }
783 })
784 .collect()
785 }
786
787 fn split_projections_agg(&mut self, projs: &[Projection]) -> (Vec<PlanProj>, Vec<AggExpr>) {
796 let mut non_agg = Vec::new();
797 let mut agg = Vec::new();
798
799 for p in projs {
800 if let HirExpr::Call {
801 name,
802 args,
803 distinct,
804 } = &p.expr
805 && is_aggregate_func(name)
806 {
807 let plan_args: Vec<Expr> = args.iter().map(|a| self.lower_expr(a)).collect();
808 agg.push(AggExpr {
809 func: name.clone(),
810 args: plan_args,
811 distinct: *distinct,
812 });
813 continue;
814 }
815 let expr = self.lower_expr(&p.expr);
816 let alias = p.alias.clone().unwrap_or_else(|| synthesise_alias(&p.expr));
817 non_agg.push(PlanProj { expr, alias });
818 }
819
820 (non_agg, agg)
821 }
822
823 fn lower_create_pattern(&mut self, pattern: &Pattern) -> Vec<WriteOp> {
826 let mut ops = Vec::new();
827 for part in &pattern.parts {
828 let paired = create_pattern_pairs(part);
830 for pair in paired {
831 match pair {
832 CreatePair::Node {
833 labels,
834 props,
835 bind,
836 } => {
837 let bind_var = bind.map(|v| self.map_var(v));
838 let props_expr = if let Some(e) = props.as_ref() {
839 self.lower_expr(e)
840 } else {
841 Expr::Map(vec![])
842 };
843 ops.push(WriteOp::CreateNode {
844 labels,
845 props: props_expr,
846 bind: bind_var,
847 });
848 }
849 CreatePair::Rel {
850 from_bind,
851 to_bind,
852 rel_type,
853 props,
854 bind,
855 } => {
856 let from = self.map_var(from_bind);
857 let to = self.map_var(to_bind);
858 let bind_rel = bind.map(|v| self.map_var(v));
859 let props_expr = if let Some(e) = props.as_ref() {
860 self.lower_expr(e)
861 } else {
862 Expr::Map(vec![])
863 };
864 ops.push(WriteOp::CreateRel {
865 from,
866 to,
867 rel_type,
868 props: props_expr,
869 bind: bind_rel,
870 });
871 }
872 }
873 }
874 }
875 ops
876 }
877
878 fn lower_merge_pattern(
879 &mut self,
880 pattern: &Pattern,
881 on_create: &[SetItem],
882 on_match: &[SetItem],
883 ) -> Vec<WriteOp> {
884 let mut ops = Vec::new();
885 let create_ops = self.lower_set_items(on_create);
886 let match_ops = self.lower_set_items(on_match);
887
888 for part in &pattern.parts {
889 let paired = create_pattern_pairs(part);
890 for pair in paired {
891 match pair {
892 CreatePair::Node {
893 labels,
894 props,
895 bind,
896 } => {
897 let bind_var = bind.map(|v| self.map_var(v));
898 let props_expr = if let Some(e) = props.as_ref() {
899 self.lower_expr(e)
900 } else {
901 Expr::Map(vec![])
902 };
903 ops.push(WriteOp::MergeNode {
904 labels,
905 props: props_expr,
906 on_create: create_ops.clone(),
907 on_match: match_ops.clone(),
908 bind: bind_var,
909 });
910 }
911 CreatePair::Rel {
912 from_bind,
913 to_bind,
914 rel_type,
915 props,
916 bind,
917 } => {
918 let from = self.map_var(from_bind);
919 let to = self.map_var(to_bind);
920 let bind_rel = bind.map(|v| self.map_var(v));
921 let props_expr = if let Some(e) = props.as_ref() {
922 self.lower_expr(e)
923 } else {
924 Expr::Map(vec![])
925 };
926 ops.push(WriteOp::MergeRel {
927 from,
928 to,
929 rel_type,
930 props: props_expr,
931 on_create: create_ops.clone(),
932 on_match: match_ops.clone(),
933 bind: bind_rel,
934 });
935 }
936 }
937 }
938 }
939 ops
940 }
941
942 fn lower_set_items(&mut self, items: &[SetItem]) -> Vec<WriteOp> {
943 items
944 .iter()
945 .flat_map(|item| self.lower_set_item(item))
946 .collect()
947 }
948
949 fn lower_set_item(&mut self, item: &SetItem) -> Vec<WriteOp> {
950 match item {
951 SetItem::Property {
952 target,
953 prop,
954 value,
955 } => {
956 let target_var = if let Some(hir_var) = expr_to_var_id(target) {
960 self.map_var(hir_var)
961 } else {
962 let v = VarId(self.next_var);
963 self.next_var += 1;
964 v
965 };
966 vec![WriteOp::SetProperty {
967 target: target_var,
968 prop: prop.clone(),
969 value: self.lower_expr(value),
970 }]
971 }
972 SetItem::Labels { target, labels } => {
973 let target_var = self.map_var(*target);
974 vec![WriteOp::SetLabels {
975 target: target_var,
976 labels: labels.clone(),
977 }]
978 }
979 SetItem::AssignMap {
980 target,
981 map: _,
982 replace: _,
983 } => {
984 let target_var = self.map_var(*target);
990 vec![WriteOp::SetLabels {
991 target: target_var,
992 labels: vec![],
993 }]
994 }
995 }
996 }
997
998 fn lower_remove_items(&mut self, items: &[RemoveItem]) -> Vec<WriteOp> {
999 items
1000 .iter()
1001 .map(|item| match item {
1002 RemoveItem::Property { target, prop } => {
1003 let target_var = if let Some(hir_var) = expr_to_var_id(target) {
1004 self.map_var(hir_var)
1005 } else {
1006 let v = VarId(self.next_var);
1007 self.next_var += 1;
1008 v
1009 };
1010 WriteOp::RemoveProperty {
1011 target: target_var,
1012 prop: prop.clone(),
1013 }
1014 }
1015 RemoveItem::Labels { target, labels } => {
1016 let target_var = self.map_var(*target);
1017 WriteOp::RemoveLabels {
1018 target: target_var,
1019 labels: labels.clone(),
1020 }
1021 }
1022 })
1023 .collect()
1024 }
1025
1026 fn lower_expr(&mut self, expr: &HirExpr) -> Expr {
1040 match expr {
1041 HirExpr::Null => Expr::Null,
1042 HirExpr::Bool(b) => Expr::Bool(*b),
1043 HirExpr::Int(i) => Expr::Int(*i),
1044 HirExpr::Float(f) => Expr::Float(*f),
1045 HirExpr::String(s) => Expr::String(s.clone()),
1046 HirExpr::Var(v) => Expr::Var(self.map_var(*v)),
1047 HirExpr::Param(name) => Expr::Param { name: name.clone() },
1048
1049 HirExpr::Prop { target, prop } => Expr::Prop {
1050 target: Box::new(self.lower_expr(target)),
1051 prop: prop.clone(),
1052 },
1053 HirExpr::Index { target, index } => Expr::Index {
1054 target: Box::new(self.lower_expr(target)),
1055 index: Box::new(self.lower_expr(index)),
1056 },
1057 HirExpr::Slice { target, start, end } => Expr::Slice {
1058 target: Box::new(self.lower_expr(target)),
1059 start: start.as_ref().map(|s| Box::new(self.lower_expr(s))),
1060 end: end.as_ref().map(|e| Box::new(self.lower_expr(e))),
1061 },
1062 HirExpr::List(items) => Expr::List(items.iter().map(|e| self.lower_expr(e)).collect()),
1063 HirExpr::Map(pairs) => Expr::Map(
1064 pairs
1065 .iter()
1066 .map(|(k, v)| (k.clone(), self.lower_expr(v)))
1067 .collect(),
1068 ),
1069 HirExpr::Call {
1070 name,
1071 args,
1072 distinct: _,
1073 } => Expr::Call {
1074 func: name.clone(),
1075 args: args.iter().map(|a| self.lower_expr(a)).collect(),
1076 },
1077 HirExpr::BinOp { op, lhs, rhs } => Expr::BinOp {
1078 op: lower_bin_op(*op),
1079 lhs: Box::new(self.lower_expr(lhs)),
1080 rhs: Box::new(self.lower_expr(rhs)),
1081 },
1082 HirExpr::UnaryOp { op, operand } => Expr::UnaryOp {
1083 op: match op {
1084 cyrs_hir::UnaryOp::Neg => UnaryOp::Neg,
1085 cyrs_hir::UnaryOp::Not => UnaryOp::Not,
1086 },
1087 operand: Box::new(self.lower_expr(operand)),
1088 },
1089 HirExpr::Case {
1090 scrutinee,
1091 arms,
1092 otherwise,
1093 } => Expr::Case {
1094 scrutinee: scrutinee.as_ref().map(|s| Box::new(self.lower_expr(s))),
1095 arms: arms
1096 .iter()
1097 .map(|(w, t)| (self.lower_expr(w), self.lower_expr(t)))
1098 .collect(),
1099 otherwise: otherwise.as_ref().map(|o| Box::new(self.lower_expr(o))),
1100 },
1101 HirExpr::IsNull { operand, negated } => Expr::IsNull {
1102 operand: Box::new(self.lower_expr(operand)),
1103 negated: *negated,
1104 },
1105 HirExpr::InList { operand, list } => Expr::InList {
1106 operand: Box::new(self.lower_expr(operand)),
1107 list: Box::new(self.lower_expr(list)),
1108 },
1109
1110 HirExpr::Unresolved(name) => {
1112 debug_assert!(
1114 false,
1115 "Unresolved variable `{name}` encountered in HIR→Plan lowering; \
1116 run name resolution (cy-b4b) before calling lower_statement"
1117 );
1118 Expr::Null
1119 }
1120
1121 HirExpr::PatternPredicate(pattern) => {
1122 let (sub_op, _sub_vars) =
1128 self.lower_match_pattern(pattern, None, false);
1129 let inner_root = self.plan.ops[sub_op.0 as usize].clone();
1130 Expr::Exists {
1131 pattern: Box::new(inner_root),
1132 }
1133 }
1134
1135 HirExpr::ListComprehension { .. } => {
1136 debug_assert!(
1139 false,
1140 "ListComprehension encountered in HIR→Plan lowering; \
1141 run cyrs_hir::desugar::desugar_statement (cy-mla) first"
1142 );
1143 Expr::Null
1144 }
1145
1146 HirExpr::ListPredicate {
1147 kind,
1148 var,
1149 iterable,
1150 predicate,
1151 } => Expr::ListPredicate {
1152 kind: lower_list_pred_kind(*kind),
1153 var: self.map_var(*var),
1154 iterable: Box::new(self.lower_expr(iterable)),
1155 predicate: predicate.as_ref().map(|p| Box::new(self.lower_expr(p))),
1156 },
1157
1158 HirExpr::MapProjection { .. } => {
1159 debug_assert!(
1162 false,
1163 "MapProjection encountered in HIR→Plan lowering; \
1164 run cyrs_hir::desugar::desugar_statement (cy-mla) first"
1165 );
1166 Expr::Null
1167 }
1168 }
1169 }
1170}
1171
1172fn synthesise_alias(expr: &HirExpr) -> SmolStr {
1178 match expr {
1179 HirExpr::Var(v) => SmolStr::new(format!("_v{}", v.0)),
1180 HirExpr::Prop { prop, .. } => prop.clone(),
1181 HirExpr::Call { name, .. } => name.clone(),
1182 _ => SmolStr::new("_"),
1183 }
1184}
1185
1186fn expr_to_var_id(expr: &HirExpr) -> Option<HirVarId> {
1188 match expr {
1189 HirExpr::Var(v) => Some(*v),
1190 _ => None,
1191 }
1192}
1193
1194#[allow(clippy::match_same_arms)]
1201fn lower_list_pred_kind(kind: HirListPredKind) -> ListPredKind {
1202 match kind {
1203 HirListPredKind::Any => ListPredKind::Any,
1204 HirListPredKind::All => ListPredKind::All,
1205 HirListPredKind::None => ListPredKind::None,
1206 HirListPredKind::Single => ListPredKind::Single,
1207 _ => ListPredKind::All,
1208 }
1209}
1210
1211fn lower_bin_op(op: cyrs_hir::BinOp) -> BinOp {
1213 match op {
1214 cyrs_hir::BinOp::Add => BinOp::Add,
1215 cyrs_hir::BinOp::Sub => BinOp::Sub,
1216 cyrs_hir::BinOp::Mul => BinOp::Mul,
1217 cyrs_hir::BinOp::Div => BinOp::Div,
1218 cyrs_hir::BinOp::Mod => BinOp::Mod,
1219 cyrs_hir::BinOp::Pow => BinOp::Pow,
1220 cyrs_hir::BinOp::Eq => BinOp::Eq,
1221 cyrs_hir::BinOp::Neq => BinOp::Neq,
1222 cyrs_hir::BinOp::Lt => BinOp::Lt,
1223 cyrs_hir::BinOp::Le => BinOp::Le,
1224 cyrs_hir::BinOp::Gt => BinOp::Gt,
1225 cyrs_hir::BinOp::Ge => BinOp::Ge,
1226 cyrs_hir::BinOp::And => BinOp::And,
1227 cyrs_hir::BinOp::Or => BinOp::Or,
1228 cyrs_hir::BinOp::Xor => BinOp::Xor,
1229 cyrs_hir::BinOp::StartsWith => BinOp::StartsWith,
1230 cyrs_hir::BinOp::EndsWith => BinOp::EndsWith,
1231 cyrs_hir::BinOp::Contains => BinOp::Contains,
1232 cyrs_hir::BinOp::RegexMatch => BinOp::RegexMatch,
1233 cyrs_hir::BinOp::Concat => BinOp::Concat,
1234 }
1235}
1236
1237fn is_aggregate_func(name: &str) -> bool {
1239 matches!(
1240 name.to_ascii_lowercase().as_str(),
1241 "count"
1242 | "sum"
1243 | "avg"
1244 | "min"
1245 | "max"
1246 | "collect"
1247 | "stdev"
1248 | "stdevp"
1249 | "percentilecont"
1250 | "percentiledisc"
1251 )
1252}
1253
1254enum CreatePair<'a> {
1258 Node {
1259 labels: Vec<SmolStr>,
1260 props: Option<&'a HirExpr>,
1261 bind: Option<HirVarId>,
1262 },
1263 Rel {
1264 from_bind: HirVarId,
1265 to_bind: HirVarId,
1266 rel_type: SmolStr,
1267 props: Option<&'a HirExpr>,
1268 bind: Option<HirVarId>,
1269 },
1270}
1271
1272fn create_pattern_pairs(part: &PatternPart) -> Vec<CreatePair<'_>> {
1277 let mut result = Vec::new();
1278 let mut node_vars: Vec<Option<HirVarId>> = Vec::new();
1279 let mut elements = part.elements.iter().peekable();
1280
1281 while let Some(elem) = elements.next() {
1282 match elem {
1283 PatternElement::Node {
1284 bind,
1285 labels,
1286 props,
1287 ..
1288 } => {
1289 node_vars.push(*bind);
1290 result.push(CreatePair::Node {
1291 labels: labels.clone(),
1292 props: props.as_ref(),
1293 bind: *bind,
1294 });
1295 }
1296 PatternElement::Rel {
1297 bind, types, props, ..
1298 } => {
1299 let Some(from_bind) = node_vars.last().copied().flatten() else {
1302 continue; };
1304
1305 let to_bind = match elements.peek() {
1307 Some(PatternElement::Node { bind: Some(v), .. }) => {
1308 let v = *v;
1309 node_vars.push(Some(v));
1310 let next = elements.next().unwrap();
1314 if let PatternElement::Node {
1315 labels,
1316 props,
1317 bind,
1318 ..
1319 } = next
1320 {
1321 result.push(CreatePair::Node {
1322 labels: labels.clone(),
1323 props: props.as_ref(),
1324 bind: *bind,
1325 });
1326 }
1327 v
1328 }
1329 _ => continue,
1332 };
1333
1334 let rel_type = types.first().cloned().unwrap_or_default();
1335 result.push(CreatePair::Rel {
1336 from_bind,
1337 to_bind,
1338 rel_type,
1339 props: props.as_ref(),
1340 bind: *bind,
1341 });
1342 }
1343 }
1344 }
1345
1346 result
1347}
1348
1349pub fn lower_union_pair(
1363 left: &Statement,
1364 right: &Statement,
1365 kind: UnionKind,
1366) -> Result<PlanStatement, PlanLowerError> {
1367 let mut left_plan = lower_statement(left)?;
1368 let right_plan = lower_statement(right)?;
1369
1370 #[allow(clippy::cast_possible_truncation)]
1374 let offset = left_plan.ops.len() as u32;
1375 #[allow(clippy::cast_possible_truncation)]
1376 let right_root = OpId(right_plan.ops.len() as u32 - 1 + offset);
1377
1378 left_plan.ops.extend(right_plan.ops);
1380 left_plan.write_ops.extend(right_plan.write_ops);
1381 for (plan_var, hir_var) in right_plan.var_map {
1383 left_plan
1384 .var_map
1385 .insert(VarId(plan_var.0 + offset), hir_var);
1386 }
1387
1388 let left_root = OpId(offset - 1);
1389 left_plan.ops.push(ReadOp::Union {
1390 left: left_root,
1391 right: right_root,
1392 kind,
1393 });
1394
1395 Ok(left_plan)
1396}
1397
1398pub fn apply_order_skip_limit(
1407 plan: &mut PlanStatement,
1408 order_keys: Vec<OrderKey>,
1409 skip: Option<Expr>,
1410 limit: Option<Expr>,
1411) {
1412 if plan.ops.is_empty() {
1413 return;
1414 }
1415 #[allow(clippy::cast_possible_truncation)]
1416 let mut root = OpId(plan.ops.len() as u32 - 1);
1417
1418 if !order_keys.is_empty() {
1419 let op = ReadOp::OrderBy {
1420 input: root,
1421 keys: order_keys,
1422 };
1423 root = plan.push(op);
1424 }
1425 if let Some(count) = skip {
1426 let op = ReadOp::Skip { input: root, count };
1427 root = plan.push(op);
1428 }
1429 if let Some(count) = limit {
1430 let op = ReadOp::Limit { input: root, count };
1431 root = plan.push(op);
1432 }
1433 let _ = root;
1434}
1435
1436#[cfg(test)]
1439mod tests {
1440 use super::*;
1441 use crate::SortDir;
1442 use cyrs_hir::desugar::desugar_statement;
1443 use cyrs_hir::lower::lower_statement as hir_lower;
1444
1445 fn plan_from(src: &str) -> PlanStatement {
1447 let hir = hir_lower(src);
1448 let hir = desugar_statement(hir);
1449 lower_statement(&hir).expect("plan_from: input HIR must be resolved and desugared")
1450 }
1451
1452 fn render(plan: &PlanStatement) -> String {
1454 use std::fmt::Write;
1455 let mut out = String::new();
1456 writeln!(out, "read_ops: {}", plan.ops.len()).unwrap();
1457 writeln!(out, "write_ops: {}", plan.write_ops.len()).unwrap();
1458 writeln!(out, "var_map_entries: {}", plan.var_map.len()).unwrap();
1459 for (i, op) in plan.ops.iter().enumerate() {
1460 writeln!(out, "op[{i}]: {}", op_tag(op)).unwrap();
1461 }
1462 for (i, wop) in plan.write_ops.iter().enumerate() {
1463 writeln!(out, "write[{i}]: {}", write_op_tag(wop)).unwrap();
1464 }
1465 out
1466 }
1467
1468 fn op_tag(op: &ReadOp) -> String {
1469 match op {
1470 ReadOp::Source { label, bind } => format!(
1471 "Source(label={}, bind={})",
1472 label
1473 .as_ref()
1474 .map_or("None".into(), |l| format!("{:?}", l.0)),
1475 bind.0
1476 ),
1477 ReadOp::Expand {
1478 from,
1479 bind_rel,
1480 bind_to,
1481 ..
1482 } => {
1483 format!(
1484 "Expand(from={}, bind_rel={}, bind_to={})",
1485 from.0, bind_rel.0, bind_to.0
1486 )
1487 }
1488 ReadOp::Filter { input, .. } => format!("Filter(input={})", input.0),
1489 ReadOp::Project { input, items } => {
1490 format!("Project(input={}, cols={})", input.0, items.len())
1491 }
1492 ReadOp::Aggregate { input, keys, aggs } => {
1493 format!(
1494 "Aggregate(input={}, keys={}, aggs={})",
1495 input.0,
1496 keys.len(),
1497 aggs.len()
1498 )
1499 }
1500 ReadOp::OrderBy { input, keys } => {
1501 format!("OrderBy(input={}, keys={})", input.0, keys.len())
1502 }
1503 ReadOp::Skip { input, .. } => format!("Skip(input={})", input.0),
1504 ReadOp::Limit { input, .. } => format!("Limit(input={})", input.0),
1505 ReadOp::Distinct { input } => format!("Distinct(input={})", input.0),
1506 ReadOp::Unwind { input, bind, .. } => {
1507 format!("Unwind(input={}, bind={})", input.0, bind.0)
1508 }
1509 ReadOp::Union { left, right, kind } => {
1510 format!("Union(left={}, right={}, kind={:?})", left.0, right.0, kind)
1511 }
1512 ReadOp::With {
1513 input,
1514 items,
1515 filter,
1516 } => {
1517 format!(
1518 "With(input={}, cols={}, has_filter={})",
1519 input.0,
1520 items.len(),
1521 filter.is_some()
1522 )
1523 }
1524 ReadOp::OptionalJoin { input, .. } => format!("OptionalJoin(input={})", input.0),
1525 }
1526 }
1527
1528 fn write_op_tag(op: &WriteOp) -> String {
1529 match op {
1530 WriteOp::CreateNode { labels, bind, .. } => {
1531 format!(
1532 "CreateNode(labels={:?}, bind={:?})",
1533 labels,
1534 bind.map(|v| v.0)
1535 )
1536 }
1537 WriteOp::CreateRel { rel_type, bind, .. } => {
1538 format!("CreateRel(type={rel_type}, bind={:?})", bind.map(|v| v.0))
1539 }
1540 WriteOp::MergeNode { labels, bind, .. } => {
1541 format!(
1542 "MergeNode(labels={:?}, bind={:?})",
1543 labels,
1544 bind.map(|v| v.0)
1545 )
1546 }
1547 WriteOp::MergeRel { rel_type, bind, .. } => {
1548 format!("MergeRel(type={rel_type}, bind={:?})", bind.map(|v| v.0))
1549 }
1550 WriteOp::SetProperty { target, prop, .. } => {
1551 format!("SetProperty(target={}, prop={prop})", target.0)
1552 }
1553 WriteOp::SetLabels { target, labels } => {
1554 format!("SetLabels(target={}, labels={:?})", target.0, labels)
1555 }
1556 WriteOp::RemoveProperty { target, prop } => {
1557 format!("RemoveProperty(target={}, prop={prop})", target.0)
1558 }
1559 WriteOp::RemoveLabels { target, labels } => {
1560 format!("RemoveLabels(target={}, labels={:?})", target.0, labels)
1561 }
1562 WriteOp::Delete { detach, targets } => {
1563 format!("Delete(detach={detach}, targets={})", targets.len())
1564 }
1565 }
1566 }
1567
1568 #[test]
1572 fn snap_single_match() {
1573 let plan = plan_from("MATCH (n) RETURN n");
1574 insta::assert_snapshot!("plan_single_match", render(&plan));
1575 }
1576
1577 #[test]
1579 fn snap_match_with_label() {
1580 let plan = plan_from("MATCH (n:Person) RETURN n");
1581 insta::assert_snapshot!("plan_match_with_label", render(&plan));
1582 }
1583
1584 #[test]
1586 fn snap_match_where() {
1587 let plan = plan_from("MATCH (n) WHERE n.age > 18 RETURN n");
1588 insta::assert_snapshot!("plan_match_where", render(&plan));
1589 }
1590
1591 #[test]
1595 fn snap_match_where_pretty_tree() {
1596 use crate::pretty::pretty;
1597 let plan = plan_from("MATCH (a) WHERE a.x = 1 RETURN a");
1598 insta::assert_snapshot!("plan_match_where_pretty_tree", pretty(&plan));
1599 }
1600
1601 #[test]
1603 fn snap_match_with() {
1604 let plan = plan_from("MATCH (n) WITH n RETURN n");
1605 insta::assert_snapshot!("plan_match_with", render(&plan));
1606 }
1607
1608 #[test]
1610 fn snap_match_return_projection() {
1611 let plan = plan_from("MATCH (n:Person) RETURN n.name, n.age");
1612 insta::assert_snapshot!("plan_match_return_projection", render(&plan));
1613 }
1614
1615 #[test]
1617 fn snap_return_distinct() {
1618 let plan = plan_from("MATCH (n) RETURN DISTINCT n.name");
1619 insta::assert_snapshot!("plan_return_distinct", render(&plan));
1620 }
1621
1622 #[test]
1626 fn snap_unwind() {
1627 use cyrs_hir::{
1628 Binding, Clause, Expr as HirExpr, HirSpan, Statement, VarId as HirVarId, VarKind,
1629 };
1630 let span = HirSpan::default();
1631 let mut stmt = Statement::new(span);
1632 let x_var = HirVarId(0);
1637 stmt.bindings.insert(
1638 x_var,
1639 Binding {
1640 id: x_var,
1641 name: "x".into(),
1642 kind: VarKind::Value,
1643 defined_at: span,
1644 },
1645 );
1646 stmt.clauses.push(Clause::Unwind {
1647 id: cyrs_hir::HirId::DUMMY,
1648 list: HirExpr::List(vec![HirExpr::Int(1), HirExpr::Int(2), HirExpr::Int(3)]),
1649 bind: x_var,
1650 span,
1651 });
1652 stmt.clauses.push(Clause::Return {
1653 id: cyrs_hir::HirId::DUMMY,
1654 projections: vec![cyrs_hir::Projection {
1655 expr: HirExpr::Var(x_var),
1656 alias: Some("x".into()),
1657 span,
1658 }],
1659 distinct: false,
1660 span,
1661 });
1662 let plan = lower_statement(&stmt).expect("manually-built HIR must be resolved");
1663 insta::assert_snapshot!("plan_unwind", render(&plan));
1664 }
1665
1666 #[test]
1668 fn snap_create_node() {
1669 let plan = plan_from("CREATE (n:Person)");
1670 insta::assert_snapshot!("plan_create_node", render(&plan));
1671 }
1672
1673 #[test]
1675 fn snap_create_rel() {
1676 let plan = plan_from("MATCH (a:Person), (b:Person) CREATE (a)-[:KNOWS]->(b)");
1677 insta::assert_snapshot!("plan_create_rel", render(&plan));
1678 }
1679
1680 #[test]
1682 fn snap_merge_node() {
1683 let plan = plan_from("MERGE (n:Person {name: 'Alice'})");
1684 insta::assert_snapshot!("plan_merge_node", render(&plan));
1685 }
1686
1687 #[test]
1689 fn snap_set_property() {
1690 let plan = plan_from("MATCH (n:Person) SET n.age = 30");
1691 insta::assert_snapshot!("plan_set_property", render(&plan));
1692 }
1693
1694 #[test]
1696 fn snap_remove_label() {
1697 let plan = plan_from("MATCH (n:Person) REMOVE n:Person");
1698 insta::assert_snapshot!("plan_remove_label", render(&plan));
1699 }
1700
1701 #[test]
1703 fn snap_delete() {
1704 let plan = plan_from("MATCH (n) DELETE n");
1705 insta::assert_snapshot!("plan_delete", render(&plan));
1706 }
1707
1708 #[test]
1710 fn snap_detach_delete() {
1711 let plan = plan_from("MATCH (n) DETACH DELETE n");
1712 insta::assert_snapshot!("plan_detach_delete", render(&plan));
1713 }
1714
1715 #[test]
1717 fn snap_aggregation_count() {
1718 let plan = plan_from("MATCH (n) RETURN count(n)");
1719 insta::assert_snapshot!("plan_aggregation_count", render(&plan));
1720 }
1721
1722 #[test]
1724 fn snap_aggregation_sum() {
1725 let plan = plan_from("MATCH (n) RETURN sum(n.age)");
1726 insta::assert_snapshot!("plan_aggregation_sum", render(&plan));
1727 }
1728
1729 #[test]
1731 fn snap_union_all() {
1732 let left_hir = desugar_statement(hir_lower("MATCH (n:Person) RETURN n"));
1733 let right_hir = desugar_statement(hir_lower("MATCH (n:Animal) RETURN n"));
1734 let plan = lower_union_pair(&left_hir, &right_hir, UnionKind::All)
1735 .expect("UNION arms must be resolved/desugared");
1736 insta::assert_snapshot!("plan_union_all", render(&plan));
1737 }
1738
1739 #[test]
1741 fn snap_union_distinct() {
1742 let left_hir = desugar_statement(hir_lower("MATCH (n:Person) RETURN n"));
1743 let right_hir = desugar_statement(hir_lower("MATCH (n:Animal) RETURN n"));
1744 let plan = lower_union_pair(&left_hir, &right_hir, UnionKind::Distinct)
1745 .expect("UNION arms must be resolved/desugared");
1746 insta::assert_snapshot!("plan_union_distinct", render(&plan));
1747 }
1748
1749 #[test]
1751 fn snap_optional_match() {
1752 let plan = plan_from("MATCH (n) OPTIONAL MATCH (n)-[:KNOWS]->(m) RETURN n, m");
1753 insta::assert_snapshot!("plan_optional_match", render(&plan));
1754 }
1755
1756 #[test]
1758 fn snap_match_rel_chain() {
1759 let plan = plan_from("MATCH (a)-[:KNOWS]->(b) RETURN a, b");
1760 insta::assert_snapshot!("plan_match_rel_chain", render(&plan));
1761 }
1762
1763 #[test]
1765 fn snap_order_skip_limit() {
1766 let mut plan = plan_from("MATCH (n) RETURN n");
1767 apply_order_skip_limit(
1768 &mut plan,
1769 vec![OrderKey {
1770 expr: Expr::Var(VarId(0)),
1771 dir: SortDir::Desc,
1772 }],
1773 Some(Expr::Int(10)),
1774 Some(Expr::Int(5)),
1775 );
1776 insta::assert_snapshot!("plan_order_skip_limit", render(&plan));
1777 }
1778
1779 #[test]
1782 fn plan_lowering_is_deterministic() {
1783 let plan1 = plan_from("MATCH (n:Person) WHERE n.age > 18 RETURN n.name, n.age");
1784 let plan2 = plan_from("MATCH (n:Person) WHERE n.age > 18 RETURN n.name, n.age");
1785 assert_eq!(render(&plan1), render(&plan2));
1786 }
1787
1788 #[test]
1791 fn single_match_returns_source_and_project() {
1792 let plan = plan_from("MATCH (n) RETURN n");
1793 assert!(plan.ops.len() >= 2);
1794 assert!(matches!(plan.ops[0], ReadOp::Source { .. }));
1795 assert!(matches!(plan.ops.last(), Some(ReadOp::Project { .. })));
1796 }
1797
1798 #[test]
1799 fn match_where_inserts_filter() {
1800 let plan = plan_from("MATCH (n) WHERE n.age > 18 RETURN n");
1801 let has_filter = plan
1802 .ops
1803 .iter()
1804 .any(|op| matches!(op, ReadOp::Filter { .. }));
1805 assert!(has_filter, "expected Filter op in plan");
1806 }
1807
1808 #[test]
1809 fn create_node_emits_write_op() {
1810 use cyrs_hir::{
1813 Binding, Clause, HirSpan, Pattern, PatternElement, PatternPart, Statement,
1814 VarId as HirVarId, VarKind,
1815 };
1816 let span = HirSpan::default();
1817 let mut stmt = Statement::new(span);
1818 let n_var = HirVarId(0);
1819 stmt.bindings.insert(
1820 n_var,
1821 Binding {
1822 id: n_var,
1823 name: "n".into(),
1824 kind: VarKind::Node,
1825 defined_at: span,
1826 },
1827 );
1828 stmt.clauses.push(Clause::Create {
1829 id: cyrs_hir::HirId::DUMMY,
1830 pattern: Pattern {
1831 parts: vec![PatternPart {
1832 named_as: None,
1833 elements: vec![PatternElement::Node {
1834 id: cyrs_hir::HirId::DUMMY,
1835 bind: Some(n_var),
1836 labels: vec!["Person".into()],
1837 props: None,
1838 span,
1839 }],
1840 }],
1841 },
1842 span,
1843 });
1844 let plan = lower_statement(&stmt).expect("manually-built HIR must be resolved");
1845 assert!(
1846 plan.write_ops
1847 .iter()
1848 .any(|w| matches!(w, WriteOp::CreateNode { .. })),
1849 "expected CreateNode write op; write_ops={:?}",
1850 plan.write_ops.iter().map(write_op_tag).collect::<Vec<_>>()
1851 );
1852 }
1853
1854 #[test]
1855 fn delete_emits_write_op() {
1856 use cyrs_hir::{
1858 Binding, Clause, Expr as HirExpr, HirSpan, Pattern, PatternElement, PatternPart,
1859 Statement, VarId as HirVarId, VarKind,
1860 };
1861 let span = HirSpan::default();
1862 let mut stmt = Statement::new(span);
1863 let n_var = HirVarId(0);
1864 stmt.bindings.insert(
1865 n_var,
1866 Binding {
1867 id: n_var,
1868 name: "n".into(),
1869 kind: VarKind::Node,
1870 defined_at: span,
1871 },
1872 );
1873 stmt.clauses.push(Clause::Match {
1874 id: cyrs_hir::HirId::DUMMY,
1875 optional: false,
1876 pattern: Pattern {
1877 parts: vec![PatternPart {
1878 named_as: None,
1879 elements: vec![PatternElement::Node {
1880 id: cyrs_hir::HirId::DUMMY,
1881 bind: Some(n_var),
1882 labels: vec![],
1883 props: None,
1884 span,
1885 }],
1886 }],
1887 },
1888 span,
1889 });
1890 stmt.clauses.push(Clause::Delete {
1891 id: cyrs_hir::HirId::DUMMY,
1892 targets: vec![HirExpr::Var(n_var)],
1893 detach: false,
1894 span,
1895 });
1896 let plan = lower_statement(&stmt).expect("manually-built HIR must be resolved");
1897 assert!(
1898 plan.write_ops
1899 .iter()
1900 .any(|w| matches!(w, WriteOp::Delete { detach: false, .. })),
1901 "expected Delete(detach=false) write op"
1902 );
1903 }
1904
1905 #[test]
1906 fn detach_delete_emits_write_op() {
1907 use cyrs_hir::{
1910 Binding, Clause, Expr as HirExpr, HirSpan, Pattern, PatternElement, PatternPart,
1911 Statement, VarId as HirVarId, VarKind,
1912 };
1913 let span = HirSpan::default();
1914 let mut stmt = Statement::new(span);
1915 let n_var = HirVarId(0);
1916 stmt.bindings.insert(
1917 n_var,
1918 Binding {
1919 id: n_var,
1920 name: "n".into(),
1921 kind: VarKind::Node,
1922 defined_at: span,
1923 },
1924 );
1925 stmt.clauses.push(Clause::Match {
1926 id: cyrs_hir::HirId::DUMMY,
1927 optional: false,
1928 pattern: Pattern {
1929 parts: vec![PatternPart {
1930 named_as: None,
1931 elements: vec![PatternElement::Node {
1932 id: cyrs_hir::HirId::DUMMY,
1933 bind: Some(n_var),
1934 labels: vec![],
1935 props: None,
1936 span,
1937 }],
1938 }],
1939 },
1940 span,
1941 });
1942 stmt.clauses.push(Clause::Delete {
1943 id: cyrs_hir::HirId::DUMMY,
1944 targets: vec![HirExpr::Var(n_var)],
1945 detach: true,
1946 span,
1947 });
1948 let plan = lower_statement(&stmt).expect("manually-built HIR must be resolved");
1949 assert!(
1950 plan.write_ops
1951 .iter()
1952 .any(|w| matches!(w, WriteOp::Delete { detach: true, .. })),
1953 "expected Delete(detach=true) write op"
1954 );
1955 }
1956
1957 #[test]
1958 fn var_map_populated_for_bound_variables() {
1959 let plan = plan_from("MATCH (n) RETURN n");
1960 assert!(
1961 !plan.var_map.is_empty(),
1962 "var_map should be populated for bound variables"
1963 );
1964 }
1965
1966 #[test]
1969 fn with_where_threads_filter_into_plan() {
1970 let plan = plan_from(
1971 "MATCH (a) UNWIND a.aliases AS alias \
1972 WITH a, alias WHERE alias CONTAINS 'Fancy' \
1973 RETURN DISTINCT a.canonical_name",
1974 );
1975 let has_with_filter = plan.ops.iter().any(|op| {
1976 matches!(
1977 op,
1978 ReadOp::With {
1979 filter: Some(_),
1980 ..
1981 }
1982 )
1983 });
1984 assert!(
1985 has_with_filter,
1986 "expected ReadOp::With with a Some(filter); plan ops = {:#?}",
1987 plan.ops
1988 );
1989 }
1990
1991 fn stmt_with_return_expr(expr: HirExpr) -> Statement {
1995 use cyrs_hir::HirSpan;
1996 let span = HirSpan::default();
1997 let mut stmt = Statement::new(span);
1998 stmt.clauses.push(Clause::Return {
1999 id: cyrs_hir::HirId::DUMMY,
2000 projections: vec![Projection {
2001 expr,
2002 alias: Some("x".into()),
2003 span,
2004 }],
2005 distinct: false,
2006 span,
2007 });
2008 stmt
2009 }
2010
2011 #[test]
2014 fn lower_statement_returns_err_on_unresolved_name() {
2015 let stmt = stmt_with_return_expr(HirExpr::Unresolved("foo".into()));
2016 let err = lower_statement(&stmt).expect_err("unresolved name must be rejected");
2017 match err {
2018 PlanLowerError::UnresolvedName { name, .. } => assert_eq!(name, "foo"),
2019 other => panic!("expected UnresolvedName, got {other:?}"),
2020 }
2021 }
2022
2023 #[test]
2025 fn lower_statement_returns_err_on_listcomp() {
2026 let expr = HirExpr::ListComprehension {
2027 filter_var: HirVarId(0),
2028 iterable: Box::new(HirExpr::List(vec![HirExpr::Int(1)])),
2029 filter: None,
2030 map_expr: Box::new(HirExpr::Var(HirVarId(0))),
2031 };
2032 let stmt = stmt_with_return_expr(expr);
2033 let err = lower_statement(&stmt).expect_err("list comprehension must be rejected");
2034 match err {
2035 PlanLowerError::UndesugaredExpr { kind, .. } => assert_eq!(kind, "ListComprehension"),
2036 other => panic!("expected UndesugaredExpr(ListComprehension), got {other:?}"),
2037 }
2038 }
2039
2040 #[test]
2042 fn lower_statement_returns_err_on_mapprojection() {
2043 let expr = HirExpr::MapProjection {
2044 base: Box::new(HirExpr::Var(HirVarId(0))),
2045 items: vec![],
2046 };
2047 let stmt = stmt_with_return_expr(expr);
2048 let err = lower_statement(&stmt).expect_err("map projection must be rejected");
2049 match err {
2050 PlanLowerError::UndesugaredExpr { kind, .. } => assert_eq!(kind, "MapProjection"),
2051 other => panic!("expected UndesugaredExpr(MapProjection), got {other:?}"),
2052 }
2053 }
2054
2055 #[test]
2061 fn lower_statement_returns_err_on_unresolved_inside_patternpredicate() {
2062 let element = PatternElement::Node {
2063 id: cyrs_hir::HirId::DUMMY,
2064 bind: None,
2065 labels: vec![],
2066 props: Some(HirExpr::Map(vec![(
2067 "k".into(),
2068 HirExpr::Unresolved("vaext".into()),
2069 )])),
2070 span: HirSpan::default(),
2071 };
2072 let pattern = cyrs_hir::Pattern {
2073 parts: vec![PatternPart {
2074 named_as: None,
2075 elements: vec![element],
2076 }],
2077 };
2078 let stmt = stmt_with_return_expr(HirExpr::PatternPredicate(pattern));
2079 let err = lower_statement(&stmt)
2080 .expect_err("unresolved name inside PatternPredicate must be rejected");
2081 match err {
2082 PlanLowerError::UnresolvedName { name, .. } => assert_eq!(name, "vaext"),
2083 other => panic!("expected UnresolvedName, got {other:?}"),
2084 }
2085 }
2086
2087 #[test]
2095 fn lower_statement_no_panic_on_unresolved_inside_patternpredicate_text() {
2096 let s = "MATCH (n) WHERE (n {k: vaext})-->() RETURN n\n";
2097 let stmt = hir_lower(s);
2098 let stmt = desugar_statement(stmt);
2099 let _ = lower_statement(&stmt);
2101 }
2102
2103 #[test]
2108 fn lower_statement_accepts_patternpredicate_as_exists() {
2109 let expr = HirExpr::PatternPredicate(cyrs_hir::Pattern { parts: vec![] });
2110 let stmt = stmt_with_return_expr(expr);
2111 let plan = lower_statement(&stmt).expect("pattern predicate must lower to Exists");
2112 let mut saw_exists = false;
2114 for op in &plan.ops {
2115 if let ReadOp::Project { items, .. } = op {
2116 for item in items {
2117 if matches!(item.expr, Expr::Exists { .. }) {
2118 saw_exists = true;
2119 }
2120 }
2121 }
2122 }
2123 assert!(
2124 saw_exists,
2125 "expected plan to carry Expr::Exists after PatternPredicate lowering, got {plan:?}"
2126 );
2127 }
2128}