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