1use crate::query::plan::{
11 ExpandOp, FilterOp, LogicalExpression, LogicalOperator, LogicalPlan, NodeScanOp, ReturnItem,
12 ReturnOp, TripleScanOp,
13};
14use grafeo_common::types::LogicalType;
15use grafeo_common::utils::error::{Error, QueryError, QueryErrorKind, Result};
16use grafeo_common::utils::strings::{find_similar, format_suggestion};
17use indexmap::IndexMap;
18use std::collections::HashSet;
19
20fn binding_error(message: impl Into<String>) -> Error {
22 Error::Query(QueryError::new(QueryErrorKind::Semantic, message))
23}
24
25fn binding_error_with_hint(message: impl Into<String>, hint: impl Into<String>) -> Error {
27 Error::Query(QueryError::new(QueryErrorKind::Semantic, message).with_hint(hint))
28}
29
30fn undefined_variable_error(variable: &str, context: &BindingContext, suffix: &str) -> Error {
32 let candidates: Vec<String> = context.variable_names();
33 let candidates_ref: Vec<&str> = candidates.iter().map(|s| s.as_str()).collect();
34
35 if let Some(suggestion) = find_similar(variable, &candidates_ref) {
36 binding_error_with_hint(
37 format!("Undefined variable '{variable}'{suffix}"),
38 format_suggestion(suggestion),
39 )
40 } else {
41 binding_error(format!("Undefined variable '{variable}'{suffix}"))
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct VariableInfo {
48 pub name: String,
50 pub data_type: LogicalType,
52 pub is_node: bool,
54 pub is_edge: bool,
56}
57
58#[derive(Debug, Clone, Default)]
64pub struct BindingContext {
65 variables: IndexMap<String, VariableInfo>,
67}
68
69impl BindingContext {
70 #[must_use]
72 pub fn new() -> Self {
73 Self {
74 variables: IndexMap::new(),
75 }
76 }
77
78 pub fn add_variable(&mut self, name: String, info: VariableInfo) {
83 self.variables.insert(name, info);
84 }
85
86 #[must_use]
88 pub fn get(&self, name: &str) -> Option<&VariableInfo> {
89 self.variables.get(name)
90 }
91
92 #[must_use]
94 pub fn contains(&self, name: &str) -> bool {
95 self.variables.contains_key(name)
96 }
97
98 #[must_use]
100 pub fn variable_names(&self) -> Vec<String> {
101 self.variables.keys().cloned().collect()
102 }
103
104 #[must_use]
106 pub fn len(&self) -> usize {
107 self.variables.len()
108 }
109
110 #[must_use]
112 pub fn is_empty(&self) -> bool {
113 self.variables.is_empty()
114 }
115
116 pub fn remove_variable(&mut self, name: &str) {
118 self.variables.shift_remove(name);
119 }
120}
121
122pub struct Binder {
130 context: BindingContext,
132}
133
134impl Binder {
135 #[must_use]
137 pub fn new() -> Self {
138 Self {
139 context: BindingContext::new(),
140 }
141 }
142
143 pub fn bind(&mut self, plan: &LogicalPlan) -> Result<BindingContext> {
149 self.bind_operator(&plan.root)?;
150 Ok(self.context.clone())
151 }
152
153 fn bind_operator(&mut self, op: &LogicalOperator) -> Result<()> {
155 match op {
156 LogicalOperator::NodeScan(scan) => self.bind_node_scan(scan),
157 LogicalOperator::Expand(expand) => self.bind_expand(expand),
158 LogicalOperator::Filter(filter) => self.bind_filter(filter),
159 LogicalOperator::Return(ret) => self.bind_return(ret),
160 LogicalOperator::Project(project) => {
161 self.bind_operator(&project.input)?;
162 for projection in &project.projections {
163 self.validate_expression(&projection.expression)?;
164 if let Some(ref alias) = projection.alias {
166 let data_type = self.infer_expression_type(&projection.expression);
168 let (is_node, is_edge) = self.infer_entity_status(&projection.expression);
172 self.context.add_variable(
173 alias.clone(),
174 VariableInfo {
175 name: alias.clone(),
176 data_type,
177 is_node,
178 is_edge,
179 },
180 );
181 }
182 }
183 Ok(())
184 }
185 LogicalOperator::Limit(limit) => self.bind_operator(&limit.input),
186 LogicalOperator::Skip(skip) => self.bind_operator(&skip.input),
187 LogicalOperator::Sort(sort) => {
188 self.bind_operator(&sort.input)?;
189 for key in &sort.keys {
190 self.validate_expression(&key.expression)?;
191 }
192 Ok(())
193 }
194 LogicalOperator::CreateNode(create) => {
195 if let Some(ref input) = create.input {
197 self.bind_operator(input)?;
198 }
199 self.context.add_variable(
200 create.variable.clone(),
201 VariableInfo {
202 name: create.variable.clone(),
203 data_type: LogicalType::Node,
204 is_node: true,
205 is_edge: false,
206 },
207 );
208 for (_, expr) in &create.properties {
210 self.validate_expression(expr)?;
211 }
212 Ok(())
213 }
214 LogicalOperator::EdgeScan(scan) => {
215 if let Some(ref input) = scan.input {
216 self.bind_operator(input)?;
217 }
218 self.context.add_variable(
219 scan.variable.clone(),
220 VariableInfo {
221 name: scan.variable.clone(),
222 data_type: LogicalType::Edge,
223 is_node: false,
224 is_edge: true,
225 },
226 );
227 Ok(())
228 }
229 LogicalOperator::Distinct(distinct) => self.bind_operator(&distinct.input),
230 LogicalOperator::Join(join) => self.bind_join(join),
231 LogicalOperator::Aggregate(agg) => self.bind_aggregate(agg),
232 LogicalOperator::CreateEdge(create) => {
233 self.bind_operator(&create.input)?;
234 if !self.context.contains(&create.from_variable) {
236 return Err(undefined_variable_error(
237 &create.from_variable,
238 &self.context,
239 " (source in CREATE EDGE)",
240 ));
241 }
242 if !self.context.contains(&create.to_variable) {
243 return Err(undefined_variable_error(
244 &create.to_variable,
245 &self.context,
246 " (target in CREATE EDGE)",
247 ));
248 }
249 if let Some(ref var) = create.variable {
251 self.context.add_variable(
252 var.clone(),
253 VariableInfo {
254 name: var.clone(),
255 data_type: LogicalType::Edge,
256 is_node: false,
257 is_edge: true,
258 },
259 );
260 }
261 for (_, expr) in &create.properties {
263 self.validate_expression(expr)?;
264 }
265 Ok(())
266 }
267 LogicalOperator::DeleteNode(delete) => {
268 self.bind_operator(&delete.input)?;
269 if !self.context.contains(&delete.variable) {
271 return Err(undefined_variable_error(
272 &delete.variable,
273 &self.context,
274 " in DELETE",
275 ));
276 }
277 Ok(())
278 }
279 LogicalOperator::DeleteEdge(delete) => {
280 self.bind_operator(&delete.input)?;
281 if !self.context.contains(&delete.variable) {
283 return Err(undefined_variable_error(
284 &delete.variable,
285 &self.context,
286 " in DELETE",
287 ));
288 }
289 Ok(())
290 }
291 LogicalOperator::SetProperty(set) => {
292 self.bind_operator(&set.input)?;
293 if !self.context.contains(&set.variable) {
295 return Err(undefined_variable_error(
296 &set.variable,
297 &self.context,
298 " in SET",
299 ));
300 }
301 for (_, expr) in &set.properties {
303 self.validate_expression(expr)?;
304 }
305 Ok(())
306 }
307 LogicalOperator::Empty => Ok(()),
308
309 LogicalOperator::Unwind(unwind) => {
310 self.bind_operator(&unwind.input)?;
312 self.validate_expression(&unwind.expression)?;
314 self.context.add_variable(
316 unwind.variable.clone(),
317 VariableInfo {
318 name: unwind.variable.clone(),
319 data_type: LogicalType::Any, is_node: false,
321 is_edge: false,
322 },
323 );
324 if let Some(ref ord_var) = unwind.ordinality_var {
326 self.context.add_variable(
327 ord_var.clone(),
328 VariableInfo {
329 name: ord_var.clone(),
330 data_type: LogicalType::Int64,
331 is_node: false,
332 is_edge: false,
333 },
334 );
335 }
336 if let Some(ref off_var) = unwind.offset_var {
338 self.context.add_variable(
339 off_var.clone(),
340 VariableInfo {
341 name: off_var.clone(),
342 data_type: LogicalType::Int64,
343 is_node: false,
344 is_edge: false,
345 },
346 );
347 }
348 Ok(())
349 }
350
351 LogicalOperator::TripleScan(scan) => self.bind_triple_scan(scan),
353 LogicalOperator::Union(union) => {
354 for input in &union.inputs {
355 self.bind_operator(input)?;
356 }
357 Ok(())
358 }
359 LogicalOperator::LeftJoin(lj) => {
360 self.bind_operator(&lj.left)?;
361 self.bind_operator(&lj.right)?;
362 if let Some(ref cond) = lj.condition {
363 self.validate_expression(cond)?;
364 }
365 Ok(())
366 }
367 LogicalOperator::AntiJoin(aj) => {
368 self.bind_operator(&aj.left)?;
369 self.bind_operator(&aj.right)?;
370 Ok(())
371 }
372 LogicalOperator::Bind(bind) => {
373 self.bind_operator(&bind.input)?;
374 self.validate_expression(&bind.expression)?;
375 self.context.add_variable(
376 bind.variable.clone(),
377 VariableInfo {
378 name: bind.variable.clone(),
379 data_type: LogicalType::Any,
380 is_node: false,
381 is_edge: false,
382 },
383 );
384 Ok(())
385 }
386 LogicalOperator::Merge(merge) => {
387 self.bind_operator(&merge.input)?;
389 for (_, expr) in &merge.match_properties {
391 self.validate_expression(expr)?;
392 }
393 for (_, expr) in &merge.on_create {
395 self.validate_expression(expr)?;
396 }
397 for (_, expr) in &merge.on_match {
399 self.validate_expression(expr)?;
400 }
401 self.context.add_variable(
403 merge.variable.clone(),
404 VariableInfo {
405 name: merge.variable.clone(),
406 data_type: LogicalType::Node,
407 is_node: true,
408 is_edge: false,
409 },
410 );
411 Ok(())
412 }
413 LogicalOperator::MergeRelationship(merge_rel) => {
414 self.bind_operator(&merge_rel.input)?;
415 if !self.context.contains(&merge_rel.source_variable) {
417 return Err(undefined_variable_error(
418 &merge_rel.source_variable,
419 &self.context,
420 " in MERGE relationship source",
421 ));
422 }
423 if !self.context.contains(&merge_rel.target_variable) {
424 return Err(undefined_variable_error(
425 &merge_rel.target_variable,
426 &self.context,
427 " in MERGE relationship target",
428 ));
429 }
430 for (_, expr) in &merge_rel.match_properties {
431 self.validate_expression(expr)?;
432 }
433 for (_, expr) in &merge_rel.on_create {
434 self.validate_expression(expr)?;
435 }
436 for (_, expr) in &merge_rel.on_match {
437 self.validate_expression(expr)?;
438 }
439 self.context.add_variable(
441 merge_rel.variable.clone(),
442 VariableInfo {
443 name: merge_rel.variable.clone(),
444 data_type: LogicalType::Edge,
445 is_node: false,
446 is_edge: true,
447 },
448 );
449 Ok(())
450 }
451 LogicalOperator::AddLabel(add_label) => {
452 self.bind_operator(&add_label.input)?;
453 if !self.context.contains(&add_label.variable) {
455 return Err(undefined_variable_error(
456 &add_label.variable,
457 &self.context,
458 " in SET labels",
459 ));
460 }
461 Ok(())
462 }
463 LogicalOperator::RemoveLabel(remove_label) => {
464 self.bind_operator(&remove_label.input)?;
465 if !self.context.contains(&remove_label.variable) {
467 return Err(undefined_variable_error(
468 &remove_label.variable,
469 &self.context,
470 " in REMOVE labels",
471 ));
472 }
473 Ok(())
474 }
475 LogicalOperator::ShortestPath(sp) => {
476 self.bind_operator(&sp.input)?;
478 if !self.context.contains(&sp.source_var) {
480 return Err(undefined_variable_error(
481 &sp.source_var,
482 &self.context,
483 " (source in shortestPath)",
484 ));
485 }
486 if !self.context.contains(&sp.target_var) {
487 return Err(undefined_variable_error(
488 &sp.target_var,
489 &self.context,
490 " (target in shortestPath)",
491 ));
492 }
493 self.context.add_variable(
495 sp.path_alias.clone(),
496 VariableInfo {
497 name: sp.path_alias.clone(),
498 data_type: LogicalType::Any, is_node: false,
500 is_edge: false,
501 },
502 );
503 let path_length_var = format!("_path_length_{}", sp.path_alias);
505 self.context.add_variable(
506 path_length_var.clone(),
507 VariableInfo {
508 name: path_length_var,
509 data_type: LogicalType::Int64,
510 is_node: false,
511 is_edge: false,
512 },
513 );
514 Ok(())
515 }
516 LogicalOperator::InsertTriple(insert) => {
518 if let Some(ref input) = insert.input {
519 self.bind_operator(input)?;
520 }
521 Ok(())
522 }
523 LogicalOperator::DeleteTriple(delete) => {
524 if let Some(ref input) = delete.input {
525 self.bind_operator(input)?;
526 }
527 Ok(())
528 }
529 LogicalOperator::Modify(modify) => {
530 self.bind_operator(&modify.where_clause)?;
531 Ok(())
532 }
533 LogicalOperator::ClearGraph(_)
534 | LogicalOperator::CreateGraph(_)
535 | LogicalOperator::DropGraph(_)
536 | LogicalOperator::LoadGraph(_)
537 | LogicalOperator::CopyGraph(_)
538 | LogicalOperator::MoveGraph(_)
539 | LogicalOperator::AddGraph(_)
540 | LogicalOperator::HorizontalAggregate(_) => Ok(()),
541 LogicalOperator::VectorScan(scan) => {
542 if let Some(ref input) = scan.input {
544 self.bind_operator(input)?;
545 }
546 self.context.add_variable(
547 scan.variable.clone(),
548 VariableInfo {
549 name: scan.variable.clone(),
550 data_type: LogicalType::Node,
551 is_node: true,
552 is_edge: false,
553 },
554 );
555 self.validate_expression(&scan.query_vector)?;
557 Ok(())
558 }
559 LogicalOperator::VectorJoin(join) => {
560 self.bind_operator(&join.input)?;
562 self.context.add_variable(
564 join.right_variable.clone(),
565 VariableInfo {
566 name: join.right_variable.clone(),
567 data_type: LogicalType::Node,
568 is_node: true,
569 is_edge: false,
570 },
571 );
572 if let Some(ref score_var) = join.score_variable {
574 self.context.add_variable(
575 score_var.clone(),
576 VariableInfo {
577 name: score_var.clone(),
578 data_type: LogicalType::Float64,
579 is_node: false,
580 is_edge: false,
581 },
582 );
583 }
584 self.validate_expression(&join.query_vector)?;
586 Ok(())
587 }
588 LogicalOperator::MapCollect(mc) => {
589 self.bind_operator(&mc.input)?;
590 self.context.add_variable(
591 mc.alias.clone(),
592 VariableInfo {
593 name: mc.alias.clone(),
594 data_type: LogicalType::Any,
595 is_node: false,
596 is_edge: false,
597 },
598 );
599 Ok(())
600 }
601 LogicalOperator::Except(except) => {
602 self.bind_operator(&except.left)?;
603 self.bind_operator(&except.right)?;
604 Ok(())
605 }
606 LogicalOperator::Intersect(intersect) => {
607 self.bind_operator(&intersect.left)?;
608 self.bind_operator(&intersect.right)?;
609 Ok(())
610 }
611 LogicalOperator::Otherwise(otherwise) => {
612 self.bind_operator(&otherwise.left)?;
613 self.bind_operator(&otherwise.right)?;
614 Ok(())
615 }
616 LogicalOperator::Apply(apply) => {
617 let pre_apply_names: HashSet<String> =
620 self.context.variable_names().iter().cloned().collect();
621
622 self.bind_operator(&apply.input)?;
623
624 let mut input_output_ctx = BindingContext::new();
630 Self::register_subplan_columns(&apply.input, &mut input_output_ctx);
631 let input_output_names: HashSet<String> =
632 input_output_ctx.variable_names().iter().cloned().collect();
633
634 if !input_output_names.is_empty() {
635 let input_internals: Vec<String> = self
637 .context
638 .variable_names()
639 .iter()
640 .filter(|n| {
641 !pre_apply_names.contains(*n) && !input_output_names.contains(*n)
642 })
643 .cloned()
644 .collect();
645 for name in input_internals {
646 self.context.remove_variable(&name);
647 }
648 }
649
650 let outer_names: HashSet<String> =
652 self.context.variable_names().iter().cloned().collect();
653
654 self.bind_operator(&apply.subplan)?;
655
656 let mut subplan_output_ctx = BindingContext::new();
660 Self::register_subplan_columns(&apply.subplan, &mut subplan_output_ctx);
661 let subplan_output_names: HashSet<String> = subplan_output_ctx
662 .variable_names()
663 .iter()
664 .cloned()
665 .collect();
666
667 let to_remove: Vec<String> = self
668 .context
669 .variable_names()
670 .iter()
671 .filter(|n| !outer_names.contains(*n) && !subplan_output_names.contains(*n))
672 .cloned()
673 .collect();
674 for name in to_remove {
675 self.context.remove_variable(&name);
676 }
677
678 Self::register_subplan_columns(&apply.subplan, &mut self.context);
680 Ok(())
681 }
682 LogicalOperator::MultiWayJoin(mwj) => {
683 for input in &mwj.inputs {
684 self.bind_operator(input)?;
685 }
686 for cond in &mwj.conditions {
687 self.validate_expression(&cond.left)?;
688 self.validate_expression(&cond.right)?;
689 }
690 Ok(())
691 }
692 LogicalOperator::ParameterScan(param_scan) => {
693 for col in ¶m_scan.columns {
695 self.context.add_variable(
696 col.clone(),
697 VariableInfo {
698 name: col.clone(),
699 data_type: LogicalType::Any,
700 is_node: true,
701 is_edge: false,
702 },
703 );
704 }
705 Ok(())
706 }
707 LogicalOperator::CreatePropertyGraph(_) => Ok(()),
709 LogicalOperator::CallProcedure(call) => {
711 if let Some(yields) = &call.yield_items {
712 for item in yields {
713 let var_name = item.alias.as_deref().unwrap_or(&item.field_name);
714 self.context.add_variable(
715 var_name.to_string(),
716 VariableInfo {
717 name: var_name.to_string(),
718 data_type: LogicalType::Any,
719 is_node: false,
720 is_edge: false,
721 },
722 );
723 }
724 }
725 Ok(())
726 }
727 LogicalOperator::LoadData(load) => {
728 self.context.add_variable(
730 load.variable.clone(),
731 VariableInfo {
732 name: load.variable.clone(),
733 data_type: LogicalType::Any,
734 is_node: false,
735 is_edge: false,
736 },
737 );
738 Ok(())
739 }
740 LogicalOperator::Construct(construct) => self.bind_operator(&construct.input),
741 LogicalOperator::TextScan(scan) => {
742 self.context.add_variable(
743 scan.variable.clone(),
744 VariableInfo {
745 name: scan.variable.clone(),
746 data_type: LogicalType::Node,
747 is_node: true,
748 is_edge: false,
749 },
750 );
751 if let Some(ref score_col) = scan.score_column {
752 self.context.add_variable(
753 score_col.clone(),
754 VariableInfo {
755 name: score_col.clone(),
756 data_type: LogicalType::Float64,
757 is_node: false,
758 is_edge: false,
759 },
760 );
761 }
762 self.validate_expression(&scan.query)?;
763 Ok(())
764 }
765 }
766 }
767
768 fn bind_triple_scan(&mut self, scan: &TripleScanOp) -> Result<()> {
770 use crate::query::plan::TripleComponent;
771
772 if let Some(ref input) = scan.input {
774 self.bind_operator(input)?;
775 }
776
777 if let TripleComponent::Variable(name) = &scan.subject
779 && !self.context.contains(name)
780 {
781 self.context.add_variable(
782 name.clone(),
783 VariableInfo {
784 name: name.clone(),
785 data_type: LogicalType::Any, is_node: false,
787 is_edge: false,
788 },
789 );
790 }
791
792 if let TripleComponent::Variable(name) = &scan.predicate
793 && !self.context.contains(name)
794 {
795 self.context.add_variable(
796 name.clone(),
797 VariableInfo {
798 name: name.clone(),
799 data_type: LogicalType::Any, is_node: false,
801 is_edge: false,
802 },
803 );
804 }
805
806 if let TripleComponent::Variable(name) = &scan.object
807 && !self.context.contains(name)
808 {
809 self.context.add_variable(
810 name.clone(),
811 VariableInfo {
812 name: name.clone(),
813 data_type: LogicalType::Any, is_node: false,
815 is_edge: false,
816 },
817 );
818 }
819
820 if let Some(TripleComponent::Variable(name)) = &scan.graph
821 && !self.context.contains(name)
822 {
823 self.context.add_variable(
824 name.clone(),
825 VariableInfo {
826 name: name.clone(),
827 data_type: LogicalType::Any, is_node: false,
829 is_edge: false,
830 },
831 );
832 }
833
834 Ok(())
835 }
836
837 fn bind_node_scan(&mut self, scan: &NodeScanOp) -> Result<()> {
839 if let Some(ref input) = scan.input {
841 self.bind_operator(input)?;
842 }
843
844 self.context.add_variable(
846 scan.variable.clone(),
847 VariableInfo {
848 name: scan.variable.clone(),
849 data_type: LogicalType::Node,
850 is_node: true,
851 is_edge: false,
852 },
853 );
854
855 Ok(())
856 }
857
858 fn bind_expand(&mut self, expand: &ExpandOp) -> Result<()> {
860 self.bind_operator(&expand.input)?;
862
863 if !self.context.contains(&expand.from_variable) {
865 return Err(undefined_variable_error(
866 &expand.from_variable,
867 &self.context,
868 " in EXPAND",
869 ));
870 }
871
872 if let Some(info) = self.context.get(&expand.from_variable)
874 && !info.is_node
875 {
876 return Err(binding_error(format!(
877 "Variable '{}' is not a node, cannot expand from it",
878 expand.from_variable
879 )));
880 }
881
882 if let Some(ref edge_var) = expand.edge_variable {
884 self.context.add_variable(
885 edge_var.clone(),
886 VariableInfo {
887 name: edge_var.clone(),
888 data_type: LogicalType::Edge,
889 is_node: false,
890 is_edge: true,
891 },
892 );
893 }
894
895 self.context.add_variable(
897 expand.to_variable.clone(),
898 VariableInfo {
899 name: expand.to_variable.clone(),
900 data_type: LogicalType::Node,
901 is_node: true,
902 is_edge: false,
903 },
904 );
905
906 if let Some(ref path_alias) = expand.path_alias {
908 self.context.add_variable(
910 path_alias.clone(),
911 VariableInfo {
912 name: path_alias.clone(),
913 data_type: LogicalType::Any,
914 is_node: false,
915 is_edge: false,
916 },
917 );
918 let path_length_var = format!("_path_length_{}", path_alias);
920 self.context.add_variable(
921 path_length_var.clone(),
922 VariableInfo {
923 name: path_length_var,
924 data_type: LogicalType::Int64,
925 is_node: false,
926 is_edge: false,
927 },
928 );
929 let path_nodes_var = format!("_path_nodes_{}", path_alias);
931 self.context.add_variable(
932 path_nodes_var.clone(),
933 VariableInfo {
934 name: path_nodes_var,
935 data_type: LogicalType::Any,
936 is_node: false,
937 is_edge: false,
938 },
939 );
940 let path_edges_var = format!("_path_edges_{}", path_alias);
942 self.context.add_variable(
943 path_edges_var.clone(),
944 VariableInfo {
945 name: path_edges_var,
946 data_type: LogicalType::Any,
947 is_node: false,
948 is_edge: false,
949 },
950 );
951 }
952
953 Ok(())
954 }
955
956 fn bind_filter(&mut self, filter: &FilterOp) -> Result<()> {
958 self.bind_operator(&filter.input)?;
960
961 self.validate_expression(&filter.predicate)?;
963
964 Ok(())
965 }
966
967 fn register_subplan_columns(plan: &LogicalOperator, ctx: &mut BindingContext) {
970 match plan {
971 LogicalOperator::Return(ret) => {
972 for item in &ret.items {
973 let col_name = if let Some(alias) = &item.alias {
974 alias.clone()
975 } else {
976 match &item.expression {
977 LogicalExpression::Variable(name) => name.clone(),
978 LogicalExpression::Property { variable, property } => {
979 format!("{variable}.{property}")
980 }
981 _ => continue,
982 }
983 };
984 ctx.add_variable(
985 col_name.clone(),
986 VariableInfo {
987 name: col_name,
988 data_type: LogicalType::Any,
989 is_node: false,
990 is_edge: false,
991 },
992 );
993 }
994 }
995 LogicalOperator::Sort(s) => Self::register_subplan_columns(&s.input, ctx),
996 LogicalOperator::Limit(l) => Self::register_subplan_columns(&l.input, ctx),
997 LogicalOperator::Distinct(d) => Self::register_subplan_columns(&d.input, ctx),
998 LogicalOperator::Aggregate(agg) => {
999 for expr in &agg.aggregates {
1001 if let Some(alias) = &expr.alias {
1002 ctx.add_variable(
1003 alias.clone(),
1004 VariableInfo {
1005 name: alias.clone(),
1006 data_type: LogicalType::Any,
1007 is_node: false,
1008 is_edge: false,
1009 },
1010 );
1011 }
1012 }
1013 }
1014 _ => {}
1015 }
1016 }
1017
1018 fn bind_return(&mut self, ret: &ReturnOp) -> Result<()> {
1020 self.bind_operator(&ret.input)?;
1022
1023 for item in &ret.items {
1026 self.validate_return_item(item)?;
1027 if let Some(ref alias) = item.alias {
1028 let data_type = self.infer_expression_type(&item.expression);
1029 self.context.add_variable(
1030 alias.clone(),
1031 VariableInfo {
1032 name: alias.clone(),
1033 data_type,
1034 is_node: false,
1035 is_edge: false,
1036 },
1037 );
1038 }
1039 }
1040
1041 Ok(())
1042 }
1043
1044 fn validate_return_item(&mut self, item: &ReturnItem) -> Result<()> {
1046 self.validate_expression(&item.expression)
1047 }
1048
1049 fn validate_expression(&mut self, expr: &LogicalExpression) -> Result<()> {
1051 match expr {
1052 LogicalExpression::Variable(name) => {
1053 if name == "*" {
1055 return Ok(());
1056 }
1057 if !self.context.contains(name) && !name.starts_with("_anon_") {
1058 return Err(undefined_variable_error(name, &self.context, ""));
1059 }
1060 Ok(())
1061 }
1062 LogicalExpression::Property { variable, .. } => {
1063 if !self.context.contains(variable) && !variable.starts_with("_anon_") {
1064 return Err(undefined_variable_error(
1065 variable,
1066 &self.context,
1067 " in property access",
1068 ));
1069 }
1070 Ok(())
1071 }
1072 LogicalExpression::Literal(_) => Ok(()),
1073 LogicalExpression::Binary { left, right, .. } => {
1074 self.validate_expression(left)?;
1075 self.validate_expression(right)
1076 }
1077 LogicalExpression::Unary { operand, .. } => self.validate_expression(operand),
1078 LogicalExpression::FunctionCall { args, .. } => {
1079 for arg in args {
1080 self.validate_expression(arg)?;
1081 }
1082 Ok(())
1083 }
1084 LogicalExpression::List(items) => {
1085 for item in items {
1086 self.validate_expression(item)?;
1087 }
1088 Ok(())
1089 }
1090 LogicalExpression::Map(pairs) => {
1091 for (_, value) in pairs {
1092 self.validate_expression(value)?;
1093 }
1094 Ok(())
1095 }
1096 LogicalExpression::IndexAccess { base, index } => {
1097 self.validate_expression(base)?;
1098 self.validate_expression(index)
1099 }
1100 LogicalExpression::SliceAccess { base, start, end } => {
1101 self.validate_expression(base)?;
1102 if let Some(s) = start {
1103 self.validate_expression(s)?;
1104 }
1105 if let Some(e) = end {
1106 self.validate_expression(e)?;
1107 }
1108 Ok(())
1109 }
1110 LogicalExpression::Case {
1111 operand,
1112 when_clauses,
1113 else_clause,
1114 } => {
1115 if let Some(op) = operand {
1116 self.validate_expression(op)?;
1117 }
1118 for (cond, result) in when_clauses {
1119 self.validate_expression(cond)?;
1120 self.validate_expression(result)?;
1121 }
1122 if let Some(else_expr) = else_clause {
1123 self.validate_expression(else_expr)?;
1124 }
1125 Ok(())
1126 }
1127 LogicalExpression::Parameter(_) => Ok(()),
1129 LogicalExpression::Labels(var)
1131 | LogicalExpression::Type(var)
1132 | LogicalExpression::Id(var) => {
1133 if !self.context.contains(var) && !var.starts_with("_anon_") {
1134 return Err(undefined_variable_error(var, &self.context, " in function"));
1135 }
1136 Ok(())
1137 }
1138 LogicalExpression::ListComprehension { list_expr, .. } => {
1139 self.validate_expression(list_expr)?;
1143 Ok(())
1144 }
1145 LogicalExpression::ListPredicate { list_expr, .. } => {
1146 self.validate_expression(list_expr)?;
1150 Ok(())
1151 }
1152 LogicalExpression::ExistsSubquery(subquery)
1153 | LogicalExpression::CountSubquery(subquery)
1154 | LogicalExpression::ValueSubquery(subquery) => {
1155 let _ = subquery; Ok(())
1159 }
1160 LogicalExpression::PatternComprehension {
1161 subplan,
1162 projection,
1163 } => {
1164 self.bind_operator(subplan)?;
1166 self.validate_expression(projection)
1168 }
1169 LogicalExpression::MapProjection { base, entries } => {
1170 if !self.context.contains(base) && !base.starts_with("_anon_") {
1171 return Err(undefined_variable_error(
1172 base,
1173 &self.context,
1174 " in map projection",
1175 ));
1176 }
1177 for entry in entries {
1178 if let crate::query::plan::MapProjectionEntry::LiteralEntry(_, expr) = entry {
1179 self.validate_expression(expr)?;
1180 }
1181 }
1182 Ok(())
1183 }
1184 LogicalExpression::Reduce {
1185 accumulator,
1186 initial,
1187 variable,
1188 list,
1189 expression,
1190 } => {
1191 self.validate_expression(initial)?;
1192 self.validate_expression(list)?;
1193 let had_acc = self.context.contains(accumulator);
1196 let had_var = self.context.contains(variable);
1197 if !had_acc {
1198 self.context.add_variable(
1199 accumulator.clone(),
1200 VariableInfo {
1201 name: accumulator.clone(),
1202 data_type: LogicalType::Any,
1203 is_node: false,
1204 is_edge: false,
1205 },
1206 );
1207 }
1208 if !had_var {
1209 self.context.add_variable(
1210 variable.clone(),
1211 VariableInfo {
1212 name: variable.clone(),
1213 data_type: LogicalType::Any,
1214 is_node: false,
1215 is_edge: false,
1216 },
1217 );
1218 }
1219 self.validate_expression(expression)?;
1220 if !had_acc {
1221 self.context.remove_variable(accumulator);
1222 }
1223 if !had_var {
1224 self.context.remove_variable(variable);
1225 }
1226 Ok(())
1227 }
1228 }
1229 }
1230
1231 fn infer_expression_type(&self, expr: &LogicalExpression) -> LogicalType {
1233 match expr {
1234 LogicalExpression::Variable(name) => {
1235 self.context
1237 .get(name)
1238 .map_or(LogicalType::Any, |info| info.data_type.clone())
1239 }
1240 LogicalExpression::Property { .. } => LogicalType::Any, LogicalExpression::Literal(value) => {
1242 use grafeo_common::types::Value;
1244 match value {
1245 Value::Bool(_) => LogicalType::Bool,
1246 Value::Int64(_) => LogicalType::Int64,
1247 Value::Float64(_) => LogicalType::Float64,
1248 Value::String(_) => LogicalType::String,
1249 Value::List(_) => LogicalType::Any, Value::Map(_) => LogicalType::Any, Value::Null => LogicalType::Any,
1252 _ => LogicalType::Any,
1253 }
1254 }
1255 LogicalExpression::Binary { .. } => LogicalType::Any, LogicalExpression::Unary { .. } => LogicalType::Any,
1257 LogicalExpression::FunctionCall { name, .. } => {
1258 match name.to_lowercase().as_str() {
1260 "count" | "sum" | "id" => LogicalType::Int64,
1261 "avg" => LogicalType::Float64,
1262 "type" => LogicalType::String,
1263 "labels" | "collect" => LogicalType::Any,
1265 _ => LogicalType::Any,
1266 }
1267 }
1268 LogicalExpression::List(_) => LogicalType::Any, LogicalExpression::Map(_) => LogicalType::Any, _ => LogicalType::Any,
1271 }
1272 }
1273
1274 fn infer_entity_status(&self, expr: &LogicalExpression) -> (bool, bool) {
1280 match expr {
1281 LogicalExpression::Variable(src) => self
1282 .context
1283 .get(src)
1284 .map_or((false, false), |info| (info.is_node, info.is_edge)),
1285 LogicalExpression::Case {
1286 when_clauses,
1287 else_clause,
1288 ..
1289 } => {
1290 let mut all_node = true;
1292 let mut all_edge = true;
1293 let mut any_branch = false;
1294 for (_, then_expr) in when_clauses {
1295 let (n, e) = self.infer_entity_status(then_expr);
1296 all_node &= n;
1297 all_edge &= e;
1298 any_branch = true;
1299 }
1300 if let Some(else_expr) = else_clause {
1301 let (n, e) = self.infer_entity_status(else_expr);
1302 all_node &= n;
1303 all_edge &= e;
1304 any_branch = true;
1305 }
1306 if any_branch {
1307 (all_node, all_edge)
1308 } else {
1309 (false, false)
1310 }
1311 }
1312 _ => (false, false),
1313 }
1314 }
1315
1316 fn bind_join(&mut self, join: &crate::query::plan::JoinOp) -> Result<()> {
1318 self.bind_operator(&join.left)?;
1320 self.bind_operator(&join.right)?;
1321
1322 for condition in &join.conditions {
1324 self.validate_expression(&condition.left)?;
1325 self.validate_expression(&condition.right)?;
1326 }
1327
1328 Ok(())
1329 }
1330
1331 fn bind_aggregate(&mut self, agg: &crate::query::plan::AggregateOp) -> Result<()> {
1333 self.bind_operator(&agg.input)?;
1335
1336 for expr in &agg.group_by {
1338 self.validate_expression(expr)?;
1339 }
1340
1341 for agg_expr in &agg.aggregates {
1343 if let Some(ref expr) = agg_expr.expression {
1344 self.validate_expression(expr)?;
1345 }
1346 if let Some(ref alias) = agg_expr.alias {
1348 self.context.add_variable(
1349 alias.clone(),
1350 VariableInfo {
1351 name: alias.clone(),
1352 data_type: LogicalType::Any,
1353 is_node: false,
1354 is_edge: false,
1355 },
1356 );
1357 }
1358 }
1359
1360 for expr in &agg.group_by {
1363 let col_name = crate::query::planner::common::expression_to_string(expr);
1364 if !self.context.contains(&col_name) {
1365 self.context.add_variable(
1366 col_name.clone(),
1367 VariableInfo {
1368 name: col_name,
1369 data_type: LogicalType::Any,
1370 is_node: false,
1371 is_edge: false,
1372 },
1373 );
1374 }
1375 }
1376
1377 Ok(())
1378 }
1379}
1380
1381impl Default for Binder {
1382 fn default() -> Self {
1383 Self::new()
1384 }
1385}
1386
1387#[cfg(test)]
1388mod tests {
1389 use super::*;
1390 use crate::query::plan::{BinaryOp, FilterOp};
1391
1392 #[test]
1393 fn test_bind_simple_scan() {
1394 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1395 items: vec![ReturnItem {
1396 expression: LogicalExpression::Variable("n".to_string()),
1397 alias: None,
1398 }],
1399 distinct: false,
1400 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1401 variable: "n".to_string(),
1402 label: Some("Person".to_string()),
1403 input: None,
1404 })),
1405 }));
1406
1407 let mut binder = Binder::new();
1408 let result = binder.bind(&plan);
1409
1410 assert!(result.is_ok());
1411 let ctx = result.unwrap();
1412 assert!(ctx.contains("n"));
1413 assert!(ctx.get("n").unwrap().is_node);
1414 }
1415
1416 #[test]
1417 fn test_bind_undefined_variable() {
1418 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1419 items: vec![ReturnItem {
1420 expression: LogicalExpression::Variable("undefined".to_string()),
1421 alias: None,
1422 }],
1423 distinct: false,
1424 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1425 variable: "n".to_string(),
1426 label: None,
1427 input: None,
1428 })),
1429 }));
1430
1431 let mut binder = Binder::new();
1432 let result = binder.bind(&plan);
1433
1434 assert!(result.is_err());
1435 let err = result.unwrap_err();
1436 assert!(err.to_string().contains("Undefined variable"));
1437 }
1438
1439 #[test]
1440 fn test_bind_property_access() {
1441 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1442 items: vec![ReturnItem {
1443 expression: LogicalExpression::Property {
1444 variable: "n".to_string(),
1445 property: "name".to_string(),
1446 },
1447 alias: None,
1448 }],
1449 distinct: false,
1450 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1451 variable: "n".to_string(),
1452 label: Some("Person".to_string()),
1453 input: None,
1454 })),
1455 }));
1456
1457 let mut binder = Binder::new();
1458 let result = binder.bind(&plan);
1459
1460 assert!(result.is_ok());
1461 }
1462
1463 #[test]
1464 fn test_bind_filter_with_undefined_variable() {
1465 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1466 items: vec![ReturnItem {
1467 expression: LogicalExpression::Variable("n".to_string()),
1468 alias: None,
1469 }],
1470 distinct: false,
1471 input: Box::new(LogicalOperator::Filter(FilterOp {
1472 predicate: LogicalExpression::Binary {
1473 left: Box::new(LogicalExpression::Property {
1474 variable: "m".to_string(), property: "age".to_string(),
1476 }),
1477 op: BinaryOp::Gt,
1478 right: Box::new(LogicalExpression::Literal(
1479 grafeo_common::types::Value::Int64(30),
1480 )),
1481 },
1482 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1483 variable: "n".to_string(),
1484 label: None,
1485 input: None,
1486 })),
1487 pushdown_hint: None,
1488 })),
1489 }));
1490
1491 let mut binder = Binder::new();
1492 let result = binder.bind(&plan);
1493
1494 assert!(result.is_err());
1495 let err = result.unwrap_err();
1496 assert!(err.to_string().contains("Undefined variable 'm'"));
1497 }
1498
1499 #[test]
1500 fn test_bind_expand() {
1501 use crate::query::plan::{ExpandDirection, ExpandOp, PathMode};
1502
1503 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1504 items: vec![
1505 ReturnItem {
1506 expression: LogicalExpression::Variable("a".to_string()),
1507 alias: None,
1508 },
1509 ReturnItem {
1510 expression: LogicalExpression::Variable("b".to_string()),
1511 alias: None,
1512 },
1513 ],
1514 distinct: false,
1515 input: Box::new(LogicalOperator::Expand(ExpandOp {
1516 from_variable: "a".to_string(),
1517 to_variable: "b".to_string(),
1518 edge_variable: Some("e".to_string()),
1519 direction: ExpandDirection::Outgoing,
1520 edge_types: vec!["KNOWS".to_string()],
1521 min_hops: 1,
1522 max_hops: Some(1),
1523 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1524 variable: "a".to_string(),
1525 label: Some("Person".to_string()),
1526 input: None,
1527 })),
1528 path_alias: None,
1529 path_mode: PathMode::Walk,
1530 })),
1531 }));
1532
1533 let mut binder = Binder::new();
1534 let result = binder.bind(&plan);
1535
1536 assert!(result.is_ok());
1537 let ctx = result.unwrap();
1538 assert!(ctx.contains("a"));
1539 assert!(ctx.contains("b"));
1540 assert!(ctx.contains("e"));
1541 assert!(ctx.get("a").unwrap().is_node);
1542 assert!(ctx.get("b").unwrap().is_node);
1543 assert!(ctx.get("e").unwrap().is_edge);
1544 }
1545
1546 #[test]
1547 fn test_bind_expand_from_undefined_variable() {
1548 use crate::query::plan::{ExpandDirection, ExpandOp, PathMode};
1550
1551 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1552 items: vec![ReturnItem {
1553 expression: LogicalExpression::Variable("b".to_string()),
1554 alias: None,
1555 }],
1556 distinct: false,
1557 input: Box::new(LogicalOperator::Expand(ExpandOp {
1558 from_variable: "undefined".to_string(), to_variable: "b".to_string(),
1560 edge_variable: None,
1561 direction: ExpandDirection::Outgoing,
1562 edge_types: vec![],
1563 min_hops: 1,
1564 max_hops: Some(1),
1565 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1566 variable: "a".to_string(),
1567 label: None,
1568 input: None,
1569 })),
1570 path_alias: None,
1571 path_mode: PathMode::Walk,
1572 })),
1573 }));
1574
1575 let mut binder = Binder::new();
1576 let result = binder.bind(&plan);
1577
1578 assert!(result.is_err());
1579 let err = result.unwrap_err();
1580 assert!(
1581 err.to_string().contains("Undefined variable 'undefined'"),
1582 "Expected error about undefined variable, got: {}",
1583 err
1584 );
1585 }
1586
1587 #[test]
1588 fn test_bind_return_with_aggregate_and_non_aggregate() {
1589 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1591 items: vec![
1592 ReturnItem {
1593 expression: LogicalExpression::FunctionCall {
1594 name: "count".to_string(),
1595 args: vec![LogicalExpression::Variable("n".to_string())],
1596 distinct: false,
1597 },
1598 alias: Some("cnt".to_string()),
1599 },
1600 ReturnItem {
1601 expression: LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
1602 alias: Some("one".to_string()),
1603 },
1604 ],
1605 distinct: false,
1606 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1607 variable: "n".to_string(),
1608 label: Some("Person".to_string()),
1609 input: None,
1610 })),
1611 }));
1612
1613 let mut binder = Binder::new();
1614 let result = binder.bind(&plan);
1615
1616 assert!(result.is_ok());
1618 }
1619
1620 #[test]
1621 fn test_bind_nested_property_access() {
1622 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1624 items: vec![
1625 ReturnItem {
1626 expression: LogicalExpression::Property {
1627 variable: "n".to_string(),
1628 property: "name".to_string(),
1629 },
1630 alias: None,
1631 },
1632 ReturnItem {
1633 expression: LogicalExpression::Property {
1634 variable: "n".to_string(),
1635 property: "age".to_string(),
1636 },
1637 alias: None,
1638 },
1639 ],
1640 distinct: false,
1641 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1642 variable: "n".to_string(),
1643 label: Some("Person".to_string()),
1644 input: None,
1645 })),
1646 }));
1647
1648 let mut binder = Binder::new();
1649 let result = binder.bind(&plan);
1650
1651 assert!(result.is_ok());
1652 }
1653
1654 #[test]
1655 fn test_bind_binary_expression_with_undefined() {
1656 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1658 items: vec![ReturnItem {
1659 expression: LogicalExpression::Binary {
1660 left: Box::new(LogicalExpression::Property {
1661 variable: "n".to_string(),
1662 property: "age".to_string(),
1663 }),
1664 op: BinaryOp::Add,
1665 right: Box::new(LogicalExpression::Property {
1666 variable: "m".to_string(), property: "age".to_string(),
1668 }),
1669 },
1670 alias: Some("total".to_string()),
1671 }],
1672 distinct: false,
1673 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1674 variable: "n".to_string(),
1675 label: None,
1676 input: None,
1677 })),
1678 }));
1679
1680 let mut binder = Binder::new();
1681 let result = binder.bind(&plan);
1682
1683 assert!(result.is_err());
1684 assert!(
1685 result
1686 .unwrap_err()
1687 .to_string()
1688 .contains("Undefined variable 'm'")
1689 );
1690 }
1691
1692 #[test]
1693 fn test_bind_duplicate_variable_definition() {
1694 use crate::query::plan::{JoinOp, JoinType};
1697
1698 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1699 items: vec![ReturnItem {
1700 expression: LogicalExpression::Variable("n".to_string()),
1701 alias: None,
1702 }],
1703 distinct: false,
1704 input: Box::new(LogicalOperator::Join(JoinOp {
1705 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1706 variable: "n".to_string(),
1707 label: Some("A".to_string()),
1708 input: None,
1709 })),
1710 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1711 variable: "m".to_string(), label: Some("B".to_string()),
1713 input: None,
1714 })),
1715 join_type: JoinType::Inner,
1716 conditions: vec![],
1717 })),
1718 }));
1719
1720 let mut binder = Binder::new();
1721 let result = binder.bind(&plan);
1722
1723 assert!(result.is_ok());
1725 let ctx = result.unwrap();
1726 assert!(ctx.contains("n"));
1727 assert!(ctx.contains("m"));
1728 }
1729
1730 #[test]
1731 fn test_bind_function_with_wrong_arity() {
1732 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1735 items: vec![ReturnItem {
1736 expression: LogicalExpression::FunctionCall {
1737 name: "count".to_string(),
1738 args: vec![], distinct: false,
1740 },
1741 alias: None,
1742 }],
1743 distinct: false,
1744 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1745 variable: "n".to_string(),
1746 label: None,
1747 input: None,
1748 })),
1749 }));
1750
1751 let mut binder = Binder::new();
1752 let result = binder.bind(&plan);
1753
1754 let _ = result; }
1759
1760 #[test]
1763 fn test_create_edge_rejects_undefined_source() {
1764 use crate::query::plan::CreateEdgeOp;
1765
1766 let plan = LogicalPlan::new(LogicalOperator::CreateEdge(CreateEdgeOp {
1767 variable: Some("e".to_string()),
1768 from_variable: "ghost".to_string(), to_variable: "b".to_string(),
1770 edge_type: "KNOWS".to_string(),
1771 properties: vec![],
1772 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1773 variable: "b".to_string(),
1774 label: None,
1775 input: None,
1776 })),
1777 }));
1778
1779 let mut binder = Binder::new();
1780 let err = binder.bind(&plan).unwrap_err();
1781 assert!(
1782 err.to_string().contains("Undefined variable 'ghost'"),
1783 "Should reject undefined source variable, got: {err}"
1784 );
1785 }
1786
1787 #[test]
1788 fn test_create_edge_rejects_undefined_target() {
1789 use crate::query::plan::CreateEdgeOp;
1790
1791 let plan = LogicalPlan::new(LogicalOperator::CreateEdge(CreateEdgeOp {
1792 variable: None,
1793 from_variable: "a".to_string(),
1794 to_variable: "missing".to_string(), edge_type: "KNOWS".to_string(),
1796 properties: vec![],
1797 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1798 variable: "a".to_string(),
1799 label: None,
1800 input: None,
1801 })),
1802 }));
1803
1804 let mut binder = Binder::new();
1805 let err = binder.bind(&plan).unwrap_err();
1806 assert!(
1807 err.to_string().contains("Undefined variable 'missing'"),
1808 "Should reject undefined target variable, got: {err}"
1809 );
1810 }
1811
1812 #[test]
1813 fn test_create_edge_validates_property_expressions() {
1814 use crate::query::plan::CreateEdgeOp;
1815
1816 let plan = LogicalPlan::new(LogicalOperator::CreateEdge(CreateEdgeOp {
1818 variable: Some("e".to_string()),
1819 from_variable: "a".to_string(),
1820 to_variable: "b".to_string(),
1821 edge_type: "KNOWS".to_string(),
1822 properties: vec![(
1823 "since".to_string(),
1824 LogicalExpression::Property {
1825 variable: "x".to_string(), property: "year".to_string(),
1827 },
1828 )],
1829 input: Box::new(LogicalOperator::Join(crate::query::plan::JoinOp {
1830 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1831 variable: "a".to_string(),
1832 label: None,
1833 input: None,
1834 })),
1835 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1836 variable: "b".to_string(),
1837 label: None,
1838 input: None,
1839 })),
1840 join_type: crate::query::plan::JoinType::Inner,
1841 conditions: vec![],
1842 })),
1843 }));
1844
1845 let mut binder = Binder::new();
1846 let err = binder.bind(&plan).unwrap_err();
1847 assert!(err.to_string().contains("Undefined variable 'x'"));
1848 }
1849
1850 #[test]
1851 fn test_set_property_rejects_undefined_variable() {
1852 use crate::query::plan::SetPropertyOp;
1853
1854 let plan = LogicalPlan::new(LogicalOperator::SetProperty(SetPropertyOp {
1855 variable: "ghost".to_string(),
1856 properties: vec![(
1857 "name".to_string(),
1858 LogicalExpression::Literal(grafeo_common::types::Value::String("Alix".into())),
1859 )],
1860 replace: false,
1861 is_edge: false,
1862 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1863 variable: "n".to_string(),
1864 label: None,
1865 input: None,
1866 })),
1867 }));
1868
1869 let mut binder = Binder::new();
1870 let err = binder.bind(&plan).unwrap_err();
1871 assert!(
1872 err.to_string().contains("in SET"),
1873 "Error should indicate SET context, got: {err}"
1874 );
1875 }
1876
1877 #[test]
1878 fn test_delete_node_rejects_undefined_variable() {
1879 use crate::query::plan::DeleteNodeOp;
1880
1881 let plan = LogicalPlan::new(LogicalOperator::DeleteNode(DeleteNodeOp {
1882 variable: "phantom".to_string(),
1883 detach: false,
1884 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1885 variable: "n".to_string(),
1886 label: None,
1887 input: None,
1888 })),
1889 }));
1890
1891 let mut binder = Binder::new();
1892 let err = binder.bind(&plan).unwrap_err();
1893 assert!(err.to_string().contains("Undefined variable 'phantom'"));
1894 }
1895
1896 #[test]
1897 fn test_delete_edge_rejects_undefined_variable() {
1898 use crate::query::plan::DeleteEdgeOp;
1899
1900 let plan = LogicalPlan::new(LogicalOperator::DeleteEdge(DeleteEdgeOp {
1901 variable: "gone".to_string(),
1902 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1903 variable: "n".to_string(),
1904 label: None,
1905 input: None,
1906 })),
1907 }));
1908
1909 let mut binder = Binder::new();
1910 let err = binder.bind(&plan).unwrap_err();
1911 assert!(err.to_string().contains("Undefined variable 'gone'"));
1912 }
1913
1914 #[test]
1917 fn test_project_alias_becomes_available_downstream() {
1918 use crate::query::plan::{ProjectOp, Projection};
1919
1920 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1922 items: vec![ReturnItem {
1923 expression: LogicalExpression::Variable("person_name".to_string()),
1924 alias: None,
1925 }],
1926 distinct: false,
1927 input: Box::new(LogicalOperator::Project(ProjectOp {
1928 projections: vec![Projection {
1929 expression: LogicalExpression::Property {
1930 variable: "n".to_string(),
1931 property: "name".to_string(),
1932 },
1933 alias: Some("person_name".to_string()),
1934 }],
1935 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1936 variable: "n".to_string(),
1937 label: None,
1938 input: None,
1939 })),
1940 pass_through_input: false,
1941 })),
1942 }));
1943
1944 let mut binder = Binder::new();
1945 let ctx = binder.bind(&plan).unwrap();
1946 assert!(
1947 ctx.contains("person_name"),
1948 "WITH alias should be available to RETURN"
1949 );
1950 }
1951
1952 #[test]
1953 fn test_project_rejects_undefined_expression() {
1954 use crate::query::plan::{ProjectOp, Projection};
1955
1956 let plan = LogicalPlan::new(LogicalOperator::Project(ProjectOp {
1957 projections: vec![Projection {
1958 expression: LogicalExpression::Variable("nope".to_string()),
1959 alias: Some("x".to_string()),
1960 }],
1961 input: Box::new(LogicalOperator::Empty),
1962 pass_through_input: false,
1963 }));
1964
1965 let mut binder = Binder::new();
1966 let result = binder.bind(&plan);
1967 assert!(result.is_err(), "WITH on undefined variable should fail");
1968 }
1969
1970 #[test]
1973 fn test_unwind_adds_element_variable() {
1974 use crate::query::plan::UnwindOp;
1975
1976 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1977 items: vec![ReturnItem {
1978 expression: LogicalExpression::Variable("item".to_string()),
1979 alias: None,
1980 }],
1981 distinct: false,
1982 input: Box::new(LogicalOperator::Unwind(UnwindOp {
1983 expression: LogicalExpression::List(vec![
1984 LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
1985 LogicalExpression::Literal(grafeo_common::types::Value::Int64(2)),
1986 ]),
1987 variable: "item".to_string(),
1988 ordinality_var: None,
1989 offset_var: None,
1990 input: Box::new(LogicalOperator::Empty),
1991 })),
1992 }));
1993
1994 let mut binder = Binder::new();
1995 let ctx = binder.bind(&plan).unwrap();
1996 assert!(ctx.contains("item"), "UNWIND variable should be in scope");
1997 let info = ctx.get("item").unwrap();
1998 assert!(
1999 !info.is_node && !info.is_edge,
2000 "UNWIND variable is not a graph element"
2001 );
2002 }
2003
2004 #[test]
2007 fn test_merge_adds_variable_and_validates_properties() {
2008 use crate::query::plan::MergeOp;
2009
2010 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2011 items: vec![ReturnItem {
2012 expression: LogicalExpression::Variable("m".to_string()),
2013 alias: None,
2014 }],
2015 distinct: false,
2016 input: Box::new(LogicalOperator::Merge(MergeOp {
2017 variable: "m".to_string(),
2018 labels: vec!["Person".to_string()],
2019 match_properties: vec![(
2020 "name".to_string(),
2021 LogicalExpression::Literal(grafeo_common::types::Value::String("Alix".into())),
2022 )],
2023 on_create: vec![(
2024 "created".to_string(),
2025 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
2026 )],
2027 on_match: vec![(
2028 "updated".to_string(),
2029 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
2030 )],
2031 input: Box::new(LogicalOperator::Empty),
2032 })),
2033 }));
2034
2035 let mut binder = Binder::new();
2036 let ctx = binder.bind(&plan).unwrap();
2037 assert!(ctx.contains("m"));
2038 assert!(
2039 ctx.get("m").unwrap().is_node,
2040 "MERGE variable should be a node"
2041 );
2042 }
2043
2044 #[test]
2045 fn test_merge_rejects_undefined_in_on_create() {
2046 use crate::query::plan::MergeOp;
2047
2048 let plan = LogicalPlan::new(LogicalOperator::Merge(MergeOp {
2049 variable: "m".to_string(),
2050 labels: vec![],
2051 match_properties: vec![],
2052 on_create: vec![(
2053 "name".to_string(),
2054 LogicalExpression::Property {
2055 variable: "other".to_string(), property: "name".to_string(),
2057 },
2058 )],
2059 on_match: vec![],
2060 input: Box::new(LogicalOperator::Empty),
2061 }));
2062
2063 let mut binder = Binder::new();
2064 let result = binder.bind(&plan);
2065 assert!(
2066 result.is_err(),
2067 "ON CREATE referencing undefined variable should fail"
2068 );
2069 }
2070
2071 #[test]
2074 fn test_shortest_path_rejects_undefined_source() {
2075 use crate::query::plan::{ExpandDirection, ShortestPathOp};
2076
2077 let plan = LogicalPlan::new(LogicalOperator::ShortestPath(ShortestPathOp {
2078 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2079 variable: "b".to_string(),
2080 label: None,
2081 input: None,
2082 })),
2083 source_var: "missing".to_string(), target_var: "b".to_string(),
2085 edge_types: vec![],
2086 direction: ExpandDirection::Both,
2087 path_alias: "p".to_string(),
2088 all_paths: false,
2089 }));
2090
2091 let mut binder = Binder::new();
2092 let err = binder.bind(&plan).unwrap_err();
2093 assert!(
2094 err.to_string().contains("source in shortestPath"),
2095 "Error should mention shortestPath source context, got: {err}"
2096 );
2097 }
2098
2099 #[test]
2100 fn test_shortest_path_adds_path_and_length_variables() {
2101 use crate::query::plan::{ExpandDirection, JoinOp, JoinType, ShortestPathOp};
2102
2103 let plan = LogicalPlan::new(LogicalOperator::ShortestPath(ShortestPathOp {
2104 input: Box::new(LogicalOperator::Join(JoinOp {
2105 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2106 variable: "a".to_string(),
2107 label: None,
2108 input: None,
2109 })),
2110 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2111 variable: "b".to_string(),
2112 label: None,
2113 input: None,
2114 })),
2115 join_type: JoinType::Cross,
2116 conditions: vec![],
2117 })),
2118 source_var: "a".to_string(),
2119 target_var: "b".to_string(),
2120 edge_types: vec!["ROAD".to_string()],
2121 direction: ExpandDirection::Outgoing,
2122 path_alias: "p".to_string(),
2123 all_paths: false,
2124 }));
2125
2126 let mut binder = Binder::new();
2127 let ctx = binder.bind(&plan).unwrap();
2128 assert!(ctx.contains("p"), "Path alias should be bound");
2129 assert!(
2130 ctx.contains("_path_length_p"),
2131 "Path length variable should be auto-created"
2132 );
2133 }
2134
2135 #[test]
2138 fn test_case_expression_validates_all_branches() {
2139 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2140 items: vec![ReturnItem {
2141 expression: LogicalExpression::Case {
2142 operand: None,
2143 when_clauses: vec![
2144 (
2145 LogicalExpression::Binary {
2146 left: Box::new(LogicalExpression::Property {
2147 variable: "n".to_string(),
2148 property: "age".to_string(),
2149 }),
2150 op: BinaryOp::Gt,
2151 right: Box::new(LogicalExpression::Literal(
2152 grafeo_common::types::Value::Int64(18),
2153 )),
2154 },
2155 LogicalExpression::Literal(grafeo_common::types::Value::String(
2156 "adult".into(),
2157 )),
2158 ),
2159 (
2160 LogicalExpression::Property {
2162 variable: "ghost".to_string(),
2163 property: "flag".to_string(),
2164 },
2165 LogicalExpression::Literal(grafeo_common::types::Value::String(
2166 "flagged".into(),
2167 )),
2168 ),
2169 ],
2170 else_clause: Some(Box::new(LogicalExpression::Literal(
2171 grafeo_common::types::Value::String("other".into()),
2172 ))),
2173 },
2174 alias: None,
2175 }],
2176 distinct: false,
2177 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2178 variable: "n".to_string(),
2179 label: None,
2180 input: None,
2181 })),
2182 }));
2183
2184 let mut binder = Binder::new();
2185 let err = binder.bind(&plan).unwrap_err();
2186 assert!(
2187 err.to_string().contains("ghost"),
2188 "CASE should validate all when-clause conditions"
2189 );
2190 }
2191
2192 #[test]
2193 fn test_case_expression_validates_else_clause() {
2194 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2195 items: vec![ReturnItem {
2196 expression: LogicalExpression::Case {
2197 operand: None,
2198 when_clauses: vec![(
2199 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
2200 LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
2201 )],
2202 else_clause: Some(Box::new(LogicalExpression::Property {
2203 variable: "missing".to_string(),
2204 property: "x".to_string(),
2205 })),
2206 },
2207 alias: None,
2208 }],
2209 distinct: false,
2210 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2211 variable: "n".to_string(),
2212 label: None,
2213 input: None,
2214 })),
2215 }));
2216
2217 let mut binder = Binder::new();
2218 let err = binder.bind(&plan).unwrap_err();
2219 assert!(
2220 err.to_string().contains("missing"),
2221 "CASE ELSE should validate its expression too"
2222 );
2223 }
2224
2225 #[test]
2226 fn test_slice_access_validates_expressions() {
2227 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2228 items: vec![ReturnItem {
2229 expression: LogicalExpression::SliceAccess {
2230 base: Box::new(LogicalExpression::Variable("n".to_string())),
2231 start: Some(Box::new(LogicalExpression::Variable(
2232 "undefined_start".to_string(),
2233 ))),
2234 end: None,
2235 },
2236 alias: None,
2237 }],
2238 distinct: false,
2239 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2240 variable: "n".to_string(),
2241 label: None,
2242 input: None,
2243 })),
2244 }));
2245
2246 let mut binder = Binder::new();
2247 let err = binder.bind(&plan).unwrap_err();
2248 assert!(err.to_string().contains("undefined_start"));
2249 }
2250
2251 #[test]
2252 fn test_list_comprehension_validates_list_source() {
2253 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2254 items: vec![ReturnItem {
2255 expression: LogicalExpression::ListComprehension {
2256 variable: "x".to_string(),
2257 list_expr: Box::new(LogicalExpression::Variable("not_defined".to_string())),
2258 filter_expr: None,
2259 map_expr: Box::new(LogicalExpression::Variable("x".to_string())),
2260 },
2261 alias: None,
2262 }],
2263 distinct: false,
2264 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2265 variable: "n".to_string(),
2266 label: None,
2267 input: None,
2268 })),
2269 }));
2270
2271 let mut binder = Binder::new();
2272 let err = binder.bind(&plan).unwrap_err();
2273 assert!(
2274 err.to_string().contains("not_defined"),
2275 "List comprehension should validate source list expression"
2276 );
2277 }
2278
2279 #[test]
2280 fn test_labels_type_id_reject_undefined() {
2281 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2283 items: vec![ReturnItem {
2284 expression: LogicalExpression::Labels("x".to_string()),
2285 alias: None,
2286 }],
2287 distinct: false,
2288 input: Box::new(LogicalOperator::Empty),
2289 }));
2290
2291 let mut binder = Binder::new();
2292 assert!(
2293 binder.bind(&plan).is_err(),
2294 "labels(x) on undefined x should fail"
2295 );
2296
2297 let plan2 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2299 items: vec![ReturnItem {
2300 expression: LogicalExpression::Type("e".to_string()),
2301 alias: None,
2302 }],
2303 distinct: false,
2304 input: Box::new(LogicalOperator::Empty),
2305 }));
2306
2307 let mut binder2 = Binder::new();
2308 assert!(
2309 binder2.bind(&plan2).is_err(),
2310 "type(e) on undefined e should fail"
2311 );
2312
2313 let plan3 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2315 items: vec![ReturnItem {
2316 expression: LogicalExpression::Id("n".to_string()),
2317 alias: None,
2318 }],
2319 distinct: false,
2320 input: Box::new(LogicalOperator::Empty),
2321 }));
2322
2323 let mut binder3 = Binder::new();
2324 assert!(
2325 binder3.bind(&plan3).is_err(),
2326 "id(n) on undefined n should fail"
2327 );
2328 }
2329
2330 #[test]
2331 fn test_expand_rejects_non_node_source() {
2332 use crate::query::plan::{ExpandDirection, ExpandOp, PathMode, UnwindOp};
2333
2334 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2337 items: vec![ReturnItem {
2338 expression: LogicalExpression::Variable("b".to_string()),
2339 alias: None,
2340 }],
2341 distinct: false,
2342 input: Box::new(LogicalOperator::Expand(ExpandOp {
2343 from_variable: "x".to_string(),
2344 to_variable: "b".to_string(),
2345 edge_variable: None,
2346 direction: ExpandDirection::Outgoing,
2347 edge_types: vec![],
2348 min_hops: 1,
2349 max_hops: Some(1),
2350 input: Box::new(LogicalOperator::Unwind(UnwindOp {
2351 expression: LogicalExpression::List(vec![]),
2352 variable: "x".to_string(),
2353 ordinality_var: None,
2354 offset_var: None,
2355 input: Box::new(LogicalOperator::Empty),
2356 })),
2357 path_alias: None,
2358 path_mode: PathMode::Walk,
2359 })),
2360 }));
2361
2362 let mut binder = Binder::new();
2363 let err = binder.bind(&plan).unwrap_err();
2364 assert!(
2365 err.to_string().contains("not a node"),
2366 "Expanding from non-node should fail, got: {err}"
2367 );
2368 }
2369
2370 #[test]
2371 fn test_add_label_rejects_undefined_variable() {
2372 use crate::query::plan::AddLabelOp;
2373
2374 let plan = LogicalPlan::new(LogicalOperator::AddLabel(AddLabelOp {
2375 variable: "missing".to_string(),
2376 labels: vec!["Admin".to_string()],
2377 input: Box::new(LogicalOperator::Empty),
2378 }));
2379
2380 let mut binder = Binder::new();
2381 let err = binder.bind(&plan).unwrap_err();
2382 assert!(err.to_string().contains("SET labels"));
2383 }
2384
2385 #[test]
2386 fn test_remove_label_rejects_undefined_variable() {
2387 use crate::query::plan::RemoveLabelOp;
2388
2389 let plan = LogicalPlan::new(LogicalOperator::RemoveLabel(RemoveLabelOp {
2390 variable: "missing".to_string(),
2391 labels: vec!["Admin".to_string()],
2392 input: Box::new(LogicalOperator::Empty),
2393 }));
2394
2395 let mut binder = Binder::new();
2396 let err = binder.bind(&plan).unwrap_err();
2397 assert!(err.to_string().contains("REMOVE labels"));
2398 }
2399
2400 #[test]
2401 fn test_sort_validates_key_expressions() {
2402 use crate::query::plan::{SortKey, SortOp, SortOrder};
2403
2404 let plan = LogicalPlan::new(LogicalOperator::Sort(SortOp {
2405 keys: vec![SortKey {
2406 expression: LogicalExpression::Property {
2407 variable: "missing".to_string(),
2408 property: "name".to_string(),
2409 },
2410 order: SortOrder::Ascending,
2411 nulls: None,
2412 }],
2413 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2414 variable: "n".to_string(),
2415 label: None,
2416 input: None,
2417 })),
2418 }));
2419
2420 let mut binder = Binder::new();
2421 assert!(
2422 binder.bind(&plan).is_err(),
2423 "ORDER BY on undefined variable should fail"
2424 );
2425 }
2426
2427 #[test]
2428 fn test_create_node_adds_variable_before_property_validation() {
2429 use crate::query::plan::CreateNodeOp;
2430
2431 let plan = LogicalPlan::new(LogicalOperator::CreateNode(CreateNodeOp {
2434 variable: "n".to_string(),
2435 labels: vec!["Person".to_string()],
2436 properties: vec![(
2437 "self_ref".to_string(),
2438 LogicalExpression::Property {
2439 variable: "n".to_string(),
2440 property: "name".to_string(),
2441 },
2442 )],
2443 input: None,
2444 }));
2445
2446 let mut binder = Binder::new();
2447 let ctx = binder.bind(&plan).unwrap();
2449 assert!(ctx.get("n").unwrap().is_node);
2450 }
2451
2452 #[test]
2453 fn test_undefined_variable_suggests_similar() {
2454 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2456 items: vec![ReturnItem {
2457 expression: LogicalExpression::Variable("persn".to_string()),
2458 alias: None,
2459 }],
2460 distinct: false,
2461 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2462 variable: "person".to_string(),
2463 label: None,
2464 input: None,
2465 })),
2466 }));
2467
2468 let mut binder = Binder::new();
2469 let err = binder.bind(&plan).unwrap_err();
2470 let msg = err.to_string();
2471 assert!(
2473 msg.contains("persn"),
2474 "Error should mention the undefined variable"
2475 );
2476 }
2477
2478 #[test]
2479 fn test_anon_variables_skip_validation() {
2480 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2482 items: vec![ReturnItem {
2483 expression: LogicalExpression::Variable("_anon_42".to_string()),
2484 alias: None,
2485 }],
2486 distinct: false,
2487 input: Box::new(LogicalOperator::Empty),
2488 }));
2489
2490 let mut binder = Binder::new();
2491 let result = binder.bind(&plan);
2492 assert!(
2493 result.is_ok(),
2494 "Anonymous variables should bypass validation"
2495 );
2496 }
2497
2498 #[test]
2499 fn test_map_expression_validates_values() {
2500 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2501 items: vec![ReturnItem {
2502 expression: LogicalExpression::Map(vec![(
2503 "key".to_string(),
2504 LogicalExpression::Variable("undefined".to_string()),
2505 )]),
2506 alias: None,
2507 }],
2508 distinct: false,
2509 input: Box::new(LogicalOperator::Empty),
2510 }));
2511
2512 let mut binder = Binder::new();
2513 assert!(
2514 binder.bind(&plan).is_err(),
2515 "Map values should be validated"
2516 );
2517 }
2518
2519 #[test]
2520 fn test_vector_scan_validates_query_vector() {
2521 use crate::query::plan::VectorScanOp;
2522
2523 let plan = LogicalPlan::new(LogicalOperator::VectorScan(VectorScanOp {
2524 variable: "result".to_string(),
2525 index_name: None,
2526 property: "embedding".to_string(),
2527 label: Some("Doc".to_string()),
2528 query_vector: LogicalExpression::Variable("undefined_vec".to_string()),
2529 k: Some(10),
2530 metric: None,
2531 min_similarity: None,
2532 max_distance: None,
2533 input: None,
2534 }));
2535
2536 let mut binder = Binder::new();
2537 let err = binder.bind(&plan).unwrap_err();
2538 assert!(err.to_string().contains("undefined_vec"));
2539 }
2540
2541 #[test]
2545 fn test_bind_unwind_ordinality_and_offset() {
2546 use crate::query::plan::UnwindOp;
2547
2548 let plan = LogicalPlan::new(LogicalOperator::Unwind(UnwindOp {
2549 expression: LogicalExpression::List(vec![
2550 LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
2551 LogicalExpression::Literal(grafeo_common::types::Value::Int64(2)),
2552 LogicalExpression::Literal(grafeo_common::types::Value::Int64(3)),
2553 ]),
2554 variable: "x".to_string(),
2555 ordinality_var: Some("i".to_string()),
2556 offset_var: Some("j".to_string()),
2557 input: Box::new(LogicalOperator::Empty),
2558 }));
2559
2560 let mut binder = Binder::new();
2561 let ctx = binder.bind(&plan).unwrap();
2562
2563 assert!(ctx.contains("x"));
2564 assert!(ctx.contains("i"));
2565 assert!(ctx.contains("j"));
2566 assert_eq!(ctx.get("i").unwrap().data_type, LogicalType::Int64);
2568 assert_eq!(ctx.get("j").unwrap().data_type, LogicalType::Int64);
2569 assert_eq!(ctx.get("x").unwrap().data_type, LogicalType::Any);
2571 for v in ["x", "i", "j"] {
2573 let info = ctx.get(v).unwrap();
2574 assert!(!info.is_node);
2575 assert!(!info.is_edge);
2576 }
2577 }
2578
2579 #[test]
2582 fn test_bind_merge_relationship_properties() {
2583 use crate::query::plan::{JoinOp, JoinType, MergeRelationshipOp};
2584
2585 let plan = LogicalPlan::new(LogicalOperator::MergeRelationship(MergeRelationshipOp {
2588 variable: "r".to_string(),
2589 source_variable: "a".to_string(),
2590 target_variable: "b".to_string(),
2591 edge_type: "WORKS".to_string(),
2592 match_properties: vec![(
2593 "start".to_string(),
2594 LogicalExpression::Literal(grafeo_common::types::Value::Int64(2026)),
2595 )],
2596 on_create: vec![],
2597 on_match: vec![],
2598 input: Box::new(LogicalOperator::Join(JoinOp {
2599 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2600 variable: "a".to_string(),
2601 label: Some("Person".to_string()),
2602 input: None,
2603 })),
2604 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2605 variable: "b".to_string(),
2606 label: Some("Company".to_string()),
2607 input: None,
2608 })),
2609 join_type: JoinType::Cross,
2610 conditions: vec![],
2611 })),
2612 }));
2613
2614 let mut binder = Binder::new();
2615 let ctx = binder.bind(&plan).unwrap();
2616 assert!(ctx.contains("a"));
2617 assert!(ctx.contains("b"));
2618 assert!(ctx.contains("r"));
2619 let rel = ctx.get("r").unwrap();
2620 assert!(rel.is_edge);
2621 assert!(!rel.is_node);
2622 assert_eq!(rel.data_type, LogicalType::Edge);
2623
2624 let bad_plan = LogicalPlan::new(LogicalOperator::MergeRelationship(MergeRelationshipOp {
2626 variable: "r".to_string(),
2627 source_variable: "a".to_string(),
2628 target_variable: "b".to_string(),
2629 edge_type: "WORKS".to_string(),
2630 match_properties: vec![(
2631 "start".to_string(),
2632 LogicalExpression::Property {
2633 variable: "ghost".to_string(),
2634 property: "year".to_string(),
2635 },
2636 )],
2637 on_create: vec![],
2638 on_match: vec![],
2639 input: Box::new(LogicalOperator::Join(JoinOp {
2640 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2641 variable: "a".to_string(),
2642 label: None,
2643 input: None,
2644 })),
2645 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2646 variable: "b".to_string(),
2647 label: None,
2648 input: None,
2649 })),
2650 join_type: JoinType::Cross,
2651 conditions: vec![],
2652 })),
2653 }));
2654 let mut binder2 = Binder::new();
2655 let err = binder2.bind(&bad_plan).unwrap_err();
2656 assert!(err.to_string().contains("Undefined variable 'ghost'"));
2657 }
2658
2659 #[test]
2662 fn test_undefined_variable_suggestion() {
2663 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2664 items: vec![ReturnItem {
2665 expression: LogicalExpression::Variable("xx".to_string()),
2666 alias: None,
2667 }],
2668 distinct: false,
2669 input: Box::new(LogicalOperator::Unwind(crate::query::plan::UnwindOp {
2670 expression: LogicalExpression::List(vec![LogicalExpression::Literal(
2671 grafeo_common::types::Value::Int64(1),
2672 )]),
2673 variable: "x".to_string(),
2674 ordinality_var: None,
2675 offset_var: None,
2676 input: Box::new(LogicalOperator::Empty),
2677 })),
2678 }));
2679
2680 let mut binder = Binder::new();
2681 let err = binder.bind(&plan).unwrap_err();
2682 let msg = err.to_string();
2683 assert!(msg.contains("Undefined variable 'xx'"), "got: {msg}");
2684 assert!(
2685 msg.contains("Did you mean 'x'?"),
2686 "should suggest the similar variable 'x', got: {msg}"
2687 );
2688 }
2689
2690 #[test]
2694 fn test_bind_aggregate_group_by_alias() {
2695 use crate::query::plan::{AggregateExpr, AggregateFunction, AggregateOp};
2696
2697 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2702 items: vec![ReturnItem {
2703 expression: LogicalExpression::Variable("cnt".to_string()),
2704 alias: None,
2705 }],
2706 distinct: false,
2707 input: Box::new(LogicalOperator::Aggregate(AggregateOp {
2708 group_by: vec![LogicalExpression::Property {
2709 variable: "n".to_string(),
2710 property: "age".to_string(),
2711 }],
2712 aggregates: vec![AggregateExpr {
2713 function: AggregateFunction::Count,
2714 expression: None,
2715 expression2: None,
2716 distinct: false,
2717 alias: Some("cnt".to_string()),
2718 percentile: None,
2719 separator: None,
2720 }],
2721 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2722 variable: "n".to_string(),
2723 label: Some("Person".to_string()),
2724 input: None,
2725 })),
2726 having: None,
2727 })),
2728 }));
2729
2730 let mut binder = Binder::new();
2731 let ctx = binder.bind(&plan).unwrap();
2732 assert!(ctx.contains("cnt"), "aggregate alias 'cnt' should be bound");
2734 assert!(
2737 ctx.contains("n.age"),
2738 "group-by output column 'n.age' should be registered, got: {:?}",
2739 ctx.variable_names()
2740 );
2741 }
2742
2743 #[test]
2748 fn test_binding_context_len_and_is_empty_and_remove() {
2749 let mut ctx = BindingContext::new();
2750 assert!(ctx.is_empty());
2751 assert_eq!(ctx.len(), 0);
2752
2753 ctx.add_variable(
2754 "vincent".to_string(),
2755 VariableInfo {
2756 name: "vincent".to_string(),
2757 data_type: LogicalType::Node,
2758 is_node: true,
2759 is_edge: false,
2760 },
2761 );
2762 ctx.add_variable(
2763 "jules".to_string(),
2764 VariableInfo {
2765 name: "jules".to_string(),
2766 data_type: LogicalType::Node,
2767 is_node: true,
2768 is_edge: false,
2769 },
2770 );
2771 assert!(!ctx.is_empty());
2772 assert_eq!(ctx.len(), 2);
2773 assert_eq!(
2774 ctx.variable_names(),
2775 vec!["vincent".to_string(), "jules".to_string()]
2776 );
2777
2778 ctx.remove_variable("vincent");
2779 assert_eq!(ctx.len(), 1);
2780 assert!(!ctx.contains("vincent"));
2781 assert!(ctx.contains("jules"));
2782
2783 ctx.remove_variable("nonexistent");
2785 assert_eq!(ctx.len(), 1);
2786 }
2787
2788 #[test]
2789 fn test_binder_default_impl() {
2790 let binder = Binder::default();
2791 assert!(binder.context.is_empty());
2792 }
2793
2794 #[test]
2799 fn test_bind_empty_operator_alone() {
2800 let plan = LogicalPlan::new(LogicalOperator::Empty);
2801 let mut binder = Binder::new();
2802 let ctx = binder.bind(&plan).unwrap();
2803 assert!(ctx.is_empty());
2804 }
2805
2806 #[test]
2807 fn test_bind_limit_and_skip_delegate_to_input() {
2808 use crate::query::plan::{CountExpr, LimitOp, SkipOp};
2809
2810 let plan = LogicalPlan::new(LogicalOperator::Limit(LimitOp {
2811 count: CountExpr::Literal(5),
2812 input: Box::new(LogicalOperator::Skip(SkipOp {
2813 count: CountExpr::Literal(1),
2814 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2815 variable: "mia".to_string(),
2816 label: Some("Person".to_string()),
2817 input: None,
2818 })),
2819 })),
2820 }));
2821
2822 let mut binder = Binder::new();
2823 let ctx = binder.bind(&plan).unwrap();
2824 assert!(ctx.contains("mia"));
2825 }
2826
2827 #[test]
2828 fn test_bind_distinct_delegates_to_input() {
2829 use crate::query::plan::DistinctOp;
2830
2831 let plan = LogicalPlan::new(LogicalOperator::Distinct(DistinctOp {
2832 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2833 variable: "butch".to_string(),
2834 label: None,
2835 input: None,
2836 })),
2837 columns: None,
2838 }));
2839 let mut binder = Binder::new();
2840 let ctx = binder.bind(&plan).unwrap();
2841 assert!(ctx.contains("butch"));
2842 }
2843
2844 #[test]
2845 fn test_bind_edge_scan_with_input_binds_edge_variable() {
2846 use crate::query::plan::EdgeScanOp;
2847
2848 let plan = LogicalPlan::new(LogicalOperator::EdgeScan(EdgeScanOp {
2849 variable: "e".to_string(),
2850 edge_types: vec!["KNOWS".to_string()],
2851 input: Some(Box::new(LogicalOperator::NodeScan(NodeScanOp {
2852 variable: "django".to_string(),
2853 label: None,
2854 input: None,
2855 }))),
2856 }));
2857
2858 let mut binder = Binder::new();
2859 let ctx = binder.bind(&plan).unwrap();
2860 assert!(ctx.contains("django"));
2861 let edge_info = ctx.get("e").expect("edge variable bound");
2862 assert!(edge_info.is_edge);
2863 assert!(!edge_info.is_node);
2864 }
2865
2866 #[test]
2867 fn test_bind_edge_scan_without_input() {
2868 use crate::query::plan::EdgeScanOp;
2869
2870 let plan = LogicalPlan::new(LogicalOperator::EdgeScan(EdgeScanOp {
2871 variable: "rel".to_string(),
2872 edge_types: vec![],
2873 input: None,
2874 }));
2875
2876 let mut binder = Binder::new();
2877 let ctx = binder.bind(&plan).unwrap();
2878 assert!(ctx.get("rel").unwrap().is_edge);
2879 }
2880
2881 #[test]
2886 fn test_bind_triple_scan_registers_all_variable_components() {
2887 use crate::query::plan::{TripleComponent, TripleScanOp};
2888
2889 let plan = LogicalPlan::new(LogicalOperator::TripleScan(TripleScanOp {
2890 subject: TripleComponent::Variable("s".to_string()),
2891 predicate: TripleComponent::Variable("p".to_string()),
2892 object: TripleComponent::Variable("o".to_string()),
2893 graph: Some(TripleComponent::Variable("g".to_string())),
2894 input: None,
2895 dataset: None,
2896 }));
2897
2898 let mut binder = Binder::new();
2899 let ctx = binder.bind(&plan).unwrap();
2900 assert!(ctx.contains("s"));
2901 assert!(ctx.contains("p"));
2902 assert!(ctx.contains("o"));
2903 assert!(ctx.contains("g"));
2904 }
2905
2906 #[test]
2907 fn test_bind_triple_scan_skips_constant_components() {
2908 use crate::query::plan::{TripleComponent, TripleScanOp};
2909 use grafeo_common::types::Value;
2910
2911 let plan = LogicalPlan::new(LogicalOperator::TripleScan(TripleScanOp {
2912 subject: TripleComponent::Iri("http://example.org/s".to_string()),
2913 predicate: TripleComponent::Iri("http://example.org/p".to_string()),
2914 object: TripleComponent::Literal(Value::Int64(42)),
2915 graph: None,
2916 input: Some(Box::new(LogicalOperator::NodeScan(NodeScanOp {
2917 variable: "existing".to_string(),
2918 label: None,
2919 input: None,
2920 }))),
2921 dataset: None,
2922 }));
2923
2924 let mut binder = Binder::new();
2925 let ctx = binder.bind(&plan).unwrap();
2926 assert!(ctx.contains("existing"));
2928 assert_eq!(ctx.len(), 1);
2930 }
2931
2932 #[test]
2933 fn test_bind_triple_scan_does_not_rebind_existing_variable() {
2934 use crate::query::plan::{TripleComponent, TripleScanOp};
2935
2936 let plan = LogicalPlan::new(LogicalOperator::TripleScan(TripleScanOp {
2939 subject: TripleComponent::Variable("s".to_string()),
2940 predicate: TripleComponent::Variable("p".to_string()),
2941 object: TripleComponent::Variable("o".to_string()),
2942 graph: None,
2943 input: Some(Box::new(LogicalOperator::NodeScan(NodeScanOp {
2944 variable: "s".to_string(),
2945 label: None,
2946 input: None,
2947 }))),
2948 dataset: None,
2949 }));
2950
2951 let mut binder = Binder::new();
2952 let ctx = binder.bind(&plan).unwrap();
2953 assert!(ctx.get("s").unwrap().is_node);
2956 assert!(ctx.contains("p"));
2957 assert!(ctx.contains("o"));
2958 }
2959
2960 #[test]
2961 fn test_bind_insert_triple_and_delete_triple_with_and_without_input() {
2962 use crate::query::plan::{DeleteTripleOp, InsertTripleOp, TripleComponent};
2963
2964 let plan = LogicalPlan::new(LogicalOperator::InsertTriple(InsertTripleOp {
2966 subject: TripleComponent::Variable("s".to_string()),
2967 predicate: TripleComponent::Iri("http://example.org/p".to_string()),
2968 object: TripleComponent::Variable("o".to_string()),
2969 graph: None,
2970 input: Some(Box::new(LogicalOperator::NodeScan(NodeScanOp {
2971 variable: "hans".to_string(),
2972 label: None,
2973 input: None,
2974 }))),
2975 }));
2976 let mut binder = Binder::new();
2977 let ctx = binder.bind(&plan).unwrap();
2978 assert!(ctx.contains("hans"));
2979
2980 let plan2 = LogicalPlan::new(LogicalOperator::InsertTriple(InsertTripleOp {
2982 subject: TripleComponent::Iri("http://example.org/s".to_string()),
2983 predicate: TripleComponent::Iri("http://example.org/p".to_string()),
2984 object: TripleComponent::Iri("http://example.org/o".to_string()),
2985 graph: None,
2986 input: None,
2987 }));
2988 let mut binder2 = Binder::new();
2989 assert!(binder2.bind(&plan2).is_ok());
2990
2991 let plan3 = LogicalPlan::new(LogicalOperator::DeleteTriple(DeleteTripleOp {
2993 subject: TripleComponent::Variable("s".to_string()),
2994 predicate: TripleComponent::Iri("http://example.org/p".to_string()),
2995 object: TripleComponent::Variable("o".to_string()),
2996 graph: None,
2997 input: Some(Box::new(LogicalOperator::NodeScan(NodeScanOp {
2998 variable: "shosanna".to_string(),
2999 label: None,
3000 input: None,
3001 }))),
3002 }));
3003 let mut binder3 = Binder::new();
3004 let ctx3 = binder3.bind(&plan3).unwrap();
3005 assert!(ctx3.contains("shosanna"));
3006
3007 let plan4 = LogicalPlan::new(LogicalOperator::DeleteTriple(DeleteTripleOp {
3009 subject: TripleComponent::Iri("http://example.org/s".to_string()),
3010 predicate: TripleComponent::Iri("http://example.org/p".to_string()),
3011 object: TripleComponent::Iri("http://example.org/o".to_string()),
3012 graph: None,
3013 input: None,
3014 }));
3015 let mut binder4 = Binder::new();
3016 assert!(binder4.bind(&plan4).is_ok());
3017 }
3018
3019 #[test]
3020 fn test_bind_modify_operator_walks_where_clause() {
3021 use crate::query::plan::ModifyOp;
3022
3023 let plan = LogicalPlan::new(LogicalOperator::Modify(ModifyOp {
3024 delete_templates: vec![],
3025 insert_templates: vec![],
3026 where_clause: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3027 variable: "beatrix".to_string(),
3028 label: None,
3029 input: None,
3030 })),
3031 graph: None,
3032 }));
3033
3034 let mut binder = Binder::new();
3035 let ctx = binder.bind(&plan).unwrap();
3036 assert!(ctx.contains("beatrix"));
3037 }
3038
3039 #[test]
3040 fn test_bind_construct_walks_input() {
3041 use crate::query::plan::ConstructOp;
3042
3043 let plan = LogicalPlan::new(LogicalOperator::Construct(ConstructOp {
3044 templates: vec![],
3045 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3046 variable: "node".to_string(),
3047 label: None,
3048 input: None,
3049 })),
3050 }));
3051 let mut binder = Binder::new();
3052 let ctx = binder.bind(&plan).unwrap();
3053 assert!(ctx.contains("node"));
3054 }
3055
3056 #[test]
3057 fn test_bind_graph_ddl_operators_succeed_without_input() {
3058 use crate::query::plan::{
3059 AddGraphOp, ClearGraphOp, CopyGraphOp, CreateGraphOp, DropGraphOp, LoadGraphOp,
3060 MoveGraphOp,
3061 };
3062
3063 let cases: Vec<LogicalOperator> = vec![
3066 LogicalOperator::ClearGraph(ClearGraphOp {
3067 graph: None,
3068 silent: false,
3069 }),
3070 LogicalOperator::CreateGraph(CreateGraphOp {
3071 graph: "http://example.org/g".to_string(),
3072 silent: false,
3073 }),
3074 LogicalOperator::DropGraph(DropGraphOp {
3075 graph: None,
3076 silent: true,
3077 }),
3078 LogicalOperator::LoadGraph(LoadGraphOp {
3079 source: "http://example.org/data".to_string(),
3080 destination: None,
3081 silent: false,
3082 }),
3083 LogicalOperator::CopyGraph(CopyGraphOp {
3084 source: None,
3085 destination: None,
3086 silent: false,
3087 }),
3088 LogicalOperator::MoveGraph(MoveGraphOp {
3089 source: None,
3090 destination: None,
3091 silent: false,
3092 }),
3093 LogicalOperator::AddGraph(AddGraphOp {
3094 source: None,
3095 destination: None,
3096 silent: false,
3097 }),
3098 ];
3099
3100 for op in cases {
3101 let plan = LogicalPlan::new(op);
3102 let mut binder = Binder::new();
3103 assert!(binder.bind(&plan).is_ok());
3104 }
3105 }
3106
3107 #[test]
3108 fn test_bind_create_property_graph_is_noop() {
3109 use crate::query::plan::CreatePropertyGraphOp;
3110
3111 let plan = LogicalPlan::new(LogicalOperator::CreatePropertyGraph(
3112 CreatePropertyGraphOp {
3113 name: "social".to_string(),
3114 node_tables: vec![],
3115 edge_tables: vec![],
3116 },
3117 ));
3118 let mut binder = Binder::new();
3119 let ctx = binder.bind(&plan).unwrap();
3120 assert!(ctx.is_empty());
3121 }
3122
3123 #[test]
3128 fn test_bind_union_walks_all_inputs() {
3129 use crate::query::plan::UnionOp;
3130
3131 let plan = LogicalPlan::new(LogicalOperator::Union(UnionOp {
3132 inputs: vec![
3133 LogicalOperator::NodeScan(NodeScanOp {
3134 variable: "amsterdam".to_string(),
3135 label: None,
3136 input: None,
3137 }),
3138 LogicalOperator::NodeScan(NodeScanOp {
3139 variable: "berlin".to_string(),
3140 label: None,
3141 input: None,
3142 }),
3143 ],
3144 }));
3145 let mut binder = Binder::new();
3146 let ctx = binder.bind(&plan).unwrap();
3147 assert!(ctx.contains("amsterdam"));
3148 assert!(ctx.contains("berlin"));
3149 }
3150
3151 #[test]
3152 fn test_bind_left_join_with_condition_validates_it() {
3153 use crate::query::plan::LeftJoinOp;
3154
3155 let ok_plan = LogicalPlan::new(LogicalOperator::LeftJoin(LeftJoinOp {
3158 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3159 variable: "paris".to_string(),
3160 label: None,
3161 input: None,
3162 })),
3163 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3164 variable: "prague".to_string(),
3165 label: None,
3166 input: None,
3167 })),
3168 condition: Some(LogicalExpression::Variable("paris".to_string())),
3169 }));
3170 let mut binder = Binder::new();
3171 assert!(binder.bind(&ok_plan).is_ok());
3172
3173 let plan_no_cond = LogicalPlan::new(LogicalOperator::LeftJoin(LeftJoinOp {
3175 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3176 variable: "paris".to_string(),
3177 label: None,
3178 input: None,
3179 })),
3180 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3181 variable: "prague".to_string(),
3182 label: None,
3183 input: None,
3184 })),
3185 condition: None,
3186 }));
3187 assert!(Binder::new().bind(&plan_no_cond).is_ok());
3188
3189 let bad_plan = LogicalPlan::new(LogicalOperator::LeftJoin(LeftJoinOp {
3191 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3192 variable: "paris".to_string(),
3193 label: None,
3194 input: None,
3195 })),
3196 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3197 variable: "prague".to_string(),
3198 label: None,
3199 input: None,
3200 })),
3201 condition: Some(LogicalExpression::Variable("missing".to_string())),
3202 }));
3203 assert!(Binder::new().bind(&bad_plan).is_err());
3204 }
3205
3206 #[test]
3207 fn test_bind_anti_join_walks_both_sides() {
3208 use crate::query::plan::AntiJoinOp;
3209
3210 let plan = LogicalPlan::new(LogicalOperator::AntiJoin(AntiJoinOp {
3211 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3212 variable: "left_side".to_string(),
3213 label: None,
3214 input: None,
3215 })),
3216 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3217 variable: "right_side".to_string(),
3218 label: None,
3219 input: None,
3220 })),
3221 }));
3222 let mut binder = Binder::new();
3223 let ctx = binder.bind(&plan).unwrap();
3224 assert!(ctx.contains("left_side"));
3225 assert!(ctx.contains("right_side"));
3226 }
3227
3228 #[test]
3229 fn test_bind_bind_operator_adds_variable_and_validates_expression() {
3230 use crate::query::plan::BindOp;
3231
3232 let plan = LogicalPlan::new(LogicalOperator::Bind(BindOp {
3234 expression: LogicalExpression::Variable("n".to_string()),
3235 variable: "x".to_string(),
3236 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3237 variable: "n".to_string(),
3238 label: None,
3239 input: None,
3240 })),
3241 }));
3242 let mut binder = Binder::new();
3243 let ctx = binder.bind(&plan).unwrap();
3244 assert!(ctx.contains("x"));
3245
3246 let bad_plan = LogicalPlan::new(LogicalOperator::Bind(BindOp {
3248 expression: LogicalExpression::Variable("ghost".to_string()),
3249 variable: "x".to_string(),
3250 input: Box::new(LogicalOperator::Empty),
3251 }));
3252 assert!(Binder::new().bind(&bad_plan).is_err());
3253 }
3254
3255 #[test]
3256 fn test_bind_except_intersect_otherwise_walk_both_sides() {
3257 use crate::query::plan::{ExceptOp, IntersectOp, OtherwiseOp};
3258
3259 for op in [
3260 LogicalOperator::Except(ExceptOp {
3261 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3262 variable: "l".to_string(),
3263 label: None,
3264 input: None,
3265 })),
3266 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3267 variable: "r".to_string(),
3268 label: None,
3269 input: None,
3270 })),
3271 all: false,
3272 }),
3273 LogicalOperator::Intersect(IntersectOp {
3274 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3275 variable: "l".to_string(),
3276 label: None,
3277 input: None,
3278 })),
3279 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3280 variable: "r".to_string(),
3281 label: None,
3282 input: None,
3283 })),
3284 all: false,
3285 }),
3286 LogicalOperator::Otherwise(OtherwiseOp {
3287 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3288 variable: "l".to_string(),
3289 label: None,
3290 input: None,
3291 })),
3292 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3293 variable: "r".to_string(),
3294 label: None,
3295 input: None,
3296 })),
3297 }),
3298 ] {
3299 let plan = LogicalPlan::new(op);
3300 let mut binder = Binder::new();
3301 let ctx = binder.bind(&plan).unwrap();
3302 assert!(ctx.contains("l"));
3303 assert!(ctx.contains("r"));
3304 }
3305 }
3306
3307 #[test]
3308 fn test_bind_multi_way_join_validates_conditions() {
3309 use crate::query::plan::{JoinCondition, MultiWayJoinOp};
3310
3311 let plan = LogicalPlan::new(LogicalOperator::MultiWayJoin(MultiWayJoinOp {
3312 inputs: vec![
3313 LogicalOperator::NodeScan(NodeScanOp {
3314 variable: "a".to_string(),
3315 label: None,
3316 input: None,
3317 }),
3318 LogicalOperator::NodeScan(NodeScanOp {
3319 variable: "b".to_string(),
3320 label: None,
3321 input: None,
3322 }),
3323 ],
3324 conditions: vec![JoinCondition {
3325 left: LogicalExpression::Variable("a".to_string()),
3326 right: LogicalExpression::Variable("b".to_string()),
3327 }],
3328 shared_variables: vec![],
3329 }));
3330 let mut binder = Binder::new();
3331 assert!(binder.bind(&plan).is_ok());
3332
3333 let bad_plan = LogicalPlan::new(LogicalOperator::MultiWayJoin(MultiWayJoinOp {
3335 inputs: vec![LogicalOperator::NodeScan(NodeScanOp {
3336 variable: "a".to_string(),
3337 label: None,
3338 input: None,
3339 })],
3340 conditions: vec![JoinCondition {
3341 left: LogicalExpression::Variable("a".to_string()),
3342 right: LogicalExpression::Variable("nope".to_string()),
3343 }],
3344 shared_variables: vec![],
3345 }));
3346 assert!(Binder::new().bind(&bad_plan).is_err());
3347 }
3348
3349 #[test]
3354 fn test_bind_parameter_scan_registers_columns() {
3355 use crate::query::plan::ParameterScanOp;
3356
3357 let plan = LogicalPlan::new(LogicalOperator::ParameterScan(ParameterScanOp {
3358 columns: vec!["vincent".to_string(), "jules".to_string()],
3359 }));
3360 let mut binder = Binder::new();
3361 let ctx = binder.bind(&plan).unwrap();
3362 assert!(ctx.contains("vincent"));
3363 assert!(ctx.contains("jules"));
3364 }
3365
3366 #[test]
3367 fn test_bind_apply_removes_internal_variables_from_input_and_subplan() {
3368 use crate::query::plan::ApplyOp;
3369
3370 let plan = LogicalPlan::new(LogicalOperator::Apply(ApplyOp {
3374 input: Box::new(LogicalOperator::Return(ReturnOp {
3375 items: vec![ReturnItem {
3376 expression: LogicalExpression::Variable("inner_scan".to_string()),
3377 alias: Some("out_col".to_string()),
3378 }],
3379 distinct: false,
3380 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3381 variable: "inner_scan".to_string(),
3382 label: None,
3383 input: None,
3384 })),
3385 })),
3386 subplan: Box::new(LogicalOperator::Return(ReturnOp {
3387 items: vec![ReturnItem {
3388 expression: LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
3389 alias: Some("sub_col".to_string()),
3390 }],
3391 distinct: false,
3392 input: Box::new(LogicalOperator::Empty),
3393 })),
3394 shared_variables: vec![],
3395 optional: false,
3396 }));
3397
3398 let mut binder = Binder::new();
3399 let ctx = binder.bind(&plan).unwrap();
3400
3401 assert!(ctx.contains("out_col"), "input projection exposed");
3404 assert!(ctx.contains("sub_col"), "subplan output registered");
3405 assert!(
3406 !ctx.contains("inner_scan"),
3407 "internal scan variable should be scoped out"
3408 );
3409 }
3410
3411 #[test]
3412 fn test_bind_apply_without_explicit_projection_keeps_input_variables() {
3413 use crate::query::plan::ApplyOp;
3414
3415 let plan = LogicalPlan::new(LogicalOperator::Apply(ApplyOp {
3418 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3419 variable: "outer".to_string(),
3420 label: None,
3421 input: None,
3422 })),
3423 subplan: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3424 variable: "inner".to_string(),
3425 label: None,
3426 input: None,
3427 })),
3428 shared_variables: vec![],
3429 optional: false,
3430 }));
3431
3432 let mut binder = Binder::new();
3433 let ctx = binder.bind(&plan).unwrap();
3434 assert!(ctx.contains("outer"));
3435 }
3436
3437 #[test]
3442 fn test_merge_relationship_rejects_undefined_source_and_target() {
3443 use crate::query::plan::MergeRelationshipOp;
3444
3445 let plan = LogicalPlan::new(LogicalOperator::MergeRelationship(MergeRelationshipOp {
3447 variable: "r".to_string(),
3448 source_variable: "phantom".to_string(),
3449 target_variable: "b".to_string(),
3450 edge_type: "KNOWS".to_string(),
3451 match_properties: vec![],
3452 on_create: vec![],
3453 on_match: vec![],
3454 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3455 variable: "b".to_string(),
3456 label: None,
3457 input: None,
3458 })),
3459 }));
3460 let err = Binder::new().bind(&plan).unwrap_err();
3461 assert!(err.to_string().contains("MERGE relationship source"));
3462
3463 let plan2 = LogicalPlan::new(LogicalOperator::MergeRelationship(MergeRelationshipOp {
3465 variable: "r".to_string(),
3466 source_variable: "a".to_string(),
3467 target_variable: "phantom".to_string(),
3468 edge_type: "KNOWS".to_string(),
3469 match_properties: vec![],
3470 on_create: vec![],
3471 on_match: vec![],
3472 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3473 variable: "a".to_string(),
3474 label: None,
3475 input: None,
3476 })),
3477 }));
3478 let err2 = Binder::new().bind(&plan2).unwrap_err();
3479 assert!(err2.to_string().contains("MERGE relationship target"));
3480 }
3481
3482 #[test]
3483 fn test_merge_relationship_happy_path_binds_edge_variable() {
3484 use crate::query::plan::{JoinOp, JoinType, MergeRelationshipOp};
3485
3486 let plan = LogicalPlan::new(LogicalOperator::MergeRelationship(MergeRelationshipOp {
3487 variable: "r".to_string(),
3488 source_variable: "a".to_string(),
3489 target_variable: "b".to_string(),
3490 edge_type: "KNOWS".to_string(),
3491 match_properties: vec![(
3492 "since".to_string(),
3493 LogicalExpression::Literal(grafeo_common::types::Value::Int64(2020)),
3494 )],
3495 on_create: vec![(
3496 "created_at".to_string(),
3497 LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
3498 )],
3499 on_match: vec![(
3500 "updated_at".to_string(),
3501 LogicalExpression::Literal(grafeo_common::types::Value::Int64(2)),
3502 )],
3503 input: Box::new(LogicalOperator::Join(JoinOp {
3504 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3505 variable: "a".to_string(),
3506 label: None,
3507 input: None,
3508 })),
3509 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3510 variable: "b".to_string(),
3511 label: None,
3512 input: None,
3513 })),
3514 join_type: JoinType::Cross,
3515 conditions: vec![],
3516 })),
3517 }));
3518 let mut binder = Binder::new();
3519 let ctx = binder.bind(&plan).unwrap();
3520 let edge = ctx.get("r").expect("edge variable bound");
3521 assert!(edge.is_edge);
3522 assert!(!edge.is_node);
3523 }
3524
3525 #[test]
3530 fn test_unwind_ordinality_and_offset_variables() {
3531 use crate::query::plan::UnwindOp;
3532
3533 let plan = LogicalPlan::new(LogicalOperator::Unwind(UnwindOp {
3534 expression: LogicalExpression::List(vec![LogicalExpression::Literal(
3535 grafeo_common::types::Value::Int64(1),
3536 )]),
3537 variable: "item".to_string(),
3538 ordinality_var: Some("ord".to_string()),
3539 offset_var: Some("off".to_string()),
3540 input: Box::new(LogicalOperator::Empty),
3541 }));
3542 let mut binder = Binder::new();
3543 let ctx = binder.bind(&plan).unwrap();
3544 assert_eq!(ctx.get("ord").unwrap().data_type, LogicalType::Int64);
3545 assert_eq!(ctx.get("off").unwrap().data_type, LogicalType::Int64);
3546 }
3547
3548 #[test]
3549 fn test_call_procedure_with_and_without_yields() {
3550 use crate::query::plan::{CallProcedureOp, ProcedureYield};
3551
3552 let plan = LogicalPlan::new(LogicalOperator::CallProcedure(CallProcedureOp {
3554 name: vec!["grafeo".to_string(), "pagerank".to_string()],
3555 arguments: vec![],
3556 yield_items: Some(vec![
3557 ProcedureYield {
3558 field_name: "nodeId".to_string(),
3559 alias: None,
3560 },
3561 ProcedureYield {
3562 field_name: "score".to_string(),
3563 alias: Some("rank".to_string()),
3564 },
3565 ]),
3566 }));
3567 let mut binder = Binder::new();
3568 let ctx = binder.bind(&plan).unwrap();
3569 assert!(ctx.contains("nodeId"));
3570 assert!(ctx.contains("rank"));
3571 assert!(!ctx.contains("score"), "aliased yield hides raw name");
3572
3573 let plan2 = LogicalPlan::new(LogicalOperator::CallProcedure(CallProcedureOp {
3575 name: vec!["grafeo".to_string(), "noop".to_string()],
3576 arguments: vec![],
3577 yield_items: None,
3578 }));
3579 let mut binder2 = Binder::new();
3580 let ctx2 = binder2.bind(&plan2).unwrap();
3581 assert!(ctx2.is_empty());
3582 }
3583
3584 #[test]
3585 fn test_load_data_binds_row_variable() {
3586 use crate::query::plan::{LoadDataFormat, LoadDataOp};
3587
3588 let plan = LogicalPlan::new(LogicalOperator::LoadData(LoadDataOp {
3589 format: LoadDataFormat::Csv,
3590 with_headers: true,
3591 path: "/tmp/data.csv".to_string(),
3592 variable: "row".to_string(),
3593 field_terminator: None,
3594 }));
3595 let mut binder = Binder::new();
3596 let ctx = binder.bind(&plan).unwrap();
3597 assert!(ctx.contains("row"));
3598 assert_eq!(ctx.get("row").unwrap().data_type, LogicalType::Any);
3599 }
3600
3601 #[test]
3602 fn test_map_collect_registers_alias() {
3603 use crate::query::plan::MapCollectOp;
3604
3605 let plan = LogicalPlan::new(LogicalOperator::MapCollect(MapCollectOp {
3606 key_var: "k".to_string(),
3607 value_var: "v".to_string(),
3608 alias: "grouped".to_string(),
3609 input: Box::new(LogicalOperator::Empty),
3610 }));
3611 let mut binder = Binder::new();
3612 let ctx = binder.bind(&plan).unwrap();
3613 assert!(ctx.contains("grouped"));
3614 }
3615
3616 #[test]
3617 fn test_vector_join_registers_right_and_score_variables() {
3618 use crate::query::plan::VectorJoinOp;
3619
3620 let plan = LogicalPlan::new(LogicalOperator::VectorJoin(VectorJoinOp {
3621 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3622 variable: "a".to_string(),
3623 label: None,
3624 input: None,
3625 })),
3626 left_vector_variable: None,
3627 left_property: None,
3628 query_vector: LogicalExpression::Literal(grafeo_common::types::Value::Int64(0)),
3629 right_variable: "b".to_string(),
3630 right_property: "embedding".to_string(),
3631 right_label: None,
3632 index_name: None,
3633 k: 5,
3634 metric: None,
3635 min_similarity: None,
3636 max_distance: None,
3637 score_variable: Some("score".to_string()),
3638 }));
3639 let mut binder = Binder::new();
3640 let ctx = binder.bind(&plan).unwrap();
3641 assert!(ctx.get("b").unwrap().is_node);
3642 assert_eq!(ctx.get("score").unwrap().data_type, LogicalType::Float64);
3643
3644 let bad_plan = LogicalPlan::new(LogicalOperator::VectorJoin(VectorJoinOp {
3646 input: Box::new(LogicalOperator::Empty),
3647 left_vector_variable: None,
3648 left_property: None,
3649 query_vector: LogicalExpression::Variable("undef_vec".to_string()),
3650 right_variable: "b".to_string(),
3651 right_property: "embedding".to_string(),
3652 right_label: None,
3653 index_name: None,
3654 k: 5,
3655 metric: None,
3656 min_similarity: None,
3657 max_distance: None,
3658 score_variable: None,
3659 }));
3660 assert!(Binder::new().bind(&bad_plan).is_err());
3661 }
3662
3663 #[test]
3668 fn test_expression_validation_unary_index_map_projection() {
3669 use crate::query::plan::{MapProjectionEntry, UnaryOp};
3670
3671 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3673 items: vec![ReturnItem {
3674 expression: LogicalExpression::Unary {
3675 op: UnaryOp::Not,
3676 operand: Box::new(LogicalExpression::Variable("ghost".to_string())),
3677 },
3678 alias: None,
3679 }],
3680 distinct: false,
3681 input: Box::new(LogicalOperator::Empty),
3682 }));
3683 assert!(Binder::new().bind(&plan).is_err());
3684
3685 let plan2 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3687 items: vec![ReturnItem {
3688 expression: LogicalExpression::IndexAccess {
3689 base: Box::new(LogicalExpression::Variable("xs".to_string())),
3690 index: Box::new(LogicalExpression::Variable("idx_ghost".to_string())),
3691 },
3692 alias: None,
3693 }],
3694 distinct: false,
3695 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3696 variable: "xs".to_string(),
3697 label: None,
3698 input: None,
3699 })),
3700 }));
3701 let err = Binder::new().bind(&plan2).unwrap_err();
3702 assert!(err.to_string().contains("idx_ghost"));
3703
3704 let plan3 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3706 items: vec![ReturnItem {
3707 expression: LogicalExpression::MapProjection {
3708 base: "n".to_string(),
3709 entries: vec![
3710 MapProjectionEntry::PropertySelector("name".to_string()),
3711 MapProjectionEntry::AllProperties,
3712 MapProjectionEntry::LiteralEntry(
3713 "extra".to_string(),
3714 LogicalExpression::Variable("missing".to_string()),
3715 ),
3716 ],
3717 },
3718 alias: None,
3719 }],
3720 distinct: false,
3721 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3722 variable: "n".to_string(),
3723 label: None,
3724 input: None,
3725 })),
3726 }));
3727 assert!(Binder::new().bind(&plan3).is_err());
3728
3729 let plan4 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3731 items: vec![ReturnItem {
3732 expression: LogicalExpression::MapProjection {
3733 base: "unknown".to_string(),
3734 entries: vec![MapProjectionEntry::AllProperties],
3735 },
3736 alias: None,
3737 }],
3738 distinct: false,
3739 input: Box::new(LogicalOperator::Empty),
3740 }));
3741 let err4 = Binder::new().bind(&plan4).unwrap_err();
3742 assert!(err4.to_string().contains("map projection"));
3743 }
3744
3745 #[test]
3746 fn test_expression_validation_list_and_parameter_and_subquery() {
3747 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3749 items: vec![ReturnItem {
3750 expression: LogicalExpression::Parameter("p".to_string()),
3751 alias: None,
3752 }],
3753 distinct: false,
3754 input: Box::new(LogicalOperator::Empty),
3755 }));
3756 assert!(Binder::new().bind(&plan).is_ok());
3757
3758 let plan2 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3760 items: vec![ReturnItem {
3761 expression: LogicalExpression::List(vec![LogicalExpression::Variable(
3762 "no_such".to_string(),
3763 )]),
3764 alias: None,
3765 }],
3766 distinct: false,
3767 input: Box::new(LogicalOperator::Empty),
3768 }));
3769 assert!(Binder::new().bind(&plan2).is_err());
3770
3771 let plan3 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3773 items: vec![ReturnItem {
3774 expression: LogicalExpression::ExistsSubquery(Box::new(LogicalOperator::Empty)),
3775 alias: None,
3776 }],
3777 distinct: false,
3778 input: Box::new(LogicalOperator::Empty),
3779 }));
3780 assert!(Binder::new().bind(&plan3).is_ok());
3781
3782 let plan4 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3783 items: vec![ReturnItem {
3784 expression: LogicalExpression::CountSubquery(Box::new(LogicalOperator::Empty)),
3785 alias: None,
3786 }],
3787 distinct: false,
3788 input: Box::new(LogicalOperator::Empty),
3789 }));
3790 assert!(Binder::new().bind(&plan4).is_ok());
3791
3792 let plan5 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3793 items: vec![ReturnItem {
3794 expression: LogicalExpression::ValueSubquery(Box::new(LogicalOperator::Empty)),
3795 alias: None,
3796 }],
3797 distinct: false,
3798 input: Box::new(LogicalOperator::Empty),
3799 }));
3800 assert!(Binder::new().bind(&plan5).is_ok());
3801 }
3802
3803 #[test]
3804 fn test_expression_validation_list_predicate_and_pattern_comprehension() {
3805 use crate::query::plan::ListPredicateKind;
3806
3807 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3809 items: vec![ReturnItem {
3810 expression: LogicalExpression::ListPredicate {
3811 kind: ListPredicateKind::All,
3812 variable: "x".to_string(),
3813 list_expr: Box::new(LogicalExpression::Variable("missing_list".to_string())),
3814 predicate: Box::new(LogicalExpression::Literal(
3815 grafeo_common::types::Value::Bool(true),
3816 )),
3817 },
3818 alias: None,
3819 }],
3820 distinct: false,
3821 input: Box::new(LogicalOperator::Empty),
3822 }));
3823 assert!(Binder::new().bind(&plan).is_err());
3824
3825 let plan2 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3827 items: vec![ReturnItem {
3828 expression: LogicalExpression::PatternComprehension {
3829 subplan: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3830 variable: "f".to_string(),
3831 label: None,
3832 input: None,
3833 })),
3834 projection: Box::new(LogicalExpression::Property {
3835 variable: "f".to_string(),
3836 property: "name".to_string(),
3837 }),
3838 },
3839 alias: Some("names".to_string()),
3840 }],
3841 distinct: false,
3842 input: Box::new(LogicalOperator::Empty),
3843 }));
3844 assert!(Binder::new().bind(&plan2).is_ok());
3845
3846 let plan3 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3848 items: vec![ReturnItem {
3849 expression: LogicalExpression::PatternComprehension {
3850 subplan: Box::new(LogicalOperator::NodeScan(NodeScanOp {
3851 variable: "f".to_string(),
3852 label: None,
3853 input: None,
3854 })),
3855 projection: Box::new(LogicalExpression::Variable("ghost".to_string())),
3856 },
3857 alias: None,
3858 }],
3859 distinct: false,
3860 input: Box::new(LogicalOperator::Empty),
3861 }));
3862 assert!(Binder::new().bind(&plan3).is_err());
3863 }
3864
3865 #[test]
3866 fn test_expression_validation_reduce_adds_and_removes_locals() {
3867 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3870 items: vec![ReturnItem {
3871 expression: LogicalExpression::Reduce {
3872 accumulator: "acc".to_string(),
3873 initial: Box::new(LogicalExpression::Literal(
3874 grafeo_common::types::Value::Int64(0),
3875 )),
3876 variable: "x".to_string(),
3877 list: Box::new(LogicalExpression::Variable("xs".to_string())),
3878 expression: Box::new(LogicalExpression::Binary {
3879 left: Box::new(LogicalExpression::Variable("acc".to_string())),
3880 op: crate::query::plan::BinaryOp::Add,
3881 right: Box::new(LogicalExpression::Variable("x".to_string())),
3882 }),
3883 },
3884 alias: Some("sum".to_string()),
3885 }],
3886 distinct: false,
3887 input: Box::new(LogicalOperator::Project(crate::query::plan::ProjectOp {
3888 projections: vec![crate::query::plan::Projection {
3889 expression: LogicalExpression::List(vec![LogicalExpression::Literal(
3890 grafeo_common::types::Value::Int64(1),
3891 )]),
3892 alias: Some("xs".to_string()),
3893 }],
3894 input: Box::new(LogicalOperator::Empty),
3895 pass_through_input: false,
3896 })),
3897 }));
3898 let mut binder = Binder::new();
3899 let ctx = binder.bind(&plan).unwrap();
3900 assert!(!ctx.contains("acc"));
3902 assert!(!ctx.contains("x"));
3903 assert!(ctx.contains("sum"));
3905 }
3906
3907 #[test]
3908 fn test_expression_validation_reduce_preserves_preexisting_locals() {
3909 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
3912 items: vec![ReturnItem {
3913 expression: LogicalExpression::Reduce {
3914 accumulator: "acc".to_string(),
3915 initial: Box::new(LogicalExpression::Literal(
3916 grafeo_common::types::Value::Int64(0),
3917 )),
3918 variable: "x".to_string(),
3919 list: Box::new(LogicalExpression::Variable("acc".to_string())),
3920 expression: Box::new(LogicalExpression::Variable("acc".to_string())),
3921 },
3922 alias: Some("r".to_string()),
3923 }],
3924 distinct: false,
3925 input: Box::new(LogicalOperator::Project(crate::query::plan::ProjectOp {
3926 projections: vec![
3927 crate::query::plan::Projection {
3928 expression: LogicalExpression::List(vec![]),
3929 alias: Some("acc".to_string()),
3930 },
3931 crate::query::plan::Projection {
3932 expression: LogicalExpression::Literal(grafeo_common::types::Value::Int64(
3933 0,
3934 )),
3935 alias: Some("x".to_string()),
3936 },
3937 ],
3938 input: Box::new(LogicalOperator::Empty),
3939 pass_through_input: false,
3940 })),
3941 }));
3942 let mut binder = Binder::new();
3943 let ctx = binder.bind(&plan).unwrap();
3944 assert!(ctx.contains("acc"));
3946 assert!(ctx.contains("x"));
3947 }
3948
3949 #[test]
3954 fn test_infer_expression_type_for_literals_and_functions() {
3955 use crate::query::plan::{ProjectOp, Projection};
3956 use grafeo_common::types::Value;
3957
3958 let projections = vec![
3960 Projection {
3961 expression: LogicalExpression::Literal(Value::Bool(true)),
3962 alias: Some("b".to_string()),
3963 },
3964 Projection {
3965 expression: LogicalExpression::Literal(Value::Int64(1)),
3966 alias: Some("i".to_string()),
3967 },
3968 Projection {
3969 expression: LogicalExpression::Literal(Value::Float64(1.5)),
3970 alias: Some("f".to_string()),
3971 },
3972 Projection {
3973 expression: LogicalExpression::Literal(Value::String("s".into())),
3974 alias: Some("s".to_string()),
3975 },
3976 Projection {
3977 expression: LogicalExpression::Literal(Value::List(std::sync::Arc::from(Vec::<
3978 Value,
3979 >::new(
3980 )))),
3981 alias: Some("l".to_string()),
3982 },
3983 Projection {
3984 expression: LogicalExpression::Literal(Value::Map(std::sync::Arc::new(
3985 std::collections::BTreeMap::new(),
3986 ))),
3987 alias: Some("m".to_string()),
3988 },
3989 Projection {
3990 expression: LogicalExpression::Literal(Value::Null),
3991 alias: Some("n".to_string()),
3992 },
3993 Projection {
3994 expression: LogicalExpression::FunctionCall {
3995 name: "count".to_string(),
3996 args: vec![LogicalExpression::Literal(Value::Int64(1))],
3997 distinct: false,
3998 },
3999 alias: Some("cnt".to_string()),
4000 },
4001 Projection {
4002 expression: LogicalExpression::FunctionCall {
4003 name: "AVG".to_string(),
4004 args: vec![LogicalExpression::Literal(Value::Int64(1))],
4005 distinct: false,
4006 },
4007 alias: Some("avg_val".to_string()),
4008 },
4009 Projection {
4010 expression: LogicalExpression::FunctionCall {
4011 name: "type".to_string(),
4012 args: vec![],
4013 distinct: false,
4014 },
4015 alias: Some("tname".to_string()),
4016 },
4017 Projection {
4018 expression: LogicalExpression::FunctionCall {
4019 name: "labels".to_string(),
4020 args: vec![],
4021 distinct: false,
4022 },
4023 alias: Some("lbls".to_string()),
4024 },
4025 Projection {
4026 expression: LogicalExpression::FunctionCall {
4027 name: "unknown_fn".to_string(),
4028 args: vec![],
4029 distinct: false,
4030 },
4031 alias: Some("u".to_string()),
4032 },
4033 Projection {
4035 expression: LogicalExpression::List(vec![]),
4036 alias: Some("lit_list".to_string()),
4037 },
4038 Projection {
4039 expression: LogicalExpression::Map(vec![]),
4040 alias: Some("lit_map".to_string()),
4041 },
4042 Projection {
4044 expression: LogicalExpression::Unary {
4045 op: crate::query::plan::UnaryOp::Not,
4046 operand: Box::new(LogicalExpression::Literal(Value::Bool(true))),
4047 },
4048 alias: Some("unary_ty".to_string()),
4049 },
4050 Projection {
4051 expression: LogicalExpression::Binary {
4052 left: Box::new(LogicalExpression::Literal(Value::Int64(1))),
4053 op: crate::query::plan::BinaryOp::Add,
4054 right: Box::new(LogicalExpression::Literal(Value::Int64(2))),
4055 },
4056 alias: Some("bin_ty".to_string()),
4057 },
4058 ];
4059
4060 let plan = LogicalPlan::new(LogicalOperator::Project(ProjectOp {
4061 projections,
4062 input: Box::new(LogicalOperator::Empty),
4063 pass_through_input: false,
4064 }));
4065 let mut binder = Binder::new();
4066 let ctx = binder.bind(&plan).unwrap();
4067
4068 assert_eq!(ctx.get("b").unwrap().data_type, LogicalType::Bool);
4069 assert_eq!(ctx.get("i").unwrap().data_type, LogicalType::Int64);
4070 assert_eq!(ctx.get("f").unwrap().data_type, LogicalType::Float64);
4071 assert_eq!(ctx.get("s").unwrap().data_type, LogicalType::String);
4072 assert_eq!(ctx.get("l").unwrap().data_type, LogicalType::Any);
4073 assert_eq!(ctx.get("m").unwrap().data_type, LogicalType::Any);
4074 assert_eq!(ctx.get("n").unwrap().data_type, LogicalType::Any);
4075 assert_eq!(ctx.get("cnt").unwrap().data_type, LogicalType::Int64);
4076 assert_eq!(ctx.get("avg_val").unwrap().data_type, LogicalType::Float64);
4077 assert_eq!(ctx.get("tname").unwrap().data_type, LogicalType::String);
4078 assert_eq!(ctx.get("lbls").unwrap().data_type, LogicalType::Any);
4079 assert_eq!(ctx.get("u").unwrap().data_type, LogicalType::Any);
4080 assert_eq!(ctx.get("lit_list").unwrap().data_type, LogicalType::Any);
4081 assert_eq!(ctx.get("lit_map").unwrap().data_type, LogicalType::Any);
4082 assert_eq!(ctx.get("unary_ty").unwrap().data_type, LogicalType::Any);
4083 assert_eq!(ctx.get("bin_ty").unwrap().data_type, LogicalType::Any);
4084 }
4085
4086 #[test]
4087 fn test_infer_entity_status_for_case_projection() {
4088 use crate::query::plan::{ProjectOp, Projection};
4089
4090 let plan = LogicalPlan::new(LogicalOperator::Project(ProjectOp {
4093 projections: vec![
4094 Projection {
4095 expression: LogicalExpression::Variable("n".to_string()),
4096 alias: Some("original".to_string()),
4097 },
4098 Projection {
4099 expression: LogicalExpression::Case {
4100 operand: None,
4101 when_clauses: vec![(
4102 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
4103 LogicalExpression::Variable("n".to_string()),
4104 )],
4105 else_clause: Some(Box::new(LogicalExpression::Variable("n".to_string()))),
4106 },
4107 alias: Some("case_node".to_string()),
4108 },
4109 Projection {
4110 expression: LogicalExpression::Case {
4112 operand: None,
4113 when_clauses: vec![],
4114 else_clause: None,
4115 },
4116 alias: Some("empty_case".to_string()),
4117 },
4118 Projection {
4119 expression: LogicalExpression::Case {
4121 operand: None,
4122 when_clauses: vec![(
4123 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
4124 LogicalExpression::Variable("n".to_string()),
4125 )],
4126 else_clause: Some(Box::new(LogicalExpression::Literal(
4127 grafeo_common::types::Value::Int64(0),
4128 ))),
4129 },
4130 alias: Some("mixed_case".to_string()),
4131 },
4132 ],
4133 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
4134 variable: "n".to_string(),
4135 label: None,
4136 input: None,
4137 })),
4138 pass_through_input: false,
4139 }));
4140 let mut binder = Binder::new();
4141 let ctx = binder.bind(&plan).unwrap();
4142
4143 assert!(ctx.get("original").unwrap().is_node);
4144 assert!(ctx.get("case_node").unwrap().is_node);
4145 let empty = ctx.get("empty_case").unwrap();
4146 assert!(!empty.is_node && !empty.is_edge);
4147 let mixed = ctx.get("mixed_case").unwrap();
4148 assert!(
4149 !mixed.is_node && !mixed.is_edge,
4150 "mixed Case branches lose entity status"
4151 );
4152 }
4153
4154 #[test]
4159 fn test_bind_aggregate_registers_group_by_column_and_alias() {
4160 use crate::query::plan::{AggregateExpr, AggregateFunction, AggregateOp};
4161
4162 let plan = LogicalPlan::new(LogicalOperator::Aggregate(AggregateOp {
4163 group_by: vec![LogicalExpression::Property {
4164 variable: "n".to_string(),
4165 property: "city".to_string(),
4166 }],
4167 aggregates: vec![AggregateExpr {
4168 function: AggregateFunction::Count,
4169 expression: Some(LogicalExpression::Variable("n".to_string())),
4170 expression2: None,
4171 distinct: false,
4172 alias: Some("c".to_string()),
4173 percentile: None,
4174 separator: None,
4175 }],
4176 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
4177 variable: "n".to_string(),
4178 label: None,
4179 input: None,
4180 })),
4181 having: None,
4182 }));
4183 let mut binder = Binder::new();
4184 let ctx = binder.bind(&plan).unwrap();
4185 assert!(ctx.contains("c"), "aggregate alias registered");
4186 assert!(ctx.contains("n.city"), "group-by column name registered");
4187 }
4188
4189 #[test]
4190 fn test_apply_registers_aggregate_subplan_columns() {
4191 use crate::query::plan::{
4192 AggregateExpr, AggregateFunction, AggregateOp, ApplyOp, DistinctOp,
4193 };
4194
4195 let subplan = LogicalOperator::Distinct(DistinctOp {
4198 input: Box::new(LogicalOperator::Aggregate(AggregateOp {
4199 group_by: vec![],
4200 aggregates: vec![AggregateExpr {
4201 function: AggregateFunction::Count,
4202 expression: None,
4203 expression2: None,
4204 distinct: false,
4205 alias: Some("total".to_string()),
4206 percentile: None,
4207 separator: None,
4208 }],
4209 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
4210 variable: "n".to_string(),
4211 label: None,
4212 input: None,
4213 })),
4214 having: None,
4215 })),
4216 columns: None,
4217 });
4218
4219 let plan = LogicalPlan::new(LogicalOperator::Apply(ApplyOp {
4220 input: Box::new(LogicalOperator::Empty),
4221 subplan: Box::new(subplan),
4222 shared_variables: vec![],
4223 optional: false,
4224 }));
4225
4226 let mut binder = Binder::new();
4227 let ctx = binder.bind(&plan).unwrap();
4228 assert!(
4229 ctx.contains("total"),
4230 "aggregate alias surfaces through Distinct wrapper"
4231 );
4232 }
4233
4234 #[test]
4235 fn test_apply_registers_sort_and_limit_wrapped_return_columns() {
4236 use crate::query::plan::{ApplyOp, CountExpr, LimitOp, SortKey, SortOp, SortOrder};
4237
4238 let subplan = LogicalOperator::Limit(LimitOp {
4240 count: CountExpr::Literal(10),
4241 input: Box::new(LogicalOperator::Sort(SortOp {
4242 keys: vec![SortKey {
4243 expression: LogicalExpression::Variable("n".to_string()),
4244 order: SortOrder::Ascending,
4245 nulls: None,
4246 }],
4247 input: Box::new(LogicalOperator::Return(ReturnOp {
4248 items: vec![
4249 ReturnItem {
4250 expression: LogicalExpression::Variable("n".to_string()),
4251 alias: None,
4252 },
4253 ReturnItem {
4254 expression: LogicalExpression::Property {
4255 variable: "n".to_string(),
4256 property: "name".to_string(),
4257 },
4258 alias: None,
4259 },
4260 ReturnItem {
4261 expression: LogicalExpression::Literal(
4263 grafeo_common::types::Value::Int64(1),
4264 ),
4265 alias: None,
4266 },
4267 ],
4268 distinct: false,
4269 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
4270 variable: "n".to_string(),
4271 label: None,
4272 input: None,
4273 })),
4274 })),
4275 })),
4276 });
4277
4278 let plan = LogicalPlan::new(LogicalOperator::Apply(ApplyOp {
4279 input: Box::new(LogicalOperator::Empty),
4280 subplan: Box::new(subplan),
4281 shared_variables: vec![],
4282 optional: false,
4283 }));
4284
4285 let mut binder = Binder::new();
4286 let ctx = binder.bind(&plan).unwrap();
4287 assert!(ctx.contains("n"));
4289 assert!(ctx.contains("n.name"));
4291 }
4292
4293 #[test]
4294 fn test_horizontal_aggregate_is_noop_in_binder() {
4295 use crate::query::plan::{AggregateFunction, EntityKind, HorizontalAggregateOp};
4296
4297 let plan = LogicalPlan::new(LogicalOperator::HorizontalAggregate(
4298 HorizontalAggregateOp {
4299 list_column: "_path_edges_p".to_string(),
4300 entity_kind: EntityKind::Edge,
4301 function: AggregateFunction::Sum,
4302 property: "weight".to_string(),
4303 alias: "total".to_string(),
4304 input: Box::new(LogicalOperator::Empty),
4305 },
4306 ));
4307 let mut binder = Binder::new();
4308 assert!(binder.bind(&plan).is_ok());
4309 }
4310
4311 #[test]
4316 fn test_expand_with_path_alias_registers_auxiliary_variables() {
4317 use crate::query::plan::{ExpandDirection, ExpandOp, PathMode};
4318
4319 let plan = LogicalPlan::new(LogicalOperator::Expand(ExpandOp {
4320 from_variable: "a".to_string(),
4321 to_variable: "b".to_string(),
4322 edge_variable: None,
4323 direction: ExpandDirection::Outgoing,
4324 edge_types: vec!["ROAD".to_string()],
4325 min_hops: 1,
4326 max_hops: Some(5),
4327 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
4328 variable: "a".to_string(),
4329 label: None,
4330 input: None,
4331 })),
4332 path_alias: Some("p".to_string()),
4333 path_mode: PathMode::Walk,
4334 }));
4335 let mut binder = Binder::new();
4336 let ctx = binder.bind(&plan).unwrap();
4337 assert!(ctx.contains("p"));
4338 assert!(ctx.contains("_path_length_p"));
4339 assert!(ctx.contains("_path_nodes_p"));
4340 assert!(ctx.contains("_path_edges_p"));
4341 }
4342}