1use crate::case_insensitive::qualify_column;
12use crate::error::{GraphError, Result};
13use crate::{ast::*, GraphConfig};
14use serde::{Deserialize, Serialize};
15use snafu::Location;
16use std::collections::HashMap;
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub enum LogicalOperator {
21 SingleRow,
23 QueryUnion {
25 left: Box<LogicalOperator>,
26 right: Box<LogicalOperator>,
27 all: bool,
28 },
29 ScanByLabel {
31 variable: String,
32 label: String,
33 properties: HashMap<String, PropertyValue>,
34 },
35
36 Unwind {
38 input: Option<Box<LogicalOperator>>,
40 expression: ValueExpression,
42 alias: String,
44 label: Option<String>,
47 },
48
49 Filter {
51 input: Box<LogicalOperator>,
52 predicate: BooleanExpression,
53 },
54
55 Expand {
59 input: Box<LogicalOperator>,
61 source_variable: String,
63 target_variable: String,
65 target_label: String,
68 relationship_types: Vec<String>,
70 direction: RelationshipDirection,
72 relationship_variable: Option<String>,
74 properties: HashMap<String, PropertyValue>,
76 target_properties: HashMap<String, PropertyValue>,
78 },
79
80 VariableLengthExpand {
85 input: Box<LogicalOperator>,
87 source_variable: String,
89 target_variable: String,
91 target_label: String,
93 relationship_types: Vec<String>,
95 direction: RelationshipDirection,
97 relationship_variable: Option<String>,
99 min_length: Option<u32>,
101 max_length: Option<u32>,
103 target_properties: HashMap<String, PropertyValue>,
105 },
106
107 Project {
109 input: Box<LogicalOperator>,
110 projections: Vec<ProjectionItem>,
111 },
112
113 Join {
115 left: Box<LogicalOperator>,
116 right: Box<LogicalOperator>,
117 join_type: JoinType,
118 },
119
120 Distinct { input: Box<LogicalOperator> },
122
123 Sort {
125 input: Box<LogicalOperator>,
126 sort_items: Vec<SortItem>,
127 },
128
129 Offset {
131 input: Box<LogicalOperator>,
132 offset: u64,
133 },
134
135 Limit {
137 input: Box<LogicalOperator>,
138 count: u64,
139 },
140
141 Union { inputs: Vec<LogicalOperator> },
145
146 PathMatch {
148 input: Box<LogicalOperator>,
149 path: PathPattern,
150 },
151
152 ShortestPath {
155 input: Box<LogicalOperator>,
156 source_variable: String,
157 target_variable: String,
158 relationship_type: String,
159 direction: RelationshipDirection,
160 min_hops: u32,
161 max_hops: u32,
162 all: bool,
163 path_variable: String,
164 },
165
166 OptionalMatch {
169 input: Box<LogicalOperator>,
170 optional: Box<LogicalOperator>,
171 keys: Vec<String>,
172 },
173
174 ListComprehension {
181 input: Box<LogicalOperator>,
182 list: ValueExpression,
183 variable: String,
184 label: Option<String>,
185 element: Box<LogicalOperator>,
186 keep: Option<BooleanExpression>,
187 map: ValueExpression,
188 output: String,
189 #[serde(default)]
191 count_only: bool,
192 },
193
194 Apply {
196 input: Box<LogicalOperator>,
197 inner: Box<LogicalOperator>,
198 imports: Vec<String>,
199 labels: HashMap<String, String>,
200 id: usize,
201 },
202 CallInput { labels: HashMap<String, String> },
204
205 ComprehensionElement,
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
211pub struct ProjectionItem {
212 pub expression: ValueExpression,
213 pub alias: Option<String>,
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
218pub enum JoinType {
219 Inner,
220 Left,
221 Right,
222 Full,
223 Cross,
224}
225
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
228pub struct SortItem {
229 pub expression: ValueExpression,
230 pub direction: SortDirection,
231}
232
233pub type FanOut = HashMap<(String, bool), f64>;
236
237pub struct LogicalPlanner<'a> {
239 variables: HashMap<String, String>, config: &'a GraphConfig,
242 fan_out: Option<&'a FanOut>,
243 adjacency: Option<&'a crate::csr_expand::AdjacencyCatalog>,
244 exists_marks: usize,
246 node_lists: HashMap<String, String>,
248 comprehensions: usize,
250 calls: usize,
251 path_labels: HashMap<String, String>,
253 rich_paths: bool,
254 seed_source: Option<LogicalOperator>,
258}
259
260impl<'a> LogicalPlanner<'a> {
261 pub fn new(config: &'a GraphConfig) -> Self {
262 Self {
263 variables: HashMap::new(),
264 config,
265 fan_out: None,
266 adjacency: None,
267 exists_marks: 0,
268 node_lists: HashMap::new(),
269 comprehensions: 0,
270 calls: 0,
271 path_labels: HashMap::new(),
272 rich_paths: false,
273 seed_source: None,
274 }
275 }
276
277 pub fn with_fan_out(mut self, fan_out: Option<&'a FanOut>) -> Self {
279 self.fan_out = fan_out;
280 self
281 }
282
283 pub(crate) fn with_adjacency(
284 mut self,
285 adjacency: Option<&'a crate::csr_expand::AdjacencyCatalog>,
286 ) -> Self {
287 self.adjacency = adjacency;
288 self
289 }
290
291 fn retain_variable_alternation(&self, patterns: &[GraphPattern]) -> bool {
292 if patterns
293 .iter()
294 .all(|p| matches!(p, GraphPattern::Path(path) if path.variable.is_some()))
295 {
296 return true;
297 }
298 let Some(catalog) = self.adjacency else {
299 return false;
300 };
301 let labels = |n: &NodePattern| -> Vec<String> {
302 n.variable
303 .as_ref()
304 .and_then(|v| self.variables.get(v))
305 .map(|l| vec![l.clone()])
306 .unwrap_or_else(|| n.labels.clone())
307 };
308 patterns.iter().all(|p| {
309 let GraphPattern::Path(path) = p else {
310 return true;
311 };
312 let mut previous = &path.start_node;
313 path.segments.iter().all(|segment| {
314 let start = previous;
315 previous = &segment.end_node;
316 let rel = &segment.relationship;
317 let Some(range) = &rel.length else {
318 return true;
319 };
320 if rel.types.len() <= 1 {
321 return true;
322 }
323 if rel.variable.is_some()
324 || !rel.properties.is_empty()
325 || rel.direction == RelationshipDirection::Undirected
326 || (range.min == Some(1) && range.max == Some(1))
327 {
328 return false;
329 }
330 let (starts, ends) = (labels(start), labels(&segment.end_node));
331 let (Some(start), Some(end)) = (starts.first(), ends.first()) else {
332 return false;
333 };
334 if starts
335 .iter()
336 .chain(&ends)
337 .any(|l| catalog.nodes(l).is_none())
338 {
339 return false;
340 }
341 crate::csr_traversal::Traversal::new(
342 self.config,
343 catalog,
344 &rel.types,
345 &rel.direction,
346 start,
347 end,
348 0,
349 0,
350 )
351 .is_some()
352 })
353 })
354 }
355
356 fn cheaper_orientation(&self, path: &PathPattern) -> Option<PathPattern> {
363 if path.variable.is_some() || path.shortest.is_some() {
364 return None;
365 }
366 let bound = |n: &NodePattern| {
367 n.variable
368 .as_ref()
369 .is_some_and(|v| self.variables.contains_key(v))
370 };
371 let (first, last) = (path.segments.first()?, path.segments.last()?);
372 if !bound(&last.end_node)
373 || path
374 .segments
375 .iter()
376 .any(|s| s.relationship.length.is_some())
377 {
378 return None;
379 }
380 if !bound(&path.start_node) {
381 return Some(Self::reversed_path(path));
382 }
383 let fan_out = self.fan_out?;
384 let hop = |rel: &RelationshipPattern, forward: bool| -> Option<f64> {
386 let rel_type = rel.types.first()?.to_lowercase();
387 let get = |outgoing: bool| fan_out.get(&(rel_type.clone(), outgoing)).copied();
388 match (&rel.direction, forward) {
389 (RelationshipDirection::Outgoing, true)
390 | (RelationshipDirection::Incoming, false) => get(true),
391 (RelationshipDirection::Incoming, true)
392 | (RelationshipDirection::Outgoing, false) => get(false),
393 (RelationshipDirection::Undirected, _) => Some(get(true)? + get(false)?),
394 }
395 };
396 let from_start = hop(&first.relationship, true)?;
397 let from_end = hop(&last.relationship, false)?;
398 (from_end < from_start).then(|| Self::reversed_path(path))
399 }
400
401 fn reversed_path(path: &PathPattern) -> PathPattern {
403 let mut nodes: Vec<&NodePattern> = std::iter::once(&path.start_node)
404 .chain(path.segments.iter().map(|s| &s.end_node))
405 .collect();
406 nodes.reverse();
407 let segments = path
408 .segments
409 .iter()
410 .rev()
411 .zip(nodes.iter().skip(1))
412 .map(|(segment, end)| PathSegment {
413 relationship: crate::pattern_order::reversed(&segment.relationship),
414 end_node: (*end).clone(),
415 })
416 .collect();
417 PathPattern {
418 start_node: nodes[0].clone(),
419 segments,
420 shortest: path.shortest.clone(),
421 variable: path.variable.clone(),
422 }
423 }
424
425 pub fn plan(&mut self, query: &CypherQuery) -> Result<LogicalOperator> {
427 self.rich_paths = crate::path_value::needs_values(query, self.config);
428 self.plan_query(query, None, false)
429 }
430
431 fn plan_query(
432 &mut self,
433 query: &CypherQuery,
434 input: Option<LogicalOperator>,
435 preserve_entities: bool,
436 ) -> Result<LogicalOperator> {
437 let seed = input.clone();
438 let imports = (
439 self.variables.clone(),
440 self.node_lists.clone(),
441 self.path_labels.clone(),
442 );
443 if preserve_entities {
444 let names = query.call_imports()?;
445 self.variables.retain(|name, _| names.contains(name));
446 self.node_lists.retain(|name, _| names.contains(name));
447 self.path_labels.retain(|name, _| names.contains(name));
448 }
449 let where_expr = query.where_clause.as_ref().map(|w| &w.expression);
451 let mut plan = self.plan_reading_clauses(input, &query.reading_clauses, where_expr)?;
452
453 if let Some(where_clause) = &query.where_clause {
455 plan = self.filter(plan, &where_clause.expression)?;
456 }
457
458 if let Some(with_clause) = &query.with_clause {
460 plan = self.plan_with_clause(with_clause, plan)?;
461 }
462
463 if !query.post_with_reading_clauses.is_empty() {
465 plan = self.plan_reading_clauses(Some(plan), &query.post_with_reading_clauses, None)?;
466 }
467
468 if let Some(post_where) = &query.post_with_where_clause {
470 plan = self.filter(plan, &post_where.expression)?;
471 }
472
473 for stage in &query.further_with_stages {
475 plan = self.plan_with_clause(&stage.with_clause, plan)?;
476 if !stage.reading_clauses.is_empty() {
477 plan = self.plan_reading_clauses(Some(plan), &stage.reading_clauses, None)?;
478 }
479 if let Some(where_clause) = &stage.where_clause {
480 plan = self.filter(plan, &where_clause.expression)?;
481 }
482 }
483
484 plan = if preserve_entities {
486 self.plan_with_clause(
487 &WithClause {
488 distinct: query.return_clause.distinct,
489 items: query.return_clause.items.clone(),
490 order_by: None,
491 limit: None,
492 where_clause: None,
493 },
494 plan,
495 )?
496 } else {
497 self.plan_return_clause(&query.return_clause, plan)?
498 };
499
500 if let Some(order_by) = &query.order_by {
502 plan = LogicalOperator::Sort {
503 input: Box::new(plan),
504 sort_items: self.sort_items(order_by),
505 };
506 }
507
508 if let Some(skip) = &query.skip {
510 plan = LogicalOperator::Offset {
511 input: Box::new(plan),
512 offset: skip.value()?,
513 };
514 }
515
516 if let Some(limit) = &query.limit {
518 plan = LogicalOperator::Limit {
519 input: Box::new(plan),
520 count: limit.value()?,
521 };
522 }
523
524 for branch in &query.union_branches {
525 let mut planner = LogicalPlanner::new(self.config)
526 .with_fan_out(self.fan_out)
527 .with_adjacency(self.adjacency);
528 planner.calls = self.calls;
529 planner.rich_paths = self.rich_paths;
530 if preserve_entities {
531 planner.variables = imports.0.clone();
532 planner.node_lists = imports.1.clone();
533 planner.path_labels = imports.2.clone();
534 }
535 let right = planner.plan_query(&branch.query, seed.clone(), preserve_entities)?;
536 self.calls = planner.calls;
537 if preserve_entities {
538 let node_bindings = |variables: &HashMap<String, String>| {
539 variables
540 .iter()
541 .filter(|(_, label)| self.config.get_node_mapping(label).is_some())
542 .map(|(name, label)| (name.clone(), label.clone()))
543 .collect::<HashMap<_, _>>()
544 };
545 if node_bindings(&self.variables) != node_bindings(&planner.variables)
546 || self.node_lists != planner.node_lists
547 {
548 return Err(GraphError::PlanError {
549 message: "CALL UNION node and node-list outputs must retain the same labels in every branch".into(),
550 location: snafu::Location::new(file!(), line!(), column!()),
551 });
552 }
553 }
554 plan = LogicalOperator::QueryUnion {
555 left: Box::new(plan),
556 right: Box::new(right),
557 all: branch.all,
558 };
559 }
560 Ok(plan)
561 }
562
563 fn plan_reading_clauses(
564 &mut self,
565 base_plan: Option<LogicalOperator>,
566 reading_clauses: &[ReadingClause],
567 filter: Option<&BooleanExpression>,
568 ) -> Result<LogicalOperator> {
569 let mut plan = base_plan;
570
571 if reading_clauses.is_empty() && plan.is_none() {
572 plan = Some(LogicalOperator::SingleRow);
573 }
574
575 for clause in reading_clauses {
576 plan = Some(self.plan_reading_clause_with_base(plan, clause, filter)?);
577 }
578
579 plan.ok_or_else(|| GraphError::PlanError {
580 message: "Failed to plan clauses".to_string(),
581 location: snafu::Location::new(file!(), line!(), column!()),
582 })
583 }
584
585 fn plan_reading_clause_with_base(
587 &mut self,
588 base: Option<LogicalOperator>,
589 clause: &ReadingClause,
590 filter: Option<&BooleanExpression>,
591 ) -> Result<LogicalOperator> {
592 match clause {
593 ReadingClause::Call(query) => self.plan_call(base, query),
594 ReadingClause::Match(match_clause) if match_clause.optional => {
595 self.plan_optional_match(base, match_clause, None)
596 }
597 ReadingClause::Match(match_clause) => {
598 let plan = self.plan_match_clause_with_base(base, match_clause, filter)?;
599 Ok(match &match_clause.where_clause {
600 Some(w) => self.filter(plan, &w.expression)?,
601 None => plan,
602 })
603 }
604 ReadingClause::Unwind(unwind_clause) => {
605 self.plan_unwind_clause_with_base(base, unwind_clause)
606 }
607 }
608 }
609
610 fn plan_call(
611 &mut self,
612 input: Option<LogicalOperator>,
613 query: &CypherQuery,
614 ) -> Result<LogicalOperator> {
615 let imports = query.call_union_imports()?;
616 self.calls += 1;
617 let id = self.calls;
618 let mut inner = Self::new(self.config)
619 .with_fan_out(self.fan_out)
620 .with_adjacency(self.adjacency);
621 inner.calls = self.calls;
622 inner.rich_paths = self.rich_paths;
623 for name in &imports {
624 if let Some(label) = self.variables.get(name) {
625 inner.variables.insert(name.clone(), label.clone());
626 }
627 if let Some(label) = self.node_lists.get(name) {
628 inner.node_lists.insert(name.clone(), label.clone());
629 }
630 if let Some(label) = self.path_labels.get(name) {
631 inner.path_labels.insert(name.clone(), label.clone());
632 }
633 }
634 let input_labels = inner.variables.clone();
635 let plan = inner.plan_query(
636 query,
637 Some(LogicalOperator::CallInput {
638 labels: input_labels,
639 }),
640 true,
641 )?;
642 self.calls = inner.calls;
643 let labels = inner.variables.clone();
644 self.variables.extend(inner.variables);
645 self.node_lists.extend(inner.node_lists);
646 for item in &query.return_clause.items {
647 if let ValueExpression::Variable(name) = &item.expression {
648 if let Some(label) = inner.path_labels.get(name) {
649 self.path_labels
650 .insert(item.alias.as_ref().unwrap_or(name).clone(), label.clone());
651 }
652 }
653 }
654 Ok(LogicalOperator::Apply {
655 input: Box::new(input.unwrap_or(LogicalOperator::SingleRow)),
656 inner: Box::new(plan),
657 imports,
658 labels,
659 id,
660 })
661 }
662
663 fn plan_unwind_clause_with_base(
665 &mut self,
666 base: Option<LogicalOperator>,
667 unwind_clause: &UnwindClause,
668 ) -> Result<LogicalOperator> {
669 let label = match &unwind_clause.expression {
670 ValueExpression::Variable(list) => self.node_lists.get(list).cloned(),
671 _ => None,
672 };
673 let kind = label.clone().unwrap_or_else(|| "Unwound".to_string());
675 self.variables.insert(unwind_clause.alias.clone(), kind);
676 let mut expression = unwind_clause.expression.clone();
677 self.resolve_value(&mut expression);
678
679 Ok(LogicalOperator::Unwind {
680 input: base.map(Box::new),
681 expression,
682 alias: unwind_clause.alias.clone(),
683 label,
684 })
685 }
686
687 fn resolve_value(&self, expr: &mut ValueExpression) {
691 match expr {
692 ValueExpression::Variable(var) => {
693 let property = match self.variables.get(var).map(String::as_str) {
694 Some("Path") if !self.rich_paths => Some("length".to_string()),
695 Some("Path") => None,
696 Some(label) => self
697 .config
698 .get_node_mapping(label)
699 .map(|m| m.id_field.clone()),
700 None => None,
701 };
702 if let Some(property) = property {
703 *expr = ValueExpression::Property(PropertyRef {
704 variable: var.clone(),
705 property,
706 });
707 }
708 }
709 ValueExpression::ScalarFunction { name, args }
710 if name.eq_ignore_ascii_case("labels") =>
711 {
712 if let ValueExpression::Variable(var) = &args[0] {
713 if let Some(mapping) = self
714 .variables
715 .get(var)
716 .and_then(|label| self.config.get_node_mapping(label))
717 {
718 *expr = ValueExpression::Case {
719 whens: vec![(
720 BooleanExpression::IsNull(ValueExpression::Property(PropertyRef {
721 variable: var.clone(),
722 property: mapping.id_field.clone(),
723 })),
724 ValueExpression::Literal(PropertyValue::Null),
725 )],
726 otherwise: Some(Box::new(ValueExpression::ScalarFunction {
727 name: "__list".to_string(),
728 args: vec![ValueExpression::Literal(PropertyValue::String(
729 mapping.label.clone(),
730 ))],
731 })),
732 };
733 return;
734 }
735 }
736 args.iter_mut().for_each(|arg| self.resolve_value(arg));
737 }
738 ValueExpression::ScalarFunction { name, args }
740 if name.eq_ignore_ascii_case("keys")
741 && matches!(args.as_slice(), [ValueExpression::Variable(_)]) => {}
742 ValueExpression::ScalarFunction { name, args }
744 if matches!(
745 name.to_lowercase().as_str(),
746 "length" | "nodes" | "relationships" | "__path_node_records"
747 ) => {}
748 ValueExpression::ScalarFunction { args, .. }
749 | ValueExpression::AggregateFunction { args, .. } => {
750 args.iter_mut().for_each(|a| self.resolve_value(a))
751 }
752 ValueExpression::Arithmetic { left, right, .. } => {
753 self.resolve_value(left);
754 self.resolve_value(right);
755 }
756 ValueExpression::Case { whens, otherwise } => {
757 for (condition, then) in whens {
758 self.resolve_boolean(condition);
759 self.resolve_value(then);
760 }
761 if let Some(e) = otherwise {
762 self.resolve_value(e);
763 }
764 }
765 ValueExpression::Reduce {
766 initial,
767 list,
768 expression,
769 ..
770 } => {
771 self.resolve_value(initial);
772 self.resolve_value(list);
773 self.resolve_value(expression);
774 }
775 ValueExpression::ListPredicate {
776 list, predicate, ..
777 } => {
778 self.resolve_value(list);
779 self.resolve_value(predicate);
780 }
781 ValueExpression::Boolean(b) => self.resolve_boolean(b),
782 ValueExpression::Map(entries) => entries
784 .iter_mut()
785 .filter(|(_, v)| !matches!(v, ValueExpression::Variable(_)))
786 .for_each(|(_, v)| self.resolve_value(v)),
787 ValueExpression::Field { base, .. } => self.resolve_value(base),
788 _ => {}
789 }
790 }
791
792 fn resolve_boolean(&self, expr: &mut BooleanExpression) {
794 match expr {
795 BooleanExpression::Comparison { left, right, .. } => {
796 self.resolve_value(left);
797 self.resolve_value(right);
798 }
799 BooleanExpression::And(a, b) | BooleanExpression::Or(a, b) => {
800 self.resolve_boolean(a);
801 self.resolve_boolean(b);
802 }
803 BooleanExpression::Not(inner) => self.resolve_boolean(inner),
804 BooleanExpression::In { expression, list } => {
805 self.resolve_value(expression);
806 list.iter_mut().for_each(|v| self.resolve_value(v));
807 }
808 BooleanExpression::Member { expression, list } => {
809 self.resolve_value(expression);
810 self.resolve_value(list);
811 }
812 BooleanExpression::Like { expression, .. }
813 | BooleanExpression::ILike { expression, .. }
814 | BooleanExpression::Contains { expression, .. }
815 | BooleanExpression::StartsWith { expression, .. }
816 | BooleanExpression::EndsWith { expression, .. }
817 | BooleanExpression::IsNull(expression)
818 | BooleanExpression::IsNotNull(expression) => self.resolve_value(expression),
819 BooleanExpression::Exists(_) | BooleanExpression::PatternExists { .. } => {}
820 }
821 }
822
823 fn sort_items(&self, order_by: &OrderByClause) -> Vec<SortItem> {
825 order_by
826 .items
827 .iter()
828 .map(|item| {
829 let mut expression = item.expression.clone();
830 self.resolve_value(&mut expression);
831 SortItem {
832 expression,
833 direction: item.direction.clone(),
834 }
835 })
836 .collect()
837 }
838
839 fn plan_match_clause_with_base(
845 &mut self,
846 base: Option<LogicalOperator>,
847 match_clause: &MatchClause,
848 filter: Option<&BooleanExpression>,
849 ) -> Result<LogicalOperator> {
850 if match_clause.patterns.is_empty() {
851 return Err(GraphError::PlanError {
852 message: "MATCH clause must have at least one pattern".to_string(),
853 location: snafu::Location::new(file!(), line!(), column!()),
854 });
855 }
856 let inferred = MatchClause {
857 patterns: crate::pattern_alternatives::infer_labels(
858 &match_clause.patterns,
859 self.config,
860 &self.variables,
861 ),
862 ..match_clause.clone()
863 };
864 let match_clause = &inferred;
865 let retain_variable = self.retain_variable_alternation(&match_clause.patterns);
866 if crate::pattern_alternatives::needs_expansion(&match_clause.patterns, retain_variable) {
867 let branches = crate::pattern_alternatives::expand(
868 &match_clause.patterns,
869 self.config,
870 &self.variables,
871 retain_variable,
872 )?;
873 return self.plan_match_branches(base, branches, filter);
874 }
875
876 let reordered = base
877 .is_none()
878 .then(|| {
879 crate::pattern_order::reorder_patterns(&match_clause.patterns, filter, self.config)
880 })
881 .flatten();
882 let oriented: Option<Vec<GraphPattern>> = base.is_some().then(|| {
883 match_clause
884 .patterns
885 .iter()
886 .map(|p| match p {
887 GraphPattern::Path(path) => self
888 .cheaper_orientation(path)
889 .map_or_else(|| p.clone(), GraphPattern::Path),
890 _ => p.clone(),
891 })
892 .collect()
893 });
894 let patterns = reordered
895 .as_deref()
896 .or(oriented.as_deref())
897 .unwrap_or(&match_clause.patterns);
898
899 let mut plan = base;
900 for pattern in patterns {
901 match pattern {
902 GraphPattern::Node(node) => {
903 let already_bound = node
904 .variable
905 .as_deref()
906 .is_some_and(|v| self.variables.contains_key(v));
907
908 match (already_bound, plan.as_ref()) {
909 (true, _) => { }
910 (false, None) => plan = Some(self.plan_node_scan(node)?),
911 (false, Some(_)) => {
912 let right = self.plan_node_scan(node)?;
913 plan = Some(LogicalOperator::Join {
914 left: Box::new(plan.unwrap()),
915 right: Box::new(right),
916 join_type: JoinType::Cross, });
918 }
919 }
920 }
921 GraphPattern::Path(path) if path.variable.is_some() => {
922 plan = Some(self.plan_named_path(plan, path)?)
923 }
924 GraphPattern::Path(path) if path.shortest.is_some() => {
925 plan = Some(self.plan_shortest_path(plan, path)?)
926 }
927 GraphPattern::Path(path)
928 if self.rich_paths
929 && path
930 .segments
931 .iter()
932 .any(|s| s.relationship.variable.is_some()) =>
933 {
934 let mut path = path.clone();
935 path.variable = Some(format!("_path_value_{}", self.variables.len()));
936 plan = Some(self.plan_named_path(plan, &path)?);
937 }
938 GraphPattern::Path(path) => plan = Some(self.plan_path(plan, path)?),
939 }
940 }
941
942 plan.ok_or_else(|| GraphError::PlanError {
943 message: "Failed to plan MATCH clause".to_string(),
944 location: snafu::Location::new(file!(), line!(), column!()),
945 })
946 }
947
948 fn filter(
950 &mut self,
951 input: LogicalOperator,
952 predicate: &BooleanExpression,
953 ) -> Result<LogicalOperator> {
954 let mut predicate = predicate.clone();
955 let saved = self.seed_source.replace(input.clone());
956 let input = self.rewrite_exists(input, &mut predicate);
957 self.seed_source = saved;
958 let input = input?;
959 self.resolve_boolean(&mut predicate);
960 Ok(LogicalOperator::Filter {
961 input: Box::new(input),
962 predicate,
963 })
964 }
965
966 fn rewrite_exists(
972 &mut self,
973 mut input: LogicalOperator,
974 expr: &mut BooleanExpression,
975 ) -> Result<LogicalOperator> {
976 match expr {
977 BooleanExpression::And(a, b) | BooleanExpression::Or(a, b) => {
978 input = self.rewrite_exists(input, a)?;
979 self.rewrite_exists(input, b)
980 }
981 BooleanExpression::Not(inner) => self.rewrite_exists(input, inner),
982 BooleanExpression::Comparison { left, right, .. } => {
983 input = self.rewrite_exists_in_value(input, left)?;
984 self.rewrite_exists_in_value(input, right)
985 }
986 BooleanExpression::PatternExists {
987 patterns,
988 where_clause,
989 } => {
990 self.exists_marks += 1;
991 let mark = format!("__exists_{}", self.exists_marks);
992 let inner = MatchClause {
993 patterns: patterns.clone(),
994 optional: true,
995 where_clause: where_clause.as_ref().map(|w| WhereClause {
996 expression: (**w).clone(),
997 }),
998 };
999 let before = self.variables.clone();
1001 let seed = self.seed_source.clone();
1002 let planned = self.plan_optional_match(Some(input), &inner, seed);
1003 self.variables = before;
1004 let LogicalOperator::OptionalMatch {
1005 input,
1006 optional,
1007 keys,
1008 } = planned?
1009 else {
1010 unreachable!("plan_optional_match returns an OptionalMatch")
1011 };
1012 let shared: Vec<String> = keys
1013 .iter()
1014 .map(|k| k.split("__").next().unwrap_or(k).to_string())
1015 .collect();
1016 let mut projections: Vec<ProjectionItem> = shared
1017 .iter()
1018 .map(|var| ProjectionItem {
1019 expression: ValueExpression::Variable(var.clone()),
1020 alias: None,
1021 })
1022 .collect();
1023 projections.push(ProjectionItem {
1024 expression: ValueExpression::Literal(PropertyValue::Boolean(true)),
1025 alias: Some(mark.clone()),
1026 });
1027 let marked = LogicalOperator::Distinct {
1028 input: Box::new(LogicalOperator::Project {
1029 input: optional,
1030 projections,
1031 }),
1032 };
1033 *expr = BooleanExpression::IsNotNull(ValueExpression::Variable(mark));
1034 Ok(LogicalOperator::OptionalMatch {
1035 input,
1036 optional: Box::new(marked),
1037 keys,
1038 })
1039 }
1040 _ => Ok(input),
1041 }
1042 }
1043
1044 fn rewrite_exists_in_value(
1046 &mut self,
1047 mut input: LogicalOperator,
1048 expr: &mut ValueExpression,
1049 ) -> Result<LogicalOperator> {
1050 match expr {
1051 ValueExpression::ScalarFunction { name, args }
1052 if name.eq_ignore_ascii_case("labels")
1053 && matches!(args.as_slice(), [ValueExpression::Variable(var)] if self.variables.get(var).is_some_and(|label| label.contains('|'))) =>
1054 {
1055 Err(GraphError::UnsupportedFeature {
1056 feature: "labels() on a mixed-label binding is not supported".to_string(),
1057 location: Location::new(file!(), line!(), column!()),
1058 })
1059 }
1060 ValueExpression::Case { whens, otherwise } => {
1061 for (condition, then) in whens.iter_mut() {
1062 input = self.rewrite_exists(input, condition)?;
1063 input = self.rewrite_exists_in_value(input, then)?;
1064 }
1065 match otherwise {
1066 Some(e) => self.rewrite_exists_in_value(input, e),
1067 None => Ok(input),
1068 }
1069 }
1070 ValueExpression::ScalarFunction { name, args }
1074 if name.eq_ignore_ascii_case("size")
1075 && matches!(
1076 args.as_slice(),
1077 [ValueExpression::ListComprehension { map: None, .. }]
1078 ) =>
1079 {
1080 let mut lowered = self.rewrite_exists_in_value(input, &mut args[0])?;
1081 let LogicalOperator::ListComprehension { count_only, .. } = &mut lowered else {
1082 unreachable!("a list comprehension lowers to its own operator")
1083 };
1084 *count_only = true;
1085 *expr = args.remove(0);
1086 Ok(lowered)
1087 }
1088 ValueExpression::ScalarFunction { args, .. }
1089 | ValueExpression::AggregateFunction { args, .. } => {
1090 for arg in args.iter_mut() {
1091 input = self.rewrite_exists_in_value(input, arg)?;
1092 }
1093 Ok(input)
1094 }
1095 ValueExpression::Arithmetic { left, right, .. } => {
1096 input = self.rewrite_exists_in_value(input, left)?;
1097 self.rewrite_exists_in_value(input, right)
1098 }
1099 ValueExpression::Reduce {
1100 initial,
1101 list,
1102 expression,
1103 ..
1104 } => {
1105 input = self.rewrite_exists_in_value(input, initial)?;
1106 input = self.rewrite_exists_in_value(input, list)?;
1107 self.rewrite_exists_in_value(input, expression)
1108 }
1109 ValueExpression::ListPredicate {
1110 kind,
1111 variable,
1112 list,
1113 predicate,
1114 } if crate::ast::needs_relational_evaluation(predicate) => {
1115 input = self.rewrite_exists_in_value(input, list)?;
1116 let original_list = (**list).clone();
1117 let mut map = (**predicate).clone();
1118 crate::ast::transform_value(&mut map, &mut |value| match value {
1121 ValueExpression::LocalVariable(name) if name == variable => {
1122 *value = ValueExpression::Variable(name.clone());
1123 }
1124 ValueExpression::Field { base, field } if matches!(base.as_ref(), ValueExpression::Variable(name) if name == variable) =>
1125 {
1126 *value = ValueExpression::Property(PropertyRef::new(
1127 variable.clone(),
1128 field.clone(),
1129 ));
1130 }
1131 ValueExpression::Reduce {
1132 accumulator,
1133 variable,
1134 expression,
1135 ..
1136 } => {
1137 crate::ast::mark_locals(expression, &[accumulator, variable]);
1138 }
1139 ValueExpression::ListPredicate {
1140 variable,
1141 predicate,
1142 ..
1143 } => {
1144 crate::ast::mark_locals(predicate, &[variable]);
1145 }
1146 _ => {}
1147 });
1148 let mut comprehension = ValueExpression::ListComprehension {
1149 variable: variable.clone(),
1150 list: list.clone(),
1151 filter: None,
1152 map: Some(Box::new(map)),
1153 };
1154 input = self.rewrite_exists_in_value(input, &mut comprehension)?;
1155 *expr = ValueExpression::Case {
1156 whens: vec![(
1157 BooleanExpression::IsNull(original_list),
1158 ValueExpression::Literal(PropertyValue::Null),
1159 )],
1160 otherwise: Some(Box::new(ValueExpression::ScalarFunction {
1161 name: format!("__predicate_{kind}"),
1162 args: vec![comprehension],
1163 })),
1164 };
1165 Ok(input)
1166 }
1167 ValueExpression::ListPredicate {
1168 list, predicate, ..
1169 } => {
1170 input = self.rewrite_exists_in_value(input, list)?;
1171 self.rewrite_exists_in_value(input, predicate)
1172 }
1173 ValueExpression::Boolean(b) => self.rewrite_exists(input, b),
1174 ValueExpression::Map(entries) => {
1175 for (_, v) in entries.iter_mut() {
1176 input = self.rewrite_exists_in_value(input, v)?;
1177 }
1178 Ok(input)
1179 }
1180 ValueExpression::Field { base, .. } => self.rewrite_exists_in_value(input, base),
1181 ValueExpression::PatternComprehension {
1182 pattern,
1183 filter,
1184 map,
1185 } => {
1186 self.comprehensions += 1;
1189 let column = format!("__pattern_{}", self.comprehensions);
1190 let inner = MatchClause {
1191 patterns: vec![GraphPattern::Path((**pattern).clone())],
1192 optional: true,
1193 where_clause: filter.as_ref().map(|w| WhereClause {
1194 expression: (**w).clone(),
1195 }),
1196 };
1197 let before = self.variables.clone();
1198 let planned = (|| -> Result<(LogicalOperator, ValueExpression)> {
1199 let seed = self.seed_source.clone();
1200 let planned = self.plan_optional_match(Some(input), &inner, seed)?;
1201 let mut value = (**map).clone();
1202 let LogicalOperator::OptionalMatch {
1203 input,
1204 optional,
1205 keys,
1206 } = planned
1207 else {
1208 unreachable!("plan_optional_match returns an OptionalMatch")
1209 };
1210 let saved = self.seed_source.replace((*optional).clone());
1211 let optional = self.rewrite_exists_in_value(*optional, &mut value);
1212 self.seed_source = saved;
1213 let optional = optional?;
1214 self.resolve_value(&mut value);
1215 let mut projections: Vec<ProjectionItem> = keys
1216 .iter()
1217 .map(|k| ProjectionItem {
1218 expression: ValueExpression::Variable(
1219 k.split("__").next().unwrap_or(k).to_string(),
1220 ),
1221 alias: None,
1222 })
1223 .collect();
1224 projections.push(ProjectionItem {
1225 expression: ValueExpression::AggregateFunction {
1226 name: "collect".to_string(),
1227 args: vec![value],
1228 distinct: false,
1229 },
1230 alias: Some(column.clone()),
1231 });
1232 let collected = LogicalOperator::Project {
1233 input: Box::new(optional),
1234 projections,
1235 };
1236 let joined = LogicalOperator::OptionalMatch {
1237 input,
1238 optional: Box::new(collected),
1239 keys,
1240 };
1241 let result = ValueExpression::ScalarFunction {
1243 name: "coalesce".to_string(),
1244 args: vec![
1245 ValueExpression::Variable(column.clone()),
1246 ValueExpression::ScalarFunction {
1247 name: "__list".to_string(),
1248 args: vec![],
1249 },
1250 ],
1251 };
1252 Ok((joined, result))
1253 })();
1254 self.variables = before;
1255 let (joined, result) = planned?;
1256 *expr = result;
1257 Ok(joined)
1258 }
1259 ValueExpression::ListComprehension {
1260 variable,
1261 list,
1262 filter,
1263 map,
1264 } => {
1265 input = self.rewrite_exists_in_value(input, list)?;
1266 self.resolve_value(list);
1267 if self.rich_paths {
1268 if let ValueExpression::ScalarFunction { name, .. } = list.as_mut() {
1269 if name.eq_ignore_ascii_case("nodes") {
1270 *name = "__path_node_records".into();
1271 }
1272 }
1273 }
1274 let label = match list.as_ref() {
1275 ValueExpression::Variable(l) => self.node_lists.get(l).cloned(),
1276 ValueExpression::ScalarFunction { name, args }
1277 if name.eq_ignore_ascii_case("nodes") =>
1278 {
1279 match args.as_slice() {
1280 [ValueExpression::Variable(p)] => self.path_labels.get(p).cloned(),
1281 _ => None,
1282 }
1283 }
1284 _ => None,
1285 };
1286 self.comprehensions += 1;
1287 let output = format!("__list_{}", self.comprehensions);
1288 let before = self.variables.clone();
1290 let saved_seed = self.seed_source.clone();
1291 let kind = label.clone().unwrap_or_else(|| "Unwound".to_string());
1292 self.variables.insert(variable.clone(), kind);
1293 let lowered = (|| -> Result<LogicalOperator> {
1294 let mut element = LogicalOperator::ComprehensionElement;
1295 self.seed_source = Some(LogicalOperator::ComprehensionElement);
1296 let mut keep = filter.as_deref().cloned();
1297 if let Some(k) = &mut keep {
1298 element = self.rewrite_exists(element, k)?;
1299 self.resolve_boolean(k);
1300 }
1301 let mut value = map
1302 .as_deref()
1303 .cloned()
1304 .unwrap_or_else(|| ValueExpression::Variable(variable.clone()));
1305 element = self.rewrite_exists_in_value(element, &mut value)?;
1306 self.resolve_value(&mut value);
1307 Ok(LogicalOperator::ListComprehension {
1308 input: Box::new(input),
1309 list: (**list).clone(),
1310 variable: variable.clone(),
1311 label,
1312 element: Box::new(element),
1313 keep,
1314 map: value,
1315 output: output.clone(),
1316 count_only: false,
1317 })
1318 })();
1319 self.variables = before;
1320 self.seed_source = saved_seed;
1321 *expr = ValueExpression::Variable(output);
1322 lowered
1323 }
1324 _ => Ok(input),
1325 }
1326 }
1327
1328 fn plan_shortest_path(
1331 &mut self,
1332 base: Option<LogicalOperator>,
1333 path: &PathPattern,
1334 ) -> Result<LogicalOperator> {
1335 let spec = path.shortest.as_ref().expect("a shortest-path pattern");
1336 let unsupported = |what: &str| GraphError::UnsupportedFeature {
1337 feature: format!("shortestPath {what}"),
1338 location: Location::new(file!(), line!(), column!()),
1339 };
1340 let [segment] = path.segments.as_slice() else {
1341 return Err(unsupported("over more than one relationship pattern"));
1342 };
1343 let rel = &segment.relationship;
1344 let [rel_type] = rel.types.as_slice() else {
1345 return Err(unsupported("needs exactly one relationship type"));
1346 };
1347 if rel.variable.is_some() || !rel.properties.is_empty() {
1348 return Err(unsupported("with a relationship variable or properties"));
1349 }
1350 let mut plan = base;
1351 let mut ends = Vec::with_capacity(2);
1352 for node in [&path.start_node, &segment.end_node] {
1353 let Some(var) = &node.variable else {
1354 return Err(unsupported("with an unnamed end node"));
1355 };
1356 if !self.variables.contains_key(var) {
1357 let scan = self.plan_node_scan(node)?;
1358 plan = Some(match plan {
1359 None => scan,
1360 Some(p) => LogicalOperator::Join {
1361 left: Box::new(p),
1362 right: Box::new(scan),
1363 join_type: JoinType::Cross,
1364 },
1365 });
1366 }
1367 ends.push(var.clone());
1368 }
1369 let path_variable = spec
1370 .variable
1371 .clone()
1372 .unwrap_or_else(|| format!("_path_{}", self.variables.len()));
1373 self.variables
1375 .insert(path_variable.clone(), "Path".to_string());
1376 if let Some(label) = self.variables.get(&ends[0]).cloned() {
1377 self.path_labels.insert(path_variable.clone(), label);
1378 }
1379 let length = rel.length.as_ref();
1380 if self.rich_paths {
1381 return Ok(LogicalOperator::PathMatch {
1382 input: Box::new(plan.expect("both ends planned")),
1383 path: path.clone(),
1384 });
1385 }
1386 Ok(LogicalOperator::ShortestPath {
1387 input: Box::new(plan.expect("both end nodes are planned")),
1388 source_variable: ends[0].clone(),
1389 target_variable: ends[1].clone(),
1390 relationship_type: rel_type.clone(),
1391 direction: rel.direction.clone(),
1392 min_hops: length.and_then(|l| l.min).unwrap_or(1),
1393 max_hops: length.and_then(|l| l.max).unwrap_or(u32::MAX),
1394 all: spec.all,
1395 path_variable,
1396 })
1397 }
1398
1399 fn plan_optional_match(
1405 &mut self,
1406 base: Option<LogicalOperator>,
1407 clause: &MatchClause,
1408 seed_source: Option<LogicalOperator>,
1409 ) -> Result<LogicalOperator> {
1410 let unsupported = |what: &str| GraphError::UnsupportedFeature {
1411 feature: format!("OPTIONAL MATCH {what}"),
1412 location: Location::new(file!(), line!(), column!()),
1413 };
1414 let base = base.ok_or_else(|| unsupported("as the first clause"))?;
1415 let mut shared = Vec::new();
1416 for pattern in &clause.patterns {
1417 let nodes: Vec<&NodePattern> = match pattern {
1418 GraphPattern::Node(n) => vec![n],
1419 GraphPattern::Path(p) => std::iter::once(&p.start_node)
1420 .chain(p.segments.iter().map(|s| &s.end_node))
1421 .collect(),
1422 };
1423 for var in nodes.into_iter().filter_map(|n| n.variable.as_ref()) {
1424 if self.variables.contains_key(var) && !shared.contains(var) {
1425 shared.push(var.clone());
1426 }
1427 }
1428 }
1429 let mut keys = Vec::with_capacity(shared.len());
1430 for var in &shared {
1431 let label = &self.variables[var];
1432 let mapping = self.config.get_node_mapping(label).ok_or_else(|| {
1433 unsupported(&format!("from '{var}', whose label '{label}' is ambiguous"))
1434 })?;
1435 keys.push(qualify_column(var, &mapping.id_field));
1436 }
1437 let mut carried = shared.clone();
1439 if let Some(w) = &clause.where_clause {
1440 let mut names = std::collections::HashSet::new();
1441 crate::pattern_order::boolean_variables(&w.expression, &mut names);
1442 let local = crate::pattern_order::pattern_variables(&clause.patterns);
1443 let mut scalars: Vec<String> = names
1444 .into_iter()
1445 .filter(|n| !local.contains(*n) && !shared.iter().any(|v| v == *n))
1446 .map(str::to_string)
1447 .collect();
1448 scalars.sort();
1449 for variable in &scalars {
1450 if !self.variables.contains_key(variable) && !shared.is_empty() {
1453 continue;
1454 }
1455 keys.push(
1456 self.variables
1457 .get(variable)
1458 .and_then(|label| self.config.get_node_mapping(label))
1459 .map(|mapping| qualify_column(variable, &mapping.id_field))
1460 .unwrap_or_else(|| variable.to_lowercase()),
1461 );
1462 }
1463 carried.extend(scalars);
1464 }
1465 if carried.is_empty() {
1466 return Err(unsupported("without a variable bound by an earlier clause"));
1467 }
1468 let seed = LogicalOperator::Distinct {
1469 input: Box::new(LogicalOperator::Project {
1470 input: Box::new(seed_source.unwrap_or_else(|| base.clone())),
1471 projections: carried
1472 .iter()
1473 .map(|var| ProjectionItem {
1474 expression: ValueExpression::Variable(var.clone()),
1475 alias: None,
1476 })
1477 .collect(),
1478 }),
1479 };
1480 let inner = MatchClause {
1481 patterns: clause.patterns.clone(),
1482 optional: false,
1483 where_clause: None,
1484 };
1485 let mut optional = self.plan_match_clause_with_base(Some(seed), &inner, None)?;
1486 if let Some(w) = &clause.where_clause {
1487 optional = self.filter(optional, &w.expression)?;
1488 }
1489 Ok(LogicalOperator::OptionalMatch {
1490 input: Box::new(base),
1491 optional: Box::new(optional),
1492 keys,
1493 })
1494 }
1495
1496 fn plan_match_branches(
1500 &mut self,
1501 base: Option<LogicalOperator>,
1502 branches: Vec<Vec<GraphPattern>>,
1503 filter: Option<&BooleanExpression>,
1504 ) -> Result<LogicalOperator> {
1505 let before = self.variables.clone();
1506 let mut inputs = Vec::new();
1507 let mut labels: HashMap<String, Vec<String>> = HashMap::new();
1508 for patterns in branches {
1509 self.variables = before.clone();
1510 let clause = MatchClause {
1511 patterns,
1512 optional: false,
1513 where_clause: None,
1514 };
1515 inputs.push(self.plan_match_clause_with_base(base.clone(), &clause, filter)?);
1516 for (var, label) in &self.variables {
1517 let seen = labels.entry(var.clone()).or_default();
1518 if !seen.contains(label) {
1519 seen.push(label.clone());
1520 }
1521 }
1522 }
1523 self.variables = labels
1526 .into_iter()
1527 .map(|(var, labels)| (var, labels.join("|")))
1528 .collect();
1529 Ok(if inputs.len() == 1 {
1530 inputs.pop().expect("one branch")
1531 } else {
1532 LogicalOperator::Union { inputs }
1533 })
1534 }
1535
1536 fn plan_node_scan(&mut self, node: &NodePattern) -> Result<LogicalOperator> {
1538 let variable = node
1539 .variable
1540 .clone()
1541 .unwrap_or_else(|| format!("_node_{}", self.variables.len()));
1542
1543 self.validate_variable_label(&variable, &node.labels)?;
1545
1546 let label = self
1548 .variables
1549 .get(&variable)
1550 .cloned()
1551 .or_else(|| node.labels.first().cloned())
1552 .unwrap_or_else(|| "Node".to_string());
1553
1554 self.variables.insert(variable.clone(), label.clone());
1556
1557 Ok(LogicalOperator::ScanByLabel {
1558 variable,
1559 label,
1560 properties: node.properties.clone(),
1561 })
1562 }
1563
1564 fn validate_variable_label(&self, variable: &str, ast_labels: &[String]) -> Result<()> {
1568 if let Some(existing_label) = self.variables.get(variable) {
1569 if let Some(ast_label) = ast_labels.first() {
1570 if ast_label != existing_label {
1571 return Err(GraphError::PlanError {
1572 message: format!(
1573 "Variable '{}' already has label '{}', cannot redefine as '{}'",
1574 variable, existing_label, ast_label
1575 ),
1576 location: snafu::Location::new(file!(), line!(), column!()),
1577 });
1578 }
1579 }
1580 }
1581 Ok(())
1582 }
1583
1584 fn plan_path(
1586 &mut self,
1587 base: Option<LogicalOperator>,
1588 path: &PathPattern,
1589 ) -> Result<LogicalOperator> {
1590 let start_bound = path
1593 .start_node
1594 .variable
1595 .as_ref()
1596 .is_some_and(|v| self.variables.contains_key(v));
1597 let (mut plan, mut current_src) = match base {
1598 Some(p) if start_bound => {
1599 let var = path
1600 .start_node
1601 .variable
1602 .clone()
1603 .expect("bound start is named");
1604 (p, var)
1605 }
1606 base => {
1607 let scan = self.plan_node_scan(&path.start_node)?;
1608 let var = self.extract_variable_from_plan(&scan)?;
1609 let plan = match base {
1610 Some(p) => LogicalOperator::Join {
1611 left: Box::new(p),
1612 right: Box::new(scan),
1613 join_type: JoinType::Cross,
1614 },
1615 None => scan,
1616 };
1617 (plan, var)
1618 }
1619 };
1620
1621 if let Some(start_var) = &path.start_node.variable {
1623 self.validate_variable_label(start_var, &path.start_node.labels)?;
1624 }
1625
1626 for segment in &path.segments {
1628 let target_variable = segment
1630 .end_node
1631 .variable
1632 .clone()
1633 .unwrap_or_else(|| format!("_node_{}", self.variables.len()));
1634
1635 self.validate_variable_label(&target_variable, &segment.end_node.labels)?;
1637
1638 let target_label = self
1640 .variables
1641 .get(&target_variable)
1642 .cloned()
1643 .or_else(|| segment.end_node.labels.first().cloned())
1644 .unwrap_or_else(|| "Node".to_string());
1645
1646 self.variables
1647 .insert(target_variable.clone(), target_label.clone());
1648
1649 let next_plan = match segment.relationship.length.as_ref() {
1651 Some(length_range)
1652 if length_range.min == Some(1) && length_range.max == Some(1) =>
1653 {
1654 LogicalOperator::Expand {
1655 input: Box::new(plan),
1656 source_variable: current_src.clone(),
1657 target_variable: target_variable.clone(),
1658 target_label: target_label.clone(),
1659 relationship_types: segment.relationship.types.clone(),
1660 direction: segment.relationship.direction.clone(),
1661 relationship_variable: segment.relationship.variable.clone(),
1662 properties: segment.relationship.properties.clone(),
1663 target_properties: segment.end_node.properties.clone(),
1664 }
1665 }
1666 Some(length_range) => LogicalOperator::VariableLengthExpand {
1667 input: Box::new(plan),
1668 source_variable: current_src.clone(),
1669 target_variable: target_variable.clone(),
1670 target_label: target_label.clone(),
1671 relationship_types: segment.relationship.types.clone(),
1672 direction: segment.relationship.direction.clone(),
1673 relationship_variable: segment.relationship.variable.clone(),
1674 min_length: length_range.min,
1675 max_length: length_range.max,
1676 target_properties: segment.end_node.properties.clone(),
1677 },
1678 None => LogicalOperator::Expand {
1679 input: Box::new(plan),
1680 source_variable: current_src.clone(),
1681 target_variable: target_variable.clone(),
1682 target_label: target_label.clone(),
1683 relationship_types: segment.relationship.types.clone(),
1684 direction: segment.relationship.direction.clone(),
1685 relationship_variable: segment.relationship.variable.clone(),
1686 properties: segment.relationship.properties.clone(),
1687 target_properties: segment.end_node.properties.clone(),
1688 },
1689 };
1690
1691 plan = next_plan;
1692 current_src = target_variable;
1693 }
1694
1695 Ok(plan)
1696 }
1697
1698 fn plan_named_path(
1699 &mut self,
1700 base: Option<LogicalOperator>,
1701 path: &PathPattern,
1702 ) -> Result<LogicalOperator> {
1703 let mut path = path.clone();
1704 if path.start_node.variable.is_none() {
1705 path.start_node.variable = Some(format!("_path_node_{}", self.variables.len()));
1706 }
1707 let start = path.start_node.variable.as_ref().unwrap();
1708 let base = if self.variables.contains_key(start) {
1709 base.expect("bound variables have input rows")
1710 } else {
1711 let scan = self.plan_node_scan(&path.start_node)?;
1712 match base {
1713 Some(input) => LogicalOperator::Join {
1714 left: Box::new(input),
1715 right: Box::new(scan),
1716 join_type: JoinType::Cross,
1717 },
1718 None => scan,
1719 }
1720 };
1721 for segment in &mut path.segments {
1722 if segment.end_node.variable.is_none() {
1723 segment.end_node.variable = Some(format!("_path_node_{}", self.variables.len()));
1724 }
1725 let variable = segment.end_node.variable.as_ref().unwrap();
1726 self.validate_variable_label(variable, &segment.end_node.labels)?;
1727 let label = self
1728 .variables
1729 .get(variable)
1730 .cloned()
1731 .or_else(|| segment.end_node.labels.first().cloned())
1732 .ok_or_else(|| GraphError::UnsupportedFeature {
1733 feature: "named path boundary needs a resolved node label".into(),
1734 location: Location::new(file!(), line!(), column!()),
1735 })?;
1736 self.variables.insert(variable.clone(), label);
1737 }
1738 self.variables
1739 .insert(path.variable.clone().unwrap(), "Path".into());
1740 Ok(LogicalOperator::PathMatch {
1741 input: Box::new(base),
1742 path,
1743 })
1744 }
1745
1746 #[allow(clippy::only_used_in_recursion)]
1748 fn extract_variable_from_plan(&self, plan: &LogicalOperator) -> Result<String> {
1749 match plan {
1750 LogicalOperator::Apply { input, .. } => self.extract_variable_from_plan(input),
1751 LogicalOperator::CallInput { .. } | LogicalOperator::SingleRow => {
1752 Err(GraphError::PlanError {
1753 message: "a singleton row binds no pattern variable".to_string(),
1754 location: snafu::Location::new(file!(), line!(), column!()),
1755 })
1756 }
1757 LogicalOperator::ScanByLabel { variable, .. } => Ok(variable.clone()),
1758 LogicalOperator::Unwind { alias, .. } => Ok(alias.clone()),
1759 LogicalOperator::Expand {
1760 target_variable, ..
1761 } => Ok(target_variable.clone()),
1762 LogicalOperator::VariableLengthExpand {
1763 target_variable, ..
1764 } => Ok(target_variable.clone()),
1765 LogicalOperator::Filter { input, .. } => self.extract_variable_from_plan(input),
1766 LogicalOperator::Project { input, .. } => self.extract_variable_from_plan(input),
1767 LogicalOperator::Distinct { input } => self.extract_variable_from_plan(input),
1768 LogicalOperator::Sort { input, .. } => self.extract_variable_from_plan(input),
1769 LogicalOperator::Offset { input, .. } => self.extract_variable_from_plan(input),
1770 LogicalOperator::Limit { input, .. } => self.extract_variable_from_plan(input),
1771 LogicalOperator::Join { left, right, .. }
1772 | LogicalOperator::QueryUnion { left, right, .. } => {
1773 self.extract_variable_from_plan(right)
1775 .or_else(|_| self.extract_variable_from_plan(left))
1776 }
1777 LogicalOperator::OptionalMatch { optional, .. } => {
1778 self.extract_variable_from_plan(optional)
1779 }
1780 LogicalOperator::ListComprehension { input, .. } => {
1781 self.extract_variable_from_plan(input)
1782 }
1783 LogicalOperator::ComprehensionElement => Err(GraphError::PlanError {
1784 message: "a comprehension's element rows bind no pattern variable".to_string(),
1785 location: snafu::Location::new(file!(), line!(), column!()),
1786 }),
1787 LogicalOperator::PathMatch { path, .. } => Ok(path
1788 .segments
1789 .last()
1790 .unwrap()
1791 .end_node
1792 .variable
1793 .clone()
1794 .unwrap()),
1795 LogicalOperator::ShortestPath {
1796 target_variable, ..
1797 } => Ok(target_variable.clone()),
1798 LogicalOperator::Union { inputs } => match inputs.first() {
1799 Some(first) => self.extract_variable_from_plan(first),
1800 None => Err(GraphError::PlanError {
1801 message: "empty UNION".to_string(),
1802 location: snafu::Location::new(file!(), line!(), column!()),
1803 }),
1804 },
1805 }
1806 }
1807
1808 fn plan_return_clause(
1810 &mut self,
1811 return_clause: &ReturnClause,
1812 input: LogicalOperator,
1813 ) -> Result<LogicalOperator> {
1814 let mut projections: Vec<ProjectionItem> = Vec::new();
1815 let mut items = return_clause.items.clone();
1816 let mut input = input;
1817 let saved = self.seed_source.replace(input.clone());
1818 for item in &mut items {
1819 let rewritten = self.rewrite_exists_in_value(input, &mut item.expression);
1820 input = match rewritten {
1821 Ok(input) => input,
1822 Err(e) => {
1823 self.seed_source = saved;
1824 return Err(e);
1825 }
1826 };
1827 }
1828 self.seed_source = saved;
1829
1830 for item in &items {
1831 let alias = &item.alias;
1832 match &item.expression {
1833 ValueExpression::Variable(var) => {
1834 match self.variables.get(var) {
1835 Some(label) if label != "Unwound" && label != "Path" => {
1837 let mapping = self.config.get_node_mapping(label).ok_or_else(|| {
1838 GraphError::PlanError {
1839 message: format!("Node label '{}' doesn't exist", label),
1840 location: Location::new(file!(), line!(), column!()),
1841 }
1842 })?;
1843
1844 projections.push(ProjectionItem {
1845 expression: ValueExpression::Property(PropertyRef {
1846 variable: var.clone(),
1847 property: mapping.id_field.clone(),
1848 }),
1849 alias: alias
1850 .clone()
1851 .map(|name| format!("{}.{}", name, mapping.id_field)),
1852 });
1853
1854 for prop in &mapping.property_fields {
1855 projections.push(ProjectionItem {
1856 expression: ValueExpression::Property(PropertyRef {
1857 variable: var.clone(),
1858 property: prop.clone(),
1859 }),
1860 alias: alias.clone().map(|name| format!("{}.{}", name, prop)),
1861 });
1862 }
1863 }
1864 _ => {
1865 projections.push(ProjectionItem {
1866 expression: item.expression.clone(),
1867 alias: alias.clone(),
1868 });
1869 }
1870 }
1871 }
1872 _ => {
1873 let mut expression = item.expression.clone();
1874 self.resolve_value(&mut expression);
1875 projections.push(ProjectionItem {
1876 expression,
1877 alias: alias.clone(),
1878 });
1879 }
1880 }
1881 }
1882
1883 self.variables.retain(|variable, _| {
1886 !projections.iter().any(|projection| {
1887 projection
1888 .alias
1889 .as_ref()
1890 .is_some_and(|alias| alias.eq_ignore_ascii_case(variable))
1891 })
1892 });
1893
1894 let mut plan = LogicalOperator::Project {
1895 input: Box::new(input),
1896 projections,
1897 };
1898
1899 if return_clause.distinct {
1901 plan = LogicalOperator::Distinct {
1902 input: Box::new(plan),
1903 };
1904 }
1905
1906 Ok(plan)
1907 }
1908
1909 fn plan_with_clause(
1911 &mut self,
1912 with_clause: &WithClause,
1913 input: LogicalOperator,
1914 ) -> Result<LogicalOperator> {
1915 let mut projections = Vec::with_capacity(with_clause.items.len());
1919 let mut items = with_clause.items.clone();
1920 let mut input = input;
1921 let saved = self.seed_source.replace(input.clone());
1922 for item in &mut items {
1923 let rewritten = self.rewrite_exists_in_value(input, &mut item.expression);
1924 input = match rewritten {
1925 Ok(input) => input,
1926 Err(e) => {
1927 self.seed_source = saved;
1928 return Err(e);
1929 }
1930 };
1931 }
1932 self.seed_source = saved;
1933 for item in &items {
1934 let alias = match (&item.alias, &item.expression) {
1935 (Some(alias), ValueExpression::Variable(var)) => {
1936 if let Some(label) = self.variables.get(var).cloned() {
1938 self.variables.insert(alias.clone(), label);
1939 }
1940 if let Some(label) = self.path_labels.get(var).cloned() {
1941 self.path_labels.insert(alias.clone(), label);
1942 }
1943 Some(alias.clone())
1944 }
1945 (Some(alias), _) => Some(alias.clone()),
1946 (None, ValueExpression::Property(prop)) => {
1947 Some(qualify_column(&prop.variable, &prop.property))
1948 }
1949 (None, ValueExpression::Variable(var)) => Some(var.clone()),
1952 (None, _) => None,
1953 };
1954 let mut expression = item.expression.clone();
1955 if !matches!(expression, ValueExpression::Variable(_)) {
1956 self.resolve_value(&mut expression);
1957 }
1958 projections.push(ProjectionItem { expression, alias });
1959 }
1960 let mut node_lists = HashMap::new();
1962 for item in &items {
1963 let label = match &item.expression {
1964 ValueExpression::AggregateFunction { name, args, .. }
1965 if name.eq_ignore_ascii_case("collect") =>
1966 {
1967 match args.as_slice() {
1968 [ValueExpression::Variable(v)] => self
1969 .variables
1970 .get(v)
1971 .filter(|l| self.config.get_node_mapping(l).is_some())
1972 .cloned(),
1973 _ => None,
1974 }
1975 }
1976 ValueExpression::Variable(v) => self.node_lists.get(v).cloned(),
1977 _ => None,
1978 };
1979 let name = match (&item.alias, &item.expression) {
1980 (Some(alias), _) => Some(alias.clone()),
1981 (None, ValueExpression::Variable(v)) => Some(v.clone()),
1982 _ => None,
1983 };
1984 if let (Some(label), Some(name)) = (label, name) {
1985 node_lists.insert(name, label);
1986 }
1987 }
1988 self.node_lists = node_lists;
1989 let kept: Vec<&str> = with_clause
1992 .items
1993 .iter()
1994 .filter_map(|item| match (&item.alias, &item.expression) {
1995 (Some(alias), ValueExpression::Variable(_)) => Some(alias.as_str()),
1996 (None, ValueExpression::Variable(var)) => Some(var.as_str()),
1997 _ => None,
1998 })
1999 .collect();
2000 self.variables.retain(|var, _| kept.contains(&var.as_str()));
2001
2002 let mut plan = LogicalOperator::Project {
2003 input: Box::new(input),
2004 projections,
2005 };
2006 if with_clause.distinct {
2007 plan = LogicalOperator::Distinct {
2008 input: Box::new(plan),
2009 };
2010 }
2011
2012 if let Some(order_by) = &with_clause.order_by {
2014 plan = LogicalOperator::Sort {
2015 input: Box::new(plan),
2016 sort_items: self.sort_items(order_by),
2017 };
2018 }
2019
2020 if let Some(limit) = &with_clause.limit {
2022 plan = LogicalOperator::Limit {
2023 input: Box::new(plan),
2024 count: limit.value()?,
2025 };
2026 }
2027
2028 if let Some(where_clause) = &with_clause.where_clause {
2029 plan = self.filter(plan, &where_clause.expression)?;
2030 }
2031
2032 Ok(plan)
2033 }
2034}
2035
2036#[cfg(test)]
2037mod tests {
2038 use super::*;
2039 use crate::{parser::parse_cypher_query, NodeMapping};
2040
2041 #[test]
2042 fn return_alias_shadows_node_only_after_projection() {
2043 let config = GraphConfig::builder()
2044 .node_table("Person", "id")
2045 .build()
2046 .unwrap();
2047 let query =
2048 parse_cypher_query("MATCH (p:Person) RETURN p.name AS P, p.id AS id ORDER BY p")
2049 .unwrap();
2050 let LogicalOperator::Sort { input, sort_items } =
2051 LogicalPlanner::new(&config).plan(&query).unwrap()
2052 else {
2053 panic!("expected sort");
2054 };
2055 assert_eq!(
2056 sort_items[0].expression,
2057 ValueExpression::Variable("p".into())
2058 );
2059 let LogicalOperator::Project { projections, .. } = *input else {
2060 panic!("expected projection");
2061 };
2062 assert_eq!(
2063 projections[1].expression,
2064 ValueExpression::Property(PropertyRef {
2065 variable: "p".into(),
2066 property: "id".into(),
2067 })
2068 );
2069
2070 let query = parse_cypher_query("MATCH (p:Person) RETURN p AS p ORDER BY p").unwrap();
2071 let LogicalOperator::Sort { sort_items, .. } =
2072 LogicalPlanner::new(&config).plan(&query).unwrap()
2073 else {
2074 panic!("expected sort");
2075 };
2076 assert_eq!(sort_items[0].expression, projections[1].expression);
2077 }
2078
2079 #[test]
2080 fn test_relationship_query_logical_plan_structure() {
2081 let query_text = r#"MATCH (p:Person {name: "Alice"})-[:KNOWS]->(f:Person) WHERE f.age > 30 RETURN f.name"#;
2082
2083 let ast = parse_cypher_query(query_text).unwrap();
2085
2086 let config = GraphConfig::default();
2088 let mut planner = LogicalPlanner::new(&config);
2089 let logical_plan = planner.plan(&ast).unwrap();
2090
2091 match &logical_plan {
2093 LogicalOperator::Project { input, projections } => {
2094 assert_eq!(projections.len(), 1);
2096 match &projections[0].expression {
2097 ValueExpression::Property(prop_ref) => {
2098 assert_eq!(prop_ref.variable, "f");
2099 assert_eq!(prop_ref.property, "name");
2100 }
2101 _ => panic!("Expected property reference for f.name"),
2102 }
2103
2104 match input.as_ref() {
2106 LogicalOperator::Filter {
2107 predicate,
2108 input: filter_input,
2109 } => {
2110 match predicate {
2112 BooleanExpression::Comparison {
2113 left,
2114 operator,
2115 right,
2116 } => {
2117 match left {
2118 ValueExpression::Property(prop_ref) => {
2119 assert_eq!(prop_ref.variable, "f");
2120 assert_eq!(prop_ref.property, "age");
2121 }
2122 _ => panic!("Expected property reference for f.age"),
2123 }
2124 assert_eq!(*operator, ComparisonOperator::GreaterThan);
2125 match right {
2126 ValueExpression::Literal(PropertyValue::Integer(val)) => {
2127 assert_eq!(*val, 30);
2128 }
2129 _ => panic!("Expected integer literal 30"),
2130 }
2131 }
2132 _ => panic!("Expected comparison expression"),
2133 }
2134
2135 match filter_input.as_ref() {
2137 LogicalOperator::Expand {
2138 input: expand_input,
2139 source_variable,
2140 target_variable,
2141 relationship_types,
2142 direction,
2143 ..
2144 } => {
2145 assert_eq!(source_variable, "p");
2146 assert_eq!(target_variable, "f");
2147 assert_eq!(relationship_types, &vec!["KNOWS".to_string()]);
2148 assert_eq!(*direction, RelationshipDirection::Outgoing);
2149
2150 match expand_input.as_ref() {
2152 LogicalOperator::ScanByLabel {
2153 variable,
2154 label,
2155 properties,
2156 } => {
2157 assert_eq!(variable, "p");
2158 assert_eq!(label, "Person");
2159
2160 assert_eq!(properties.len(), 1);
2162 match properties.get("name") {
2163 Some(PropertyValue::String(val)) => {
2164 assert_eq!(val, "Alice");
2165 }
2166 _ => {
2167 panic!("Expected name property with value 'Alice'")
2168 }
2169 }
2170 }
2171 _ => panic!("Expected ScanByLabel with properties for Person"),
2172 }
2173 }
2174 _ => panic!("Expected Expand operation"),
2175 }
2176 }
2177 _ => panic!("Expected Filter for f.age > 30"),
2178 }
2179 }
2180 _ => panic!("Expected Project at the top level"),
2181 }
2182 }
2183
2184 #[test]
2185 fn test_simple_node_query_logical_plan() {
2186 let query_text = "MATCH (n:Person) RETURN n.name";
2187
2188 let ast = parse_cypher_query(query_text).unwrap();
2189 let config = GraphConfig::default();
2190 let mut planner = LogicalPlanner::new(&config);
2191 let logical_plan = planner.plan(&ast).unwrap();
2192
2193 match &logical_plan {
2195 LogicalOperator::Project { input, projections } => {
2196 assert_eq!(projections.len(), 1);
2197 match input.as_ref() {
2198 LogicalOperator::ScanByLabel {
2199 variable, label, ..
2200 } => {
2201 assert_eq!(variable, "n");
2202 assert_eq!(label, "Person");
2203 }
2204 _ => panic!("Expected ScanByLabel"),
2205 }
2206 }
2207 _ => panic!("Expected Project"),
2208 }
2209 }
2210
2211 #[test]
2212 fn test_node_with_properties_logical_plan() {
2213 let query_text = "MATCH (n:Person {age: 25}) RETURN n.name";
2214
2215 let ast = parse_cypher_query(query_text).unwrap();
2216 let config = GraphConfig::default();
2217 let mut planner = LogicalPlanner::new(&config);
2218 let logical_plan = planner.plan(&ast).unwrap();
2219
2220 match &logical_plan {
2223 LogicalOperator::Project { input, .. } => {
2224 match input.as_ref() {
2225 LogicalOperator::ScanByLabel {
2226 variable,
2227 label,
2228 properties,
2229 } => {
2230 assert_eq!(variable, "n");
2231 assert_eq!(label, "Person");
2232
2233 assert_eq!(properties.len(), 1);
2235 match properties.get("age") {
2236 Some(PropertyValue::Integer(25)) => {}
2237 _ => panic!("Expected age property with value 25"),
2238 }
2239 }
2240 _ => panic!("Expected ScanByLabel with properties"),
2241 }
2242 }
2243 _ => panic!("Expected Project"),
2244 }
2245 }
2246
2247 #[test]
2248 fn test_variable_length_path_logical_plan() {
2249 let query_text = "MATCH (a:Person)-[:KNOWS*1..2]->(b:Person) RETURN b.name";
2250
2251 let ast = parse_cypher_query(query_text).unwrap();
2252 let config = GraphConfig::default();
2253 let mut planner = LogicalPlanner::new(&config);
2254 let logical_plan = planner.plan(&ast).unwrap();
2255
2256 match &logical_plan {
2258 LogicalOperator::Project { input, .. } => match input.as_ref() {
2259 LogicalOperator::VariableLengthExpand {
2260 input: expand_input,
2261 source_variable,
2262 target_variable,
2263 relationship_types,
2264 min_length,
2265 max_length,
2266 ..
2267 } => {
2268 assert_eq!(source_variable, "a");
2269 assert_eq!(target_variable, "b");
2270 assert_eq!(relationship_types, &vec!["KNOWS".to_string()]);
2271 assert_eq!(*min_length, Some(1));
2272 assert_eq!(*max_length, Some(2));
2273
2274 match expand_input.as_ref() {
2275 LogicalOperator::ScanByLabel {
2276 variable, label, ..
2277 } => {
2278 assert_eq!(variable, "a");
2279 assert_eq!(label, "Person");
2280 }
2281 _ => panic!("Expected ScanByLabel"),
2282 }
2283 }
2284 _ => panic!("Expected VariableLengthExpand"),
2285 },
2286 _ => panic!("Expected Project"),
2287 }
2288 }
2289
2290 #[test]
2291 fn test_where_clause_logical_plan() {
2292 let query_text = r#"MATCH (n:Person) WHERE n.age > 25 RETURN n.name"#;
2294
2295 let ast = parse_cypher_query(query_text).unwrap();
2296 let config = GraphConfig::default();
2297 let mut planner = LogicalPlanner::new(&config);
2298 let logical_plan = planner.plan(&ast).unwrap();
2299
2300 match &logical_plan {
2302 LogicalOperator::Project { input, .. } => {
2303 match input.as_ref() {
2304 LogicalOperator::Filter {
2305 predicate,
2306 input: scan_input,
2307 } => {
2308 match predicate {
2310 BooleanExpression::Comparison {
2311 left,
2312 operator,
2313 right: _,
2314 } => {
2315 match left {
2316 ValueExpression::Property(prop_ref) => {
2317 assert_eq!(prop_ref.variable, "n");
2318 assert_eq!(prop_ref.property, "age");
2319 }
2320 _ => panic!("Expected property reference for age"),
2321 }
2322 assert_eq!(*operator, ComparisonOperator::GreaterThan);
2323 }
2324 _ => panic!("Expected comparison expression"),
2325 }
2326
2327 match scan_input.as_ref() {
2328 LogicalOperator::ScanByLabel { .. } => {}
2329 _ => panic!("Expected ScanByLabel"),
2330 }
2331 }
2332 _ => panic!("Expected Filter"),
2333 }
2334 }
2335 _ => panic!("Expected Project"),
2336 }
2337 }
2338
2339 #[test]
2340 fn test_multiple_node_patterns_join_in_match() {
2341 let query_text = "MATCH (a:Person), (b:Company) RETURN a.name, b.name";
2342
2343 let ast = parse_cypher_query(query_text).unwrap();
2344 let config = GraphConfig::default();
2345 let mut planner = LogicalPlanner::new(&config);
2346 let logical_plan = planner.plan(&ast).unwrap();
2347
2348 match &logical_plan {
2350 LogicalOperator::Project { input, projections } => {
2351 assert_eq!(projections.len(), 2);
2352 match input.as_ref() {
2353 LogicalOperator::Join {
2354 left,
2355 right,
2356 join_type,
2357 } => {
2358 assert!(matches!(join_type, JoinType::Cross));
2359 match left.as_ref() {
2360 LogicalOperator::ScanByLabel {
2361 variable, label, ..
2362 } => {
2363 assert_eq!(variable, "a");
2364 assert_eq!(label, "Person");
2365 }
2366 _ => panic!("Expected left ScanByLabel for a:Person"),
2367 }
2368 match right.as_ref() {
2369 LogicalOperator::ScanByLabel {
2370 variable, label, ..
2371 } => {
2372 assert_eq!(variable, "b");
2373 assert_eq!(label, "Company");
2374 }
2375 _ => panic!("Expected right ScanByLabel for b:Company"),
2376 }
2377 }
2378 _ => panic!("Expected Join after Project"),
2379 }
2380 }
2381 _ => panic!("Expected Project at top level"),
2382 }
2383 }
2384
2385 #[test]
2386 fn test_shared_variable_chained_paths_in_match() {
2387 let query_text =
2388 "MATCH (a:Person)-[:KNOWS]->(b:Person), (b)-[:LIKES]->(c:Thing) RETURN c.name";
2389
2390 let ast = parse_cypher_query(query_text).unwrap();
2391 let config = GraphConfig::default();
2392 let mut planner = LogicalPlanner::new(&config);
2393 let logical_plan = planner.plan(&ast).unwrap();
2394
2395 match &logical_plan {
2397 LogicalOperator::Project { input, .. } => match input.as_ref() {
2398 LogicalOperator::Expand {
2399 source_variable: src2,
2400 target_variable: tgt2,
2401 input: inner2,
2402 ..
2403 } => {
2404 assert_eq!(src2, "b");
2405 assert_eq!(tgt2, "c");
2406 match inner2.as_ref() {
2407 LogicalOperator::Expand {
2408 source_variable: src1,
2409 target_variable: tgt1,
2410 input: inner1,
2411 ..
2412 } => {
2413 assert_eq!(src1, "a");
2414 assert_eq!(tgt1, "b");
2415 match inner1.as_ref() {
2416 LogicalOperator::ScanByLabel {
2417 variable, label, ..
2418 } => {
2419 assert_eq!(variable, "a");
2420 assert_eq!(label, "Person");
2421 }
2422 _ => panic!("Expected ScanByLabel for a:Person"),
2423 }
2424 }
2425 _ => panic!("Expected first Expand a->b"),
2426 }
2427 }
2428 _ => panic!("Expected second Expand b->c at top of input"),
2429 },
2430 _ => panic!("Expected Project at top level"),
2431 }
2432 }
2433
2434 #[test]
2435 fn test_fixed_length_variable_path_is_expand() {
2436 let query_text = "MATCH (a:Person)-[:KNOWS*1..1]->(b:Person) RETURN b.name";
2437
2438 let ast = parse_cypher_query(query_text).unwrap();
2439 let config = GraphConfig::default();
2440 let mut planner = LogicalPlanner::new(&config);
2441 let logical_plan = planner.plan(&ast).unwrap();
2442
2443 match &logical_plan {
2444 LogicalOperator::Project { input, .. } => match input.as_ref() {
2445 LogicalOperator::Expand {
2446 source_variable,
2447 target_variable,
2448 ..
2449 } => {
2450 assert_eq!(source_variable, "a");
2451 assert_eq!(target_variable, "b");
2452 }
2453 _ => panic!("Expected Expand for fixed-length *1..1"),
2454 },
2455 _ => panic!("Expected Project at top level"),
2456 }
2457 }
2458
2459 #[test]
2460 fn test_distinct_and_order_limit_wrapping() {
2461 let q1 = "MATCH (n:Person) RETURN DISTINCT n.name";
2463 let ast1 = parse_cypher_query(q1).unwrap();
2464 let config = GraphConfig::default();
2465 let mut planner = LogicalPlanner::new(&config);
2466 let logical1 = planner.plan(&ast1).unwrap();
2467 match logical1 {
2468 LogicalOperator::Distinct { input } => match *input {
2469 LogicalOperator::Project { .. } => {}
2470 _ => panic!("Expected Project under Distinct"),
2471 },
2472 _ => panic!("Expected Distinct at top level"),
2473 }
2474
2475 let q2 = "MATCH (n:Person) RETURN n.name ORDER BY n.name LIMIT 10";
2477 let ast2 = parse_cypher_query(q2).unwrap();
2478 let mut planner2 = LogicalPlanner::new(&config);
2479 let logical2 = planner2.plan(&ast2).unwrap();
2480 match logical2 {
2481 LogicalOperator::Limit { input, count } => {
2482 assert_eq!(count, 10);
2483 match *input {
2484 LogicalOperator::Sort { input: inner, .. } => match *inner {
2485 LogicalOperator::Project { .. } => {}
2486 _ => panic!("Expected Project under Sort"),
2487 },
2488 _ => panic!("Expected Sort under Limit"),
2489 }
2490 }
2491 _ => panic!("Expected Limit at top level"),
2492 }
2493 }
2494
2495 #[test]
2496 fn test_order_skip_limit_wrapping() {
2497 let q = "MATCH (n:Person) RETURN n.name ORDER BY n.name SKIP 5 LIMIT 10";
2499 let ast = parse_cypher_query(q).unwrap();
2500 let config = GraphConfig::default();
2501 let mut planner = LogicalPlanner::new(&config);
2502 let logical = planner.plan(&ast).unwrap();
2503 match logical {
2504 LogicalOperator::Limit { input, count } => {
2505 assert_eq!(count, 10);
2506 match *input {
2507 LogicalOperator::Offset {
2508 input: inner,
2509 offset,
2510 } => {
2511 assert_eq!(offset, 5);
2512 match *inner {
2513 LogicalOperator::Sort { input: inner2, .. } => match *inner2 {
2514 LogicalOperator::Project { .. } => {}
2515 _ => panic!("Expected Project under Sort"),
2516 },
2517 _ => panic!("Expected Sort under Offset"),
2518 }
2519 }
2520 _ => panic!("Expected Offset under Limit"),
2521 }
2522 }
2523 _ => panic!("Expected Limit at top level"),
2524 }
2525 }
2526
2527 #[test]
2528 fn test_skip_only_wrapping() {
2529 let q = "MATCH (n:Person) RETURN n.name SKIP 3";
2531 let ast = parse_cypher_query(q).unwrap();
2532 let config = GraphConfig::default();
2533 let mut planner = LogicalPlanner::new(&config);
2534 let logical = planner.plan(&ast).unwrap();
2535 match logical {
2536 LogicalOperator::Offset { input, offset } => {
2537 assert_eq!(offset, 3);
2538 match *input {
2539 LogicalOperator::Project { .. } => {}
2540 _ => panic!("Expected Project under Offset"),
2541 }
2542 }
2543 _ => panic!("Expected Offset at top level"),
2544 }
2545 }
2546
2547 #[test]
2548 fn test_relationship_properties_pushed_into_expand() {
2549 let q = "MATCH (a)-[:KNOWS {since: 2020}]->(b) RETURN b.name";
2550 let ast = parse_cypher_query(q).unwrap();
2551 let config = GraphConfig::default();
2552 let mut planner = LogicalPlanner::new(&config);
2553 let logical = planner.plan(&ast).unwrap();
2554 match logical {
2555 LogicalOperator::Project { input, .. } => match *input {
2556 LogicalOperator::Expand { properties, .. } => match properties.get("since") {
2557 Some(PropertyValue::Integer(2020)) => {}
2558 _ => panic!("Expected relationship property since=2020 in Expand"),
2559 },
2560 _ => panic!("Expected Expand under Project"),
2561 },
2562 _ => panic!("Expected Project at top level"),
2563 }
2564 }
2565
2566 #[test]
2567 fn test_multiple_match_clauses_cross_join() {
2568 let q = "MATCH (a:Person) MATCH (b:Company) RETURN a.name, b.name";
2569 let ast = parse_cypher_query(q).unwrap();
2570 let config = GraphConfig::default();
2571 let mut planner = LogicalPlanner::new(&config);
2572 let logical = planner.plan(&ast).unwrap();
2573 match logical {
2574 LogicalOperator::Project { input, .. } => match *input {
2575 LogicalOperator::Join {
2576 left,
2577 right,
2578 join_type,
2579 } => {
2580 assert!(matches!(join_type, JoinType::Cross));
2581 match (*left, *right) {
2582 (
2583 LogicalOperator::ScanByLabel {
2584 variable: va,
2585 label: la,
2586 ..
2587 },
2588 LogicalOperator::ScanByLabel {
2589 variable: vb,
2590 label: lb,
2591 ..
2592 },
2593 ) => {
2594 assert_eq!(va, "a");
2595 assert_eq!(la, "Person");
2596 assert_eq!(vb, "b");
2597 assert_eq!(lb, "Company");
2598 }
2599 _ => panic!("Expected two scans under Join"),
2600 }
2601 }
2602 _ => panic!("Expected Join under Project"),
2603 },
2604 _ => panic!("Expected Project at top level"),
2605 }
2606 }
2607
2608 #[test]
2609 fn test_variable_only_node_default_label() {
2610 let q = "MATCH (x) RETURN x.name";
2611 let ast = parse_cypher_query(q).unwrap();
2612 let config = GraphConfig::default();
2613 let mut planner = LogicalPlanner::new(&config);
2614 let logical = planner.plan(&ast).unwrap();
2615 match logical {
2616 LogicalOperator::Project { input, .. } => match *input {
2617 LogicalOperator::ScanByLabel {
2618 variable, label, ..
2619 } => {
2620 assert_eq!(variable, "x");
2621 assert_eq!(label, "Node");
2622 }
2623 _ => panic!("Expected ScanByLabel under Project"),
2624 },
2625 _ => panic!("Expected Project at top level"),
2626 }
2627 }
2628
2629 #[test]
2630 fn test_multi_label_node_is_a_union_of_its_labels() {
2631 let q = "MATCH (n:Person:Employee) RETURN n.name";
2633 let ast = parse_cypher_query(q).unwrap();
2634 let config = GraphConfig::default();
2635 let mut planner = LogicalPlanner::new(&config);
2636 let logical = planner.plan(&ast).unwrap();
2637 let LogicalOperator::Project { input, .. } = logical else {
2638 panic!("Expected Project at top level");
2639 };
2640 let LogicalOperator::Union { inputs } = *input else {
2641 panic!("Expected Union under Project");
2642 };
2643 let labels: Vec<_> = inputs
2644 .iter()
2645 .map(|op| match op {
2646 LogicalOperator::ScanByLabel { label, .. } => label.as_str(),
2647 other => panic!("Expected ScanByLabel, got {other:?}"),
2648 })
2649 .collect();
2650 assert_eq!(labels, ["Person", "Employee"]);
2651 }
2652
2653 #[test]
2654 fn test_open_ended_and_partial_var_length_ranges() {
2655 let q1 = "MATCH (a)-[:R*]->(b:Node) RETURN b.name";
2657 let ast1 = parse_cypher_query(q1).unwrap();
2658 let config = GraphConfig::default();
2659 let mut planner1 = LogicalPlanner::new(&config);
2660 let plan1 = planner1.plan(&ast1).unwrap();
2661 match plan1 {
2662 LogicalOperator::Project { input, .. } => match *input {
2663 LogicalOperator::VariableLengthExpand {
2664 min_length,
2665 max_length,
2666 ..
2667 } => {
2668 assert_eq!(min_length, None);
2669 assert_eq!(max_length, None);
2670 }
2671 _ => panic!("Expected VariableLengthExpand for *"),
2672 },
2673 _ => panic!("Expected Project at top level"),
2674 }
2675
2676 let q2 = "MATCH (a)-[:R*2..]->(b) RETURN b.name";
2678 let ast2 = parse_cypher_query(q2).unwrap();
2679 let mut planner2 = LogicalPlanner::new(&config);
2680 let plan2 = planner2.plan(&ast2).unwrap();
2681 match plan2 {
2682 LogicalOperator::Project { input, .. } => match *input {
2683 LogicalOperator::VariableLengthExpand {
2684 min_length,
2685 max_length,
2686 ..
2687 } => {
2688 assert_eq!(min_length, Some(2));
2689 assert_eq!(max_length, None);
2690 }
2691 _ => panic!("Expected VariableLengthExpand for *2.."),
2692 },
2693 _ => panic!("Expected Project at top level"),
2694 }
2695
2696 let q3 = "MATCH (a)-[:R*..3]->(b) RETURN b.name";
2698 let ast3 = parse_cypher_query(q3).unwrap();
2699 let mut planner3 = LogicalPlanner::new(&config);
2700 let plan3 = planner3.plan(&ast3).unwrap();
2701 match plan3 {
2702 LogicalOperator::Project { input, .. } => match *input {
2703 LogicalOperator::VariableLengthExpand {
2704 min_length,
2705 max_length,
2706 ..
2707 } => {
2708 assert_eq!(min_length, None);
2709 assert_eq!(max_length, Some(3));
2710 }
2711 _ => panic!("Expected VariableLengthExpand for *..3"),
2712 },
2713 _ => panic!("Expected Project at top level"),
2714 }
2715 }
2716
2717 #[test]
2718 fn test_variable_reuse_across_patterns() {
2719 let query_text =
2720 "MATCH (a:Person)-[:KNOWS]->(shared:Person), (shared)-[:KNOWS]->(b:Person) RETURN b.name";
2721
2722 let ast = parse_cypher_query(query_text).unwrap();
2723 let config = GraphConfig::default();
2724 let mut planner = LogicalPlanner::new(&config);
2725 let logical_plan = planner.plan(&ast).unwrap();
2726
2727 match &logical_plan {
2729 LogicalOperator::Project { input, .. } => match input.as_ref() {
2730 LogicalOperator::Expand {
2731 input: inner,
2732 source_variable,
2733 target_variable,
2734 ..
2735 } => {
2736 assert_eq!(source_variable, "shared");
2737 assert_eq!(target_variable, "b");
2738
2739 match inner.as_ref() {
2740 LogicalOperator::Expand {
2741 source_variable: first_src,
2742 target_variable: first_dst,
2743 ..
2744 } => {
2745 assert_eq!(first_src, "a");
2746 assert_eq!(first_dst, "shared");
2747 }
2748 _ => panic!("Expected first Expand (a->shared)"),
2749 }
2750 }
2751 _ => panic!("Expected second Expand (shared->b)"),
2752 },
2753 _ => panic!("Expected Project at top level"),
2754 }
2755 }
2756
2757 #[test]
2758 fn test_variable_reuse_with_conflicting_labels() {
2759 let query_text =
2760 "MATCH (a:Person)-[:KNOWS]->(shared:Person), (shared:Company)-[:EMPLOYS]->(b:Person) RETURN b.name";
2761
2762 let ast = parse_cypher_query(query_text).unwrap();
2763 let config = GraphConfig::default();
2764 let mut planner = LogicalPlanner::new(&config);
2765 let err = planner.plan(&ast).unwrap_err();
2766 let err_msg = err.to_string();
2767
2768 assert!(
2769 err_msg.contains("already has label 'Person'")
2770 && err_msg.contains("cannot redefine as 'Company'"),
2771 "Expected error about label conflict, got: {}",
2772 err_msg
2773 );
2774 }
2775
2776 #[test]
2777 fn test_return_node_variable() {
2778 let query_text = "MATCH (a:Person) RETURN a";
2779
2780 let ast = parse_cypher_query(query_text).unwrap();
2781 let config = GraphConfig::builder()
2782 .with_node_mapping(NodeMapping {
2783 label: "Person".to_string(),
2784 id_field: "id".to_string(),
2785 property_fields: vec!["name".to_string(), "age".to_string()],
2786 filter_conditions: None,
2787 })
2788 .build()
2789 .unwrap();
2790 let mut planner = LogicalPlanner::new(&config);
2791 let logical_plan = planner.plan(&ast).unwrap();
2792
2793 match &logical_plan {
2794 LogicalOperator::Project { projections, .. } => {
2795 assert_eq!(projections.len(), 3);
2796 match &projections[0].expression {
2797 ValueExpression::Property(prop_ref) => {
2798 assert_eq!(prop_ref.variable, "a");
2799 assert_eq!(prop_ref.property, "id");
2800 }
2801 _ => panic!("Expected property reference for a.id"),
2802 }
2803 match &projections[1].expression {
2804 ValueExpression::Property(prop_ref) => {
2805 assert_eq!(prop_ref.variable, "a");
2806 assert_eq!(prop_ref.property, "name");
2807 }
2808 _ => panic!("Expected property reference for a.name"),
2809 }
2810 match &projections[2].expression {
2811 ValueExpression::Property(prop_ref) => {
2812 assert_eq!(prop_ref.variable, "a");
2813 assert_eq!(prop_ref.property, "age");
2814 }
2815 _ => panic!("Expected property reference for a.age"),
2816 }
2817 }
2818 _ => panic!("Expected Project at the top level"),
2819 }
2820 }
2821
2822 #[test]
2823 fn test_return_node_variable_with_alias() {
2824 let query_text = "MATCH (a:Person) RETURN a AS b";
2825
2826 let ast = parse_cypher_query(query_text).unwrap();
2827 let config = GraphConfig::builder()
2828 .node_table("Person", "id")
2829 .build()
2830 .unwrap();
2831 let mut planner = LogicalPlanner::new(&config);
2832 let logical_plan = planner.plan(&ast).unwrap();
2833
2834 match &logical_plan {
2835 LogicalOperator::Project { projections, .. } => {
2836 assert_eq!(projections.len(), 1);
2837 match &projections[0].expression {
2838 ValueExpression::Property(prop_ref) => {
2839 assert_eq!(prop_ref.variable, "a");
2840 assert_eq!(prop_ref.property, "id");
2841 }
2842 _ => panic!("Expected property reference for a.id"),
2843 }
2844 match &projections[0].alias {
2845 Some(alias) => assert_eq!(alias, "b.id"),
2846 None => panic!("Expected alias for a.id as b.id"),
2847 }
2848 }
2849 _ => panic!("Expected Project at the top level"),
2850 }
2851 }
2852
2853 #[test]
2854 fn test_return_node_variable_no_label() {
2855 let query_text = "MATCH (a:Person) RETURN a";
2856
2857 let ast = parse_cypher_query(query_text).unwrap();
2858 let config = GraphConfig::default();
2859 let mut planner = LogicalPlanner::new(&config);
2860 let err = planner.plan(&ast).unwrap_err();
2861 let err_msg = err.to_string();
2862
2863 assert!(
2864 err_msg.contains("Node label 'Person' doesn't exist"),
2865 "Expected error about missing label 'Person', got: {}",
2866 err_msg
2867 );
2868 }
2869
2870 fn expand_order(op: &LogicalOperator, out: &mut Vec<String>) {
2872 match op {
2873 LogicalOperator::Expand {
2874 input,
2875 relationship_types,
2876 ..
2877 } => {
2878 expand_order(input, out);
2879 out.push(relationship_types[0].clone());
2880 }
2881 LogicalOperator::Filter { input, .. }
2882 | LogicalOperator::Project { input, .. }
2883 | LogicalOperator::Distinct { input }
2884 | LogicalOperator::Sort { input, .. }
2885 | LogicalOperator::Limit { input, .. }
2886 | LogicalOperator::Offset { input, .. } => expand_order(input, out),
2887 LogicalOperator::OptionalMatch { optional, .. } => expand_order(optional, out),
2888 _ => {}
2889 }
2890 }
2891
2892 #[test]
2893 fn continued_path_starts_from_the_bound_end_with_smaller_fan_out() {
2894 let config = GraphConfig::builder()
2895 .node_table("Person", "id")
2896 .node_table("Post", "id")
2897 .node_table("Forum", "id")
2898 .rel_table("postHasCreator", "Post", "Person", "src", "dst")
2899 .rel_table("containerOf", "Forum", "Post", "src", "dst")
2900 .build()
2901 .unwrap();
2902 let q = "MATCH (friend:Person), (forum:Forum) WITH friend, forum \
2903 OPTIONAL MATCH (friend)<-[:postHasCreator]-(post:Post)<-[:containerOf]-(forum) \
2904 RETURN count(post.id) AS n";
2905 let ast = parse_cypher_query(q).unwrap();
2906 let first_hop = |creator_in: f64, container_out: f64| {
2907 let fan_out: FanOut = [
2908 (("posthascreator".to_string(), false), creator_in),
2909 (("posthascreator".to_string(), true), 1.0),
2910 (("containerof".to_string(), true), container_out),
2911 (("containerof".to_string(), false), 1.0),
2912 ]
2913 .into_iter()
2914 .collect();
2915 let plan = LogicalPlanner::new(&config)
2916 .with_fan_out(Some(&fan_out))
2917 .plan(&ast)
2918 .unwrap();
2919 let mut order = Vec::new();
2920 expand_order(&plan, &mut order);
2921 order[0].clone()
2922 };
2923 assert_eq!(first_hop(100.0, 11.0), "containerOf");
2925 assert_eq!(first_hop(11.0, 100.0), "postHasCreator");
2927 }
2928
2929 #[test]
2930 fn continued_path_bound_only_at_its_far_end_starts_there() {
2931 let config = GraphConfig::builder()
2933 .node_table("Person", "id")
2934 .rel_table("KNOWS", "Person", "Person", "src", "dst")
2935 .build()
2936 .unwrap();
2937 let q = "MATCH (a:Person)-[:KNOWS]->(b:Person) WITH DISTINCT b \
2938 MATCH (a:Person)-[:KNOWS]->(b) RETURN a.id, b.id";
2939 let plan = LogicalPlanner::new(&config)
2940 .plan(&parse_cypher_query(q).unwrap())
2941 .unwrap();
2942 let text = format!("{plan:?}");
2943 let last_expand = text.rfind("Expand {").unwrap();
2944 assert!(
2945 text[last_expand..].contains("source_variable: \"b\""),
2946 "{plan:#?}"
2947 );
2948 }
2949}