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