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(_)
535 | LogicalOperator::HorizontalAggregate(_) => Ok(()),
536 LogicalOperator::VectorScan(scan) => {
537 if let Some(ref input) = scan.input {
539 self.bind_operator(input)?;
540 }
541 self.context.add_variable(
542 scan.variable.clone(),
543 VariableInfo {
544 name: scan.variable.clone(),
545 data_type: LogicalType::Node,
546 is_node: true,
547 is_edge: false,
548 },
549 );
550 self.validate_expression(&scan.query_vector)?;
552 Ok(())
553 }
554 LogicalOperator::VectorJoin(join) => {
555 self.bind_operator(&join.input)?;
557 self.context.add_variable(
559 join.right_variable.clone(),
560 VariableInfo {
561 name: join.right_variable.clone(),
562 data_type: LogicalType::Node,
563 is_node: true,
564 is_edge: false,
565 },
566 );
567 if let Some(ref score_var) = join.score_variable {
569 self.context.add_variable(
570 score_var.clone(),
571 VariableInfo {
572 name: score_var.clone(),
573 data_type: LogicalType::Float64,
574 is_node: false,
575 is_edge: false,
576 },
577 );
578 }
579 self.validate_expression(&join.query_vector)?;
581 Ok(())
582 }
583 LogicalOperator::MapCollect(mc) => {
584 self.bind_operator(&mc.input)?;
585 self.context.add_variable(
586 mc.alias.clone(),
587 VariableInfo {
588 name: mc.alias.clone(),
589 data_type: LogicalType::Any,
590 is_node: false,
591 is_edge: false,
592 },
593 );
594 Ok(())
595 }
596 LogicalOperator::Except(except) => {
597 self.bind_operator(&except.left)?;
598 self.bind_operator(&except.right)?;
599 Ok(())
600 }
601 LogicalOperator::Intersect(intersect) => {
602 self.bind_operator(&intersect.left)?;
603 self.bind_operator(&intersect.right)?;
604 Ok(())
605 }
606 LogicalOperator::Otherwise(otherwise) => {
607 self.bind_operator(&otherwise.left)?;
608 self.bind_operator(&otherwise.right)?;
609 Ok(())
610 }
611 LogicalOperator::Apply(apply) => {
612 self.bind_operator(&apply.input)?;
613 self.bind_operator(&apply.subplan)?;
614 Ok(())
615 }
616 LogicalOperator::MultiWayJoin(mwj) => {
617 for input in &mwj.inputs {
618 self.bind_operator(input)?;
619 }
620 for cond in &mwj.conditions {
621 self.validate_expression(&cond.left)?;
622 self.validate_expression(&cond.right)?;
623 }
624 Ok(())
625 }
626 LogicalOperator::ParameterScan(param_scan) => {
627 for col in ¶m_scan.columns {
629 self.context.add_variable(
630 col.clone(),
631 VariableInfo {
632 name: col.clone(),
633 data_type: LogicalType::Any,
634 is_node: true,
635 is_edge: false,
636 },
637 );
638 }
639 Ok(())
640 }
641 LogicalOperator::CreatePropertyGraph(_) => Ok(()),
643 LogicalOperator::CallProcedure(call) => {
645 if let Some(yields) = &call.yield_items {
646 for item in yields {
647 let var_name = item.alias.as_deref().unwrap_or(&item.field_name);
648 self.context.add_variable(
649 var_name.to_string(),
650 VariableInfo {
651 name: var_name.to_string(),
652 data_type: LogicalType::Any,
653 is_node: false,
654 is_edge: false,
655 },
656 );
657 }
658 }
659 Ok(())
660 }
661 }
662 }
663
664 fn bind_triple_scan(&mut self, scan: &TripleScanOp) -> Result<()> {
666 use crate::query::plan::TripleComponent;
667
668 if let Some(ref input) = scan.input {
670 self.bind_operator(input)?;
671 }
672
673 if let TripleComponent::Variable(name) = &scan.subject
675 && !self.context.contains(name)
676 {
677 self.context.add_variable(
678 name.clone(),
679 VariableInfo {
680 name: name.clone(),
681 data_type: LogicalType::Any, is_node: false,
683 is_edge: false,
684 },
685 );
686 }
687
688 if let TripleComponent::Variable(name) = &scan.predicate
689 && !self.context.contains(name)
690 {
691 self.context.add_variable(
692 name.clone(),
693 VariableInfo {
694 name: name.clone(),
695 data_type: LogicalType::Any, is_node: false,
697 is_edge: false,
698 },
699 );
700 }
701
702 if let TripleComponent::Variable(name) = &scan.object
703 && !self.context.contains(name)
704 {
705 self.context.add_variable(
706 name.clone(),
707 VariableInfo {
708 name: name.clone(),
709 data_type: LogicalType::Any, is_node: false,
711 is_edge: false,
712 },
713 );
714 }
715
716 if let Some(TripleComponent::Variable(name)) = &scan.graph
717 && !self.context.contains(name)
718 {
719 self.context.add_variable(
720 name.clone(),
721 VariableInfo {
722 name: name.clone(),
723 data_type: LogicalType::Any, is_node: false,
725 is_edge: false,
726 },
727 );
728 }
729
730 Ok(())
731 }
732
733 fn bind_node_scan(&mut self, scan: &NodeScanOp) -> Result<()> {
735 if let Some(ref input) = scan.input {
737 self.bind_operator(input)?;
738 }
739
740 self.context.add_variable(
742 scan.variable.clone(),
743 VariableInfo {
744 name: scan.variable.clone(),
745 data_type: LogicalType::Node,
746 is_node: true,
747 is_edge: false,
748 },
749 );
750
751 Ok(())
752 }
753
754 fn bind_expand(&mut self, expand: &ExpandOp) -> Result<()> {
756 self.bind_operator(&expand.input)?;
758
759 if !self.context.contains(&expand.from_variable) {
761 return Err(undefined_variable_error(
762 &expand.from_variable,
763 &self.context,
764 " in EXPAND",
765 ));
766 }
767
768 if let Some(info) = self.context.get(&expand.from_variable)
770 && !info.is_node
771 {
772 return Err(binding_error(format!(
773 "Variable '{}' is not a node, cannot expand from it",
774 expand.from_variable
775 )));
776 }
777
778 if let Some(ref edge_var) = expand.edge_variable {
780 self.context.add_variable(
781 edge_var.clone(),
782 VariableInfo {
783 name: edge_var.clone(),
784 data_type: LogicalType::Edge,
785 is_node: false,
786 is_edge: true,
787 },
788 );
789 }
790
791 self.context.add_variable(
793 expand.to_variable.clone(),
794 VariableInfo {
795 name: expand.to_variable.clone(),
796 data_type: LogicalType::Node,
797 is_node: true,
798 is_edge: false,
799 },
800 );
801
802 if let Some(ref path_alias) = expand.path_alias {
804 self.context.add_variable(
806 path_alias.clone(),
807 VariableInfo {
808 name: path_alias.clone(),
809 data_type: LogicalType::Any,
810 is_node: false,
811 is_edge: false,
812 },
813 );
814 let path_length_var = format!("_path_length_{}", path_alias);
816 self.context.add_variable(
817 path_length_var.clone(),
818 VariableInfo {
819 name: path_length_var,
820 data_type: LogicalType::Int64,
821 is_node: false,
822 is_edge: false,
823 },
824 );
825 let path_nodes_var = format!("_path_nodes_{}", path_alias);
827 self.context.add_variable(
828 path_nodes_var.clone(),
829 VariableInfo {
830 name: path_nodes_var,
831 data_type: LogicalType::Any,
832 is_node: false,
833 is_edge: false,
834 },
835 );
836 let path_edges_var = format!("_path_edges_{}", path_alias);
838 self.context.add_variable(
839 path_edges_var.clone(),
840 VariableInfo {
841 name: path_edges_var,
842 data_type: LogicalType::Any,
843 is_node: false,
844 is_edge: false,
845 },
846 );
847 }
848
849 Ok(())
850 }
851
852 fn bind_filter(&mut self, filter: &FilterOp) -> Result<()> {
854 self.bind_operator(&filter.input)?;
856
857 self.validate_expression(&filter.predicate)?;
859
860 Ok(())
861 }
862
863 fn bind_return(&mut self, ret: &ReturnOp) -> Result<()> {
865 self.bind_operator(&ret.input)?;
867
868 for item in &ret.items {
871 self.validate_return_item(item)?;
872 if let Some(ref alias) = item.alias {
873 let data_type = self.infer_expression_type(&item.expression);
874 self.context.add_variable(
875 alias.clone(),
876 VariableInfo {
877 name: alias.clone(),
878 data_type,
879 is_node: false,
880 is_edge: false,
881 },
882 );
883 }
884 }
885
886 Ok(())
887 }
888
889 fn validate_return_item(&mut self, item: &ReturnItem) -> Result<()> {
891 self.validate_expression(&item.expression)
892 }
893
894 fn validate_expression(&mut self, expr: &LogicalExpression) -> Result<()> {
896 match expr {
897 LogicalExpression::Variable(name) => {
898 if name == "*" {
900 return Ok(());
901 }
902 if !self.context.contains(name) && !name.starts_with("_anon_") {
903 return Err(undefined_variable_error(name, &self.context, ""));
904 }
905 Ok(())
906 }
907 LogicalExpression::Property { variable, .. } => {
908 if !self.context.contains(variable) && !variable.starts_with("_anon_") {
909 return Err(undefined_variable_error(
910 variable,
911 &self.context,
912 " in property access",
913 ));
914 }
915 Ok(())
916 }
917 LogicalExpression::Literal(_) => Ok(()),
918 LogicalExpression::Binary { left, right, .. } => {
919 self.validate_expression(left)?;
920 self.validate_expression(right)
921 }
922 LogicalExpression::Unary { operand, .. } => self.validate_expression(operand),
923 LogicalExpression::FunctionCall { args, .. } => {
924 for arg in args {
925 self.validate_expression(arg)?;
926 }
927 Ok(())
928 }
929 LogicalExpression::List(items) => {
930 for item in items {
931 self.validate_expression(item)?;
932 }
933 Ok(())
934 }
935 LogicalExpression::Map(pairs) => {
936 for (_, value) in pairs {
937 self.validate_expression(value)?;
938 }
939 Ok(())
940 }
941 LogicalExpression::IndexAccess { base, index } => {
942 self.validate_expression(base)?;
943 self.validate_expression(index)
944 }
945 LogicalExpression::SliceAccess { base, start, end } => {
946 self.validate_expression(base)?;
947 if let Some(s) = start {
948 self.validate_expression(s)?;
949 }
950 if let Some(e) = end {
951 self.validate_expression(e)?;
952 }
953 Ok(())
954 }
955 LogicalExpression::Case {
956 operand,
957 when_clauses,
958 else_clause,
959 } => {
960 if let Some(op) = operand {
961 self.validate_expression(op)?;
962 }
963 for (cond, result) in when_clauses {
964 self.validate_expression(cond)?;
965 self.validate_expression(result)?;
966 }
967 if let Some(else_expr) = else_clause {
968 self.validate_expression(else_expr)?;
969 }
970 Ok(())
971 }
972 LogicalExpression::Parameter(_) => Ok(()),
974 LogicalExpression::Labels(var)
976 | LogicalExpression::Type(var)
977 | LogicalExpression::Id(var) => {
978 if !self.context.contains(var) && !var.starts_with("_anon_") {
979 return Err(undefined_variable_error(var, &self.context, " in function"));
980 }
981 Ok(())
982 }
983 LogicalExpression::ListComprehension { list_expr, .. } => {
984 self.validate_expression(list_expr)?;
988 Ok(())
989 }
990 LogicalExpression::ListPredicate { list_expr, .. } => {
991 self.validate_expression(list_expr)?;
995 Ok(())
996 }
997 LogicalExpression::ExistsSubquery(subquery)
998 | LogicalExpression::CountSubquery(subquery) => {
999 let _ = subquery; Ok(())
1003 }
1004 LogicalExpression::PatternComprehension {
1005 subplan,
1006 projection,
1007 } => {
1008 self.bind_operator(subplan)?;
1010 self.validate_expression(projection)
1012 }
1013 LogicalExpression::MapProjection { base, entries } => {
1014 if !self.context.contains(base) && !base.starts_with("_anon_") {
1015 return Err(undefined_variable_error(
1016 base,
1017 &self.context,
1018 " in map projection",
1019 ));
1020 }
1021 for entry in entries {
1022 if let crate::query::plan::MapProjectionEntry::LiteralEntry(_, expr) = entry {
1023 self.validate_expression(expr)?;
1024 }
1025 }
1026 Ok(())
1027 }
1028 LogicalExpression::Reduce {
1029 accumulator,
1030 initial,
1031 variable,
1032 list,
1033 expression,
1034 } => {
1035 self.validate_expression(initial)?;
1036 self.validate_expression(list)?;
1037 let had_acc = self.context.contains(accumulator);
1040 let had_var = self.context.contains(variable);
1041 if !had_acc {
1042 self.context.add_variable(
1043 accumulator.clone(),
1044 VariableInfo {
1045 name: accumulator.clone(),
1046 data_type: LogicalType::Any,
1047 is_node: false,
1048 is_edge: false,
1049 },
1050 );
1051 }
1052 if !had_var {
1053 self.context.add_variable(
1054 variable.clone(),
1055 VariableInfo {
1056 name: variable.clone(),
1057 data_type: LogicalType::Any,
1058 is_node: false,
1059 is_edge: false,
1060 },
1061 );
1062 }
1063 self.validate_expression(expression)?;
1064 if !had_acc {
1065 self.context.remove_variable(accumulator);
1066 }
1067 if !had_var {
1068 self.context.remove_variable(variable);
1069 }
1070 Ok(())
1071 }
1072 }
1073 }
1074
1075 fn infer_expression_type(&self, expr: &LogicalExpression) -> LogicalType {
1077 match expr {
1078 LogicalExpression::Variable(name) => {
1079 self.context
1081 .get(name)
1082 .map_or(LogicalType::Any, |info| info.data_type.clone())
1083 }
1084 LogicalExpression::Property { .. } => LogicalType::Any, LogicalExpression::Literal(value) => {
1086 use grafeo_common::types::Value;
1088 match value {
1089 Value::Bool(_) => LogicalType::Bool,
1090 Value::Int64(_) => LogicalType::Int64,
1091 Value::Float64(_) => LogicalType::Float64,
1092 Value::String(_) => LogicalType::String,
1093 Value::List(_) => LogicalType::Any, Value::Map(_) => LogicalType::Any, Value::Null => LogicalType::Any,
1096 _ => LogicalType::Any,
1097 }
1098 }
1099 LogicalExpression::Binary { .. } => LogicalType::Any, LogicalExpression::Unary { .. } => LogicalType::Any,
1101 LogicalExpression::FunctionCall { name, .. } => {
1102 match name.to_lowercase().as_str() {
1104 "count" | "sum" | "id" => LogicalType::Int64,
1105 "avg" => LogicalType::Float64,
1106 "type" => LogicalType::String,
1107 "labels" | "collect" => LogicalType::Any,
1109 _ => LogicalType::Any,
1110 }
1111 }
1112 LogicalExpression::List(_) => LogicalType::Any, LogicalExpression::Map(_) => LogicalType::Any, _ => LogicalType::Any,
1115 }
1116 }
1117
1118 fn bind_join(&mut self, join: &crate::query::plan::JoinOp) -> Result<()> {
1120 self.bind_operator(&join.left)?;
1122 self.bind_operator(&join.right)?;
1123
1124 for condition in &join.conditions {
1126 self.validate_expression(&condition.left)?;
1127 self.validate_expression(&condition.right)?;
1128 }
1129
1130 Ok(())
1131 }
1132
1133 fn bind_aggregate(&mut self, agg: &crate::query::plan::AggregateOp) -> Result<()> {
1135 self.bind_operator(&agg.input)?;
1137
1138 for expr in &agg.group_by {
1140 self.validate_expression(expr)?;
1141 }
1142
1143 for agg_expr in &agg.aggregates {
1145 if let Some(ref expr) = agg_expr.expression {
1146 self.validate_expression(expr)?;
1147 }
1148 if let Some(ref alias) = agg_expr.alias {
1150 self.context.add_variable(
1151 alias.clone(),
1152 VariableInfo {
1153 name: alias.clone(),
1154 data_type: LogicalType::Any,
1155 is_node: false,
1156 is_edge: false,
1157 },
1158 );
1159 }
1160 }
1161
1162 Ok(())
1163 }
1164}
1165
1166impl Default for Binder {
1167 fn default() -> Self {
1168 Self::new()
1169 }
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174 use super::*;
1175 use crate::query::plan::{BinaryOp, FilterOp};
1176
1177 #[test]
1178 fn test_bind_simple_scan() {
1179 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1180 items: vec![ReturnItem {
1181 expression: LogicalExpression::Variable("n".to_string()),
1182 alias: None,
1183 }],
1184 distinct: false,
1185 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1186 variable: "n".to_string(),
1187 label: Some("Person".to_string()),
1188 input: None,
1189 })),
1190 }));
1191
1192 let mut binder = Binder::new();
1193 let result = binder.bind(&plan);
1194
1195 assert!(result.is_ok());
1196 let ctx = result.unwrap();
1197 assert!(ctx.contains("n"));
1198 assert!(ctx.get("n").unwrap().is_node);
1199 }
1200
1201 #[test]
1202 fn test_bind_undefined_variable() {
1203 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1204 items: vec![ReturnItem {
1205 expression: LogicalExpression::Variable("undefined".to_string()),
1206 alias: None,
1207 }],
1208 distinct: false,
1209 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1210 variable: "n".to_string(),
1211 label: None,
1212 input: None,
1213 })),
1214 }));
1215
1216 let mut binder = Binder::new();
1217 let result = binder.bind(&plan);
1218
1219 assert!(result.is_err());
1220 let err = result.unwrap_err();
1221 assert!(err.to_string().contains("Undefined variable"));
1222 }
1223
1224 #[test]
1225 fn test_bind_property_access() {
1226 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1227 items: vec![ReturnItem {
1228 expression: LogicalExpression::Property {
1229 variable: "n".to_string(),
1230 property: "name".to_string(),
1231 },
1232 alias: None,
1233 }],
1234 distinct: false,
1235 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1236 variable: "n".to_string(),
1237 label: Some("Person".to_string()),
1238 input: None,
1239 })),
1240 }));
1241
1242 let mut binder = Binder::new();
1243 let result = binder.bind(&plan);
1244
1245 assert!(result.is_ok());
1246 }
1247
1248 #[test]
1249 fn test_bind_filter_with_undefined_variable() {
1250 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1251 items: vec![ReturnItem {
1252 expression: LogicalExpression::Variable("n".to_string()),
1253 alias: None,
1254 }],
1255 distinct: false,
1256 input: Box::new(LogicalOperator::Filter(FilterOp {
1257 predicate: LogicalExpression::Binary {
1258 left: Box::new(LogicalExpression::Property {
1259 variable: "m".to_string(), property: "age".to_string(),
1261 }),
1262 op: BinaryOp::Gt,
1263 right: Box::new(LogicalExpression::Literal(
1264 grafeo_common::types::Value::Int64(30),
1265 )),
1266 },
1267 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1268 variable: "n".to_string(),
1269 label: None,
1270 input: None,
1271 })),
1272 pushdown_hint: None,
1273 })),
1274 }));
1275
1276 let mut binder = Binder::new();
1277 let result = binder.bind(&plan);
1278
1279 assert!(result.is_err());
1280 let err = result.unwrap_err();
1281 assert!(err.to_string().contains("Undefined variable 'm'"));
1282 }
1283
1284 #[test]
1285 fn test_bind_expand() {
1286 use crate::query::plan::{ExpandDirection, ExpandOp, PathMode};
1287
1288 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1289 items: vec![
1290 ReturnItem {
1291 expression: LogicalExpression::Variable("a".to_string()),
1292 alias: None,
1293 },
1294 ReturnItem {
1295 expression: LogicalExpression::Variable("b".to_string()),
1296 alias: None,
1297 },
1298 ],
1299 distinct: false,
1300 input: Box::new(LogicalOperator::Expand(ExpandOp {
1301 from_variable: "a".to_string(),
1302 to_variable: "b".to_string(),
1303 edge_variable: Some("e".to_string()),
1304 direction: ExpandDirection::Outgoing,
1305 edge_types: vec!["KNOWS".to_string()],
1306 min_hops: 1,
1307 max_hops: Some(1),
1308 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1309 variable: "a".to_string(),
1310 label: Some("Person".to_string()),
1311 input: None,
1312 })),
1313 path_alias: None,
1314 path_mode: PathMode::Walk,
1315 })),
1316 }));
1317
1318 let mut binder = Binder::new();
1319 let result = binder.bind(&plan);
1320
1321 assert!(result.is_ok());
1322 let ctx = result.unwrap();
1323 assert!(ctx.contains("a"));
1324 assert!(ctx.contains("b"));
1325 assert!(ctx.contains("e"));
1326 assert!(ctx.get("a").unwrap().is_node);
1327 assert!(ctx.get("b").unwrap().is_node);
1328 assert!(ctx.get("e").unwrap().is_edge);
1329 }
1330
1331 #[test]
1332 fn test_bind_expand_from_undefined_variable() {
1333 use crate::query::plan::{ExpandDirection, ExpandOp, PathMode};
1335
1336 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1337 items: vec![ReturnItem {
1338 expression: LogicalExpression::Variable("b".to_string()),
1339 alias: None,
1340 }],
1341 distinct: false,
1342 input: Box::new(LogicalOperator::Expand(ExpandOp {
1343 from_variable: "undefined".to_string(), to_variable: "b".to_string(),
1345 edge_variable: None,
1346 direction: ExpandDirection::Outgoing,
1347 edge_types: vec![],
1348 min_hops: 1,
1349 max_hops: Some(1),
1350 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1351 variable: "a".to_string(),
1352 label: None,
1353 input: None,
1354 })),
1355 path_alias: None,
1356 path_mode: PathMode::Walk,
1357 })),
1358 }));
1359
1360 let mut binder = Binder::new();
1361 let result = binder.bind(&plan);
1362
1363 assert!(result.is_err());
1364 let err = result.unwrap_err();
1365 assert!(
1366 err.to_string().contains("Undefined variable 'undefined'"),
1367 "Expected error about undefined variable, got: {}",
1368 err
1369 );
1370 }
1371
1372 #[test]
1373 fn test_bind_return_with_aggregate_and_non_aggregate() {
1374 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1376 items: vec![
1377 ReturnItem {
1378 expression: LogicalExpression::FunctionCall {
1379 name: "count".to_string(),
1380 args: vec![LogicalExpression::Variable("n".to_string())],
1381 distinct: false,
1382 },
1383 alias: Some("cnt".to_string()),
1384 },
1385 ReturnItem {
1386 expression: LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
1387 alias: Some("one".to_string()),
1388 },
1389 ],
1390 distinct: false,
1391 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1392 variable: "n".to_string(),
1393 label: Some("Person".to_string()),
1394 input: None,
1395 })),
1396 }));
1397
1398 let mut binder = Binder::new();
1399 let result = binder.bind(&plan);
1400
1401 assert!(result.is_ok());
1403 }
1404
1405 #[test]
1406 fn test_bind_nested_property_access() {
1407 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1409 items: vec![
1410 ReturnItem {
1411 expression: LogicalExpression::Property {
1412 variable: "n".to_string(),
1413 property: "name".to_string(),
1414 },
1415 alias: None,
1416 },
1417 ReturnItem {
1418 expression: LogicalExpression::Property {
1419 variable: "n".to_string(),
1420 property: "age".to_string(),
1421 },
1422 alias: None,
1423 },
1424 ],
1425 distinct: false,
1426 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1427 variable: "n".to_string(),
1428 label: Some("Person".to_string()),
1429 input: None,
1430 })),
1431 }));
1432
1433 let mut binder = Binder::new();
1434 let result = binder.bind(&plan);
1435
1436 assert!(result.is_ok());
1437 }
1438
1439 #[test]
1440 fn test_bind_binary_expression_with_undefined() {
1441 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1443 items: vec![ReturnItem {
1444 expression: LogicalExpression::Binary {
1445 left: Box::new(LogicalExpression::Property {
1446 variable: "n".to_string(),
1447 property: "age".to_string(),
1448 }),
1449 op: BinaryOp::Add,
1450 right: Box::new(LogicalExpression::Property {
1451 variable: "m".to_string(), property: "age".to_string(),
1453 }),
1454 },
1455 alias: Some("total".to_string()),
1456 }],
1457 distinct: false,
1458 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1459 variable: "n".to_string(),
1460 label: None,
1461 input: None,
1462 })),
1463 }));
1464
1465 let mut binder = Binder::new();
1466 let result = binder.bind(&plan);
1467
1468 assert!(result.is_err());
1469 assert!(
1470 result
1471 .unwrap_err()
1472 .to_string()
1473 .contains("Undefined variable 'm'")
1474 );
1475 }
1476
1477 #[test]
1478 fn test_bind_duplicate_variable_definition() {
1479 use crate::query::plan::{JoinOp, JoinType};
1482
1483 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1484 items: vec![ReturnItem {
1485 expression: LogicalExpression::Variable("n".to_string()),
1486 alias: None,
1487 }],
1488 distinct: false,
1489 input: Box::new(LogicalOperator::Join(JoinOp {
1490 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1491 variable: "n".to_string(),
1492 label: Some("A".to_string()),
1493 input: None,
1494 })),
1495 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1496 variable: "m".to_string(), label: Some("B".to_string()),
1498 input: None,
1499 })),
1500 join_type: JoinType::Inner,
1501 conditions: vec![],
1502 })),
1503 }));
1504
1505 let mut binder = Binder::new();
1506 let result = binder.bind(&plan);
1507
1508 assert!(result.is_ok());
1510 let ctx = result.unwrap();
1511 assert!(ctx.contains("n"));
1512 assert!(ctx.contains("m"));
1513 }
1514
1515 #[test]
1516 fn test_bind_function_with_wrong_arity() {
1517 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1520 items: vec![ReturnItem {
1521 expression: LogicalExpression::FunctionCall {
1522 name: "count".to_string(),
1523 args: vec![], distinct: false,
1525 },
1526 alias: None,
1527 }],
1528 distinct: false,
1529 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1530 variable: "n".to_string(),
1531 label: None,
1532 input: None,
1533 })),
1534 }));
1535
1536 let mut binder = Binder::new();
1537 let result = binder.bind(&plan);
1538
1539 let _ = result; }
1544
1545 #[test]
1548 fn test_create_edge_rejects_undefined_source() {
1549 use crate::query::plan::CreateEdgeOp;
1550
1551 let plan = LogicalPlan::new(LogicalOperator::CreateEdge(CreateEdgeOp {
1552 variable: Some("e".to_string()),
1553 from_variable: "ghost".to_string(), to_variable: "b".to_string(),
1555 edge_type: "KNOWS".to_string(),
1556 properties: vec![],
1557 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1558 variable: "b".to_string(),
1559 label: None,
1560 input: None,
1561 })),
1562 }));
1563
1564 let mut binder = Binder::new();
1565 let err = binder.bind(&plan).unwrap_err();
1566 assert!(
1567 err.to_string().contains("Undefined variable 'ghost'"),
1568 "Should reject undefined source variable, got: {err}"
1569 );
1570 }
1571
1572 #[test]
1573 fn test_create_edge_rejects_undefined_target() {
1574 use crate::query::plan::CreateEdgeOp;
1575
1576 let plan = LogicalPlan::new(LogicalOperator::CreateEdge(CreateEdgeOp {
1577 variable: None,
1578 from_variable: "a".to_string(),
1579 to_variable: "missing".to_string(), edge_type: "KNOWS".to_string(),
1581 properties: vec![],
1582 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1583 variable: "a".to_string(),
1584 label: None,
1585 input: None,
1586 })),
1587 }));
1588
1589 let mut binder = Binder::new();
1590 let err = binder.bind(&plan).unwrap_err();
1591 assert!(
1592 err.to_string().contains("Undefined variable 'missing'"),
1593 "Should reject undefined target variable, got: {err}"
1594 );
1595 }
1596
1597 #[test]
1598 fn test_create_edge_validates_property_expressions() {
1599 use crate::query::plan::CreateEdgeOp;
1600
1601 let plan = LogicalPlan::new(LogicalOperator::CreateEdge(CreateEdgeOp {
1603 variable: Some("e".to_string()),
1604 from_variable: "a".to_string(),
1605 to_variable: "b".to_string(),
1606 edge_type: "KNOWS".to_string(),
1607 properties: vec![(
1608 "since".to_string(),
1609 LogicalExpression::Property {
1610 variable: "x".to_string(), property: "year".to_string(),
1612 },
1613 )],
1614 input: Box::new(LogicalOperator::Join(crate::query::plan::JoinOp {
1615 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1616 variable: "a".to_string(),
1617 label: None,
1618 input: None,
1619 })),
1620 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1621 variable: "b".to_string(),
1622 label: None,
1623 input: None,
1624 })),
1625 join_type: crate::query::plan::JoinType::Inner,
1626 conditions: vec![],
1627 })),
1628 }));
1629
1630 let mut binder = Binder::new();
1631 let err = binder.bind(&plan).unwrap_err();
1632 assert!(err.to_string().contains("Undefined variable 'x'"));
1633 }
1634
1635 #[test]
1636 fn test_set_property_rejects_undefined_variable() {
1637 use crate::query::plan::SetPropertyOp;
1638
1639 let plan = LogicalPlan::new(LogicalOperator::SetProperty(SetPropertyOp {
1640 variable: "ghost".to_string(),
1641 properties: vec![(
1642 "name".to_string(),
1643 LogicalExpression::Literal(grafeo_common::types::Value::String("Alix".into())),
1644 )],
1645 replace: false,
1646 is_edge: false,
1647 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1648 variable: "n".to_string(),
1649 label: None,
1650 input: None,
1651 })),
1652 }));
1653
1654 let mut binder = Binder::new();
1655 let err = binder.bind(&plan).unwrap_err();
1656 assert!(
1657 err.to_string().contains("in SET"),
1658 "Error should indicate SET context, got: {err}"
1659 );
1660 }
1661
1662 #[test]
1663 fn test_delete_node_rejects_undefined_variable() {
1664 use crate::query::plan::DeleteNodeOp;
1665
1666 let plan = LogicalPlan::new(LogicalOperator::DeleteNode(DeleteNodeOp {
1667 variable: "phantom".to_string(),
1668 detach: false,
1669 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1670 variable: "n".to_string(),
1671 label: None,
1672 input: None,
1673 })),
1674 }));
1675
1676 let mut binder = Binder::new();
1677 let err = binder.bind(&plan).unwrap_err();
1678 assert!(err.to_string().contains("Undefined variable 'phantom'"));
1679 }
1680
1681 #[test]
1682 fn test_delete_edge_rejects_undefined_variable() {
1683 use crate::query::plan::DeleteEdgeOp;
1684
1685 let plan = LogicalPlan::new(LogicalOperator::DeleteEdge(DeleteEdgeOp {
1686 variable: "gone".to_string(),
1687 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1688 variable: "n".to_string(),
1689 label: None,
1690 input: None,
1691 })),
1692 }));
1693
1694 let mut binder = Binder::new();
1695 let err = binder.bind(&plan).unwrap_err();
1696 assert!(err.to_string().contains("Undefined variable 'gone'"));
1697 }
1698
1699 #[test]
1702 fn test_project_alias_becomes_available_downstream() {
1703 use crate::query::plan::{ProjectOp, Projection};
1704
1705 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1707 items: vec![ReturnItem {
1708 expression: LogicalExpression::Variable("person_name".to_string()),
1709 alias: None,
1710 }],
1711 distinct: false,
1712 input: Box::new(LogicalOperator::Project(ProjectOp {
1713 projections: vec![Projection {
1714 expression: LogicalExpression::Property {
1715 variable: "n".to_string(),
1716 property: "name".to_string(),
1717 },
1718 alias: Some("person_name".to_string()),
1719 }],
1720 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1721 variable: "n".to_string(),
1722 label: None,
1723 input: None,
1724 })),
1725 })),
1726 }));
1727
1728 let mut binder = Binder::new();
1729 let ctx = binder.bind(&plan).unwrap();
1730 assert!(
1731 ctx.contains("person_name"),
1732 "WITH alias should be available to RETURN"
1733 );
1734 }
1735
1736 #[test]
1737 fn test_project_rejects_undefined_expression() {
1738 use crate::query::plan::{ProjectOp, Projection};
1739
1740 let plan = LogicalPlan::new(LogicalOperator::Project(ProjectOp {
1741 projections: vec![Projection {
1742 expression: LogicalExpression::Variable("nope".to_string()),
1743 alias: Some("x".to_string()),
1744 }],
1745 input: Box::new(LogicalOperator::Empty),
1746 }));
1747
1748 let mut binder = Binder::new();
1749 let result = binder.bind(&plan);
1750 assert!(result.is_err(), "WITH on undefined variable should fail");
1751 }
1752
1753 #[test]
1756 fn test_unwind_adds_element_variable() {
1757 use crate::query::plan::UnwindOp;
1758
1759 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1760 items: vec![ReturnItem {
1761 expression: LogicalExpression::Variable("item".to_string()),
1762 alias: None,
1763 }],
1764 distinct: false,
1765 input: Box::new(LogicalOperator::Unwind(UnwindOp {
1766 expression: LogicalExpression::List(vec![
1767 LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
1768 LogicalExpression::Literal(grafeo_common::types::Value::Int64(2)),
1769 ]),
1770 variable: "item".to_string(),
1771 ordinality_var: None,
1772 offset_var: None,
1773 input: Box::new(LogicalOperator::Empty),
1774 })),
1775 }));
1776
1777 let mut binder = Binder::new();
1778 let ctx = binder.bind(&plan).unwrap();
1779 assert!(ctx.contains("item"), "UNWIND variable should be in scope");
1780 let info = ctx.get("item").unwrap();
1781 assert!(
1782 !info.is_node && !info.is_edge,
1783 "UNWIND variable is not a graph element"
1784 );
1785 }
1786
1787 #[test]
1790 fn test_merge_adds_variable_and_validates_properties() {
1791 use crate::query::plan::MergeOp;
1792
1793 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1794 items: vec![ReturnItem {
1795 expression: LogicalExpression::Variable("m".to_string()),
1796 alias: None,
1797 }],
1798 distinct: false,
1799 input: Box::new(LogicalOperator::Merge(MergeOp {
1800 variable: "m".to_string(),
1801 labels: vec!["Person".to_string()],
1802 match_properties: vec![(
1803 "name".to_string(),
1804 LogicalExpression::Literal(grafeo_common::types::Value::String("Alix".into())),
1805 )],
1806 on_create: vec![(
1807 "created".to_string(),
1808 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
1809 )],
1810 on_match: vec![(
1811 "updated".to_string(),
1812 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
1813 )],
1814 input: Box::new(LogicalOperator::Empty),
1815 })),
1816 }));
1817
1818 let mut binder = Binder::new();
1819 let ctx = binder.bind(&plan).unwrap();
1820 assert!(ctx.contains("m"));
1821 assert!(
1822 ctx.get("m").unwrap().is_node,
1823 "MERGE variable should be a node"
1824 );
1825 }
1826
1827 #[test]
1828 fn test_merge_rejects_undefined_in_on_create() {
1829 use crate::query::plan::MergeOp;
1830
1831 let plan = LogicalPlan::new(LogicalOperator::Merge(MergeOp {
1832 variable: "m".to_string(),
1833 labels: vec![],
1834 match_properties: vec![],
1835 on_create: vec![(
1836 "name".to_string(),
1837 LogicalExpression::Property {
1838 variable: "other".to_string(), property: "name".to_string(),
1840 },
1841 )],
1842 on_match: vec![],
1843 input: Box::new(LogicalOperator::Empty),
1844 }));
1845
1846 let mut binder = Binder::new();
1847 let result = binder.bind(&plan);
1848 assert!(
1849 result.is_err(),
1850 "ON CREATE referencing undefined variable should fail"
1851 );
1852 }
1853
1854 #[test]
1857 fn test_shortest_path_rejects_undefined_source() {
1858 use crate::query::plan::{ExpandDirection, ShortestPathOp};
1859
1860 let plan = LogicalPlan::new(LogicalOperator::ShortestPath(ShortestPathOp {
1861 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1862 variable: "b".to_string(),
1863 label: None,
1864 input: None,
1865 })),
1866 source_var: "missing".to_string(), target_var: "b".to_string(),
1868 edge_types: vec![],
1869 direction: ExpandDirection::Both,
1870 path_alias: "p".to_string(),
1871 all_paths: false,
1872 }));
1873
1874 let mut binder = Binder::new();
1875 let err = binder.bind(&plan).unwrap_err();
1876 assert!(
1877 err.to_string().contains("source in shortestPath"),
1878 "Error should mention shortestPath source context, got: {err}"
1879 );
1880 }
1881
1882 #[test]
1883 fn test_shortest_path_adds_path_and_length_variables() {
1884 use crate::query::plan::{ExpandDirection, JoinOp, JoinType, ShortestPathOp};
1885
1886 let plan = LogicalPlan::new(LogicalOperator::ShortestPath(ShortestPathOp {
1887 input: Box::new(LogicalOperator::Join(JoinOp {
1888 left: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1889 variable: "a".to_string(),
1890 label: None,
1891 input: None,
1892 })),
1893 right: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1894 variable: "b".to_string(),
1895 label: None,
1896 input: None,
1897 })),
1898 join_type: JoinType::Cross,
1899 conditions: vec![],
1900 })),
1901 source_var: "a".to_string(),
1902 target_var: "b".to_string(),
1903 edge_types: vec!["ROAD".to_string()],
1904 direction: ExpandDirection::Outgoing,
1905 path_alias: "p".to_string(),
1906 all_paths: false,
1907 }));
1908
1909 let mut binder = Binder::new();
1910 let ctx = binder.bind(&plan).unwrap();
1911 assert!(ctx.contains("p"), "Path alias should be bound");
1912 assert!(
1913 ctx.contains("_path_length_p"),
1914 "Path length variable should be auto-created"
1915 );
1916 }
1917
1918 #[test]
1921 fn test_case_expression_validates_all_branches() {
1922 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1923 items: vec![ReturnItem {
1924 expression: LogicalExpression::Case {
1925 operand: None,
1926 when_clauses: vec![
1927 (
1928 LogicalExpression::Binary {
1929 left: Box::new(LogicalExpression::Property {
1930 variable: "n".to_string(),
1931 property: "age".to_string(),
1932 }),
1933 op: BinaryOp::Gt,
1934 right: Box::new(LogicalExpression::Literal(
1935 grafeo_common::types::Value::Int64(18),
1936 )),
1937 },
1938 LogicalExpression::Literal(grafeo_common::types::Value::String(
1939 "adult".into(),
1940 )),
1941 ),
1942 (
1943 LogicalExpression::Property {
1945 variable: "ghost".to_string(),
1946 property: "flag".to_string(),
1947 },
1948 LogicalExpression::Literal(grafeo_common::types::Value::String(
1949 "flagged".into(),
1950 )),
1951 ),
1952 ],
1953 else_clause: Some(Box::new(LogicalExpression::Literal(
1954 grafeo_common::types::Value::String("other".into()),
1955 ))),
1956 },
1957 alias: None,
1958 }],
1959 distinct: false,
1960 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1961 variable: "n".to_string(),
1962 label: None,
1963 input: None,
1964 })),
1965 }));
1966
1967 let mut binder = Binder::new();
1968 let err = binder.bind(&plan).unwrap_err();
1969 assert!(
1970 err.to_string().contains("ghost"),
1971 "CASE should validate all when-clause conditions"
1972 );
1973 }
1974
1975 #[test]
1976 fn test_case_expression_validates_else_clause() {
1977 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1978 items: vec![ReturnItem {
1979 expression: LogicalExpression::Case {
1980 operand: None,
1981 when_clauses: vec![(
1982 LogicalExpression::Literal(grafeo_common::types::Value::Bool(true)),
1983 LogicalExpression::Literal(grafeo_common::types::Value::Int64(1)),
1984 )],
1985 else_clause: Some(Box::new(LogicalExpression::Property {
1986 variable: "missing".to_string(),
1987 property: "x".to_string(),
1988 })),
1989 },
1990 alias: None,
1991 }],
1992 distinct: false,
1993 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
1994 variable: "n".to_string(),
1995 label: None,
1996 input: None,
1997 })),
1998 }));
1999
2000 let mut binder = Binder::new();
2001 let err = binder.bind(&plan).unwrap_err();
2002 assert!(
2003 err.to_string().contains("missing"),
2004 "CASE ELSE should validate its expression too"
2005 );
2006 }
2007
2008 #[test]
2009 fn test_slice_access_validates_expressions() {
2010 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2011 items: vec![ReturnItem {
2012 expression: LogicalExpression::SliceAccess {
2013 base: Box::new(LogicalExpression::Variable("n".to_string())),
2014 start: Some(Box::new(LogicalExpression::Variable(
2015 "undefined_start".to_string(),
2016 ))),
2017 end: None,
2018 },
2019 alias: None,
2020 }],
2021 distinct: false,
2022 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2023 variable: "n".to_string(),
2024 label: None,
2025 input: None,
2026 })),
2027 }));
2028
2029 let mut binder = Binder::new();
2030 let err = binder.bind(&plan).unwrap_err();
2031 assert!(err.to_string().contains("undefined_start"));
2032 }
2033
2034 #[test]
2035 fn test_list_comprehension_validates_list_source() {
2036 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2037 items: vec![ReturnItem {
2038 expression: LogicalExpression::ListComprehension {
2039 variable: "x".to_string(),
2040 list_expr: Box::new(LogicalExpression::Variable("not_defined".to_string())),
2041 filter_expr: None,
2042 map_expr: Box::new(LogicalExpression::Variable("x".to_string())),
2043 },
2044 alias: None,
2045 }],
2046 distinct: false,
2047 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2048 variable: "n".to_string(),
2049 label: None,
2050 input: None,
2051 })),
2052 }));
2053
2054 let mut binder = Binder::new();
2055 let err = binder.bind(&plan).unwrap_err();
2056 assert!(
2057 err.to_string().contains("not_defined"),
2058 "List comprehension should validate source list expression"
2059 );
2060 }
2061
2062 #[test]
2063 fn test_labels_type_id_reject_undefined() {
2064 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2066 items: vec![ReturnItem {
2067 expression: LogicalExpression::Labels("x".to_string()),
2068 alias: None,
2069 }],
2070 distinct: false,
2071 input: Box::new(LogicalOperator::Empty),
2072 }));
2073
2074 let mut binder = Binder::new();
2075 assert!(
2076 binder.bind(&plan).is_err(),
2077 "labels(x) on undefined x should fail"
2078 );
2079
2080 let plan2 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2082 items: vec![ReturnItem {
2083 expression: LogicalExpression::Type("e".to_string()),
2084 alias: None,
2085 }],
2086 distinct: false,
2087 input: Box::new(LogicalOperator::Empty),
2088 }));
2089
2090 let mut binder2 = Binder::new();
2091 assert!(
2092 binder2.bind(&plan2).is_err(),
2093 "type(e) on undefined e should fail"
2094 );
2095
2096 let plan3 = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2098 items: vec![ReturnItem {
2099 expression: LogicalExpression::Id("n".to_string()),
2100 alias: None,
2101 }],
2102 distinct: false,
2103 input: Box::new(LogicalOperator::Empty),
2104 }));
2105
2106 let mut binder3 = Binder::new();
2107 assert!(
2108 binder3.bind(&plan3).is_err(),
2109 "id(n) on undefined n should fail"
2110 );
2111 }
2112
2113 #[test]
2114 fn test_expand_rejects_non_node_source() {
2115 use crate::query::plan::{ExpandDirection, ExpandOp, PathMode, UnwindOp};
2116
2117 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2120 items: vec![ReturnItem {
2121 expression: LogicalExpression::Variable("b".to_string()),
2122 alias: None,
2123 }],
2124 distinct: false,
2125 input: Box::new(LogicalOperator::Expand(ExpandOp {
2126 from_variable: "x".to_string(),
2127 to_variable: "b".to_string(),
2128 edge_variable: None,
2129 direction: ExpandDirection::Outgoing,
2130 edge_types: vec![],
2131 min_hops: 1,
2132 max_hops: Some(1),
2133 input: Box::new(LogicalOperator::Unwind(UnwindOp {
2134 expression: LogicalExpression::List(vec![]),
2135 variable: "x".to_string(),
2136 ordinality_var: None,
2137 offset_var: None,
2138 input: Box::new(LogicalOperator::Empty),
2139 })),
2140 path_alias: None,
2141 path_mode: PathMode::Walk,
2142 })),
2143 }));
2144
2145 let mut binder = Binder::new();
2146 let err = binder.bind(&plan).unwrap_err();
2147 assert!(
2148 err.to_string().contains("not a node"),
2149 "Expanding from non-node should fail, got: {err}"
2150 );
2151 }
2152
2153 #[test]
2154 fn test_add_label_rejects_undefined_variable() {
2155 use crate::query::plan::AddLabelOp;
2156
2157 let plan = LogicalPlan::new(LogicalOperator::AddLabel(AddLabelOp {
2158 variable: "missing".to_string(),
2159 labels: vec!["Admin".to_string()],
2160 input: Box::new(LogicalOperator::Empty),
2161 }));
2162
2163 let mut binder = Binder::new();
2164 let err = binder.bind(&plan).unwrap_err();
2165 assert!(err.to_string().contains("SET labels"));
2166 }
2167
2168 #[test]
2169 fn test_remove_label_rejects_undefined_variable() {
2170 use crate::query::plan::RemoveLabelOp;
2171
2172 let plan = LogicalPlan::new(LogicalOperator::RemoveLabel(RemoveLabelOp {
2173 variable: "missing".to_string(),
2174 labels: vec!["Admin".to_string()],
2175 input: Box::new(LogicalOperator::Empty),
2176 }));
2177
2178 let mut binder = Binder::new();
2179 let err = binder.bind(&plan).unwrap_err();
2180 assert!(err.to_string().contains("REMOVE labels"));
2181 }
2182
2183 #[test]
2184 fn test_sort_validates_key_expressions() {
2185 use crate::query::plan::{SortKey, SortOp, SortOrder};
2186
2187 let plan = LogicalPlan::new(LogicalOperator::Sort(SortOp {
2188 keys: vec![SortKey {
2189 expression: LogicalExpression::Property {
2190 variable: "missing".to_string(),
2191 property: "name".to_string(),
2192 },
2193 order: SortOrder::Ascending,
2194 nulls: None,
2195 }],
2196 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2197 variable: "n".to_string(),
2198 label: None,
2199 input: None,
2200 })),
2201 }));
2202
2203 let mut binder = Binder::new();
2204 assert!(
2205 binder.bind(&plan).is_err(),
2206 "ORDER BY on undefined variable should fail"
2207 );
2208 }
2209
2210 #[test]
2211 fn test_create_node_adds_variable_before_property_validation() {
2212 use crate::query::plan::CreateNodeOp;
2213
2214 let plan = LogicalPlan::new(LogicalOperator::CreateNode(CreateNodeOp {
2217 variable: "n".to_string(),
2218 labels: vec!["Person".to_string()],
2219 properties: vec![(
2220 "self_ref".to_string(),
2221 LogicalExpression::Property {
2222 variable: "n".to_string(),
2223 property: "name".to_string(),
2224 },
2225 )],
2226 input: None,
2227 }));
2228
2229 let mut binder = Binder::new();
2230 let ctx = binder.bind(&plan).unwrap();
2232 assert!(ctx.get("n").unwrap().is_node);
2233 }
2234
2235 #[test]
2236 fn test_undefined_variable_suggests_similar() {
2237 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2239 items: vec![ReturnItem {
2240 expression: LogicalExpression::Variable("persn".to_string()),
2241 alias: None,
2242 }],
2243 distinct: false,
2244 input: Box::new(LogicalOperator::NodeScan(NodeScanOp {
2245 variable: "person".to_string(),
2246 label: None,
2247 input: None,
2248 })),
2249 }));
2250
2251 let mut binder = Binder::new();
2252 let err = binder.bind(&plan).unwrap_err();
2253 let msg = err.to_string();
2254 assert!(
2256 msg.contains("persn"),
2257 "Error should mention the undefined variable"
2258 );
2259 }
2260
2261 #[test]
2262 fn test_anon_variables_skip_validation() {
2263 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2265 items: vec![ReturnItem {
2266 expression: LogicalExpression::Variable("_anon_42".to_string()),
2267 alias: None,
2268 }],
2269 distinct: false,
2270 input: Box::new(LogicalOperator::Empty),
2271 }));
2272
2273 let mut binder = Binder::new();
2274 let result = binder.bind(&plan);
2275 assert!(
2276 result.is_ok(),
2277 "Anonymous variables should bypass validation"
2278 );
2279 }
2280
2281 #[test]
2282 fn test_map_expression_validates_values() {
2283 let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
2284 items: vec![ReturnItem {
2285 expression: LogicalExpression::Map(vec![(
2286 "key".to_string(),
2287 LogicalExpression::Variable("undefined".to_string()),
2288 )]),
2289 alias: None,
2290 }],
2291 distinct: false,
2292 input: Box::new(LogicalOperator::Empty),
2293 }));
2294
2295 let mut binder = Binder::new();
2296 assert!(
2297 binder.bind(&plan).is_err(),
2298 "Map values should be validated"
2299 );
2300 }
2301
2302 #[test]
2303 fn test_vector_scan_validates_query_vector() {
2304 use crate::query::plan::VectorScanOp;
2305
2306 let plan = LogicalPlan::new(LogicalOperator::VectorScan(VectorScanOp {
2307 variable: "result".to_string(),
2308 index_name: None,
2309 property: "embedding".to_string(),
2310 label: Some("Doc".to_string()),
2311 query_vector: LogicalExpression::Variable("undefined_vec".to_string()),
2312 k: 10,
2313 metric: None,
2314 min_similarity: None,
2315 max_distance: None,
2316 input: None,
2317 }));
2318
2319 let mut binder = Binder::new();
2320 let err = binder.bind(&plan).unwrap_err();
2321 assert!(err.to_string().contains("undefined_vec"));
2322 }
2323}