Skip to main content

grafeo_engine/query/
binder.rs

1//! Semantic validation - catching errors before execution.
2//!
3//! The binder walks the logical plan and validates that everything makes sense:
4//! - Is that variable actually defined? (You can't use `RETURN x` if `x` wasn't matched)
5//! - Does that property access make sense? (Accessing `.age` on an integer fails)
6//! - Are types compatible? (Can't compare a string to an integer)
7//!
8//! Better to catch these errors early than waste time executing a broken query.
9
10use 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
19/// Creates a semantic binding error.
20fn binding_error(message: impl Into<String>) -> Error {
21    Error::Query(QueryError::new(QueryErrorKind::Semantic, message))
22}
23
24/// Creates a semantic binding error with a hint.
25fn 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
29/// Creates an "undefined variable" error with a suggestion if a similar variable exists.
30fn 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/// Information about a bound variable.
45#[derive(Debug, Clone)]
46pub struct VariableInfo {
47    /// The name of the variable.
48    pub name: String,
49    /// The inferred type of the variable.
50    pub data_type: LogicalType,
51    /// Whether this variable is a node.
52    pub is_node: bool,
53    /// Whether this variable is an edge.
54    pub is_edge: bool,
55}
56
57/// Context containing all bound variables and their information.
58#[derive(Debug, Clone, Default)]
59pub struct BindingContext {
60    /// Map from variable name to its info.
61    variables: HashMap<String, VariableInfo>,
62    /// Variables in order of definition.
63    order: Vec<String>,
64}
65
66impl BindingContext {
67    /// Creates a new empty binding context.
68    #[must_use]
69    pub fn new() -> Self {
70        Self {
71            variables: HashMap::new(),
72            order: Vec::new(),
73        }
74    }
75
76    /// Adds a variable to the context.
77    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    /// Looks up a variable by name.
85    #[must_use]
86    pub fn get(&self, name: &str) -> Option<&VariableInfo> {
87        self.variables.get(name)
88    }
89
90    /// Checks if a variable is defined.
91    #[must_use]
92    pub fn contains(&self, name: &str) -> bool {
93        self.variables.contains_key(name)
94    }
95
96    /// Returns all variable names in definition order.
97    #[must_use]
98    pub fn variable_names(&self) -> &[String] {
99        &self.order
100    }
101
102    /// Returns the number of bound variables.
103    #[must_use]
104    pub fn len(&self) -> usize {
105        self.variables.len()
106    }
107
108    /// Returns true if no variables are bound.
109    #[must_use]
110    pub fn is_empty(&self) -> bool {
111        self.variables.is_empty()
112    }
113
114    /// Removes a variable from the context (used for temporary scoping).
115    pub fn remove_variable(&mut self, name: &str) {
116        self.variables.remove(name);
117        self.order.retain(|n| n != name);
118    }
119}
120
121/// Semantic binder for query plans.
122///
123/// The binder walks the logical plan and:
124/// 1. Collects all variable definitions
125/// 2. Validates that all variable references are valid
126/// 3. Infers types where possible
127/// 4. Reports semantic errors
128pub struct Binder {
129    /// The current binding context.
130    context: BindingContext,
131}
132
133impl Binder {
134    /// Creates a new binder.
135    #[must_use]
136    pub fn new() -> Self {
137        Self {
138            context: BindingContext::new(),
139        }
140    }
141
142    /// Binds a logical plan, returning the binding context.
143    ///
144    /// # Errors
145    ///
146    /// Returns an error if semantic validation fails.
147    pub fn bind(&mut self, plan: &LogicalPlan) -> Result<BindingContext> {
148        self.bind_operator(&plan.root)?;
149        Ok(self.context.clone())
150    }
151
152    /// Binds a single logical operator.
153    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                    // Add the projection alias to the context (for WITH clause support)
164                    if let Some(ref alias) = projection.alias {
165                        // Determine the type from the expression
166                        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                // CreateNode introduces a new variable
191                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                // Validate property expressions
204                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                // Validate that source and target variables are defined
230                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                // Add edge variable if present
245                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                // Validate property expressions
257                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                // Validate that the variable to delete is defined
265                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                // Validate that the variable to delete is defined
277                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                // Validate that the variable to update is defined
289                if !self.context.contains(&set.variable) {
290                    return Err(undefined_variable_error(
291                        &set.variable,
292                        &self.context,
293                        " in SET",
294                    ));
295                }
296                // Validate property value expressions
297                for (_, expr) in &set.properties {
298                    self.validate_expression(expr)?;
299                }
300                Ok(())
301            }
302            LogicalOperator::Empty => Ok(()),
303
304            LogicalOperator::Unwind(unwind) => {
305                // First bind the input
306                self.bind_operator(&unwind.input)?;
307                // Validate the expression being unwound
308                self.validate_expression(&unwind.expression)?;
309                // Add the new variable to the context
310                self.context.add_variable(
311                    unwind.variable.clone(),
312                    VariableInfo {
313                        name: unwind.variable.clone(),
314                        data_type: LogicalType::Any, // Unwound elements can be any type
315                        is_node: false,
316                        is_edge: false,
317                    },
318                );
319                // Add ORDINALITY variable if present (1-based index)
320                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                // Add OFFSET variable if present (0-based index)
332                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            // RDF/SPARQL operators
347            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                // First bind the input
383                self.bind_operator(&merge.input)?;
384                // Validate the match property expressions
385                for (_, expr) in &merge.match_properties {
386                    self.validate_expression(expr)?;
387                }
388                // Validate the ON CREATE property expressions
389                for (_, expr) in &merge.on_create {
390                    self.validate_expression(expr)?;
391                }
392                // Validate the ON MATCH property expressions
393                for (_, expr) in &merge.on_match {
394                    self.validate_expression(expr)?;
395                }
396                // MERGE introduces a new variable
397                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                // Validate source and target variables exist
411                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                // MERGE relationship introduces the edge variable
435                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                // Validate that the variable exists
449                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                // Validate that the variable exists
461                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                // First bind the input
472                self.bind_operator(&sp.input)?;
473                // Validate that source and target variables are defined
474                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                // Add the path alias variable to the context
489                self.context.add_variable(
490                    sp.path_alias.clone(),
491                    VariableInfo {
492                        name: sp.path_alias.clone(),
493                        data_type: LogicalType::Any, // Path is a complex type
494                        is_node: false,
495                        is_edge: false,
496                    },
497                );
498                // Also add the path length variable for length(p) calls
499                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            // SPARQL Update operators - these don't require variable binding
512            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                // VectorScan introduces a variable for matched nodes
538                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                // Validate the query vector expression
551                self.validate_expression(&scan.query_vector)?;
552                Ok(())
553            }
554            LogicalOperator::VectorJoin(join) => {
555                // VectorJoin takes input from left side and produces right-side matches
556                self.bind_operator(&join.input)?;
557                // Add right variable for matched nodes
558                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                // Optionally add score variable
568                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                // Validate the query vector expression
580                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                // Register parameter columns as variables (injected by outer Apply)
628                for col in &param_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            // DDL operators don't need binding: they're handled before the binder
642            LogicalOperator::CreatePropertyGraph(_) => Ok(()),
643            // Procedure calls: register yielded columns as variables for downstream operators
644            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    /// Binds a triple scan operator (for RDF/SPARQL).
665    fn bind_triple_scan(&mut self, scan: &TripleScanOp) -> Result<()> {
666        use crate::query::plan::TripleComponent;
667
668        // First bind the input if present
669        if let Some(ref input) = scan.input {
670            self.bind_operator(input)?;
671        }
672
673        // Add variables for subject, predicate, object
674        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, // RDF term
682                    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, // IRI
696                    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, // RDF term
710                    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, // IRI
724                    is_node: false,
725                    is_edge: false,
726                },
727            );
728        }
729
730        Ok(())
731    }
732
733    /// Binds a node scan operator.
734    fn bind_node_scan(&mut self, scan: &NodeScanOp) -> Result<()> {
735        // First bind the input if present
736        if let Some(ref input) = scan.input {
737            self.bind_operator(input)?;
738        }
739
740        // Add the scanned variable to scope
741        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    /// Binds an expand operator.
755    fn bind_expand(&mut self, expand: &ExpandOp) -> Result<()> {
756        // First bind the input
757        self.bind_operator(&expand.input)?;
758
759        // Validate that the source variable is defined
760        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        // Validate that the source is a node
769        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        // Add edge variable if present
779        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        // Add target variable
792        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        // Add path variables for variable-length paths
803        if let Some(ref path_alias) = expand.path_alias {
804            // Register the path variable itself (e.g. p in MATCH p=...)
805            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            // length(p) → _path_length_p
815            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            // nodes(p) → _path_nodes_p
826            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            // edges(p) → _path_edges_p
837            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    /// Binds a filter operator.
853    fn bind_filter(&mut self, filter: &FilterOp) -> Result<()> {
854        // First bind the input
855        self.bind_operator(&filter.input)?;
856
857        // Validate the predicate expression
858        self.validate_expression(&filter.predicate)?;
859
860        Ok(())
861    }
862
863    /// Binds a return operator.
864    fn bind_return(&mut self, ret: &ReturnOp) -> Result<()> {
865        // First bind the input
866        self.bind_operator(&ret.input)?;
867
868        // Validate all return expressions and register aliases
869        // (aliases must be visible to parent Sort for ORDER BY resolution)
870        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    /// Validates a return item.
890    fn validate_return_item(&mut self, item: &ReturnItem) -> Result<()> {
891        self.validate_expression(&item.expression)
892    }
893
894    /// Validates that an expression only references defined variables.
895    fn validate_expression(&mut self, expr: &LogicalExpression) -> Result<()> {
896        match expr {
897            LogicalExpression::Variable(name) => {
898                // "*" is a wildcard marker for RETURN *, expanded by the planner
899                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            // Parameter references are validated externally
973            LogicalExpression::Parameter(_) => Ok(()),
974            // labels(n), type(e), id(n) need the variable to be defined
975            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                // Validate the list expression against the outer context.
985                // The filter and map expressions use the iteration variable
986                // which is locally scoped, so we skip validating them here.
987                self.validate_expression(list_expr)?;
988                Ok(())
989            }
990            LogicalExpression::ListPredicate { list_expr, .. } => {
991                // Validate the list expression against the outer context.
992                // The predicate uses the iteration variable which is locally
993                // scoped, so we skip validating it against the outer context.
994                self.validate_expression(list_expr)?;
995                Ok(())
996            }
997            LogicalExpression::ExistsSubquery(subquery)
998            | LogicalExpression::CountSubquery(subquery) => {
999                // Subqueries have their own binding context
1000                // For now, just validate the structure exists
1001                let _ = subquery; // Would need recursive binding
1002                Ok(())
1003            }
1004            LogicalExpression::PatternComprehension {
1005                subplan,
1006                projection,
1007            } => {
1008                // Bind the subplan to register pattern variables (e.g., `f` in `(p)-[:KNOWS]->(f)`)
1009                self.bind_operator(subplan)?;
1010                // Now validate the projection expression (e.g., `f.name`)
1011                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                // accumulator and variable are locally scoped: inject them
1038                // into context, validate body, then remove
1039                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    /// Infers the type of an expression for use in WITH clause aliasing.
1076    fn infer_expression_type(&self, expr: &LogicalExpression) -> LogicalType {
1077        match expr {
1078            LogicalExpression::Variable(name) => {
1079                // Look up the variable type from context
1080                self.context
1081                    .get(name)
1082                    .map_or(LogicalType::Any, |info| info.data_type.clone())
1083            }
1084            LogicalExpression::Property { .. } => LogicalType::Any, // Properties can be any type
1085            LogicalExpression::Literal(value) => {
1086                // Infer type from literal value
1087                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, // Complex type
1094                    Value::Map(_) => LogicalType::Any,  // Complex type
1095                    Value::Null => LogicalType::Any,
1096                    _ => LogicalType::Any,
1097                }
1098            }
1099            LogicalExpression::Binary { .. } => LogicalType::Any, // Could be bool or numeric
1100            LogicalExpression::Unary { .. } => LogicalType::Any,
1101            LogicalExpression::FunctionCall { name, .. } => {
1102                // Infer based on function name
1103                match name.to_lowercase().as_str() {
1104                    "count" | "sum" | "id" => LogicalType::Int64,
1105                    "avg" => LogicalType::Float64,
1106                    "type" => LogicalType::String,
1107                    // List-returning functions use Any since we don't track element type
1108                    "labels" | "collect" => LogicalType::Any,
1109                    _ => LogicalType::Any,
1110                }
1111            }
1112            LogicalExpression::List(_) => LogicalType::Any, // Complex type
1113            LogicalExpression::Map(_) => LogicalType::Any,  // Complex type
1114            _ => LogicalType::Any,
1115        }
1116    }
1117
1118    /// Binds a join operator.
1119    fn bind_join(&mut self, join: &crate::query::plan::JoinOp) -> Result<()> {
1120        // Bind both sides of the join
1121        self.bind_operator(&join.left)?;
1122        self.bind_operator(&join.right)?;
1123
1124        // Validate join conditions
1125        for condition in &join.conditions {
1126            self.validate_expression(&condition.left)?;
1127            self.validate_expression(&condition.right)?;
1128        }
1129
1130        Ok(())
1131    }
1132
1133    /// Binds an aggregate operator.
1134    fn bind_aggregate(&mut self, agg: &crate::query::plan::AggregateOp) -> Result<()> {
1135        // Bind the input first
1136        self.bind_operator(&agg.input)?;
1137
1138        // Validate group by expressions
1139        for expr in &agg.group_by {
1140            self.validate_expression(expr)?;
1141        }
1142
1143        // Validate aggregate expressions
1144        for agg_expr in &agg.aggregates {
1145            if let Some(ref expr) = agg_expr.expression {
1146                self.validate_expression(expr)?;
1147            }
1148            // Add the alias as a new variable if present
1149            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(), // undefined!
1260                        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        // Tests that expanding from an undefined variable produces a clear error
1334        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(), // not defined!
1344                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        // Tests binding of aggregate functions alongside regular expressions
1375        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        // This should succeed - count(n) with literal is valid
1402        assert!(result.is_ok());
1403    }
1404
1405    #[test]
1406    fn test_bind_nested_property_access() {
1407        // Tests that nested property access on the same variable works
1408        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        // Tests that binary expressions with undefined variables produce errors
1442        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(), // undefined!
1452                        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        // Tests behavior when the same variable is defined twice (via two NodeScans)
1480        // This is typically not allowed or the second shadows the first
1481        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(), // different variable is fine
1497                    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        // Join with different variables should work
1509        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        // Tests that functions with wrong number of arguments are handled
1518        // (behavior depends on whether binder validates arity)
1519        let plan = LogicalPlan::new(LogicalOperator::Return(ReturnOp {
1520            items: vec![ReturnItem {
1521                expression: LogicalExpression::FunctionCall {
1522                    name: "count".to_string(),
1523                    args: vec![], // count() needs an argument
1524                    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        // The binder may or may not catch this - if it passes, execution will fail
1540        // This test documents current behavior
1541        // If binding fails, that's fine; if it passes, execution will handle it
1542        let _ = result; // We're just testing it doesn't panic
1543    }
1544
1545    // --- Mutation operator validation ---
1546
1547    #[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(), // not defined!
1554            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(), // not defined!
1580            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        // Source and target defined, but property references undefined variable
1602        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(), // undefined!
1611                    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    // --- WITH/Project clause ---
1700
1701    #[test]
1702    fn test_project_alias_becomes_available_downstream() {
1703        use crate::query::plan::{ProjectOp, Projection};
1704
1705        // WITH n.name AS person_name RETURN person_name
1706        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    // --- UNWIND ---
1754
1755    #[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    // --- MERGE ---
1788
1789    #[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(), // undefined!
1839                    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    // --- ShortestPath ---
1855
1856    #[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(), // not defined
1867            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    // --- Expression validation edge cases ---
1919
1920    #[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                            // This branch references undefined variable
1944                            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        // labels(x) where x is not defined
2065        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        // type(e) where e is not defined
2081        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        // id(n) where n is not defined
2097        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        // UNWIND [1,2] AS x  -- x is not a node
2118        // MATCH (x)-[:E]->(b)  -- should fail: x isn't a node
2119        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        // CREATE (n:Person {friend: n.name}) - referencing the node being created
2215        // The variable should be available for property expressions (self-reference)
2216        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        // This should succeed because CreateNode adds the variable before validating properties
2231        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        // 'person' is defined, user types 'persn' - should get a suggestion
2238        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        // The error should contain the variable name at minimum
2255        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        // Variables starting with _anon_ are anonymous and should be silently accepted
2264        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}