Skip to main content

grafeo_engine/query/
binder.rs

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