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 std::collections::HashMap;
18
19fn binding_error(message: impl Into<String>) -> Error {
21 Error::Query(QueryError::new(QueryErrorKind::Semantic, message))
22}
23
24fn binding_error_with_hint(message: impl Into<String>, hint: impl Into<String>) -> Error {
26 Error::Query(QueryError::new(QueryErrorKind::Semantic, message).with_hint(hint))
27}
28
29fn undefined_variable_error(variable: &str, context: &BindingContext, suffix: &str) -> Error {
31 let candidates: Vec<String> = context.variable_names().to_vec();
32 let candidates_ref: Vec<&str> = candidates.iter().map(|s| s.as_str()).collect();
33
34 if let Some(suggestion) = find_similar(variable, &candidates_ref) {
35 binding_error_with_hint(
36 format!("Undefined variable '{variable}'{suffix}"),
37 format_suggestion(suggestion),
38 )
39 } else {
40 binding_error(format!("Undefined variable '{variable}'{suffix}"))
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct VariableInfo {
47 pub name: String,
49 pub data_type: LogicalType,
51 pub is_node: bool,
53 pub is_edge: bool,
55}
56
57#[derive(Debug, Clone, Default)]
59pub struct BindingContext {
60 variables: HashMap<String, VariableInfo>,
62 order: Vec<String>,
64}
65
66impl BindingContext {
67 #[must_use]
69 pub fn new() -> Self {
70 Self {
71 variables: HashMap::new(),
72 order: Vec::new(),
73 }
74 }
75
76 pub fn add_variable(&mut self, name: String, info: VariableInfo) {
78 if !self.variables.contains_key(&name) {
79 self.order.push(name.clone());
80 }
81 self.variables.insert(name, info);
82 }
83
84 #[must_use]
86 pub fn get(&self, name: &str) -> Option<&VariableInfo> {
87 self.variables.get(name)
88 }
89
90 #[must_use]
92 pub fn contains(&self, name: &str) -> bool {
93 self.variables.contains_key(name)
94 }
95
96 #[must_use]
98 pub fn variable_names(&self) -> &[String] {
99 &self.order
100 }
101
102 #[must_use]
104 pub fn len(&self) -> usize {
105 self.variables.len()
106 }
107
108 #[must_use]
110 pub fn is_empty(&self) -> bool {
111 self.variables.is_empty()
112 }
113
114 pub fn remove_variable(&mut self, name: &str) {
116 self.variables.remove(name);
117 self.order.retain(|n| n != name);
118 }
119}
120
121pub struct Binder {
129 context: BindingContext,
131}
132
133impl Binder {
134 #[must_use]
136 pub fn new() -> Self {
137 Self {
138 context: BindingContext::new(),
139 }
140 }
141
142 pub fn bind(&mut self, plan: &LogicalPlan) -> Result<BindingContext> {
148 self.bind_operator(&plan.root)?;
149 Ok(self.context.clone())
150 }
151
152 fn bind_operator(&mut self, op: &LogicalOperator) -> Result<()> {
154 match op {
155 LogicalOperator::NodeScan(scan) => self.bind_node_scan(scan),
156 LogicalOperator::Expand(expand) => self.bind_expand(expand),
157 LogicalOperator::Filter(filter) => self.bind_filter(filter),
158 LogicalOperator::Return(ret) => self.bind_return(ret),
159 LogicalOperator::Project(project) => {
160 self.bind_operator(&project.input)?;
161 for projection in &project.projections {
162 self.validate_expression(&projection.expression)?;
163 if let Some(ref alias) = projection.alias {
165 let data_type = self.infer_expression_type(&projection.expression);
167 self.context.add_variable(
168 alias.clone(),
169 VariableInfo {
170 name: alias.clone(),
171 data_type,
172 is_node: false,
173 is_edge: false,
174 },
175 );
176 }
177 }
178 Ok(())
179 }
180 LogicalOperator::Limit(limit) => self.bind_operator(&limit.input),
181 LogicalOperator::Skip(skip) => self.bind_operator(&skip.input),
182 LogicalOperator::Sort(sort) => {
183 self.bind_operator(&sort.input)?;
184 for key in &sort.keys {
185 self.validate_expression(&key.expression)?;
186 }
187 Ok(())
188 }
189 LogicalOperator::CreateNode(create) => {
190 if let Some(ref input) = create.input {
192 self.bind_operator(input)?;
193 }
194 self.context.add_variable(
195 create.variable.clone(),
196 VariableInfo {
197 name: create.variable.clone(),
198 data_type: LogicalType::Node,
199 is_node: true,
200 is_edge: false,
201 },
202 );
203 for (_, expr) in &create.properties {
205 self.validate_expression(expr)?;
206 }
207 Ok(())
208 }
209 LogicalOperator::EdgeScan(scan) => {
210 if let Some(ref input) = scan.input {
211 self.bind_operator(input)?;
212 }
213 self.context.add_variable(
214 scan.variable.clone(),
215 VariableInfo {
216 name: scan.variable.clone(),
217 data_type: LogicalType::Edge,
218 is_node: false,
219 is_edge: true,
220 },
221 );
222 Ok(())
223 }
224 LogicalOperator::Distinct(distinct) => self.bind_operator(&distinct.input),
225 LogicalOperator::Join(join) => self.bind_join(join),
226 LogicalOperator::Aggregate(agg) => self.bind_aggregate(agg),
227 LogicalOperator::CreateEdge(create) => {
228 self.bind_operator(&create.input)?;
229 if !self.context.contains(&create.from_variable) {
231 return Err(undefined_variable_error(
232 &create.from_variable,
233 &self.context,
234 " (source in CREATE EDGE)",
235 ));
236 }
237 if !self.context.contains(&create.to_variable) {
238 return Err(undefined_variable_error(
239 &create.to_variable,
240 &self.context,
241 " (target in CREATE EDGE)",
242 ));
243 }
244 if let Some(ref var) = create.variable {
246 self.context.add_variable(
247 var.clone(),
248 VariableInfo {
249 name: var.clone(),
250 data_type: LogicalType::Edge,
251 is_node: false,
252 is_edge: true,
253 },
254 );
255 }
256 for (_, expr) in &create.properties {
258 self.validate_expression(expr)?;
259 }
260 Ok(())
261 }
262 LogicalOperator::DeleteNode(delete) => {
263 self.bind_operator(&delete.input)?;
264 if !self.context.contains(&delete.variable) {
266 return Err(undefined_variable_error(
267 &delete.variable,
268 &self.context,
269 " in DELETE",
270 ));
271 }
272 Ok(())
273 }
274 LogicalOperator::DeleteEdge(delete) => {
275 self.bind_operator(&delete.input)?;
276 if !self.context.contains(&delete.variable) {
278 return Err(undefined_variable_error(
279 &delete.variable,
280 &self.context,
281 " in DELETE",
282 ));
283 }
284 Ok(())
285 }
286 LogicalOperator::SetProperty(set) => {
287 self.bind_operator(&set.input)?;
288 if !self.context.contains(&set.variable) {
290 return Err(undefined_variable_error(
291 &set.variable,
292 &self.context,
293 " in SET",
294 ));
295 }
296 for (_, expr) in &set.properties {
298 self.validate_expression(expr)?;
299 }
300 Ok(())
301 }
302 LogicalOperator::Empty => Ok(()),
303
304 LogicalOperator::Unwind(unwind) => {
305 self.bind_operator(&unwind.input)?;
307 self.validate_expression(&unwind.expression)?;
309 self.context.add_variable(
311 unwind.variable.clone(),
312 VariableInfo {
313 name: unwind.variable.clone(),
314 data_type: LogicalType::Any, is_node: false,
316 is_edge: false,
317 },
318 );
319 if let Some(ref ord_var) = unwind.ordinality_var {
321 self.context.add_variable(
322 ord_var.clone(),
323 VariableInfo {
324 name: ord_var.clone(),
325 data_type: LogicalType::Int64,
326 is_node: false,
327 is_edge: false,
328 },
329 );
330 }
331 if let Some(ref off_var) = unwind.offset_var {
333 self.context.add_variable(
334 off_var.clone(),
335 VariableInfo {
336 name: off_var.clone(),
337 data_type: LogicalType::Int64,
338 is_node: false,
339 is_edge: false,
340 },
341 );
342 }
343 Ok(())
344 }
345
346 LogicalOperator::TripleScan(scan) => self.bind_triple_scan(scan),
348 LogicalOperator::Union(union) => {
349 for input in &union.inputs {
350 self.bind_operator(input)?;
351 }
352 Ok(())
353 }
354 LogicalOperator::LeftJoin(lj) => {
355 self.bind_operator(&lj.left)?;
356 self.bind_operator(&lj.right)?;
357 if let Some(ref cond) = lj.condition {
358 self.validate_expression(cond)?;
359 }
360 Ok(())
361 }
362 LogicalOperator::AntiJoin(aj) => {
363 self.bind_operator(&aj.left)?;
364 self.bind_operator(&aj.right)?;
365 Ok(())
366 }
367 LogicalOperator::Bind(bind) => {
368 self.bind_operator(&bind.input)?;
369 self.validate_expression(&bind.expression)?;
370 self.context.add_variable(
371 bind.variable.clone(),
372 VariableInfo {
373 name: bind.variable.clone(),
374 data_type: LogicalType::Any,
375 is_node: false,
376 is_edge: false,
377 },
378 );
379 Ok(())
380 }
381 LogicalOperator::Merge(merge) => {
382 self.bind_operator(&merge.input)?;
384 for (_, expr) in &merge.match_properties {
386 self.validate_expression(expr)?;
387 }
388 for (_, expr) in &merge.on_create {
390 self.validate_expression(expr)?;
391 }
392 for (_, expr) in &merge.on_match {
394 self.validate_expression(expr)?;
395 }
396 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 Ok(())
407 }
408 LogicalOperator::MergeRelationship(merge_rel) => {
409 self.bind_operator(&merge_rel.input)?;
410 if !self.context.contains(&merge_rel.source_variable) {
412 return Err(undefined_variable_error(
413 &merge_rel.source_variable,
414 &self.context,
415 " in MERGE relationship source",
416 ));
417 }
418 if !self.context.contains(&merge_rel.target_variable) {
419 return Err(undefined_variable_error(
420 &merge_rel.target_variable,
421 &self.context,
422 " in MERGE relationship target",
423 ));
424 }
425 for (_, expr) in &merge_rel.match_properties {
426 self.validate_expression(expr)?;
427 }
428 for (_, expr) in &merge_rel.on_create {
429 self.validate_expression(expr)?;
430 }
431 for (_, expr) in &merge_rel.on_match {
432 self.validate_expression(expr)?;
433 }
434 self.context.add_variable(
436 merge_rel.variable.clone(),
437 VariableInfo {
438 name: merge_rel.variable.clone(),
439 data_type: LogicalType::Edge,
440 is_node: false,
441 is_edge: true,
442 },
443 );
444 Ok(())
445 }
446 LogicalOperator::AddLabel(add_label) => {
447 self.bind_operator(&add_label.input)?;
448 if !self.context.contains(&add_label.variable) {
450 return Err(undefined_variable_error(
451 &add_label.variable,
452 &self.context,
453 " in SET labels",
454 ));
455 }
456 Ok(())
457 }
458 LogicalOperator::RemoveLabel(remove_label) => {
459 self.bind_operator(&remove_label.input)?;
460 if !self.context.contains(&remove_label.variable) {
462 return Err(undefined_variable_error(
463 &remove_label.variable,
464 &self.context,
465 " in REMOVE labels",
466 ));
467 }
468 Ok(())
469 }
470 LogicalOperator::ShortestPath(sp) => {
471 self.bind_operator(&sp.input)?;
473 if !self.context.contains(&sp.source_var) {
475 return Err(undefined_variable_error(
476 &sp.source_var,
477 &self.context,
478 " (source in shortestPath)",
479 ));
480 }
481 if !self.context.contains(&sp.target_var) {
482 return Err(undefined_variable_error(
483 &sp.target_var,
484 &self.context,
485 " (target in shortestPath)",
486 ));
487 }
488 self.context.add_variable(
490 sp.path_alias.clone(),
491 VariableInfo {
492 name: sp.path_alias.clone(),
493 data_type: LogicalType::Any, is_node: false,
495 is_edge: false,
496 },
497 );
498 let path_length_var = format!("_path_length_{}", sp.path_alias);
500 self.context.add_variable(
501 path_length_var.clone(),
502 VariableInfo {
503 name: path_length_var,
504 data_type: LogicalType::Int64,
505 is_node: false,
506 is_edge: false,
507 },
508 );
509 Ok(())
510 }
511 LogicalOperator::InsertTriple(insert) => {
513 if let Some(ref input) = insert.input {
514 self.bind_operator(input)?;
515 }
516 Ok(())
517 }
518 LogicalOperator::DeleteTriple(delete) => {
519 if let Some(ref input) = delete.input {
520 self.bind_operator(input)?;
521 }
522 Ok(())
523 }
524 LogicalOperator::Modify(modify) => {
525 self.bind_operator(&modify.where_clause)?;
526 Ok(())
527 }
528 LogicalOperator::ClearGraph(_)
529 | LogicalOperator::CreateGraph(_)
530 | LogicalOperator::DropGraph(_)
531 | LogicalOperator::LoadGraph(_)
532 | LogicalOperator::CopyGraph(_)
533 | LogicalOperator::MoveGraph(_)
534 | LogicalOperator::AddGraph(_) => Ok(()),
535 LogicalOperator::VectorScan(scan) => {
536 if let Some(ref input) = scan.input {
538 self.bind_operator(input)?;
539 }
540 self.context.add_variable(
541 scan.variable.clone(),
542 VariableInfo {
543 name: scan.variable.clone(),
544 data_type: LogicalType::Node,
545 is_node: true,
546 is_edge: false,
547 },
548 );
549 self.validate_expression(&scan.query_vector)?;
551 Ok(())
552 }
553 LogicalOperator::VectorJoin(join) => {
554 self.bind_operator(&join.input)?;
556 self.context.add_variable(
558 join.right_variable.clone(),
559 VariableInfo {
560 name: join.right_variable.clone(),
561 data_type: LogicalType::Node,
562 is_node: true,
563 is_edge: false,
564 },
565 );
566 if let Some(ref score_var) = join.score_variable {
568 self.context.add_variable(
569 score_var.clone(),
570 VariableInfo {
571 name: score_var.clone(),
572 data_type: LogicalType::Float64,
573 is_node: false,
574 is_edge: false,
575 },
576 );
577 }
578 self.validate_expression(&join.query_vector)?;
580 Ok(())
581 }
582 LogicalOperator::MapCollect(mc) => {
583 self.bind_operator(&mc.input)?;
584 self.context.add_variable(
585 mc.alias.clone(),
586 VariableInfo {
587 name: mc.alias.clone(),
588 data_type: LogicalType::Any,
589 is_node: false,
590 is_edge: false,
591 },
592 );
593 Ok(())
594 }
595 LogicalOperator::Except(except) => {
596 self.bind_operator(&except.left)?;
597 self.bind_operator(&except.right)?;
598 Ok(())
599 }
600 LogicalOperator::Intersect(intersect) => {
601 self.bind_operator(&intersect.left)?;
602 self.bind_operator(&intersect.right)?;
603 Ok(())
604 }
605 LogicalOperator::Otherwise(otherwise) => {
606 self.bind_operator(&otherwise.left)?;
607 self.bind_operator(&otherwise.right)?;
608 Ok(())
609 }
610 LogicalOperator::Apply(apply) => {
611 self.bind_operator(&apply.input)?;
612 self.bind_operator(&apply.subplan)?;
613 Ok(())
614 }
615 LogicalOperator::CreatePropertyGraph(_) => Ok(()),
617 LogicalOperator::CallProcedure(call) => {
619 if let Some(yields) = &call.yield_items {
620 for item in yields {
621 let var_name = item.alias.as_deref().unwrap_or(&item.field_name);
622 self.context.add_variable(
623 var_name.to_string(),
624 VariableInfo {
625 name: var_name.to_string(),
626 data_type: LogicalType::Any,
627 is_node: false,
628 is_edge: false,
629 },
630 );
631 }
632 }
633 Ok(())
634 }
635 }
636 }
637
638 fn bind_triple_scan(&mut self, scan: &TripleScanOp) -> Result<()> {
640 use crate::query::plan::TripleComponent;
641
642 if let Some(ref input) = scan.input {
644 self.bind_operator(input)?;
645 }
646
647 if let TripleComponent::Variable(name) = &scan.subject
649 && !self.context.contains(name)
650 {
651 self.context.add_variable(
652 name.clone(),
653 VariableInfo {
654 name: name.clone(),
655 data_type: LogicalType::Any, is_node: false,
657 is_edge: false,
658 },
659 );
660 }
661
662 if let TripleComponent::Variable(name) = &scan.predicate
663 && !self.context.contains(name)
664 {
665 self.context.add_variable(
666 name.clone(),
667 VariableInfo {
668 name: name.clone(),
669 data_type: LogicalType::Any, is_node: false,
671 is_edge: false,
672 },
673 );
674 }
675
676 if let TripleComponent::Variable(name) = &scan.object
677 && !self.context.contains(name)
678 {
679 self.context.add_variable(
680 name.clone(),
681 VariableInfo {
682 name: name.clone(),
683 data_type: LogicalType::Any, is_node: false,
685 is_edge: false,
686 },
687 );
688 }
689
690 if let Some(TripleComponent::Variable(name)) = &scan.graph
691 && !self.context.contains(name)
692 {
693 self.context.add_variable(
694 name.clone(),
695 VariableInfo {
696 name: name.clone(),
697 data_type: LogicalType::Any, is_node: false,
699 is_edge: false,
700 },
701 );
702 }
703
704 Ok(())
705 }
706
707 fn bind_node_scan(&mut self, scan: &NodeScanOp) -> Result<()> {
709 if let Some(ref input) = scan.input {
711 self.bind_operator(input)?;
712 }
713
714 self.context.add_variable(
716 scan.variable.clone(),
717 VariableInfo {
718 name: scan.variable.clone(),
719 data_type: LogicalType::Node,
720 is_node: true,
721 is_edge: false,
722 },
723 );
724
725 Ok(())
726 }
727
728 fn bind_expand(&mut self, expand: &ExpandOp) -> Result<()> {
730 self.bind_operator(&expand.input)?;
732
733 if !self.context.contains(&expand.from_variable) {
735 return Err(undefined_variable_error(
736 &expand.from_variable,
737 &self.context,
738 " in EXPAND",
739 ));
740 }
741
742 if let Some(info) = self.context.get(&expand.from_variable)
744 && !info.is_node
745 {
746 return Err(binding_error(format!(
747 "Variable '{}' is not a node, cannot expand from it",
748 expand.from_variable
749 )));
750 }
751
752 if let Some(ref edge_var) = expand.edge_variable {
754 self.context.add_variable(
755 edge_var.clone(),
756 VariableInfo {
757 name: edge_var.clone(),
758 data_type: LogicalType::Edge,
759 is_node: false,
760 is_edge: true,
761 },
762 );
763 }
764
765 self.context.add_variable(
767 expand.to_variable.clone(),
768 VariableInfo {
769 name: expand.to_variable.clone(),
770 data_type: LogicalType::Node,
771 is_node: true,
772 is_edge: false,
773 },
774 );
775
776 if let Some(ref path_alias) = expand.path_alias {
778 self.context.add_variable(
780 path_alias.clone(),
781 VariableInfo {
782 name: path_alias.clone(),
783 data_type: LogicalType::Any,
784 is_node: false,
785 is_edge: false,
786 },
787 );
788 let path_length_var = format!("_path_length_{}", path_alias);
790 self.context.add_variable(
791 path_length_var.clone(),
792 VariableInfo {
793 name: path_length_var,
794 data_type: LogicalType::Int64,
795 is_node: false,
796 is_edge: false,
797 },
798 );
799 let path_nodes_var = format!("_path_nodes_{}", path_alias);
801 self.context.add_variable(
802 path_nodes_var.clone(),
803 VariableInfo {
804 name: path_nodes_var,
805 data_type: LogicalType::Any,
806 is_node: false,
807 is_edge: false,
808 },
809 );
810 let path_edges_var = format!("_path_edges_{}", path_alias);
812 self.context.add_variable(
813 path_edges_var.clone(),
814 VariableInfo {
815 name: path_edges_var,
816 data_type: LogicalType::Any,
817 is_node: false,
818 is_edge: false,
819 },
820 );
821 }
822
823 Ok(())
824 }
825
826 fn bind_filter(&mut self, filter: &FilterOp) -> Result<()> {
828 self.bind_operator(&filter.input)?;
830
831 self.validate_expression(&filter.predicate)?;
833
834 Ok(())
835 }
836
837 fn bind_return(&mut self, ret: &ReturnOp) -> Result<()> {
839 self.bind_operator(&ret.input)?;
841
842 for item in &ret.items {
845 self.validate_return_item(item)?;
846 if let Some(ref alias) = item.alias {
847 let data_type = self.infer_expression_type(&item.expression);
848 self.context.add_variable(
849 alias.clone(),
850 VariableInfo {
851 name: alias.clone(),
852 data_type,
853 is_node: false,
854 is_edge: false,
855 },
856 );
857 }
858 }
859
860 Ok(())
861 }
862
863 fn validate_return_item(&mut self, item: &ReturnItem) -> Result<()> {
865 self.validate_expression(&item.expression)
866 }
867
868 fn validate_expression(&mut self, expr: &LogicalExpression) -> Result<()> {
870 match expr {
871 LogicalExpression::Variable(name) => {
872 if name == "*" {
874 return Ok(());
875 }
876 if !self.context.contains(name) && !name.starts_with("_anon_") {
877 return Err(undefined_variable_error(name, &self.context, ""));
878 }
879 Ok(())
880 }
881 LogicalExpression::Property { variable, .. } => {
882 if !self.context.contains(variable) && !variable.starts_with("_anon_") {
883 return Err(undefined_variable_error(
884 variable,
885 &self.context,
886 " in property access",
887 ));
888 }
889 Ok(())
890 }
891 LogicalExpression::Literal(_) => Ok(()),
892 LogicalExpression::Binary { left, right, .. } => {
893 self.validate_expression(left)?;
894 self.validate_expression(right)
895 }
896 LogicalExpression::Unary { operand, .. } => self.validate_expression(operand),
897 LogicalExpression::FunctionCall { args, .. } => {
898 for arg in args {
899 self.validate_expression(arg)?;
900 }
901 Ok(())
902 }
903 LogicalExpression::List(items) => {
904 for item in items {
905 self.validate_expression(item)?;
906 }
907 Ok(())
908 }
909 LogicalExpression::Map(pairs) => {
910 for (_, value) in pairs {
911 self.validate_expression(value)?;
912 }
913 Ok(())
914 }
915 LogicalExpression::IndexAccess { base, index } => {
916 self.validate_expression(base)?;
917 self.validate_expression(index)
918 }
919 LogicalExpression::SliceAccess { base, start, end } => {
920 self.validate_expression(base)?;
921 if let Some(s) = start {
922 self.validate_expression(s)?;
923 }
924 if let Some(e) = end {
925 self.validate_expression(e)?;
926 }
927 Ok(())
928 }
929 LogicalExpression::Case {
930 operand,
931 when_clauses,
932 else_clause,
933 } => {
934 if let Some(op) = operand {
935 self.validate_expression(op)?;
936 }
937 for (cond, result) in when_clauses {
938 self.validate_expression(cond)?;
939 self.validate_expression(result)?;
940 }
941 if let Some(else_expr) = else_clause {
942 self.validate_expression(else_expr)?;
943 }
944 Ok(())
945 }
946 LogicalExpression::Parameter(_) => Ok(()),
948 LogicalExpression::Labels(var)
950 | LogicalExpression::Type(var)
951 | LogicalExpression::Id(var) => {
952 if !self.context.contains(var) && !var.starts_with("_anon_") {
953 return Err(undefined_variable_error(var, &self.context, " in function"));
954 }
955 Ok(())
956 }
957 LogicalExpression::ListComprehension { list_expr, .. } => {
958 self.validate_expression(list_expr)?;
962 Ok(())
963 }
964 LogicalExpression::ListPredicate { list_expr, .. } => {
965 self.validate_expression(list_expr)?;
969 Ok(())
970 }
971 LogicalExpression::ExistsSubquery(subquery)
972 | LogicalExpression::CountSubquery(subquery) => {
973 let _ = subquery; Ok(())
977 }
978 LogicalExpression::PatternComprehension { projection, .. } => {
979 self.validate_expression(projection)
981 }
982 LogicalExpression::MapProjection { base, entries } => {
983 if !self.context.contains(base) && !base.starts_with("_anon_") {
984 return Err(undefined_variable_error(
985 base,
986 &self.context,
987 " in map projection",
988 ));
989 }
990 for entry in entries {
991 if let crate::query::plan::MapProjectionEntry::LiteralEntry(_, expr) = entry {
992 self.validate_expression(expr)?;
993 }
994 }
995 Ok(())
996 }
997 LogicalExpression::Reduce {
998 accumulator,
999 initial,
1000 variable,
1001 list,
1002 expression,
1003 } => {
1004 self.validate_expression(initial)?;
1005 self.validate_expression(list)?;
1006 let had_acc = self.context.contains(accumulator);
1009 let had_var = self.context.contains(variable);
1010 if !had_acc {
1011 self.context.add_variable(
1012 accumulator.clone(),
1013 VariableInfo {
1014 name: accumulator.clone(),
1015 data_type: LogicalType::Any,
1016 is_node: false,
1017 is_edge: false,
1018 },
1019 );
1020 }
1021 if !had_var {
1022 self.context.add_variable(
1023 variable.clone(),
1024 VariableInfo {
1025 name: variable.clone(),
1026 data_type: LogicalType::Any,
1027 is_node: false,
1028 is_edge: false,
1029 },
1030 );
1031 }
1032 self.validate_expression(expression)?;
1033 if !had_acc {
1034 self.context.remove_variable(accumulator);
1035 }
1036 if !had_var {
1037 self.context.remove_variable(variable);
1038 }
1039 Ok(())
1040 }
1041 }
1042 }
1043
1044 fn infer_expression_type(&self, expr: &LogicalExpression) -> LogicalType {
1046 match expr {
1047 LogicalExpression::Variable(name) => {
1048 self.context
1050 .get(name)
1051 .map_or(LogicalType::Any, |info| info.data_type.clone())
1052 }
1053 LogicalExpression::Property { .. } => LogicalType::Any, LogicalExpression::Literal(value) => {
1055 use grafeo_common::types::Value;
1057 match value {
1058 Value::Bool(_) => LogicalType::Bool,
1059 Value::Int64(_) => LogicalType::Int64,
1060 Value::Float64(_) => LogicalType::Float64,
1061 Value::String(_) => LogicalType::String,
1062 Value::List(_) => LogicalType::Any, Value::Map(_) => LogicalType::Any, Value::Null => LogicalType::Any,
1065 _ => LogicalType::Any,
1066 }
1067 }
1068 LogicalExpression::Binary { .. } => LogicalType::Any, LogicalExpression::Unary { .. } => LogicalType::Any,
1070 LogicalExpression::FunctionCall { name, .. } => {
1071 match name.to_lowercase().as_str() {
1073 "count" | "sum" | "id" => LogicalType::Int64,
1074 "avg" => LogicalType::Float64,
1075 "type" => LogicalType::String,
1076 "labels" | "collect" => LogicalType::Any,
1078 _ => LogicalType::Any,
1079 }
1080 }
1081 LogicalExpression::List(_) => LogicalType::Any, LogicalExpression::Map(_) => LogicalType::Any, _ => LogicalType::Any,
1084 }
1085 }
1086
1087 fn bind_join(&mut self, join: &crate::query::plan::JoinOp) -> Result<()> {
1089 self.bind_operator(&join.left)?;
1091 self.bind_operator(&join.right)?;
1092
1093 for condition in &join.conditions {
1095 self.validate_expression(&condition.left)?;
1096 self.validate_expression(&condition.right)?;
1097 }
1098
1099 Ok(())
1100 }
1101
1102 fn bind_aggregate(&mut self, agg: &crate::query::plan::AggregateOp) -> Result<()> {
1104 self.bind_operator(&agg.input)?;
1106
1107 for expr in &agg.group_by {
1109 self.validate_expression(expr)?;
1110 }
1111
1112 for agg_expr in &agg.aggregates {
1114 if let Some(ref expr) = agg_expr.expression {
1115 self.validate_expression(expr)?;
1116 }
1117 if let Some(ref alias) = agg_expr.alias {
1119 self.context.add_variable(
1120 alias.clone(),
1121 VariableInfo {
1122 name: alias.clone(),
1123 data_type: LogicalType::Any,
1124 is_node: false,
1125 is_edge: false,
1126 },
1127 );
1128 }
1129 }
1130
1131 Ok(())
1132 }
1133}
1134
1135impl Default for Binder {
1136 fn default() -> Self {
1137 Self::new()
1138 }
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143 use super::*;
1144 use crate::query::plan::{BinaryOp, FilterOp};
1145
1146 #[test]
1147 fn test_bind_simple_scan() {
1148 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1149 items: vec![ReturnItem {
1150 expression: LogicalExpression::Variable("n".to_string()),
1151 alias: None,
1152 }],
1153 distinct: false,
1154 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1155 variable: "n".to_string(),
1156 label: Some("Person".to_string()),
1157 input: None,
1158 })),
1159 }));
1160
1161 let mut binder = Binder::new();
1162 let result = binder.bind(&plan);
1163
1164 assert!(result.is_ok());
1165 let ctx = result.unwrap();
1166 assert!(ctx.contains("n"));
1167 assert!(ctx.get("n").unwrap().is_node);
1168 }
1169
1170 #[test]
1171 fn test_bind_undefined_variable() {
1172 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1173 items: vec![ReturnItem {
1174 expression: LogicalExpression::Variable("undefined".to_string()),
1175 alias: None,
1176 }],
1177 distinct: false,
1178 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1179 variable: "n".to_string(),
1180 label: None,
1181 input: None,
1182 })),
1183 }));
1184
1185 let mut binder = Binder::new();
1186 let result = binder.bind(&plan);
1187
1188 assert!(result.is_err());
1189 let err = result.unwrap_err();
1190 assert!(err.to_string().contains("Undefined variable"));
1191 }
1192
1193 #[test]
1194 fn test_bind_property_access() {
1195 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1196 items: vec![ReturnItem {
1197 expression: LogicalExpression::Property {
1198 variable: "n".to_string(),
1199 property: "name".to_string(),
1200 },
1201 alias: None,
1202 }],
1203 distinct: false,
1204 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1205 variable: "n".to_string(),
1206 label: Some("Person".to_string()),
1207 input: None,
1208 })),
1209 }));
1210
1211 let mut binder = Binder::new();
1212 let result = binder.bind(&plan);
1213
1214 assert!(result.is_ok());
1215 }
1216
1217 #[test]
1218 fn test_bind_filter_with_undefined_variable() {
1219 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1220 items: vec![ReturnItem {
1221 expression: LogicalExpression::Variable("n".to_string()),
1222 alias: None,
1223 }],
1224 distinct: false,
1225 input: Box::new(LogicalOperator::Filter(FilterOp {
1226 predicate: LogicalExpression::Binary {
1227 left: Box::new(LogicalExpression::Property {
1228 variable: "m".to_string(), property: "age".to_string(),
1230 }),
1231 op: BinaryOp::Gt,
1232 right: Box::new(LogicalExpression::Literal(
1233 grafeo_common::types::Value::Int64(30),
1234 )),
1235 },
1236 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1237 variable: "n".to_string(),
1238 label: None,
1239 input: None,
1240 })),
1241 })),
1242 }));
1243
1244 let mut binder = Binder::new();
1245 let result = binder.bind(&plan);
1246
1247 assert!(result.is_err());
1248 let err = result.unwrap_err();
1249 assert!(err.to_string().contains("Undefined variable 'm'"));
1250 }
1251
1252 #[test]
1253 fn test_bind_expand() {
1254 use crate::query::plan::{ExpandDirection, ExpandOp, PathMode};
1255
1256 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1257 items: vec![
1258 ReturnItem {
1259 expression: LogicalExpression::Variable("a".to_string()),
1260 alias: None,
1261 },
1262 ReturnItem {
1263 expression: LogicalExpression::Variable("b".to_string()),
1264 alias: None,
1265 },
1266 ],
1267 distinct: false,
1268 input: Box::new(LogicalOperator::Expand(ExpandOp {
1269 from_variable: "a".to_string(),
1270 to_variable: "b".to_string(),
1271 edge_variable: Some("e".to_string()),
1272 direction: ExpandDirection::Outgoing,
1273 edge_types: vec!["KNOWS".to_string()],
1274 min_hops: 1,
1275 max_hops: Some(1),
1276 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1277 variable: "a".to_string(),
1278 label: Some("Person".to_string()),
1279 input: None,
1280 })),
1281 path_alias: None,
1282 path_mode: PathMode::Walk,
1283 })),
1284 }));
1285
1286 let mut binder = Binder::new();
1287 let result = binder.bind(&plan);
1288
1289 assert!(result.is_ok());
1290 let ctx = result.unwrap();
1291 assert!(ctx.contains("a"));
1292 assert!(ctx.contains("b"));
1293 assert!(ctx.contains("e"));
1294 assert!(ctx.get("a").unwrap().is_node);
1295 assert!(ctx.get("b").unwrap().is_node);
1296 assert!(ctx.get("e").unwrap().is_edge);
1297 }
1298
1299 #[test]
1300 fn test_bind_expand_from_undefined_variable() {
1301 use crate::query::plan::{ExpandDirection, ExpandOp, PathMode};
1303
1304 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1305 items: vec![ReturnItem {
1306 expression: LogicalExpression::Variable("b".to_string()),
1307 alias: None,
1308 }],
1309 distinct: false,
1310 input: Box::new(LogicalOperator::Expand(ExpandOp {
1311 from_variable: "undefined".to_string(), to_variable: "b".to_string(),
1313 edge_variable: None,
1314 direction: ExpandDirection::Outgoing,
1315 edge_types: vec![],
1316 min_hops: 1,
1317 max_hops: Some(1),
1318 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1319 variable: "a".to_string(),
1320 label: None,
1321 input: None,
1322 })),
1323 path_alias: None,
1324 path_mode: PathMode::Walk,
1325 })),
1326 }));
1327
1328 let mut binder = Binder::new();
1329 let result = binder.bind(&plan);
1330
1331 assert!(result.is_err());
1332 let err = result.unwrap_err();
1333 assert!(
1334 err.to_string().contains("Undefined variable 'undefined'"),
1335 "Expected error about undefined variable, got: {}",
1336 err
1337 );
1338 }
1339
1340 #[test]
1341 fn test_bind_return_with_aggregate_and_non_aggregate() {
1342 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1344 items: vec![
1345 ReturnItem {
1346 expression: LogicalExpression::FunctionCall {
1347 name: "count".to_string(),
1348 args: vec![LogicalExpression::Variable("n".to_string())],
1349 distinct: false,
1350 },
1351 alias: Some("cnt".to_string()),
1352 },
1353 ReturnItem {
1354 expression: LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
1355 alias: Some("one".to_string()),
1356 },
1357 ],
1358 distinct: false,
1359 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1360 variable: "n".to_string(),
1361 label: Some("Person".to_string()),
1362 input: None,
1363 })),
1364 }));
1365
1366 let mut binder = Binder::new();
1367 let result = binder.bind(&plan);
1368
1369 assert!(result.is_ok());
1371 }
1372
1373 #[test]
1374 fn test_bind_nested_property_access() {
1375 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1377 items: vec![
1378 ReturnItem {
1379 expression: LogicalExpression::Property {
1380 variable: "n".to_string(),
1381 property: "name".to_string(),
1382 },
1383 alias: None,
1384 },
1385 ReturnItem {
1386 expression: LogicalExpression::Property {
1387 variable: "n".to_string(),
1388 property: "age".to_string(),
1389 },
1390 alias: None,
1391 },
1392 ],
1393 distinct: false,
1394 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1395 variable: "n".to_string(),
1396 label: Some("Person".to_string()),
1397 input: None,
1398 })),
1399 }));
1400
1401 let mut binder = Binder::new();
1402 let result = binder.bind(&plan);
1403
1404 assert!(result.is_ok());
1405 }
1406
1407 #[test]
1408 fn test_bind_binary_expression_with_undefined() {
1409 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1411 items: vec![ReturnItem {
1412 expression: LogicalExpression::Binary {
1413 left: Box::new(LogicalExpression::Property {
1414 variable: "n".to_string(),
1415 property: "age".to_string(),
1416 }),
1417 op: BinaryOp::Add,
1418 right: Box::new(LogicalExpression::Property {
1419 variable: "m".to_string(), property: "age".to_string(),
1421 }),
1422 },
1423 alias: Some("total".to_string()),
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 assert!(
1438 result
1439 .unwrap_err()
1440 .to_string()
1441 .contains("Undefined variable 'm'")
1442 );
1443 }
1444
1445 #[test]
1446 fn test_bind_duplicate_variable_definition() {
1447 use crate::query::plan::{JoinOp, JoinType};
1450
1451 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1452 items: vec![ReturnItem {
1453 expression: LogicalExpression::Variable("n".to_string()),
1454 alias: None,
1455 }],
1456 distinct: false,
1457 input: Box::new(LogicalOperator::Join(JoinOp {
1458 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1459 variable: "n".to_string(),
1460 label: Some("A".to_string()),
1461 input: None,
1462 })),
1463 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1464 variable: "m".to_string(), label: Some("B".to_string()),
1466 input: None,
1467 })),
1468 join_type: JoinType::Inner,
1469 conditions: vec![],
1470 })),
1471 }));
1472
1473 let mut binder = Binder::new();
1474 let result = binder.bind(&plan);
1475
1476 assert!(result.is_ok());
1478 let ctx = result.unwrap();
1479 assert!(ctx.contains("n"));
1480 assert!(ctx.contains("m"));
1481 }
1482
1483 #[test]
1484 fn test_bind_function_with_wrong_arity() {
1485 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1488 items: vec![ReturnItem {
1489 expression: LogicalExpression::FunctionCall {
1490 name: "count".to_string(),
1491 args: vec![], distinct: false,
1493 },
1494 alias: None,
1495 }],
1496 distinct: false,
1497 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1498 variable: "n".to_string(),
1499 label: None,
1500 input: None,
1501 })),
1502 }));
1503
1504 let mut binder = Binder::new();
1505 let result = binder.bind(&plan);
1506
1507 let _ = result; }
1512
1513 #[test]
1516 fn test_create_edge_rejects_undefined_source() {
1517 use crate::query::plan::CreateEdgeOp;
1518
1519 let plan = LogicalPlan::new(LogicalOperator::CreateEdge(CreateEdgeOp {
1520 variable: Some("e".to_string()),
1521 from_variable: "ghost".to_string(), to_variable: "b".to_string(),
1523 edge_type: "KNOWS".to_string(),
1524 properties: vec![],
1525 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1526 variable: "b".to_string(),
1527 label: None,
1528 input: None,
1529 })),
1530 }));
1531
1532 let mut binder = Binder::new();
1533 let err = binder.bind(&plan).unwrap_err();
1534 assert!(
1535 err.to_string().contains("Undefined variable 'ghost'"),
1536 "Should reject undefined source variable, got: {err}"
1537 );
1538 }
1539
1540 #[test]
1541 fn test_create_edge_rejects_undefined_target() {
1542 use crate::query::plan::CreateEdgeOp;
1543
1544 let plan = LogicalPlan::new(LogicalOperator::CreateEdge(CreateEdgeOp {
1545 variable: None,
1546 from_variable: "a".to_string(),
1547 to_variable: "missing".to_string(), edge_type: "KNOWS".to_string(),
1549 properties: vec![],
1550 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1551 variable: "a".to_string(),
1552 label: None,
1553 input: None,
1554 })),
1555 }));
1556
1557 let mut binder = Binder::new();
1558 let err = binder.bind(&plan).unwrap_err();
1559 assert!(
1560 err.to_string().contains("Undefined variable 'missing'"),
1561 "Should reject undefined target variable, got: {err}"
1562 );
1563 }
1564
1565 #[test]
1566 fn test_create_edge_validates_property_expressions() {
1567 use crate::query::plan::CreateEdgeOp;
1568
1569 let plan = LogicalPlan::new(LogicalOperator::CreateEdge(CreateEdgeOp {
1571 variable: Some("e".to_string()),
1572 from_variable: "a".to_string(),
1573 to_variable: "b".to_string(),
1574 edge_type: "KNOWS".to_string(),
1575 properties: vec![(
1576 "since".to_string(),
1577 LogicalExpression::Property {
1578 variable: "x".to_string(), property: "year".to_string(),
1580 },
1581 )],
1582 input: Box::new(LogicalOperator::Join(crate::query::plan::JoinOp {
1583 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1584 variable: "a".to_string(),
1585 label: None,
1586 input: None,
1587 })),
1588 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1589 variable: "b".to_string(),
1590 label: None,
1591 input: None,
1592 })),
1593 join_type: crate::query::plan::JoinType::Inner,
1594 conditions: vec![],
1595 })),
1596 }));
1597
1598 let mut binder = Binder::new();
1599 let err = binder.bind(&plan).unwrap_err();
1600 assert!(err.to_string().contains("Undefined variable 'x'"));
1601 }
1602
1603 #[test]
1604 fn test_set_property_rejects_undefined_variable() {
1605 use crate::query::plan::SetPropertyOp;
1606
1607 let plan = LogicalPlan::new(LogicalOperator::SetProperty(SetPropertyOp {
1608 variable: "ghost".to_string(),
1609 properties: vec![(
1610 "name".to_string(),
1611 LogicalExpression::Literal(grafeo_common::types::Value::String("Alice".into())),
1612 )],
1613 replace: false,
1614 is_edge: false,
1615 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1616 variable: "n".to_string(),
1617 label: None,
1618 input: None,
1619 })),
1620 }));
1621
1622 let mut binder = Binder::new();
1623 let err = binder.bind(&plan).unwrap_err();
1624 assert!(
1625 err.to_string().contains("in SET"),
1626 "Error should indicate SET context, got: {err}"
1627 );
1628 }
1629
1630 #[test]
1631 fn test_delete_node_rejects_undefined_variable() {
1632 use crate::query::plan::DeleteNodeOp;
1633
1634 let plan = LogicalPlan::new(LogicalOperator::DeleteNode(DeleteNodeOp {
1635 variable: "phantom".to_string(),
1636 detach: false,
1637 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1638 variable: "n".to_string(),
1639 label: None,
1640 input: None,
1641 })),
1642 }));
1643
1644 let mut binder = Binder::new();
1645 let err = binder.bind(&plan).unwrap_err();
1646 assert!(err.to_string().contains("Undefined variable 'phantom'"));
1647 }
1648
1649 #[test]
1650 fn test_delete_edge_rejects_undefined_variable() {
1651 use crate::query::plan::DeleteEdgeOp;
1652
1653 let plan = LogicalPlan::new(LogicalOperator::DeleteEdge(DeleteEdgeOp {
1654 variable: "gone".to_string(),
1655 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1656 variable: "n".to_string(),
1657 label: None,
1658 input: None,
1659 })),
1660 }));
1661
1662 let mut binder = Binder::new();
1663 let err = binder.bind(&plan).unwrap_err();
1664 assert!(err.to_string().contains("Undefined variable 'gone'"));
1665 }
1666
1667 #[test]
1670 fn test_project_alias_becomes_available_downstream() {
1671 use crate::query::plan::{ProjectOp, Projection};
1672
1673 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1675 items: vec![ReturnItem {
1676 expression: LogicalExpression::Variable("person_name".to_string()),
1677 alias: None,
1678 }],
1679 distinct: false,
1680 input: Box::new(LogicalOperator::Project(ProjectOp {
1681 projections: vec![Projection {
1682 expression: LogicalExpression::Property {
1683 variable: "n".to_string(),
1684 property: "name".to_string(),
1685 },
1686 alias: Some("person_name".to_string()),
1687 }],
1688 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1689 variable: "n".to_string(),
1690 label: None,
1691 input: None,
1692 })),
1693 })),
1694 }));
1695
1696 let mut binder = Binder::new();
1697 let ctx = binder.bind(&plan).unwrap();
1698 assert!(
1699 ctx.contains("person_name"),
1700 "WITH alias should be available to RETURN"
1701 );
1702 }
1703
1704 #[test]
1705 fn test_project_rejects_undefined_expression() {
1706 use crate::query::plan::{ProjectOp, Projection};
1707
1708 let plan = LogicalPlan::new(LogicalOperator::Project(ProjectOp {
1709 projections: vec![Projection {
1710 expression: LogicalExpression::Variable("nope".to_string()),
1711 alias: Some("x".to_string()),
1712 }],
1713 input: Box::new(LogicalOperator::Empty),
1714 }));
1715
1716 let mut binder = Binder::new();
1717 let result = binder.bind(&plan);
1718 assert!(result.is_err(), "WITH on undefined variable should fail");
1719 }
1720
1721 #[test]
1724 fn test_unwind_adds_element_variable() {
1725 use crate::query::plan::UnwindOp;
1726
1727 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1728 items: vec![ReturnItem {
1729 expression: LogicalExpression::Variable("item".to_string()),
1730 alias: None,
1731 }],
1732 distinct: false,
1733 input: Box::new(LogicalOperator::Unwind(UnwindOp {
1734 expression: LogicalExpression::List(vec![
1735 LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
1736 LogicalExpression::Literal(grafeo_common::types::Value::Int64(2)),
1737 ]),
1738 variable: "item".to_string(),
1739 ordinality_var: None,
1740 offset_var: None,
1741 input: Box::new(LogicalOperator::Empty),
1742 })),
1743 }));
1744
1745 let mut binder = Binder::new();
1746 let ctx = binder.bind(&plan).unwrap();
1747 assert!(ctx.contains("item"), "UNWIND variable should be in scope");
1748 let info = ctx.get("item").unwrap();
1749 assert!(
1750 !info.is_node && !info.is_edge,
1751 "UNWIND variable is not a graph element"
1752 );
1753 }
1754
1755 #[test]
1758 fn test_merge_adds_variable_and_validates_properties() {
1759 use crate::query::plan::MergeOp;
1760
1761 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1762 items: vec![ReturnItem {
1763 expression: LogicalExpression::Variable("m".to_string()),
1764 alias: None,
1765 }],
1766 distinct: false,
1767 input: Box::new(LogicalOperator::Merge(MergeOp {
1768 variable: "m".to_string(),
1769 labels: vec!["Person".to_string()],
1770 match_properties: vec![(
1771 "name".to_string(),
1772 LogicalExpression::Literal(grafeo_common::types::Value::String("Alice".into())),
1773 )],
1774 on_create: vec![(
1775 "created".to_string(),
1776 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
1777 )],
1778 on_match: vec![(
1779 "updated".to_string(),
1780 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
1781 )],
1782 input: Box::new(LogicalOperator::Empty),
1783 })),
1784 }));
1785
1786 let mut binder = Binder::new();
1787 let ctx = binder.bind(&plan).unwrap();
1788 assert!(ctx.contains("m"));
1789 assert!(
1790 ctx.get("m").unwrap().is_node,
1791 "MERGE variable should be a node"
1792 );
1793 }
1794
1795 #[test]
1796 fn test_merge_rejects_undefined_in_on_create() {
1797 use crate::query::plan::MergeOp;
1798
1799 let plan = LogicalPlan::new(LogicalOperator::Merge(MergeOp {
1800 variable: "m".to_string(),
1801 labels: vec![],
1802 match_properties: vec![],
1803 on_create: vec![(
1804 "name".to_string(),
1805 LogicalExpression::Property {
1806 variable: "other".to_string(), property: "name".to_string(),
1808 },
1809 )],
1810 on_match: vec![],
1811 input: Box::new(LogicalOperator::Empty),
1812 }));
1813
1814 let mut binder = Binder::new();
1815 let result = binder.bind(&plan);
1816 assert!(
1817 result.is_err(),
1818 "ON CREATE referencing undefined variable should fail"
1819 );
1820 }
1821
1822 #[test]
1825 fn test_shortest_path_rejects_undefined_source() {
1826 use crate::query::plan::{ExpandDirection, ShortestPathOp};
1827
1828 let plan = LogicalPlan::new(LogicalOperator::ShortestPath(ShortestPathOp {
1829 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1830 variable: "b".to_string(),
1831 label: None,
1832 input: None,
1833 })),
1834 source_var: "missing".to_string(), target_var: "b".to_string(),
1836 edge_types: vec![],
1837 direction: ExpandDirection::Both,
1838 path_alias: "p".to_string(),
1839 all_paths: false,
1840 }));
1841
1842 let mut binder = Binder::new();
1843 let err = binder.bind(&plan).unwrap_err();
1844 assert!(
1845 err.to_string().contains("source in shortestPath"),
1846 "Error should mention shortestPath source context, got: {err}"
1847 );
1848 }
1849
1850 #[test]
1851 fn test_shortest_path_adds_path_and_length_variables() {
1852 use crate::query::plan::{ExpandDirection, JoinOp, JoinType, ShortestPathOp};
1853
1854 let plan = LogicalPlan::new(LogicalOperator::ShortestPath(ShortestPathOp {
1855 input: Box::new(LogicalOperator::Join(JoinOp {
1856 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1857 variable: "a".to_string(),
1858 label: None,
1859 input: None,
1860 })),
1861 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1862 variable: "b".to_string(),
1863 label: None,
1864 input: None,
1865 })),
1866 join_type: JoinType::Cross,
1867 conditions: vec![],
1868 })),
1869 source_var: "a".to_string(),
1870 target_var: "b".to_string(),
1871 edge_types: vec!["ROAD".to_string()],
1872 direction: ExpandDirection::Outgoing,
1873 path_alias: "p".to_string(),
1874 all_paths: false,
1875 }));
1876
1877 let mut binder = Binder::new();
1878 let ctx = binder.bind(&plan).unwrap();
1879 assert!(ctx.contains("p"), "Path alias should be bound");
1880 assert!(
1881 ctx.contains("_path_length_p"),
1882 "Path length variable should be auto-created"
1883 );
1884 }
1885
1886 #[test]
1889 fn test_case_expression_validates_all_branches() {
1890 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1891 items: vec![ReturnItem {
1892 expression: LogicalExpression::Case {
1893 operand: None,
1894 when_clauses: vec![
1895 (
1896 LogicalExpression::Binary {
1897 left: Box::new(LogicalExpression::Property {
1898 variable: "n".to_string(),
1899 property: "age".to_string(),
1900 }),
1901 op: BinaryOp::Gt,
1902 right: Box::new(LogicalExpression::Literal(
1903 grafeo_common::types::Value::Int64(18),
1904 )),
1905 },
1906 LogicalExpression::Literal(grafeo_common::types::Value::String(
1907 "adult".into(),
1908 )),
1909 ),
1910 (
1911 LogicalExpression::Property {
1913 variable: "ghost".to_string(),
1914 property: "flag".to_string(),
1915 },
1916 LogicalExpression::Literal(grafeo_common::types::Value::String(
1917 "flagged".into(),
1918 )),
1919 ),
1920 ],
1921 else_clause: Some(Box::new(LogicalExpression::Literal(
1922 grafeo_common::types::Value::String("other".into()),
1923 ))),
1924 },
1925 alias: None,
1926 }],
1927 distinct: false,
1928 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1929 variable: "n".to_string(),
1930 label: None,
1931 input: None,
1932 })),
1933 }));
1934
1935 let mut binder = Binder::new();
1936 let err = binder.bind(&plan).unwrap_err();
1937 assert!(
1938 err.to_string().contains("ghost"),
1939 "CASE should validate all when-clause conditions"
1940 );
1941 }
1942
1943 #[test]
1944 fn test_case_expression_validates_else_clause() {
1945 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1946 items: vec![ReturnItem {
1947 expression: LogicalExpression::Case {
1948 operand: None,
1949 when_clauses: vec![(
1950 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
1951 LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
1952 )],
1953 else_clause: Some(Box::new(LogicalExpression::Property {
1954 variable: "missing".to_string(),
1955 property: "x".to_string(),
1956 })),
1957 },
1958 alias: None,
1959 }],
1960 distinct: false,
1961 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1962 variable: "n".to_string(),
1963 label: None,
1964 input: None,
1965 })),
1966 }));
1967
1968 let mut binder = Binder::new();
1969 let err = binder.bind(&plan).unwrap_err();
1970 assert!(
1971 err.to_string().contains("missing"),
1972 "CASE ELSE should validate its expression too"
1973 );
1974 }
1975
1976 #[test]
1977 fn test_slice_access_validates_expressions() {
1978 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1979 items: vec![ReturnItem {
1980 expression: LogicalExpression::SliceAccess {
1981 base: Box::new(LogicalExpression::Variable("n".to_string())),
1982 start: Some(Box::new(LogicalExpression::Variable(
1983 "undefined_start".to_string(),
1984 ))),
1985 end: None,
1986 },
1987 alias: None,
1988 }],
1989 distinct: false,
1990 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1991 variable: "n".to_string(),
1992 label: None,
1993 input: None,
1994 })),
1995 }));
1996
1997 let mut binder = Binder::new();
1998 let err = binder.bind(&plan).unwrap_err();
1999 assert!(err.to_string().contains("undefined_start"));
2000 }
2001
2002 #[test]
2003 fn test_list_comprehension_validates_list_source() {
2004 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2005 items: vec![ReturnItem {
2006 expression: LogicalExpression::ListComprehension {
2007 variable: "x".to_string(),
2008 list_expr: Box::new(LogicalExpression::Variable("not_defined".to_string())),
2009 filter_expr: None,
2010 map_expr: Box::new(LogicalExpression::Variable("x".to_string())),
2011 },
2012 alias: None,
2013 }],
2014 distinct: false,
2015 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2016 variable: "n".to_string(),
2017 label: None,
2018 input: None,
2019 })),
2020 }));
2021
2022 let mut binder = Binder::new();
2023 let err = binder.bind(&plan).unwrap_err();
2024 assert!(
2025 err.to_string().contains("not_defined"),
2026 "List comprehension should validate source list expression"
2027 );
2028 }
2029
2030 #[test]
2031 fn test_labels_type_id_reject_undefined() {
2032 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2034 items: vec![ReturnItem {
2035 expression: LogicalExpression::Labels("x".to_string()),
2036 alias: None,
2037 }],
2038 distinct: false,
2039 input: Box::new(LogicalOperator::Empty),
2040 }));
2041
2042 let mut binder = Binder::new();
2043 assert!(
2044 binder.bind(&plan).is_err(),
2045 "labels(x) on undefined x should fail"
2046 );
2047
2048 let plan2 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2050 items: vec![ReturnItem {
2051 expression: LogicalExpression::Type("e".to_string()),
2052 alias: None,
2053 }],
2054 distinct: false,
2055 input: Box::new(LogicalOperator::Empty),
2056 }));
2057
2058 let mut binder2 = Binder::new();
2059 assert!(
2060 binder2.bind(&plan2).is_err(),
2061 "type(e) on undefined e should fail"
2062 );
2063
2064 let plan3 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2066 items: vec![ReturnItem {
2067 expression: LogicalExpression::Id("n".to_string()),
2068 alias: None,
2069 }],
2070 distinct: false,
2071 input: Box::new(LogicalOperator::Empty),
2072 }));
2073
2074 let mut binder3 = Binder::new();
2075 assert!(
2076 binder3.bind(&plan3).is_err(),
2077 "id(n) on undefined n should fail"
2078 );
2079 }
2080
2081 #[test]
2082 fn test_expand_rejects_non_node_source() {
2083 use crate::query::plan::{ExpandDirection, ExpandOp, PathMode, UnwindOp};
2084
2085 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2088 items: vec![ReturnItem {
2089 expression: LogicalExpression::Variable("b".to_string()),
2090 alias: None,
2091 }],
2092 distinct: false,
2093 input: Box::new(LogicalOperator::Expand(ExpandOp {
2094 from_variable: "x".to_string(),
2095 to_variable: "b".to_string(),
2096 edge_variable: None,
2097 direction: ExpandDirection::Outgoing,
2098 edge_types: vec![],
2099 min_hops: 1,
2100 max_hops: Some(1),
2101 input: Box::new(LogicalOperator::Unwind(UnwindOp {
2102 expression: LogicalExpression::List(vec![]),
2103 variable: "x".to_string(),
2104 ordinality_var: None,
2105 offset_var: None,
2106 input: Box::new(LogicalOperator::Empty),
2107 })),
2108 path_alias: None,
2109 path_mode: PathMode::Walk,
2110 })),
2111 }));
2112
2113 let mut binder = Binder::new();
2114 let err = binder.bind(&plan).unwrap_err();
2115 assert!(
2116 err.to_string().contains("not a node"),
2117 "Expanding from non-node should fail, got: {err}"
2118 );
2119 }
2120
2121 #[test]
2122 fn test_add_label_rejects_undefined_variable() {
2123 use crate::query::plan::AddLabelOp;
2124
2125 let plan = LogicalPlan::new(LogicalOperator::AddLabel(AddLabelOp {
2126 variable: "missing".to_string(),
2127 labels: vec!["Admin".to_string()],
2128 input: Box::new(LogicalOperator::Empty),
2129 }));
2130
2131 let mut binder = Binder::new();
2132 let err = binder.bind(&plan).unwrap_err();
2133 assert!(err.to_string().contains("SET labels"));
2134 }
2135
2136 #[test]
2137 fn test_remove_label_rejects_undefined_variable() {
2138 use crate::query::plan::RemoveLabelOp;
2139
2140 let plan = LogicalPlan::new(LogicalOperator::RemoveLabel(RemoveLabelOp {
2141 variable: "missing".to_string(),
2142 labels: vec!["Admin".to_string()],
2143 input: Box::new(LogicalOperator::Empty),
2144 }));
2145
2146 let mut binder = Binder::new();
2147 let err = binder.bind(&plan).unwrap_err();
2148 assert!(err.to_string().contains("REMOVE labels"));
2149 }
2150
2151 #[test]
2152 fn test_sort_validates_key_expressions() {
2153 use crate::query::plan::{SortKey, SortOp, SortOrder};
2154
2155 let plan = LogicalPlan::new(LogicalOperator::Sort(SortOp {
2156 keys: vec![SortKey {
2157 expression: LogicalExpression::Property {
2158 variable: "missing".to_string(),
2159 property: "name".to_string(),
2160 },
2161 order: SortOrder::Ascending,
2162 }],
2163 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2164 variable: "n".to_string(),
2165 label: None,
2166 input: None,
2167 })),
2168 }));
2169
2170 let mut binder = Binder::new();
2171 assert!(
2172 binder.bind(&plan).is_err(),
2173 "ORDER BY on undefined variable should fail"
2174 );
2175 }
2176
2177 #[test]
2178 fn test_create_node_adds_variable_before_property_validation() {
2179 use crate::query::plan::CreateNodeOp;
2180
2181 let plan = LogicalPlan::new(LogicalOperator::CreateNode(CreateNodeOp {
2184 variable: "n".to_string(),
2185 labels: vec!["Person".to_string()],
2186 properties: vec![(
2187 "self_ref".to_string(),
2188 LogicalExpression::Property {
2189 variable: "n".to_string(),
2190 property: "name".to_string(),
2191 },
2192 )],
2193 input: None,
2194 }));
2195
2196 let mut binder = Binder::new();
2197 let ctx = binder.bind(&plan).unwrap();
2199 assert!(ctx.get("n").unwrap().is_node);
2200 }
2201
2202 #[test]
2203 fn test_undefined_variable_suggests_similar() {
2204 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2206 items: vec![ReturnItem {
2207 expression: LogicalExpression::Variable("persn".to_string()),
2208 alias: None,
2209 }],
2210 distinct: false,
2211 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2212 variable: "person".to_string(),
2213 label: None,
2214 input: None,
2215 })),
2216 }));
2217
2218 let mut binder = Binder::new();
2219 let err = binder.bind(&plan).unwrap_err();
2220 let msg = err.to_string();
2221 assert!(
2223 msg.contains("persn"),
2224 "Error should mention the undefined variable"
2225 );
2226 }
2227
2228 #[test]
2229 fn test_anon_variables_skip_validation() {
2230 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2232 items: vec![ReturnItem {
2233 expression: LogicalExpression::Variable("_anon_42".to_string()),
2234 alias: None,
2235 }],
2236 distinct: false,
2237 input: Box::new(LogicalOperator::Empty),
2238 }));
2239
2240 let mut binder = Binder::new();
2241 let result = binder.bind(&plan);
2242 assert!(
2243 result.is_ok(),
2244 "Anonymous variables should bypass validation"
2245 );
2246 }
2247
2248 #[test]
2249 fn test_map_expression_validates_values() {
2250 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2251 items: vec![ReturnItem {
2252 expression: LogicalExpression::Map(vec![(
2253 "key".to_string(),
2254 LogicalExpression::Variable("undefined".to_string()),
2255 )]),
2256 alias: None,
2257 }],
2258 distinct: false,
2259 input: Box::new(LogicalOperator::Empty),
2260 }));
2261
2262 let mut binder = Binder::new();
2263 assert!(
2264 binder.bind(&plan).is_err(),
2265 "Map values should be validated"
2266 );
2267 }
2268
2269 #[test]
2270 fn test_vector_scan_validates_query_vector() {
2271 use crate::query::plan::VectorScanOp;
2272
2273 let plan = LogicalPlan::new(LogicalOperator::VectorScan(VectorScanOp {
2274 variable: "result".to_string(),
2275 index_name: None,
2276 property: "embedding".to_string(),
2277 label: Some("Doc".to_string()),
2278 query_vector: LogicalExpression::Variable("undefined_vec".to_string()),
2279 k: 10,
2280 metric: None,
2281 min_similarity: None,
2282 max_distance: None,
2283 input: None,
2284 }));
2285
2286 let mut binder = Binder::new();
2287 let err = binder.bind(&plan).unwrap_err();
2288 assert!(err.to_string().contains("undefined_vec"));
2289 }
2290}