1use crate::query::pushdown::{PredicateAnalyzer, try_label_or_to_union, try_type_or_to_union};
5use anyhow::{Result, anyhow};
6use arrow_array::RecordBatch;
7use arrow_schema::{DataType, SchemaRef};
8use parking_lot::RwLock;
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11use uni_common::Value;
12use uni_common::core::schema::{
13 EmbeddingConfig, FullTextIndexConfig, IndexDefinition, JsonFtsIndexConfig, ScalarIndexConfig,
14 ScalarIndexType, Schema, TokenizerConfig, VectorIndexConfig,
15};
16use uni_cypher::ast::{
17 AlterEdgeType, AlterLabel, BinaryOp, CallKind, Clause, CreateConstraint, CreateEdgeType,
18 CreateLabel, CypherLiteral, Direction, DropConstraint, DropEdgeType, DropLabel, Expr,
19 MatchClause, MergeClause, NodePattern, PathPattern, Pattern, PatternElement, Query,
20 RelationshipPattern, RemoveItem, ReturnClause, ReturnItem, SchemaCommand, SetClause, SetItem,
21 ShortestPathMode, ShowConstraints, SortItem, Statement, WindowSpec, WithClause,
22 WithRecursiveClause,
23};
24
25pub(crate) const STRUCT_ONLY_SENTINEL: &str = "__set_struct__";
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum VariableType {
46 Node,
48 Edge,
50 Path,
52 Scalar,
55 ScalarLiteral,
58 Imported,
61}
62
63impl VariableType {
64 fn is_compatible_with(self, expected: VariableType) -> bool {
68 self == expected
69 || self == VariableType::Imported
70 || (self == VariableType::ScalarLiteral && expected == VariableType::Scalar)
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct VariableInfo {
78 pub name: String,
80 pub var_type: VariableType,
82 pub is_vlp: bool,
86}
87
88impl VariableInfo {
89 pub fn new(name: String, var_type: VariableType) -> Self {
90 Self {
91 name,
92 var_type,
93 is_vlp: false,
94 }
95 }
96}
97
98fn find_var_in_scope<'a>(vars: &'a [VariableInfo], name: &str) -> Option<&'a VariableInfo> {
100 vars.iter().find(|v| v.name == name)
101}
102
103fn is_var_in_scope(vars: &[VariableInfo], name: &str) -> bool {
105 find_var_in_scope(vars, name).is_some()
106}
107
108fn contains_pattern_predicate(expr: &Expr) -> bool {
110 if matches!(
111 expr,
112 Expr::Exists {
113 from_pattern_predicate: true,
114 ..
115 }
116 ) {
117 return true;
118 }
119 let mut found = false;
120 expr.for_each_child(&mut |child| {
121 if !found {
122 found = contains_pattern_predicate(child);
123 }
124 });
125 found
126}
127
128fn add_var_to_scope(
131 vars: &mut Vec<VariableInfo>,
132 name: &str,
133 var_type: VariableType,
134) -> Result<()> {
135 if name.is_empty() {
136 return Ok(());
137 }
138
139 if let Some(existing) = vars.iter_mut().find(|v| v.name == name) {
140 if existing.var_type == VariableType::Imported {
141 existing.var_type = var_type;
143 } else if var_type == VariableType::Imported || existing.var_type == var_type {
144 } else if matches!(
146 existing.var_type,
147 VariableType::Scalar | VariableType::ScalarLiteral
148 ) && matches!(var_type, VariableType::Node | VariableType::Edge)
149 {
150 existing.var_type = var_type;
153 } else {
154 return Err(anyhow!(
155 "SyntaxError: VariableTypeConflict - Variable '{}' already defined as {:?}, cannot use as {:?}",
156 name,
157 existing.var_type,
158 var_type
159 ));
160 }
161 } else {
162 vars.push(VariableInfo::new(name.to_string(), var_type));
163 }
164 Ok(())
165}
166
167fn vars_to_strings(vars: &[VariableInfo]) -> Vec<String> {
169 vars.iter().map(|v| v.name.clone()).collect()
170}
171
172fn infer_with_output_type(expr: &Expr, vars_in_scope: &[VariableInfo]) -> VariableType {
173 match expr {
174 Expr::Variable(v) => find_var_in_scope(vars_in_scope, v)
175 .map(|info| info.var_type)
176 .unwrap_or(VariableType::Scalar),
177 Expr::Literal(CypherLiteral::Null) => VariableType::Imported,
178 Expr::Literal(CypherLiteral::Integer(_))
180 | Expr::Literal(CypherLiteral::Float(_))
181 | Expr::Literal(CypherLiteral::String(_))
182 | Expr::Literal(CypherLiteral::Bool(_))
183 | Expr::Literal(CypherLiteral::Bytes(_)) => VariableType::ScalarLiteral,
184 Expr::FunctionCall { name, args, .. } => {
185 let lower = name.to_lowercase();
186 if lower == "coalesce" {
187 infer_coalesce_type(args, vars_in_scope)
188 } else if lower == "collect" && !args.is_empty() {
189 let collected = infer_with_output_type(&args[0], vars_in_scope);
190 if matches!(
191 collected,
192 VariableType::Node
193 | VariableType::Edge
194 | VariableType::Path
195 | VariableType::Imported
196 ) {
197 collected
198 } else {
199 VariableType::Scalar
200 }
201 } else {
202 VariableType::Scalar
203 }
204 }
205 Expr::List(_) => VariableType::ScalarLiteral,
210 _ => VariableType::Scalar,
211 }
212}
213
214fn infer_coalesce_type(args: &[Expr], vars_in_scope: &[VariableInfo]) -> VariableType {
215 let mut resolved: Option<VariableType> = None;
216 let mut saw_imported = false;
217 for arg in args {
218 let t = infer_with_output_type(arg, vars_in_scope);
219 match t {
220 VariableType::Node | VariableType::Edge | VariableType::Path => {
221 if let Some(existing) = resolved {
222 if existing != t {
223 return VariableType::Scalar;
224 }
225 } else {
226 resolved = Some(t);
227 }
228 }
229 VariableType::Imported => saw_imported = true,
230 VariableType::Scalar | VariableType::ScalarLiteral => {}
231 }
232 }
233 if let Some(t) = resolved {
234 t
235 } else if saw_imported {
236 VariableType::Imported
237 } else {
238 VariableType::Scalar
239 }
240}
241
242fn infer_unwind_output_type(expr: &Expr, vars_in_scope: &[VariableInfo]) -> VariableType {
243 match expr {
244 Expr::Variable(v) => find_var_in_scope(vars_in_scope, v)
245 .map(|info| info.var_type)
246 .unwrap_or(VariableType::Scalar),
247 Expr::FunctionCall { name, args, .. }
248 if name.eq_ignore_ascii_case("collect") && !args.is_empty() =>
249 {
250 infer_with_output_type(&args[0], vars_in_scope)
251 }
252 Expr::List(items) => {
253 let mut inferred: Option<VariableType> = None;
254 for item in items {
255 let t = infer_with_output_type(item, vars_in_scope);
256 if !matches!(
257 t,
258 VariableType::Node
259 | VariableType::Edge
260 | VariableType::Path
261 | VariableType::Imported
262 ) {
263 return VariableType::Scalar;
264 }
265 if let Some(existing) = inferred {
266 if existing != t
267 && t != VariableType::Imported
268 && existing != VariableType::Imported
269 {
270 return VariableType::Scalar;
271 }
272 if existing == VariableType::Imported && t != VariableType::Imported {
273 inferred = Some(t);
274 }
275 } else {
276 inferred = Some(t);
277 }
278 }
279 inferred.unwrap_or(VariableType::Scalar)
280 }
281 _ => VariableType::Scalar,
282 }
283}
284
285fn collect_expr_variables(expr: &Expr) -> Vec<String> {
287 let mut vars = Vec::new();
288 collect_expr_variables_inner(expr, &mut vars);
289 vars
290}
291
292fn collect_expr_parameters(expr: &Expr, names: &mut Vec<String>) {
299 match expr {
300 Expr::Parameter(name) => {
301 if !names.contains(name) {
302 names.push(name.clone());
303 }
304 }
305 Expr::UnaryOp { expr: e, .. } => collect_expr_parameters(e, names),
306 Expr::BinaryOp { left, right, .. } => {
307 collect_expr_parameters(left, names);
308 collect_expr_parameters(right, names);
309 }
310 Expr::FunctionCall { args, .. } => {
311 for a in args {
312 collect_expr_parameters(a, names);
313 }
314 }
315 _ => {}
316 }
317}
318
319fn collect_expr_variables_inner(expr: &Expr, vars: &mut Vec<String>) {
320 let mut add_var = |name: &String| {
321 if !vars.contains(name) {
322 vars.push(name.clone());
323 }
324 };
325
326 match expr {
327 Expr::Variable(name) => add_var(name),
328 Expr::Property(base, _) => collect_expr_variables_inner(base, vars),
329 Expr::BinaryOp { left, right, .. } => {
330 collect_expr_variables_inner(left, vars);
331 collect_expr_variables_inner(right, vars);
332 }
333 Expr::UnaryOp { expr: e, .. }
334 | Expr::IsNull(e)
335 | Expr::IsNotNull(e)
336 | Expr::IsUnique(e) => collect_expr_variables_inner(e, vars),
337 Expr::FunctionCall { args, .. } => {
338 for a in args {
339 collect_expr_variables_inner(a, vars);
340 }
341 }
342 Expr::List(items) => {
343 for item in items {
344 collect_expr_variables_inner(item, vars);
345 }
346 }
347 Expr::In { expr: e, list } => {
348 collect_expr_variables_inner(e, vars);
349 collect_expr_variables_inner(list, vars);
350 }
351 Expr::Case {
352 expr: case_expr,
353 when_then,
354 else_expr,
355 } => {
356 if let Some(e) = case_expr {
357 collect_expr_variables_inner(e, vars);
358 }
359 for (w, t) in when_then {
360 collect_expr_variables_inner(w, vars);
361 collect_expr_variables_inner(t, vars);
362 }
363 if let Some(e) = else_expr {
364 collect_expr_variables_inner(e, vars);
365 }
366 }
367 Expr::Map(entries) => {
368 for (_, v) in entries {
369 collect_expr_variables_inner(v, vars);
370 }
371 }
372 Expr::LabelCheck { expr, .. } => collect_expr_variables_inner(expr, vars),
373 Expr::ArrayIndex { array, index } => {
374 collect_expr_variables_inner(array, vars);
375 collect_expr_variables_inner(index, vars);
376 }
377 Expr::ArraySlice { array, start, end } => {
378 collect_expr_variables_inner(array, vars);
379 if let Some(s) = start {
380 collect_expr_variables_inner(s, vars);
381 }
382 if let Some(e) = end {
383 collect_expr_variables_inner(e, vars);
384 }
385 }
386 _ => {}
389 }
390}
391
392fn rewrite_order_by_expr_with_aliases(expr: &Expr, aliases: &HashMap<String, Expr>) -> Expr {
397 let repr = expr.to_string_repr();
398 if let Some(rewritten) = aliases.get(&repr) {
399 return rewritten.clone();
400 }
401
402 match expr {
403 Expr::Variable(name) => aliases.get(name).cloned().unwrap_or_else(|| expr.clone()),
404 Expr::Property(base, prop) => Expr::Property(
405 Box::new(rewrite_order_by_expr_with_aliases(base, aliases)),
406 prop.clone(),
407 ),
408 Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
409 left: Box::new(rewrite_order_by_expr_with_aliases(left, aliases)),
410 op: *op,
411 right: Box::new(rewrite_order_by_expr_with_aliases(right, aliases)),
412 },
413 Expr::UnaryOp { op, expr: inner } => Expr::UnaryOp {
414 op: *op,
415 expr: Box::new(rewrite_order_by_expr_with_aliases(inner, aliases)),
416 },
417 Expr::FunctionCall {
418 name,
419 args,
420 distinct,
421 window_spec,
422 } => Expr::FunctionCall {
423 name: name.clone(),
424 args: args
425 .iter()
426 .map(|a| rewrite_order_by_expr_with_aliases(a, aliases))
427 .collect(),
428 distinct: *distinct,
429 window_spec: window_spec.clone(),
430 },
431 Expr::List(items) => Expr::List(
432 items
433 .iter()
434 .map(|item| rewrite_order_by_expr_with_aliases(item, aliases))
435 .collect(),
436 ),
437 Expr::Map(entries) => Expr::Map(
438 entries
439 .iter()
440 .map(|(k, v)| (k.clone(), rewrite_order_by_expr_with_aliases(v, aliases)))
441 .collect(),
442 ),
443 Expr::Case {
444 expr: case_expr,
445 when_then,
446 else_expr,
447 } => Expr::Case {
448 expr: case_expr
449 .as_ref()
450 .map(|e| Box::new(rewrite_order_by_expr_with_aliases(e, aliases))),
451 when_then: when_then
452 .iter()
453 .map(|(w, t)| {
454 (
455 rewrite_order_by_expr_with_aliases(w, aliases),
456 rewrite_order_by_expr_with_aliases(t, aliases),
457 )
458 })
459 .collect(),
460 else_expr: else_expr
461 .as_ref()
462 .map(|e| Box::new(rewrite_order_by_expr_with_aliases(e, aliases))),
463 },
464 _ => expr.clone(),
467 }
468}
469
470fn validate_function_call(name: &str, args: &[Expr], vars_in_scope: &[VariableInfo]) -> Result<()> {
473 let name_lower = name.to_lowercase();
474
475 if name_lower == "labels"
477 && let Some(Expr::Variable(var_name)) = args.first()
478 && let Some(info) = find_var_in_scope(vars_in_scope, var_name)
479 && !info.var_type.is_compatible_with(VariableType::Node)
480 {
481 return Err(anyhow!(
482 "SyntaxError: InvalidArgumentType - labels() requires a node argument"
483 ));
484 }
485
486 if name_lower == "type"
488 && let Some(Expr::Variable(var_name)) = args.first()
489 && let Some(info) = find_var_in_scope(vars_in_scope, var_name)
490 && !info.var_type.is_compatible_with(VariableType::Edge)
491 {
492 return Err(anyhow!(
493 "SyntaxError: InvalidArgumentType - type() requires a relationship argument"
494 ));
495 }
496
497 if name_lower == "properties"
499 && let Some(arg) = args.first()
500 {
501 match arg {
502 Expr::Literal(CypherLiteral::Integer(_))
503 | Expr::Literal(CypherLiteral::Float(_))
504 | Expr::Literal(CypherLiteral::String(_))
505 | Expr::Literal(CypherLiteral::Bool(_))
506 | Expr::List(_) => {
507 return Err(anyhow!(
508 "SyntaxError: InvalidArgumentType - properties() requires a node, relationship, or map"
509 ));
510 }
511 Expr::Variable(var_name) => {
512 if let Some(info) = find_var_in_scope(vars_in_scope, var_name)
513 && matches!(
514 info.var_type,
515 VariableType::Scalar | VariableType::ScalarLiteral
516 )
517 {
518 return Err(anyhow!(
519 "SyntaxError: InvalidArgumentType - properties() requires a node, relationship, or map"
520 ));
521 }
522 }
523 _ => {}
524 }
525 }
526
527 if (name_lower == "nodes" || name_lower == "relationships")
529 && let Some(Expr::Variable(var_name)) = args.first()
530 && let Some(info) = find_var_in_scope(vars_in_scope, var_name)
531 && !info.var_type.is_compatible_with(VariableType::Path)
532 {
533 return Err(anyhow!(
534 "SyntaxError: InvalidArgumentType - {}() requires a path argument",
535 name_lower
536 ));
537 }
538
539 if name_lower == "size"
541 && let Some(Expr::Variable(var_name)) = args.first()
542 && let Some(info) = find_var_in_scope(vars_in_scope, var_name)
543 && info.var_type == VariableType::Path
544 {
545 return Err(anyhow!(
546 "SyntaxError: InvalidArgumentType - size() requires a string, list, or map argument"
547 ));
548 }
549
550 if (name_lower == "length" || name_lower == "size")
554 && let Some(Expr::Variable(var_name)) = args.first()
555 && let Some(info) = find_var_in_scope(vars_in_scope, var_name)
556 && (info.var_type == VariableType::Node
557 || (info.var_type == VariableType::Edge && !info.is_vlp))
558 {
559 return Err(anyhow!(
560 "SyntaxError: InvalidArgumentType - {}() requires a string, list, or path argument",
561 name_lower
562 ));
563 }
564
565 Ok(())
566}
567
568fn is_non_boolean_literal(expr: &Expr) -> bool {
570 matches!(
571 expr,
572 Expr::Literal(CypherLiteral::Integer(_))
573 | Expr::Literal(CypherLiteral::Float(_))
574 | Expr::Literal(CypherLiteral::String(_))
575 | Expr::List(_)
576 | Expr::Map(_)
577 )
578}
579
580fn validate_boolean_expression(expr: &Expr) -> Result<()> {
582 if let Expr::BinaryOp { left, op, right } = expr
584 && matches!(op, BinaryOp::And | BinaryOp::Or | BinaryOp::Xor)
585 {
586 let op_name = format!("{op:?}").to_uppercase();
587 for operand in [left.as_ref(), right.as_ref()] {
588 if is_non_boolean_literal(operand) {
589 return Err(anyhow!(
590 "SyntaxError: InvalidArgumentType - {} requires boolean arguments",
591 op_name
592 ));
593 }
594 }
595 }
596 if let Expr::UnaryOp {
597 op: uni_cypher::ast::UnaryOp::Not,
598 expr: inner,
599 } = expr
600 && is_non_boolean_literal(inner)
601 {
602 return Err(anyhow!(
603 "SyntaxError: InvalidArgumentType - NOT requires a boolean argument"
604 ));
605 }
606 let mut result = Ok(());
607 expr.for_each_child(&mut |child| {
608 if result.is_ok() {
609 result = validate_boolean_expression(child);
610 }
611 });
612 result
613}
614
615fn validate_expression_variables(expr: &Expr, vars_in_scope: &[VariableInfo]) -> Result<()> {
617 let used_vars = collect_expr_variables(expr);
618 for var in used_vars {
619 if !is_var_in_scope(vars_in_scope, &var) {
620 return Err(anyhow!(
621 "SyntaxError: UndefinedVariable - Variable '{}' not defined",
622 var
623 ));
624 }
625 }
626 Ok(())
627}
628
629fn is_aggregate_function_name(name: &str) -> bool {
631 matches!(
632 name.to_lowercase().as_str(),
633 "count"
634 | "sum"
635 | "avg"
636 | "min"
637 | "max"
638 | "collect"
639 | "stdev"
640 | "stddev"
641 | "stdevp"
642 | "stddevp"
643 | "variance"
644 | "variancep"
645 | "percentiledisc"
646 | "percentilecont"
647 | "btic_min"
648 | "btic_max"
649 | "btic_span_agg"
650 | "btic_count_at"
651 ) || uni_cypher::is_known_plugin_aggregate(name)
652}
653
654fn is_window_function(expr: &Expr) -> bool {
656 matches!(
657 expr,
658 Expr::FunctionCall {
659 window_spec: Some(_),
660 ..
661 }
662 )
663}
664
665fn is_compound_aggregate(expr: &Expr) -> bool {
670 if !expr.is_aggregate() {
671 return false;
672 }
673 match expr {
674 Expr::FunctionCall {
675 name, window_spec, ..
676 } => {
677 if window_spec.is_some() {
679 return true; }
681 !is_aggregate_function_name(name)
682 }
683 Expr::CountSubquery(_) | Expr::CollectSubquery(_) => false,
685 _ => true,
687 }
688}
689
690fn extract_inner_aggregates(expr: &Expr) -> Vec<Expr> {
698 let mut out = Vec::new();
699 extract_inner_aggregates_rec(expr, &mut out);
700 out
701}
702
703fn extract_inner_aggregates_rec(expr: &Expr, out: &mut Vec<Expr>) {
704 match expr {
705 Expr::FunctionCall {
706 name, window_spec, ..
707 } if window_spec.is_none() && is_aggregate_function_name(name) => {
708 out.push(expr.clone());
710 }
711 Expr::CountSubquery(_) | Expr::CollectSubquery(_) => {
712 out.push(expr.clone());
713 }
714 Expr::ListComprehension { list, .. } => {
716 extract_inner_aggregates_rec(list, out);
717 }
718 Expr::Quantifier { list, .. } => {
720 extract_inner_aggregates_rec(list, out);
721 }
722 Expr::Reduce { init, list, .. } => {
724 extract_inner_aggregates_rec(init, out);
725 extract_inner_aggregates_rec(list, out);
726 }
727 Expr::FunctionCall { args, .. } => {
729 for arg in args {
730 extract_inner_aggregates_rec(arg, out);
731 }
732 }
733 Expr::BinaryOp { left, right, .. } => {
734 extract_inner_aggregates_rec(left, out);
735 extract_inner_aggregates_rec(right, out);
736 }
737 Expr::UnaryOp { expr: e, .. }
738 | Expr::IsNull(e)
739 | Expr::IsNotNull(e)
740 | Expr::IsUnique(e) => extract_inner_aggregates_rec(e, out),
741 Expr::Property(base, _) => extract_inner_aggregates_rec(base, out),
742 Expr::List(items) => {
743 for item in items {
744 extract_inner_aggregates_rec(item, out);
745 }
746 }
747 Expr::Case {
748 expr: case_expr,
749 when_then,
750 else_expr,
751 } => {
752 if let Some(e) = case_expr {
753 extract_inner_aggregates_rec(e, out);
754 }
755 for (w, t) in when_then {
756 extract_inner_aggregates_rec(w, out);
757 extract_inner_aggregates_rec(t, out);
758 }
759 if let Some(e) = else_expr {
760 extract_inner_aggregates_rec(e, out);
761 }
762 }
763 Expr::In {
764 expr: in_expr,
765 list,
766 } => {
767 extract_inner_aggregates_rec(in_expr, out);
768 extract_inner_aggregates_rec(list, out);
769 }
770 Expr::ArrayIndex { array, index } => {
771 extract_inner_aggregates_rec(array, out);
772 extract_inner_aggregates_rec(index, out);
773 }
774 Expr::ArraySlice { array, start, end } => {
775 extract_inner_aggregates_rec(array, out);
776 if let Some(s) = start {
777 extract_inner_aggregates_rec(s, out);
778 }
779 if let Some(e) = end {
780 extract_inner_aggregates_rec(e, out);
781 }
782 }
783 Expr::Map(entries) => {
784 for (_, v) in entries {
785 extract_inner_aggregates_rec(v, out);
786 }
787 }
788 _ => {}
789 }
790}
791
792fn replace_aggregates_with_columns(expr: &Expr) -> Expr {
798 match expr {
799 Expr::FunctionCall {
800 name, window_spec, ..
801 } if window_spec.is_none() && is_aggregate_function_name(name) => {
802 Expr::Variable(aggregate_column_name(expr))
804 }
805 Expr::CountSubquery(_) | Expr::CollectSubquery(_) => {
806 Expr::Variable(aggregate_column_name(expr))
807 }
808 Expr::ListComprehension {
809 variable,
810 list,
811 where_clause,
812 map_expr,
813 } => Expr::ListComprehension {
814 variable: variable.clone(),
815 list: Box::new(replace_aggregates_with_columns(list)),
816 where_clause: where_clause.clone(), map_expr: map_expr.clone(), },
819 Expr::Quantifier {
820 quantifier,
821 variable,
822 list,
823 predicate,
824 } => Expr::Quantifier {
825 quantifier: *quantifier,
826 variable: variable.clone(),
827 list: Box::new(replace_aggregates_with_columns(list)),
828 predicate: predicate.clone(), },
830 Expr::Reduce {
831 accumulator,
832 init,
833 variable,
834 list,
835 expr: body,
836 } => Expr::Reduce {
837 accumulator: accumulator.clone(),
838 init: Box::new(replace_aggregates_with_columns(init)),
839 variable: variable.clone(),
840 list: Box::new(replace_aggregates_with_columns(list)),
841 expr: body.clone(), },
843 Expr::FunctionCall {
844 name,
845 args,
846 distinct,
847 window_spec,
848 } => Expr::FunctionCall {
849 name: name.clone(),
850 args: args.iter().map(replace_aggregates_with_columns).collect(),
851 distinct: *distinct,
852 window_spec: window_spec.clone(),
853 },
854 Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
855 left: Box::new(replace_aggregates_with_columns(left)),
856 op: *op,
857 right: Box::new(replace_aggregates_with_columns(right)),
858 },
859 Expr::UnaryOp { op, expr: e } => Expr::UnaryOp {
860 op: *op,
861 expr: Box::new(replace_aggregates_with_columns(e)),
862 },
863 Expr::IsNull(e) => Expr::IsNull(Box::new(replace_aggregates_with_columns(e))),
864 Expr::IsNotNull(e) => Expr::IsNotNull(Box::new(replace_aggregates_with_columns(e))),
865 Expr::IsUnique(e) => Expr::IsUnique(Box::new(replace_aggregates_with_columns(e))),
866 Expr::Property(base, prop) => Expr::Property(
867 Box::new(replace_aggregates_with_columns(base)),
868 prop.clone(),
869 ),
870 Expr::List(items) => {
871 Expr::List(items.iter().map(replace_aggregates_with_columns).collect())
872 }
873 Expr::Case {
874 expr: case_expr,
875 when_then,
876 else_expr,
877 } => Expr::Case {
878 expr: case_expr
879 .as_ref()
880 .map(|e| Box::new(replace_aggregates_with_columns(e))),
881 when_then: when_then
882 .iter()
883 .map(|(w, t)| {
884 (
885 replace_aggregates_with_columns(w),
886 replace_aggregates_with_columns(t),
887 )
888 })
889 .collect(),
890 else_expr: else_expr
891 .as_ref()
892 .map(|e| Box::new(replace_aggregates_with_columns(e))),
893 },
894 Expr::In {
895 expr: in_expr,
896 list,
897 } => Expr::In {
898 expr: Box::new(replace_aggregates_with_columns(in_expr)),
899 list: Box::new(replace_aggregates_with_columns(list)),
900 },
901 Expr::ArrayIndex { array, index } => Expr::ArrayIndex {
902 array: Box::new(replace_aggregates_with_columns(array)),
903 index: Box::new(replace_aggregates_with_columns(index)),
904 },
905 Expr::ArraySlice { array, start, end } => Expr::ArraySlice {
906 array: Box::new(replace_aggregates_with_columns(array)),
907 start: start
908 .as_ref()
909 .map(|e| Box::new(replace_aggregates_with_columns(e))),
910 end: end
911 .as_ref()
912 .map(|e| Box::new(replace_aggregates_with_columns(e))),
913 },
914 Expr::Map(entries) => Expr::Map(
915 entries
916 .iter()
917 .map(|(k, v)| (k.clone(), replace_aggregates_with_columns(v)))
918 .collect(),
919 ),
920 other => other.clone(),
922 }
923}
924
925fn contains_aggregate_recursive(expr: &Expr) -> bool {
927 match expr {
928 Expr::FunctionCall { name, args, .. } => {
929 is_aggregate_function_name(name) || args.iter().any(contains_aggregate_recursive)
930 }
931 Expr::BinaryOp { left, right, .. } => {
932 contains_aggregate_recursive(left) || contains_aggregate_recursive(right)
933 }
934 Expr::UnaryOp { expr: e, .. }
935 | Expr::IsNull(e)
936 | Expr::IsNotNull(e)
937 | Expr::IsUnique(e) => contains_aggregate_recursive(e),
938 Expr::List(items) => items.iter().any(contains_aggregate_recursive),
939 Expr::Case {
940 expr,
941 when_then,
942 else_expr,
943 } => {
944 expr.as_deref().is_some_and(contains_aggregate_recursive)
945 || when_then.iter().any(|(w, t)| {
946 contains_aggregate_recursive(w) || contains_aggregate_recursive(t)
947 })
948 || else_expr
949 .as_deref()
950 .is_some_and(contains_aggregate_recursive)
951 }
952 Expr::In { expr, list } => {
953 contains_aggregate_recursive(expr) || contains_aggregate_recursive(list)
954 }
955 Expr::Property(base, _) => contains_aggregate_recursive(base),
956 Expr::ListComprehension { list, .. } => {
957 contains_aggregate_recursive(list)
959 }
960 Expr::Quantifier { list, .. } => contains_aggregate_recursive(list),
961 Expr::Reduce { init, list, .. } => {
962 contains_aggregate_recursive(init) || contains_aggregate_recursive(list)
963 }
964 Expr::ArrayIndex { array, index } => {
965 contains_aggregate_recursive(array) || contains_aggregate_recursive(index)
966 }
967 Expr::ArraySlice { array, start, end } => {
968 contains_aggregate_recursive(array)
969 || start.as_deref().is_some_and(contains_aggregate_recursive)
970 || end.as_deref().is_some_and(contains_aggregate_recursive)
971 }
972 Expr::Map(entries) => entries.iter().any(|(_, v)| contains_aggregate_recursive(v)),
973 _ => false,
974 }
975}
976
977fn contains_non_deterministic(expr: &Expr) -> bool {
979 if matches!(expr, Expr::FunctionCall { name, .. } if name.eq_ignore_ascii_case("rand")) {
980 return true;
981 }
982 let mut found = false;
983 expr.for_each_child(&mut |child| {
984 if !found {
985 found = contains_non_deterministic(child);
986 }
987 });
988 found
989}
990
991fn collect_aggregate_reprs(expr: &Expr, out: &mut HashSet<String>) {
992 match expr {
993 Expr::FunctionCall { name, args, .. } => {
994 if is_aggregate_function_name(name) {
995 out.insert(expr.to_string_repr());
996 return;
997 }
998 for arg in args {
999 collect_aggregate_reprs(arg, out);
1000 }
1001 }
1002 Expr::BinaryOp { left, right, .. } => {
1003 collect_aggregate_reprs(left, out);
1004 collect_aggregate_reprs(right, out);
1005 }
1006 Expr::UnaryOp { expr, .. }
1007 | Expr::IsNull(expr)
1008 | Expr::IsNotNull(expr)
1009 | Expr::IsUnique(expr) => collect_aggregate_reprs(expr, out),
1010 Expr::List(items) => {
1011 for item in items {
1012 collect_aggregate_reprs(item, out);
1013 }
1014 }
1015 Expr::Case {
1016 expr,
1017 when_then,
1018 else_expr,
1019 } => {
1020 if let Some(e) = expr {
1021 collect_aggregate_reprs(e, out);
1022 }
1023 for (w, t) in when_then {
1024 collect_aggregate_reprs(w, out);
1025 collect_aggregate_reprs(t, out);
1026 }
1027 if let Some(e) = else_expr {
1028 collect_aggregate_reprs(e, out);
1029 }
1030 }
1031 Expr::In { expr, list } => {
1032 collect_aggregate_reprs(expr, out);
1033 collect_aggregate_reprs(list, out);
1034 }
1035 Expr::Property(base, _) => collect_aggregate_reprs(base, out),
1036 Expr::ListComprehension { list, .. } => {
1037 collect_aggregate_reprs(list, out);
1038 }
1039 Expr::Quantifier { list, .. } => {
1040 collect_aggregate_reprs(list, out);
1041 }
1042 Expr::Reduce { init, list, .. } => {
1043 collect_aggregate_reprs(init, out);
1044 collect_aggregate_reprs(list, out);
1045 }
1046 Expr::ArrayIndex { array, index } => {
1047 collect_aggregate_reprs(array, out);
1048 collect_aggregate_reprs(index, out);
1049 }
1050 Expr::ArraySlice { array, start, end } => {
1051 collect_aggregate_reprs(array, out);
1052 if let Some(s) = start {
1053 collect_aggregate_reprs(s, out);
1054 }
1055 if let Some(e) = end {
1056 collect_aggregate_reprs(e, out);
1057 }
1058 }
1059 _ => {}
1060 }
1061}
1062
1063#[derive(Debug, Clone)]
1064enum NonAggregateRef {
1065 Var(String),
1066 Property {
1067 repr: String,
1068 base_var: Option<String>,
1069 },
1070}
1071
1072fn collect_non_aggregate_refs(expr: &Expr, inside_agg: bool, out: &mut Vec<NonAggregateRef>) {
1073 match expr {
1074 Expr::FunctionCall { name, args, .. } => {
1075 if is_aggregate_function_name(name) {
1076 return;
1077 }
1078 for arg in args {
1079 collect_non_aggregate_refs(arg, inside_agg, out);
1080 }
1081 }
1082 Expr::Variable(v) if !inside_agg => out.push(NonAggregateRef::Var(v.clone())),
1083 Expr::Property(base, _) if !inside_agg => {
1084 let base_var = if let Expr::Variable(v) = base.as_ref() {
1085 Some(v.clone())
1086 } else {
1087 None
1088 };
1089 out.push(NonAggregateRef::Property {
1090 repr: expr.to_string_repr(),
1091 base_var,
1092 });
1093 }
1094 Expr::BinaryOp { left, right, .. } => {
1095 collect_non_aggregate_refs(left, inside_agg, out);
1096 collect_non_aggregate_refs(right, inside_agg, out);
1097 }
1098 Expr::UnaryOp { expr, .. }
1099 | Expr::IsNull(expr)
1100 | Expr::IsNotNull(expr)
1101 | Expr::IsUnique(expr) => collect_non_aggregate_refs(expr, inside_agg, out),
1102 Expr::List(items) => {
1103 for item in items {
1104 collect_non_aggregate_refs(item, inside_agg, out);
1105 }
1106 }
1107 Expr::Case {
1108 expr,
1109 when_then,
1110 else_expr,
1111 } => {
1112 if let Some(e) = expr {
1113 collect_non_aggregate_refs(e, inside_agg, out);
1114 }
1115 for (w, t) in when_then {
1116 collect_non_aggregate_refs(w, inside_agg, out);
1117 collect_non_aggregate_refs(t, inside_agg, out);
1118 }
1119 if let Some(e) = else_expr {
1120 collect_non_aggregate_refs(e, inside_agg, out);
1121 }
1122 }
1123 Expr::In { expr, list } => {
1124 collect_non_aggregate_refs(expr, inside_agg, out);
1125 collect_non_aggregate_refs(list, inside_agg, out);
1126 }
1127 Expr::ListComprehension { list, .. } => {
1130 collect_non_aggregate_refs(list, inside_agg, out);
1131 }
1132 Expr::Quantifier { list, .. } => {
1133 collect_non_aggregate_refs(list, inside_agg, out);
1134 }
1135 Expr::Reduce { init, list, .. } => {
1136 collect_non_aggregate_refs(init, inside_agg, out);
1137 collect_non_aggregate_refs(list, inside_agg, out);
1138 }
1139 _ => {}
1140 }
1141}
1142
1143fn validate_with_order_by_aggregate_item(
1144 expr: &Expr,
1145 projected_aggregate_reprs: &HashSet<String>,
1146 projected_simple_reprs: &HashSet<String>,
1147 projected_aliases: &HashSet<String>,
1148) -> Result<()> {
1149 let mut aggregate_reprs = HashSet::new();
1150 collect_aggregate_reprs(expr, &mut aggregate_reprs);
1151 for agg in aggregate_reprs {
1152 if !projected_aggregate_reprs.contains(&agg) {
1153 return Err(anyhow!(
1154 "SyntaxError: UndefinedVariable - Aggregation expression '{}' is not projected in WITH",
1155 agg
1156 ));
1157 }
1158 }
1159
1160 let mut refs = Vec::new();
1161 collect_non_aggregate_refs(expr, false, &mut refs);
1162 refs.retain(|r| match r {
1163 NonAggregateRef::Var(v) => !projected_aliases.contains(v),
1164 NonAggregateRef::Property { repr, .. } => !projected_simple_reprs.contains(repr),
1165 });
1166
1167 let mut dedup = HashSet::new();
1168 refs.retain(|r| {
1169 let key = match r {
1170 NonAggregateRef::Var(v) => format!("v:{v}"),
1171 NonAggregateRef::Property { repr, .. } => format!("p:{repr}"),
1172 };
1173 dedup.insert(key)
1174 });
1175
1176 if refs.len() > 1 {
1177 return Err(anyhow!(
1178 "SyntaxError: AmbiguousAggregationExpression - ORDER BY item mixes aggregation with multiple non-grouping references"
1179 ));
1180 }
1181
1182 if let Some(r) = refs.first() {
1183 return match r {
1184 NonAggregateRef::Var(v) => Err(anyhow!(
1185 "SyntaxError: UndefinedVariable - Variable '{}' not defined",
1186 v
1187 )),
1188 NonAggregateRef::Property { base_var, .. } => Err(anyhow!(
1189 "SyntaxError: UndefinedVariable - Variable '{}' not defined",
1190 base_var
1191 .clone()
1192 .unwrap_or_else(|| "<property-base>".to_string())
1193 )),
1194 };
1195 }
1196
1197 Ok(())
1198}
1199
1200fn validate_no_aggregation_in_where(predicate: &Expr) -> Result<()> {
1202 if contains_aggregate_recursive(predicate) {
1203 return Err(anyhow!(
1204 "SyntaxError: InvalidAggregation - Aggregation functions not allowed in WHERE"
1205 ));
1206 }
1207 Ok(())
1208}
1209
1210#[derive(Debug, Clone, Copy)]
1211enum ConstNumber {
1212 Int(i64),
1213 Float(f64),
1214}
1215
1216impl ConstNumber {
1217 fn to_f64(self) -> f64 {
1218 match self {
1219 Self::Int(v) => v as f64,
1220 Self::Float(v) => v,
1221 }
1222 }
1223}
1224
1225fn eval_const_numeric_expr(
1226 expr: &Expr,
1227 params: &HashMap<String, uni_common::Value>,
1228) -> Result<ConstNumber> {
1229 match expr {
1230 Expr::Literal(CypherLiteral::Integer(n)) => Ok(ConstNumber::Int(*n)),
1231 Expr::Literal(CypherLiteral::Float(f)) => Ok(ConstNumber::Float(*f)),
1232 Expr::Parameter(name) => match params.get(name) {
1233 Some(uni_common::Value::Int(n)) => Ok(ConstNumber::Int(*n)),
1234 Some(uni_common::Value::Float(f)) => Ok(ConstNumber::Float(*f)),
1235 Some(uni_common::Value::Null) => Err(anyhow!(
1236 "TypeError: InvalidArgumentType - expected numeric value for parameter ${}, got null",
1237 name
1238 )),
1239 Some(other) => Err(anyhow!(
1240 "TypeError: InvalidArgumentType - expected numeric value for parameter ${}, got {:?}",
1241 name,
1242 other
1243 )),
1244 None => Err(anyhow!(
1245 "SyntaxError: InvalidArgumentType - expression is not a constant integer expression"
1246 )),
1247 },
1248 Expr::UnaryOp {
1249 op: uni_cypher::ast::UnaryOp::Neg,
1250 expr,
1251 } => match eval_const_numeric_expr(expr, params)? {
1252 ConstNumber::Int(v) => Ok(ConstNumber::Int(-v)),
1253 ConstNumber::Float(v) => Ok(ConstNumber::Float(-v)),
1254 },
1255 Expr::BinaryOp { left, op, right } => {
1256 let l = eval_const_numeric_expr(left, params)?;
1257 let r = eval_const_numeric_expr(right, params)?;
1258 match op {
1259 BinaryOp::Add => match (l, r) {
1260 (ConstNumber::Int(a), ConstNumber::Int(b)) => Ok(ConstNumber::Int(a + b)),
1261 _ => Ok(ConstNumber::Float(l.to_f64() + r.to_f64())),
1262 },
1263 BinaryOp::Sub => match (l, r) {
1264 (ConstNumber::Int(a), ConstNumber::Int(b)) => Ok(ConstNumber::Int(a - b)),
1265 _ => Ok(ConstNumber::Float(l.to_f64() - r.to_f64())),
1266 },
1267 BinaryOp::Mul => match (l, r) {
1268 (ConstNumber::Int(a), ConstNumber::Int(b)) => Ok(ConstNumber::Int(a * b)),
1269 _ => Ok(ConstNumber::Float(l.to_f64() * r.to_f64())),
1270 },
1271 BinaryOp::Div => Ok(ConstNumber::Float(l.to_f64() / r.to_f64())),
1272 BinaryOp::Mod => match (l, r) {
1273 (ConstNumber::Int(a), ConstNumber::Int(b)) => Ok(ConstNumber::Int(a % b)),
1274 _ => Ok(ConstNumber::Float(l.to_f64() % r.to_f64())),
1275 },
1276 BinaryOp::Pow => Ok(ConstNumber::Float(l.to_f64().powf(r.to_f64()))),
1277 _ => Err(anyhow!(
1278 "SyntaxError: InvalidArgumentType - unsupported operator in constant expression"
1279 )),
1280 }
1281 }
1282 Expr::FunctionCall { name, args, .. } => {
1283 let lower = name.to_lowercase();
1284 match lower.as_str() {
1285 "rand" if args.is_empty() => {
1286 use rand::RngExt;
1287 let mut rng = rand::rng();
1288 Ok(ConstNumber::Float(rng.random::<f64>()))
1289 }
1290 "tointeger" | "toint" if args.len() == 1 => {
1291 match eval_const_numeric_expr(&args[0], params)? {
1292 ConstNumber::Int(v) => Ok(ConstNumber::Int(v)),
1293 ConstNumber::Float(v) => Ok(ConstNumber::Int(v.trunc() as i64)),
1294 }
1295 }
1296 "ceil" if args.len() == 1 => Ok(ConstNumber::Float(
1297 eval_const_numeric_expr(&args[0], params)?.to_f64().ceil(),
1298 )),
1299 "floor" if args.len() == 1 => Ok(ConstNumber::Float(
1300 eval_const_numeric_expr(&args[0], params)?.to_f64().floor(),
1301 )),
1302 "abs" if args.len() == 1 => match eval_const_numeric_expr(&args[0], params)? {
1303 ConstNumber::Int(v) => Ok(ConstNumber::Int(v.abs())),
1304 ConstNumber::Float(v) => Ok(ConstNumber::Float(v.abs())),
1305 },
1306 _ => Err(anyhow!(
1307 "SyntaxError: InvalidArgumentType - expression is not a constant integer expression"
1308 )),
1309 }
1310 }
1311 _ => Err(anyhow!(
1312 "SyntaxError: InvalidArgumentType - expression is not a constant integer expression"
1313 )),
1314 }
1315}
1316
1317fn parse_non_negative_integer(
1320 expr: &Expr,
1321 clause_name: &str,
1322 params: &HashMap<String, uni_common::Value>,
1323) -> Result<Option<usize>> {
1324 let referenced_vars = collect_expr_variables(expr);
1325 if !referenced_vars.is_empty() {
1326 return Err(anyhow!(
1327 "SyntaxError: NonConstantExpression - {} requires expression independent of row variables",
1328 clause_name
1329 ));
1330 }
1331
1332 let value = eval_const_numeric_expr(expr, params)?;
1333 let as_int = match value {
1334 ConstNumber::Int(v) => v,
1335 ConstNumber::Float(v) => {
1336 if !v.is_finite() || (v.fract().abs() > f64::EPSILON) {
1337 return Err(anyhow!(
1338 "SyntaxError: InvalidArgumentType - {} requires integer, got float",
1339 clause_name
1340 ));
1341 }
1342 v as i64
1343 }
1344 };
1345 if as_int < 0 {
1346 return Err(anyhow!(
1347 "SyntaxError: NegativeIntegerArgument - {} requires non-negative integer",
1348 clause_name
1349 ));
1350 }
1351 Ok(Some(as_int as usize))
1352}
1353
1354fn validate_no_nested_aggregation(expr: &Expr) -> Result<()> {
1356 if let Expr::FunctionCall { name, args, .. } = expr
1357 && is_aggregate_function_name(name)
1358 {
1359 for arg in args {
1360 if contains_aggregate_recursive(arg) {
1361 return Err(anyhow!(
1362 "SyntaxError: NestedAggregation - Cannot nest aggregation functions"
1363 ));
1364 }
1365 if contains_non_deterministic(arg) {
1366 return Err(anyhow!(
1367 "SyntaxError: NonConstantExpression - Non-deterministic function inside aggregation"
1368 ));
1369 }
1370 }
1371 }
1372 let mut result = Ok(());
1373 expr.for_each_child(&mut |child| {
1374 if result.is_ok() {
1375 result = validate_no_nested_aggregation(child);
1376 }
1377 });
1378 result
1379}
1380
1381fn validate_no_deleted_entity_access(expr: &Expr, deleted_vars: &HashSet<String>) -> Result<()> {
1385 if let Expr::Property(inner, _) = expr
1387 && let Expr::Variable(name) = inner.as_ref()
1388 && deleted_vars.contains(name)
1389 {
1390 return Err(anyhow!(
1391 "EntityNotFound: DeletedEntityAccess - Cannot access properties of deleted entity '{}'",
1392 name
1393 ));
1394 }
1395 if let Expr::FunctionCall { name, args, .. } = expr
1397 && matches!(name.to_lowercase().as_str(), "labels" | "keys")
1398 && args.len() == 1
1399 && let Expr::Variable(var) = &args[0]
1400 && deleted_vars.contains(var)
1401 {
1402 return Err(anyhow!(
1403 "EntityNotFound: DeletedEntityAccess - Cannot access {} of deleted entity '{}'",
1404 name.to_lowercase(),
1405 var
1406 ));
1407 }
1408 let mut result = Ok(());
1409 expr.for_each_child(&mut |child| {
1410 if result.is_ok() {
1411 result = validate_no_deleted_entity_access(child, deleted_vars);
1412 }
1413 });
1414 result
1415}
1416
1417fn validate_property_variables(
1420 properties: &Option<Expr>,
1421 vars_in_scope: &[VariableInfo],
1422 create_vars: &[&str],
1423) -> Result<()> {
1424 if let Some(props) = properties {
1425 for var in collect_expr_variables(props) {
1426 if !is_var_in_scope(vars_in_scope, &var) && !create_vars.contains(&var.as_str()) {
1427 return Err(anyhow!(
1428 "SyntaxError: UndefinedVariable - Variable '{}' not defined",
1429 var
1430 ));
1431 }
1432 }
1433 }
1434 Ok(())
1435}
1436
1437fn check_not_already_bound(
1440 name: &str,
1441 vars_in_scope: &[VariableInfo],
1442 create_vars: &[&str],
1443) -> Result<()> {
1444 if is_var_in_scope(vars_in_scope, name) {
1445 return Err(anyhow!(
1446 "SyntaxError: VariableAlreadyBound - Variable '{}' already defined",
1447 name
1448 ));
1449 }
1450 if create_vars.contains(&name) {
1451 return Err(anyhow!(
1452 "SyntaxError: VariableAlreadyBound - Variable '{}' already defined in CREATE",
1453 name
1454 ));
1455 }
1456 Ok(())
1457}
1458
1459fn build_merge_scope(pattern: &Pattern, vars_in_scope: &[VariableInfo]) -> Vec<VariableInfo> {
1460 let mut scope = vars_in_scope.to_vec();
1461
1462 for path in &pattern.paths {
1463 if let Some(path_var) = &path.variable
1464 && !path_var.is_empty()
1465 && !is_var_in_scope(&scope, path_var)
1466 {
1467 scope.push(VariableInfo::new(path_var.clone(), VariableType::Path));
1468 }
1469 for element in &path.elements {
1470 match element {
1471 PatternElement::Node(n) => {
1472 if let Some(v) = &n.variable
1473 && !v.is_empty()
1474 && !is_var_in_scope(&scope, v)
1475 {
1476 scope.push(VariableInfo::new(v.clone(), VariableType::Node));
1477 }
1478 }
1479 PatternElement::Relationship(r) => {
1480 if let Some(v) = &r.variable
1481 && !v.is_empty()
1482 && !is_var_in_scope(&scope, v)
1483 {
1484 scope.push(VariableInfo::new(v.clone(), VariableType::Edge));
1485 }
1486 }
1487 PatternElement::Parenthesized { .. } => {}
1488 }
1489 }
1490 }
1491
1492 scope
1493}
1494
1495fn validate_merge_set_item(item: &SetItem, vars_in_scope: &[VariableInfo]) -> Result<()> {
1496 match item {
1497 SetItem::Property { expr, value } => {
1498 validate_expression_variables(expr, vars_in_scope)?;
1499 validate_expression(expr, vars_in_scope)?;
1500 validate_expression_variables(value, vars_in_scope)?;
1501 validate_expression(value, vars_in_scope)?;
1502 if contains_pattern_predicate(expr) || contains_pattern_predicate(value) {
1503 return Err(anyhow!(
1504 "SyntaxError: UnexpectedSyntax - Pattern predicates are not allowed in SET"
1505 ));
1506 }
1507 }
1508 SetItem::Variable { variable, value } | SetItem::VariablePlus { variable, value } => {
1509 if !is_var_in_scope(vars_in_scope, variable) {
1510 return Err(anyhow!(
1511 "SyntaxError: UndefinedVariable - Variable '{}' not defined",
1512 variable
1513 ));
1514 }
1515 validate_expression_variables(value, vars_in_scope)?;
1516 validate_expression(value, vars_in_scope)?;
1517 if contains_pattern_predicate(value) {
1518 return Err(anyhow!(
1519 "SyntaxError: UnexpectedSyntax - Pattern predicates are not allowed in SET"
1520 ));
1521 }
1522 }
1523 SetItem::Labels { variable, .. } => {
1524 if !is_var_in_scope(vars_in_scope, variable) {
1525 return Err(anyhow!(
1526 "SyntaxError: UndefinedVariable - Variable '{}' not defined",
1527 variable
1528 ));
1529 }
1530 }
1531 }
1532
1533 Ok(())
1534}
1535
1536fn reject_null_merge_properties(properties: &Option<Expr>) -> Result<()> {
1539 if let Some(Expr::Map(entries)) = properties {
1540 for (key, value) in entries {
1541 if matches!(value, Expr::Literal(CypherLiteral::Null)) {
1542 return Err(anyhow!(
1543 "SemanticError: MergeReadOwnWrites - MERGE cannot use null property value for '{}'",
1544 key
1545 ));
1546 }
1547 }
1548 }
1549 Ok(())
1550}
1551
1552fn collect_pattern_labels(pattern: &uni_cypher::ast::Pattern) -> Vec<String> {
1557 let mut out = Vec::new();
1558 for path in &pattern.paths {
1559 for element in &path.elements {
1560 if let PatternElement::Node(n) = element {
1561 for l in n.labels.names() {
1562 out.push(l.clone());
1563 }
1564 }
1565 }
1566 }
1567 out
1568}
1569
1570fn validate_merge_clause(merge_clause: &MergeClause, vars_in_scope: &[VariableInfo]) -> Result<()> {
1571 for path in &merge_clause.pattern.paths {
1572 for element in &path.elements {
1573 match element {
1574 PatternElement::Node(n) => {
1575 if let Some(Expr::Parameter(_)) = &n.properties {
1576 return Err(anyhow!(
1577 "SyntaxError: InvalidParameterUse - Parameters cannot be used as node predicates"
1578 ));
1579 }
1580 reject_null_merge_properties(&n.properties)?;
1581 if let Some(variable) = &n.variable
1585 && !variable.is_empty()
1586 && is_var_in_scope(vars_in_scope, variable)
1587 {
1588 let is_standalone = path.elements.len() == 1;
1589 let has_new_labels = !n.labels.is_empty();
1590 let has_new_properties = n.properties.is_some();
1591 if is_standalone || has_new_labels || has_new_properties {
1592 return Err(anyhow!(
1593 "SyntaxError: VariableAlreadyBound - Variable '{}' already defined",
1594 variable
1595 ));
1596 }
1597 }
1598 }
1599 PatternElement::Relationship(r) => {
1600 if let Some(variable) = &r.variable
1601 && !variable.is_empty()
1602 && is_var_in_scope(vars_in_scope, variable)
1603 {
1604 return Err(anyhow!(
1605 "SyntaxError: VariableAlreadyBound - Variable '{}' already defined",
1606 variable
1607 ));
1608 }
1609 if r.types.len() != 1 {
1610 return Err(anyhow!(
1611 "SyntaxError: NoSingleRelationshipType - Exactly one relationship type required for MERGE"
1612 ));
1613 }
1614 if r.range.is_some() {
1615 return Err(anyhow!(
1616 "SyntaxError: CreatingVarLength - Variable length relationships cannot be created"
1617 ));
1618 }
1619 if let Some(Expr::Parameter(_)) = &r.properties {
1620 return Err(anyhow!(
1621 "SyntaxError: InvalidParameterUse - Parameters cannot be used as relationship predicates"
1622 ));
1623 }
1624 reject_null_merge_properties(&r.properties)?;
1625 }
1626 PatternElement::Parenthesized { .. } => {}
1627 }
1628 }
1629 }
1630
1631 let merge_scope = build_merge_scope(&merge_clause.pattern, vars_in_scope);
1632 for item in &merge_clause.on_create {
1633 validate_merge_set_item(item, &merge_scope)?;
1634 }
1635 for item in &merge_clause.on_match {
1636 validate_merge_set_item(item, &merge_scope)?;
1637 }
1638
1639 Ok(())
1640}
1641
1642fn validate_expression(expr: &Expr, vars_in_scope: &[VariableInfo]) -> Result<()> {
1644 validate_boolean_expression(expr)?;
1646 validate_no_nested_aggregation(expr)?;
1647
1648 fn validate_all(exprs: &[Expr], vars: &[VariableInfo]) -> Result<()> {
1650 for e in exprs {
1651 validate_expression(e, vars)?;
1652 }
1653 Ok(())
1654 }
1655
1656 match expr {
1657 Expr::FunctionCall { name, args, .. } => {
1658 validate_function_call(name, args, vars_in_scope)?;
1659 validate_all(args, vars_in_scope)
1660 }
1661 Expr::BinaryOp { left, right, .. } => {
1662 validate_expression(left, vars_in_scope)?;
1663 validate_expression(right, vars_in_scope)
1664 }
1665 Expr::UnaryOp { expr: e, .. }
1666 | Expr::IsNull(e)
1667 | Expr::IsNotNull(e)
1668 | Expr::IsUnique(e) => validate_expression(e, vars_in_scope),
1669 Expr::Property(base, prop) => {
1670 if let Expr::Variable(var_name) = base.as_ref()
1671 && let Some(var_info) = find_var_in_scope(vars_in_scope, var_name)
1672 {
1673 if var_info.var_type == VariableType::Path {
1675 return Err(anyhow!(
1676 "SyntaxError: InvalidArgumentType - Type mismatch: expected Node or Relationship but was Path for property access '{}.{}'",
1677 var_name,
1678 prop
1679 ));
1680 }
1681 if var_info.var_type == VariableType::ScalarLiteral {
1683 return Err(anyhow!(
1684 "TypeError: InvalidArgumentType - Property access on a non-graph element is not allowed"
1685 ));
1686 }
1687 }
1688 validate_expression(base, vars_in_scope)
1689 }
1690 Expr::List(items) => validate_all(items, vars_in_scope),
1691 Expr::Case {
1692 expr: case_expr,
1693 when_then,
1694 else_expr,
1695 } => {
1696 if let Some(e) = case_expr {
1697 validate_expression(e, vars_in_scope)?;
1698 }
1699 for (w, t) in when_then {
1700 validate_expression(w, vars_in_scope)?;
1701 validate_expression(t, vars_in_scope)?;
1702 }
1703 if let Some(e) = else_expr {
1704 validate_expression(e, vars_in_scope)?;
1705 }
1706 Ok(())
1707 }
1708 Expr::In { expr: e, list } => {
1709 validate_expression(e, vars_in_scope)?;
1710 validate_expression(list, vars_in_scope)
1711 }
1712 Expr::Exists {
1713 query,
1714 from_pattern_predicate: true,
1715 } => {
1716 if let Query::Single(stmt) = query.as_ref() {
1719 for clause in &stmt.clauses {
1720 if let Clause::Match(m) = clause {
1721 for path in &m.pattern.paths {
1722 for elem in &path.elements {
1723 match elem {
1724 PatternElement::Node(n) => {
1725 if let Some(var) = &n.variable
1726 && !is_var_in_scope(vars_in_scope, var)
1727 {
1728 return Err(anyhow!(
1729 "SyntaxError: UndefinedVariable - Variable '{}' not defined",
1730 var
1731 ));
1732 }
1733 }
1734 PatternElement::Relationship(r) => {
1735 if let Some(var) = &r.variable
1736 && !is_var_in_scope(vars_in_scope, var)
1737 {
1738 return Err(anyhow!(
1739 "SyntaxError: UndefinedVariable - Variable '{}' not defined",
1740 var
1741 ));
1742 }
1743 }
1744 _ => {}
1745 }
1746 }
1747 }
1748 }
1749 }
1750 }
1751 Ok(())
1752 }
1753 _ => Ok(()),
1754 }
1755}
1756
1757#[derive(Debug, Clone)]
1761pub struct QppStepInfo {
1762 pub edge_type_ids: Vec<u32>,
1764 pub direction: Direction,
1766 pub target_label: Option<String>,
1768}
1769
1770#[derive(Debug, Clone)]
1775#[non_exhaustive]
1776pub enum FusionKind {
1777 BtreeUnion,
1779 SortedKWayMerge,
1781 VidUidForkFirst,
1785 AnnRerank,
1789 Bm25Rrf,
1793}
1794
1795#[derive(Debug, Clone)]
1801pub enum LogicalPlan {
1802 Union {
1804 left: Box<LogicalPlan>,
1805 right: Box<LogicalPlan>,
1806 all: bool,
1808 },
1809 Scan {
1811 label_id: u16,
1812 labels: Vec<String>,
1813 variable: String,
1814 filter: Option<Expr>,
1815 optional: bool,
1816 },
1817 FusedIndexScan {
1830 label_id: u16,
1831 labels: Vec<String>,
1832 variable: String,
1833 filter: Option<Expr>,
1834 optional: bool,
1835 kind: FusionKind,
1836 },
1837 FusedIndexScanWrapped {
1849 inner: Box<LogicalPlan>,
1850 kind: FusionKind,
1851 },
1852 ExtIdLookup {
1855 variable: String,
1856 ext_id: String,
1857 filter: Option<Expr>,
1858 optional: bool,
1859 },
1860 ScanAll {
1863 variable: String,
1864 filter: Option<Expr>,
1865 optional: bool,
1866 },
1867 ScanMainByLabels {
1872 labels: Vec<String>,
1873 variable: String,
1874 filter: Option<Expr>,
1875 optional: bool,
1876 },
1877 Empty,
1879 Unwind {
1881 input: Box<LogicalPlan>,
1882 expr: Expr,
1883 variable: String,
1884 },
1885 Traverse {
1886 input: Box<LogicalPlan>,
1887 edge_type_ids: Vec<u32>,
1888 direction: Direction,
1889 source_variable: String,
1890 target_variable: String,
1891 target_label_id: u16,
1892 step_variable: Option<String>,
1893 min_hops: usize,
1894 max_hops: usize,
1895 optional: bool,
1896 target_filter: Option<Expr>,
1897 path_variable: Option<String>,
1898 edge_properties: HashSet<String>,
1899 is_variable_length: bool,
1902 optional_pattern_vars: HashSet<String>,
1906 scope_match_variables: HashSet<String>,
1911 edge_filter_expr: Option<Expr>,
1913 path_mode: crate::query::df_graph::nfa::PathMode,
1915 qpp_steps: Option<Vec<QppStepInfo>>,
1919 },
1920 TraverseMainByType {
1924 type_names: Vec<String>,
1925 input: Box<LogicalPlan>,
1926 direction: Direction,
1927 source_variable: String,
1928 target_variable: String,
1929 step_variable: Option<String>,
1930 min_hops: usize,
1931 max_hops: usize,
1932 optional: bool,
1933 target_filter: Option<Expr>,
1934 path_variable: Option<String>,
1935 is_variable_length: bool,
1938 optional_pattern_vars: HashSet<String>,
1941 scope_match_variables: HashSet<String>,
1945 edge_filter_expr: Option<Expr>,
1947 path_mode: crate::query::df_graph::nfa::PathMode,
1949 },
1950 Filter {
1951 input: Box<LogicalPlan>,
1952 predicate: Expr,
1953 optional_variables: HashSet<String>,
1957 },
1958 Create {
1959 input: Box<LogicalPlan>,
1960 pattern: Pattern,
1961 },
1962 CreateBatch {
1967 input: Box<LogicalPlan>,
1968 patterns: Vec<Pattern>,
1969 },
1970 Merge {
1971 input: Box<LogicalPlan>,
1972 pattern: Pattern,
1973 on_match: Option<SetClause>,
1974 on_create: Option<SetClause>,
1975 },
1976 Set {
1977 input: Box<LogicalPlan>,
1978 items: Vec<SetItem>,
1979 },
1980 Remove {
1981 input: Box<LogicalPlan>,
1982 items: Vec<RemoveItem>,
1983 },
1984 Delete {
1985 input: Box<LogicalPlan>,
1986 items: Vec<Expr>,
1987 detach: bool,
1988 },
1989 Foreach {
1991 input: Box<LogicalPlan>,
1992 variable: String,
1993 list: Expr,
1994 body: Vec<LogicalPlan>,
1995 },
1996 Sort {
1997 input: Box<LogicalPlan>,
1998 order_by: Vec<SortItem>,
1999 },
2000 Limit {
2001 input: Box<LogicalPlan>,
2002 skip: Option<usize>,
2003 fetch: Option<usize>,
2004 },
2005 Aggregate {
2006 input: Box<LogicalPlan>,
2007 group_by: Vec<Expr>,
2008 aggregates: Vec<Expr>,
2009 },
2010 Distinct {
2011 input: Box<LogicalPlan>,
2012 },
2013 Window {
2014 input: Box<LogicalPlan>,
2015 window_exprs: Vec<Expr>,
2016 },
2017 Project {
2018 input: Box<LogicalPlan>,
2019 projections: Vec<(Expr, Option<String>)>,
2020 },
2021 CrossJoin {
2022 left: Box<LogicalPlan>,
2023 right: Box<LogicalPlan>,
2024 },
2025 Apply {
2026 input: Box<LogicalPlan>,
2027 subquery: Box<LogicalPlan>,
2028 input_filter: Option<Expr>,
2029 },
2030 RecursiveCTE {
2031 cte_name: String,
2032 initial: Box<LogicalPlan>,
2033 recursive: Box<LogicalPlan>,
2034 },
2035 ProcedureCall {
2036 procedure_name: String,
2037 arguments: Vec<Expr>,
2038 yield_items: Vec<(String, Option<String>)>,
2039 },
2040 SubqueryCall {
2041 input: Box<LogicalPlan>,
2042 subquery: Box<LogicalPlan>,
2043 },
2044 VectorKnn {
2045 label_id: u16,
2046 variable: String,
2047 property: String,
2048 query: Expr,
2049 k: usize,
2050 threshold: Option<f32>,
2051 },
2052 InvertedIndexLookup {
2053 label_id: u16,
2054 variable: String,
2055 property: String,
2056 terms: Expr,
2057 },
2058 ShortestPath {
2059 input: Box<LogicalPlan>,
2060 edge_type_ids: Vec<u32>,
2061 direction: Direction,
2062 source_variable: String,
2063 target_variable: String,
2064 target_label_id: u16,
2065 path_variable: String,
2066 min_hops: u32,
2068 max_hops: u32,
2070 },
2071 AllShortestPaths {
2073 input: Box<LogicalPlan>,
2074 edge_type_ids: Vec<u32>,
2075 direction: Direction,
2076 source_variable: String,
2077 target_variable: String,
2078 target_label_id: u16,
2079 path_variable: String,
2080 min_hops: u32,
2082 max_hops: u32,
2084 },
2085 QuantifiedPattern {
2086 input: Box<LogicalPlan>,
2087 pattern_plan: Box<LogicalPlan>, min_iterations: u32,
2089 max_iterations: u32,
2090 path_variable: Option<String>,
2091 start_variable: String, binding_variable: String, },
2094 CreateVectorIndex {
2096 config: VectorIndexConfig,
2097 if_not_exists: bool,
2098 },
2099 CreateFullTextIndex {
2100 config: FullTextIndexConfig,
2101 if_not_exists: bool,
2102 },
2103 CreateScalarIndex {
2104 config: ScalarIndexConfig,
2105 if_not_exists: bool,
2106 },
2107 CreateJsonFtsIndex {
2108 config: JsonFtsIndexConfig,
2109 if_not_exists: bool,
2110 },
2111 DropIndex {
2112 name: String,
2113 if_exists: bool,
2114 },
2115 ShowIndexes {
2116 filter: Option<String>,
2117 },
2118 Copy {
2119 target: String,
2120 source: String,
2121 is_export: bool,
2122 options: HashMap<String, Value>,
2123 },
2124 Backup {
2125 destination: String,
2126 options: HashMap<String, Value>,
2127 },
2128 Explain {
2129 plan: Box<LogicalPlan>,
2130 },
2131 ShowDatabase,
2133 ShowConfig,
2134 ShowStatistics,
2135 Vacuum,
2136 Checkpoint,
2137 CopyTo {
2138 label: String,
2139 path: String,
2140 format: String,
2141 options: HashMap<String, Value>,
2142 },
2143 CopyFrom {
2144 label: String,
2145 path: String,
2146 format: String,
2147 options: HashMap<String, Value>,
2148 },
2149 CreateLabel(CreateLabel),
2151 CreateEdgeType(CreateEdgeType),
2152 AlterLabel(AlterLabel),
2153 AlterEdgeType(AlterEdgeType),
2154 DropLabel(DropLabel),
2155 DropEdgeType(DropEdgeType),
2156 CreateConstraint(CreateConstraint),
2158 DropConstraint(DropConstraint),
2159 ShowConstraints(ShowConstraints),
2160 BindZeroLengthPath {
2163 input: Box<LogicalPlan>,
2164 node_variable: String,
2165 path_variable: String,
2166 },
2167 BindPath {
2170 input: Box<LogicalPlan>,
2171 node_variables: Vec<String>,
2172 edge_variables: Vec<String>,
2173 path_variable: String,
2174 },
2175
2176 LocyProgram {
2179 strata: Vec<super::planner_locy_types::LocyStratum>,
2180 commands: Vec<super::planner_locy_types::LocyCommand>,
2181 derived_scan_registry: Arc<super::df_graph::locy_fixpoint::DerivedScanRegistry>,
2182 max_iterations: usize,
2183 timeout: std::time::Duration,
2184 max_derived_bytes: usize,
2185 deterministic_best_by: bool,
2186 strict_probability_domain: bool,
2187 probability_epsilon: f64,
2188 exact_probability: bool,
2189 max_bdd_variables: usize,
2190 top_k_proofs: usize,
2191 semiring_kind: uni_locy::SemiringKind,
2196 classifier_registry: Arc<uni_locy::ClassifierRegistry>,
2199 classifier_cache: Option<Arc<uni_locy::ModelInvocationCache>>,
2203 classifier_provenance_store: Option<Arc<uni_locy::NeuralProvenanceStore>>,
2208 },
2209 LocyFold {
2211 input: Box<LogicalPlan>,
2212 key_columns: Vec<String>,
2213 fold_bindings: Vec<(String, Expr)>,
2214 strict_probability_domain: bool,
2215 probability_epsilon: f64,
2216 },
2217 LocyBestBy {
2219 input: Box<LogicalPlan>,
2220 key_columns: Vec<String>,
2221 criteria: Vec<(Expr, bool)>,
2223 },
2224 LocyPriority {
2226 input: Box<LogicalPlan>,
2227 key_columns: Vec<String>,
2228 },
2229 LocyDerivedScan {
2231 scan_index: usize,
2232 data: Arc<RwLock<Vec<RecordBatch>>>,
2233 schema: SchemaRef,
2234 },
2235 LocyProject {
2238 input: Box<LogicalPlan>,
2239 projections: Vec<(Expr, Option<String>)>,
2240 target_types: Vec<DataType>,
2242 },
2243 LocyModelInvoke {
2256 input: Box<LogicalPlan>,
2257 invocations: Vec<uni_locy::ModelInvocation>,
2258 classifier_registry: Arc<uni_locy::ClassifierRegistry>,
2259 classifier_cache: Option<Arc<uni_locy::ModelInvocationCache>>,
2260 classifier_provenance_store: Option<Arc<uni_locy::NeuralProvenanceStore>>,
2267 path_context_handles: std::collections::HashMap<
2275 String,
2276 super::df_graph::locy_model_invoke::PathContextHandle,
2277 >,
2278 },
2279}
2280
2281struct VectorSimilarityPredicate {
2283 variable: String,
2284 property: String,
2285 query: Expr,
2286 threshold: Option<f32>,
2287}
2288
2289struct VectorSimilarityExtraction {
2291 predicate: VectorSimilarityPredicate,
2293 residual: Option<Expr>,
2295}
2296
2297fn extract_vector_similarity(expr: &Expr) -> Option<VectorSimilarityExtraction> {
2304 match expr {
2305 Expr::BinaryOp { left, op, right } => {
2306 if matches!(op, BinaryOp::And) {
2308 if let Some(vs) = extract_simple_vector_similarity(left) {
2310 return Some(VectorSimilarityExtraction {
2311 predicate: vs,
2312 residual: Some(right.as_ref().clone()),
2313 });
2314 }
2315 if let Some(vs) = extract_simple_vector_similarity(right) {
2317 return Some(VectorSimilarityExtraction {
2318 predicate: vs,
2319 residual: Some(left.as_ref().clone()),
2320 });
2321 }
2322 if let Some(mut extraction) = extract_vector_similarity(left) {
2324 extraction.residual = Some(combine_with_and(
2325 extraction.residual,
2326 right.as_ref().clone(),
2327 ));
2328 return Some(extraction);
2329 }
2330 if let Some(mut extraction) = extract_vector_similarity(right) {
2331 extraction.residual =
2332 Some(combine_with_and(extraction.residual, left.as_ref().clone()));
2333 return Some(extraction);
2334 }
2335 return None;
2336 }
2337
2338 if let Some(vs) = extract_simple_vector_similarity(expr) {
2340 return Some(VectorSimilarityExtraction {
2341 predicate: vs,
2342 residual: None,
2343 });
2344 }
2345 None
2346 }
2347 _ => None,
2348 }
2349}
2350
2351fn combine_with_and(opt_expr: Option<Expr>, other: Expr) -> Expr {
2353 match opt_expr {
2354 Some(e) => Expr::BinaryOp {
2355 left: Box::new(e),
2356 op: BinaryOp::And,
2357 right: Box::new(other),
2358 },
2359 None => other,
2360 }
2361}
2362
2363fn extract_simple_vector_similarity(expr: &Expr) -> Option<VectorSimilarityPredicate> {
2365 match expr {
2366 Expr::BinaryOp { left, op, right } => {
2367 if matches!(op, BinaryOp::Gt | BinaryOp::GtEq)
2369 && let (Some(vs), Some(thresh)) = (
2370 extract_vector_similarity_call(left),
2371 extract_float_literal(right),
2372 )
2373 {
2374 return Some(VectorSimilarityPredicate {
2375 variable: vs.0,
2376 property: vs.1,
2377 query: vs.2,
2378 threshold: Some(thresh),
2379 });
2380 }
2381 if matches!(op, BinaryOp::Lt | BinaryOp::LtEq)
2383 && let (Some(thresh), Some(vs)) = (
2384 extract_float_literal(left),
2385 extract_vector_similarity_call(right),
2386 )
2387 {
2388 return Some(VectorSimilarityPredicate {
2389 variable: vs.0,
2390 property: vs.1,
2391 query: vs.2,
2392 threshold: Some(thresh),
2393 });
2394 }
2395 if matches!(op, BinaryOp::ApproxEq)
2397 && let Expr::Property(var_expr, prop) = left.as_ref()
2398 && let Expr::Variable(var) = var_expr.as_ref()
2399 {
2400 return Some(VectorSimilarityPredicate {
2401 variable: var.clone(),
2402 property: prop.clone(),
2403 query: right.as_ref().clone(),
2404 threshold: None,
2405 });
2406 }
2407 None
2408 }
2409 _ => None,
2410 }
2411}
2412
2413fn extract_vector_similarity_call(expr: &Expr) -> Option<(String, String, Expr)> {
2415 if let Expr::FunctionCall { name, args, .. } = expr
2416 && name.eq_ignore_ascii_case("vector_similarity")
2417 && args.len() == 2
2418 {
2419 if let Expr::Property(var_expr, prop) = &args[0]
2421 && let Expr::Variable(var) = var_expr.as_ref()
2422 {
2423 return Some((var.clone(), prop.clone(), args[1].clone()));
2425 }
2426 }
2427 None
2428}
2429
2430fn extract_float_literal(expr: &Expr) -> Option<f32> {
2432 match expr {
2433 Expr::Literal(CypherLiteral::Integer(i)) => Some(*i as f32),
2434 Expr::Literal(CypherLiteral::Float(f)) => Some(*f as f32),
2435 _ => None,
2436 }
2437}
2438
2439#[derive(Debug)]
2445pub struct QueryPlanner {
2446 schema: Arc<Schema>,
2447 gen_expr_cache: HashMap<(String, String), Expr>,
2449 anon_counter: std::sync::atomic::AtomicUsize,
2451 params: HashMap<String, uni_common::Value>,
2453 plugin_registry: Option<Arc<uni_plugin::PluginRegistry>>,
2456 replacement_scans_enabled: bool,
2458 folded_limit_skip_params: std::sync::Mutex<std::collections::BTreeSet<String>>,
2464}
2465
2466struct TraverseParams<'a> {
2467 rel: &'a RelationshipPattern,
2468 target_node: &'a NodePattern,
2469 optional: bool,
2470 path_variable: Option<String>,
2471 optional_pattern_vars: HashSet<String>,
2474}
2475
2476impl QueryPlanner {
2477 pub fn new(schema: Arc<Schema>) -> Self {
2482 let mut gen_expr_cache = HashMap::new();
2484 for (label, props) in &schema.properties {
2485 for (gen_col, meta) in props {
2486 if let Some(expr_str) = &meta.generation_expression
2487 && let Ok(parsed_expr) = uni_cypher::parse_expression(expr_str)
2488 {
2489 gen_expr_cache.insert((label.clone(), gen_col.clone()), parsed_expr);
2490 }
2491 }
2492 }
2493 Self {
2494 schema,
2495 gen_expr_cache,
2496 anon_counter: std::sync::atomic::AtomicUsize::new(0),
2497 params: HashMap::new(),
2498 plugin_registry: None,
2499 replacement_scans_enabled: false,
2500 folded_limit_skip_params: std::sync::Mutex::new(std::collections::BTreeSet::new()),
2501 }
2502 }
2503
2504 pub(crate) fn schema(&self) -> &Schema {
2506 &self.schema
2507 }
2508
2509 fn note_folded_limit_skip(&self, expr: &Expr) {
2512 let mut names = Vec::new();
2513 collect_expr_parameters(expr, &mut names);
2514 if !names.is_empty()
2515 && let Ok(mut acc) = self.folded_limit_skip_params.lock()
2516 {
2517 acc.extend(names);
2518 }
2519 }
2520
2521 #[must_use]
2529 pub fn folded_limit_skip_params(&self) -> Vec<String> {
2530 self.folded_limit_skip_params
2531 .lock()
2532 .map(|acc| acc.iter().cloned().collect())
2533 .unwrap_or_default()
2534 }
2535
2536 pub fn with_params(mut self, params: HashMap<String, uni_common::Value>) -> Self {
2538 self.params = params;
2539 self
2540 }
2541
2542 #[must_use]
2547 pub fn with_plugin_registry(mut self, registry: Arc<uni_plugin::PluginRegistry>) -> Self {
2548 self.plugin_registry = Some(registry);
2549 self
2550 }
2551
2552 #[must_use]
2555 pub fn with_replacement_scans(mut self, enabled: bool) -> Self {
2556 self.replacement_scans_enabled = enabled;
2557 self
2558 }
2559
2560 fn allocate_virtual_label(
2573 &self,
2574 name: &str,
2575 ) -> Result<Option<(u16, Arc<dyn uni_plugin::traits::catalog::CatalogTable>)>> {
2576 let Some(registry) = self.plugin_registry.as_ref() else {
2577 return Ok(None);
2578 };
2579 let mut claimed: Option<Arc<dyn uni_plugin::traits::catalog::CatalogTable>> = None;
2581 for cat in registry.catalogs() {
2582 if let Some(t) = cat.resolve_label(name) {
2583 claimed = Some(t);
2584 break;
2585 }
2586 }
2587 if claimed.is_none() {
2590 use uni_plugin::traits::catalog::{Replacement, ReplacementRequest};
2591 if let Some(Replacement::CatalogTable(t)) =
2592 self.consult_replacement_scan(ReplacementRequest::Label(name))
2593 {
2594 claimed = Some(t);
2595 }
2596 }
2597 let Some(table) = claimed else {
2598 return Ok(None);
2599 };
2600 let id = registry
2601 .register_virtual_label(name, Arc::clone(&table))
2602 .map_err(|e| anyhow!("virtual label registration failed for `{name}`: {e}"))?;
2603 Ok(Some((id, table)))
2604 }
2605
2606 fn reject_virtual_label_writes(&self, labels: &[String], op: &str) -> Result<()> {
2614 let Some(registry) = self.plugin_registry.as_ref() else {
2615 return Ok(());
2616 };
2617 for label in labels {
2618 if registry.virtual_label_by_name(label).is_some() {
2619 return Err(anyhow!(
2620 "Cannot {op} on virtual (catalog-resolved) label `{label}` — virtual \
2621 labels are read-only; write back via the originating catalog \
2622 instead"
2623 ));
2624 }
2625 }
2626 Ok(())
2627 }
2628
2629 fn allocate_virtual_edge_type(
2631 &self,
2632 name: &str,
2633 ) -> Result<Option<(u32, Arc<dyn uni_plugin::traits::catalog::CatalogTable>)>> {
2634 let Some(registry) = self.plugin_registry.as_ref() else {
2635 return Ok(None);
2636 };
2637 let mut claimed: Option<Arc<dyn uni_plugin::traits::catalog::CatalogTable>> = None;
2638 for cat in registry.catalogs() {
2639 if let Some(t) = cat.resolve_edge_type(name) {
2640 claimed = Some(t);
2641 break;
2642 }
2643 }
2644 let Some(table) = claimed else {
2645 return Ok(None);
2646 };
2647 let id = registry
2648 .register_virtual_edge_type(name, Arc::clone(&table))
2649 .map_err(|e| anyhow!("virtual edge-type registration failed for `{name}`: {e}"))?;
2650 Ok(Some((id, table)))
2651 }
2652
2653 pub(crate) fn consult_replacement_scan(
2659 &self,
2660 request: uni_plugin::traits::catalog::ReplacementRequest<'_>,
2661 ) -> Option<uni_plugin::traits::catalog::Replacement> {
2662 if !self.replacement_scans_enabled {
2663 return None;
2664 }
2665 let registry = self.plugin_registry.as_ref()?;
2666 for r in registry.replacement_scans().iter() {
2667 if let Some(replacement) = r.replace(&request) {
2668 tracing::debug!(
2669 target: "uni.plugin.registry",
2670 ?request,
2671 ?replacement,
2672 "identifier resolved via ReplacementScanProvider"
2673 );
2674 return Some(replacement);
2675 }
2676 }
2677 None
2678 }
2679
2680 fn procedure_resolves(&self, user_name: &str) -> bool {
2687 let Some(registry) = self.plugin_registry.as_ref() else {
2688 return false;
2689 };
2690 if uni_plugin::QName::candidate_splits(user_name).any(|q| registry.procedure(&q).is_some())
2694 {
2695 return true;
2696 }
2697 let stripped = user_name.strip_prefix("uni.").unwrap_or(user_name);
2698 for plugin_id in ["uni", "builtin", "apoc-core", "custom"] {
2699 if registry
2700 .procedure(&uni_plugin::QName::new(plugin_id, stripped))
2701 .is_some()
2702 {
2703 return true;
2704 }
2705 }
2706 false
2707 }
2708
2709 fn qname_from_user(name: &str) -> uni_plugin::QName {
2717 uni_plugin::QName::parse(name).unwrap_or_else(|_| uni_plugin::QName::new("user", name))
2718 }
2719
2720 fn rewrite_function_calls_in_query(
2731 &self,
2732 query: uni_cypher::ast::Query,
2733 ) -> Result<uni_cypher::ast::Query> {
2734 if !self.replacement_scans_enabled || self.plugin_registry.is_none() {
2735 return Ok(query);
2736 }
2737 let mut rename = |name: &str| -> Result<Option<String>> {
2738 let qname = Self::qname_from_user(name);
2739 use uni_plugin::traits::catalog::{Replacement, ReplacementRequest};
2740 match self.consult_replacement_scan(ReplacementRequest::Function(&qname)) {
2741 Some(Replacement::Function(new_qname)) => {
2742 let rewritten = match new_qname.namespace() {
2752 "builtin" | "user" => new_qname.local().to_string(),
2753 _ => new_qname.to_string(),
2754 };
2755 tracing::debug!(
2756 target: "uni.plugin.registry",
2757 from = %name,
2758 to = %rewritten,
2759 "function call rerouted via ReplacementScanProvider"
2760 );
2761 Ok(Some(rewritten))
2762 }
2763 Some(other) => Err(anyhow!(
2764 "ReplacementScanProvider returned wrong variant for Function \
2765 request `{}`: expected `Function`, got {:?}",
2766 name,
2767 other
2768 )),
2769 None => Ok(None),
2770 }
2771 };
2772 crate::query::rewrite::function_rename::rewrite_function_calls_in_query(query, &mut rename)
2773 }
2774
2775 pub fn plan(&self, query: Query) -> Result<LogicalPlan> {
2777 self.plan_with_scope(query, Vec::new())
2778 }
2779
2780 pub fn plan_with_scope(&self, query: Query, vars: Vec<String>) -> Result<LogicalPlan> {
2785 let rewritten_query = crate::query::rewrite::rewrite_query(query)?;
2787 let rewritten_query = self.rewrite_function_calls_in_query(rewritten_query)?;
2795 if Self::has_mixed_union_modes(&rewritten_query) {
2796 return Err(anyhow!(
2797 "SyntaxError: InvalidClauseComposition - Cannot mix UNION and UNION ALL in the same query"
2798 ));
2799 }
2800
2801 match rewritten_query {
2802 Query::Single(stmt) => self.plan_single(stmt, vars),
2803 Query::Union { left, right, all } => {
2804 let l = self.plan_with_scope(*left, vars.clone())?;
2805 let r = self.plan_with_scope(*right, vars)?;
2806
2807 let left_cols = Self::extract_projection_columns(&l);
2809 let right_cols = Self::extract_projection_columns(&r);
2810
2811 if left_cols != right_cols {
2812 return Err(anyhow!(
2813 "SyntaxError: DifferentColumnsInUnion - UNION queries must have same column names"
2814 ));
2815 }
2816
2817 Ok(LogicalPlan::Union {
2818 left: Box::new(l),
2819 right: Box::new(r),
2820 all,
2821 })
2822 }
2823 Query::Schema(cmd) => self.plan_schema_command(*cmd),
2824 Query::Explain(inner) => {
2825 let inner_plan = self.plan_with_scope(*inner, vars)?;
2826 Ok(LogicalPlan::Explain {
2827 plan: Box::new(inner_plan),
2828 })
2829 }
2830 Query::TimeTravel { .. } => {
2831 unreachable!("TimeTravel should be resolved at API layer before planning")
2832 }
2833 }
2834 }
2835
2836 fn collect_union_modes(query: &Query, out: &mut HashSet<bool>) {
2837 match query {
2838 Query::Union { left, right, all } => {
2839 out.insert(*all);
2840 Self::collect_union_modes(left, out);
2841 Self::collect_union_modes(right, out);
2842 }
2843 Query::Explain(inner) => Self::collect_union_modes(inner, out),
2844 Query::TimeTravel { query, .. } => Self::collect_union_modes(query, out),
2845 Query::Single(_) | Query::Schema(_) => {}
2846 }
2847 }
2848
2849 fn has_mixed_union_modes(query: &Query) -> bool {
2850 let mut modes = HashSet::new();
2851 Self::collect_union_modes(query, &mut modes);
2852 modes.len() > 1
2853 }
2854
2855 fn next_anon_var(&self) -> String {
2856 let id = self
2857 .anon_counter
2858 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2859 format!("_anon_{}", id)
2860 }
2861
2862 fn extract_projection_columns(plan: &LogicalPlan) -> Vec<String> {
2865 match plan {
2866 LogicalPlan::Project { projections, .. } => projections
2867 .iter()
2868 .map(|(expr, alias)| alias.clone().unwrap_or_else(|| expr.to_string_repr()))
2869 .collect(),
2870 LogicalPlan::Limit { input, .. }
2871 | LogicalPlan::Sort { input, .. }
2872 | LogicalPlan::Distinct { input, .. }
2873 | LogicalPlan::Filter { input, .. } => Self::extract_projection_columns(input),
2874 LogicalPlan::Union { left, right, .. } => {
2875 let left_cols = Self::extract_projection_columns(left);
2876 if left_cols.is_empty() {
2877 Self::extract_projection_columns(right)
2878 } else {
2879 left_cols
2880 }
2881 }
2882 LogicalPlan::Aggregate {
2883 group_by,
2884 aggregates,
2885 ..
2886 } => {
2887 let mut cols: Vec<String> = group_by.iter().map(|e| e.to_string_repr()).collect();
2888 cols.extend(aggregates.iter().map(|e| e.to_string_repr()));
2889 cols
2890 }
2891 _ => Vec::new(),
2892 }
2893 }
2894
2895 fn plan_return_clause(
2896 &self,
2897 return_clause: &ReturnClause,
2898 plan: LogicalPlan,
2899 vars_in_scope: &[VariableInfo],
2900 ) -> Result<LogicalPlan> {
2901 let mut plan = plan;
2902 let mut group_by = Vec::new();
2903 let mut aggregates = Vec::new();
2904 let mut compound_agg_exprs: Vec<Expr> = Vec::new();
2905 let mut has_agg = false;
2906 let mut projections = Vec::new();
2907 let mut projected_aggregate_reprs: HashSet<String> = HashSet::new();
2908 let mut projected_simple_reprs: HashSet<String> = HashSet::new();
2909 let mut projected_aliases: HashSet<String> = HashSet::new();
2910
2911 for item in &return_clause.items {
2912 match item {
2913 ReturnItem::All => {
2914 let user_vars: Vec<_> = vars_in_scope
2917 .iter()
2918 .filter(|v| !v.name.starts_with("_anon_"))
2919 .collect();
2920 if user_vars.is_empty() {
2921 return Err(anyhow!(
2922 "SyntaxError: NoVariablesInScope - RETURN * is not allowed when there are no variables in scope"
2923 ));
2924 }
2925 for v in user_vars {
2926 projections.push((Expr::Variable(v.name.clone()), Some(v.name.clone())));
2927 if !group_by.contains(&Expr::Variable(v.name.clone())) {
2928 group_by.push(Expr::Variable(v.name.clone()));
2929 }
2930 projected_aliases.insert(v.name.clone());
2931 projected_simple_reprs.insert(v.name.clone());
2932 }
2933 }
2934 ReturnItem::Expr {
2935 expr,
2936 alias,
2937 source_text,
2938 } => {
2939 if matches!(expr, Expr::Wildcard) {
2940 for v in vars_in_scope {
2941 projections
2942 .push((Expr::Variable(v.name.clone()), Some(v.name.clone())));
2943 if !group_by.contains(&Expr::Variable(v.name.clone())) {
2944 group_by.push(Expr::Variable(v.name.clone()));
2945 }
2946 projected_aliases.insert(v.name.clone());
2947 projected_simple_reprs.insert(v.name.clone());
2948 }
2949 } else {
2950 validate_expression_variables(expr, vars_in_scope)?;
2952 validate_expression(expr, vars_in_scope)?;
2954 if contains_pattern_predicate(expr) {
2956 return Err(anyhow!(
2957 "SyntaxError: UnexpectedSyntax - Pattern predicates are not allowed in RETURN"
2958 ));
2959 }
2960
2961 let effective_alias = alias.clone().or_else(|| source_text.clone());
2963 projections.push((expr.clone(), effective_alias));
2964 if expr.is_aggregate() && !is_compound_aggregate(expr) {
2965 has_agg = true;
2967 aggregates.push(expr.clone());
2968 projected_aggregate_reprs.insert(expr.to_string_repr());
2969 } else if !is_window_function(expr)
2970 && (expr.is_aggregate() || contains_aggregate_recursive(expr))
2971 {
2972 has_agg = true;
2975 compound_agg_exprs.push(expr.clone());
2976 for inner in extract_inner_aggregates(expr) {
2977 let repr = inner.to_string_repr();
2978 if !projected_aggregate_reprs.contains(&repr) {
2979 aggregates.push(inner);
2980 projected_aggregate_reprs.insert(repr);
2981 }
2982 }
2983 } else if !group_by.contains(expr) {
2984 group_by.push(expr.clone());
2985 if matches!(expr, Expr::Variable(_) | Expr::Property(_, _)) {
2986 projected_simple_reprs.insert(expr.to_string_repr());
2987 }
2988 }
2989
2990 if let Some(a) = alias {
2991 if projected_aliases.contains(a) {
2992 return Err(anyhow!(
2993 "SyntaxError: ColumnNameConflict - Duplicate column name '{}' in RETURN",
2994 a
2995 ));
2996 }
2997 projected_aliases.insert(a.clone());
2998 } else if let Expr::Variable(v) = expr {
2999 if projected_aliases.contains(v) {
3000 return Err(anyhow!(
3001 "SyntaxError: ColumnNameConflict - Duplicate column name '{}' in RETURN",
3002 v
3003 ));
3004 }
3005 projected_aliases.insert(v.clone());
3006 }
3007 }
3008 }
3009 }
3010 }
3011
3012 if has_agg {
3015 let group_by_reprs: HashSet<String> =
3016 group_by.iter().map(|e| e.to_string_repr()).collect();
3017 for expr in &compound_agg_exprs {
3018 let mut refs = Vec::new();
3019 collect_non_aggregate_refs(expr, false, &mut refs);
3020 for r in &refs {
3021 let is_covered = match r {
3022 NonAggregateRef::Var(v) => group_by_reprs.contains(v),
3023 NonAggregateRef::Property { repr, .. } => group_by_reprs.contains(repr),
3024 };
3025 if !is_covered {
3026 return Err(anyhow!(
3027 "SyntaxError: AmbiguousAggregationExpression - Expression mixes aggregation with non-grouped reference"
3028 ));
3029 }
3030 }
3031 }
3032 }
3033
3034 if has_agg {
3035 plan = LogicalPlan::Aggregate {
3036 input: Box::new(plan),
3037 group_by,
3038 aggregates,
3039 };
3040 }
3041
3042 let mut window_exprs = Vec::new();
3043 for (expr, _) in &projections {
3044 Self::collect_window_functions(expr, &mut window_exprs);
3045 }
3046
3047 if let Some(order_by) = &return_clause.order_by {
3048 for item in order_by {
3049 Self::collect_window_functions(&item.expr, &mut window_exprs);
3050 }
3051 }
3052
3053 let has_window_exprs = !window_exprs.is_empty();
3054
3055 if has_window_exprs {
3056 let mut props_needed_for_window: Vec<Expr> = Vec::new();
3060 for window_expr in &window_exprs {
3061 Self::collect_properties_from_expr(window_expr, &mut props_needed_for_window);
3062 }
3063
3064 let non_window_projections: Vec<_> = projections
3067 .iter()
3068 .filter_map(|(expr, alias)| {
3069 let keep = if let Expr::FunctionCall { window_spec, .. } = expr {
3071 window_spec.is_none()
3072 } else {
3073 true
3074 };
3075
3076 if keep {
3077 let new_alias = if matches!(expr, Expr::Property(..)) {
3079 Some(expr.to_string_repr())
3080 } else {
3081 alias.clone()
3082 };
3083 Some((expr.clone(), new_alias))
3084 } else {
3085 None
3086 }
3087 })
3088 .collect();
3089
3090 if !non_window_projections.is_empty() || !props_needed_for_window.is_empty() {
3091 let mut intermediate_projections = non_window_projections;
3092 for prop in &props_needed_for_window {
3095 if !intermediate_projections
3096 .iter()
3097 .any(|(e, _)| e.to_string_repr() == prop.to_string_repr())
3098 {
3099 let qualified_name = prop.to_string_repr();
3100 intermediate_projections.push((prop.clone(), Some(qualified_name)));
3101 }
3102 }
3103
3104 if !intermediate_projections.is_empty() {
3105 plan = LogicalPlan::Project {
3106 input: Box::new(plan),
3107 projections: intermediate_projections,
3108 };
3109 }
3110 }
3111
3112 let transformed_window_exprs: Vec<Expr> = window_exprs
3115 .into_iter()
3116 .map(Self::transform_window_expr_properties)
3117 .collect();
3118
3119 plan = LogicalPlan::Window {
3120 input: Box::new(plan),
3121 window_exprs: transformed_window_exprs,
3122 };
3123 }
3124
3125 if let Some(order_by) = &return_clause.order_by {
3126 let alias_exprs: HashMap<String, Expr> = projections
3127 .iter()
3128 .filter_map(|(expr, alias)| {
3129 alias.as_ref().map(|a| {
3130 let rewritten = if has_agg && !has_window_exprs {
3134 if expr.is_aggregate() && !is_compound_aggregate(expr) {
3135 Expr::Variable(aggregate_column_name(expr))
3136 } else if is_compound_aggregate(expr)
3137 || (!expr.is_aggregate() && contains_aggregate_recursive(expr))
3138 {
3139 replace_aggregates_with_columns(expr)
3140 } else {
3141 Expr::Variable(expr.to_string_repr())
3142 }
3143 } else {
3144 expr.clone()
3145 };
3146 (a.clone(), rewritten)
3147 })
3148 })
3149 .collect();
3150
3151 let order_by_scope: Vec<VariableInfo> = if return_clause.distinct {
3154 let mut scope = Vec::new();
3157 for (expr, alias) in &projections {
3158 if let Some(a) = alias
3159 && !is_var_in_scope(&scope, a)
3160 {
3161 scope.push(VariableInfo::new(a.clone(), VariableType::Scalar));
3162 }
3163 if let Expr::Variable(v) = expr
3164 && !is_var_in_scope(&scope, v)
3165 {
3166 scope.push(VariableInfo::new(v.clone(), VariableType::Scalar));
3167 }
3168 }
3169 scope
3170 } else {
3171 let mut scope = vars_in_scope.to_vec();
3172 for (expr, alias) in &projections {
3173 if let Some(a) = alias
3174 && !is_var_in_scope(&scope, a)
3175 {
3176 scope.push(VariableInfo::new(a.clone(), VariableType::Scalar));
3177 } else if let Expr::Variable(v) = expr
3178 && !is_var_in_scope(&scope, v)
3179 {
3180 scope.push(VariableInfo::new(v.clone(), VariableType::Scalar));
3181 }
3182 }
3183 scope
3184 };
3185 for item in order_by {
3187 let matches_projected_expr = return_clause.distinct
3190 && projections
3191 .iter()
3192 .any(|(expr, _)| expr.to_string_repr() == item.expr.to_string_repr());
3193 if !matches_projected_expr {
3194 validate_expression_variables(&item.expr, &order_by_scope)?;
3195 validate_expression(&item.expr, &order_by_scope)?;
3196 }
3197 let has_aggregate_in_item = contains_aggregate_recursive(&item.expr);
3198 if has_aggregate_in_item && !has_agg {
3199 return Err(anyhow!(
3200 "SyntaxError: InvalidAggregation - Aggregation functions not allowed in ORDER BY after RETURN"
3201 ));
3202 }
3203 if has_agg && has_aggregate_in_item {
3204 validate_with_order_by_aggregate_item(
3205 &item.expr,
3206 &projected_aggregate_reprs,
3207 &projected_simple_reprs,
3208 &projected_aliases,
3209 )?;
3210 }
3211 }
3212 let rewritten_order_by: Vec<SortItem> = order_by
3213 .iter()
3214 .map(|item| SortItem {
3215 expr: {
3216 let mut rewritten =
3217 rewrite_order_by_expr_with_aliases(&item.expr, &alias_exprs);
3218 if has_agg && !has_window_exprs {
3219 rewritten = replace_aggregates_with_columns(&rewritten);
3220 }
3221 rewritten
3222 },
3223 ascending: item.ascending,
3224 })
3225 .collect();
3226 plan = LogicalPlan::Sort {
3227 input: Box::new(plan),
3228 order_by: rewritten_order_by,
3229 };
3230 }
3231
3232 if return_clause.skip.is_some() || return_clause.limit.is_some() {
3233 let skip = return_clause
3234 .skip
3235 .as_ref()
3236 .map(|e| {
3237 self.note_folded_limit_skip(e);
3238 parse_non_negative_integer(e, "SKIP", &self.params)
3239 })
3240 .transpose()?
3241 .flatten();
3242 let fetch = return_clause
3243 .limit
3244 .as_ref()
3245 .map(|e| {
3246 self.note_folded_limit_skip(e);
3247 parse_non_negative_integer(e, "LIMIT", &self.params)
3248 })
3249 .transpose()?
3250 .flatten();
3251
3252 plan = LogicalPlan::Limit {
3253 input: Box::new(plan),
3254 skip,
3255 fetch,
3256 };
3257 }
3258
3259 if !projections.is_empty() {
3260 let final_projections = if has_agg || has_window_exprs {
3263 projections
3264 .into_iter()
3265 .map(|(expr, alias)| {
3266 if expr.is_aggregate() && !is_compound_aggregate(&expr) && !has_window_exprs
3268 {
3269 let col_name = aggregate_column_name(&expr);
3271 (Expr::Variable(col_name), alias)
3272 } else if !has_window_exprs
3273 && (is_compound_aggregate(&expr)
3274 || (!expr.is_aggregate() && contains_aggregate_recursive(&expr)))
3275 {
3276 (replace_aggregates_with_columns(&expr), alias)
3279 }
3280 else if has_agg
3284 && !has_window_exprs
3285 && !matches!(expr, Expr::Variable(_) | Expr::Property(_, _))
3286 {
3287 (Expr::Variable(expr.to_string_repr()), alias)
3288 }
3289 else if let Expr::FunctionCall {
3291 window_spec: Some(_),
3292 ..
3293 } = &expr
3294 {
3295 let window_col_name = expr.to_string_repr();
3298 (Expr::Variable(window_col_name), alias)
3300 } else {
3301 (expr, alias)
3302 }
3303 })
3304 .collect()
3305 } else {
3306 projections
3307 };
3308
3309 plan = LogicalPlan::Project {
3310 input: Box::new(plan),
3311 projections: final_projections,
3312 };
3313 }
3314
3315 if return_clause.distinct {
3316 plan = LogicalPlan::Distinct {
3317 input: Box::new(plan),
3318 };
3319 }
3320
3321 Ok(plan)
3322 }
3323
3324 fn plan_single(&self, query: Statement, initial_vars: Vec<String>) -> Result<LogicalPlan> {
3325 let typed_vars: Vec<VariableInfo> = initial_vars
3326 .into_iter()
3327 .map(|name| VariableInfo::new(name, VariableType::Imported))
3328 .collect();
3329 self.plan_single_typed(query, typed_vars)
3330 }
3331
3332 fn rewrite_and_plan_typed(
3338 &self,
3339 query: Query,
3340 typed_vars: &[VariableInfo],
3341 ) -> Result<LogicalPlan> {
3342 let rewritten = crate::query::rewrite::rewrite_query(query)?;
3343 match rewritten {
3344 Query::Single(stmt) => self.plan_single_typed(stmt, typed_vars.to_vec()),
3345 other => self.plan_with_scope(other, vars_to_strings(typed_vars)),
3346 }
3347 }
3348
3349 fn plan_single_typed(
3350 &self,
3351 query: Statement,
3352 initial_vars: Vec<VariableInfo>,
3353 ) -> Result<LogicalPlan> {
3354 let mut plan = LogicalPlan::Empty;
3355
3356 if !initial_vars.is_empty() {
3357 let projections = initial_vars
3361 .iter()
3362 .map(|v| (Expr::Parameter(v.name.clone()), Some(v.name.clone())))
3363 .collect();
3364 plan = LogicalPlan::Project {
3365 input: Box::new(plan),
3366 projections,
3367 };
3368 }
3369
3370 let mut vars_in_scope: Vec<VariableInfo> = initial_vars;
3371 let mut create_introduced_vars: HashSet<String> = HashSet::new();
3375 let mut deleted_vars: HashSet<String> = HashSet::new();
3378
3379 let clause_count = query.clauses.len();
3380 for (clause_idx, clause) in query.clauses.into_iter().enumerate() {
3381 match clause {
3382 Clause::Match(match_clause) => {
3383 plan = self.plan_match_clause(&match_clause, plan, &mut vars_in_scope)?;
3384 }
3385 Clause::Unwind(unwind) => {
3386 plan = LogicalPlan::Unwind {
3387 input: Box::new(plan),
3388 expr: unwind.expr.clone(),
3389 variable: unwind.variable.clone(),
3390 };
3391 let unwind_out_type = infer_unwind_output_type(&unwind.expr, &vars_in_scope);
3392 add_var_to_scope(&mut vars_in_scope, &unwind.variable, unwind_out_type)?;
3393 }
3394 Clause::Call(call_clause) => {
3395 match &call_clause.kind {
3396 CallKind::Procedure {
3397 procedure,
3398 arguments,
3399 } => {
3400 for arg in arguments {
3402 if contains_aggregate_recursive(arg) {
3403 return Err(anyhow!(
3404 "SyntaxError: InvalidAggregation - Aggregation expressions are not allowed as arguments to procedure calls"
3405 ));
3406 }
3407 }
3408
3409 let has_yield_star = call_clause.yield_items.len() == 1
3410 && call_clause.yield_items[0].name == "*"
3411 && call_clause.yield_items[0].alias.is_none();
3412 if has_yield_star && clause_idx + 1 < clause_count {
3413 return Err(anyhow!(
3414 "SyntaxError: UnexpectedSyntax - YIELD * is only allowed in standalone procedure calls"
3415 ));
3416 }
3417
3418 let mut yield_names = Vec::new();
3420 for item in &call_clause.yield_items {
3421 if item.name == "*" {
3422 continue;
3423 }
3424 let output_name = item.alias.as_ref().unwrap_or(&item.name);
3425 if yield_names.contains(output_name) {
3426 return Err(anyhow!(
3427 "SyntaxError: VariableAlreadyBound - Variable '{}' already appears in YIELD clause",
3428 output_name
3429 ));
3430 }
3431 if clause_idx > 0
3433 && vars_in_scope.iter().any(|v| v.name == *output_name)
3434 {
3435 return Err(anyhow!(
3436 "SyntaxError: VariableAlreadyBound - Variable '{}' already declared in outer scope",
3437 output_name
3438 ));
3439 }
3440 yield_names.push(output_name.clone());
3441 }
3442
3443 let mut yields = Vec::new();
3444 for item in &call_clause.yield_items {
3445 if item.name == "*" {
3446 continue;
3447 }
3448 yields.push((item.name.clone(), item.alias.clone()));
3449 let var_name = item.alias.as_ref().unwrap_or(&item.name);
3450 add_var_to_scope(
3453 &mut vars_in_scope,
3454 var_name,
3455 VariableType::Imported,
3456 )?;
3457 }
3458 let procedure_name = if self.replacement_scans_enabled
3467 && !self.procedure_resolves(procedure)
3468 {
3469 use uni_plugin::traits::catalog::{
3470 Replacement, ReplacementRequest,
3471 };
3472 let qname = Self::qname_from_user(procedure);
3473 match self
3474 .consult_replacement_scan(ReplacementRequest::Procedure(&qname))
3475 {
3476 Some(Replacement::Procedure(new_qname)) => {
3477 let rewritten = new_qname.to_string();
3478 if !self.procedure_resolves(&rewritten) {
3479 return Err(anyhow!(
3480 "ReplacementScanProvider rerouted procedure \
3481 `{}` to `{}`, which also did not resolve",
3482 procedure,
3483 rewritten
3484 ));
3485 }
3486 tracing::debug!(
3487 target: "uni.plugin.registry",
3488 from = %procedure,
3489 to = %rewritten,
3490 "procedure rerouted via ReplacementScanProvider"
3491 );
3492 rewritten
3493 }
3494 Some(other) => {
3495 return Err(anyhow!(
3496 "ReplacementScanProvider returned wrong variant \
3497 for Procedure request `{}`: expected \
3498 `Procedure`, got {:?}",
3499 procedure,
3500 other
3501 ));
3502 }
3503 None => procedure.clone(),
3504 }
3505 } else {
3506 procedure.clone()
3507 };
3508 let proc_plan = LogicalPlan::ProcedureCall {
3509 procedure_name,
3510 arguments: arguments.clone(),
3511 yield_items: yields.clone(),
3512 };
3513
3514 if matches!(plan, LogicalPlan::Empty) {
3515 plan = proc_plan;
3517 } else if yields.is_empty() {
3518 } else {
3521 plan = LogicalPlan::Apply {
3523 input: Box::new(plan),
3524 subquery: Box::new(proc_plan),
3525 input_filter: None,
3526 };
3527 }
3528 }
3529 CallKind::Subquery(query) => {
3530 let subquery_plan =
3531 self.rewrite_and_plan_typed(*query.clone(), &vars_in_scope)?;
3532
3533 let subquery_vars = Self::collect_plan_variables(&subquery_plan);
3535
3536 for var in subquery_vars {
3538 if !is_var_in_scope(&vars_in_scope, &var) {
3539 add_var_to_scope(
3540 &mut vars_in_scope,
3541 &var,
3542 VariableType::Scalar,
3543 )?;
3544 }
3545 }
3546
3547 plan = LogicalPlan::SubqueryCall {
3548 input: Box::new(plan),
3549 subquery: Box::new(subquery_plan),
3550 };
3551 }
3552 }
3553 }
3554 Clause::Merge(merge_clause) => {
3555 validate_merge_clause(&merge_clause, &vars_in_scope)?;
3556 let merge_labels = collect_pattern_labels(&merge_clause.pattern);
3559 self.reject_virtual_label_writes(&merge_labels, "MERGE")?;
3560
3561 plan = LogicalPlan::Merge {
3562 input: Box::new(plan),
3563 pattern: merge_clause.pattern.clone(),
3564 on_match: Some(SetClause {
3565 items: merge_clause.on_match.clone(),
3566 }),
3567 on_create: Some(SetClause {
3568 items: merge_clause.on_create.clone(),
3569 }),
3570 };
3571
3572 for path in &merge_clause.pattern.paths {
3573 if let Some(path_var) = &path.variable
3574 && !path_var.is_empty()
3575 && !is_var_in_scope(&vars_in_scope, path_var)
3576 {
3577 add_var_to_scope(&mut vars_in_scope, path_var, VariableType::Path)?;
3578 }
3579 for element in &path.elements {
3580 if let PatternElement::Node(n) = element {
3581 if let Some(v) = &n.variable
3582 && !is_var_in_scope(&vars_in_scope, v)
3583 {
3584 add_var_to_scope(&mut vars_in_scope, v, VariableType::Node)?;
3585 }
3586 } else if let PatternElement::Relationship(r) = element
3587 && let Some(v) = &r.variable
3588 && !is_var_in_scope(&vars_in_scope, v)
3589 {
3590 add_var_to_scope(&mut vars_in_scope, v, VariableType::Edge)?;
3591 }
3592 }
3593 }
3594 }
3595 Clause::Create(create_clause) => {
3596 let create_labels = collect_pattern_labels(&create_clause.pattern);
3599 self.reject_virtual_label_writes(&create_labels, "CREATE")?;
3600 let mut create_vars: Vec<&str> = Vec::new();
3607 for path in &create_clause.pattern.paths {
3608 let is_standalone_node = path.elements.len() == 1;
3609 for element in &path.elements {
3610 match element {
3611 PatternElement::Node(n) => {
3612 validate_property_variables(
3613 &n.properties,
3614 &vars_in_scope,
3615 &create_vars,
3616 )?;
3617
3618 if let Some(v) = n.variable.as_deref()
3619 && !v.is_empty()
3620 {
3621 let is_creation =
3623 !n.labels.is_empty() || n.properties.is_some();
3624
3625 if is_creation {
3626 check_not_already_bound(
3627 v,
3628 &vars_in_scope,
3629 &create_vars,
3630 )?;
3631 create_vars.push(v);
3632 } else if is_standalone_node
3633 && is_var_in_scope(&vars_in_scope, v)
3634 && !create_introduced_vars.contains(v)
3635 {
3636 return Err(anyhow!(
3641 "SyntaxError: VariableAlreadyBound - '{}'",
3642 v
3643 ));
3644 } else if !create_vars.contains(&v) {
3645 create_vars.push(v);
3647 }
3648 }
3650 }
3651 PatternElement::Relationship(r) => {
3652 validate_property_variables(
3653 &r.properties,
3654 &vars_in_scope,
3655 &create_vars,
3656 )?;
3657
3658 if let Some(v) = r.variable.as_deref()
3659 && !v.is_empty()
3660 {
3661 check_not_already_bound(v, &vars_in_scope, &create_vars)?;
3662 create_vars.push(v);
3663 }
3664
3665 if r.types.len() != 1 {
3667 return Err(anyhow!(
3668 "SyntaxError: NoSingleRelationshipType - Exactly one relationship type required for CREATE"
3669 ));
3670 }
3671 if r.direction == Direction::Both {
3672 return Err(anyhow!(
3673 "SyntaxError: RequiresDirectedRelationship - Only directed relationships are supported in CREATE"
3674 ));
3675 }
3676 if r.range.is_some() {
3677 return Err(anyhow!(
3678 "SyntaxError: CreatingVarLength - Variable length relationships cannot be created"
3679 ));
3680 }
3681 }
3682 PatternElement::Parenthesized { .. } => {}
3683 }
3684 }
3685 }
3686
3687 match &mut plan {
3689 LogicalPlan::CreateBatch { patterns, .. } => {
3690 patterns.push(create_clause.pattern.clone());
3692 }
3693 LogicalPlan::Create { input, pattern } => {
3694 let first_pattern = pattern.clone();
3696 plan = LogicalPlan::CreateBatch {
3697 input: input.clone(),
3698 patterns: vec![first_pattern, create_clause.pattern.clone()],
3699 };
3700 }
3701 _ => {
3702 plan = LogicalPlan::Create {
3704 input: Box::new(plan),
3705 pattern: create_clause.pattern.clone(),
3706 };
3707 }
3708 }
3709 for path in &create_clause.pattern.paths {
3711 for element in &path.elements {
3712 match element {
3713 PatternElement::Node(n) => {
3714 if let Some(var) = &n.variable
3715 && !var.is_empty()
3716 {
3717 create_introduced_vars.insert(var.clone());
3718 add_var_to_scope(
3719 &mut vars_in_scope,
3720 var,
3721 VariableType::Node,
3722 )?;
3723 }
3724 }
3725 PatternElement::Relationship(r) => {
3726 if let Some(var) = &r.variable
3727 && !var.is_empty()
3728 {
3729 create_introduced_vars.insert(var.clone());
3730 add_var_to_scope(
3731 &mut vars_in_scope,
3732 var,
3733 VariableType::Edge,
3734 )?;
3735 }
3736 }
3737 PatternElement::Parenthesized { .. } => {
3738 }
3740 }
3741 }
3742 }
3743 }
3744 Clause::Set(set_clause) => {
3745 for item in &set_clause.items {
3747 match item {
3748 SetItem::Property { value, .. }
3749 | SetItem::Variable { value, .. }
3750 | SetItem::VariablePlus { value, .. } => {
3751 validate_expression_variables(value, &vars_in_scope)?;
3752 validate_expression(value, &vars_in_scope)?;
3753 if contains_pattern_predicate(value) {
3754 return Err(anyhow!(
3755 "SyntaxError: UnexpectedSyntax - Pattern predicates are not allowed in SET"
3756 ));
3757 }
3758 }
3759 SetItem::Labels { .. } => {}
3760 }
3761 }
3762 plan = LogicalPlan::Set {
3763 input: Box::new(plan),
3764 items: set_clause.items.clone(),
3765 };
3766 }
3767 Clause::Remove(remove_clause) => {
3768 plan = LogicalPlan::Remove {
3769 input: Box::new(plan),
3770 items: remove_clause.items.clone(),
3771 };
3772 }
3773 Clause::Delete(delete_clause) => {
3774 for item in &delete_clause.items {
3776 if matches!(item, Expr::LabelCheck { .. }) {
3778 return Err(anyhow!(
3779 "SyntaxError: InvalidDelete - DELETE requires a simple variable reference, not a label expression"
3780 ));
3781 }
3782 let vars_used = collect_expr_variables(item);
3783 if vars_used.is_empty() {
3785 return Err(anyhow!(
3786 "SyntaxError: InvalidArgumentType - DELETE requires node or relationship, not a literal expression"
3787 ));
3788 }
3789 for var in &vars_used {
3790 if find_var_in_scope(&vars_in_scope, var).is_none() {
3792 return Err(anyhow!(
3793 "SyntaxError: UndefinedVariable - Variable '{}' not defined",
3794 var
3795 ));
3796 }
3797 }
3798 if let Expr::Variable(name) = item
3803 && let Some(info) = find_var_in_scope(&vars_in_scope, name)
3804 && matches!(
3805 info.var_type,
3806 VariableType::Scalar | VariableType::ScalarLiteral
3807 )
3808 {
3809 return Err(anyhow!(
3810 "SyntaxError: InvalidArgumentType - DELETE requires node or relationship, '{}' is a scalar value",
3811 name
3812 ));
3813 }
3814 }
3815 for item in &delete_clause.items {
3817 if let Expr::Variable(name) = item {
3818 deleted_vars.insert(name.clone());
3819 }
3820 }
3821 plan = LogicalPlan::Delete {
3822 input: Box::new(plan),
3823 items: delete_clause.items.clone(),
3824 detach: delete_clause.detach,
3825 };
3826 }
3827 Clause::With(with_clause) => {
3828 let (new_plan, new_vars) =
3829 self.plan_with_clause(&with_clause, plan, &vars_in_scope)?;
3830 plan = new_plan;
3831 vars_in_scope = new_vars;
3832 }
3833 Clause::WithRecursive(with_recursive) => {
3834 plan = self.plan_with_recursive(&with_recursive, plan, &vars_in_scope)?;
3836 add_var_to_scope(
3838 &mut vars_in_scope,
3839 &with_recursive.name,
3840 VariableType::Scalar,
3841 )?;
3842 }
3843 Clause::Return(return_clause) => {
3844 if !deleted_vars.is_empty() {
3846 for item in &return_clause.items {
3847 if let ReturnItem::Expr { expr, .. } = item {
3848 validate_no_deleted_entity_access(expr, &deleted_vars)?;
3849 }
3850 }
3851 }
3852 plan = self.plan_return_clause(&return_clause, plan, &vars_in_scope)?;
3853 } }
3855 }
3856
3857 let plan = match &plan {
3862 LogicalPlan::Create { .. }
3863 | LogicalPlan::CreateBatch { .. }
3864 | LogicalPlan::Delete { .. }
3865 | LogicalPlan::Set { .. }
3866 | LogicalPlan::Remove { .. }
3867 | LogicalPlan::Merge { .. } => LogicalPlan::Limit {
3868 input: Box::new(plan),
3869 skip: None,
3870 fetch: Some(0),
3871 },
3872 _ => plan,
3873 };
3874
3875 Ok(plan)
3876 }
3877
3878 fn collect_properties_from_expr(expr: &Expr, collected: &mut Vec<Expr>) {
3879 match expr {
3880 Expr::Property(_, _)
3881 if !collected
3882 .iter()
3883 .any(|e| e.to_string_repr() == expr.to_string_repr()) =>
3884 {
3885 collected.push(expr.clone());
3886 }
3887 Expr::Property(_, _) => {}
3888 Expr::Variable(_) => {
3889 }
3891 Expr::BinaryOp { left, right, .. } => {
3892 Self::collect_properties_from_expr(left, collected);
3893 Self::collect_properties_from_expr(right, collected);
3894 }
3895 Expr::FunctionCall {
3896 args, window_spec, ..
3897 } => {
3898 for arg in args {
3899 Self::collect_properties_from_expr(arg, collected);
3900 }
3901 if let Some(spec) = window_spec {
3902 for partition_expr in &spec.partition_by {
3903 Self::collect_properties_from_expr(partition_expr, collected);
3904 }
3905 for sort_item in &spec.order_by {
3906 Self::collect_properties_from_expr(&sort_item.expr, collected);
3907 }
3908 }
3909 }
3910 Expr::List(items) => {
3911 for item in items {
3912 Self::collect_properties_from_expr(item, collected);
3913 }
3914 }
3915 Expr::UnaryOp { expr: e, .. }
3916 | Expr::IsNull(e)
3917 | Expr::IsNotNull(e)
3918 | Expr::IsUnique(e) => {
3919 Self::collect_properties_from_expr(e, collected);
3920 }
3921 Expr::Case {
3922 expr,
3923 when_then,
3924 else_expr,
3925 } => {
3926 if let Some(e) = expr {
3927 Self::collect_properties_from_expr(e, collected);
3928 }
3929 for (w, t) in when_then {
3930 Self::collect_properties_from_expr(w, collected);
3931 Self::collect_properties_from_expr(t, collected);
3932 }
3933 if let Some(e) = else_expr {
3934 Self::collect_properties_from_expr(e, collected);
3935 }
3936 }
3937 Expr::In { expr, list } => {
3938 Self::collect_properties_from_expr(expr, collected);
3939 Self::collect_properties_from_expr(list, collected);
3940 }
3941 Expr::ArrayIndex { array, index } => {
3942 Self::collect_properties_from_expr(array, collected);
3943 Self::collect_properties_from_expr(index, collected);
3944 }
3945 Expr::ArraySlice { array, start, end } => {
3946 Self::collect_properties_from_expr(array, collected);
3947 if let Some(s) = start {
3948 Self::collect_properties_from_expr(s, collected);
3949 }
3950 if let Some(e) = end {
3951 Self::collect_properties_from_expr(e, collected);
3952 }
3953 }
3954 _ => {}
3955 }
3956 }
3957
3958 fn collect_window_functions(expr: &Expr, collected: &mut Vec<Expr>) {
3959 if let Expr::FunctionCall { window_spec, .. } = expr {
3960 if window_spec.is_some() {
3962 if !collected
3963 .iter()
3964 .any(|e| e.to_string_repr() == expr.to_string_repr())
3965 {
3966 collected.push(expr.clone());
3967 }
3968 return;
3969 }
3970 }
3971
3972 match expr {
3973 Expr::BinaryOp { left, right, .. } => {
3974 Self::collect_window_functions(left, collected);
3975 Self::collect_window_functions(right, collected);
3976 }
3977 Expr::FunctionCall { args, .. } => {
3978 for arg in args {
3979 Self::collect_window_functions(arg, collected);
3980 }
3981 }
3982 Expr::List(items) => {
3983 for i in items {
3984 Self::collect_window_functions(i, collected);
3985 }
3986 }
3987 Expr::Map(items) => {
3988 for (_, i) in items {
3989 Self::collect_window_functions(i, collected);
3990 }
3991 }
3992 Expr::IsNull(e) | Expr::IsNotNull(e) | Expr::UnaryOp { expr: e, .. } => {
3993 Self::collect_window_functions(e, collected);
3994 }
3995 Expr::Case {
3996 expr,
3997 when_then,
3998 else_expr,
3999 } => {
4000 if let Some(e) = expr {
4001 Self::collect_window_functions(e, collected);
4002 }
4003 for (w, t) in when_then {
4004 Self::collect_window_functions(w, collected);
4005 Self::collect_window_functions(t, collected);
4006 }
4007 if let Some(e) = else_expr {
4008 Self::collect_window_functions(e, collected);
4009 }
4010 }
4011 Expr::Reduce {
4012 init, list, expr, ..
4013 } => {
4014 Self::collect_window_functions(init, collected);
4015 Self::collect_window_functions(list, collected);
4016 Self::collect_window_functions(expr, collected);
4017 }
4018 Expr::Quantifier {
4019 list, predicate, ..
4020 } => {
4021 Self::collect_window_functions(list, collected);
4022 Self::collect_window_functions(predicate, collected);
4023 }
4024 Expr::In { expr, list } => {
4025 Self::collect_window_functions(expr, collected);
4026 Self::collect_window_functions(list, collected);
4027 }
4028 Expr::ArrayIndex { array, index } => {
4029 Self::collect_window_functions(array, collected);
4030 Self::collect_window_functions(index, collected);
4031 }
4032 Expr::ArraySlice { array, start, end } => {
4033 Self::collect_window_functions(array, collected);
4034 if let Some(s) = start {
4035 Self::collect_window_functions(s, collected);
4036 }
4037 if let Some(e) = end {
4038 Self::collect_window_functions(e, collected);
4039 }
4040 }
4041 Expr::Property(e, _) => Self::collect_window_functions(e, collected),
4042 Expr::CountSubquery(_) | Expr::Exists { .. } => {}
4043 _ => {}
4044 }
4045 }
4046
4047 fn transform_window_expr_properties(expr: Expr) -> Expr {
4056 let Expr::FunctionCall {
4057 name,
4058 args,
4059 window_spec: Some(spec),
4060 distinct,
4061 } = expr
4062 else {
4063 return expr;
4064 };
4065
4066 let transformed_args = args
4069 .into_iter()
4070 .map(Self::transform_property_to_variable)
4071 .collect();
4072
4073 let transformed_partition_by = spec
4075 .partition_by
4076 .into_iter()
4077 .map(Self::transform_property_to_variable)
4078 .collect();
4079
4080 let transformed_order_by = spec
4081 .order_by
4082 .into_iter()
4083 .map(|item| SortItem {
4084 expr: Self::transform_property_to_variable(item.expr),
4085 ascending: item.ascending,
4086 })
4087 .collect();
4088
4089 Expr::FunctionCall {
4090 name,
4091 args: transformed_args,
4092 window_spec: Some(WindowSpec {
4093 partition_by: transformed_partition_by,
4094 order_by: transformed_order_by,
4095 }),
4096 distinct,
4097 }
4098 }
4099
4100 fn transform_property_to_variable(expr: Expr) -> Expr {
4104 let Expr::Property(base, prop) = expr else {
4105 return expr;
4106 };
4107
4108 match *base {
4109 Expr::Variable(var) => Expr::Variable(format!("{}.{}", var, prop)),
4110 other => Expr::Property(Box::new(Self::transform_property_to_variable(other)), prop),
4111 }
4112 }
4113
4114 fn transform_valid_at_to_function(expr: Expr) -> Expr {
4119 match expr {
4120 Expr::ValidAt {
4121 entity,
4122 timestamp,
4123 start_prop,
4124 end_prop,
4125 } => {
4126 let start = start_prop.unwrap_or_else(|| "valid_from".to_string());
4127 let end = end_prop.unwrap_or_else(|| "valid_to".to_string());
4128
4129 Expr::FunctionCall {
4130 name: "uni.temporal.validAt".to_string(),
4131 args: vec![
4132 Self::transform_valid_at_to_function(*entity),
4133 Expr::Literal(CypherLiteral::String(start)),
4134 Expr::Literal(CypherLiteral::String(end)),
4135 Self::transform_valid_at_to_function(*timestamp),
4136 ],
4137 distinct: false,
4138 window_spec: None,
4139 }
4140 }
4141 Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
4143 left: Box::new(Self::transform_valid_at_to_function(*left)),
4144 op,
4145 right: Box::new(Self::transform_valid_at_to_function(*right)),
4146 },
4147 Expr::UnaryOp { op, expr } => Expr::UnaryOp {
4148 op,
4149 expr: Box::new(Self::transform_valid_at_to_function(*expr)),
4150 },
4151 Expr::FunctionCall {
4152 name,
4153 args,
4154 distinct,
4155 window_spec,
4156 } => Expr::FunctionCall {
4157 name,
4158 args: args
4159 .into_iter()
4160 .map(Self::transform_valid_at_to_function)
4161 .collect(),
4162 distinct,
4163 window_spec,
4164 },
4165 Expr::Property(base, prop) => {
4166 Expr::Property(Box::new(Self::transform_valid_at_to_function(*base)), prop)
4167 }
4168 Expr::List(items) => Expr::List(
4169 items
4170 .into_iter()
4171 .map(Self::transform_valid_at_to_function)
4172 .collect(),
4173 ),
4174 Expr::In { expr, list } => Expr::In {
4175 expr: Box::new(Self::transform_valid_at_to_function(*expr)),
4176 list: Box::new(Self::transform_valid_at_to_function(*list)),
4177 },
4178 Expr::IsNull(e) => Expr::IsNull(Box::new(Self::transform_valid_at_to_function(*e))),
4179 Expr::IsNotNull(e) => {
4180 Expr::IsNotNull(Box::new(Self::transform_valid_at_to_function(*e)))
4181 }
4182 Expr::IsUnique(e) => Expr::IsUnique(Box::new(Self::transform_valid_at_to_function(*e))),
4183 other => other,
4185 }
4186 }
4187
4188 fn rewrite_id_to_vid(expr: Expr, vars_in_scope: &[VariableInfo]) -> Expr {
4197 match expr {
4198 Expr::FunctionCall {
4199 name,
4200 args,
4201 distinct,
4202 window_spec,
4203 } if args.len() == 1 && Self::metadata_function_column(&name, None).is_some() => {
4204 if let Expr::Variable(ref var) = args[0] {
4205 let var_type = find_var_in_scope(vars_in_scope, var).map(|v| v.var_type);
4209 let column = Self::metadata_function_column(&name, var_type)
4210 .unwrap()
4211 .to_string();
4212 Expr::Property(Box::new(Expr::Variable(var.clone())), column)
4213 } else {
4214 Expr::FunctionCall {
4215 name,
4216 args,
4217 distinct,
4218 window_spec,
4219 }
4220 }
4221 }
4222 Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
4223 left: Box::new(Self::rewrite_id_to_vid(*left, vars_in_scope)),
4224 op,
4225 right: Box::new(Self::rewrite_id_to_vid(*right, vars_in_scope)),
4226 },
4227 Expr::UnaryOp { op, expr: inner } => Expr::UnaryOp {
4228 op,
4229 expr: Box::new(Self::rewrite_id_to_vid(*inner, vars_in_scope)),
4230 },
4231 other => other,
4232 }
4233 }
4234
4235 fn metadata_function_column(
4242 name: &str,
4243 var_type: Option<VariableType>,
4244 ) -> Option<&'static str> {
4245 if name.eq_ignore_ascii_case("id") {
4246 if matches!(var_type, Some(VariableType::Edge)) {
4247 Some("_eid")
4248 } else {
4249 Some("_vid")
4250 }
4251 } else if name.eq_ignore_ascii_case("created_at") {
4252 Some("_created_at")
4253 } else if name.eq_ignore_ascii_case("updated_at") {
4254 Some("_updated_at")
4255 } else {
4256 None
4257 }
4258 }
4259
4260 fn plan_match_clause(
4262 &self,
4263 match_clause: &MatchClause,
4264 plan: LogicalPlan,
4265 vars_in_scope: &mut Vec<VariableInfo>,
4266 ) -> Result<LogicalPlan> {
4267 let mut plan = plan;
4268
4269 if match_clause.pattern.paths.is_empty() {
4270 return Err(anyhow!("Empty pattern"));
4271 }
4272
4273 let vars_before_pattern = vars_in_scope.len();
4275
4276 for path in &match_clause.pattern.paths {
4277 if let Some(mode) = &path.shortest_path_mode {
4278 plan =
4279 self.plan_shortest_path(path, plan, vars_in_scope, mode, vars_before_pattern)?;
4280 } else {
4281 plan = self.plan_path(
4282 path,
4283 plan,
4284 vars_in_scope,
4285 match_clause.optional,
4286 vars_before_pattern,
4287 )?;
4288 }
4289 }
4290
4291 let optional_vars: HashSet<String> = if match_clause.optional {
4293 vars_in_scope[vars_before_pattern..]
4294 .iter()
4295 .map(|v| v.name.clone())
4296 .collect()
4297 } else {
4298 HashSet::new()
4299 };
4300
4301 if let Some(predicate) = &match_clause.where_clause {
4303 plan = self.plan_where_clause(predicate, plan, vars_in_scope, optional_vars)?;
4304 }
4305
4306 Ok(plan)
4307 }
4308
4309 fn plan_shortest_path(
4311 &self,
4312 path: &PathPattern,
4313 plan: LogicalPlan,
4314 vars_in_scope: &mut Vec<VariableInfo>,
4315 mode: &ShortestPathMode,
4316 _vars_before_pattern: usize,
4317 ) -> Result<LogicalPlan> {
4318 let mut plan = plan;
4319 let elements = &path.elements;
4320
4321 if elements.len() < 3 || elements.len().is_multiple_of(2) {
4323 return Err(anyhow!(
4324 "shortestPath requires at least one relationship: (a)-[*]->(b)"
4325 ));
4326 }
4327
4328 let source_node = match &elements[0] {
4329 PatternElement::Node(n) => n,
4330 _ => return Err(anyhow!("ShortestPath must start with a node")),
4331 };
4332 let rel = match &elements[1] {
4333 PatternElement::Relationship(r) => r,
4334 _ => {
4335 return Err(anyhow!(
4336 "ShortestPath middle element must be a relationship"
4337 ));
4338 }
4339 };
4340 let target_node = match &elements[2] {
4341 PatternElement::Node(n) => n,
4342 _ => return Err(anyhow!("ShortestPath must end with a node")),
4343 };
4344
4345 let source_var = source_node
4346 .variable
4347 .clone()
4348 .ok_or_else(|| anyhow!("Source node must have variable in shortestPath"))?;
4349 let target_var = target_node
4350 .variable
4351 .clone()
4352 .ok_or_else(|| anyhow!("Target node must have variable in shortestPath"))?;
4353 let path_var = path
4354 .variable
4355 .clone()
4356 .ok_or_else(|| anyhow!("shortestPath must be assigned to a variable"))?;
4357
4358 let source_bound = is_var_in_scope(vars_in_scope, &source_var);
4359 let target_bound = is_var_in_scope(vars_in_scope, &target_var);
4360
4361 if !source_bound {
4363 plan = self.plan_unbound_node(source_node, &source_var, plan, false)?;
4364 } else if let Some(prop_filter) =
4365 self.properties_to_expr(&source_var, &source_node.properties)
4366 {
4367 plan = LogicalPlan::Filter {
4368 input: Box::new(plan),
4369 predicate: prop_filter,
4370 optional_variables: HashSet::new(),
4371 };
4372 }
4373
4374 let target_label_id = if !target_bound {
4376 let target_label_name = target_node
4378 .labels
4379 .first()
4380 .ok_or_else(|| anyhow!("Target node must have label if not already bound"))?;
4381 let target_label_id =
4386 if let Some(meta) = self.schema.get_label_case_insensitive(target_label_name) {
4387 meta.id
4388 } else if let Some((vid, _)) = self.allocate_virtual_label(target_label_name)? {
4389 vid
4390 } else {
4391 return Err(anyhow!("Label {} not found", target_label_name));
4392 };
4393
4394 let target_scan = LogicalPlan::Scan {
4395 label_id: target_label_id,
4396 labels: target_node.labels.names().to_vec(),
4397 variable: target_var.clone(),
4398 filter: self.properties_to_expr(&target_var, &target_node.properties),
4399 optional: false,
4400 };
4401
4402 plan = Self::join_with_plan(plan, target_scan);
4403 target_label_id
4404 } else {
4405 if let Some(prop_filter) = self.properties_to_expr(&target_var, &target_node.properties)
4406 {
4407 plan = LogicalPlan::Filter {
4408 input: Box::new(plan),
4409 predicate: prop_filter,
4410 optional_variables: HashSet::new(),
4411 };
4412 }
4413 0 };
4415
4416 let edge_type_ids = if rel.types.is_empty() {
4418 self.schema.all_edge_type_ids()
4420 } else {
4421 let mut ids = Vec::new();
4422 for type_name in &rel.types {
4423 let id = if let Some(meta) = self.schema.edge_types.get(type_name) {
4424 meta.id
4425 } else if let Some((vid, _)) = self.allocate_virtual_edge_type(type_name)? {
4426 vid
4427 } else {
4428 return Err(anyhow!("Edge type {} not found", type_name));
4429 };
4430 ids.push(id);
4431 }
4432 ids
4433 };
4434
4435 let min_hops = rel.range.as_ref().and_then(|r| r.min).unwrap_or(1);
4437 let max_hops = rel.range.as_ref().and_then(|r| r.max).unwrap_or(u32::MAX);
4438
4439 let sp_plan = match mode {
4440 ShortestPathMode::Shortest => LogicalPlan::ShortestPath {
4441 input: Box::new(plan),
4442 edge_type_ids,
4443 direction: rel.direction.clone(),
4444 source_variable: source_var.clone(),
4445 target_variable: target_var.clone(),
4446 target_label_id,
4447 path_variable: path_var.clone(),
4448 min_hops,
4449 max_hops,
4450 },
4451 ShortestPathMode::AllShortest => LogicalPlan::AllShortestPaths {
4452 input: Box::new(plan),
4453 edge_type_ids,
4454 direction: rel.direction.clone(),
4455 source_variable: source_var.clone(),
4456 target_variable: target_var.clone(),
4457 target_label_id,
4458 path_variable: path_var.clone(),
4459 min_hops,
4460 max_hops,
4461 },
4462 };
4463
4464 if !source_bound {
4465 add_var_to_scope(vars_in_scope, &source_var, VariableType::Node)?;
4466 }
4467 if !target_bound {
4468 add_var_to_scope(vars_in_scope, &target_var, VariableType::Node)?;
4469 }
4470 add_var_to_scope(vars_in_scope, &path_var, VariableType::Path)?;
4471
4472 Ok(sp_plan)
4473 }
4474 pub fn plan_pattern(
4479 &self,
4480 pattern: &Pattern,
4481 initial_vars: &[VariableInfo],
4482 ) -> Result<LogicalPlan> {
4483 let mut vars_in_scope: Vec<VariableInfo> = initial_vars.to_vec();
4484 let vars_before_pattern = vars_in_scope.len();
4485 let mut plan = LogicalPlan::Empty;
4486 for path in &pattern.paths {
4487 plan = self.plan_path(path, plan, &mut vars_in_scope, false, vars_before_pattern)?;
4488 }
4489 Ok(plan)
4490 }
4491
4492 fn plan_path(
4494 &self,
4495 path: &PathPattern,
4496 plan: LogicalPlan,
4497 vars_in_scope: &mut Vec<VariableInfo>,
4498 optional: bool,
4499 vars_before_pattern: usize,
4500 ) -> Result<LogicalPlan> {
4501 let mut plan = plan;
4502 let elements = &path.elements;
4503 let mut i = 0;
4504
4505 let path_variable = path.variable.clone();
4506
4507 if let Some(pv) = &path_variable
4509 && !pv.is_empty()
4510 && is_var_in_scope(vars_in_scope, pv)
4511 {
4512 return Err(anyhow!(
4513 "SyntaxError: VariableAlreadyBound - Variable '{}' already defined",
4514 pv
4515 ));
4516 }
4517
4518 if let Some(pv) = &path_variable
4520 && !pv.is_empty()
4521 {
4522 for element in elements {
4523 match element {
4524 PatternElement::Node(n) => {
4525 if let Some(v) = &n.variable
4526 && v == pv
4527 {
4528 return Err(anyhow!(
4529 "SyntaxError: VariableAlreadyBound - Variable '{}' already defined",
4530 pv
4531 ));
4532 }
4533 }
4534 PatternElement::Relationship(r) => {
4535 if let Some(v) = &r.variable
4536 && v == pv
4537 {
4538 return Err(anyhow!(
4539 "SyntaxError: VariableAlreadyBound - Variable '{}' already defined",
4540 pv
4541 ));
4542 }
4543 }
4544 PatternElement::Parenthesized { .. } => {}
4545 }
4546 }
4547 }
4548
4549 let mut optional_pattern_vars: HashSet<String> = if optional {
4552 let mut vars = HashSet::new();
4553 for element in elements {
4554 match element {
4555 PatternElement::Node(n) => {
4556 if let Some(v) = &n.variable
4557 && !v.is_empty()
4558 && !is_var_in_scope(vars_in_scope, v)
4559 {
4560 vars.insert(v.clone());
4561 }
4562 }
4563 PatternElement::Relationship(r) => {
4564 if let Some(v) = &r.variable
4565 && !v.is_empty()
4566 && !is_var_in_scope(vars_in_scope, v)
4567 {
4568 vars.insert(v.clone());
4569 }
4570 }
4571 PatternElement::Parenthesized { pattern, .. } => {
4572 for nested_elem in &pattern.elements {
4574 match nested_elem {
4575 PatternElement::Node(n) => {
4576 if let Some(v) = &n.variable
4577 && !v.is_empty()
4578 && !is_var_in_scope(vars_in_scope, v)
4579 {
4580 vars.insert(v.clone());
4581 }
4582 }
4583 PatternElement::Relationship(r) => {
4584 if let Some(v) = &r.variable
4585 && !v.is_empty()
4586 && !is_var_in_scope(vars_in_scope, v)
4587 {
4588 vars.insert(v.clone());
4589 }
4590 }
4591 _ => {}
4592 }
4593 }
4594 }
4595 }
4596 }
4597 if let Some(pv) = &path_variable
4599 && !pv.is_empty()
4600 {
4601 vars.insert(pv.clone());
4602 }
4603 vars
4604 } else {
4605 HashSet::new()
4606 };
4607
4608 let path_bound_edge_vars: HashSet<String> = {
4613 let mut bound = HashSet::new();
4614 for element in elements {
4615 if let PatternElement::Relationship(rel) = element
4616 && let Some(ref var_name) = rel.variable
4617 && !var_name.is_empty()
4618 && vars_in_scope[..vars_before_pattern]
4619 .iter()
4620 .any(|v| v.name == *var_name)
4621 {
4622 bound.insert(var_name.clone());
4623 }
4624 }
4625 bound
4626 };
4627
4628 let mut had_traverses = false;
4630 let mut single_node_variable: Option<String> = None;
4632 let mut path_node_vars: Vec<String> = Vec::new();
4634 let mut path_edge_vars: Vec<String> = Vec::new();
4635 let mut last_outer_node_var: Option<String> = None;
4638
4639 while i < elements.len() {
4641 let element = &elements[i];
4642 match element {
4643 PatternElement::Node(n) => {
4644 let mut variable = n.variable.clone().unwrap_or_default();
4645 if variable.is_empty() {
4646 variable = self.next_anon_var();
4647 }
4648 if single_node_variable.is_none() {
4650 single_node_variable = Some(variable.clone());
4651 }
4652 let is_bound =
4653 !variable.is_empty() && is_var_in_scope(vars_in_scope, &variable);
4654 if optional && !is_bound {
4655 optional_pattern_vars.insert(variable.clone());
4656 }
4657
4658 if is_bound {
4659 if let Some(info) = find_var_in_scope(vars_in_scope, &variable)
4661 && !info.var_type.is_compatible_with(VariableType::Node)
4662 {
4663 return Err(anyhow!(
4664 "SyntaxError: VariableTypeConflict - Variable '{}' already defined as {:?}, cannot use as Node",
4665 variable,
4666 info.var_type
4667 ));
4668 }
4669 if let Some(node_filter) =
4670 self.node_filter_expr(&variable, &n.labels, &n.properties)
4671 {
4672 plan = LogicalPlan::Filter {
4673 input: Box::new(plan),
4674 predicate: node_filter,
4675 optional_variables: HashSet::new(),
4676 };
4677 }
4678 } else {
4679 plan = self.plan_unbound_node(n, &variable, plan, optional)?;
4680 if !variable.is_empty() {
4681 add_var_to_scope(vars_in_scope, &variable, VariableType::Node)?;
4682 }
4683 }
4684
4685 if path_variable.is_some() && path_node_vars.is_empty() {
4687 path_node_vars.push(variable.clone());
4688 }
4689
4690 let mut current_source_var = variable;
4692 last_outer_node_var = Some(current_source_var.clone());
4693 i += 1;
4694 while i < elements.len() {
4695 if let PatternElement::Relationship(r) = &elements[i] {
4696 if i + 1 < elements.len() {
4697 let target_node_part = &elements[i + 1];
4698 if let PatternElement::Node(n_target) = target_node_part {
4699 let is_vlp = r.range.is_some();
4702 let traverse_path_var =
4703 if is_vlp { path_variable.clone() } else { None };
4704
4705 if is_vlp
4710 && let Some(pv) = path_variable.as_ref()
4711 && !path_node_vars.is_empty()
4712 {
4713 plan = LogicalPlan::BindPath {
4714 input: Box::new(plan),
4715 node_variables: std::mem::take(&mut path_node_vars),
4716 edge_variables: std::mem::take(&mut path_edge_vars),
4717 path_variable: pv.clone(),
4718 };
4719 if !is_var_in_scope(vars_in_scope, pv) {
4720 add_var_to_scope(
4721 vars_in_scope,
4722 pv,
4723 VariableType::Path,
4724 )?;
4725 }
4726 }
4727
4728 let target_was_bound =
4730 n_target.variable.as_ref().is_some_and(|v| {
4731 !v.is_empty() && is_var_in_scope(vars_in_scope, v)
4732 });
4733 let (new_plan, target_var, effective_target) = self
4734 .plan_traverse_with_source(
4735 plan,
4736 vars_in_scope,
4737 TraverseParams {
4738 rel: r,
4739 target_node: n_target,
4740 optional,
4741 path_variable: traverse_path_var,
4742 optional_pattern_vars: optional_pattern_vars
4743 .clone(),
4744 },
4745 ¤t_source_var,
4746 vars_before_pattern,
4747 &path_bound_edge_vars,
4748 )?;
4749 plan = new_plan;
4750 if optional && !target_was_bound {
4751 optional_pattern_vars.insert(target_var.clone());
4752 }
4753
4754 if path_variable.is_some() && !is_vlp {
4756 if let Some(ev) = &r.variable {
4762 path_edge_vars.push(ev.clone());
4763 } else {
4764 path_edge_vars
4765 .push(format!("__eid_to_{}", effective_target));
4766 }
4767 path_node_vars.push(target_var.clone());
4768 }
4769
4770 current_source_var = target_var;
4771 last_outer_node_var = Some(current_source_var.clone());
4772 had_traverses = true;
4773 i += 2;
4774 } else {
4775 return Err(anyhow!("Relationship must be followed by a node"));
4776 }
4777 } else {
4778 return Err(anyhow!("Relationship cannot be the last element"));
4779 }
4780 } else {
4781 break;
4782 }
4783 }
4784 }
4785 PatternElement::Relationship(_) => {
4786 return Err(anyhow!("Pattern must start with a node"));
4787 }
4788 PatternElement::Parenthesized { pattern, range } => {
4789 if pattern.elements.len() < 3 || pattern.elements.len() % 2 == 0 {
4792 return Err(anyhow!(
4793 "Quantified pattern must have node-relationship-node structure (odd number >= 3 elements)"
4794 ));
4795 }
4796
4797 let source_node = match &pattern.elements[0] {
4798 PatternElement::Node(n) => n,
4799 _ => return Err(anyhow!("Quantified pattern must start with a node")),
4800 };
4801
4802 let mut qpp_rels: Vec<(&RelationshipPattern, &NodePattern)> = Vec::new();
4804 for pair_idx in (1..pattern.elements.len()).step_by(2) {
4805 let rel = match &pattern.elements[pair_idx] {
4806 PatternElement::Relationship(r) => r,
4807 _ => {
4808 return Err(anyhow!(
4809 "Quantified pattern element at position {} must be a relationship",
4810 pair_idx
4811 ));
4812 }
4813 };
4814 let node = match &pattern.elements[pair_idx + 1] {
4815 PatternElement::Node(n) => n,
4816 _ => {
4817 return Err(anyhow!(
4818 "Quantified pattern element at position {} must be a node",
4819 pair_idx + 1
4820 ));
4821 }
4822 };
4823 if rel.range.is_some() {
4825 return Err(anyhow!(
4826 "Nested quantifiers not supported: ((a)-[:REL*n]->(b)){{m}}"
4827 ));
4828 }
4829 qpp_rels.push((rel, node));
4830 }
4831
4832 let inner_target_node = qpp_rels.last().unwrap().1;
4836 let outer_target_node = if i + 1 < elements.len() {
4837 match &elements[i + 1] {
4838 PatternElement::Node(n) => Some(n),
4839 _ => None,
4840 }
4841 } else {
4842 None
4843 };
4844 let target_node = outer_target_node.unwrap_or(inner_target_node);
4847
4848 let use_simple_vlp = qpp_rels.len() == 1
4851 && inner_target_node
4852 .labels
4853 .first()
4854 .and_then(|l| self.schema.get_label_case_insensitive(l))
4855 .is_none();
4856
4857 let source_variable = if let Some(ref outer_src) = last_outer_node_var {
4862 if let Some(prop_filter) =
4865 self.properties_to_expr(outer_src, &source_node.properties)
4866 {
4867 plan = LogicalPlan::Filter {
4868 input: Box::new(plan),
4869 predicate: prop_filter,
4870 optional_variables: HashSet::new(),
4871 };
4872 }
4873 outer_src.clone()
4874 } else {
4875 let sv = source_node
4876 .variable
4877 .clone()
4878 .filter(|v| !v.is_empty())
4879 .unwrap_or_else(|| self.next_anon_var());
4880
4881 if is_var_in_scope(vars_in_scope, &sv) {
4882 if let Some(prop_filter) =
4884 self.properties_to_expr(&sv, &source_node.properties)
4885 {
4886 plan = LogicalPlan::Filter {
4887 input: Box::new(plan),
4888 predicate: prop_filter,
4889 optional_variables: HashSet::new(),
4890 };
4891 }
4892 } else {
4893 plan = self.plan_unbound_node(source_node, &sv, plan, optional)?;
4895 add_var_to_scope(vars_in_scope, &sv, VariableType::Node)?;
4896 if optional {
4897 optional_pattern_vars.insert(sv.clone());
4898 }
4899 }
4900 sv
4901 };
4902
4903 if use_simple_vlp {
4904 let mut relationship = qpp_rels[0].0.clone();
4906 relationship.range = range.clone();
4907
4908 let target_was_bound = target_node
4909 .variable
4910 .as_ref()
4911 .is_some_and(|v| !v.is_empty() && is_var_in_scope(vars_in_scope, v));
4912 let (new_plan, target_var, _effective_target) = self
4913 .plan_traverse_with_source(
4914 plan,
4915 vars_in_scope,
4916 TraverseParams {
4917 rel: &relationship,
4918 target_node,
4919 optional,
4920 path_variable: path_variable.clone(),
4921 optional_pattern_vars: optional_pattern_vars.clone(),
4922 },
4923 &source_variable,
4924 vars_before_pattern,
4925 &path_bound_edge_vars,
4926 )?;
4927 plan = new_plan;
4928 if optional && !target_was_bound {
4929 optional_pattern_vars.insert(target_var);
4930 }
4931 } else {
4932 let mut qpp_step_infos = Vec::new();
4934 let mut all_edge_type_ids = Vec::new();
4935
4936 for (rel, node) in &qpp_rels {
4937 let mut step_edge_type_ids = Vec::new();
4938 if rel.types.is_empty() {
4939 step_edge_type_ids = self.schema.all_edge_type_ids();
4940 } else {
4941 for type_name in &rel.types {
4942 if let Some(edge_meta) = self.schema.edge_types.get(type_name) {
4943 step_edge_type_ids.push(edge_meta.id);
4944 }
4945 }
4946 }
4947 all_edge_type_ids.extend_from_slice(&step_edge_type_ids);
4948
4949 let target_label = node.labels.first().and_then(|l| {
4950 self.schema.get_label_case_insensitive(l).map(|_| l.clone())
4951 });
4952
4953 qpp_step_infos.push(QppStepInfo {
4954 edge_type_ids: step_edge_type_ids,
4955 direction: rel.direction.clone(),
4956 target_label,
4957 });
4958 }
4959
4960 all_edge_type_ids.sort_unstable();
4962 all_edge_type_ids.dedup();
4963
4964 let hops_per_iter = qpp_step_infos.len();
4966 const QPP_DEFAULT_MAX_HOPS: usize = 100;
4967 let (min_iter, max_iter) = if let Some(range) = range {
4968 let min = range.min.unwrap_or(1) as usize;
4969 let max = range
4970 .max
4971 .map(|m| m as usize)
4972 .unwrap_or(QPP_DEFAULT_MAX_HOPS / hops_per_iter);
4973 (min, max)
4974 } else {
4975 (1, 1)
4976 };
4977 let min_hops = min_iter * hops_per_iter;
4978 let max_hops = max_iter * hops_per_iter;
4979
4980 let target_variable = target_node
4982 .variable
4983 .clone()
4984 .filter(|v| !v.is_empty())
4985 .unwrap_or_else(|| self.next_anon_var());
4986
4987 let target_is_bound = is_var_in_scope(vars_in_scope, &target_variable);
4988
4989 let target_label_meta = target_node
4991 .labels
4992 .first()
4993 .and_then(|l| self.schema.get_label_case_insensitive(l));
4994
4995 let mut scope_match_variables: HashSet<String> = vars_in_scope
4997 [vars_before_pattern..]
4998 .iter()
4999 .map(|v| v.name.clone())
5000 .collect();
5001 scope_match_variables.insert(target_variable.clone());
5002
5003 let rebound_target_var = if target_is_bound {
5005 Some(target_variable.clone())
5006 } else {
5007 None
5008 };
5009 let effective_target_var = if let Some(ref bv) = rebound_target_var {
5010 format!("__rebound_{}", bv)
5011 } else {
5012 target_variable.clone()
5013 };
5014
5015 plan = LogicalPlan::Traverse {
5016 input: Box::new(plan),
5017 edge_type_ids: all_edge_type_ids,
5018 direction: qpp_rels[0].0.direction.clone(),
5019 source_variable: source_variable.to_string(),
5020 target_variable: effective_target_var.clone(),
5021 target_label_id: target_label_meta.map(|m| m.id).unwrap_or(0),
5022 step_variable: None, min_hops,
5024 max_hops,
5025 optional,
5026 target_filter: self.node_filter_expr(
5027 &target_variable,
5028 &target_node.labels,
5029 &target_node.properties,
5030 ),
5031 path_variable: path_variable.clone(),
5032 edge_properties: HashSet::new(),
5033 is_variable_length: true,
5034 optional_pattern_vars: optional_pattern_vars.clone(),
5035 scope_match_variables,
5036 edge_filter_expr: None,
5037 path_mode: crate::query::df_graph::nfa::PathMode::Trail,
5038 qpp_steps: Some(qpp_step_infos),
5039 };
5040
5041 if let Some(ref btv) = rebound_target_var {
5043 let filter_pred = Expr::BinaryOp {
5045 left: Box::new(Expr::Property(
5046 Box::new(Expr::Variable(effective_target_var.clone())),
5047 "_vid".to_string(),
5048 )),
5049 op: BinaryOp::Eq,
5050 right: Box::new(Expr::Property(
5051 Box::new(Expr::Variable(btv.clone())),
5052 "_vid".to_string(),
5053 )),
5054 };
5055 plan = LogicalPlan::Filter {
5056 input: Box::new(plan),
5057 predicate: filter_pred,
5058 optional_variables: if optional {
5059 optional_pattern_vars.clone()
5060 } else {
5061 HashSet::new()
5062 },
5063 };
5064 }
5065
5066 if !target_is_bound {
5068 add_var_to_scope(vars_in_scope, &target_variable, VariableType::Node)?;
5069 }
5070
5071 if let Some(ref pv) = path_variable
5073 && !pv.is_empty()
5074 && !is_var_in_scope(vars_in_scope, pv)
5075 {
5076 add_var_to_scope(vars_in_scope, pv, VariableType::Path)?;
5077 }
5078 }
5079 had_traverses = true;
5080
5081 if outer_target_node.is_some() {
5083 i += 2; } else {
5085 i += 1;
5086 }
5087 }
5088 }
5089 }
5090
5091 if let Some(ref path_var) = path_variable
5094 && !path_var.is_empty()
5095 && !had_traverses
5096 && let Some(node_var) = single_node_variable
5097 {
5098 plan = LogicalPlan::BindZeroLengthPath {
5099 input: Box::new(plan),
5100 node_variable: node_var,
5101 path_variable: path_var.clone(),
5102 };
5103 add_var_to_scope(vars_in_scope, path_var, VariableType::Path)?;
5104 }
5105
5106 if let Some(ref path_var) = path_variable
5108 && !path_var.is_empty()
5109 && had_traverses
5110 && !path_node_vars.is_empty()
5111 && !is_var_in_scope(vars_in_scope, path_var)
5112 {
5113 plan = LogicalPlan::BindPath {
5114 input: Box::new(plan),
5115 node_variables: path_node_vars,
5116 edge_variables: path_edge_vars,
5117 path_variable: path_var.clone(),
5118 };
5119 add_var_to_scope(vars_in_scope, path_var, VariableType::Path)?;
5120 }
5121
5122 Ok(plan)
5123 }
5124
5125 fn plan_traverse_with_source(
5132 &self,
5133 plan: LogicalPlan,
5134 vars_in_scope: &mut Vec<VariableInfo>,
5135 params: TraverseParams<'_>,
5136 source_variable: &str,
5137 vars_before_pattern: usize,
5138 path_bound_edge_vars: &HashSet<String>,
5139 ) -> Result<(LogicalPlan, String, String)> {
5140 if let Some(Expr::Parameter(_)) = ¶ms.rel.properties {
5142 return Err(anyhow!(
5143 "SyntaxError: InvalidParameterUse - Parameters cannot be used as relationship predicates"
5144 ));
5145 }
5146
5147 let mut edge_type_ids = Vec::new();
5148 let mut dst_labels = Vec::new();
5149 let mut unknown_types = Vec::new();
5150
5151 if params.rel.types.is_empty() {
5152 edge_type_ids = self.schema.all_edge_type_ids();
5155 for meta in self.schema.edge_types.values() {
5156 dst_labels.extend(meta.dst_labels.iter().cloned());
5157 }
5158 } else {
5159 for type_name in ¶ms.rel.types {
5160 if let Some(edge_meta) = self.schema.edge_types.get(type_name) {
5161 edge_type_ids.push(edge_meta.id);
5163 dst_labels.extend(edge_meta.dst_labels.iter().cloned());
5164 } else if let Some((vid, _)) = self.allocate_virtual_edge_type(type_name)? {
5165 edge_type_ids.push(vid);
5171 } else {
5172 unknown_types.push(type_name.clone());
5174 }
5175 }
5176 }
5177
5178 edge_type_ids.sort_unstable();
5180 edge_type_ids.dedup();
5181 unknown_types.sort_unstable();
5182 unknown_types.dedup();
5183
5184 let mut target_variable = params.target_node.variable.clone().unwrap_or_default();
5185 if target_variable.is_empty() {
5186 target_variable = self.next_anon_var();
5187 }
5188 let target_is_bound =
5189 !target_variable.is_empty() && is_var_in_scope(vars_in_scope, &target_variable);
5190
5191 if let Some(rel_var) = ¶ms.rel.variable
5194 && !rel_var.is_empty()
5195 && rel_var == &target_variable
5196 {
5197 return Err(anyhow!(
5198 "SyntaxError: VariableTypeConflict - Variable '{}' already defined as relationship, cannot use as node",
5199 rel_var
5200 ));
5201 }
5202
5203 let mut bound_edge_var: Option<String> = None;
5208 let mut bound_edge_list_var: Option<String> = None;
5209 if let Some(rel_var) = ¶ms.rel.variable
5210 && !rel_var.is_empty()
5211 && let Some(info) = find_var_in_scope(vars_in_scope, rel_var)
5212 {
5213 let is_from_previous_clause = vars_in_scope[..vars_before_pattern]
5214 .iter()
5215 .any(|v| v.name == *rel_var);
5216
5217 if info.var_type == VariableType::Edge {
5218 if is_from_previous_clause {
5220 bound_edge_var = Some(rel_var.clone());
5223 } else {
5224 return Err(anyhow!(
5226 "SyntaxError: RelationshipUniquenessViolation - Relationship variable '{}' is already used in this pattern",
5227 rel_var
5228 ));
5229 }
5230 } else if params.rel.range.is_some()
5231 && is_from_previous_clause
5232 && matches!(
5233 info.var_type,
5234 VariableType::Scalar | VariableType::ScalarLiteral
5235 )
5236 {
5237 bound_edge_list_var = Some(rel_var.clone());
5240 } else if !info.var_type.is_compatible_with(VariableType::Edge) {
5241 return Err(anyhow!(
5242 "SyntaxError: VariableTypeConflict - Variable '{}' already defined as {:?}, cannot use as relationship",
5243 rel_var,
5244 info.var_type
5245 ));
5246 }
5247 }
5248
5249 if target_is_bound
5252 && let Some(info) = find_var_in_scope(vars_in_scope, &target_variable)
5253 && !info.var_type.is_compatible_with(VariableType::Node)
5254 {
5255 return Err(anyhow!(
5256 "SyntaxError: VariableTypeConflict - Variable '{}' already defined as {:?}, cannot use as Node",
5257 target_variable,
5258 info.var_type
5259 ));
5260 }
5261
5262 if !unknown_types.is_empty() && edge_type_ids.is_empty() {
5266 let is_variable_length = params.rel.range.is_some();
5269
5270 const DEFAULT_MAX_HOPS: usize = 100;
5271 let (min_hops, max_hops) = if let Some(range) = ¶ms.rel.range {
5272 let min = range.min.unwrap_or(1) as usize;
5273 let max = range.max.map(|m| m as usize).unwrap_or(DEFAULT_MAX_HOPS);
5274 (min, max)
5275 } else {
5276 (1, 1)
5277 };
5278
5279 let step_var = params.rel.variable.clone();
5285 let path_var = params.path_variable.clone();
5286
5287 let mut scope_match_variables: HashSet<String> = vars_in_scope[vars_before_pattern..]
5289 .iter()
5290 .map(|v| v.name.clone())
5291 .collect();
5292 if let Some(ref sv) = step_var {
5293 if bound_edge_var.is_none() {
5297 scope_match_variables.insert(sv.clone());
5298 }
5299 }
5300 scope_match_variables.insert(target_variable.clone());
5301 scope_match_variables.extend(
5307 path_bound_edge_vars
5308 .iter()
5309 .filter(|v| bound_edge_var.as_ref() != Some(*v))
5310 .cloned(),
5311 );
5312
5313 let mut plan = LogicalPlan::TraverseMainByType {
5314 type_names: unknown_types,
5315 input: Box::new(plan),
5316 direction: params.rel.direction.clone(),
5317 source_variable: source_variable.to_string(),
5318 target_variable: target_variable.clone(),
5319 step_variable: step_var.clone(),
5320 min_hops,
5321 max_hops,
5322 optional: params.optional,
5323 target_filter: self.node_filter_expr(
5324 &target_variable,
5325 ¶ms.target_node.labels,
5326 ¶ms.target_node.properties,
5327 ),
5328 path_variable: path_var.clone(),
5329 is_variable_length,
5330 optional_pattern_vars: params.optional_pattern_vars.clone(),
5331 scope_match_variables,
5332 edge_filter_expr: if is_variable_length {
5333 let filter_var = step_var
5334 .clone()
5335 .unwrap_or_else(|| "__anon_edge".to_string());
5336 self.properties_to_expr(&filter_var, ¶ms.rel.properties)
5337 } else {
5338 None
5339 },
5340 path_mode: crate::query::df_graph::nfa::PathMode::Trail,
5341 };
5342
5343 if target_is_bound
5347 && let Some(info) = find_var_in_scope(vars_in_scope, &target_variable)
5348 && info.var_type == VariableType::Imported
5349 {
5350 plan = Self::wrap_with_bound_target_filter(plan, &target_variable);
5351 }
5352
5353 if !is_variable_length
5358 && let Some(edge_var_name) = step_var.as_ref()
5359 && let Some(edge_prop_filter) =
5360 self.properties_to_expr(edge_var_name, ¶ms.rel.properties)
5361 {
5362 let filter_optional_vars = if params.optional {
5363 params.optional_pattern_vars.clone()
5364 } else {
5365 HashSet::new()
5366 };
5367 plan = LogicalPlan::Filter {
5368 input: Box::new(plan),
5369 predicate: edge_prop_filter,
5370 optional_variables: filter_optional_vars,
5371 };
5372 }
5373
5374 if let Some(sv) = &step_var {
5376 add_var_to_scope(vars_in_scope, sv, VariableType::Edge)?;
5377 if is_variable_length
5378 && let Some(info) = vars_in_scope.iter_mut().find(|v| v.name == *sv)
5379 {
5380 info.is_vlp = true;
5381 }
5382 }
5383 if let Some(pv) = &path_var
5384 && !is_var_in_scope(vars_in_scope, pv)
5385 {
5386 add_var_to_scope(vars_in_scope, pv, VariableType::Path)?;
5387 }
5388 if !is_var_in_scope(vars_in_scope, &target_variable) {
5389 add_var_to_scope(vars_in_scope, &target_variable, VariableType::Node)?;
5390 }
5391
5392 return Ok((plan, target_variable.clone(), target_variable));
5393 }
5394
5395 if !unknown_types.is_empty() {
5398 return Err(anyhow!(
5399 "Mixed known and unknown edge types not yet supported. Unknown: {:?}",
5400 unknown_types
5401 ));
5402 }
5403
5404 let mut virtual_target_label_id: Option<u16> = None;
5411 let target_label_meta = if let Some(label_name) = params.target_node.labels.first() {
5412 match self.schema.get_label_case_insensitive(label_name) {
5415 Some(meta) => Some(meta),
5416 None => {
5417 if let Some((vid, _)) = self.allocate_virtual_label(label_name)? {
5418 virtual_target_label_id = Some(vid);
5419 }
5420 None
5421 }
5422 }
5423 } else if !target_is_bound {
5424 let unique_dsts: Vec<_> = dst_labels
5426 .into_iter()
5427 .collect::<HashSet<_>>()
5428 .into_iter()
5429 .collect();
5430 if unique_dsts.len() == 1 {
5431 let label_name = &unique_dsts[0];
5432 self.schema.get_label_case_insensitive(label_name)
5433 } else {
5434 None
5438 }
5439 } else {
5440 None
5441 };
5442
5443 let is_variable_length = params.rel.range.is_some();
5445
5446 const DEFAULT_MAX_HOPS: usize = 100;
5449 let (min_hops, max_hops) = if let Some(range) = ¶ms.rel.range {
5450 let min = range.min.unwrap_or(1) as usize;
5451 let max = range.max.map(|m| m as usize).unwrap_or(DEFAULT_MAX_HOPS);
5452 (min, max)
5453 } else {
5454 (1, 1)
5455 };
5456
5457 let step_var = params.rel.variable.clone();
5462 let path_var = params.path_variable.clone();
5463
5464 let rebound_var = bound_edge_var
5467 .as_ref()
5468 .or(bound_edge_list_var.as_ref())
5469 .cloned();
5470 let effective_step_var = if let Some(ref bv) = rebound_var {
5471 Some(format!("__rebound_{}", bv))
5472 } else {
5473 step_var.clone()
5474 };
5475
5476 let rebound_target_var = if target_is_bound && !target_variable.is_empty() {
5480 let is_imported = find_var_in_scope(vars_in_scope, &target_variable)
5481 .map(|info| info.var_type == VariableType::Imported)
5482 .unwrap_or(false);
5483 if !is_imported {
5484 Some(target_variable.clone())
5485 } else {
5486 None
5487 }
5488 } else {
5489 None
5490 };
5491
5492 let effective_target_var = if let Some(ref bv) = rebound_target_var {
5493 format!("__rebound_{}", bv)
5494 } else {
5495 target_variable.clone()
5496 };
5497
5498 let mut scope_match_variables: HashSet<String> = vars_in_scope[vars_before_pattern..]
5504 .iter()
5505 .map(|v| v.name.clone())
5506 .collect();
5507 if let Some(ref sv) = effective_step_var {
5509 scope_match_variables.insert(sv.clone());
5510 }
5511 scope_match_variables.insert(effective_target_var.clone());
5513 scope_match_variables.extend(path_bound_edge_vars.iter().cloned());
5516
5517 let mut plan = LogicalPlan::Traverse {
5518 input: Box::new(plan),
5519 edge_type_ids,
5520 direction: params.rel.direction.clone(),
5521 source_variable: source_variable.to_string(),
5522 target_variable: effective_target_var.clone(),
5523 target_label_id: target_label_meta
5524 .map(|m| m.id)
5525 .or(virtual_target_label_id)
5526 .unwrap_or(0),
5527 step_variable: effective_step_var.clone(),
5528 min_hops,
5529 max_hops,
5530 optional: params.optional,
5531 target_filter: self.node_filter_expr(
5532 &target_variable,
5533 ¶ms.target_node.labels,
5534 ¶ms.target_node.properties,
5535 ),
5536 path_variable: path_var.clone(),
5537 edge_properties: HashSet::new(),
5538 is_variable_length,
5539 optional_pattern_vars: params.optional_pattern_vars.clone(),
5540 scope_match_variables,
5541 edge_filter_expr: if is_variable_length {
5542 let filter_var = effective_step_var
5548 .clone()
5549 .unwrap_or_else(|| "__anon_edge".to_string());
5550 self.properties_to_expr(&filter_var, ¶ms.rel.properties)
5551 } else {
5552 None
5553 },
5554 path_mode: crate::query::df_graph::nfa::PathMode::Trail,
5555 qpp_steps: None,
5556 };
5557
5558 let filter_optional_vars = if params.optional {
5561 params.optional_pattern_vars.clone()
5562 } else {
5563 HashSet::new()
5564 };
5565
5566 if !is_variable_length
5570 && let Some(edge_var_name) = effective_step_var.as_ref()
5571 && let Some(edge_prop_filter) =
5572 self.properties_to_expr(edge_var_name, ¶ms.rel.properties)
5573 {
5574 plan = LogicalPlan::Filter {
5575 input: Box::new(plan),
5576 predicate: edge_prop_filter,
5577 optional_variables: filter_optional_vars.clone(),
5578 };
5579 }
5580
5581 if target_is_bound
5585 && let Some(info) = find_var_in_scope(vars_in_scope, &target_variable)
5586 && info.var_type == VariableType::Imported
5587 {
5588 plan = Self::wrap_with_bound_target_filter(plan, &target_variable);
5589 }
5590
5591 if let Some(ref bv) = bound_edge_var {
5593 let temp_var = format!("__rebound_{}", bv);
5594 let bound_check = Expr::BinaryOp {
5595 left: Box::new(Expr::Property(
5596 Box::new(Expr::Variable(temp_var)),
5597 "_eid".to_string(),
5598 )),
5599 op: BinaryOp::Eq,
5600 right: Box::new(Expr::Property(
5601 Box::new(Expr::Variable(bv.clone())),
5602 "_eid".to_string(),
5603 )),
5604 };
5605 plan = LogicalPlan::Filter {
5606 input: Box::new(plan),
5607 predicate: bound_check,
5608 optional_variables: filter_optional_vars.clone(),
5609 };
5610 }
5611
5612 if let Some(ref bv) = bound_edge_list_var {
5615 let temp_var = format!("__rebound_{}", bv);
5616 let temp_eids = Expr::ListComprehension {
5617 variable: "__rebound_edge".to_string(),
5618 list: Box::new(Expr::Variable(temp_var)),
5619 where_clause: None,
5620 map_expr: Box::new(Expr::FunctionCall {
5621 name: "toInteger".to_string(),
5622 args: vec![Expr::Property(
5623 Box::new(Expr::Variable("__rebound_edge".to_string())),
5624 "_eid".to_string(),
5625 )],
5626 distinct: false,
5627 window_spec: None,
5628 }),
5629 };
5630 let bound_eids = Expr::ListComprehension {
5631 variable: "__bound_edge".to_string(),
5632 list: Box::new(Expr::Variable(bv.clone())),
5633 where_clause: None,
5634 map_expr: Box::new(Expr::FunctionCall {
5635 name: "toInteger".to_string(),
5636 args: vec![Expr::Property(
5637 Box::new(Expr::Variable("__bound_edge".to_string())),
5638 "_eid".to_string(),
5639 )],
5640 distinct: false,
5641 window_spec: None,
5642 }),
5643 };
5644 let bound_list_check = Expr::BinaryOp {
5645 left: Box::new(temp_eids),
5646 op: BinaryOp::Eq,
5647 right: Box::new(bound_eids),
5648 };
5649 plan = LogicalPlan::Filter {
5650 input: Box::new(plan),
5651 predicate: bound_list_check,
5652 optional_variables: filter_optional_vars.clone(),
5653 };
5654 }
5655
5656 if let Some(ref bv) = rebound_target_var {
5659 let temp_var = format!("__rebound_{}", bv);
5660 let bound_check = Expr::BinaryOp {
5661 left: Box::new(Expr::Property(
5662 Box::new(Expr::Variable(temp_var.clone())),
5663 "_vid".to_string(),
5664 )),
5665 op: BinaryOp::Eq,
5666 right: Box::new(Expr::Property(
5667 Box::new(Expr::Variable(bv.clone())),
5668 "_vid".to_string(),
5669 )),
5670 };
5671 let mut rebound_filter_vars = filter_optional_vars;
5678 if params.optional {
5679 rebound_filter_vars.insert(temp_var);
5680 }
5681 plan = LogicalPlan::Filter {
5682 input: Box::new(plan),
5683 predicate: bound_check,
5684 optional_variables: rebound_filter_vars,
5685 };
5686 }
5687
5688 if let Some(sv) = &step_var
5691 && bound_edge_var.is_none()
5692 && bound_edge_list_var.is_none()
5693 {
5694 add_var_to_scope(vars_in_scope, sv, VariableType::Edge)?;
5695 if is_variable_length
5696 && let Some(info) = vars_in_scope.iter_mut().find(|v| v.name == *sv)
5697 {
5698 info.is_vlp = true;
5699 }
5700 }
5701 if let Some(pv) = &path_var
5702 && !is_var_in_scope(vars_in_scope, pv)
5703 {
5704 add_var_to_scope(vars_in_scope, pv, VariableType::Path)?;
5705 }
5706 if !is_var_in_scope(vars_in_scope, &target_variable) {
5707 add_var_to_scope(vars_in_scope, &target_variable, VariableType::Node)?;
5708 }
5709
5710 Ok((plan, target_variable, effective_target_var))
5711 }
5712
5713 fn join_with_plan(existing: LogicalPlan, new: LogicalPlan) -> LogicalPlan {
5718 if matches!(existing, LogicalPlan::Empty) {
5719 new
5720 } else {
5721 LogicalPlan::CrossJoin {
5722 left: Box::new(existing),
5723 right: Box::new(new),
5724 }
5725 }
5726 }
5727
5728 fn split_node_property_filters_for_scan(
5735 &self,
5736 variable: &str,
5737 properties: &Option<Expr>,
5738 ) -> (Option<Expr>, Option<Expr>) {
5739 let entries = match properties {
5740 Some(Expr::Map(entries)) => entries,
5741 _ => return (None, None),
5742 };
5743
5744 if entries.is_empty() {
5745 return (None, None);
5746 }
5747
5748 let mut pushdown_entries = Vec::new();
5749 let mut residual_entries = Vec::new();
5750
5751 for (prop, val_expr) in entries {
5752 let vars = collect_expr_variables(val_expr);
5753 if vars.iter().all(|v| v == variable) {
5754 pushdown_entries.push((prop.clone(), val_expr.clone()));
5755 } else {
5756 residual_entries.push((prop.clone(), val_expr.clone()));
5757 }
5758 }
5759
5760 let pushdown_map = if pushdown_entries.is_empty() {
5761 None
5762 } else {
5763 Some(Expr::Map(pushdown_entries))
5764 };
5765 let residual_map = if residual_entries.is_empty() {
5766 None
5767 } else {
5768 Some(Expr::Map(residual_entries))
5769 };
5770
5771 (
5772 self.properties_to_expr(variable, &pushdown_map),
5773 self.properties_to_expr(variable, &residual_map),
5774 )
5775 }
5776
5777 fn label_branches_share_property_schema(&self, labels: &[String]) -> bool {
5795 if labels.len() < 2 {
5796 return true;
5797 }
5798 let mut iter = labels.iter();
5799 let first = iter.next().expect("len >= 2");
5800 let Some(first_props) = self.schema.properties.get(first) else {
5801 return false;
5802 };
5803 for label in iter {
5804 let Some(props) = self.schema.properties.get(label) else {
5805 return false;
5806 };
5807 if props.len() != first_props.len() {
5808 return false;
5809 }
5810 for (name, meta) in first_props {
5811 let Some(other_meta) = props.get(name) else {
5812 return false;
5813 };
5814 if meta.r#type != other_meta.r#type {
5815 return false;
5816 }
5817 }
5818 }
5819 true
5820 }
5821
5822 fn plan_unbound_node(
5824 &self,
5825 node: &NodePattern,
5826 variable: &str,
5827 plan: LogicalPlan,
5828 optional: bool,
5829 ) -> Result<LogicalPlan> {
5830 let properties = match &node.properties {
5832 Some(Expr::Map(entries)) => entries.as_slice(),
5833 Some(Expr::Parameter(_)) => {
5834 return Err(anyhow!(
5835 "SyntaxError: InvalidParameterUse - Parameters cannot be used as node predicates"
5836 ));
5837 }
5838 Some(_) => return Err(anyhow!("Node properties must be a Map")),
5839 None => &[],
5840 };
5841
5842 let has_existing_scope = !matches!(plan, LogicalPlan::Empty);
5843
5844 let apply_residual_filter = |input: LogicalPlan, residual: Option<Expr>| -> LogicalPlan {
5845 if let Some(predicate) = residual {
5846 LogicalPlan::Filter {
5847 input: Box::new(input),
5848 predicate,
5849 optional_variables: HashSet::new(),
5850 }
5851 } else {
5852 input
5853 }
5854 };
5855
5856 let (node_scan_filter, node_residual_filter) = if has_existing_scope {
5857 self.split_node_property_filters_for_scan(variable, &node.properties)
5858 } else {
5859 (self.properties_to_expr(variable, &node.properties), None)
5860 };
5861
5862 if node.labels.is_empty() {
5864 if let Some((_, ext_id_value)) = properties.iter().find(|(k, _)| k == "ext_id") {
5866 let ext_id = match ext_id_value {
5868 Expr::Literal(CypherLiteral::String(s)) => s.clone(),
5869 _ => {
5870 return Err(anyhow!("ext_id must be a string literal for direct lookup"));
5871 }
5872 };
5873
5874 let remaining_props: Vec<_> = properties
5876 .iter()
5877 .filter(|(k, _)| k != "ext_id")
5878 .cloned()
5879 .collect();
5880
5881 let remaining_expr = if remaining_props.is_empty() {
5882 None
5883 } else {
5884 Some(Expr::Map(remaining_props))
5885 };
5886
5887 let (prop_filter, residual_filter) = if has_existing_scope {
5888 self.split_node_property_filters_for_scan(variable, &remaining_expr)
5889 } else {
5890 (self.properties_to_expr(variable, &remaining_expr), None)
5891 };
5892
5893 let ext_id_lookup = LogicalPlan::ExtIdLookup {
5894 variable: variable.to_string(),
5895 ext_id,
5896 filter: prop_filter,
5897 optional,
5898 };
5899
5900 let joined = Self::join_with_plan(plan, ext_id_lookup);
5901 return Ok(apply_residual_filter(joined, residual_filter));
5902 }
5903
5904 let scan_all = LogicalPlan::ScanAll {
5906 variable: variable.to_string(),
5907 filter: node_scan_filter,
5908 optional,
5909 };
5910
5911 let joined = Self::join_with_plan(plan, scan_all);
5912 return Ok(apply_residual_filter(joined, node_residual_filter));
5913 }
5914
5915 if node.labels.is_proper_disjunction() {
5927 let label_names: Vec<String> = node.labels.names().to_vec();
5928
5929 let use_main_table_branches = !self.label_branches_share_property_schema(&label_names);
5946
5947 let mut branches: Vec<LogicalPlan> = Vec::with_capacity(label_names.len());
5948 for label_name in &label_names {
5949 let branch = if use_main_table_branches {
5950 LogicalPlan::ScanMainByLabels {
5951 labels: vec![label_name.clone()],
5952 variable: variable.to_string(),
5953 filter: node_scan_filter.clone(),
5954 optional,
5955 }
5956 } else {
5957 let meta = self
5958 .schema
5959 .get_label_case_insensitive(label_name)
5960 .expect("share_property_schema true implies all labels in schema");
5961 LogicalPlan::Scan {
5962 label_id: meta.id,
5963 labels: vec![label_name.clone()],
5964 variable: variable.to_string(),
5965 filter: node_scan_filter.clone(),
5966 optional,
5967 }
5968 };
5969 branches.push(branch);
5970 }
5971 let mut iter = branches.into_iter();
5974 let mut union_plan = iter
5975 .next()
5976 .expect("is_proper_disjunction implies at least 2 labels");
5977 for next in iter {
5978 union_plan = LogicalPlan::Union {
5979 left: Box::new(union_plan),
5980 right: Box::new(next),
5981 all: false,
5982 };
5983 }
5984 let joined = Self::join_with_plan(plan, union_plan);
5985 return Ok(apply_residual_filter(joined, node_residual_filter));
5986 }
5987
5988 let label_name = &node.labels[0];
5990
5991 if let Some(label_meta) = self.schema.get_label_case_insensitive(label_name) {
5993 let scan = LogicalPlan::Scan {
5995 label_id: label_meta.id,
5996 labels: node.labels.names().to_vec(),
5997 variable: variable.to_string(),
5998 filter: node_scan_filter,
5999 optional,
6000 };
6001
6002 let joined = Self::join_with_plan(plan, scan);
6003 Ok(apply_residual_filter(joined, node_residual_filter))
6004 } else {
6005 if let Some((virtual_id, _)) = self.allocate_virtual_label(label_name)? {
6013 let scan = LogicalPlan::Scan {
6014 label_id: virtual_id,
6015 labels: node.labels.names().to_vec(),
6016 variable: variable.to_string(),
6017 filter: node_scan_filter,
6018 optional,
6019 };
6020 let joined = Self::join_with_plan(plan, scan);
6021 return Ok(apply_residual_filter(joined, node_residual_filter));
6022 }
6023 if self.replacement_scans_enabled {
6024 return Err(anyhow!(
6025 "Label `{}` is not defined in schema and no \
6026 CatalogProvider or ReplacementScanProvider claimed it; \
6027 strict-mode (replacement_scans=true) requires the label \
6028 to resolve",
6029 label_name
6030 ));
6031 }
6032
6033 let scan_main = LogicalPlan::ScanMainByLabels {
6034 labels: node.labels.names().to_vec(),
6035 variable: variable.to_string(),
6036 filter: node_scan_filter,
6037 optional,
6038 };
6039
6040 let joined = Self::join_with_plan(plan, scan_main);
6041 Ok(apply_residual_filter(joined, node_residual_filter))
6042 }
6043 }
6044
6045 fn plan_where_clause(
6050 &self,
6051 predicate: &Expr,
6052 plan: LogicalPlan,
6053 vars_in_scope: &[VariableInfo],
6054 optional_vars: HashSet<String>,
6055 ) -> Result<LogicalPlan> {
6056 validate_no_aggregation_in_where(predicate)?;
6058
6059 validate_expression_variables(predicate, vars_in_scope)?;
6061
6062 validate_expression(predicate, vars_in_scope)?;
6064
6065 if let Expr::Variable(var_name) = predicate
6067 && let Some(info) = find_var_in_scope(vars_in_scope, var_name)
6068 && matches!(
6069 info.var_type,
6070 VariableType::Node | VariableType::Edge | VariableType::Path
6071 )
6072 {
6073 return Err(anyhow!(
6074 "SyntaxError: InvalidArgumentType - Type mismatch: expected Boolean but was {:?}",
6075 info.var_type
6076 ));
6077 }
6078
6079 let mut plan = plan;
6080
6081 let transformed_predicate = Self::transform_valid_at_to_function(predicate.clone());
6083
6084 let transformed_predicate = Self::rewrite_id_to_vid(transformed_predicate, vars_in_scope);
6087
6088 let mut current_predicate =
6089 self.rewrite_predicates_using_indexes(&transformed_predicate, &plan, vars_in_scope)?;
6090
6091 if let Some(extraction) = extract_vector_similarity(¤t_predicate) {
6093 let vs = &extraction.predicate;
6094 if Self::find_scan_label_id(&plan, &vs.variable).is_some() {
6095 plan = Self::replace_scan_with_knn(
6096 plan,
6097 &vs.variable,
6098 &vs.property,
6099 vs.query.clone(),
6100 vs.threshold,
6101 );
6102 if let Some(residual) = extraction.residual {
6103 current_predicate = residual;
6104 } else {
6105 current_predicate = Expr::TRUE;
6106 }
6107 }
6108 }
6109
6110 let conjuncts = Self::split_and_conjuncts(¤t_predicate);
6121 let mut keep: Vec<Expr> = Vec::with_capacity(conjuncts.len());
6122 for conj in conjuncts {
6123 let mut consumed = false;
6124 for var in vars_in_scope {
6125 if optional_vars.contains(&var.name) {
6126 continue;
6127 }
6128 if Self::is_scan_all_for(&plan, &var.name)
6130 && let Some(labels) = try_label_or_to_union(&conj, &var.name)
6131 {
6132 plan = self.replace_scan_all_with_label_union(plan, &var.name, &labels, false);
6133 consumed = true;
6134 break;
6135 }
6136 if let Some(types) = try_type_or_to_union(&conj, &var.name)
6138 && Self::merge_traverse_types_for(&plan, &var.name, &types).is_some()
6139 {
6140 let mut ids: Vec<u32> = Vec::with_capacity(types.len());
6141 let mut all_known = true;
6142 for t in &types {
6143 match self.schema.edge_types.get(t) {
6144 Some(meta) => ids.push(meta.id),
6145 None => {
6146 all_known = false;
6147 break;
6148 }
6149 }
6150 }
6151 if all_known {
6152 plan = Self::set_traverse_edge_type_ids(plan, &var.name, ids);
6153 consumed = true;
6154 break;
6155 }
6156 }
6157 }
6158 if !consumed {
6159 keep.push(conj);
6160 }
6161 }
6162 current_predicate = Self::combine_predicates(keep).unwrap_or(Expr::TRUE);
6163
6164 for var in vars_in_scope {
6169 if optional_vars.contains(&var.name) {
6171 continue;
6172 }
6173
6174 if Self::find_scan_label_id(&plan, &var.name).is_some() {
6176 let (pushable, residual) =
6177 Self::extract_variable_predicates(¤t_predicate, &var.name);
6178
6179 for pred in pushable {
6180 plan = Self::push_predicate_to_scan(plan, &var.name, pred);
6181 }
6182
6183 if let Some(r) = residual {
6184 current_predicate = r;
6185 } else {
6186 current_predicate = Expr::TRUE;
6187 }
6188 } else if Self::is_traverse_target(&plan, &var.name) {
6189 let (pushable, residual) =
6191 Self::extract_variable_predicates(¤t_predicate, &var.name);
6192
6193 for pred in pushable {
6194 plan = Self::push_predicate_to_traverse(plan, &var.name, pred);
6195 }
6196
6197 if let Some(r) = residual {
6198 current_predicate = r;
6199 } else {
6200 current_predicate = Expr::TRUE;
6201 }
6202 }
6203 }
6204
6205 plan = Self::push_predicates_to_apply(plan, &mut current_predicate);
6208
6209 if !current_predicate.is_true_literal() {
6211 plan = LogicalPlan::Filter {
6212 input: Box::new(plan),
6213 predicate: current_predicate,
6214 optional_variables: optional_vars,
6215 };
6216 }
6217
6218 Ok(plan)
6219 }
6220
6221 fn rewrite_predicates_using_indexes(
6222 &self,
6223 predicate: &Expr,
6224 plan: &LogicalPlan,
6225 vars_in_scope: &[VariableInfo],
6226 ) -> Result<Expr> {
6227 let mut rewritten = predicate.clone();
6228
6229 for var in vars_in_scope {
6230 if let Some(label_id) = Self::find_scan_label_id(plan, &var.name) {
6231 let label_name = self.schema.label_name_by_id(label_id).map(str::to_owned);
6233
6234 if let Some(label) = label_name
6235 && let Some(props) = self.schema.properties.get(&label)
6236 {
6237 for (gen_col, meta) in props {
6238 if meta.generation_expression.is_some() {
6239 if let Some(schema_expr) =
6241 self.gen_expr_cache.get(&(label.clone(), gen_col.clone()))
6242 {
6243 rewritten = Self::replace_expression(
6245 rewritten,
6246 schema_expr,
6247 &var.name,
6248 gen_col,
6249 );
6250 }
6251 }
6252 }
6253 }
6254 }
6255 }
6256 Ok(rewritten)
6257 }
6258
6259 fn replace_expression(expr: Expr, schema_expr: &Expr, query_var: &str, gen_col: &str) -> Expr {
6260 let schema_var = schema_expr.extract_variable();
6262
6263 if let Some(s_var) = schema_var {
6264 let target_expr = schema_expr.substitute_variable(&s_var, query_var);
6265
6266 if expr == target_expr {
6267 return Expr::Property(
6268 Box::new(Expr::Variable(query_var.to_string())),
6269 gen_col.to_string(),
6270 );
6271 }
6272 }
6273
6274 match expr {
6276 Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
6277 left: Box::new(Self::replace_expression(
6278 *left,
6279 schema_expr,
6280 query_var,
6281 gen_col,
6282 )),
6283 op,
6284 right: Box::new(Self::replace_expression(
6285 *right,
6286 schema_expr,
6287 query_var,
6288 gen_col,
6289 )),
6290 },
6291 Expr::UnaryOp { op, expr } => Expr::UnaryOp {
6292 op,
6293 expr: Box::new(Self::replace_expression(
6294 *expr,
6295 schema_expr,
6296 query_var,
6297 gen_col,
6298 )),
6299 },
6300 Expr::FunctionCall {
6301 name,
6302 args,
6303 distinct,
6304 window_spec,
6305 } => Expr::FunctionCall {
6306 name,
6307 args: args
6308 .into_iter()
6309 .map(|a| Self::replace_expression(a, schema_expr, query_var, gen_col))
6310 .collect(),
6311 distinct,
6312 window_spec,
6313 },
6314 Expr::IsNull(expr) => Expr::IsNull(Box::new(Self::replace_expression(
6315 *expr,
6316 schema_expr,
6317 query_var,
6318 gen_col,
6319 ))),
6320 Expr::IsNotNull(expr) => Expr::IsNotNull(Box::new(Self::replace_expression(
6321 *expr,
6322 schema_expr,
6323 query_var,
6324 gen_col,
6325 ))),
6326 Expr::IsUnique(expr) => Expr::IsUnique(Box::new(Self::replace_expression(
6327 *expr,
6328 schema_expr,
6329 query_var,
6330 gen_col,
6331 ))),
6332 Expr::ArrayIndex {
6333 array: e,
6334 index: idx,
6335 } => Expr::ArrayIndex {
6336 array: Box::new(Self::replace_expression(
6337 *e,
6338 schema_expr,
6339 query_var,
6340 gen_col,
6341 )),
6342 index: Box::new(Self::replace_expression(
6343 *idx,
6344 schema_expr,
6345 query_var,
6346 gen_col,
6347 )),
6348 },
6349 Expr::ArraySlice { array, start, end } => Expr::ArraySlice {
6350 array: Box::new(Self::replace_expression(
6351 *array,
6352 schema_expr,
6353 query_var,
6354 gen_col,
6355 )),
6356 start: start.map(|s| {
6357 Box::new(Self::replace_expression(
6358 *s,
6359 schema_expr,
6360 query_var,
6361 gen_col,
6362 ))
6363 }),
6364 end: end.map(|e| {
6365 Box::new(Self::replace_expression(
6366 *e,
6367 schema_expr,
6368 query_var,
6369 gen_col,
6370 ))
6371 }),
6372 },
6373 Expr::List(exprs) => Expr::List(
6374 exprs
6375 .into_iter()
6376 .map(|e| Self::replace_expression(e, schema_expr, query_var, gen_col))
6377 .collect(),
6378 ),
6379 Expr::Map(entries) => Expr::Map(
6380 entries
6381 .into_iter()
6382 .map(|(k, v)| {
6383 (
6384 k,
6385 Self::replace_expression(v, schema_expr, query_var, gen_col),
6386 )
6387 })
6388 .collect(),
6389 ),
6390 Expr::Property(e, prop) => Expr::Property(
6391 Box::new(Self::replace_expression(
6392 *e,
6393 schema_expr,
6394 query_var,
6395 gen_col,
6396 )),
6397 prop,
6398 ),
6399 Expr::Case {
6400 expr: case_expr,
6401 when_then,
6402 else_expr,
6403 } => Expr::Case {
6404 expr: case_expr.map(|e| {
6405 Box::new(Self::replace_expression(
6406 *e,
6407 schema_expr,
6408 query_var,
6409 gen_col,
6410 ))
6411 }),
6412 when_then: when_then
6413 .into_iter()
6414 .map(|(w, t)| {
6415 (
6416 Self::replace_expression(w, schema_expr, query_var, gen_col),
6417 Self::replace_expression(t, schema_expr, query_var, gen_col),
6418 )
6419 })
6420 .collect(),
6421 else_expr: else_expr.map(|e| {
6422 Box::new(Self::replace_expression(
6423 *e,
6424 schema_expr,
6425 query_var,
6426 gen_col,
6427 ))
6428 }),
6429 },
6430 Expr::Reduce {
6431 accumulator,
6432 init,
6433 variable: reduce_var,
6434 list,
6435 expr: reduce_expr,
6436 } => Expr::Reduce {
6437 accumulator,
6438 init: Box::new(Self::replace_expression(
6439 *init,
6440 schema_expr,
6441 query_var,
6442 gen_col,
6443 )),
6444 variable: reduce_var,
6445 list: Box::new(Self::replace_expression(
6446 *list,
6447 schema_expr,
6448 query_var,
6449 gen_col,
6450 )),
6451 expr: Box::new(Self::replace_expression(
6452 *reduce_expr,
6453 schema_expr,
6454 query_var,
6455 gen_col,
6456 )),
6457 },
6458
6459 _ => expr,
6461 }
6462 }
6463
6464 fn is_scan_all_for(plan: &LogicalPlan, variable: &str) -> bool {
6470 match plan {
6471 LogicalPlan::ScanAll { variable: var, .. } => var == variable,
6472 LogicalPlan::Filter { input, .. }
6473 | LogicalPlan::Project { input, .. }
6474 | LogicalPlan::Sort { input, .. }
6475 | LogicalPlan::Limit { input, .. }
6476 | LogicalPlan::Aggregate { input, .. }
6477 | LogicalPlan::Apply { input, .. }
6478 | LogicalPlan::Traverse { input, .. } => Self::is_scan_all_for(input, variable),
6479 LogicalPlan::CrossJoin { left, right } => {
6480 Self::is_scan_all_for(left, variable) || Self::is_scan_all_for(right, variable)
6481 }
6482 LogicalPlan::Union { left, right, .. } => {
6483 Self::is_scan_all_for(left, variable) || Self::is_scan_all_for(right, variable)
6484 }
6485 _ => false,
6486 }
6487 }
6488
6489 fn replace_scan_all_with_label_union(
6494 &self,
6495 plan: LogicalPlan,
6496 variable: &str,
6497 labels: &[String],
6498 optional: bool,
6499 ) -> LogicalPlan {
6500 match plan {
6501 LogicalPlan::ScanAll {
6502 variable: var,
6503 filter,
6504 optional: scan_optional,
6505 } if var == variable => {
6506 let use_main_table_branches = !self.label_branches_share_property_schema(labels);
6512
6513 let mut branches: Vec<LogicalPlan> = Vec::with_capacity(labels.len());
6514 for label in labels {
6515 let branch = if use_main_table_branches {
6516 LogicalPlan::ScanMainByLabels {
6517 labels: vec![label.clone()],
6518 variable: variable.to_string(),
6519 filter: filter.clone(),
6520 optional: scan_optional || optional,
6521 }
6522 } else {
6523 let meta = self
6524 .schema
6525 .get_label_case_insensitive(label)
6526 .expect("share_property_schema true implies all labels in schema");
6527 LogicalPlan::Scan {
6528 label_id: meta.id,
6529 labels: vec![label.clone()],
6530 variable: variable.to_string(),
6531 filter: filter.clone(),
6532 optional: scan_optional || optional,
6533 }
6534 };
6535 branches.push(branch);
6536 }
6537 let mut iter = branches.into_iter();
6538 let mut union_plan = iter.next().expect("at least one label");
6539 for next in iter {
6540 union_plan = LogicalPlan::Union {
6541 left: Box::new(union_plan),
6542 right: Box::new(next),
6543 all: false,
6544 };
6545 }
6546 union_plan
6547 }
6548 LogicalPlan::Filter {
6549 input,
6550 predicate,
6551 optional_variables,
6552 } => LogicalPlan::Filter {
6553 input: Box::new(
6554 self.replace_scan_all_with_label_union(*input, variable, labels, optional),
6555 ),
6556 predicate,
6557 optional_variables,
6558 },
6559 LogicalPlan::Project { input, projections } => LogicalPlan::Project {
6560 input: Box::new(
6561 self.replace_scan_all_with_label_union(*input, variable, labels, optional),
6562 ),
6563 projections,
6564 },
6565 LogicalPlan::CrossJoin { left, right } => {
6566 if Self::is_scan_all_for(&left, variable) {
6567 LogicalPlan::CrossJoin {
6568 left: Box::new(
6569 self.replace_scan_all_with_label_union(
6570 *left, variable, labels, optional,
6571 ),
6572 ),
6573 right,
6574 }
6575 } else {
6576 LogicalPlan::CrossJoin {
6577 left,
6578 right: Box::new(
6579 self.replace_scan_all_with_label_union(
6580 *right, variable, labels, optional,
6581 ),
6582 ),
6583 }
6584 }
6585 }
6586 LogicalPlan::Traverse {
6587 input,
6588 edge_type_ids,
6589 direction,
6590 source_variable,
6591 target_variable,
6592 target_label_id,
6593 step_variable,
6594 min_hops,
6595 max_hops,
6596 optional: trav_optional,
6597 target_filter,
6598 path_variable,
6599 edge_properties,
6600 is_variable_length,
6601 optional_pattern_vars,
6602 scope_match_variables,
6603 edge_filter_expr,
6604 path_mode,
6605 qpp_steps,
6606 } => LogicalPlan::Traverse {
6607 input: Box::new(
6608 self.replace_scan_all_with_label_union(*input, variable, labels, optional),
6609 ),
6610 edge_type_ids,
6611 direction,
6612 source_variable,
6613 target_variable,
6614 target_label_id,
6615 step_variable,
6616 min_hops,
6617 max_hops,
6618 optional: trav_optional,
6619 target_filter,
6620 path_variable,
6621 edge_properties,
6622 is_variable_length,
6623 optional_pattern_vars,
6624 scope_match_variables,
6625 edge_filter_expr,
6626 path_mode,
6627 qpp_steps,
6628 },
6629 other => other,
6630 }
6631 }
6632
6633 fn merge_traverse_types_for(
6638 plan: &LogicalPlan,
6639 edge_var: &str,
6640 _types: &[String],
6641 ) -> Option<()> {
6642 match plan {
6643 LogicalPlan::Traverse {
6644 step_variable,
6645 input,
6646 ..
6647 } => {
6648 if step_variable.as_deref() == Some(edge_var) {
6649 Some(())
6650 } else {
6651 Self::merge_traverse_types_for(input, edge_var, _types)
6652 }
6653 }
6654 LogicalPlan::Filter { input, .. }
6655 | LogicalPlan::Project { input, .. }
6656 | LogicalPlan::Sort { input, .. }
6657 | LogicalPlan::Limit { input, .. }
6658 | LogicalPlan::Aggregate { input, .. }
6659 | LogicalPlan::Apply { input, .. } => {
6660 Self::merge_traverse_types_for(input, edge_var, _types)
6661 }
6662 LogicalPlan::CrossJoin { left, right } | LogicalPlan::Union { left, right, .. } => {
6663 Self::merge_traverse_types_for(left, edge_var, _types)
6664 .or_else(|| Self::merge_traverse_types_for(right, edge_var, _types))
6665 }
6666 _ => None,
6667 }
6668 }
6669
6670 fn set_traverse_edge_type_ids(
6673 plan: LogicalPlan,
6674 edge_var: &str,
6675 new_ids: Vec<u32>,
6676 ) -> LogicalPlan {
6677 match plan {
6678 LogicalPlan::Traverse {
6679 input,
6680 edge_type_ids,
6681 direction,
6682 source_variable,
6683 target_variable,
6684 target_label_id,
6685 step_variable,
6686 min_hops,
6687 max_hops,
6688 optional,
6689 target_filter,
6690 path_variable,
6691 edge_properties,
6692 is_variable_length,
6693 optional_pattern_vars,
6694 scope_match_variables,
6695 edge_filter_expr,
6696 path_mode,
6697 qpp_steps,
6698 } => {
6699 let matches_var = step_variable.as_deref() == Some(edge_var);
6700 let recursed_input = if matches_var {
6701 input
6702 } else {
6703 Box::new(Self::set_traverse_edge_type_ids(
6704 *input,
6705 edge_var,
6706 new_ids.clone(),
6707 ))
6708 };
6709 LogicalPlan::Traverse {
6710 input: recursed_input,
6711 edge_type_ids: if matches_var { new_ids } else { edge_type_ids },
6712 direction,
6713 source_variable,
6714 target_variable,
6715 target_label_id,
6716 step_variable,
6717 min_hops,
6718 max_hops,
6719 optional,
6720 target_filter,
6721 path_variable,
6722 edge_properties,
6723 is_variable_length,
6724 optional_pattern_vars,
6725 scope_match_variables,
6726 edge_filter_expr,
6727 path_mode,
6728 qpp_steps,
6729 }
6730 }
6731 LogicalPlan::Filter {
6732 input,
6733 predicate,
6734 optional_variables,
6735 } => LogicalPlan::Filter {
6736 input: Box::new(Self::set_traverse_edge_type_ids(*input, edge_var, new_ids)),
6737 predicate,
6738 optional_variables,
6739 },
6740 LogicalPlan::Project { input, projections } => LogicalPlan::Project {
6741 input: Box::new(Self::set_traverse_edge_type_ids(*input, edge_var, new_ids)),
6742 projections,
6743 },
6744 LogicalPlan::CrossJoin { left, right } => LogicalPlan::CrossJoin {
6745 left: Box::new(Self::set_traverse_edge_type_ids(
6746 *left,
6747 edge_var,
6748 new_ids.clone(),
6749 )),
6750 right: Box::new(Self::set_traverse_edge_type_ids(*right, edge_var, new_ids)),
6751 },
6752 other => other,
6753 }
6754 }
6755
6756 fn is_traverse_target(plan: &LogicalPlan, variable: &str) -> bool {
6758 match plan {
6759 LogicalPlan::Traverse {
6760 target_variable,
6761 input,
6762 ..
6763 } => target_variable == variable || Self::is_traverse_target(input, variable),
6764 LogicalPlan::Filter { input, .. }
6765 | LogicalPlan::Project { input, .. }
6766 | LogicalPlan::Sort { input, .. }
6767 | LogicalPlan::Limit { input, .. }
6768 | LogicalPlan::Aggregate { input, .. }
6769 | LogicalPlan::Apply { input, .. } => Self::is_traverse_target(input, variable),
6770 LogicalPlan::CrossJoin { left, right } => {
6771 Self::is_traverse_target(left, variable)
6772 || Self::is_traverse_target(right, variable)
6773 }
6774 _ => false,
6775 }
6776 }
6777
6778 fn push_predicate_to_traverse(
6780 plan: LogicalPlan,
6781 variable: &str,
6782 predicate: Expr,
6783 ) -> LogicalPlan {
6784 match plan {
6785 LogicalPlan::Traverse {
6786 input,
6787 edge_type_ids,
6788 direction,
6789 source_variable,
6790 target_variable,
6791 target_label_id,
6792 step_variable,
6793 min_hops,
6794 max_hops,
6795 optional,
6796 target_filter,
6797 path_variable,
6798 edge_properties,
6799 is_variable_length,
6800 optional_pattern_vars,
6801 scope_match_variables,
6802 edge_filter_expr,
6803 path_mode,
6804 qpp_steps,
6805 } => {
6806 if target_variable == variable {
6807 let new_filter = match target_filter {
6809 Some(existing) => Some(Expr::BinaryOp {
6810 left: Box::new(existing),
6811 op: BinaryOp::And,
6812 right: Box::new(predicate),
6813 }),
6814 None => Some(predicate),
6815 };
6816 LogicalPlan::Traverse {
6817 input,
6818 edge_type_ids,
6819 direction,
6820 source_variable,
6821 target_variable,
6822 target_label_id,
6823 step_variable,
6824 min_hops,
6825 max_hops,
6826 optional,
6827 target_filter: new_filter,
6828 path_variable,
6829 edge_properties,
6830 is_variable_length,
6831 optional_pattern_vars,
6832 scope_match_variables,
6833 edge_filter_expr,
6834 path_mode,
6835 qpp_steps,
6836 }
6837 } else {
6838 LogicalPlan::Traverse {
6840 input: Box::new(Self::push_predicate_to_traverse(
6841 *input, variable, predicate,
6842 )),
6843 edge_type_ids,
6844 direction,
6845 source_variable,
6846 target_variable,
6847 target_label_id,
6848 step_variable,
6849 min_hops,
6850 max_hops,
6851 optional,
6852 target_filter,
6853 path_variable,
6854 edge_properties,
6855 is_variable_length,
6856 optional_pattern_vars,
6857 scope_match_variables,
6858 edge_filter_expr,
6859 path_mode,
6860 qpp_steps,
6861 }
6862 }
6863 }
6864 LogicalPlan::Filter {
6865 input,
6866 predicate: p,
6867 optional_variables: opt_vars,
6868 } => LogicalPlan::Filter {
6869 input: Box::new(Self::push_predicate_to_traverse(
6870 *input, variable, predicate,
6871 )),
6872 predicate: p,
6873 optional_variables: opt_vars,
6874 },
6875 LogicalPlan::Project { input, projections } => LogicalPlan::Project {
6876 input: Box::new(Self::push_predicate_to_traverse(
6877 *input, variable, predicate,
6878 )),
6879 projections,
6880 },
6881 LogicalPlan::CrossJoin { left, right } => {
6882 if Self::is_traverse_target(&left, variable) {
6884 LogicalPlan::CrossJoin {
6885 left: Box::new(Self::push_predicate_to_traverse(
6886 *left, variable, predicate,
6887 )),
6888 right,
6889 }
6890 } else {
6891 LogicalPlan::CrossJoin {
6892 left,
6893 right: Box::new(Self::push_predicate_to_traverse(
6894 *right, variable, predicate,
6895 )),
6896 }
6897 }
6898 }
6899 other => other,
6900 }
6901 }
6902
6903 fn plan_with_clause(
6905 &self,
6906 with_clause: &WithClause,
6907 plan: LogicalPlan,
6908 vars_in_scope: &[VariableInfo],
6909 ) -> Result<(LogicalPlan, Vec<VariableInfo>)> {
6910 let mut plan = plan;
6911 let mut group_by: Vec<Expr> = Vec::new();
6912 let mut aggregates: Vec<Expr> = Vec::new();
6913 let mut compound_agg_exprs: Vec<Expr> = Vec::new();
6914 let mut has_agg = false;
6915 let mut projections = Vec::new();
6916 let mut new_vars: Vec<VariableInfo> = Vec::new();
6917 let mut projected_aggregate_reprs: HashSet<String> = HashSet::new();
6918 let mut projected_simple_reprs: HashSet<String> = HashSet::new();
6919 let mut projected_aliases: HashSet<String> = HashSet::new();
6920 let mut has_unaliased_non_variable_expr = false;
6921
6922 for item in &with_clause.items {
6923 match item {
6924 ReturnItem::All => {
6925 for v in vars_in_scope {
6927 projections.push((Expr::Variable(v.name.clone()), Some(v.name.clone())));
6928 projected_aliases.insert(v.name.clone());
6929 projected_simple_reprs.insert(v.name.clone());
6930 }
6931 new_vars.extend(vars_in_scope.iter().cloned());
6932 }
6933 ReturnItem::Expr { expr, alias, .. } => {
6934 if matches!(expr, Expr::Wildcard) {
6935 for v in vars_in_scope {
6936 projections
6937 .push((Expr::Variable(v.name.clone()), Some(v.name.clone())));
6938 projected_aliases.insert(v.name.clone());
6939 projected_simple_reprs.insert(v.name.clone());
6940 }
6941 new_vars.extend(vars_in_scope.iter().cloned());
6942 } else {
6943 validate_expression_variables(expr, vars_in_scope)?;
6945 validate_expression(expr, vars_in_scope)?;
6946 if contains_pattern_predicate(expr) {
6948 return Err(anyhow!(
6949 "SyntaxError: UnexpectedSyntax - Pattern predicates are not allowed in WITH"
6950 ));
6951 }
6952
6953 projections.push((expr.clone(), alias.clone()));
6954 if expr.is_aggregate() && !is_compound_aggregate(expr) {
6955 has_agg = true;
6957 aggregates.push(expr.clone());
6958 projected_aggregate_reprs.insert(expr.to_string_repr());
6959 } else if !is_window_function(expr)
6960 && (expr.is_aggregate() || contains_aggregate_recursive(expr))
6961 {
6962 has_agg = true;
6964 compound_agg_exprs.push(expr.clone());
6965 for inner in extract_inner_aggregates(expr) {
6966 let repr = inner.to_string_repr();
6967 if !projected_aggregate_reprs.contains(&repr) {
6968 aggregates.push(inner);
6969 projected_aggregate_reprs.insert(repr);
6970 }
6971 }
6972 } else if !group_by.contains(expr) {
6973 group_by.push(expr.clone());
6974 if matches!(expr, Expr::Variable(_) | Expr::Property(_, _)) {
6975 projected_simple_reprs.insert(expr.to_string_repr());
6976 }
6977 }
6978
6979 if let Some(a) = alias {
6982 if projected_aliases.contains(a) {
6983 return Err(anyhow!(
6984 "SyntaxError: ColumnNameConflict - Duplicate column name '{}' in WITH",
6985 a
6986 ));
6987 }
6988 let inferred = infer_with_output_type(expr, vars_in_scope);
6989 new_vars.push(VariableInfo::new(a.clone(), inferred));
6990 projected_aliases.insert(a.clone());
6991 } else if let Expr::Variable(v) = expr {
6992 if projected_aliases.contains(v) {
6993 return Err(anyhow!(
6994 "SyntaxError: ColumnNameConflict - Duplicate column name '{}' in WITH",
6995 v
6996 ));
6997 }
6998 if let Some(existing) = find_var_in_scope(vars_in_scope, v) {
7000 new_vars.push(existing.clone());
7001 } else {
7002 new_vars.push(VariableInfo::new(v.clone(), VariableType::Scalar));
7003 }
7004 projected_aliases.insert(v.clone());
7005 } else {
7006 has_unaliased_non_variable_expr = true;
7007 }
7008 }
7009 }
7010 }
7011 }
7012
7013 let projected_names: HashSet<&str> = new_vars.iter().map(|v| v.name.as_str()).collect();
7016 let mut passthrough_extras: Vec<String> = Vec::new();
7017 let mut seen_passthrough: HashSet<String> = HashSet::new();
7018
7019 if let Some(predicate) = &with_clause.where_clause {
7020 for name in collect_expr_variables(predicate) {
7021 if !projected_names.contains(name.as_str())
7022 && find_var_in_scope(vars_in_scope, &name).is_some()
7023 && seen_passthrough.insert(name.clone())
7024 {
7025 passthrough_extras.push(name);
7026 }
7027 }
7028 }
7029
7030 if !has_agg && let Some(order_by) = &with_clause.order_by {
7033 for item in order_by {
7034 for name in collect_expr_variables(&item.expr) {
7035 if !projected_names.contains(name.as_str())
7036 && find_var_in_scope(vars_in_scope, &name).is_some()
7037 && seen_passthrough.insert(name.clone())
7038 {
7039 passthrough_extras.push(name);
7040 }
7041 }
7042 }
7043 }
7044
7045 let needs_cleanup = !passthrough_extras.is_empty();
7046 for extra in &passthrough_extras {
7047 projections.push((Expr::Variable(extra.clone()), Some(extra.clone())));
7048 }
7049
7050 if has_agg {
7053 let group_by_reprs: HashSet<String> =
7054 group_by.iter().map(|e| e.to_string_repr()).collect();
7055 for expr in &compound_agg_exprs {
7056 let mut refs = Vec::new();
7057 collect_non_aggregate_refs(expr, false, &mut refs);
7058 for r in &refs {
7059 let is_covered = match r {
7060 NonAggregateRef::Var(v) => group_by_reprs.contains(v),
7061 NonAggregateRef::Property { repr, .. } => group_by_reprs.contains(repr),
7062 };
7063 if !is_covered {
7064 return Err(anyhow!(
7065 "SyntaxError: AmbiguousAggregationExpression - Expression mixes aggregation with non-grouped reference"
7066 ));
7067 }
7068 }
7069 }
7070 }
7071
7072 if has_agg {
7073 plan = LogicalPlan::Aggregate {
7074 input: Box::new(plan),
7075 group_by,
7076 aggregates,
7077 };
7078
7079 let rename_projections: Vec<(Expr, Option<String>)> = projections
7082 .iter()
7083 .map(|(expr, alias)| {
7084 if expr.is_aggregate() && !is_compound_aggregate(expr) {
7085 (Expr::Variable(aggregate_column_name(expr)), alias.clone())
7087 } else if is_compound_aggregate(expr)
7088 || (!expr.is_aggregate() && contains_aggregate_recursive(expr))
7089 {
7090 (replace_aggregates_with_columns(expr), alias.clone())
7093 } else {
7094 (Expr::Variable(expr.to_string_repr()), alias.clone())
7095 }
7096 })
7097 .collect();
7098 plan = LogicalPlan::Project {
7099 input: Box::new(plan),
7100 projections: rename_projections,
7101 };
7102 } else if !projections.is_empty() {
7103 plan = LogicalPlan::Project {
7104 input: Box::new(plan),
7105 projections: projections.clone(),
7106 };
7107 }
7108
7109 if let Some(predicate) = &with_clause.where_clause {
7111 plan = LogicalPlan::Filter {
7112 input: Box::new(plan),
7113 predicate: predicate.clone(),
7114 optional_variables: HashSet::new(),
7115 };
7116 }
7117
7118 if let Some(order_by) = &with_clause.order_by {
7122 let with_order_aliases: HashMap<String, Expr> = projections
7125 .iter()
7126 .flat_map(|(expr, alias)| {
7127 let output_col = if let Some(a) = alias {
7128 a.clone()
7129 } else if expr.is_aggregate() && !is_compound_aggregate(expr) {
7130 aggregate_column_name(expr)
7131 } else {
7132 expr.to_string_repr()
7133 };
7134
7135 let mut entries = Vec::new();
7136 if let Some(a) = alias {
7138 entries.push((a.clone(), Expr::Variable(output_col.clone())));
7139 }
7140 entries.push((expr.to_string_repr(), Expr::Variable(output_col)));
7142 entries
7143 })
7144 .collect();
7145
7146 let order_by_scope: Vec<VariableInfo> = {
7147 let mut scope = new_vars.clone();
7148 for v in vars_in_scope {
7149 if !is_var_in_scope(&scope, &v.name) {
7150 scope.push(v.clone());
7151 }
7152 }
7153 scope
7154 };
7155 for item in order_by {
7156 validate_expression_variables(&item.expr, &order_by_scope)?;
7157 validate_expression(&item.expr, &order_by_scope)?;
7158 let has_aggregate_in_item = contains_aggregate_recursive(&item.expr);
7159 if has_aggregate_in_item && !has_agg {
7160 return Err(anyhow!(
7161 "SyntaxError: InvalidAggregation - Aggregation functions not allowed in ORDER BY of WITH"
7162 ));
7163 }
7164 if has_agg && has_aggregate_in_item {
7165 validate_with_order_by_aggregate_item(
7166 &item.expr,
7167 &projected_aggregate_reprs,
7168 &projected_simple_reprs,
7169 &projected_aliases,
7170 )?;
7171 }
7172 }
7173 let rewritten_order_by: Vec<SortItem> = order_by
7174 .iter()
7175 .map(|item| {
7176 let mut expr =
7177 rewrite_order_by_expr_with_aliases(&item.expr, &with_order_aliases);
7178 if has_agg {
7179 expr = replace_aggregates_with_columns(&expr);
7182 expr = rewrite_order_by_expr_with_aliases(&expr, &with_order_aliases);
7185 }
7186 SortItem {
7187 expr,
7188 ascending: item.ascending,
7189 }
7190 })
7191 .collect();
7192 plan = LogicalPlan::Sort {
7193 input: Box::new(plan),
7194 order_by: rewritten_order_by,
7195 };
7196 }
7197
7198 if has_unaliased_non_variable_expr {
7203 return Err(anyhow!(
7204 "SyntaxError: NoExpressionAlias - All non-variable expressions in WITH must be aliased"
7205 ));
7206 }
7207
7208 let skip = with_clause
7210 .skip
7211 .as_ref()
7212 .map(|e| {
7213 self.note_folded_limit_skip(e);
7214 parse_non_negative_integer(e, "SKIP", &self.params)
7215 })
7216 .transpose()?
7217 .flatten();
7218 let fetch = with_clause
7219 .limit
7220 .as_ref()
7221 .map(|e| {
7222 self.note_folded_limit_skip(e);
7223 parse_non_negative_integer(e, "LIMIT", &self.params)
7224 })
7225 .transpose()?
7226 .flatten();
7227
7228 if skip.is_some() || fetch.is_some() {
7229 plan = LogicalPlan::Limit {
7230 input: Box::new(plan),
7231 skip,
7232 fetch,
7233 };
7234 }
7235
7236 if needs_cleanup {
7238 let cleanup_projections: Vec<(Expr, Option<String>)> = new_vars
7239 .iter()
7240 .map(|v| (Expr::Variable(v.name.clone()), Some(v.name.clone())))
7241 .collect();
7242 plan = LogicalPlan::Project {
7243 input: Box::new(plan),
7244 projections: cleanup_projections,
7245 };
7246 }
7247
7248 if with_clause.distinct {
7249 plan = LogicalPlan::Distinct {
7250 input: Box::new(plan),
7251 };
7252 }
7253
7254 Ok((plan, new_vars))
7255 }
7256
7257 fn plan_with_recursive(
7258 &self,
7259 with_recursive: &WithRecursiveClause,
7260 _prev_plan: LogicalPlan,
7261 vars_in_scope: &[VariableInfo],
7262 ) -> Result<LogicalPlan> {
7263 match &*with_recursive.query {
7265 Query::Union { left, right, .. } => {
7266 let initial_plan = self.rewrite_and_plan_typed(*left.clone(), vars_in_scope)?;
7268
7269 let mut recursive_scope = vars_in_scope.to_vec();
7272 recursive_scope.push(VariableInfo::new(
7273 with_recursive.name.clone(),
7274 VariableType::Scalar,
7275 ));
7276 let recursive_plan =
7277 self.rewrite_and_plan_typed(*right.clone(), &recursive_scope)?;
7278
7279 Ok(LogicalPlan::RecursiveCTE {
7280 cte_name: with_recursive.name.clone(),
7281 initial: Box::new(initial_plan),
7282 recursive: Box::new(recursive_plan),
7283 })
7284 }
7285 _ => Err(anyhow::anyhow!(
7286 "WITH RECURSIVE requires a UNION query with anchor and recursive parts"
7287 )),
7288 }
7289 }
7290
7291 pub fn properties_to_expr(&self, variable: &str, properties: &Option<Expr>) -> Option<Expr> {
7292 let entries = match properties {
7293 Some(Expr::Map(entries)) => entries,
7294 _ => return None,
7295 };
7296
7297 if entries.is_empty() {
7298 return None;
7299 }
7300 let mut final_expr = None;
7301 for (prop, val_expr) in entries {
7302 let eq_expr = Expr::BinaryOp {
7303 left: Box::new(Expr::Property(
7304 Box::new(Expr::Variable(variable.to_string())),
7305 prop.clone(),
7306 )),
7307 op: BinaryOp::Eq,
7308 right: Box::new(val_expr.clone()),
7309 };
7310
7311 if let Some(e) = final_expr {
7312 final_expr = Some(Expr::BinaryOp {
7313 left: Box::new(e),
7314 op: BinaryOp::And,
7315 right: Box::new(eq_expr),
7316 });
7317 } else {
7318 final_expr = Some(eq_expr);
7319 }
7320 }
7321 final_expr
7322 }
7323
7324 pub fn node_filter_expr(
7329 &self,
7330 variable: &str,
7331 labels: &[String],
7332 properties: &Option<Expr>,
7333 ) -> Option<Expr> {
7334 let mut final_expr = None;
7335
7336 for label in labels {
7338 let label_check = Expr::FunctionCall {
7339 name: "hasLabel".to_string(),
7340 args: vec![
7341 Expr::Variable(variable.to_string()),
7342 Expr::Literal(CypherLiteral::String(label.clone())),
7343 ],
7344 distinct: false,
7345 window_spec: None,
7346 };
7347
7348 final_expr = match final_expr {
7349 Some(e) => Some(Expr::BinaryOp {
7350 left: Box::new(e),
7351 op: BinaryOp::And,
7352 right: Box::new(label_check),
7353 }),
7354 None => Some(label_check),
7355 };
7356 }
7357
7358 if let Some(prop_expr) = self.properties_to_expr(variable, properties) {
7360 final_expr = match final_expr {
7361 Some(e) => Some(Expr::BinaryOp {
7362 left: Box::new(e),
7363 op: BinaryOp::And,
7364 right: Box::new(prop_expr),
7365 }),
7366 None => Some(prop_expr),
7367 };
7368 }
7369
7370 final_expr
7371 }
7372
7373 fn wrap_with_bound_target_filter(plan: LogicalPlan, target_variable: &str) -> LogicalPlan {
7378 let bound_check = Expr::BinaryOp {
7384 left: Box::new(Expr::Property(
7385 Box::new(Expr::Variable(target_variable.to_string())),
7386 "_vid".to_string(),
7387 )),
7388 op: BinaryOp::Eq,
7389 right: Box::new(Expr::Variable(format!("{}._vid", target_variable))),
7390 };
7391 LogicalPlan::Filter {
7392 input: Box::new(plan),
7393 predicate: bound_check,
7394 optional_variables: HashSet::new(),
7395 }
7396 }
7397
7398 fn replace_scan_with_knn(
7400 plan: LogicalPlan,
7401 variable: &str,
7402 property: &str,
7403 query: Expr,
7404 threshold: Option<f32>,
7405 ) -> LogicalPlan {
7406 match plan {
7407 LogicalPlan::Scan {
7408 label_id,
7409 labels,
7410 variable: scan_var,
7411 filter,
7412 optional,
7413 } => {
7414 if scan_var == variable {
7415 let knn = LogicalPlan::VectorKnn {
7423 label_id,
7424 variable: variable.to_string(),
7425 property: property.to_string(),
7426 query,
7427 k: 100, threshold,
7429 };
7430
7431 if let Some(f) = filter {
7432 LogicalPlan::Filter {
7433 input: Box::new(knn),
7434 predicate: f,
7435 optional_variables: HashSet::new(),
7436 }
7437 } else {
7438 knn
7439 }
7440 } else {
7441 LogicalPlan::Scan {
7442 label_id,
7443 labels,
7444 variable: scan_var,
7445 filter,
7446 optional,
7447 }
7448 }
7449 }
7450 LogicalPlan::Filter {
7451 input,
7452 predicate,
7453 optional_variables,
7454 } => LogicalPlan::Filter {
7455 input: Box::new(Self::replace_scan_with_knn(
7456 *input, variable, property, query, threshold,
7457 )),
7458 predicate,
7459 optional_variables,
7460 },
7461 LogicalPlan::Project { input, projections } => LogicalPlan::Project {
7462 input: Box::new(Self::replace_scan_with_knn(
7463 *input, variable, property, query, threshold,
7464 )),
7465 projections,
7466 },
7467 LogicalPlan::Limit { input, skip, fetch } => {
7468 LogicalPlan::Limit {
7473 input: Box::new(Self::replace_scan_with_knn(
7474 *input, variable, property, query, threshold,
7475 )),
7476 skip,
7477 fetch,
7478 }
7479 }
7480 LogicalPlan::CrossJoin { left, right } => LogicalPlan::CrossJoin {
7481 left: Box::new(Self::replace_scan_with_knn(
7482 *left,
7483 variable,
7484 property,
7485 query.clone(),
7486 threshold,
7487 )),
7488 right: Box::new(Self::replace_scan_with_knn(
7489 *right, variable, property, query, threshold,
7490 )),
7491 },
7492 other => other,
7493 }
7494 }
7495
7496 fn find_scan_label_id(plan: &LogicalPlan, variable: &str) -> Option<u16> {
7498 match plan {
7499 LogicalPlan::Scan {
7500 label_id,
7501 variable: var,
7502 ..
7503 } if var == variable => Some(*label_id),
7504 LogicalPlan::ScanAll { variable: var, .. } if var == variable => Some(0),
7505 LogicalPlan::Filter { input, .. }
7506 | LogicalPlan::Project { input, .. }
7507 | LogicalPlan::Sort { input, .. }
7508 | LogicalPlan::Limit { input, .. }
7509 | LogicalPlan::Aggregate { input, .. }
7510 | LogicalPlan::Apply { input, .. } => Self::find_scan_label_id(input, variable),
7511 LogicalPlan::CrossJoin { left, right } => Self::find_scan_label_id(left, variable)
7512 .or_else(|| Self::find_scan_label_id(right, variable)),
7513 LogicalPlan::Traverse { input, .. } => Self::find_scan_label_id(input, variable),
7514 _ => None,
7515 }
7516 }
7517
7518 fn push_predicate_to_scan(plan: LogicalPlan, variable: &str, predicate: Expr) -> LogicalPlan {
7520 match plan {
7521 LogicalPlan::Scan {
7522 label_id,
7523 labels,
7524 variable: var,
7525 filter,
7526 optional,
7527 } if var == variable => {
7528 let new_filter = match filter {
7530 Some(existing) => Some(Expr::BinaryOp {
7531 left: Box::new(existing),
7532 op: BinaryOp::And,
7533 right: Box::new(predicate),
7534 }),
7535 None => Some(predicate),
7536 };
7537 LogicalPlan::Scan {
7538 label_id,
7539 labels,
7540 variable: var,
7541 filter: new_filter,
7542 optional,
7543 }
7544 }
7545 LogicalPlan::ScanAll {
7546 variable: var,
7547 filter,
7548 optional,
7549 } if var == variable => {
7550 let new_filter = match filter {
7551 Some(existing) => Some(Expr::BinaryOp {
7552 left: Box::new(existing),
7553 op: BinaryOp::And,
7554 right: Box::new(predicate),
7555 }),
7556 None => Some(predicate),
7557 };
7558 LogicalPlan::ScanAll {
7559 variable: var,
7560 filter: new_filter,
7561 optional,
7562 }
7563 }
7564 LogicalPlan::Filter {
7565 input,
7566 predicate: p,
7567 optional_variables: opt_vars,
7568 } => LogicalPlan::Filter {
7569 input: Box::new(Self::push_predicate_to_scan(*input, variable, predicate)),
7570 predicate: p,
7571 optional_variables: opt_vars,
7572 },
7573 LogicalPlan::Project { input, projections } => LogicalPlan::Project {
7574 input: Box::new(Self::push_predicate_to_scan(*input, variable, predicate)),
7575 projections,
7576 },
7577 LogicalPlan::CrossJoin { left, right } => {
7578 if Self::find_scan_label_id(&left, variable).is_some() {
7580 LogicalPlan::CrossJoin {
7581 left: Box::new(Self::push_predicate_to_scan(*left, variable, predicate)),
7582 right,
7583 }
7584 } else {
7585 LogicalPlan::CrossJoin {
7586 left,
7587 right: Box::new(Self::push_predicate_to_scan(*right, variable, predicate)),
7588 }
7589 }
7590 }
7591 LogicalPlan::Traverse {
7592 input,
7593 edge_type_ids,
7594 direction,
7595 source_variable,
7596 target_variable,
7597 target_label_id,
7598 step_variable,
7599 min_hops,
7600 max_hops,
7601 optional,
7602 target_filter,
7603 path_variable,
7604 edge_properties,
7605 is_variable_length,
7606 optional_pattern_vars,
7607 scope_match_variables,
7608 edge_filter_expr,
7609 path_mode,
7610 qpp_steps,
7611 } => LogicalPlan::Traverse {
7612 input: Box::new(Self::push_predicate_to_scan(*input, variable, predicate)),
7613 edge_type_ids,
7614 direction,
7615 source_variable,
7616 target_variable,
7617 target_label_id,
7618 step_variable,
7619 min_hops,
7620 max_hops,
7621 optional,
7622 target_filter,
7623 path_variable,
7624 edge_properties,
7625 is_variable_length,
7626 optional_pattern_vars,
7627 scope_match_variables,
7628 edge_filter_expr,
7629 path_mode,
7630 qpp_steps,
7631 },
7632 other => other,
7633 }
7634 }
7635
7636 fn extract_variable_predicates(predicate: &Expr, variable: &str) -> (Vec<Expr>, Option<Expr>) {
7638 let analyzer = PredicateAnalyzer::new();
7639 let analysis = analyzer.analyze(predicate, variable);
7640
7641 let residual = if analysis.residual.is_empty() {
7643 None
7644 } else {
7645 let mut iter = analysis.residual.into_iter();
7646 let first = iter.next().unwrap();
7647 Some(iter.fold(first, |acc, e| Expr::BinaryOp {
7648 left: Box::new(acc),
7649 op: BinaryOp::And,
7650 right: Box::new(e),
7651 }))
7652 };
7653
7654 (analysis.pushable, residual)
7655 }
7656
7657 fn split_and_conjuncts(expr: &Expr) -> Vec<Expr> {
7663 match expr {
7664 Expr::BinaryOp {
7665 left,
7666 op: BinaryOp::And,
7667 right,
7668 } => {
7669 let mut result = Self::split_and_conjuncts(left);
7670 result.extend(Self::split_and_conjuncts(right));
7671 result
7672 }
7673 _ => vec![expr.clone()],
7674 }
7675 }
7676
7677 fn combine_predicates(predicates: Vec<Expr>) -> Option<Expr> {
7679 if predicates.is_empty() {
7680 return None;
7681 }
7682 let mut result = predicates[0].clone();
7683 for pred in predicates.iter().skip(1) {
7684 result = Expr::BinaryOp {
7685 left: Box::new(result),
7686 op: BinaryOp::And,
7687 right: Box::new(pred.clone()),
7688 };
7689 }
7690 Some(result)
7691 }
7692
7693 fn collect_expr_variables(expr: &Expr) -> HashSet<String> {
7695 let mut vars = HashSet::new();
7696 Self::collect_expr_variables_impl(expr, &mut vars);
7697 vars
7698 }
7699
7700 fn collect_expr_variables_impl(expr: &Expr, vars: &mut HashSet<String>) {
7701 match expr {
7702 Expr::Variable(name) => {
7703 vars.insert(name.clone());
7704 }
7705 Expr::Property(inner, _) => {
7706 if let Expr::Variable(name) = inner.as_ref() {
7707 vars.insert(name.clone());
7708 } else {
7709 Self::collect_expr_variables_impl(inner, vars);
7710 }
7711 }
7712 Expr::BinaryOp { left, right, .. } => {
7713 Self::collect_expr_variables_impl(left, vars);
7714 Self::collect_expr_variables_impl(right, vars);
7715 }
7716 Expr::UnaryOp { expr, .. } => Self::collect_expr_variables_impl(expr, vars),
7717 Expr::IsNull(e) | Expr::IsNotNull(e) => Self::collect_expr_variables_impl(e, vars),
7718 Expr::FunctionCall { args, .. } => {
7719 for arg in args {
7720 Self::collect_expr_variables_impl(arg, vars);
7721 }
7722 }
7723 Expr::List(items) => {
7724 for item in items {
7725 Self::collect_expr_variables_impl(item, vars);
7726 }
7727 }
7728 Expr::Case {
7729 expr,
7730 when_then,
7731 else_expr,
7732 } => {
7733 if let Some(e) = expr {
7734 Self::collect_expr_variables_impl(e, vars);
7735 }
7736 for (w, t) in when_then {
7737 Self::collect_expr_variables_impl(w, vars);
7738 Self::collect_expr_variables_impl(t, vars);
7739 }
7740 if let Some(e) = else_expr {
7741 Self::collect_expr_variables_impl(e, vars);
7742 }
7743 }
7744 Expr::LabelCheck { expr, .. } => Self::collect_expr_variables_impl(expr, vars),
7745 _ => {}
7748 }
7749 }
7750
7751 fn collect_plan_variables(plan: &LogicalPlan) -> HashSet<String> {
7753 let mut vars = HashSet::new();
7754 Self::collect_plan_variables_impl(plan, &mut vars);
7755 vars
7756 }
7757
7758 fn collect_plan_variables_impl(plan: &LogicalPlan, vars: &mut HashSet<String>) {
7759 match plan {
7760 LogicalPlan::Scan { variable, .. } => {
7761 vars.insert(variable.clone());
7762 }
7763 LogicalPlan::Traverse {
7764 target_variable,
7765 step_variable,
7766 input,
7767 path_variable,
7768 ..
7769 } => {
7770 vars.insert(target_variable.clone());
7771 if let Some(sv) = step_variable {
7772 vars.insert(sv.clone());
7773 }
7774 if let Some(pv) = path_variable {
7775 vars.insert(pv.clone());
7776 }
7777 Self::collect_plan_variables_impl(input, vars);
7778 }
7779 LogicalPlan::Filter { input, .. } => Self::collect_plan_variables_impl(input, vars),
7780 LogicalPlan::Project { input, projections } => {
7781 for (expr, alias) in projections {
7782 if let Some(a) = alias {
7783 vars.insert(a.clone());
7784 } else if let Expr::Variable(v) = expr {
7785 vars.insert(v.clone());
7786 }
7787 }
7788 Self::collect_plan_variables_impl(input, vars);
7789 }
7790 LogicalPlan::Apply {
7791 input, subquery, ..
7792 } => {
7793 Self::collect_plan_variables_impl(input, vars);
7794 Self::collect_plan_variables_impl(subquery, vars);
7795 }
7796 LogicalPlan::CrossJoin { left, right } => {
7797 Self::collect_plan_variables_impl(left, vars);
7798 Self::collect_plan_variables_impl(right, vars);
7799 }
7800 LogicalPlan::Unwind {
7801 input, variable, ..
7802 } => {
7803 vars.insert(variable.clone());
7804 Self::collect_plan_variables_impl(input, vars);
7805 }
7806 LogicalPlan::Aggregate { input, .. } => {
7807 Self::collect_plan_variables_impl(input, vars);
7808 }
7809 LogicalPlan::Distinct { input } => {
7810 Self::collect_plan_variables_impl(input, vars);
7811 }
7812 LogicalPlan::Sort { input, .. } => {
7813 Self::collect_plan_variables_impl(input, vars);
7814 }
7815 LogicalPlan::Limit { input, .. } => {
7816 Self::collect_plan_variables_impl(input, vars);
7817 }
7818 LogicalPlan::VectorKnn { variable, .. } => {
7819 vars.insert(variable.clone());
7820 }
7821 LogicalPlan::ProcedureCall { yield_items, .. } => {
7822 for (name, alias) in yield_items {
7823 vars.insert(alias.clone().unwrap_or_else(|| name.clone()));
7824 }
7825 }
7826 LogicalPlan::ShortestPath {
7827 input,
7828 path_variable,
7829 ..
7830 } => {
7831 vars.insert(path_variable.clone());
7832 Self::collect_plan_variables_impl(input, vars);
7833 }
7834 LogicalPlan::AllShortestPaths {
7835 input,
7836 path_variable,
7837 ..
7838 } => {
7839 vars.insert(path_variable.clone());
7840 Self::collect_plan_variables_impl(input, vars);
7841 }
7842 LogicalPlan::RecursiveCTE {
7843 initial, recursive, ..
7844 } => {
7845 Self::collect_plan_variables_impl(initial, vars);
7846 Self::collect_plan_variables_impl(recursive, vars);
7847 }
7848 LogicalPlan::SubqueryCall {
7849 input, subquery, ..
7850 } => {
7851 Self::collect_plan_variables_impl(input, vars);
7852 Self::collect_plan_variables_impl(subquery, vars);
7853 }
7854 _ => {}
7855 }
7856 }
7857
7858 fn extract_apply_input_predicates(
7861 predicate: &Expr,
7862 input_variables: &HashSet<String>,
7863 subquery_new_variables: &HashSet<String>,
7864 ) -> (Vec<Expr>, Vec<Expr>) {
7865 let conjuncts = Self::split_and_conjuncts(predicate);
7866 let mut input_preds = Vec::new();
7867 let mut remaining = Vec::new();
7868
7869 for conj in conjuncts {
7870 let vars = Self::collect_expr_variables(&conj);
7871
7872 let refs_input_only = vars.iter().all(|v| input_variables.contains(v));
7874 let refs_any_subquery = vars.iter().any(|v| subquery_new_variables.contains(v));
7875
7876 if refs_input_only && !refs_any_subquery && !vars.is_empty() {
7877 input_preds.push(conj);
7878 } else {
7879 remaining.push(conj);
7880 }
7881 }
7882
7883 (input_preds, remaining)
7884 }
7885
7886 fn push_predicates_to_apply(plan: LogicalPlan, current_predicate: &mut Expr) -> LogicalPlan {
7889 match plan {
7890 LogicalPlan::Apply {
7891 input,
7892 subquery,
7893 input_filter,
7894 } => {
7895 let input_vars = Self::collect_plan_variables(&input);
7897
7898 let subquery_vars = Self::collect_plan_variables(&subquery);
7900 let new_subquery_vars: HashSet<String> =
7901 subquery_vars.difference(&input_vars).cloned().collect();
7902
7903 let (input_preds, remaining) = Self::extract_apply_input_predicates(
7905 current_predicate,
7906 &input_vars,
7907 &new_subquery_vars,
7908 );
7909
7910 *current_predicate = if remaining.is_empty() {
7912 Expr::TRUE
7913 } else {
7914 Self::combine_predicates(remaining).unwrap()
7915 };
7916
7917 let new_input_filter = if input_preds.is_empty() {
7919 input_filter
7920 } else {
7921 let extracted = Self::combine_predicates(input_preds).unwrap();
7922 match input_filter {
7923 Some(existing) => Some(Expr::BinaryOp {
7924 left: Box::new(existing),
7925 op: BinaryOp::And,
7926 right: Box::new(extracted),
7927 }),
7928 None => Some(extracted),
7929 }
7930 };
7931
7932 let new_input = Self::push_predicates_to_apply(*input, current_predicate);
7934
7935 LogicalPlan::Apply {
7936 input: Box::new(new_input),
7937 subquery,
7938 input_filter: new_input_filter,
7939 }
7940 }
7941 LogicalPlan::Filter {
7943 input,
7944 predicate,
7945 optional_variables,
7946 } => LogicalPlan::Filter {
7947 input: Box::new(Self::push_predicates_to_apply(*input, current_predicate)),
7948 predicate,
7949 optional_variables,
7950 },
7951 LogicalPlan::Project { input, projections } => LogicalPlan::Project {
7952 input: Box::new(Self::push_predicates_to_apply(*input, current_predicate)),
7953 projections,
7954 },
7955 LogicalPlan::Sort { input, order_by } => LogicalPlan::Sort {
7956 input: Box::new(Self::push_predicates_to_apply(*input, current_predicate)),
7957 order_by,
7958 },
7959 LogicalPlan::Limit { input, skip, fetch } => LogicalPlan::Limit {
7960 input: Box::new(Self::push_predicates_to_apply(*input, current_predicate)),
7961 skip,
7962 fetch,
7963 },
7964 LogicalPlan::Aggregate {
7965 input,
7966 group_by,
7967 aggregates,
7968 } => LogicalPlan::Aggregate {
7969 input: Box::new(Self::push_predicates_to_apply(*input, current_predicate)),
7970 group_by,
7971 aggregates,
7972 },
7973 LogicalPlan::CrossJoin { left, right } => LogicalPlan::CrossJoin {
7974 left: Box::new(Self::push_predicates_to_apply(*left, current_predicate)),
7975 right: Box::new(Self::push_predicates_to_apply(*right, current_predicate)),
7976 },
7977 LogicalPlan::Traverse {
7978 input,
7979 edge_type_ids,
7980 direction,
7981 source_variable,
7982 target_variable,
7983 target_label_id,
7984 step_variable,
7985 min_hops,
7986 max_hops,
7987 optional,
7988 target_filter,
7989 path_variable,
7990 edge_properties,
7991 is_variable_length,
7992 optional_pattern_vars,
7993 scope_match_variables,
7994 edge_filter_expr,
7995 path_mode,
7996 qpp_steps,
7997 } => LogicalPlan::Traverse {
7998 input: Box::new(Self::push_predicates_to_apply(*input, current_predicate)),
7999 edge_type_ids,
8000 direction,
8001 source_variable,
8002 target_variable,
8003 target_label_id,
8004 step_variable,
8005 min_hops,
8006 max_hops,
8007 optional,
8008 target_filter,
8009 path_variable,
8010 edge_properties,
8011 is_variable_length,
8012 optional_pattern_vars,
8013 scope_match_variables,
8014 edge_filter_expr,
8015 path_mode,
8016 qpp_steps,
8017 },
8018 other => other,
8019 }
8020 }
8021}
8022
8023pub fn aggregate_column_name(expr: &Expr) -> String {
8030 expr.to_string_repr()
8031}
8032
8033#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
8035pub struct ExplainOutput {
8036 pub plan_text: String,
8038 pub index_usage: Vec<IndexUsage>,
8040 pub cost_estimates: CostEstimates,
8042 pub warnings: Vec<String>,
8044 pub suggestions: Vec<IndexSuggestion>,
8046}
8047
8048#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
8050pub struct IndexSuggestion {
8051 pub label_or_type: String,
8053 pub property: String,
8055 pub index_type: String,
8057 pub reason: String,
8059 pub create_statement: String,
8061}
8062
8063#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
8065pub struct IndexUsage {
8066 pub label_or_type: String,
8067 pub property: String,
8068 pub index_type: String,
8069 pub used: bool,
8071 pub reason: Option<String>,
8073}
8074
8075#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
8077pub struct CostEstimates {
8078 pub estimated_rows: f64,
8080 pub estimated_cost: f64,
8082}
8083
8084impl QueryPlanner {
8085 pub fn explain_plan(&self, ast: Query) -> Result<ExplainOutput> {
8087 let plan = self.plan(ast)?;
8088 self.explain_logical_plan(&plan)
8089 }
8090
8091 pub fn explain_logical_plan(&self, plan: &LogicalPlan) -> Result<ExplainOutput> {
8093 let index_usage = self.analyze_index_usage(plan)?;
8094 let cost_estimates = self.estimate_costs(plan)?;
8095 let suggestions = self.collect_index_suggestions(plan);
8096 let warnings = Vec::new();
8097 let plan_text = format!("{:#?}", plan);
8098
8099 Ok(ExplainOutput {
8100 plan_text,
8101 index_usage,
8102 cost_estimates,
8103 warnings,
8104 suggestions,
8105 })
8106 }
8107
8108 fn analyze_index_usage(&self, plan: &LogicalPlan) -> Result<Vec<IndexUsage>> {
8109 let mut usage = Vec::new();
8110 self.collect_index_usage(plan, &mut usage);
8111 Ok(usage)
8112 }
8113
8114 fn collect_index_usage(&self, plan: &LogicalPlan, usage: &mut Vec<IndexUsage>) {
8115 match plan {
8116 LogicalPlan::Scan {
8117 label_id,
8118 filter: Some(filter),
8119 ..
8120 } => {
8121 if let Some(label_name) = self.schema.label_name_by_id(*label_id) {
8125 let analyzer = crate::query::pushdown::IndexAwareAnalyzer::new(&self.schema);
8126 if let LogicalPlan::Scan { variable, .. } = plan {
8129 let strategy = analyzer.analyze(filter, variable, *label_id);
8130 for prop in strategy.hash_index_columns {
8131 usage.push(IndexUsage {
8132 label_or_type: label_name.to_string(),
8133 property: prop,
8134 index_type: "HASH".to_string(),
8135 used: true,
8136 reason: Some(
8137 "Hash index point lookup pushed into Lance scan".to_string(),
8138 ),
8139 });
8140 }
8141 }
8142 }
8143 }
8144 LogicalPlan::Scan { .. } => {}
8145 LogicalPlan::VectorKnn {
8146 label_id, property, ..
8147 } => {
8148 let label_name = self.schema.label_name_by_id(*label_id).unwrap_or("?");
8149 usage.push(IndexUsage {
8150 label_or_type: label_name.to_string(),
8151 property: property.clone(),
8152 index_type: "VECTOR".to_string(),
8153 used: true,
8154 reason: None,
8155 });
8156 }
8157 LogicalPlan::Explain { plan } => self.collect_index_usage(plan, usage),
8158 LogicalPlan::Filter { input, .. } => self.collect_index_usage(input, usage),
8159 LogicalPlan::Project { input, .. } => self.collect_index_usage(input, usage),
8160 LogicalPlan::Limit { input, .. } => self.collect_index_usage(input, usage),
8161 LogicalPlan::Sort { input, .. } => self.collect_index_usage(input, usage),
8162 LogicalPlan::Aggregate { input, .. } => self.collect_index_usage(input, usage),
8163 LogicalPlan::Traverse { input, .. } => self.collect_index_usage(input, usage),
8164 LogicalPlan::Union { left, right, .. } | LogicalPlan::CrossJoin { left, right } => {
8165 self.collect_index_usage(left, usage);
8166 self.collect_index_usage(right, usage);
8167 }
8168 _ => {}
8169 }
8170 }
8171
8172 fn estimate_costs(&self, _plan: &LogicalPlan) -> Result<CostEstimates> {
8173 Ok(CostEstimates {
8174 estimated_rows: 100.0,
8175 estimated_cost: 10.0,
8176 })
8177 }
8178
8179 fn collect_index_suggestions(&self, plan: &LogicalPlan) -> Vec<IndexSuggestion> {
8185 let mut suggestions = Vec::new();
8186 self.collect_temporal_suggestions(plan, &mut suggestions);
8187 suggestions
8188 }
8189
8190 fn collect_temporal_suggestions(
8192 &self,
8193 plan: &LogicalPlan,
8194 suggestions: &mut Vec<IndexSuggestion>,
8195 ) {
8196 match plan {
8197 LogicalPlan::Filter {
8198 input, predicate, ..
8199 } => {
8200 self.detect_temporal_pattern(predicate, suggestions);
8202 self.collect_temporal_suggestions(input, suggestions);
8204 }
8205 LogicalPlan::Explain { plan } => self.collect_temporal_suggestions(plan, suggestions),
8206 LogicalPlan::Project { input, .. } => {
8207 self.collect_temporal_suggestions(input, suggestions)
8208 }
8209 LogicalPlan::Limit { input, .. } => {
8210 self.collect_temporal_suggestions(input, suggestions)
8211 }
8212 LogicalPlan::Sort { input, .. } => {
8213 self.collect_temporal_suggestions(input, suggestions)
8214 }
8215 LogicalPlan::Aggregate { input, .. } => {
8216 self.collect_temporal_suggestions(input, suggestions)
8217 }
8218 LogicalPlan::Traverse { input, .. } => {
8219 self.collect_temporal_suggestions(input, suggestions)
8220 }
8221 LogicalPlan::Union { left, right, .. } | LogicalPlan::CrossJoin { left, right } => {
8222 self.collect_temporal_suggestions(left, suggestions);
8223 self.collect_temporal_suggestions(right, suggestions);
8224 }
8225 _ => {}
8226 }
8227 }
8228
8229 fn detect_temporal_pattern(&self, expr: &Expr, suggestions: &mut Vec<IndexSuggestion>) {
8235 match expr {
8236 Expr::FunctionCall { name, args, .. }
8238 if (name.eq_ignore_ascii_case("uni.temporal.validAt")
8239 || name.eq_ignore_ascii_case("validAt"))
8240 && args.len() >= 2 =>
8241 {
8242 let start_prop = if let Some(Expr::Literal(CypherLiteral::String(s))) = args.get(1)
8244 {
8245 s.clone()
8246 } else {
8247 "valid_from".to_string()
8248 };
8249
8250 if let Some(var) = args.first().and_then(|e| e.extract_variable()) {
8252 self.suggest_temporal_index(&var, &start_prop, suggestions);
8253 }
8254 }
8255
8256 Expr::BinaryOp {
8258 left,
8259 op: BinaryOp::And,
8260 right,
8261 } => {
8262 if let Expr::BinaryOp {
8264 left: prop_expr,
8265 op: BinaryOp::LtEq,
8266 ..
8267 } = left.as_ref()
8268 && let Expr::Property(base, prop_name) = prop_expr.as_ref()
8269 && (prop_name == "valid_from"
8270 || prop_name.contains("start")
8271 || prop_name.contains("from")
8272 || prop_name.contains("begin"))
8273 && let Some(var) = base.extract_variable()
8274 {
8275 self.suggest_temporal_index(&var, prop_name, suggestions);
8276 }
8277
8278 self.detect_temporal_pattern(left.as_ref(), suggestions);
8280 self.detect_temporal_pattern(right.as_ref(), suggestions);
8281 }
8282
8283 Expr::BinaryOp { left, right, .. } => {
8285 self.detect_temporal_pattern(left.as_ref(), suggestions);
8286 self.detect_temporal_pattern(right.as_ref(), suggestions);
8287 }
8288
8289 _ => {}
8290 }
8291 }
8292
8293 fn suggest_temporal_index(
8295 &self,
8296 _variable: &str,
8297 property: &str,
8298 suggestions: &mut Vec<IndexSuggestion>,
8299 ) {
8300 let mut has_index = false;
8303
8304 for index in &self.schema.indexes {
8305 if let IndexDefinition::Scalar(config) = index
8306 && config.properties.contains(&property.to_string())
8307 {
8308 has_index = true;
8309 break;
8310 }
8311 }
8312
8313 if !has_index {
8314 let already_suggested = suggestions.iter().any(|s| s.property == property);
8316 if !already_suggested {
8317 suggestions.push(IndexSuggestion {
8318 label_or_type: "(detected from temporal query)".to_string(),
8319 property: property.to_string(),
8320 index_type: "SCALAR (BTree)".to_string(),
8321 reason: format!(
8322 "Temporal queries using '{}' can benefit from a scalar index for range scans",
8323 property
8324 ),
8325 create_statement: format!(
8326 "CREATE INDEX idx_{} FOR (n:YourLabel) ON (n.{})",
8327 property, property
8328 ),
8329 });
8330 }
8331 }
8332 }
8333
8334 fn normalize_expression_for_storage(expr: &Expr) -> String {
8339 match expr {
8340 Expr::Property(base, prop) if matches!(**base, Expr::Variable(_)) => prop.clone(),
8341 _ => {
8342 let expr_str = expr.to_string_repr();
8344 Self::strip_variable_prefix(&expr_str)
8345 }
8346 }
8347 }
8348
8349 fn strip_variable_prefix(expr_str: &str) -> String {
8352 use regex::Regex;
8353 let re = Regex::new(r"\b\w+\.(\w+)").unwrap();
8355 re.replace_all(expr_str, "$1").to_string()
8356 }
8357
8358 fn plan_schema_command(&self, cmd: SchemaCommand) -> Result<LogicalPlan> {
8360 match cmd {
8361 SchemaCommand::CreateVectorIndex(c) => {
8362 use uni_common::vector_index_opts::{
8363 VectorIndexOpts, build_vector_index_type, parse_vector_metric,
8364 };
8365 let opt = |key: &str| -> Option<u32> {
8368 c.options.get(key).and_then(|v| {
8369 v.as_u64()
8370 .map(|n| n as u32)
8371 .or_else(|| v.as_str().and_then(|s| s.parse::<u32>().ok()))
8372 })
8373 };
8374 let opt_u8 = |key: &str| -> Option<u8> {
8375 c.options.get(key).and_then(|v| {
8376 v.as_u64()
8377 .map(|n| n as u8)
8378 .or_else(|| v.as_str().and_then(|s| s.parse::<u8>().ok()))
8379 })
8380 };
8381 let opt_u64 = |key: &str| -> Option<u64> {
8382 c.options.get(key).and_then(|v| {
8383 v.as_u64()
8384 .or_else(|| v.as_str().and_then(|s| s.parse::<u64>().ok()))
8385 })
8386 };
8387 let index_type = build_vector_index_type(&VectorIndexOpts {
8390 type_name: c.options.get("type").and_then(|v| v.as_str()),
8391 partitions: opt("partitions"),
8392 m: opt("m"),
8393 ef_construction: opt("ef_construction"),
8394 sub_vectors: opt("sub_vectors"),
8395 num_bits: opt_u8("num_bits"),
8396 k_sim: opt("k_sim"),
8397 reps: opt("reps"),
8398 d_proj: opt("d_proj"),
8399 seed: opt_u64("seed"),
8400 inner: c.options.get("inner").and_then(|v| v.as_str()),
8401 });
8402
8403 let embedding_config = if let Some(emb_val) = c.options.get("embedding") {
8405 Self::parse_embedding_config(emb_val)?
8406 } else {
8407 None
8408 };
8409
8410 let metric = parse_vector_metric(c.options.get("metric").and_then(|v| v.as_str()))?;
8412
8413 let config = VectorIndexConfig {
8414 name: c.name,
8415 label: c.label,
8416 property: c.property,
8417 metric,
8418 index_type,
8419 embedding_config,
8420 metadata: Default::default(),
8421 };
8422 Ok(LogicalPlan::CreateVectorIndex {
8423 config,
8424 if_not_exists: c.if_not_exists,
8425 })
8426 }
8427 SchemaCommand::CreateFullTextIndex(cfg) => Ok(LogicalPlan::CreateFullTextIndex {
8428 config: FullTextIndexConfig {
8429 name: cfg.name,
8430 label: cfg.label,
8431 properties: cfg.properties,
8432 tokenizer: TokenizerConfig::Standard,
8433 with_positions: true,
8434 metadata: Default::default(),
8435 },
8436 if_not_exists: cfg.if_not_exists,
8437 }),
8438 SchemaCommand::CreateScalarIndex(cfg) => {
8439 let properties: Vec<String> = cfg
8441 .expressions
8442 .iter()
8443 .map(Self::normalize_expression_for_storage)
8444 .collect();
8445
8446 Ok(LogicalPlan::CreateScalarIndex {
8447 config: ScalarIndexConfig {
8448 name: cfg.name,
8449 label: cfg.label,
8450 properties,
8451 index_type: ScalarIndexType::BTree,
8452 where_clause: cfg.where_clause.map(|e| e.to_string_repr()),
8453 metadata: Default::default(),
8454 },
8455 if_not_exists: cfg.if_not_exists,
8456 })
8457 }
8458 SchemaCommand::CreateJsonFtsIndex(cfg) => {
8459 let with_positions = cfg
8460 .options
8461 .get("with_positions")
8462 .and_then(|v| v.as_bool())
8463 .unwrap_or(false);
8464 Ok(LogicalPlan::CreateJsonFtsIndex {
8465 config: JsonFtsIndexConfig {
8466 name: cfg.name,
8467 label: cfg.label,
8468 column: cfg.column,
8469 paths: Vec::new(),
8470 with_positions,
8471 metadata: Default::default(),
8472 },
8473 if_not_exists: cfg.if_not_exists,
8474 })
8475 }
8476 SchemaCommand::DropIndex(drop) => Ok(LogicalPlan::DropIndex {
8477 name: drop.name,
8478 if_exists: false, }),
8480 SchemaCommand::CreateConstraint(c) => Ok(LogicalPlan::CreateConstraint(c)),
8481 SchemaCommand::DropConstraint(c) => Ok(LogicalPlan::DropConstraint(c)),
8482 SchemaCommand::CreateLabel(c) => Ok(LogicalPlan::CreateLabel(c)),
8483 SchemaCommand::CreateEdgeType(c) => Ok(LogicalPlan::CreateEdgeType(c)),
8484 SchemaCommand::AlterLabel(c) => Ok(LogicalPlan::AlterLabel(c)),
8485 SchemaCommand::AlterEdgeType(c) => Ok(LogicalPlan::AlterEdgeType(c)),
8486 SchemaCommand::DropLabel(c) => Ok(LogicalPlan::DropLabel(c)),
8487 SchemaCommand::DropEdgeType(c) => Ok(LogicalPlan::DropEdgeType(c)),
8488 SchemaCommand::ShowConstraints(c) => Ok(LogicalPlan::ShowConstraints(c)),
8489 SchemaCommand::ShowIndexes(c) => Ok(LogicalPlan::ShowIndexes { filter: c.filter }),
8490 SchemaCommand::ShowDatabase => Ok(LogicalPlan::ShowDatabase),
8491 SchemaCommand::ShowConfig => Ok(LogicalPlan::ShowConfig),
8492 SchemaCommand::ShowStatistics => Ok(LogicalPlan::ShowStatistics),
8493 SchemaCommand::Vacuum => Ok(LogicalPlan::Vacuum),
8494 SchemaCommand::Checkpoint => Ok(LogicalPlan::Checkpoint),
8495 SchemaCommand::Backup { path } => Ok(LogicalPlan::Backup {
8496 destination: path,
8497 options: HashMap::new(),
8498 }),
8499 SchemaCommand::CopyTo(cmd) => Ok(LogicalPlan::CopyTo {
8500 label: cmd.label,
8501 path: cmd.path,
8502 format: cmd.format,
8503 options: cmd.options,
8504 }),
8505 SchemaCommand::CopyFrom(cmd) => Ok(LogicalPlan::CopyFrom {
8506 label: cmd.label,
8507 path: cmd.path,
8508 format: cmd.format,
8509 options: cmd.options,
8510 }),
8511 }
8512 }
8513
8514 fn parse_embedding_config(emb_val: &Value) -> Result<Option<EmbeddingConfig>> {
8515 let obj = emb_val
8516 .as_object()
8517 .ok_or_else(|| anyhow!("embedding option must be an object"))?;
8518
8519 let alias = obj
8521 .get("alias")
8522 .and_then(|v| v.as_str())
8523 .ok_or_else(|| anyhow!("embedding.alias is required"))?;
8524
8525 let source_properties = obj
8527 .get("source")
8528 .and_then(|v| v.as_array())
8529 .ok_or_else(|| anyhow!("embedding.source is required and must be an array"))?
8530 .iter()
8531 .filter_map(|v| v.as_str().map(|s| s.to_string()))
8532 .collect::<Vec<_>>();
8533
8534 if source_properties.is_empty() {
8535 return Err(anyhow!(
8536 "embedding.source must contain at least one property"
8537 ));
8538 }
8539
8540 let batch_size = obj
8541 .get("batch_size")
8542 .and_then(|v| v.as_u64())
8543 .map(|v| v as usize)
8544 .unwrap_or(32);
8545
8546 let document_prefix = obj
8547 .get("document_prefix")
8548 .and_then(|v| v.as_str())
8549 .map(|s| s.to_string());
8550
8551 let query_prefix = obj
8552 .get("query_prefix")
8553 .and_then(|v| v.as_str())
8554 .map(|s| s.to_string());
8555
8556 Ok(Some(EmbeddingConfig {
8557 alias: alias.to_string(),
8558 source_properties,
8559 batch_size,
8560 document_prefix,
8561 query_prefix,
8562 }))
8563 }
8564}
8565
8566pub fn collect_properties_from_plan(plan: &LogicalPlan) -> HashMap<String, HashSet<String>> {
8573 let mut properties: HashMap<String, HashSet<String>> = HashMap::new();
8574 collect_properties_recursive(plan, &mut properties);
8575 properties
8576}
8577
8578fn collect_properties_recursive(
8580 plan: &LogicalPlan,
8581 properties: &mut HashMap<String, HashSet<String>>,
8582) {
8583 match plan {
8584 LogicalPlan::Window {
8585 input,
8586 window_exprs,
8587 } => {
8588 for expr in window_exprs {
8590 collect_properties_from_expr_into(expr, properties);
8591 }
8592 collect_properties_recursive(input, properties);
8593 }
8594 LogicalPlan::Project { input, projections } => {
8595 for (expr, _alias) in projections {
8596 collect_properties_from_expr_into(expr, properties);
8597 }
8598 collect_properties_recursive(input, properties);
8599 }
8600 LogicalPlan::Sort { input, order_by } => {
8601 for sort_item in order_by {
8602 collect_properties_from_expr_into(&sort_item.expr, properties);
8603 }
8604 collect_properties_recursive(input, properties);
8605 }
8606 LogicalPlan::Filter {
8607 input, predicate, ..
8608 } => {
8609 collect_properties_from_expr_into(predicate, properties);
8610 collect_properties_recursive(input, properties);
8611 }
8612 LogicalPlan::Aggregate {
8613 input,
8614 group_by,
8615 aggregates,
8616 } => {
8617 for expr in group_by {
8618 collect_properties_from_expr_into(expr, properties);
8619 }
8620 for expr in aggregates {
8621 collect_properties_from_expr_into(expr, properties);
8622 }
8623 collect_properties_recursive(input, properties);
8624 }
8625 LogicalPlan::Scan {
8626 filter: Some(expr), ..
8627 } => {
8628 collect_properties_from_expr_into(expr, properties);
8629 }
8630 LogicalPlan::Scan { filter: None, .. } => {}
8631 LogicalPlan::ExtIdLookup {
8632 filter: Some(expr), ..
8633 } => {
8634 collect_properties_from_expr_into(expr, properties);
8635 }
8636 LogicalPlan::ExtIdLookup { filter: None, .. } => {}
8637 LogicalPlan::ScanAll {
8638 filter: Some(expr), ..
8639 } => {
8640 collect_properties_from_expr_into(expr, properties);
8641 }
8642 LogicalPlan::ScanAll { filter: None, .. } => {}
8643 LogicalPlan::ScanMainByLabels {
8644 filter: Some(expr), ..
8645 } => {
8646 collect_properties_from_expr_into(expr, properties);
8647 }
8648 LogicalPlan::ScanMainByLabels { filter: None, .. } => {}
8649 LogicalPlan::TraverseMainByType {
8650 input,
8651 target_filter,
8652 ..
8653 } => {
8654 if let Some(expr) = target_filter {
8655 collect_properties_from_expr_into(expr, properties);
8656 }
8657 collect_properties_recursive(input, properties);
8658 }
8659 LogicalPlan::Traverse {
8660 input,
8661 target_filter,
8662 step_variable: _,
8663 ..
8664 } => {
8665 if let Some(expr) = target_filter {
8666 collect_properties_from_expr_into(expr, properties);
8667 }
8668 collect_properties_recursive(input, properties);
8672 }
8673 LogicalPlan::Unwind { input, expr, .. } => {
8674 collect_properties_from_expr_into(expr, properties);
8675 collect_properties_recursive(input, properties);
8676 }
8677 LogicalPlan::Create { input, pattern } => {
8678 mark_pattern_variables(pattern, properties);
8683 collect_properties_recursive(input, properties);
8684 }
8685 LogicalPlan::CreateBatch { input, patterns } => {
8686 for pattern in patterns {
8687 mark_pattern_variables(pattern, properties);
8688 }
8689 collect_properties_recursive(input, properties);
8690 }
8691 LogicalPlan::Merge {
8692 input,
8693 pattern,
8694 on_match,
8695 on_create,
8696 } => {
8697 mark_pattern_variables(pattern, properties);
8698 if let Some(set_clause) = on_match {
8699 mark_set_item_variables(&set_clause.items, properties);
8700 }
8701 if let Some(set_clause) = on_create {
8702 mark_set_item_variables(&set_clause.items, properties);
8703 }
8704 collect_properties_recursive(input, properties);
8705 }
8706 LogicalPlan::Set { input, items } => {
8707 mark_set_item_variables(items, properties);
8708 collect_properties_recursive(input, properties);
8709 }
8710 LogicalPlan::Remove { input, items } => {
8711 for item in items {
8712 match item {
8713 RemoveItem::Property(expr) => {
8714 collect_properties_from_expr_into(expr, properties);
8717 if let Expr::Property(base, _) = expr
8718 && let Expr::Variable(var) = base.as_ref()
8719 {
8720 properties
8721 .entry(var.clone())
8722 .or_default()
8723 .insert("*".to_string());
8724 }
8725 }
8726 RemoveItem::Labels { variable, .. } => {
8727 properties
8729 .entry(variable.clone())
8730 .or_default()
8731 .insert("*".to_string());
8732 }
8733 }
8734 }
8735 collect_properties_recursive(input, properties);
8736 }
8737 LogicalPlan::Delete { input, items, .. } => {
8738 for expr in items {
8739 collect_properties_from_expr_into(expr, properties);
8740 }
8741 collect_properties_recursive(input, properties);
8742 }
8743 LogicalPlan::Foreach {
8744 input, list, body, ..
8745 } => {
8746 collect_properties_from_expr_into(list, properties);
8747 for plan in body {
8748 collect_properties_recursive(plan, properties);
8749 }
8750 collect_properties_recursive(input, properties);
8751 }
8752 LogicalPlan::Limit { input, .. } => {
8753 collect_properties_recursive(input, properties);
8754 }
8755 LogicalPlan::CrossJoin { left, right } => {
8756 collect_properties_recursive(left, properties);
8757 collect_properties_recursive(right, properties);
8758 }
8759 LogicalPlan::Apply {
8760 input,
8761 subquery,
8762 input_filter,
8763 } => {
8764 if let Some(expr) = input_filter {
8765 collect_properties_from_expr_into(expr, properties);
8766 }
8767 collect_properties_recursive(input, properties);
8768 collect_properties_recursive(subquery, properties);
8769 }
8770 LogicalPlan::Union { left, right, .. } => {
8771 collect_properties_recursive(left, properties);
8772 collect_properties_recursive(right, properties);
8773 }
8774 LogicalPlan::RecursiveCTE {
8775 initial, recursive, ..
8776 } => {
8777 collect_properties_recursive(initial, properties);
8778 collect_properties_recursive(recursive, properties);
8779 }
8780 LogicalPlan::ProcedureCall { arguments, .. } => {
8781 for arg in arguments {
8782 collect_properties_from_expr_into(arg, properties);
8783 }
8784 }
8785 LogicalPlan::VectorKnn { query, .. } => {
8786 collect_properties_from_expr_into(query, properties);
8787 }
8788 LogicalPlan::InvertedIndexLookup { terms, .. } => {
8789 collect_properties_from_expr_into(terms, properties);
8790 }
8791 LogicalPlan::ShortestPath { input, .. } => {
8792 collect_properties_recursive(input, properties);
8793 }
8794 LogicalPlan::AllShortestPaths { input, .. } => {
8795 collect_properties_recursive(input, properties);
8796 }
8797 LogicalPlan::Distinct { input } => {
8798 collect_properties_recursive(input, properties);
8799 }
8800 LogicalPlan::QuantifiedPattern {
8801 input,
8802 pattern_plan,
8803 ..
8804 } => {
8805 collect_properties_recursive(input, properties);
8806 collect_properties_recursive(pattern_plan, properties);
8807 }
8808 LogicalPlan::BindZeroLengthPath { input, .. } => {
8809 collect_properties_recursive(input, properties);
8810 }
8811 LogicalPlan::BindPath { input, .. } => {
8812 collect_properties_recursive(input, properties);
8813 }
8814 LogicalPlan::SubqueryCall { input, subquery } => {
8815 collect_properties_recursive(input, properties);
8816 collect_properties_recursive(subquery, properties);
8817 }
8818 LogicalPlan::LocyProject {
8819 input, projections, ..
8820 } => {
8821 for (expr, _alias) in projections {
8822 match expr {
8823 Expr::Variable(name) if !name.contains('.') => {
8827 properties
8828 .entry(name.clone())
8829 .or_default()
8830 .insert("_vid".to_string());
8831 }
8832 _ => collect_properties_from_expr_into(expr, properties),
8833 }
8834 }
8835 collect_properties_recursive(input, properties);
8836 }
8837 LogicalPlan::LocyFold {
8838 input,
8839 fold_bindings,
8840 ..
8841 } => {
8842 for (_name, expr) in fold_bindings {
8843 collect_properties_from_expr_into(expr, properties);
8844 }
8845 collect_properties_recursive(input, properties);
8846 }
8847 LogicalPlan::LocyBestBy {
8848 input, criteria, ..
8849 } => {
8850 for (expr, _asc) in criteria {
8851 collect_properties_from_expr_into(expr, properties);
8852 }
8853 collect_properties_recursive(input, properties);
8854 }
8855 LogicalPlan::LocyPriority { input, .. } => {
8856 collect_properties_recursive(input, properties);
8857 }
8858 LogicalPlan::LocyModelInvoke { input, .. } => {
8859 collect_properties_recursive(input, properties);
8865 }
8866 _ => {}
8868 }
8869}
8870
8871fn mark_set_item_variables(items: &[SetItem], properties: &mut HashMap<String, HashSet<String>>) {
8873 for item in items {
8874 match item {
8875 SetItem::Property { expr, value } => {
8876 collect_properties_from_expr_into(expr, properties);
8888 collect_properties_from_expr_into(value, properties);
8889 if let Expr::Property(base, _) = expr
8890 && let Expr::Variable(var) = base.as_ref()
8891 {
8892 properties
8893 .entry(var.clone())
8894 .or_default()
8895 .insert(STRUCT_ONLY_SENTINEL.to_string());
8896 }
8897 }
8898 SetItem::Labels { variable, .. } => {
8899 properties
8901 .entry(variable.clone())
8902 .or_default()
8903 .insert("*".to_string());
8904 }
8905 SetItem::Variable { variable, value } | SetItem::VariablePlus { variable, value } => {
8906 properties
8908 .entry(variable.clone())
8909 .or_default()
8910 .insert("*".to_string());
8911 collect_properties_from_expr_into(value, properties);
8912 }
8913 }
8914 }
8915}
8916
8917fn mark_pattern_variables(pattern: &Pattern, properties: &mut HashMap<String, HashSet<String>>) {
8922 for path in &pattern.paths {
8923 if let Some(ref v) = path.variable {
8924 properties
8925 .entry(v.clone())
8926 .or_default()
8927 .insert("*".to_string());
8928 }
8929 for element in &path.elements {
8930 match element {
8931 PatternElement::Node(n) => {
8932 if let Some(ref v) = n.variable {
8933 properties
8934 .entry(v.clone())
8935 .or_default()
8936 .insert("*".to_string());
8937 }
8938 if let Some(ref props) = n.properties {
8940 collect_properties_from_expr_into(props, properties);
8941 }
8942 }
8943 PatternElement::Relationship(r) => {
8944 if let Some(ref v) = r.variable {
8945 properties
8946 .entry(v.clone())
8947 .or_default()
8948 .insert("*".to_string());
8949 }
8950 if let Some(ref props) = r.properties {
8951 collect_properties_from_expr_into(props, properties);
8952 }
8953 }
8954 PatternElement::Parenthesized { pattern, .. } => {
8955 let sub = Pattern {
8956 paths: vec![pattern.as_ref().clone()],
8957 };
8958 mark_pattern_variables(&sub, properties);
8959 }
8960 }
8961 }
8962 }
8963}
8964
8965fn collect_properties_from_expr_into(
8967 expr: &Expr,
8968 properties: &mut HashMap<String, HashSet<String>>,
8969) {
8970 match expr {
8971 Expr::PatternComprehension {
8972 where_clause,
8973 map_expr,
8974 ..
8975 } => {
8976 if let Some(where_expr) = where_clause {
8980 collect_properties_from_expr_into(where_expr, properties);
8981 }
8982 collect_properties_from_expr_into(map_expr, properties);
8983 }
8984 Expr::Variable(name) => {
8985 if let Some((var, prop)) = name.split_once('.') {
8987 properties
8988 .entry(var.to_string())
8989 .or_default()
8990 .insert(prop.to_string());
8991 } else {
8992 properties
8994 .entry(name.clone())
8995 .or_default()
8996 .insert("*".to_string());
8997 }
8998 }
8999 Expr::Property(base, name) => {
9000 if let Expr::Variable(var) = base.as_ref() {
9002 properties
9003 .entry(var.clone())
9004 .or_default()
9005 .insert(name.clone());
9006 } else {
9009 collect_properties_from_expr_into(base, properties);
9011 }
9012 }
9013 Expr::BinaryOp { left, right, .. } => {
9014 collect_properties_from_expr_into(left, properties);
9015 collect_properties_from_expr_into(right, properties);
9016 }
9017 Expr::FunctionCall {
9018 name,
9019 args,
9020 window_spec,
9021 ..
9022 } => {
9023 analyze_function_property_requirements(name, args, properties);
9025
9026 for arg in args {
9028 collect_properties_from_expr_into(arg, properties);
9029 }
9030
9031 if let Some(spec) = window_spec {
9033 for part_expr in &spec.partition_by {
9034 collect_properties_from_expr_into(part_expr, properties);
9035 }
9036 for sort_item in &spec.order_by {
9037 collect_properties_from_expr_into(&sort_item.expr, properties);
9038 }
9039 }
9040 }
9041 Expr::UnaryOp { expr, .. } => {
9042 collect_properties_from_expr_into(expr, properties);
9043 }
9044 Expr::List(items) => {
9045 for item in items {
9046 collect_properties_from_expr_into(item, properties);
9047 }
9048 }
9049 Expr::Map(entries) => {
9050 for (_key, value) in entries {
9051 collect_properties_from_expr_into(value, properties);
9052 }
9053 }
9054 Expr::ListComprehension {
9055 list,
9056 where_clause,
9057 map_expr,
9058 ..
9059 } => {
9060 collect_properties_from_expr_into(list, properties);
9061 if let Some(where_expr) = where_clause {
9062 collect_properties_from_expr_into(where_expr, properties);
9063 }
9064 collect_properties_from_expr_into(map_expr, properties);
9065 }
9066 Expr::Case {
9067 expr,
9068 when_then,
9069 else_expr,
9070 } => {
9071 if let Some(scrutinee_expr) = expr {
9072 collect_properties_from_expr_into(scrutinee_expr, properties);
9073 }
9074 for (when, then) in when_then {
9075 collect_properties_from_expr_into(when, properties);
9076 collect_properties_from_expr_into(then, properties);
9077 }
9078 if let Some(default_expr) = else_expr {
9079 collect_properties_from_expr_into(default_expr, properties);
9080 }
9081 }
9082 Expr::Quantifier {
9083 list, predicate, ..
9084 } => {
9085 collect_properties_from_expr_into(list, properties);
9086 collect_properties_from_expr_into(predicate, properties);
9087 }
9088 Expr::Reduce {
9089 init, list, expr, ..
9090 } => {
9091 collect_properties_from_expr_into(init, properties);
9092 collect_properties_from_expr_into(list, properties);
9093 collect_properties_from_expr_into(expr, properties);
9094 }
9095 Expr::Exists { query, .. } => {
9096 collect_properties_from_subquery(query, properties);
9101 }
9102 Expr::CountSubquery(query) | Expr::CollectSubquery(query) => {
9103 collect_properties_from_subquery(query, properties);
9104 }
9105 Expr::IsNull(expr) | Expr::IsNotNull(expr) | Expr::IsUnique(expr) => {
9106 collect_properties_from_expr_into(expr, properties);
9107 }
9108 Expr::In { expr, list } => {
9109 collect_properties_from_expr_into(expr, properties);
9110 collect_properties_from_expr_into(list, properties);
9111 }
9112 Expr::ArrayIndex { array, index } => {
9113 if let Expr::Variable(var) = array.as_ref() {
9114 if let Expr::Literal(CypherLiteral::String(prop_name)) = index.as_ref() {
9115 properties
9117 .entry(var.clone())
9118 .or_default()
9119 .insert(prop_name.clone());
9120 } else {
9121 properties
9123 .entry(var.clone())
9124 .or_default()
9125 .insert("*".to_string());
9126 }
9127 }
9128 collect_properties_from_expr_into(array, properties);
9129 collect_properties_from_expr_into(index, properties);
9130 }
9131 Expr::ArraySlice { array, start, end } => {
9132 collect_properties_from_expr_into(array, properties);
9133 if let Some(start_expr) = start {
9134 collect_properties_from_expr_into(start_expr, properties);
9135 }
9136 if let Some(end_expr) = end {
9137 collect_properties_from_expr_into(end_expr, properties);
9138 }
9139 }
9140 Expr::ValidAt {
9141 entity,
9142 timestamp,
9143 start_prop,
9144 end_prop,
9145 } => {
9146 if let Expr::Variable(var) = entity.as_ref() {
9148 if let Some(prop) = start_prop {
9149 properties
9150 .entry(var.clone())
9151 .or_default()
9152 .insert(prop.clone());
9153 }
9154 if let Some(prop) = end_prop {
9155 properties
9156 .entry(var.clone())
9157 .or_default()
9158 .insert(prop.clone());
9159 }
9160 }
9161 collect_properties_from_expr_into(entity, properties);
9162 collect_properties_from_expr_into(timestamp, properties);
9163 }
9164 Expr::MapProjection { base, items } => {
9165 collect_properties_from_expr_into(base, properties);
9166 for item in items {
9167 match item {
9168 uni_cypher::ast::MapProjectionItem::Property(prop) => {
9169 if let Expr::Variable(var) = base.as_ref() {
9170 properties
9171 .entry(var.clone())
9172 .or_default()
9173 .insert(prop.clone());
9174 }
9175 }
9176 uni_cypher::ast::MapProjectionItem::AllProperties => {
9177 if let Expr::Variable(var) = base.as_ref() {
9178 properties
9179 .entry(var.clone())
9180 .or_default()
9181 .insert("*".to_string());
9182 }
9183 }
9184 uni_cypher::ast::MapProjectionItem::LiteralEntry(_, expr) => {
9185 collect_properties_from_expr_into(expr, properties);
9186 }
9187 uni_cypher::ast::MapProjectionItem::Variable(_) => {}
9188 }
9189 }
9190 }
9191 Expr::LabelCheck { expr, .. } => {
9192 collect_properties_from_expr_into(expr, properties);
9193 }
9194 Expr::Parameter(name) => {
9198 properties
9199 .entry(name.clone())
9200 .or_default()
9201 .insert("*".to_string());
9202 }
9203 Expr::Literal(_) | Expr::Wildcard => {}
9205 }
9206}
9207
9208fn collect_properties_from_subquery(
9214 query: &Query,
9215 properties: &mut HashMap<String, HashSet<String>>,
9216) {
9217 match query {
9218 Query::Single(stmt) => {
9219 for clause in &stmt.clauses {
9220 match clause {
9221 Clause::Match(m) => {
9222 if let Some(ref wc) = m.where_clause {
9223 collect_properties_from_expr_into(wc, properties);
9224 }
9225 }
9226 Clause::With(w) => {
9227 for item in &w.items {
9228 if let ReturnItem::Expr { expr, .. } = item {
9229 collect_properties_from_expr_into(expr, properties);
9230 }
9231 }
9232 if let Some(ref wc) = w.where_clause {
9233 collect_properties_from_expr_into(wc, properties);
9234 }
9235 }
9236 Clause::Return(r) => {
9237 for item in &r.items {
9238 if let ReturnItem::Expr { expr, .. } = item {
9239 collect_properties_from_expr_into(expr, properties);
9240 }
9241 }
9242 }
9243 _ => {}
9244 }
9245 }
9246 }
9247 Query::Union { left, right, .. } => {
9248 collect_properties_from_subquery(left, properties);
9249 collect_properties_from_subquery(right, properties);
9250 }
9251 _ => {}
9252 }
9253}
9254
9255fn analyze_function_property_requirements(
9265 name: &str,
9266 args: &[Expr],
9267 properties: &mut HashMap<String, HashSet<String>>,
9268) {
9269 use crate::query::function_props::get_function_spec;
9270
9271 fn mark_wildcard(var: &str, properties: &mut HashMap<String, HashSet<String>>) {
9273 properties
9274 .entry(var.to_string())
9275 .or_default()
9276 .insert("*".to_string());
9277 }
9278
9279 if name.eq_ignore_ascii_case("created_at") || name.eq_ignore_ascii_case("updated_at") {
9282 if let Some(Expr::Variable(var)) = args.first() {
9283 let col = if name.eq_ignore_ascii_case("created_at") {
9284 "_created_at"
9285 } else {
9286 "_updated_at"
9287 };
9288 properties
9289 .entry(var.clone())
9290 .or_default()
9291 .insert(col.to_string());
9292 }
9293 return;
9294 }
9295
9296 let Some(spec) = get_function_spec(name) else {
9297 for arg in args {
9299 if let Expr::Variable(var) = arg {
9300 mark_wildcard(var, properties);
9301 }
9302 }
9303 return;
9304 };
9305
9306 for &(prop_arg_idx, entity_arg_idx) in spec.property_name_args {
9308 let entity_arg = args.get(entity_arg_idx);
9309 let prop_arg = args.get(prop_arg_idx);
9310
9311 match (entity_arg, prop_arg) {
9312 (Some(Expr::Variable(var)), Some(Expr::Literal(CypherLiteral::String(prop)))) => {
9313 properties
9314 .entry(var.clone())
9315 .or_default()
9316 .insert(prop.clone());
9317 }
9318 (Some(Expr::Variable(var)), Some(Expr::Parameter(_))) => {
9319 mark_wildcard(var, properties);
9321 }
9322 _ => {}
9323 }
9324 }
9325
9326 if spec.needs_full_entity {
9328 for &idx in spec.entity_args {
9329 if let Some(Expr::Variable(var)) = args.get(idx) {
9330 mark_wildcard(var, properties);
9331 }
9332 }
9333 }
9334}
9335
9336pub trait ForkIndexLookup {
9345 fn fork_index_for(
9346 &self,
9347 label: &str,
9348 column: &str,
9349 ) -> Option<uni_store::fork::ForkLocalIndexKind>;
9350
9351 fn fork_index_for_label_id(
9358 &self,
9359 _label_id: u16,
9360 _column: &str,
9361 ) -> Option<uni_store::fork::ForkLocalIndexKind> {
9362 None
9363 }
9364}
9365
9366impl ForkIndexLookup for uni_store::storage::StorageManager {
9367 fn fork_index_for(
9368 &self,
9369 label: &str,
9370 column: &str,
9371 ) -> Option<uni_store::fork::ForkLocalIndexKind> {
9372 self.fork_index_exists(label, column)
9373 }
9374
9375 fn fork_index_for_label_id(
9376 &self,
9377 label_id: u16,
9378 column: &str,
9379 ) -> Option<uni_store::fork::ForkLocalIndexKind> {
9380 let schema = self.schema_manager().schema();
9381 let label_name = schema.label_name_by_id(label_id)?;
9382 self.fork_index_exists(label_name, column)
9383 }
9384}
9385
9386#[must_use]
9418pub fn fuse_create_set(plan: LogicalPlan) -> LogicalPlan {
9419 match plan {
9420 LogicalPlan::Set { input, items } => {
9421 let input = fuse_create_set(*input);
9424 match input {
9425 LogicalPlan::Create {
9426 input: child,
9427 pattern,
9428 } => {
9429 let bound_vars = crate::query::df_planner::collect_plan_variables(&child);
9430 match try_fuse_set_items(std::slice::from_ref(&pattern), &items, &bound_vars) {
9431 Some(mut patterns) => LogicalPlan::Create {
9432 input: child,
9433 pattern: patterns
9436 .pop()
9437 .expect("one pattern in yields one pattern out"),
9438 },
9439 None => LogicalPlan::Set {
9440 input: Box::new(LogicalPlan::Create {
9441 input: child,
9442 pattern,
9443 }),
9444 items,
9445 },
9446 }
9447 }
9448 LogicalPlan::CreateBatch {
9449 input: child,
9450 patterns,
9451 } => {
9452 let bound_vars = crate::query::df_planner::collect_plan_variables(&child);
9453 match try_fuse_set_items(&patterns, &items, &bound_vars) {
9454 Some(fused) => LogicalPlan::CreateBatch {
9455 input: child,
9456 patterns: fused,
9457 },
9458 None => LogicalPlan::Set {
9459 input: Box::new(LogicalPlan::CreateBatch {
9460 input: child,
9461 patterns,
9462 }),
9463 items,
9464 },
9465 }
9466 }
9467 other => LogicalPlan::Set {
9468 input: Box::new(other),
9469 items,
9470 },
9471 }
9472 }
9473 LogicalPlan::Project { input, projections } => LogicalPlan::Project {
9478 input: Box::new(fuse_create_set(*input)),
9479 projections,
9480 },
9481 LogicalPlan::Limit { input, skip, fetch } => LogicalPlan::Limit {
9482 input: Box::new(fuse_create_set(*input)),
9483 skip,
9484 fetch,
9485 },
9486 LogicalPlan::Sort { input, order_by } => LogicalPlan::Sort {
9487 input: Box::new(fuse_create_set(*input)),
9488 order_by,
9489 },
9490 LogicalPlan::Filter {
9491 input,
9492 predicate,
9493 optional_variables,
9494 } => LogicalPlan::Filter {
9495 input: Box::new(fuse_create_set(*input)),
9496 predicate,
9497 optional_variables,
9498 },
9499 LogicalPlan::Create { input, pattern } => LogicalPlan::Create {
9500 input: Box::new(fuse_create_set(*input)),
9501 pattern,
9502 },
9503 LogicalPlan::CreateBatch { input, patterns } => LogicalPlan::CreateBatch {
9504 input: Box::new(fuse_create_set(*input)),
9505 patterns,
9506 },
9507 other => other,
9508 }
9509}
9510
9511fn try_fuse_set_items(
9525 patterns: &[Pattern],
9526 items: &[SetItem],
9527 bound_vars: &HashSet<String>,
9528) -> Option<Vec<Pattern>> {
9529 let mut owner: HashMap<String, usize> = HashMap::new();
9532 for (idx, pattern) in patterns.iter().enumerate() {
9533 for var in crate::query::df_graph::mutation_common::pattern_variable_names(pattern) {
9534 if bound_vars.contains(&var) {
9535 continue;
9536 }
9537 owner.entry(var).or_insert(idx);
9538 }
9539 }
9540
9541 let mut out = patterns.to_vec();
9542 for item in items {
9543 let SetItem::Property { expr, value } = item else {
9544 return None; };
9546 let Expr::Property(base, prop) = expr else {
9547 return None; };
9549 let Expr::Variable(var) = base.as_ref() else {
9550 return None; };
9552 let Some(&idx) = owner.get(var) else {
9553 return None; };
9555 if collect_expr_variables(value)
9559 .iter()
9560 .any(|referenced| owner.contains_key(referenced))
9561 {
9562 return None;
9563 }
9564 if !merge_pattern_property(&mut out[idx], var, prop, value) {
9565 return None; }
9567 }
9568 Some(out)
9569}
9570
9571fn merge_pattern_property(pattern: &mut Pattern, var: &str, prop: &str, value: &Expr) -> bool {
9578 for path in &mut pattern.paths {
9579 if merge_into_elements(&mut path.elements, var, prop, value) {
9580 return true;
9581 }
9582 }
9583 false
9584}
9585
9586fn merge_into_elements(
9588 elements: &mut [PatternElement],
9589 var: &str,
9590 prop: &str,
9591 value: &Expr,
9592) -> bool {
9593 for element in elements {
9594 match element {
9595 PatternElement::Node(n) if n.variable.as_deref() == Some(var) => {
9596 return set_map_property(&mut n.properties, prop, value.clone());
9597 }
9598 PatternElement::Relationship(r) if r.variable.as_deref() == Some(var) => {
9599 return set_map_property(&mut r.properties, prop, value.clone());
9600 }
9601 PatternElement::Parenthesized { pattern, .. } => {
9602 if merge_into_elements(&mut pattern.elements, var, prop, value) {
9603 return true;
9604 }
9605 }
9606 _ => {}
9607 }
9608 }
9609 false
9610}
9611
9612fn set_map_property(props: &mut Option<Expr>, prop: &str, value: Expr) -> bool {
9617 match props {
9618 None => {
9619 *props = Some(Expr::Map(vec![(prop.to_string(), value)]));
9620 true
9621 }
9622 Some(Expr::Map(entries)) => {
9623 entries.retain(|(k, _)| k != prop);
9624 entries.push((prop.to_string(), value));
9625 true
9626 }
9627 Some(_) => false,
9628 }
9629}
9630
9631#[must_use]
9641pub fn rewrite_for_fork_fusion<L: ForkIndexLookup>(plan: LogicalPlan, lookup: &L) -> LogicalPlan {
9642 rewrite_node(plan, lookup)
9643}
9644
9645fn rewrite_node<L: ForkIndexLookup>(plan: LogicalPlan, lookup: &L) -> LogicalPlan {
9646 match plan {
9647 LogicalPlan::Scan {
9648 label_id,
9649 labels,
9650 variable,
9651 filter,
9652 optional,
9653 } => {
9654 let kind = if labels.len() == 1
9658 && let Some(col) = filter
9659 .as_ref()
9660 .and_then(|f| equality_target_column(f, &variable))
9661 && let Some(idx_kind) = lookup.fork_index_for(&labels[0], &col)
9662 {
9663 into_fusion_kind(idx_kind)
9664 } else {
9665 None
9666 };
9667 match kind {
9668 Some(kind) => LogicalPlan::FusedIndexScan {
9669 label_id,
9670 labels,
9671 variable,
9672 filter,
9673 optional,
9674 kind,
9675 },
9676 None => LogicalPlan::Scan {
9677 label_id,
9678 labels,
9679 variable,
9680 filter,
9681 optional,
9682 },
9683 }
9684 }
9685 LogicalPlan::ProcedureCall {
9698 procedure_name,
9699 arguments,
9700 yield_items,
9701 } => {
9702 let kind = procedure_call_fusion_kind(&procedure_name, &arguments, lookup);
9703 let inner = LogicalPlan::ProcedureCall {
9704 procedure_name,
9705 arguments,
9706 yield_items,
9707 };
9708 match kind {
9709 Some(kind) => LogicalPlan::FusedIndexScanWrapped {
9710 inner: Box::new(inner),
9711 kind,
9712 },
9713 None => inner,
9714 }
9715 }
9716 LogicalPlan::VectorKnn {
9717 label_id,
9718 variable,
9719 property,
9720 query,
9721 k,
9722 threshold,
9723 } => {
9724 if let Some(idx_kind) = lookup.fork_index_for_label_id(label_id, &property)
9725 && let Some(kind) = into_fusion_kind(idx_kind)
9726 {
9727 LogicalPlan::FusedIndexScanWrapped {
9728 inner: Box::new(LogicalPlan::VectorKnn {
9729 label_id,
9730 variable,
9731 property,
9732 query,
9733 k,
9734 threshold,
9735 }),
9736 kind,
9737 }
9738 } else {
9739 LogicalPlan::VectorKnn {
9740 label_id,
9741 variable,
9742 property,
9743 query,
9744 k,
9745 threshold,
9746 }
9747 }
9748 }
9749 LogicalPlan::InvertedIndexLookup {
9750 label_id,
9751 variable,
9752 property,
9753 terms,
9754 } => {
9755 if let Some(idx_kind) = lookup.fork_index_for_label_id(label_id, &property)
9756 && let Some(kind) = into_fusion_kind(idx_kind)
9757 {
9758 LogicalPlan::FusedIndexScanWrapped {
9759 inner: Box::new(LogicalPlan::InvertedIndexLookup {
9760 label_id,
9761 variable,
9762 property,
9763 terms,
9764 }),
9765 kind,
9766 }
9767 } else {
9768 LogicalPlan::InvertedIndexLookup {
9769 label_id,
9770 variable,
9771 property,
9772 terms,
9773 }
9774 }
9775 }
9776 LogicalPlan::Filter {
9781 input,
9782 predicate,
9783 optional_variables,
9784 } => LogicalPlan::Filter {
9785 input: Box::new(rewrite_node(*input, lookup)),
9786 predicate,
9787 optional_variables,
9788 },
9789 LogicalPlan::Project { input, projections } => LogicalPlan::Project {
9790 input: Box::new(rewrite_node(*input, lookup)),
9791 projections,
9792 },
9793 LogicalPlan::Limit { input, skip, fetch } => LogicalPlan::Limit {
9794 input: Box::new(rewrite_node(*input, lookup)),
9795 skip,
9796 fetch,
9797 },
9798 LogicalPlan::Sort { input, order_by } => {
9799 let new_input = match (*input, &order_by[..]) {
9806 (
9807 LogicalPlan::Scan {
9808 label_id,
9809 labels,
9810 variable,
9811 filter,
9812 optional,
9813 },
9814 [single_sort],
9815 ) if labels.len() == 1
9816 && let Some(col) = column_of_scan_variable(&single_sort.expr, &variable)
9817 && let Some(uni_store::fork::ForkLocalIndexKind::Sorted) =
9818 lookup.fork_index_for(&labels[0], &col) =>
9819 {
9820 LogicalPlan::FusedIndexScan {
9821 label_id,
9822 labels,
9823 variable,
9824 filter,
9825 optional,
9826 kind: FusionKind::SortedKWayMerge,
9827 }
9828 }
9829 (other_input, _) => rewrite_node(other_input, lookup),
9830 };
9831 LogicalPlan::Sort {
9832 input: Box::new(new_input),
9833 order_by,
9834 }
9835 }
9836 LogicalPlan::Union { left, right, all } => LogicalPlan::Union {
9837 left: Box::new(rewrite_node(*left, lookup)),
9838 right: Box::new(rewrite_node(*right, lookup)),
9839 all,
9840 },
9841 other => other,
9845 }
9846}
9847
9848fn procedure_call_fusion_kind<L: ForkIndexLookup>(
9861 procedure_name: &str,
9862 arguments: &[Expr],
9863 lookup: &L,
9864) -> Option<FusionKind> {
9865 if arguments.len() < 2 {
9866 return None;
9867 }
9868 let label = match &arguments[0] {
9869 Expr::Literal(uni_cypher::ast::CypherLiteral::String(s)) => s.as_str(),
9870 _ => return None,
9871 };
9872 let column = match &arguments[1] {
9873 Expr::Literal(uni_cypher::ast::CypherLiteral::String(s)) => s.as_str(),
9874 _ => return None,
9875 };
9876 let expected = match procedure_name {
9877 "uni.vector.query" => uni_store::fork::ForkLocalIndexKind::Vector,
9878 "uni.fts.query" => uni_store::fork::ForkLocalIndexKind::FullText,
9879 _ => return None,
9880 };
9881 let registered = lookup.fork_index_for(label, column)?;
9882 if registered != expected {
9883 return None;
9884 }
9885 into_fusion_kind(registered)
9886}
9887
9888fn into_fusion_kind(kind: uni_store::fork::ForkLocalIndexKind) -> Option<FusionKind> {
9892 use uni_store::fork::ForkLocalIndexKind as K;
9893 match kind {
9894 K::VidUid => Some(FusionKind::VidUidForkFirst),
9895 K::ScalarBtree => Some(FusionKind::BtreeUnion),
9896 K::Sorted => Some(FusionKind::SortedKWayMerge),
9897 K::Vector => Some(FusionKind::AnnRerank),
9898 K::FullText => Some(FusionKind::Bm25Rrf),
9899 _ => None,
9904 }
9905}
9906
9907fn equality_target_column(filter: &Expr, scan_variable: &str) -> Option<String> {
9913 let (lhs, rhs) = match filter {
9914 Expr::BinaryOp {
9915 left,
9916 op: uni_cypher::ast::BinaryOp::Eq,
9917 right,
9918 } => (left.as_ref(), right.as_ref()),
9919 _ => return None,
9920 };
9921 if let Some(col) = column_of_scan_variable(lhs, scan_variable)
9923 && is_constant_or_param(rhs)
9924 {
9925 return Some(col);
9926 }
9927 if let Some(col) = column_of_scan_variable(rhs, scan_variable)
9928 && is_constant_or_param(lhs)
9929 {
9930 return Some(col);
9931 }
9932 None
9933}
9934
9935fn column_of_scan_variable(expr: &Expr, scan_variable: &str) -> Option<String> {
9936 if let Expr::Property(base, prop) = expr
9937 && let Expr::Variable(v) = base.as_ref()
9938 && v == scan_variable
9939 {
9940 return Some(prop.clone());
9941 }
9942 None
9943}
9944
9945fn is_constant_or_param(expr: &Expr) -> bool {
9946 matches!(expr, Expr::Literal(_) | Expr::Parameter(_))
9947}
9948
9949#[cfg(test)]
9950mod pushdown_tests {
9951 use super::*;
9952
9953 #[test]
9954 fn test_validat_extracts_property_names() {
9955 let mut properties = HashMap::new();
9957
9958 let args = vec![
9959 Expr::Variable("e".to_string()),
9960 Expr::Literal(CypherLiteral::String("start".to_string())),
9961 Expr::Literal(CypherLiteral::String("end".to_string())),
9962 Expr::Variable("ts".to_string()),
9963 ];
9964
9965 analyze_function_property_requirements("uni.temporal.validAt", &args, &mut properties);
9966
9967 assert!(properties.contains_key("e"));
9968 let e_props: HashSet<String> = ["start".to_string(), "end".to_string()]
9969 .iter()
9970 .cloned()
9971 .collect();
9972 assert_eq!(properties.get("e").unwrap(), &e_props);
9973 }
9974
9975 #[test]
9976 fn test_keys_requires_wildcard() {
9977 let mut properties = HashMap::new();
9979
9980 let args = vec![Expr::Variable("n".to_string())];
9981
9982 analyze_function_property_requirements("keys", &args, &mut properties);
9983
9984 assert!(properties.contains_key("n"));
9985 let n_props: HashSet<String> = ["*".to_string()].iter().cloned().collect();
9986 assert_eq!(properties.get("n").unwrap(), &n_props);
9987 }
9988
9989 #[test]
9990 fn test_properties_requires_wildcard() {
9991 let mut properties = HashMap::new();
9993
9994 let args = vec![Expr::Variable("n".to_string())];
9995
9996 analyze_function_property_requirements("properties", &args, &mut properties);
9997
9998 assert!(properties.contains_key("n"));
9999 let n_props: HashSet<String> = ["*".to_string()].iter().cloned().collect();
10000 assert_eq!(properties.get("n").unwrap(), &n_props);
10001 }
10002
10003 #[test]
10004 fn test_unknown_function_conservative() {
10005 let mut properties = HashMap::new();
10007
10008 let args = vec![Expr::Variable("e".to_string())];
10009
10010 analyze_function_property_requirements("customUdf", &args, &mut properties);
10011
10012 assert!(properties.contains_key("e"));
10013 let e_props: HashSet<String> = ["*".to_string()].iter().cloned().collect();
10014 assert_eq!(properties.get("e").unwrap(), &e_props);
10015 }
10016
10017 #[test]
10018 fn test_parameter_property_name() {
10019 let mut properties = HashMap::new();
10021
10022 let args = vec![
10023 Expr::Variable("e".to_string()),
10024 Expr::Parameter("start".to_string()),
10025 Expr::Parameter("end".to_string()),
10026 Expr::Variable("ts".to_string()),
10027 ];
10028
10029 analyze_function_property_requirements("uni.temporal.validAt", &args, &mut properties);
10030
10031 assert!(properties.contains_key("e"));
10032 assert!(properties.get("e").unwrap().contains("*"));
10033 }
10034
10035 #[test]
10036 fn test_validat_expr_extracts_properties() {
10037 let mut properties = HashMap::new();
10039
10040 let validat_expr = Expr::ValidAt {
10041 entity: Box::new(Expr::Variable("e".to_string())),
10042 timestamp: Box::new(Expr::Variable("ts".to_string())),
10043 start_prop: Some("valid_from".to_string()),
10044 end_prop: Some("valid_to".to_string()),
10045 };
10046
10047 collect_properties_from_expr_into(&validat_expr, &mut properties);
10048
10049 assert!(properties.contains_key("e"));
10050 assert!(properties.get("e").unwrap().contains("valid_from"));
10051 assert!(properties.get("e").unwrap().contains("valid_to"));
10052 }
10053
10054 #[test]
10055 fn test_array_index_requires_wildcard() {
10056 let mut properties = HashMap::new();
10058
10059 let array_index_expr = Expr::ArrayIndex {
10060 array: Box::new(Expr::Variable("e".to_string())),
10061 index: Box::new(Expr::Variable("prop".to_string())),
10062 };
10063
10064 collect_properties_from_expr_into(&array_index_expr, &mut properties);
10065
10066 assert!(properties.contains_key("e"));
10067 assert!(properties.get("e").unwrap().contains("*"));
10068 }
10069
10070 #[test]
10071 fn test_property_access_extraction() {
10072 let mut properties = HashMap::new();
10074
10075 let prop_access = Expr::Property(
10076 Box::new(Expr::Variable("e".to_string())),
10077 "name".to_string(),
10078 );
10079
10080 collect_properties_from_expr_into(&prop_access, &mut properties);
10081
10082 assert!(properties.contains_key("e"));
10083 assert!(properties.get("e").unwrap().contains("name"));
10084 }
10085}