gqlite 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/ext/Cargo.toml +8 -8
  3. data/ext/db-index/Cargo.toml +2 -2
  4. data/ext/gqlitedb/Cargo.toml +0 -2
  5. data/ext/gqlitedb/src/aggregators.rs +28 -0
  6. data/ext/gqlitedb/src/compiler/expression_analyser.rs +33 -0
  7. data/ext/gqlitedb/src/compiler/variables_manager.rs +111 -11
  8. data/ext/gqlitedb/src/compiler.rs +506 -48
  9. data/ext/gqlitedb/src/connection.rs +14 -2
  10. data/ext/gqlitedb/src/consts.rs +1 -1
  11. data/ext/gqlitedb/src/error.rs +31 -0
  12. data/ext/gqlitedb/src/interpreter/evaluators.rs +153 -331
  13. data/ext/gqlitedb/src/interpreter/instructions.rs +77 -0
  14. data/ext/gqlitedb/src/interpreter/mod.rs +1 -0
  15. data/ext/gqlitedb/src/interpreter/path/bfs.rs +494 -0
  16. data/ext/gqlitedb/src/interpreter/path/dfs.rs +267 -0
  17. data/ext/gqlitedb/src/interpreter/path/group.rs +418 -0
  18. data/ext/gqlitedb/src/interpreter/path.rs +542 -0
  19. data/ext/gqlitedb/src/planner.rs +231 -6
  20. data/ext/gqlitedb/src/query_result.rs +19 -4
  21. data/ext/gqlitedb/src/store/redb.rs +1 -1
  22. data/ext/gqlitedb/src/tests/compiler.rs +2454 -1
  23. data/ext/gqlitedb/src/tests/connection/postgres.rs +22 -0
  24. data/ext/gqlitedb/src/tests/connection/redb.rs +24 -0
  25. data/ext/gqlitedb/src/tests/connection/sqlite.rs +16 -0
  26. data/ext/gqlitedb/src/tests/connection.rs +101 -0
  27. data/ext/gqlitedb/src/tests/evaluators.rs +2206 -1
  28. data/ext/gqlitedb/src/tests/parser.rs +1225 -0
  29. data/ext/gqlitedb/src/tests/planner.rs +433 -2
  30. data/ext/gqlitedb/src/tests/store/vector_index/postgres.rs +5 -5
  31. data/ext/gqlitedb/src/tests/store/vector_index/sqlite.rs +3 -1
  32. data/ext/gqlitedb/src/tests/templates/ast.rs +294 -1
  33. data/ext/gqlitedb/src/tests/templates/programs.rs +318 -0
  34. data/ext/gqliterb/Cargo.toml +0 -1
  35. data/ext/gqlparser/src/oc/ast.rs +72 -3
  36. data/ext/gqlparser/src/oc/error.rs +18 -0
  37. data/ext/gqlparser/src/oc/lexer.rs +157 -2
  38. data/ext/gqlparser/src/oc/parser/tests.rs +735 -29
  39. data/ext/gqlparser/src/oc/parser.rs +1312 -182
  40. data/ext/gqlparser/src/oc/string.rs +172 -0
  41. data/ext/gqlparser/src/oc.rs +1 -0
  42. data/ext/graphcore/src/value/value_map.rs +2 -2
  43. data/ext/graphcore/src/value.rs +13 -1
  44. metadata +6 -2
  45. data/ext/gqlitedb/src/parser/gql.pest +0 -198
@@ -3,13 +3,13 @@ use gqlparser::oc::ast;
3
3
  use crate::{
4
4
  compiler::{expression_analyser::ExpressionType, variables_manager::VariablesManager},
5
5
  interpreter::instructions::{CreateActionVariable, VariablesSizes},
6
- planner::{LogicalPlan, LogicalPlans, ProjectionKind},
6
+ planner::{self, LogicalPlan, LogicalPlans, ProjectionKind},
7
7
  prelude::*,
8
8
  };
9
9
 
10
10
  use interpreter::instructions::{
11
- self, Block, BlockMatch, CreateAction, Instruction, Instructions, Modifiers, RWAggregation,
12
- RWExpression,
11
+ self, Block, BlockMatch, CreateAction, Instruction, Instructions, Modifiers, PathStrategy,
12
+ RWAggregation, RWExpression, StopCondition,
13
13
  };
14
14
 
15
15
  pub(crate) mod expression_analyser;
@@ -49,6 +49,59 @@ fn is_scan_plan(plan: &LogicalPlan) -> bool
49
49
  }
50
50
  }
51
51
 
52
+ fn collect_pattern_variables(patterns: &[ast::Pattern]) -> Vec<ast::VariableIdentifier>
53
+ {
54
+ let mut variables = Vec::new();
55
+ for pattern in patterns
56
+ {
57
+ match pattern
58
+ {
59
+ ast::Pattern::Node(n) => variables.extend(n.variable.clone()),
60
+ ast::Pattern::Edge(e) =>
61
+ {
62
+ variables.extend(e.source.variable.clone());
63
+ variables.extend(e.variable.clone());
64
+ variables.extend(e.destination.variable.clone());
65
+ }
66
+ ast::Pattern::Path(p) => variables.push(p.variable.clone()),
67
+ ast::Pattern::QuantifiedPath(qp) =>
68
+ {
69
+ variables.extend(qp.variable.clone());
70
+ variables.extend(collect_pattern_variables(&qp.group));
71
+ }
72
+ }
73
+ }
74
+ variables
75
+ }
76
+
77
+ /// If this QuantifiedPathPattern is a "plain" single-edge quantifier -
78
+ /// group.len() == 1, and the group's own edge already has
79
+ /// qp.source/qp.destination as its source/destination (Epic 2's
80
+ /// original construction) - returns that edge. Returns None for
81
+ /// everything else: multi-element groups, or explicit group syntax
82
+ /// where the group's own nodes differ from the outer anchors.
83
+ pub(super) fn as_plain_quantifier(qp: &ast::QuantifiedPathPattern) -> Option<&ast::EdgePattern>
84
+ {
85
+ if qp.group.len() != 1
86
+ {
87
+ return None;
88
+ }
89
+ let ast::Pattern::Edge(inner_edge) = &qp.group[0]
90
+ else
91
+ {
92
+ return None;
93
+ };
94
+ if inner_edge.source.variable == qp.source.variable
95
+ && inner_edge.destination.variable == qp.destination.variable
96
+ {
97
+ Some(inner_edge)
98
+ }
99
+ else
100
+ {
101
+ None
102
+ }
103
+ }
104
+
52
105
  struct Compiler
53
106
  {
54
107
  function_manager: functions::Manager,
@@ -74,6 +127,35 @@ impl Compiler
74
127
  col_id + self.variables_manager.variables_count()
75
128
  }
76
129
 
130
+ fn compile_path_strategy(&mut self, strategy: &planner::PathStrategy) -> Result<PathStrategy>
131
+ {
132
+ match strategy
133
+ {
134
+ planner::PathStrategy::Dfs => Ok(PathStrategy::Dfs),
135
+ planner::PathStrategy::Bfs { stop_condition } =>
136
+ {
137
+ let compiled_stop_condition = match stop_condition
138
+ {
139
+ planner::StopCondition::Shortest(expr) =>
140
+ {
141
+ let mut instructions = Instructions::new();
142
+ self.compile_expression(expr, &mut instructions, &mut None)?;
143
+ StopCondition::Shortest(instructions)
144
+ }
145
+ planner::StopCondition::ShortestGroups(expr) =>
146
+ {
147
+ let mut instructions = Instructions::new();
148
+ self.compile_expression(expr, &mut instructions, &mut None)?;
149
+ StopCondition::ShortestGroups(instructions)
150
+ }
151
+ };
152
+ Ok(PathStrategy::Bfs {
153
+ stop_condition: compiled_stop_condition,
154
+ })
155
+ }
156
+ }
157
+ }
158
+
77
159
  fn compile_expression(
78
160
  &mut self,
79
161
  expression: &gqlparser::oc::ast::Expression,
@@ -132,6 +214,7 @@ impl Compiler
132
214
  init_instructions,
133
215
  aggregator,
134
216
  argument_instructions,
217
+ distinct: function_call.distinct,
135
218
  },
136
219
  ));
137
220
  Instruction::GetVariable { col_id: var_col_id }
@@ -317,6 +400,21 @@ impl Compiler
317
400
  instructions.push(Instruction::IsNullUnaryOperator);
318
401
  Instruction::NotUnaryOperator
319
402
  }
403
+ ast::Expression::Contains(contains) =>
404
+ {
405
+ compile_binary_op!(self, contains, instructions, aggregations);
406
+ Instruction::StringContainsOperator
407
+ }
408
+ ast::Expression::StartsWith(starts_with) =>
409
+ {
410
+ compile_binary_op!(self, starts_with, instructions, aggregations);
411
+ Instruction::StringStartsWithOperator
412
+ }
413
+ ast::Expression::EndsWith(ends_with) =>
414
+ {
415
+ compile_binary_op!(self, ends_with, instructions, aggregations);
416
+ Instruction::StringEndsWithOperator
417
+ }
320
418
  };
321
419
  instructions.push(expr);
322
420
  Ok(())
@@ -425,11 +523,6 @@ impl Compiler
425
523
  .variables_manager
426
524
  .mark_variables_as_set(&edge.variable)?;
427
525
  self.compile_optional_expression(&edge.properties, instructions)?;
428
- // Reject variable-length relationships in CREATE statements
429
- if edge.recursive_range.is_some()
430
- {
431
- return Err(CompileTimeError::CreatingVarLength.into());
432
- }
433
526
  if !edge.labels.is_string()
434
527
  {
435
528
  Err(CompileTimeError::NoSingleRelationshipType {
@@ -545,7 +638,7 @@ impl Compiler
545
638
 
546
639
  fn compile_create_patterns(
547
640
  &mut self,
548
- patterns: impl IntoIterator<Item = gqlparser::oc::ast::Pattern>,
641
+ patterns: impl IntoIterator<Item = planner::PlannedPattern>,
549
642
  ) -> Result<Block>
550
643
  {
551
644
  let actions = patterns.into_iter().map(|c| {
@@ -553,15 +646,15 @@ impl Compiler
553
646
  let mut variables = Vec::<CreateActionVariable>::new();
554
647
  match c
555
648
  {
556
- gqlparser::oc::ast::Pattern::Node(node) =>
649
+ planner::PlannedPattern::Node(node) =>
557
650
  {
558
651
  self.compile_create_node(node, &mut instructions, &mut variables)?;
559
652
  }
560
- gqlparser::oc::ast::Pattern::Edge(edge) =>
653
+ planner::PlannedPattern::Edge(edge) =>
561
654
  {
562
655
  self.compile_create_edge(edge, &mut instructions, &mut variables, None)?;
563
656
  }
564
- gqlparser::oc::ast::Pattern::Path(path) =>
657
+ planner::PlannedPattern::Path(path) =>
565
658
  {
566
659
  self
567
660
  .variables_manager
@@ -573,6 +666,10 @@ impl Compiler
573
666
  Some(path.variable),
574
667
  )?;
575
668
  }
669
+ planner::PlannedPattern::QuantifiedPath { .. } =>
670
+ {
671
+ return Err(CompileTimeError::CreatingVarLength.into());
672
+ }
576
673
  }
577
674
  Ok(CreateAction {
578
675
  instructions,
@@ -619,6 +716,26 @@ impl Compiler
619
716
  filter.push(Instruction::AndBinaryOperator);
620
717
  filter.push(Instruction::Swap);
621
718
  }
719
+ if let Some(predicate) = &node.predicate
720
+ {
721
+ self.compile_expression(predicate, filter, &mut None)?;
722
+ // When there are no labels, we use InverseRot3 to combine the predicate result
723
+ // with the passes_so_far/candidate stack (same as the label loop does).
724
+ // When there are labels, the outer code already uses Rot3, so we only
725
+ // apply these stack operations if there were no labels.
726
+ if node.labels.is_all_inclusive()
727
+ {
728
+ filter.push(Instruction::InverseRot3);
729
+ filter.push(Instruction::AndBinaryOperator);
730
+ filter.push(Instruction::Swap);
731
+ }
732
+ else
733
+ {
734
+ filter.push(Instruction::Rot3);
735
+ filter.push(Instruction::AndBinaryOperator);
736
+ filter.push(Instruction::Swap);
737
+ }
738
+ }
622
739
  instructions.push(Instruction::CreateNodeQuery { labels });
623
740
  Ok(())
624
741
  }
@@ -629,6 +746,7 @@ impl Compiler
629
746
  edge: &gqlparser::oc::ast::EdgePattern,
630
747
  single_match: bool,
631
748
  previous_edges: &mut Vec<usize>,
749
+ recursive_range: Option<std::ops::Range<usize>>,
632
750
  ) -> Result<BlockMatch>
633
751
  {
634
752
  let mut instructions = Instructions::new();
@@ -681,6 +799,15 @@ impl Compiler
681
799
  }
682
800
  if self.variables_manager.is_set_variable(&edge.variable)?
683
801
  {
802
+ // Predicates on reused edge variables are not supported.
803
+ // This intentionally diverges from the silent-ignore behavior for
804
+ // `properties` on reused variables: predicates represent load-bearing
805
+ // user intent (a filter condition), whereas redundant properties on a
806
+ // reused variable are more likely user error that's safe to ignore.
807
+ if edge.predicate.is_some()
808
+ {
809
+ return Err(CompileTimeError::InvalidArgumentType.into());
810
+ }
684
811
  instructions.push(Instruction::GetVariable {
685
812
  col_id: self
686
813
  .variables_manager
@@ -707,6 +834,23 @@ impl Compiler
707
834
  filter.push(Instruction::AndBinaryOperator);
708
835
  filter.push(Instruction::Swap);
709
836
  }
837
+ if let Some(predicate) = &edge.predicate
838
+ {
839
+ self.compile_expression(predicate, &mut filter, &mut None)?;
840
+ // Same logic as compile_match_node: use InverseRot3 when there are no labels
841
+ if edge.labels.is_all_inclusive()
842
+ {
843
+ filter.push(Instruction::InverseRot3);
844
+ filter.push(Instruction::AndBinaryOperator);
845
+ filter.push(Instruction::Swap);
846
+ }
847
+ else
848
+ {
849
+ filter.push(Instruction::Rot3);
850
+ filter.push(Instruction::AndBinaryOperator);
851
+ filter.push(Instruction::Swap);
852
+ }
853
+ }
710
854
  instructions.push(Instruction::CreateEdgeQuery { labels });
711
855
  }
712
856
 
@@ -764,7 +908,281 @@ impl Compiler
764
908
  .get_variable_index_option(&path_variable)?,
765
909
  filter,
766
910
  directivity: edge.directivity,
767
- recursive_range: edge.recursive_range.clone(),
911
+ recursive_range,
912
+ })
913
+ }
914
+
915
+ fn compile_quantified_path(
916
+ &mut self,
917
+ qp: ast::QuantifiedPathPattern,
918
+ strategy: &planner::PathStrategy,
919
+ ) -> Result<BlockMatch>
920
+ {
921
+ let mut source_instructions = Instructions::new();
922
+ let mut source_filter = Instructions::new();
923
+ let source_variable = qp.source.variable.clone();
924
+ if !self
925
+ .variables_manager
926
+ .is_set_variable(&qp.source.variable)?
927
+ {
928
+ self.compile_match_node(
929
+ &qp.source,
930
+ &mut source_instructions,
931
+ &mut source_filter,
932
+ None,
933
+ )?;
934
+ }
935
+
936
+ let destination_variable = qp.destination.variable.clone();
937
+ let mut destination_filter = Instructions::new();
938
+ let is_dest_set = self
939
+ .variables_manager
940
+ .is_set_variable(&qp.destination.variable)?;
941
+ if !is_dest_set
942
+ {
943
+ // Do NOT call compile_match_node here. Its filter output assumes it's being
944
+ // appended into an already-running stack-based accumulator (with Duplicate/Rot3/Swap
945
+ // instructions). check_destination_filter always starts from a fresh, empty stack.
946
+ // This is a fundamental contract mismatch. Instead, build destination_filter
947
+ // directly as a standalone, empty-stack-starting boolean check.
948
+
949
+ let dest_col = match &qp.destination.variable
950
+ {
951
+ Some(var_id) => self.variables_manager.get_variable_index(var_id)?,
952
+ None =>
953
+ {
954
+ return Err(
955
+ error::InternalError::Unreachable {
956
+ context: "compile_quantified_path: destination.variable should always be Some",
957
+ }
958
+ .into(),
959
+ );
960
+ }
961
+ };
962
+
963
+ let mut checks: Vec<Instructions> = Vec::new();
964
+
965
+ // Build label check
966
+ if !matches!(qp.destination.labels, ast::LabelExpression::None)
967
+ {
968
+ let mut label_check = Instructions::new();
969
+ label_check.push(Instruction::GetVariable { col_id: dest_col });
970
+ let has_label_function = self
971
+ .function_manager
972
+ .get_function::<CompileTimeError>("has_label")?;
973
+ Self::compile_filter_labels(
974
+ &mut label_check,
975
+ &qp.destination.labels,
976
+ &has_label_function,
977
+ )?;
978
+ checks.push(label_check);
979
+ }
980
+
981
+ // Build property checks
982
+ if let Some(properties) = &qp.destination.properties
983
+ {
984
+ if let ast::Expression::Map(map) = properties
985
+ {
986
+ for (key, value_expr) in &map.map
987
+ {
988
+ let mut prop_check = Instructions::new();
989
+ prop_check.push(Instruction::GetVariable { col_id: dest_col });
990
+ prop_check.push(Instruction::MemberAccess {
991
+ path: vec![key.clone()],
992
+ });
993
+ self.compile_expression(value_expr, &mut prop_check, &mut None)?;
994
+ prop_check.push(Instruction::EqualBinaryOperator);
995
+ checks.push(prop_check);
996
+ }
997
+ }
998
+ else
999
+ {
1000
+ // Non-Map properties expressions (parameters, variables, etc.) not yet supported
1001
+ return Err(error::InternalError::Unreachable {
1002
+ context: "compile_quantified_path: destination properties as non-Map expression not yet supported for destination_filter",
1003
+ }
1004
+ .into());
1005
+ }
1006
+ }
1007
+
1008
+ // Build predicate (WHERE) check
1009
+ if let Some(predicate) = &qp.destination.predicate
1010
+ {
1011
+ let mut pred_check = Instructions::new();
1012
+ self.compile_expression(predicate, &mut pred_check, &mut None)?;
1013
+ checks.push(pred_check);
1014
+ }
1015
+
1016
+ // Combine all checks with AND
1017
+ for (i, check) in checks.into_iter().enumerate()
1018
+ {
1019
+ destination_filter.extend(check);
1020
+ if i > 0
1021
+ {
1022
+ destination_filter.push(Instruction::AndBinaryOperator);
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ // Mark the group's own entry point as already-bound BEFORE compiling
1028
+ // group_instructions - this tells compile_match_edge to generate GetVariable
1029
+ // instructions instead of unconstrained CreateNodeQuery, so the source node
1030
+ // will be constrained to whatever value is in that column at runtime.
1031
+ let group_entry_variable_identifier = qp
1032
+ .group
1033
+ .first()
1034
+ .and_then(|p| {
1035
+ match p {
1036
+ ast::Pattern::Node(n) => n.variable.clone(),
1037
+ ast::Pattern::Edge(e) => e.source.variable.clone(),
1038
+ _ => None,
1039
+ }
1040
+ })
1041
+ .ok_or_else(|| error::InternalError::Unreachable {
1042
+ context: "compile_quantified_path: group should start with a node or edge (guaranteed by parser)",
1043
+ })?;
1044
+ self
1045
+ .variables_manager
1046
+ .mark_variables_as_set(&Some(group_entry_variable_identifier.clone()))?;
1047
+ let group_entry_variable = self
1048
+ .variables_manager
1049
+ .get_variable_index(&group_entry_variable_identifier)?;
1050
+
1051
+ // For bare quantifiers (where the group's terminal node is the same as the
1052
+ // outer destination), create a modified group with unconstrained destination
1053
+ // nodes. This prevents compile_match_edge from baking qp.destination's
1054
+ // constraints into every per-hop edge query.
1055
+ let group_terminal_variable_identifier = qp
1056
+ .group
1057
+ .iter()
1058
+ .rev()
1059
+ .find_map(|p| {
1060
+ if let ast::Pattern::Edge(e) = p {
1061
+ e.destination.variable.clone()
1062
+ } else {
1063
+ None
1064
+ }
1065
+ })
1066
+ .ok_or_else(|| error::InternalError::Unreachable {
1067
+ context: "compile_quantified_path: group should have at least one edge (guaranteed by parser)",
1068
+ })?;
1069
+
1070
+ let modified_group = if Some(&group_terminal_variable_identifier)
1071
+ == qp.destination.variable.as_ref()
1072
+ && (qp.destination.labels != ast::LabelExpression::None
1073
+ || qp.destination.properties.is_some()
1074
+ || qp.destination.predicate.is_some())
1075
+ {
1076
+ // Create a copy of the group with unconstrained destination
1077
+ qp.group
1078
+ .iter()
1079
+ .map(|p| {
1080
+ if let ast::Pattern::Edge(e) = p
1081
+ {
1082
+ let mut modified_edge = e.clone();
1083
+ // Strip constraints from destination while keeping the variable name
1084
+ modified_edge.destination = ast::NodePattern {
1085
+ variable: modified_edge.destination.variable.clone(),
1086
+ labels: ast::LabelExpression::None,
1087
+ properties: None,
1088
+ predicate: None,
1089
+ };
1090
+ ast::Pattern::Edge(modified_edge)
1091
+ }
1092
+ else
1093
+ {
1094
+ p.clone()
1095
+ }
1096
+ })
1097
+ .collect()
1098
+ }
1099
+ else
1100
+ {
1101
+ qp.group.clone()
1102
+ };
1103
+
1104
+ let group_variable_identifiers = collect_pattern_variables(&modified_group);
1105
+ let planned_group = planner::plan_patterns(modified_group.clone());
1106
+ let group_instructions = self.compile_pattern_blocks(&planned_group)?;
1107
+ let group_variables = group_variable_identifiers
1108
+ .iter()
1109
+ .map(|v| self.variables_manager.get_variable_index(v))
1110
+ .collect::<Result<Vec<_>>>()?;
1111
+
1112
+ // Find the terminal variable: the destination of the last edge in the group
1113
+ let group_terminal_variable = self
1114
+ .variables_manager
1115
+ .get_variable_index(&group_terminal_variable_identifier)?;
1116
+
1117
+ let group_where_filter = match &qp.group_where
1118
+ {
1119
+ Some(expr) =>
1120
+ {
1121
+ let mut instructions = Instructions::new();
1122
+ self.compile_expression(expr, &mut instructions, &mut None)?;
1123
+ Some(instructions)
1124
+ }
1125
+ None => None,
1126
+ };
1127
+
1128
+ let path_filter = match &qp.path_where
1129
+ {
1130
+ Some(expr) =>
1131
+ {
1132
+ let mut instructions = Instructions::new();
1133
+ self.compile_expression(expr, &mut instructions, &mut None)?;
1134
+ Some(instructions)
1135
+ }
1136
+ None => None,
1137
+ };
1138
+
1139
+ let path_variable_opt = qp.variable.clone();
1140
+ let path_variable = path_variable_opt
1141
+ .as_ref()
1142
+ .map(|v| self.variables_manager.get_variable_index(v))
1143
+ .transpose()?;
1144
+
1145
+ // Mark all pattern variables as set (same as compile_match_edge does)
1146
+ self
1147
+ .variables_manager
1148
+ .mark_variables_as_set(&source_variable)?;
1149
+ self
1150
+ .variables_manager
1151
+ .mark_variables_as_set(&destination_variable)?;
1152
+ self
1153
+ .variables_manager
1154
+ .mark_variables_as_set(&path_variable_opt)?;
1155
+ for var in &group_variable_identifiers
1156
+ {
1157
+ self
1158
+ .variables_manager
1159
+ .mark_variables_as_set(&Some(var.clone()))?;
1160
+ }
1161
+
1162
+ let compiled_strategy = self.compile_path_strategy(strategy)?;
1163
+
1164
+ Ok(BlockMatch::MatchQuantifiedPath {
1165
+ source_instructions,
1166
+ source_variable: source_variable
1167
+ .map(|v| self.variables_manager.get_variable_index(&v))
1168
+ .transpose()?,
1169
+ source_filter,
1170
+ destination_variable: destination_variable
1171
+ .map(|v| self.variables_manager.get_variable_index(&v))
1172
+ .transpose()?,
1173
+ destination_filter,
1174
+ path_variable,
1175
+ quantifier: qp.quantifier.expect(
1176
+ "compile_quantified_path called with quantifier: None — the pre-filter \
1177
+ scoping-wrapper use (design §1d) isn't reachable until Milestone 5",
1178
+ ),
1179
+ group_instructions,
1180
+ group_variables,
1181
+ group_terminal_variable,
1182
+ group_entry_variable,
1183
+ group_where_filter,
1184
+ path_filter,
1185
+ strategy: compiled_strategy,
768
1186
  })
769
1187
  }
770
1188
 
@@ -853,45 +1271,77 @@ impl Compiler
853
1271
  })
854
1272
  }
855
1273
 
856
- fn compile_match_patterns(
1274
+ fn compile_pattern_blocks(
857
1275
  &mut self,
858
- patterns: &[gqlparser::oc::ast::Pattern],
859
- where_expression: &Option<gqlparser::oc::ast::Expression>,
860
- optional: bool,
861
- ) -> Result<Block>
1276
+ patterns: &[planner::PlannedPattern],
1277
+ ) -> Result<Vec<BlockMatch>>
862
1278
  {
863
1279
  let is_single_match = patterns.len() == 1;
864
1280
  let mut edge_variables = vec![];
865
- let blocks = patterns.iter().map(|c| match c
866
- {
867
- gqlparser::oc::ast::Pattern::Node(node) =>
1281
+ patterns
1282
+ .iter()
1283
+ .map(|c| match c
868
1284
  {
869
- let mut instructions = Instructions::new();
870
- let mut filter = Instructions::new();
871
- self.compile_match_node(node, &mut instructions, &mut filter, None)?;
872
- self
873
- .variables_manager
874
- .mark_variables_as_set(&node.variable)?;
875
- Ok(BlockMatch::MatchNode {
876
- instructions,
877
- variable: self
1285
+ planner::PlannedPattern::Node(node) =>
1286
+ {
1287
+ let mut instructions = Instructions::new();
1288
+ let mut filter = Instructions::new();
1289
+ self.compile_match_node(node, &mut instructions, &mut filter, None)?;
1290
+ self
878
1291
  .variables_manager
879
- .get_variable_index_option(&node.variable)?,
880
- filter,
881
- })
882
- }
883
- gqlparser::oc::ast::Pattern::Edge(edge) =>
884
- {
885
- self.compile_match_edge(None, edge, is_single_match, &mut edge_variables)
886
- }
887
- gqlparser::oc::ast::Pattern::Path(path) => self.compile_match_edge(
888
- Some(path.variable.to_owned()),
889
- &path.edge,
890
- is_single_match,
891
- &mut edge_variables,
892
- ),
893
- });
894
- let blocks = blocks.collect::<Result<_>>()?;
1292
+ .mark_variables_as_set(&node.variable)?;
1293
+ Ok(BlockMatch::MatchNode {
1294
+ instructions,
1295
+ variable: self
1296
+ .variables_manager
1297
+ .get_variable_index_option(&node.variable)?,
1298
+ filter,
1299
+ })
1300
+ }
1301
+ planner::PlannedPattern::Edge(edge) =>
1302
+ {
1303
+ self.compile_match_edge(None, edge, is_single_match, &mut edge_variables, None)
1304
+ }
1305
+ planner::PlannedPattern::Path(path) => self.compile_match_edge(
1306
+ Some(path.variable.to_owned()),
1307
+ &path.edge,
1308
+ is_single_match,
1309
+ &mut edge_variables,
1310
+ None,
1311
+ ),
1312
+ planner::PlannedPattern::QuantifiedPath {
1313
+ pattern: qp,
1314
+ strategy,
1315
+ } =>
1316
+ {
1317
+ if let Some(edge) = as_plain_quantifier(qp)
1318
+ && matches!(strategy, planner::PathStrategy::Dfs)
1319
+ {
1320
+ self.compile_match_edge(
1321
+ qp.variable.clone(),
1322
+ edge,
1323
+ is_single_match,
1324
+ &mut edge_variables,
1325
+ qp.quantifier.clone(),
1326
+ )
1327
+ }
1328
+ else
1329
+ {
1330
+ self.compile_quantified_path(qp.clone(), strategy)
1331
+ }
1332
+ }
1333
+ })
1334
+ .collect::<Result<_>>()
1335
+ }
1336
+
1337
+ fn compile_match_patterns(
1338
+ &mut self,
1339
+ patterns: &[planner::PlannedPattern],
1340
+ where_expression: &Option<gqlparser::oc::ast::Expression>,
1341
+ optional: bool,
1342
+ ) -> Result<Block>
1343
+ {
1344
+ let blocks = self.compile_pattern_blocks(patterns)?;
895
1345
  let mut filter = Instructions::new();
896
1346
  if let Some(where_expression) = where_expression
897
1347
  {
@@ -980,6 +1430,7 @@ impl Compiler
980
1430
  },
981
1431
  )?;
982
1432
  Ok(instructions::Modifiers {
1433
+ distinct: modifiers.distinct,
983
1434
  limit,
984
1435
  skip,
985
1436
  order_by,
@@ -988,7 +1439,7 @@ impl Compiler
988
1439
 
989
1440
  fn unpack_scan_plan(
990
1441
  logical_plan: LogicalPlan,
991
- ) -> Result<(Vec<ast::Pattern>, Option<ast::Expression>, bool)>
1442
+ ) -> Result<(Vec<planner::PlannedPattern>, Option<ast::Expression>, bool)>
992
1443
  {
993
1444
  match logical_plan
994
1445
  {
@@ -1100,6 +1551,12 @@ impl Compiler
1100
1551
  projection.modifiers.limit = Some(expression);
1101
1552
  Ok(projection)
1102
1553
  }
1554
+ LogicalPlan::Distinct { source } =>
1555
+ {
1556
+ let mut projection = Self::unpack_projection_plan(*source)?;
1557
+ projection.modifiers.distinct = true;
1558
+ Ok(projection)
1559
+ }
1103
1560
  _ => Err(
1104
1561
  InternalError::Unreachable {
1105
1562
  context: "compile/unpack_projection_plan",
@@ -1239,7 +1696,8 @@ pub(crate) fn compile(
1239
1696
  | LogicalPlan::Sort { .. }
1240
1697
  | LogicalPlan::Skip { .. }
1241
1698
  | LogicalPlan::Limit { .. }
1242
- | LogicalPlan::Filter { .. } => compiler.compile_projection_plan(logical_plan),
1699
+ | LogicalPlan::Filter { .. }
1700
+ | LogicalPlan::Distinct { .. } => compiler.compile_projection_plan(logical_plan),
1243
1701
  LogicalPlan::Call { name, arguments } =>
1244
1702
  {
1245
1703
  let mut instructions = Instructions::new();