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