1use crate::ast::*;
12use crate::error::{GraphError, Result};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
18pub enum LogicalOperator {
19 ScanByLabel {
21 variable: String,
22 label: String,
23 properties: HashMap<String, PropertyValue>,
24 },
25
26 Unwind {
28 input: Option<Box<LogicalOperator>>,
30 expression: ValueExpression,
32 alias: String,
34 },
35
36 Filter {
38 input: Box<LogicalOperator>,
39 predicate: BooleanExpression,
40 },
41
42 Expand {
46 input: Box<LogicalOperator>,
48 source_variable: String,
50 target_variable: String,
52 target_label: String,
55 relationship_types: Vec<String>,
57 direction: RelationshipDirection,
59 relationship_variable: Option<String>,
61 properties: HashMap<String, PropertyValue>,
63 target_properties: HashMap<String, PropertyValue>,
65 },
66
67 VariableLengthExpand {
72 input: Box<LogicalOperator>,
74 source_variable: String,
76 target_variable: String,
78 relationship_types: Vec<String>,
80 direction: RelationshipDirection,
82 relationship_variable: Option<String>,
84 min_length: Option<u32>,
86 max_length: Option<u32>,
88 target_properties: HashMap<String, PropertyValue>,
90 },
91
92 Project {
94 input: Box<LogicalOperator>,
95 projections: Vec<ProjectionItem>,
96 },
97
98 Join {
100 left: Box<LogicalOperator>,
101 right: Box<LogicalOperator>,
102 join_type: JoinType,
103 },
104
105 Distinct { input: Box<LogicalOperator> },
107
108 Sort {
110 input: Box<LogicalOperator>,
111 sort_items: Vec<SortItem>,
112 },
113
114 Offset {
116 input: Box<LogicalOperator>,
117 offset: u64,
118 },
119
120 Limit {
122 input: Box<LogicalOperator>,
123 count: u64,
124 },
125}
126
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129pub struct ProjectionItem {
130 pub expression: ValueExpression,
131 pub alias: Option<String>,
132}
133
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub enum JoinType {
137 Inner,
138 Left,
139 Right,
140 Full,
141 Cross,
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
146pub struct SortItem {
147 pub expression: ValueExpression,
148 pub direction: SortDirection,
149}
150
151pub struct LogicalPlanner {
153 variables: HashMap<String, String>, }
156
157impl LogicalPlanner {
158 pub fn new() -> Self {
159 Self {
160 variables: HashMap::new(),
161 }
162 }
163
164 pub fn plan(&mut self, query: &CypherQuery) -> Result<LogicalOperator> {
166 let mut plan = self.plan_reading_clauses(None, &query.reading_clauses)?;
168
169 if let Some(where_clause) = &query.where_clause {
171 plan = LogicalOperator::Filter {
172 input: Box::new(plan),
173 predicate: where_clause.expression.clone(),
174 };
175 }
176
177 if let Some(with_clause) = &query.with_clause {
179 plan = self.plan_with_clause(with_clause, plan)?;
180 }
181
182 if !query.post_with_reading_clauses.is_empty() {
184 plan = self.plan_reading_clauses(Some(plan), &query.post_with_reading_clauses)?;
185 }
186
187 if let Some(post_where) = &query.post_with_where_clause {
189 plan = LogicalOperator::Filter {
190 input: Box::new(plan),
191 predicate: post_where.expression.clone(),
192 };
193 }
194
195 plan = self.plan_return_clause(&query.return_clause, plan)?;
197
198 if let Some(order_by) = &query.order_by {
200 plan = LogicalOperator::Sort {
201 input: Box::new(plan),
202 sort_items: order_by
203 .items
204 .iter()
205 .map(|item| SortItem {
206 expression: item.expression.clone(),
207 direction: item.direction.clone(),
208 })
209 .collect(),
210 };
211 }
212
213 if let Some(skip) = query.skip {
215 plan = LogicalOperator::Offset {
216 input: Box::new(plan),
217 offset: skip,
218 };
219 }
220
221 if let Some(limit) = query.limit {
223 plan = LogicalOperator::Limit {
224 input: Box::new(plan),
225 count: limit,
226 };
227 }
228
229 Ok(plan)
230 }
231
232 fn plan_reading_clauses(
233 &mut self,
234 base_plan: Option<LogicalOperator>,
235 reading_clauses: &[ReadingClause],
236 ) -> Result<LogicalOperator> {
237 let mut plan = base_plan;
238
239 if reading_clauses.is_empty() && plan.is_none() {
240 return Err(GraphError::PlanError {
241 message: "Query must have at least one MATCH or UNWIND clause".to_string(),
242 location: snafu::Location::new(file!(), line!(), column!()),
243 });
244 }
245
246 for clause in reading_clauses {
247 plan = Some(self.plan_reading_clause_with_base(plan, clause)?);
248 }
249
250 plan.ok_or_else(|| GraphError::PlanError {
251 message: "Failed to plan clauses".to_string(),
252 location: snafu::Location::new(file!(), line!(), column!()),
253 })
254 }
255
256 fn plan_reading_clause_with_base(
258 &mut self,
259 base: Option<LogicalOperator>,
260 clause: &ReadingClause,
261 ) -> Result<LogicalOperator> {
262 match clause {
263 ReadingClause::Match(match_clause) => {
264 self.plan_match_clause_with_base(base, match_clause)
265 }
266 ReadingClause::Unwind(unwind_clause) => {
267 self.plan_unwind_clause_with_base(base, unwind_clause)
268 }
269 }
270 }
271
272 fn plan_unwind_clause_with_base(
274 &mut self,
275 base: Option<LogicalOperator>,
276 unwind_clause: &UnwindClause,
277 ) -> Result<LogicalOperator> {
278 self.variables
280 .insert(unwind_clause.alias.clone(), "Unwound".to_string());
281
282 Ok(LogicalOperator::Unwind {
283 input: base.map(Box::new),
284 expression: unwind_clause.expression.clone(),
285 alias: unwind_clause.alias.clone(),
286 })
287 }
288
289 fn plan_match_clause_with_base(
291 &mut self,
292 base: Option<LogicalOperator>,
293 match_clause: &MatchClause,
294 ) -> Result<LogicalOperator> {
295 if match_clause.patterns.is_empty() {
296 return Err(GraphError::PlanError {
297 message: "MATCH clause must have at least one pattern".to_string(),
298 location: snafu::Location::new(file!(), line!(), column!()),
299 });
300 }
301
302 let mut plan = base;
303 for pattern in &match_clause.patterns {
304 match pattern {
305 GraphPattern::Node(node) => {
306 let already_bound = node
307 .variable
308 .as_deref()
309 .is_some_and(|v| self.variables.contains_key(v));
310
311 match (already_bound, plan.as_ref()) {
312 (true, _) => { }
313 (false, None) => plan = Some(self.plan_node_scan(node)?),
314 (false, Some(_)) => {
315 let right = self.plan_node_scan(node)?;
316 plan = Some(LogicalOperator::Join {
317 left: Box::new(plan.unwrap()),
318 right: Box::new(right),
319 join_type: JoinType::Cross, });
321 }
322 }
323 }
324 GraphPattern::Path(path) => plan = Some(self.plan_path(plan, path)?),
325 }
326 }
327
328 plan.ok_or_else(|| GraphError::PlanError {
329 message: "Failed to plan MATCH clause".to_string(),
330 location: snafu::Location::new(file!(), line!(), column!()),
331 })
332 }
333
334 fn plan_node_scan(&mut self, node: &NodePattern) -> Result<LogicalOperator> {
336 let variable = node
337 .variable
338 .clone()
339 .unwrap_or_else(|| format!("_node_{}", self.variables.len()));
340
341 let label = node
342 .labels
343 .first()
344 .cloned()
345 .unwrap_or_else(|| "Node".to_string());
346
347 self.variables.insert(variable.clone(), label.clone());
349
350 Ok(LogicalOperator::ScanByLabel {
351 variable,
352 label,
353 properties: node.properties.clone(),
354 })
355 }
356
357 fn plan_path(
361 &mut self,
362 base: Option<LogicalOperator>,
363 path: &PathPattern,
364 ) -> Result<LogicalOperator> {
365 let mut plan = if let Some(p) = base {
367 p
368 } else {
369 self.plan_node_scan(&path.start_node)?
370 };
371
372 let mut current_src = match &path.start_node.variable {
374 Some(var) => var.clone(),
375 None => self.extract_variable_from_plan(&plan)?,
376 };
377
378 for segment in &path.segments {
380 let target_variable = segment
382 .end_node
383 .variable
384 .clone()
385 .unwrap_or_else(|| format!("_node_{}", self.variables.len()));
386
387 let target_label = segment
388 .end_node
389 .labels
390 .first()
391 .cloned()
392 .unwrap_or_else(|| "Node".to_string());
393 self.variables
394 .insert(target_variable.clone(), target_label.clone());
395
396 let next_plan = match segment.relationship.length.as_ref() {
398 Some(length_range)
399 if length_range.min == Some(1) && length_range.max == Some(1) =>
400 {
401 LogicalOperator::Expand {
402 input: Box::new(plan),
403 source_variable: current_src.clone(),
404 target_variable: target_variable.clone(),
405 target_label: target_label.clone(),
406 relationship_types: segment.relationship.types.clone(),
407 direction: segment.relationship.direction.clone(),
408 relationship_variable: segment.relationship.variable.clone(),
409 properties: segment.relationship.properties.clone(),
410 target_properties: segment.end_node.properties.clone(),
411 }
412 }
413 Some(length_range) => LogicalOperator::VariableLengthExpand {
414 input: Box::new(plan),
415 source_variable: current_src.clone(),
416 target_variable: target_variable.clone(),
417 relationship_types: segment.relationship.types.clone(),
418 direction: segment.relationship.direction.clone(),
419 relationship_variable: segment.relationship.variable.clone(),
420 min_length: length_range.min,
421 max_length: length_range.max,
422 target_properties: segment.end_node.properties.clone(),
423 },
424 None => LogicalOperator::Expand {
425 input: Box::new(plan),
426 source_variable: current_src.clone(),
427 target_variable: target_variable.clone(),
428 target_label: target_label.clone(),
429 relationship_types: segment.relationship.types.clone(),
430 direction: segment.relationship.direction.clone(),
431 relationship_variable: segment.relationship.variable.clone(),
432 properties: segment.relationship.properties.clone(),
433 target_properties: segment.end_node.properties.clone(),
434 },
435 };
436
437 plan = next_plan;
438 current_src = target_variable;
439 }
440
441 Ok(plan)
442 }
443
444 #[allow(clippy::only_used_in_recursion)]
446 fn extract_variable_from_plan(&self, plan: &LogicalOperator) -> Result<String> {
447 match plan {
448 LogicalOperator::ScanByLabel { variable, .. } => Ok(variable.clone()),
449 LogicalOperator::Unwind { alias, .. } => Ok(alias.clone()),
450 LogicalOperator::Expand {
451 target_variable, ..
452 } => Ok(target_variable.clone()),
453 LogicalOperator::VariableLengthExpand {
454 target_variable, ..
455 } => Ok(target_variable.clone()),
456 LogicalOperator::Filter { input, .. } => self.extract_variable_from_plan(input),
457 LogicalOperator::Project { input, .. } => self.extract_variable_from_plan(input),
458 LogicalOperator::Distinct { input } => self.extract_variable_from_plan(input),
459 LogicalOperator::Sort { input, .. } => self.extract_variable_from_plan(input),
460 LogicalOperator::Offset { input, .. } => self.extract_variable_from_plan(input),
461 LogicalOperator::Limit { input, .. } => self.extract_variable_from_plan(input),
462 LogicalOperator::Join { left, right, .. } => {
463 self.extract_variable_from_plan(right)
465 .or_else(|_| self.extract_variable_from_plan(left))
466 }
467 }
468 }
469
470 fn plan_return_clause(
472 &self,
473 return_clause: &ReturnClause,
474 input: LogicalOperator,
475 ) -> Result<LogicalOperator> {
476 let projections = return_clause
477 .items
478 .iter()
479 .map(|item| ProjectionItem {
480 expression: item.expression.clone(),
481 alias: item.alias.clone(),
482 })
483 .collect();
484
485 let mut plan = LogicalOperator::Project {
486 input: Box::new(input),
487 projections,
488 };
489
490 if return_clause.distinct {
492 plan = LogicalOperator::Distinct {
493 input: Box::new(plan),
494 };
495 }
496
497 Ok(plan)
498 }
499
500 fn plan_with_clause(
502 &self,
503 with_clause: &WithClause,
504 input: LogicalOperator,
505 ) -> Result<LogicalOperator> {
506 let projections = with_clause
508 .items
509 .iter()
510 .map(|item| ProjectionItem {
511 expression: item.expression.clone(),
512 alias: item.alias.clone(),
513 })
514 .collect();
515
516 let mut plan = LogicalOperator::Project {
517 input: Box::new(input),
518 projections,
519 };
520
521 if let Some(order_by) = &with_clause.order_by {
523 plan = LogicalOperator::Sort {
524 input: Box::new(plan),
525 sort_items: order_by
526 .items
527 .iter()
528 .map(|item| SortItem {
529 expression: item.expression.clone(),
530 direction: item.direction.clone(),
531 })
532 .collect(),
533 };
534 }
535
536 if let Some(limit) = with_clause.limit {
538 plan = LogicalOperator::Limit {
539 input: Box::new(plan),
540 count: limit,
541 };
542 }
543
544 Ok(plan)
545 }
546}
547
548impl Default for LogicalPlanner {
549 fn default() -> Self {
550 Self::new()
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use crate::parser::parse_cypher_query;
558
559 #[test]
560 fn test_relationship_query_logical_plan_structure() {
561 let query_text = r#"MATCH (p:Person {name: "Alice"})-[:KNOWS]->(f:Person) WHERE f.age > 30 RETURN f.name"#;
562
563 let ast = parse_cypher_query(query_text).unwrap();
565
566 let mut planner = LogicalPlanner::new();
568 let logical_plan = planner.plan(&ast).unwrap();
569
570 match &logical_plan {
572 LogicalOperator::Project { input, projections } => {
573 assert_eq!(projections.len(), 1);
575 match &projections[0].expression {
576 ValueExpression::Property(prop_ref) => {
577 assert_eq!(prop_ref.variable, "f");
578 assert_eq!(prop_ref.property, "name");
579 }
580 _ => panic!("Expected property reference for f.name"),
581 }
582
583 match input.as_ref() {
585 LogicalOperator::Filter {
586 predicate,
587 input: filter_input,
588 } => {
589 match predicate {
591 BooleanExpression::Comparison {
592 left,
593 operator,
594 right,
595 } => {
596 match left {
597 ValueExpression::Property(prop_ref) => {
598 assert_eq!(prop_ref.variable, "f");
599 assert_eq!(prop_ref.property, "age");
600 }
601 _ => panic!("Expected property reference for f.age"),
602 }
603 assert_eq!(*operator, ComparisonOperator::GreaterThan);
604 match right {
605 ValueExpression::Literal(PropertyValue::Integer(val)) => {
606 assert_eq!(*val, 30);
607 }
608 _ => panic!("Expected integer literal 30"),
609 }
610 }
611 _ => panic!("Expected comparison expression"),
612 }
613
614 match filter_input.as_ref() {
616 LogicalOperator::Expand {
617 input: expand_input,
618 source_variable,
619 target_variable,
620 relationship_types,
621 direction,
622 ..
623 } => {
624 assert_eq!(source_variable, "p");
625 assert_eq!(target_variable, "f");
626 assert_eq!(relationship_types, &vec!["KNOWS".to_string()]);
627 assert_eq!(*direction, RelationshipDirection::Outgoing);
628
629 match expand_input.as_ref() {
631 LogicalOperator::ScanByLabel {
632 variable,
633 label,
634 properties,
635 } => {
636 assert_eq!(variable, "p");
637 assert_eq!(label, "Person");
638
639 assert_eq!(properties.len(), 1);
641 match properties.get("name") {
642 Some(PropertyValue::String(val)) => {
643 assert_eq!(val, "Alice");
644 }
645 _ => {
646 panic!("Expected name property with value 'Alice'")
647 }
648 }
649 }
650 _ => panic!("Expected ScanByLabel with properties for Person"),
651 }
652 }
653 _ => panic!("Expected Expand operation"),
654 }
655 }
656 _ => panic!("Expected Filter for f.age > 30"),
657 }
658 }
659 _ => panic!("Expected Project at the top level"),
660 }
661 }
662
663 #[test]
664 fn test_simple_node_query_logical_plan() {
665 let query_text = "MATCH (n:Person) RETURN n.name";
666
667 let ast = parse_cypher_query(query_text).unwrap();
668 let mut planner = LogicalPlanner::new();
669 let logical_plan = planner.plan(&ast).unwrap();
670
671 match &logical_plan {
673 LogicalOperator::Project { input, projections } => {
674 assert_eq!(projections.len(), 1);
675 match input.as_ref() {
676 LogicalOperator::ScanByLabel {
677 variable, label, ..
678 } => {
679 assert_eq!(variable, "n");
680 assert_eq!(label, "Person");
681 }
682 _ => panic!("Expected ScanByLabel"),
683 }
684 }
685 _ => panic!("Expected Project"),
686 }
687 }
688
689 #[test]
690 fn test_node_with_properties_logical_plan() {
691 let query_text = "MATCH (n:Person {age: 25}) RETURN n.name";
692
693 let ast = parse_cypher_query(query_text).unwrap();
694 let mut planner = LogicalPlanner::new();
695 let logical_plan = planner.plan(&ast).unwrap();
696
697 match &logical_plan {
700 LogicalOperator::Project { input, .. } => {
701 match input.as_ref() {
702 LogicalOperator::ScanByLabel {
703 variable,
704 label,
705 properties,
706 } => {
707 assert_eq!(variable, "n");
708 assert_eq!(label, "Person");
709
710 assert_eq!(properties.len(), 1);
712 match properties.get("age") {
713 Some(PropertyValue::Integer(25)) => {}
714 _ => panic!("Expected age property with value 25"),
715 }
716 }
717 _ => panic!("Expected ScanByLabel with properties"),
718 }
719 }
720 _ => panic!("Expected Project"),
721 }
722 }
723
724 #[test]
725 fn test_variable_length_path_logical_plan() {
726 let query_text = "MATCH (a:Person)-[:KNOWS*1..2]->(b:Person) RETURN b.name";
727
728 let ast = parse_cypher_query(query_text).unwrap();
729 let mut planner = LogicalPlanner::new();
730 let logical_plan = planner.plan(&ast).unwrap();
731
732 match &logical_plan {
734 LogicalOperator::Project { input, .. } => match input.as_ref() {
735 LogicalOperator::VariableLengthExpand {
736 input: expand_input,
737 source_variable,
738 target_variable,
739 relationship_types,
740 min_length,
741 max_length,
742 ..
743 } => {
744 assert_eq!(source_variable, "a");
745 assert_eq!(target_variable, "b");
746 assert_eq!(relationship_types, &vec!["KNOWS".to_string()]);
747 assert_eq!(*min_length, Some(1));
748 assert_eq!(*max_length, Some(2));
749
750 match expand_input.as_ref() {
751 LogicalOperator::ScanByLabel {
752 variable, label, ..
753 } => {
754 assert_eq!(variable, "a");
755 assert_eq!(label, "Person");
756 }
757 _ => panic!("Expected ScanByLabel"),
758 }
759 }
760 _ => panic!("Expected VariableLengthExpand"),
761 },
762 _ => panic!("Expected Project"),
763 }
764 }
765
766 #[test]
767 fn test_where_clause_logical_plan() {
768 let query_text = r#"MATCH (n:Person) WHERE n.age > 25 RETURN n.name"#;
770
771 let ast = parse_cypher_query(query_text).unwrap();
772 let mut planner = LogicalPlanner::new();
773 let logical_plan = planner.plan(&ast).unwrap();
774
775 match &logical_plan {
777 LogicalOperator::Project { input, .. } => {
778 match input.as_ref() {
779 LogicalOperator::Filter {
780 predicate,
781 input: scan_input,
782 } => {
783 match predicate {
785 BooleanExpression::Comparison {
786 left,
787 operator,
788 right: _,
789 } => {
790 match left {
791 ValueExpression::Property(prop_ref) => {
792 assert_eq!(prop_ref.variable, "n");
793 assert_eq!(prop_ref.property, "age");
794 }
795 _ => panic!("Expected property reference for age"),
796 }
797 assert_eq!(*operator, ComparisonOperator::GreaterThan);
798 }
799 _ => panic!("Expected comparison expression"),
800 }
801
802 match scan_input.as_ref() {
803 LogicalOperator::ScanByLabel { .. } => {}
804 _ => panic!("Expected ScanByLabel"),
805 }
806 }
807 _ => panic!("Expected Filter"),
808 }
809 }
810 _ => panic!("Expected Project"),
811 }
812 }
813
814 #[test]
815 fn test_multiple_node_patterns_join_in_match() {
816 let query_text = "MATCH (a:Person), (b:Company) RETURN a.name, b.name";
817
818 let ast = parse_cypher_query(query_text).unwrap();
819 let mut planner = LogicalPlanner::new();
820 let logical_plan = planner.plan(&ast).unwrap();
821
822 match &logical_plan {
824 LogicalOperator::Project { input, projections } => {
825 assert_eq!(projections.len(), 2);
826 match input.as_ref() {
827 LogicalOperator::Join {
828 left,
829 right,
830 join_type,
831 } => {
832 assert!(matches!(join_type, JoinType::Cross));
833 match left.as_ref() {
834 LogicalOperator::ScanByLabel {
835 variable, label, ..
836 } => {
837 assert_eq!(variable, "a");
838 assert_eq!(label, "Person");
839 }
840 _ => panic!("Expected left ScanByLabel for a:Person"),
841 }
842 match right.as_ref() {
843 LogicalOperator::ScanByLabel {
844 variable, label, ..
845 } => {
846 assert_eq!(variable, "b");
847 assert_eq!(label, "Company");
848 }
849 _ => panic!("Expected right ScanByLabel for b:Company"),
850 }
851 }
852 _ => panic!("Expected Join after Project"),
853 }
854 }
855 _ => panic!("Expected Project at top level"),
856 }
857 }
858
859 #[test]
860 fn test_shared_variable_chained_paths_in_match() {
861 let query_text =
862 "MATCH (a:Person)-[:KNOWS]->(b:Person), (b)-[:LIKES]->(c:Thing) RETURN c.name";
863
864 let ast = parse_cypher_query(query_text).unwrap();
865 let mut planner = LogicalPlanner::new();
866 let logical_plan = planner.plan(&ast).unwrap();
867
868 match &logical_plan {
870 LogicalOperator::Project { input, .. } => match input.as_ref() {
871 LogicalOperator::Expand {
872 source_variable: src2,
873 target_variable: tgt2,
874 input: inner2,
875 ..
876 } => {
877 assert_eq!(src2, "b");
878 assert_eq!(tgt2, "c");
879 match inner2.as_ref() {
880 LogicalOperator::Expand {
881 source_variable: src1,
882 target_variable: tgt1,
883 input: inner1,
884 ..
885 } => {
886 assert_eq!(src1, "a");
887 assert_eq!(tgt1, "b");
888 match inner1.as_ref() {
889 LogicalOperator::ScanByLabel {
890 variable, label, ..
891 } => {
892 assert_eq!(variable, "a");
893 assert_eq!(label, "Person");
894 }
895 _ => panic!("Expected ScanByLabel for a:Person"),
896 }
897 }
898 _ => panic!("Expected first Expand a->b"),
899 }
900 }
901 _ => panic!("Expected second Expand b->c at top of input"),
902 },
903 _ => panic!("Expected Project at top level"),
904 }
905 }
906
907 #[test]
908 fn test_fixed_length_variable_path_is_expand() {
909 let query_text = "MATCH (a:Person)-[:KNOWS*1..1]->(b:Person) RETURN b.name";
910
911 let ast = parse_cypher_query(query_text).unwrap();
912 let mut planner = LogicalPlanner::new();
913 let logical_plan = planner.plan(&ast).unwrap();
914
915 match &logical_plan {
916 LogicalOperator::Project { input, .. } => match input.as_ref() {
917 LogicalOperator::Expand {
918 source_variable,
919 target_variable,
920 ..
921 } => {
922 assert_eq!(source_variable, "a");
923 assert_eq!(target_variable, "b");
924 }
925 _ => panic!("Expected Expand for fixed-length *1..1"),
926 },
927 _ => panic!("Expected Project at top level"),
928 }
929 }
930
931 #[test]
932 fn test_distinct_and_order_limit_wrapping() {
933 let q1 = "MATCH (n:Person) RETURN DISTINCT n.name";
935 let ast1 = parse_cypher_query(q1).unwrap();
936 let mut planner = LogicalPlanner::new();
937 let logical1 = planner.plan(&ast1).unwrap();
938 match logical1 {
939 LogicalOperator::Distinct { input } => match *input {
940 LogicalOperator::Project { .. } => {}
941 _ => panic!("Expected Project under Distinct"),
942 },
943 _ => panic!("Expected Distinct at top level"),
944 }
945
946 let q2 = "MATCH (n:Person) RETURN n.name ORDER BY n.name LIMIT 10";
948 let ast2 = parse_cypher_query(q2).unwrap();
949 let mut planner2 = LogicalPlanner::new();
950 let logical2 = planner2.plan(&ast2).unwrap();
951 match logical2 {
952 LogicalOperator::Limit { input, count } => {
953 assert_eq!(count, 10);
954 match *input {
955 LogicalOperator::Sort { input: inner, .. } => match *inner {
956 LogicalOperator::Project { .. } => {}
957 _ => panic!("Expected Project under Sort"),
958 },
959 _ => panic!("Expected Sort under Limit"),
960 }
961 }
962 _ => panic!("Expected Limit at top level"),
963 }
964 }
965
966 #[test]
967 fn test_order_skip_limit_wrapping() {
968 let q = "MATCH (n:Person) RETURN n.name ORDER BY n.name SKIP 5 LIMIT 10";
970 let ast = parse_cypher_query(q).unwrap();
971 let mut planner = LogicalPlanner::new();
972 let logical = planner.plan(&ast).unwrap();
973 match logical {
974 LogicalOperator::Limit { input, count } => {
975 assert_eq!(count, 10);
976 match *input {
977 LogicalOperator::Offset {
978 input: inner,
979 offset,
980 } => {
981 assert_eq!(offset, 5);
982 match *inner {
983 LogicalOperator::Sort { input: inner2, .. } => match *inner2 {
984 LogicalOperator::Project { .. } => {}
985 _ => panic!("Expected Project under Sort"),
986 },
987 _ => panic!("Expected Sort under Offset"),
988 }
989 }
990 _ => panic!("Expected Offset under Limit"),
991 }
992 }
993 _ => panic!("Expected Limit at top level"),
994 }
995 }
996
997 #[test]
998 fn test_skip_only_wrapping() {
999 let q = "MATCH (n:Person) RETURN n.name SKIP 3";
1001 let ast = parse_cypher_query(q).unwrap();
1002 let mut planner = LogicalPlanner::new();
1003 let logical = planner.plan(&ast).unwrap();
1004 match logical {
1005 LogicalOperator::Offset { input, offset } => {
1006 assert_eq!(offset, 3);
1007 match *input {
1008 LogicalOperator::Project { .. } => {}
1009 _ => panic!("Expected Project under Offset"),
1010 }
1011 }
1012 _ => panic!("Expected Offset at top level"),
1013 }
1014 }
1015
1016 #[test]
1017 fn test_relationship_properties_pushed_into_expand() {
1018 let q = "MATCH (a)-[:KNOWS {since: 2020}]->(b) RETURN b";
1019 let ast = parse_cypher_query(q).unwrap();
1020 let mut planner = LogicalPlanner::new();
1021 let logical = planner.plan(&ast).unwrap();
1022 match logical {
1023 LogicalOperator::Project { input, .. } => match *input {
1024 LogicalOperator::Expand { properties, .. } => match properties.get("since") {
1025 Some(PropertyValue::Integer(2020)) => {}
1026 _ => panic!("Expected relationship property since=2020 in Expand"),
1027 },
1028 _ => panic!("Expected Expand under Project"),
1029 },
1030 _ => panic!("Expected Project at top level"),
1031 }
1032 }
1033
1034 #[test]
1035 fn test_multiple_match_clauses_cross_join() {
1036 let q = "MATCH (a:Person) MATCH (b:Company) RETURN a.name, b.name";
1037 let ast = parse_cypher_query(q).unwrap();
1038 let mut planner = LogicalPlanner::new();
1039 let logical = planner.plan(&ast).unwrap();
1040 match logical {
1041 LogicalOperator::Project { input, .. } => match *input {
1042 LogicalOperator::Join {
1043 left,
1044 right,
1045 join_type,
1046 } => {
1047 assert!(matches!(join_type, JoinType::Cross));
1048 match (*left, *right) {
1049 (
1050 LogicalOperator::ScanByLabel {
1051 variable: va,
1052 label: la,
1053 ..
1054 },
1055 LogicalOperator::ScanByLabel {
1056 variable: vb,
1057 label: lb,
1058 ..
1059 },
1060 ) => {
1061 assert_eq!(va, "a");
1062 assert_eq!(la, "Person");
1063 assert_eq!(vb, "b");
1064 assert_eq!(lb, "Company");
1065 }
1066 _ => panic!("Expected two scans under Join"),
1067 }
1068 }
1069 _ => panic!("Expected Join under Project"),
1070 },
1071 _ => panic!("Expected Project at top level"),
1072 }
1073 }
1074
1075 #[test]
1076 fn test_variable_only_node_default_label() {
1077 let q = "MATCH (x) RETURN x";
1078 let ast = parse_cypher_query(q).unwrap();
1079 let mut planner = LogicalPlanner::new();
1080 let logical = planner.plan(&ast).unwrap();
1081 match logical {
1082 LogicalOperator::Project { input, .. } => match *input {
1083 LogicalOperator::ScanByLabel {
1084 variable, label, ..
1085 } => {
1086 assert_eq!(variable, "x");
1087 assert_eq!(label, "Node");
1088 }
1089 _ => panic!("Expected ScanByLabel under Project"),
1090 },
1091 _ => panic!("Expected Project at top level"),
1092 }
1093 }
1094
1095 #[test]
1096 fn test_multi_label_node_uses_first_label() {
1097 let q = "MATCH (n:Person:Employee) RETURN n";
1098 let ast = parse_cypher_query(q).unwrap();
1099 let mut planner = LogicalPlanner::new();
1100 let logical = planner.plan(&ast).unwrap();
1101 match logical {
1102 LogicalOperator::Project { input, .. } => match *input {
1103 LogicalOperator::ScanByLabel { label, .. } => {
1104 assert_eq!(label, "Person");
1105 }
1106 _ => panic!("Expected ScanByLabel under Project"),
1107 },
1108 _ => panic!("Expected Project at top level"),
1109 }
1110 }
1111
1112 #[test]
1113 fn test_open_ended_and_partial_var_length_ranges() {
1114 let q1 = "MATCH (a)-[:R*]->(b) RETURN b";
1116 let ast1 = parse_cypher_query(q1).unwrap();
1117 let mut planner1 = LogicalPlanner::new();
1118 let plan1 = planner1.plan(&ast1).unwrap();
1119 match plan1 {
1120 LogicalOperator::Project { input, .. } => match *input {
1121 LogicalOperator::VariableLengthExpand {
1122 min_length,
1123 max_length,
1124 ..
1125 } => {
1126 assert_eq!(min_length, None);
1127 assert_eq!(max_length, None);
1128 }
1129 _ => panic!("Expected VariableLengthExpand for *"),
1130 },
1131 _ => panic!("Expected Project at top level"),
1132 }
1133
1134 let q2 = "MATCH (a)-[:R*2..]->(b) RETURN b";
1136 let ast2 = parse_cypher_query(q2).unwrap();
1137 let mut planner2 = LogicalPlanner::new();
1138 let plan2 = planner2.plan(&ast2).unwrap();
1139 match plan2 {
1140 LogicalOperator::Project { input, .. } => match *input {
1141 LogicalOperator::VariableLengthExpand {
1142 min_length,
1143 max_length,
1144 ..
1145 } => {
1146 assert_eq!(min_length, Some(2));
1147 assert_eq!(max_length, None);
1148 }
1149 _ => panic!("Expected VariableLengthExpand for *2.."),
1150 },
1151 _ => panic!("Expected Project at top level"),
1152 }
1153
1154 let q3 = "MATCH (a)-[:R*..3]->(b) RETURN b";
1156 let ast3 = parse_cypher_query(q3).unwrap();
1157 let mut planner3 = LogicalPlanner::new();
1158 let plan3 = planner3.plan(&ast3).unwrap();
1159 match plan3 {
1160 LogicalOperator::Project { input, .. } => match *input {
1161 LogicalOperator::VariableLengthExpand {
1162 min_length,
1163 max_length,
1164 ..
1165 } => {
1166 assert_eq!(min_length, None);
1167 assert_eq!(max_length, Some(3));
1168 }
1169 _ => panic!("Expected VariableLengthExpand for *..3"),
1170 },
1171 _ => panic!("Expected Project at top level"),
1172 }
1173 }
1174}