Skip to main content

lance_csr/
semantic.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4//! Semantic analysis for graph queries
5//!
6//! This module implements the semantic analysis phase of the query pipeline:
7//! Parse → **Semantic Analysis** → Logical Plan → Physical Plan
8//!
9//! Semantic analysis validates the query and enriches the AST with type information.
10
11use crate::ast::*;
12use crate::case_insensitive::CaseInsensitiveLookup;
13use crate::config::GraphConfig;
14use crate::error::{GraphError, Result};
15use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17
18mod types;
19use types::{arithmetic_operator, comparison_operator, ExpressionType};
20
21/// Semantic analyzer - validates and enriches the AST
22pub struct SemanticAnalyzer {
23    config: GraphConfig,
24    variables: HashMap<String, VariableInfo>,
25    /// Variables whose scope a WITH ended; still type-checked at the end.
26    retired: Vec<VariableInfo>,
27    current_scope: ScopeType,
28    catalog: Option<Arc<dyn lance_csr_catalog::GraphSourceCatalog>>,
29    expression_types: HashMap<String, ExpressionType>,
30    call_body: bool,
31}
32
33/// Information about a variable in the query
34#[derive(Debug, Clone)]
35pub struct VariableInfo {
36    pub name: String,
37    pub variable_type: VariableType,
38    pub labels: Vec<String>,
39    pub properties: HashSet<String>,
40    pub defined_in: ScopeType,
41}
42
43/// Type of a variable
44#[derive(Debug, Clone, PartialEq)]
45pub enum VariableType {
46    Node,
47    Relationship,
48    Path,
49    Property,
50}
51
52/// Scope where a variable is defined
53#[derive(Debug, Clone, PartialEq)]
54pub enum ScopeType {
55    Match,
56    Where,
57    With,
58    PostWithWhere,
59    Return,
60    OrderBy,
61}
62
63/// Semantic analysis result with validated and enriched AST
64#[derive(Debug, Clone)]
65pub struct SemanticResult {
66    /// The AST with parameters substituted and validated
67    pub ast: CypherQuery,
68    pub variables: HashMap<String, VariableInfo>,
69    pub errors: Vec<String>,
70    pub warnings: Vec<String>,
71}
72
73impl SemanticAnalyzer {
74    pub fn new(config: GraphConfig) -> Self {
75        Self {
76            config,
77            variables: HashMap::new(),
78            retired: Vec::new(),
79            current_scope: ScopeType::Match,
80            catalog: None,
81            expression_types: HashMap::new(),
82            call_body: false,
83        }
84    }
85
86    /// Use the execution snapshot's schemas for checks that configuration alone cannot make.
87    pub(crate) fn with_catalog(
88        mut self,
89        catalog: Arc<dyn lance_csr_catalog::GraphSourceCatalog>,
90    ) -> Self {
91        self.catalog = Some(catalog);
92        self
93    }
94
95    /// Analyze a Cypher query AST
96    pub fn analyze(
97        &mut self,
98        query: &CypherQuery,
99        parameters: &HashMap<String, serde_json::Value>,
100    ) -> Result<SemanticResult> {
101        // UNION siblings receive the same available CALL imports, never variables
102        // created by the preceding branch. Each branch opts in with its own WITH.
103        let (imports, import_types) = if self.call_body {
104            let available = (self.variables.clone(), self.expression_types.clone());
105            let names = query.call_imports()?;
106            self.variables.retain(|name, _| names.contains(name));
107            self.expression_types.retain(|name, _| names.contains(name));
108            available
109        } else {
110            (HashMap::new(), HashMap::new())
111        };
112        // Clone the query to perform parameter substitution
113        let mut analyzed_query = query.clone();
114
115        // Perform parameter substitution
116        self.substitute_parameters(&mut analyzed_query, parameters)?;
117
118        let mut errors = Vec::new();
119        let mut warnings = Vec::new();
120
121        // Phase 1: Variable discovery in READING clauses (MATCH/UNWIND)
122        self.current_scope = ScopeType::Match;
123        for clause in &analyzed_query.reading_clauses {
124            match clause {
125                ReadingClause::Call(query) => {
126                    if let Err(error) = self.analyze_call(query) {
127                        errors.push(format!("CALL clause error: {error}"));
128                    }
129                }
130                ReadingClause::Match(match_clause) => {
131                    if let Err(e) = self.analyze_match_clause(match_clause) {
132                        errors.push(format!("MATCH clause error: {}", e));
133                    }
134                }
135                ReadingClause::Unwind(unwind_clause) => {
136                    if let Err(e) = self.analyze_unwind_clause(unwind_clause) {
137                        errors.push(format!("UNWIND clause error: {}", e));
138                    }
139                }
140            }
141        }
142
143        // Phase 2: Validate WHERE clause (before WITH)
144        if let Some(where_clause) = &analyzed_query.where_clause {
145            self.current_scope = ScopeType::Where;
146            if let Err(e) = self.analyze_where_clause(where_clause) {
147                errors.push(format!("WHERE clause error: {}", e));
148            }
149        }
150
151        // Phase 3: Validate WITH clause if present
152        if let Some(with_clause) = &analyzed_query.with_clause {
153            self.current_scope = ScopeType::With;
154            if let Err(e) = self.analyze_with_clause(with_clause) {
155                errors.push(format!("WITH clause error: {}", e));
156            }
157        }
158
159        // Phase 4: Variable discovery in post-WITH READING clauses (query chaining)
160        self.current_scope = ScopeType::Match;
161        for clause in &analyzed_query.post_with_reading_clauses {
162            match clause {
163                ReadingClause::Call(query) => {
164                    if let Err(error) = self.analyze_call(query) {
165                        errors.push(format!("CALL clause error: {error}"));
166                    }
167                }
168                ReadingClause::Match(match_clause) => {
169                    if let Err(e) = self.analyze_match_clause(match_clause) {
170                        errors.push(format!("Post-WITH MATCH clause error: {}", e));
171                    }
172                }
173                ReadingClause::Unwind(unwind_clause) => {
174                    if let Err(e) = self.analyze_unwind_clause(unwind_clause) {
175                        errors.push(format!("Post-WITH UNWIND clause error: {}", e));
176                    }
177                }
178            }
179        }
180
181        // Phase 4: Validate post-WITH WHERE clause if present
182        if let Some(post_where) = &analyzed_query.post_with_where_clause {
183            self.current_scope = ScopeType::PostWithWhere;
184            if let Err(e) = self.analyze_where_clause(post_where) {
185                errors.push(format!("Post-WITH WHERE clause error: {}", e));
186            }
187        }
188
189        // Later WITH stages: the same three checks as the first stage, in order.
190        for stage in &analyzed_query.further_with_stages {
191            self.current_scope = ScopeType::With;
192            if let Err(e) = self.analyze_with_clause(&stage.with_clause) {
193                errors.push(format!("WITH clause error: {}", e));
194            }
195            self.current_scope = ScopeType::Match;
196            for clause in &stage.reading_clauses {
197                let result = match clause {
198                    ReadingClause::Call(query) => self.analyze_call(query),
199                    ReadingClause::Match(match_clause) => self.analyze_match_clause(match_clause),
200                    ReadingClause::Unwind(unwind_clause) => {
201                        self.analyze_unwind_clause(unwind_clause)
202                    }
203                };
204                if let Err(e) = result {
205                    errors.push(format!("Post-WITH reading clause error: {}", e));
206                }
207            }
208            if let Some(where_clause) = &stage.where_clause {
209                self.current_scope = ScopeType::PostWithWhere;
210                if let Err(e) = self.analyze_where_clause(where_clause) {
211                    errors.push(format!("Post-WITH WHERE clause error: {}", e));
212                }
213            }
214        }
215
216        // Phase 5: Validate RETURN clause
217        self.current_scope = ScopeType::Return;
218        if let Err(e) = self.analyze_return_clause(&analyzed_query.return_clause) {
219            errors.push(format!("RETURN clause error: {}", e));
220        }
221
222        // Phase 6: Validate ORDER BY clause
223        if let Some(order_by) = &analyzed_query.order_by {
224            self.current_scope = ScopeType::OrderBy;
225            if let Err(e) = self.analyze_order_by_clause(order_by) {
226                errors.push(format!("ORDER BY clause error: {}", e));
227            }
228        }
229
230        if self.call_body
231            && query.return_clause.items.iter().any(|item| {
232                item.alias.is_none() && !matches!(item.expression, ValueExpression::Variable(_))
233            })
234        {
235            errors.push("CALL RETURN expressions need an alias".into());
236        }
237
238        // Phase 7: Schema validation
239        self.validate_schema(&mut warnings);
240
241        // Phase 8: Type checking
242        self.validate_types(&mut errors);
243
244        // openCypher allows UNION or UNION ALL throughout a query, not a mix of both.
245        if let Some(first) = analyzed_query.union_branches.first() {
246            if analyzed_query
247                .union_branches
248                .iter()
249                .any(|branch| branch.all != first.all)
250            {
251                errors.push("Invalid combination of UNION and UNION ALL".into());
252            }
253        }
254
255        for branch in &mut analyzed_query.union_branches {
256            let mut analyzer = SemanticAnalyzer::new(self.config.clone());
257            analyzer.catalog = self.catalog.clone();
258            analyzer.call_body = self.call_body;
259            analyzer.variables = imports.clone();
260            analyzer.expression_types = import_types.clone();
261            let analyzed = analyzer.analyze(&branch.query, parameters)?;
262            branch.query = analyzed.ast;
263            errors.extend(
264                analyzed
265                    .errors
266                    .into_iter()
267                    .map(|e| format!("UNION branch: {e}")),
268            );
269            warnings.extend(analyzed.warnings);
270        }
271
272        Ok(SemanticResult {
273            ast: analyzed_query,
274            variables: self.variables.clone(),
275            errors,
276            warnings,
277        })
278    }
279
280    fn analyze_call(&mut self, query: &CypherQuery) -> Result<()> {
281        let error = |message| GraphError::PlanError {
282            message,
283            location: snafu::Location::new(file!(), line!(), column!()),
284        };
285        let mut inner = Self::new(self.config.clone());
286        inner.catalog = self.catalog.clone();
287        inner.call_body = true;
288        for name in query.call_union_imports()? {
289            let variable = self
290                .variables
291                .get(&name)
292                .ok_or_else(|| error(format!("CALL imports undefined variable '{name}'")))?;
293            inner.variables.insert(name.clone(), variable.clone());
294            if let Some(value_type) = self.expression_types.get(&name) {
295                inner.expression_types.insert(name, value_type.clone());
296            }
297        }
298        // Outer analysis already substituted all user parameters recursively.
299        let analyzed = inner.analyze(query, &HashMap::new())?;
300        if !analyzed.errors.is_empty() {
301            return Err(error(analyzed.errors.join("\n")));
302        }
303        let mut exports = HashSet::new();
304        for item in &query.return_clause.items {
305            let name = match (&item.alias, &item.expression) {
306                (Some(alias), _) => alias.to_lowercase(),
307                (None, ValueExpression::Variable(name)) => name.to_lowercase(),
308                _ => return Err(error("CALL RETURN expressions need an alias".into())),
309            };
310            if self.variables.contains_key(&name) || !exports.insert(name.clone()) {
311                return Err(error(format!(
312                    "CALL export '{name}' collides with an existing variable"
313                )));
314            }
315            let value_type = inner.expression_type(&item.expression);
316            let (variable_type, labels) = match &value_type {
317                ExpressionType::Entity(kind, labels) => (kind.clone(), labels.clone()),
318                _ => (VariableType::Property, vec![]),
319            };
320            self.variables.insert(
321                name.clone(),
322                VariableInfo {
323                    name: name.clone(),
324                    variable_type,
325                    labels,
326                    properties: HashSet::new(),
327                    defined_in: self.current_scope.clone(),
328                },
329            );
330            self.expression_types.insert(name, value_type);
331        }
332        Ok(())
333    }
334
335    /// Analyze MATCH clause and discover variables
336    fn analyze_match_clause(&mut self, match_clause: &MatchClause) -> Result<()> {
337        for pattern in &match_clause.patterns {
338            self.analyze_graph_pattern(pattern)?;
339        }
340        if let Some(where_clause) = &match_clause.where_clause {
341            self.analyze_where_clause(where_clause)?;
342        }
343        Ok(())
344    }
345
346    /// Analyze UNWIND clause and register variables
347    fn analyze_unwind_clause(&mut self, unwind_clause: &UnwindClause) -> Result<()> {
348        self.analyze_value_expression(&unwind_clause.expression)?;
349        let element_type = self.expression_type(&unwind_clause.expression).element();
350
351        // Register the aliased variable (normalize to lowercase for case-insensitive behavior)
352        let var_name = &unwind_clause.alias;
353        let var_name_lower = var_name.to_lowercase();
354        if let Some(existing) = self.variables.get_mut(&var_name_lower) {
355            // Shadowing or redefinition - in Cypher variables can be bound multiple times in some contexts
356            // But here we enforce uniqueness of types mostly.
357            // For now, treat UNWIND alias as a Property type variable.
358            if existing.variable_type != VariableType::Property {
359                return Err(GraphError::PlanError {
360                    message: format!("Variable '{}' redefined with different type", var_name),
361                    location: snafu::Location::new(file!(), line!(), column!()),
362                });
363            }
364        } else {
365            let var_info = VariableInfo {
366                name: var_name.clone(),
367                variable_type: VariableType::Property,
368                labels: vec![],
369                properties: HashSet::new(),
370                defined_in: self.current_scope.clone(),
371            };
372            self.variables.insert(var_name_lower.clone(), var_info);
373        }
374        self.expression_types.insert(var_name_lower, element_type);
375        Ok(())
376    }
377
378    /// Analyze a graph pattern and register variables
379    fn analyze_graph_pattern(&mut self, pattern: &GraphPattern) -> Result<()> {
380        match pattern {
381            GraphPattern::Node(node) => {
382                self.register_node_variable(node)?;
383            }
384            GraphPattern::Path(path) => {
385                // Register start node
386                self.register_node_variable(&path.start_node)?;
387
388                // Register variables in each segment
389                for segment in &path.segments {
390                    // Validate relationship length constraints if present
391                    self.validate_length_range(&segment.relationship)?;
392                    // Register relationship variable if present
393                    if let Some(rel_var) = &segment.relationship.variable {
394                        self.register_relationship_variable(rel_var, &segment.relationship)?;
395                    }
396
397                    // Register end node
398                    self.register_node_variable(&segment.end_node)?;
399                }
400                if let Some(path_var) = path
401                    .variable
402                    .as_ref()
403                    .or_else(|| path.shortest.as_ref().and_then(|s| s.variable.as_ref()))
404                {
405                    self.register_projection_alias(path_var);
406                    if let Some(info) = self.variables.get_mut(&path_var.to_lowercase()) {
407                        info.variable_type = VariableType::Path;
408                    }
409                }
410            }
411        }
412        Ok(())
413    }
414
415    /// Register a node variable
416    fn register_node_variable(&mut self, node: &NodePattern) -> Result<()> {
417        if let Some(var_name) = &node.variable {
418            // Normalize to lowercase for case-insensitive behavior
419            let var_name_lower = var_name.to_lowercase();
420            if let Some(existing) = self.variables.get_mut(&var_name_lower) {
421                // An UNWIND alias over a list of nodes is matched as a node.
422                if existing.variable_type == VariableType::Property {
423                    existing.variable_type = VariableType::Node;
424                }
425                if existing.variable_type != VariableType::Node {
426                    return Err(GraphError::PlanError {
427                        message: format!("Variable '{}' redefined with different type", var_name),
428                        location: snafu::Location::new(file!(), line!(), column!()),
429                    });
430                }
431                for label in &node.labels {
432                    if !existing.labels.contains(label) {
433                        existing.labels.push(label.clone());
434                    }
435                }
436                for prop in node.properties.keys() {
437                    existing.properties.insert(prop.clone());
438                }
439            } else {
440                let var_info = VariableInfo {
441                    name: var_name.clone(),
442                    variable_type: VariableType::Node,
443                    labels: node.labels.clone(),
444                    properties: node.properties.keys().cloned().collect(),
445                    defined_in: self.current_scope.clone(),
446                };
447                self.variables.insert(var_name_lower, var_info);
448            }
449            // MATCH binds an entity even when the name previously came from UNWIND.
450            let name = var_name.to_lowercase();
451            let info = &self.variables[&name];
452            let mut labels = info.labels.clone();
453            if let Some(ExpressionType::Entity(_, inferred)) = self.expression_types.get(&name) {
454                for label in inferred {
455                    if !labels.contains(label) {
456                        labels.push(label.clone());
457                    }
458                }
459            }
460            self.expression_types
461                .insert(name, ExpressionType::Entity(VariableType::Node, labels));
462        }
463        Ok(())
464    }
465
466    /// Register a relationship variable
467    fn register_relationship_variable(
468        &mut self,
469        var_name: &str,
470        rel: &RelationshipPattern,
471    ) -> Result<()> {
472        // Normalize to lowercase for case-insensitive behavior
473        let var_name_lower = var_name.to_lowercase();
474        if let Some(existing) = self.variables.get_mut(&var_name_lower) {
475            if existing.variable_type != VariableType::Relationship {
476                return Err(GraphError::PlanError {
477                    message: format!("Variable '{}' redefined with different type", var_name),
478                    location: snafu::Location::new(file!(), line!(), column!()),
479                });
480            }
481            for rel_type in &rel.types {
482                if !existing.labels.contains(rel_type) {
483                    existing.labels.push(rel_type.clone());
484                }
485            }
486            for prop in rel.properties.keys() {
487                existing.properties.insert(prop.clone());
488            }
489        } else {
490            let var_info = VariableInfo {
491                name: var_name.to_string(),
492                variable_type: VariableType::Relationship,
493                labels: rel.types.clone(), // Relationship types are like labels
494                properties: rel.properties.keys().cloned().collect(),
495                defined_in: self.current_scope.clone(),
496            };
497            self.variables.insert(var_name_lower, var_info);
498        }
499        Ok(())
500    }
501
502    /// Analyze WHERE clause
503    fn analyze_where_clause(&mut self, where_clause: &WhereClause) -> Result<()> {
504        self.analyze_boolean_expression(&where_clause.expression)
505    }
506
507    /// Analyze boolean expression and check variable references
508    fn analyze_boolean_expression(&mut self, expr: &BooleanExpression) -> Result<()> {
509        match expr {
510            BooleanExpression::Comparison {
511                left,
512                operator,
513                right,
514            } => {
515                self.analyze_value_expression(left)?;
516                self.analyze_value_expression(right)?;
517                self.validate_binary(left, comparison_operator(operator), right)?;
518            }
519            BooleanExpression::And(left, right) | BooleanExpression::Or(left, right) => {
520                self.analyze_boolean_expression(left)?;
521                self.analyze_boolean_expression(right)?;
522            }
523            BooleanExpression::Not(inner) => {
524                self.analyze_boolean_expression(inner)?;
525            }
526            BooleanExpression::Exists(prop_ref) => {
527                self.validate_property_reference(prop_ref)?;
528            }
529            BooleanExpression::PatternExists {
530                patterns,
531                where_clause,
532            } => {
533                let before = self.variables.clone();
534                let before_types = self.expression_types.clone();
535                let result = (|| -> Result<()> {
536                    for pattern in patterns {
537                        self.analyze_graph_pattern(pattern)?;
538                    }
539                    if let Some(w) = where_clause {
540                        self.analyze_boolean_expression(w)?;
541                    }
542                    Ok(())
543                })();
544                self.variables = before;
545                self.expression_types = before_types;
546                result?;
547            }
548            BooleanExpression::In { expression, list } => {
549                self.analyze_value_expression(expression)?;
550                for item in list {
551                    self.analyze_value_expression(item)?;
552                }
553            }
554            BooleanExpression::Member { expression, list } => {
555                self.analyze_value_expression(expression)?;
556                self.analyze_value_expression(list)?;
557            }
558            BooleanExpression::Like { expression, .. } => {
559                self.analyze_value_expression(expression)?;
560            }
561            BooleanExpression::ILike { expression, .. } => {
562                self.analyze_value_expression(expression)?;
563            }
564            BooleanExpression::Contains { expression, .. } => {
565                self.analyze_value_expression(expression)?;
566            }
567            BooleanExpression::StartsWith { expression, .. } => {
568                self.analyze_value_expression(expression)?;
569            }
570            BooleanExpression::EndsWith { expression, .. } => {
571                self.analyze_value_expression(expression)?;
572            }
573            BooleanExpression::IsNull(expression) => {
574                self.analyze_value_expression(expression)?;
575            }
576            BooleanExpression::IsNotNull(expression) => {
577                self.analyze_value_expression(expression)?;
578            }
579        }
580        Ok(())
581    }
582
583    /// Analyze value expression and check variable references
584    fn analyze_value_expression(&mut self, expr: &ValueExpression) -> Result<()> {
585        match expr {
586            ValueExpression::Property(prop_ref) => {
587                self.validate_property_reference(prop_ref)?;
588            }
589            ValueExpression::Literal(_) => {
590                // Literals are always valid
591            }
592            ValueExpression::Variable(var) | ValueExpression::LocalVariable(var) => {
593                // Use case-insensitive lookup
594                if !self.variables.contains_key_ci(var) {
595                    return Err(GraphError::PlanError {
596                        message: format!("Undefined variable: '{}'", var),
597                        location: snafu::Location::new(file!(), line!(), column!()),
598                    });
599                }
600            }
601            ValueExpression::ScalarFunction { name, args } => {
602                let function_name = name.to_lowercase();
603                // Validate arity and known functions
604                match function_name.as_str() {
605                    "tolower" | "lower" | "toupper" | "upper" | "abs" | "round" | "floor"
606                    | "ceil" | "sqrt" | "trim" | "ltrim" | "rtrim" | "reverse" | "last"
607                    | "tail" | "keys" | "labels" | "relationships" | "startnode" | "endnode" => {
608                        if args.len() != 1 {
609                            return Err(GraphError::PlanError {
610                                message: format!(
611                                    "{} requires exactly 1 argument, got {}",
612                                    name.to_uppercase(),
613                                    args.len()
614                                ),
615                                location: snafu::Location::new(file!(), line!(), column!()),
616                            });
617                        }
618                    }
619                    "date_part" | "to_epoch_ms" | "length" | "nodes" | "size" | "left"
620                    | "right" | "split" | "replace" | "__index" => {
621                        let want = match function_name.as_str() {
622                            "date_part" | "__index" | "left" | "right" | "split" => 2,
623                            "replace" => 3,
624                            _ => 1,
625                        };
626                        if args.len() != want {
627                            return Err(GraphError::PlanError {
628                                message: format!(
629                                    "{} requires exactly {} argument(s), got {}",
630                                    name.to_uppercase(),
631                                    want,
632                                    args.len()
633                                ),
634                                location: snafu::Location::new(file!(), line!(), column!()),
635                            });
636                        }
637                    }
638                    "substring" | "range" => {
639                        if !(2..=3).contains(&args.len()) {
640                            return Err(GraphError::PlanError {
641                                message: format!(
642                                    "{} requires 2 or 3 arguments, got {}",
643                                    name,
644                                    args.len()
645                                ),
646                                location: snafu::Location::new(file!(), line!(), column!()),
647                            });
648                        }
649                    }
650                    "__list" => {}
651                    "head" | "tointeger" | "tofloat" | "date" | "datetime" => {
652                        if args.len() != 1 {
653                            return Err(GraphError::PlanError {
654                                message: format!("{} requires exactly 1 argument", name),
655                                location: snafu::Location::new(file!(), line!(), column!()),
656                            });
657                        }
658                    }
659                    "duration.inseconds" => {
660                        if args.len() != 2 {
661                            return Err(GraphError::PlanError {
662                                message: "duration.inSeconds requires exactly 2 arguments"
663                                    .to_string(),
664                                location: snafu::Location::new(file!(), line!(), column!()),
665                            });
666                        }
667                    }
668                    "tostring" => {
669                        if args.len() != 1 {
670                            return Err(GraphError::PlanError {
671                                message: "TOSTRING requires exactly 1 argument".to_string(),
672                                location: snafu::Location::new(file!(), line!(), column!()),
673                            });
674                        }
675                    }
676                    "coalesce" => {
677                        if args.is_empty() {
678                            return Err(GraphError::PlanError {
679                                message: "COALESCE requires at least 1 argument".to_string(),
680                                location: snafu::Location::new(file!(), line!(), column!()),
681                            });
682                        }
683                    }
684                    _ => {
685                        // Unknown scalar function - reject early with helpful error
686                        return Err(GraphError::UnsupportedFeature {
687                            feature: format!(
688                                "Cypher function '{}' is not implemented. Supported scalar functions: toLower, lower, toUpper, upper, toString, coalesce, date_part, to_epoch_ms, length, nodes, size, range, head, last, tail, keys, labels, reverse, left, right, substring, replace, split, trim, lTrim, rTrim, abs, round, floor, ceil, sqrt, toInteger, toFloat, date, datetime, duration.inSeconds. Supported aggregate functions: COUNT, SUM, AVG, MIN, MAX, COLLECT.",
689                                name
690                            ),
691                            location: snafu::Location::new(file!(), line!(), column!()),
692                        });
693                    }
694                }
695
696                if function_name == "labels" {
697                    if let ValueExpression::Variable(var) = &args[0] {
698                        if let Some(info) = self.variables.get_ci(var) {
699                            if info
700                                .labels
701                                .iter()
702                                .map(|label| label.to_lowercase())
703                                .collect::<HashSet<_>>()
704                                .len()
705                                > 1
706                            {
707                                return Err(GraphError::UnsupportedFeature {
708                                    feature: "labels() on a mixed-label binding is not supported"
709                                        .to_string(),
710                                    location: snafu::Location::new(file!(), line!(), column!()),
711                                });
712                            }
713                            if info.variable_type == VariableType::Relationship {
714                                return Err(GraphError::PlanError {
715                                    message: "labels() requires a node, not a relationship"
716                                        .to_string(),
717                                    location: snafu::Location::new(file!(), line!(), column!()),
718                                });
719                            }
720                        }
721                    }
722                }
723
724                // Validate arguments recursively
725                for arg in args {
726                    self.analyze_value_expression(arg)?;
727                }
728            }
729            ValueExpression::AggregateFunction {
730                name,
731                args,
732                distinct,
733            } => {
734                let function_name = name.to_lowercase();
735                // Validate known aggregate functions
736                match function_name.as_str() {
737                    "count" | "sum" | "avg" | "min" | "max" | "collect" => {
738                        // DISTINCT is only supported for COUNT and COLLECT
739                        // Other aggregates silently ignore it in execution, so reject early
740                        if *distinct && !matches!(function_name.as_str(), "count" | "collect") {
741                            return Err(GraphError::UnsupportedFeature {
742                                feature: format!(
743                                    "DISTINCT is only supported with COUNT and COLLECT, not {}",
744                                    function_name.to_uppercase()
745                                ),
746                                location: snafu::Location::new(file!(), line!(), column!()),
747                            });
748                        }
749
750                        // COUNT(DISTINCT *) is semantically meaningless
751                        // It would count distinct values of lit(1) which is always 1
752                        if *distinct && function_name == "count" {
753                            if let Some(ValueExpression::Variable(v)) = args.first() {
754                                if v == "*" {
755                                    return Err(GraphError::PlanError {
756                                        message: "COUNT(DISTINCT *) is not supported. \
757                                            Use COUNT(*) to count all rows, or \
758                                            COUNT(DISTINCT property) to count distinct values."
759                                            .to_string(),
760                                        location: snafu::Location::new(file!(), line!(), column!()),
761                                    });
762                                }
763                            }
764                        }
765                        // All aggregates require exactly 1 argument
766                        if args.len() != 1 {
767                            return Err(GraphError::PlanError {
768                                message: format!(
769                                    "{} requires exactly 1 argument, got {}",
770                                    function_name.to_uppercase(),
771                                    args.len()
772                                ),
773                                location: snafu::Location::new(file!(), line!(), column!()),
774                            });
775                        }
776
777                        // Additional validation for SUM, AVG, MIN, MAX: they require properties, not bare variables
778                        // Only COUNT and COLLECT allow bare variables (COUNT(*), COUNT(p), COLLECT(p))
779                        if matches!(function_name.as_str(), "sum" | "avg" | "min" | "max") {
780                            // A scalar alias from an earlier WITH (`sum(messageX)`) is fine.
781                            let entity = |v: &str| {
782                                !matches!(
783                                    self.variables.get_ci(v).map(|i| &i.variable_type),
784                                    Some(VariableType::Property)
785                                )
786                            };
787                            if let Some(ValueExpression::Variable(v)) = args
788                                .first()
789                                .filter(|a| matches!(a, ValueExpression::Variable(v) if entity(v)))
790                            {
791                                return Err(GraphError::PlanError {
792                                    message: format!(
793                                        "{}({}) is invalid - {} requires a property like {}({}.property). You cannot {} a node/entity.",
794                                        function_name.to_uppercase(), v, function_name.to_uppercase(), function_name.to_uppercase(), v, function_name
795                                    ),
796                                    location: snafu::Location::new(file!(), line!(), column!()),
797                                });
798                            }
799                        }
800                    }
801                    _ => {
802                        // Unknown aggregate function - reject early
803                        return Err(GraphError::UnsupportedFeature {
804                            feature: format!(
805                                "Cypher aggregate function '{}' is not implemented. Supported aggregate functions: COUNT, SUM, AVG, MIN, MAX, COLLECT.",
806                                name
807                            ),
808                            location: snafu::Location::new(file!(), line!(), column!()),
809                        });
810                    }
811                }
812
813                // Validate arguments recursively.
814                // Special-case COUNT(*) where '*' isn't a real variable.
815                for arg in args {
816                    if function_name == "count"
817                        && matches!(arg, ValueExpression::Variable(v) if v == "*")
818                    {
819                        continue;
820                    }
821                    self.analyze_value_expression(arg)?;
822                }
823            }
824            ValueExpression::Case { whens, otherwise } => {
825                for (condition, then) in whens {
826                    self.analyze_boolean_expression(condition)?;
827                    self.analyze_value_expression(then)?;
828                }
829                if let Some(otherwise) = otherwise {
830                    self.analyze_value_expression(otherwise)?;
831                }
832            }
833            ValueExpression::Arithmetic {
834                left,
835                operator,
836                right,
837            } => {
838                // Validate arithmetic operands recursively
839                self.analyze_value_expression(left)?;
840                self.analyze_value_expression(right)?;
841                self.validate_binary(left, arithmetic_operator(operator), right)?;
842            }
843            ValueExpression::VectorDistance { left, right, .. } => {
844                // Validate vector distance function arguments
845                self.analyze_value_expression(left)?;
846                self.analyze_value_expression(right)?;
847
848                // Check that at least one argument references a property
849                let has_property = matches!(**left, ValueExpression::Property(_))
850                    || matches!(**right, ValueExpression::Property(_));
851
852                if !has_property {
853                    return Err(GraphError::PlanError {
854                        message: "vector_distance() requires at least one argument to be a property reference".to_string(),
855                        location: snafu::Location::new(file!(), line!(), column!()),
856                    });
857                }
858            }
859            ValueExpression::VectorSimilarity { left, right, .. } => {
860                // Validate vector similarity function arguments
861                self.analyze_value_expression(left)?;
862                self.analyze_value_expression(right)?;
863
864                // Check that at least one argument references a property
865                let has_property = matches!(**left, ValueExpression::Property(_))
866                    || matches!(**right, ValueExpression::Property(_));
867
868                if !has_property {
869                    return Err(GraphError::PlanError {
870                        message: "vector_similarity() requires at least one argument to be a property reference".to_string(),
871                        location: snafu::Location::new(file!(), line!(), column!()),
872                    });
873                }
874            }
875            ValueExpression::VectorLiteral(values) => {
876                // Validate non-empty
877                if values.is_empty() {
878                    return Err(GraphError::PlanError {
879                        message: "Vector literal cannot be empty".to_string(),
880                        location: snafu::Location::new(file!(), line!(), column!()),
881                    });
882                }
883
884                // Note: Very large vectors (>4096 dimensions) may impact performance
885                // but we don't enforce a hard limit here
886            }
887            ValueExpression::Parameter(_) => {
888                // Parameters are always valid (resolved at runtime)
889            }
890            ValueExpression::Boolean(b) => self.analyze_boolean_expression(b)?,
891            ValueExpression::Map(entries) => {
892                for (_, v) in entries {
893                    self.analyze_value_expression(v)?;
894                }
895            }
896            ValueExpression::Field { base, .. } => self.analyze_value_expression(base)?,
897            ValueExpression::PatternComprehension {
898                pattern,
899                filter,
900                map,
901            } => {
902                // New variables of the pattern are local to the comprehension.
903                let before = self.variables.clone();
904                let before_types = self.expression_types.clone();
905                let result = (|| -> Result<()> {
906                    self.analyze_graph_pattern(&GraphPattern::Path((**pattern).clone()))?;
907                    if let Some(b) = filter {
908                        self.analyze_boolean_expression(b)?;
909                    }
910                    self.analyze_value_expression(map)
911                })();
912                self.variables = before;
913                self.expression_types = before_types;
914                result?;
915            }
916            ValueExpression::Reduce {
917                accumulator,
918                initial,
919                variable,
920                list,
921                expression,
922            } => {
923                self.analyze_value_expression(initial)?;
924                self.analyze_value_expression(list)?;
925                if accumulator.eq_ignore_ascii_case(variable) {
926                    return Err(GraphError::PlanError {
927                        message: "reduce accumulator and element variables must differ".to_string(),
928                        location: snafu::Location::new(file!(), line!(), column!()),
929                    });
930                }
931                self.analyze_lambda(
932                    &[
933                        (accumulator, self.expression_type(initial)),
934                        (variable, self.expression_type(list).element()),
935                    ],
936                    expression,
937                    false,
938                )?;
939            }
940            ValueExpression::ListPredicate {
941                variable,
942                list,
943                predicate,
944                ..
945            } => {
946                self.analyze_value_expression(list)?;
947                self.analyze_lambda(
948                    &[(variable, self.expression_type(list).element())],
949                    predicate,
950                    true,
951                )?;
952            }
953            ValueExpression::ListComprehension {
954                variable,
955                list,
956                filter,
957                map,
958            } => {
959                self.analyze_value_expression(list)?;
960                // The variable is local to the comprehension.
961                let before = self.variables.clone();
962                let before_types = self.expression_types.clone();
963                let element_type = self.expression_type(list).element();
964                self.register_projection_alias(variable);
965                self.expression_types
966                    .insert(variable.to_lowercase(), element_type);
967                let result = (|| -> Result<()> {
968                    if let Some(b) = filter {
969                        self.analyze_boolean_expression(b)?;
970                    }
971                    if let Some(m) = map {
972                        self.analyze_value_expression(m)?;
973                    }
974                    Ok(())
975                })();
976                self.variables = before;
977                self.expression_types = before_types;
978                result?;
979            }
980        }
981        Ok(())
982    }
983
984    fn analyze_lambda(
985        &mut self,
986        names: &[(&String, ExpressionType)],
987        expression: &ValueExpression,
988        allow_patterns: bool,
989    ) -> Result<()> {
990        let before = self.variables.clone();
991        let before_types = self.expression_types.clone();
992        for (name, value_type) in names {
993            self.variables.remove(&name.to_lowercase());
994            self.register_projection_alias(name);
995            self.expression_types
996                .insert(name.to_lowercase(), value_type.clone());
997        }
998        let mut unsupported =
999            !allow_patterns && crate::ast::needs_relational_evaluation(expression);
1000        crate::ast::transform_value(&mut expression.clone(), &mut |value| {
1001            unsupported |= matches!(value, ValueExpression::AggregateFunction { .. });
1002        });
1003        let result = if unsupported {
1004            Err(GraphError::UnsupportedFeature {
1005                feature: "graph expressions/comprehensions inside a reduce body, or aggregates inside a lambda body; compute the list before the body".to_string(),
1006                location: snafu::Location::new(file!(), line!(), column!()),
1007            })
1008        } else {
1009            self.analyze_value_expression(expression)
1010        };
1011        self.variables = before;
1012        self.expression_types = before_types;
1013        result
1014    }
1015
1016    fn register_projection_alias(&mut self, alias: &str) {
1017        // Use case-insensitive lookup and store normalized key
1018        if self.variables.contains_key_ci(alias) {
1019            return;
1020        }
1021
1022        let var_info = VariableInfo {
1023            name: alias.to_string(),
1024            variable_type: VariableType::Property,
1025            labels: vec![],
1026            properties: HashSet::new(),
1027            defined_in: self.current_scope.clone(),
1028        };
1029        self.variables.insert(alias.to_lowercase(), var_info);
1030    }
1031
1032    /// Validate property reference
1033    fn validate_property_reference(&self, prop_ref: &PropertyRef) -> Result<()> {
1034        // Use case-insensitive lookup
1035        if !self.variables.contains_key_ci(&prop_ref.variable) {
1036            return Err(GraphError::PlanError {
1037                message: format!("Undefined variable: '{}'", prop_ref.variable),
1038                location: snafu::Location::new(file!(), line!(), column!()),
1039            });
1040        }
1041        Ok(())
1042    }
1043
1044    /// Analyze RETURN clause
1045    fn analyze_return_clause(&mut self, return_clause: &ReturnClause) -> Result<()> {
1046        for item in &return_clause.items {
1047            self.analyze_value_expression(&item.expression)?;
1048            if let Some(alias) = &item.alias {
1049                let value_type = self.expression_type(&item.expression);
1050                self.register_projection_alias(alias);
1051                self.expression_types
1052                    .insert(alias.to_lowercase(), value_type);
1053            }
1054        }
1055        Ok(())
1056    }
1057
1058    /// Analyze WITH clause
1059    fn analyze_with_clause(&mut self, with_clause: &WithClause) -> Result<()> {
1060        // Validate WITH item expressions (similar to RETURN)
1061        for item in &with_clause.items {
1062            self.analyze_value_expression(&item.expression)?;
1063        }
1064        // WITH ends the scope of everything it does not carry; `WITH b AS x` renames b.
1065        let mut kept = HashMap::new();
1066        let mut kept_types = HashMap::new();
1067        for item in &with_clause.items {
1068            let carried = match &item.expression {
1069                ValueExpression::Variable(v) => self.variables.get(&v.to_lowercase()).cloned(),
1070                _ => None,
1071            };
1072            let name = match (&item.alias, &item.expression) {
1073                (Some(alias), _) => alias.clone(),
1074                (None, ValueExpression::Variable(v)) => v.clone(),
1075                // An unaliased `WITH b.name` stays addressable as `b.name`.
1076                (None, ValueExpression::Property(p)) => {
1077                    if let Some(info) = self.variables.get(&p.variable.to_lowercase()) {
1078                        kept.insert(p.variable.to_lowercase(), info.clone());
1079                    }
1080                    if let Some(value_type) = self.expression_types.get(&p.variable.to_lowercase())
1081                    {
1082                        kept_types.insert(p.variable.to_lowercase(), value_type.clone());
1083                    }
1084                    continue;
1085                }
1086                (None, _) => continue,
1087            };
1088            kept_types.insert(name.to_lowercase(), self.expression_type(&item.expression));
1089            let info = carried.unwrap_or_else(|| VariableInfo {
1090                name: name.clone(),
1091                variable_type: VariableType::Property,
1092                labels: vec![],
1093                properties: HashSet::new(),
1094                defined_in: self.current_scope.clone(),
1095            });
1096            kept.insert(name.to_lowercase(), info);
1097        }
1098        self.retired
1099            .extend(std::mem::take(&mut self.variables).into_values());
1100        self.variables = kept;
1101        self.expression_types = kept_types;
1102        // Validate ORDER BY within WITH if present
1103        if let Some(order_by) = &with_clause.order_by {
1104            for item in &order_by.items {
1105                self.analyze_value_expression(&item.expression)?;
1106            }
1107        }
1108        if let Some(where_clause) = &with_clause.where_clause {
1109            self.analyze_where_clause(where_clause)?;
1110        }
1111        Ok(())
1112    }
1113
1114    /// Analyze ORDER BY clause
1115    fn analyze_order_by_clause(&mut self, order_by: &OrderByClause) -> Result<()> {
1116        for item in &order_by.items {
1117            self.analyze_value_expression(&item.expression)?;
1118        }
1119        Ok(())
1120    }
1121
1122    /// Validate schema references against configuration
1123    fn validate_schema(&self, warnings: &mut Vec<String>) {
1124        for var_info in self.variables.values() {
1125            match var_info.variable_type {
1126                VariableType::Node => {
1127                    for label in &var_info.labels {
1128                        if self.config.get_node_mapping(label).is_none() {
1129                            warnings.push(format!("Node label '{}' not found in schema", label));
1130                        }
1131                    }
1132                }
1133                VariableType::Relationship => {
1134                    for rel_type in &var_info.labels {
1135                        if self.config.get_relationship_mapping(rel_type).is_none() {
1136                            warnings.push(format!(
1137                                "Relationship type '{}' not found in schema",
1138                                rel_type
1139                            ));
1140                        }
1141                    }
1142                }
1143                _ => {}
1144            }
1145        }
1146    }
1147
1148    /// Validate types and operations
1149    fn validate_types(&self, errors: &mut Vec<String>) {
1150        // Expression operations are checked during traversal, while their scope is available.
1151
1152        // Check that properties referenced in patterns exist in schema when property fields are defined
1153        for var_info in self.variables.values().chain(&self.retired) {
1154            match var_info.variable_type {
1155                VariableType::Node => {
1156                    // Collect property_fields from all known label mappings that specify properties
1157                    let mut label_property_sets: Vec<&[String]> = Vec::new();
1158                    for label in &var_info.labels {
1159                        if let Some(mapping) = self.config.get_node_mapping(label) {
1160                            if !mapping.property_fields.is_empty() {
1161                                label_property_sets.push(&mapping.property_fields);
1162                            }
1163                        }
1164                    }
1165
1166                    if !label_property_sets.is_empty() {
1167                        'prop: for prop in &var_info.properties {
1168                            // Property is valid if present in at least one label's property_fields
1169                            // Use case-insensitive comparison
1170                            let prop_lower = prop.to_lowercase();
1171                            for fields in &label_property_sets {
1172                                if fields.iter().any(|f| f.to_lowercase() == prop_lower) {
1173                                    continue 'prop;
1174                                }
1175                            }
1176                            errors.push(format!(
1177                                "Property '{}' not found on labels {:?}",
1178                                prop, var_info.labels
1179                            ));
1180                        }
1181                    }
1182                }
1183                VariableType::Relationship => {
1184                    // Collect property_fields from all known relationship mappings that specify properties
1185                    let mut rel_property_sets: Vec<&[String]> = Vec::new();
1186                    for rel_type in &var_info.labels {
1187                        if let Some(mapping) = self.config.get_relationship_mapping(rel_type) {
1188                            if !mapping.property_fields.is_empty() {
1189                                rel_property_sets.push(&mapping.property_fields);
1190                            }
1191                        }
1192                    }
1193
1194                    if !rel_property_sets.is_empty() {
1195                        'prop_rel: for prop in &var_info.properties {
1196                            // Use case-insensitive comparison for relationship properties
1197                            let prop_lower = prop.to_lowercase();
1198                            for fields in &rel_property_sets {
1199                                if fields.iter().any(|f| f.to_lowercase() == prop_lower) {
1200                                    continue 'prop_rel;
1201                                }
1202                            }
1203                            errors.push(format!(
1204                                "Property '{}' not found on relationship types {:?}",
1205                                prop, var_info.labels
1206                            ));
1207                        }
1208                    }
1209                }
1210                _ => {}
1211            }
1212        }
1213    }
1214}
1215
1216impl SemanticAnalyzer {
1217    fn validate_length_range(&self, rel: &RelationshipPattern) -> Result<()> {
1218        if let Some(len) = &rel.length {
1219            if let (Some(min), Some(max)) = (len.min, len.max) {
1220                if min > max {
1221                    return Err(GraphError::PlanError {
1222                        message: "Invalid path length range: min > max".to_string(),
1223                        location: snafu::Location::new(file!(), line!(), column!()),
1224                    });
1225                }
1226            }
1227        }
1228        Ok(())
1229    }
1230    /// Substitute parameters with literal values in the AST
1231    fn substitute_parameters(
1232        &self,
1233        query: &mut CypherQuery,
1234        parameters: &HashMap<String, serde_json::Value>,
1235    ) -> Result<()> {
1236        crate::parameter_substitution::substitute_parameters(query, parameters)
1237    }
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242    use super::*;
1243    use crate::ast::{
1244        ArithmeticOperator, BooleanExpression, CypherQuery, GraphPattern, LengthRange, MatchClause,
1245        NodePattern, PathPattern, PathSegment, PropertyRef, PropertyValue, RelationshipDirection,
1246        RelationshipPattern, ReturnClause, ReturnItem, ValueExpression, WhereClause,
1247    };
1248    use crate::config::{GraphConfig, NodeMapping};
1249
1250    fn test_config() -> GraphConfig {
1251        GraphConfig::builder()
1252            .node_table("Person", "id")
1253            .node_table("Employee", "id")
1254            .node_table("Company", "id")
1255            .rel_table("KNOWS", "Person", "Person", "src_id", "dst_id")
1256            .build()
1257            .unwrap()
1258    }
1259
1260    #[test]
1261    fn union_branches_do_not_inherit_variables() {
1262        let query = crate::parser::parse_cypher_query(
1263            "MATCH (p:Person) RETURN p.id AS id UNION RETURN p.id AS id",
1264        )
1265        .unwrap();
1266        let result = SemanticAnalyzer::new(test_config())
1267            .analyze(&query, &HashMap::new())
1268            .unwrap();
1269        assert!(result.errors.iter().any(|e| e.contains("UNION branch")));
1270    }
1271
1272    // Helper: analyze a query that only has a single RETURN expression
1273    fn analyze_return_expr(expr: ValueExpression) -> Result<SemanticResult> {
1274        let query = CypherQuery {
1275            reading_clauses: vec![],
1276            where_clause: None,
1277            with_clause: None,
1278            post_with_reading_clauses: vec![],
1279            post_with_where_clause: None,
1280            further_with_stages: vec![],
1281            union_branches: Vec::new(),
1282            return_clause: ReturnClause {
1283                distinct: false,
1284                items: vec![ReturnItem {
1285                    expression: expr,
1286                    alias: None,
1287                }],
1288            },
1289            limit: None,
1290            order_by: None,
1291            skip: None,
1292        };
1293        let mut analyzer = SemanticAnalyzer::new(test_config());
1294        analyzer.analyze(&query, &HashMap::new())
1295    }
1296
1297    // Helper: analyze a query with a single MATCH (var:label) and a RETURN expression
1298    fn analyze_return_with_match(
1299        var: &str,
1300        label: &str,
1301        expr: ValueExpression,
1302    ) -> Result<SemanticResult> {
1303        let node = NodePattern::new(Some(var.to_string())).with_label(label);
1304        let query = CypherQuery {
1305            reading_clauses: vec![ReadingClause::Match(MatchClause {
1306                patterns: vec![GraphPattern::Node(node)],
1307                optional: false,
1308                where_clause: None,
1309            })],
1310            where_clause: None,
1311            with_clause: None,
1312            post_with_reading_clauses: vec![],
1313            post_with_where_clause: None,
1314            further_with_stages: vec![],
1315            union_branches: Vec::new(),
1316            return_clause: ReturnClause {
1317                distinct: false,
1318                items: vec![ReturnItem {
1319                    expression: expr,
1320                    alias: None,
1321                }],
1322            },
1323            limit: None,
1324            order_by: None,
1325            skip: None,
1326        };
1327        let mut analyzer = SemanticAnalyzer::new(test_config());
1328        analyzer.analyze(&query, &HashMap::new())
1329    }
1330
1331    #[test]
1332    fn test_merge_node_variable_metadata() {
1333        // MATCH (n:Person {age: 30}), (n:Employee {dept: "X"})
1334        let node1 = NodePattern::new(Some("n".to_string()))
1335            .with_label("Person")
1336            .with_property("age", PropertyValue::Integer(30));
1337        let node2 = NodePattern::new(Some("n".to_string()))
1338            .with_label("Employee")
1339            .with_property("dept", PropertyValue::String("X".to_string()));
1340
1341        let query = CypherQuery {
1342            reading_clauses: vec![ReadingClause::Match(MatchClause {
1343                patterns: vec![GraphPattern::Node(node1), GraphPattern::Node(node2)],
1344                optional: false,
1345                where_clause: None,
1346            })],
1347            where_clause: None,
1348            with_clause: None,
1349            post_with_reading_clauses: vec![],
1350            post_with_where_clause: None,
1351            further_with_stages: vec![],
1352            union_branches: Vec::new(),
1353            return_clause: ReturnClause {
1354                distinct: false,
1355                items: vec![],
1356            },
1357            limit: None,
1358            order_by: None,
1359            skip: None,
1360        };
1361
1362        let mut analyzer = SemanticAnalyzer::new(test_config());
1363        let result = analyzer.analyze(&query, &HashMap::new()).unwrap();
1364        assert!(result.errors.is_empty());
1365        let n = result.variables.get("n").expect("variable n present");
1366        // Labels merged
1367        assert!(n.labels.contains(&"Person".to_string()));
1368        assert!(n.labels.contains(&"Employee".to_string()));
1369        // Properties unioned
1370        assert!(n.properties.contains("age"));
1371        assert!(n.properties.contains("dept"));
1372    }
1373
1374    #[test]
1375    fn test_invalid_length_range_collects_error() {
1376        let start = NodePattern::new(Some("a".to_string())).with_label("Person");
1377        let end = NodePattern::new(Some("b".to_string())).with_label("Person");
1378        let mut rel = RelationshipPattern::new(RelationshipDirection::Outgoing)
1379            .with_variable("r")
1380            .with_type("KNOWS");
1381        rel.length = Some(LengthRange {
1382            min: Some(3),
1383            max: Some(2),
1384        });
1385
1386        let path = PathPattern {
1387            start_node: start,
1388            segments: vec![PathSegment {
1389                relationship: rel,
1390                end_node: end,
1391            }],
1392            shortest: None,
1393            variable: None,
1394        };
1395
1396        let query = CypherQuery {
1397            reading_clauses: vec![ReadingClause::Match(MatchClause {
1398                patterns: vec![GraphPattern::Path(path)],
1399                optional: false,
1400                where_clause: None,
1401            })],
1402            where_clause: None,
1403            with_clause: None,
1404            post_with_reading_clauses: vec![],
1405            post_with_where_clause: None,
1406            further_with_stages: vec![],
1407            union_branches: Vec::new(),
1408            return_clause: ReturnClause {
1409                distinct: false,
1410                items: vec![],
1411            },
1412            limit: None,
1413            order_by: None,
1414            skip: None,
1415        };
1416
1417        let mut analyzer = SemanticAnalyzer::new(test_config());
1418        let result = analyzer.analyze(&query, &HashMap::new()).unwrap();
1419        assert!(result
1420            .errors
1421            .iter()
1422            .any(|e| e.contains("Invalid path length range")));
1423    }
1424
1425    #[test]
1426    fn test_undefined_variable_in_where() {
1427        // MATCH (n:Person) WHERE EXISTS(m.name)
1428        let node = NodePattern::new(Some("n".to_string())).with_label("Person");
1429        let where_clause = WhereClause {
1430            expression: BooleanExpression::Exists(PropertyRef::new("m", "name")),
1431        };
1432        let query = CypherQuery {
1433            reading_clauses: vec![ReadingClause::Match(MatchClause {
1434                patterns: vec![GraphPattern::Node(node)],
1435                optional: false,
1436                where_clause: None,
1437            })],
1438            where_clause: Some(where_clause),
1439            with_clause: None,
1440            post_with_reading_clauses: vec![],
1441            post_with_where_clause: None,
1442            further_with_stages: vec![],
1443            union_branches: Vec::new(),
1444            return_clause: ReturnClause {
1445                distinct: false,
1446                items: vec![],
1447            },
1448            limit: None,
1449            order_by: None,
1450            skip: None,
1451        };
1452
1453        let mut analyzer = SemanticAnalyzer::new(test_config());
1454        let result = analyzer.analyze(&query, &HashMap::new()).unwrap();
1455        assert!(result
1456            .errors
1457            .iter()
1458            .any(|e| e.contains("Undefined variable: 'm'")));
1459    }
1460
1461    #[test]
1462    fn test_variable_redefinition_between_node_and_relationship() {
1463        // MATCH (n:Person)-[n:KNOWS]->(m:Person)
1464        let start = NodePattern::new(Some("n".to_string())).with_label("Person");
1465        let end = NodePattern::new(Some("m".to_string())).with_label("Person");
1466        let rel = RelationshipPattern::new(RelationshipDirection::Outgoing)
1467            .with_variable("n")
1468            .with_type("KNOWS");
1469
1470        let path = PathPattern {
1471            start_node: start,
1472            segments: vec![PathSegment {
1473                relationship: rel,
1474                end_node: end,
1475            }],
1476            shortest: None,
1477            variable: None,
1478        };
1479
1480        let query = CypherQuery {
1481            reading_clauses: vec![ReadingClause::Match(MatchClause {
1482                patterns: vec![GraphPattern::Path(path)],
1483                optional: false,
1484                where_clause: None,
1485            })],
1486            where_clause: None,
1487            with_clause: None,
1488            post_with_reading_clauses: vec![],
1489            post_with_where_clause: None,
1490            further_with_stages: vec![],
1491            union_branches: Vec::new(),
1492            return_clause: ReturnClause {
1493                distinct: false,
1494                items: vec![],
1495            },
1496            limit: None,
1497            order_by: None,
1498            skip: None,
1499        };
1500
1501        let mut analyzer = SemanticAnalyzer::new(test_config());
1502        let result = analyzer.analyze(&query, &HashMap::new()).unwrap();
1503        assert!(result
1504            .errors
1505            .iter()
1506            .any(|e| e.contains("redefined with different type")));
1507    }
1508
1509    #[test]
1510    fn test_unknown_node_label_warns() {
1511        // MATCH (x:Unknown)
1512        let node = NodePattern::new(Some("x".to_string())).with_label("Unknown");
1513        let query = CypherQuery {
1514            reading_clauses: vec![ReadingClause::Match(MatchClause {
1515                patterns: vec![GraphPattern::Node(node)],
1516                optional: false,
1517                where_clause: None,
1518            })],
1519            post_with_reading_clauses: vec![],
1520            post_with_where_clause: None,
1521            further_with_stages: vec![],
1522            union_branches: Vec::new(),
1523            where_clause: None,
1524            with_clause: None,
1525            return_clause: ReturnClause {
1526                distinct: false,
1527                items: vec![],
1528            },
1529            limit: None,
1530            order_by: None,
1531            skip: None,
1532        };
1533
1534        let mut analyzer = SemanticAnalyzer::new(test_config());
1535        let result = analyzer.analyze(&query, &HashMap::new()).unwrap();
1536        assert!(result
1537            .warnings
1538            .iter()
1539            .any(|w| w.contains("Node label 'Unknown' not found in schema")));
1540    }
1541
1542    #[test]
1543    fn test_property_not_in_schema_reports_error() {
1544        // Configure Person with allowed property 'name' only
1545        let custom_config = GraphConfig::builder()
1546            .with_node_mapping(
1547                NodeMapping::new("Person", "id").with_properties(vec!["name".to_string()]),
1548            )
1549            .rel_table("KNOWS", "Person", "Person", "src_id", "dst_id")
1550            .build()
1551            .unwrap();
1552
1553        // MATCH (n:Person {age: 30})
1554        let node = NodePattern::new(Some("n".to_string()))
1555            .with_label("Person")
1556            .with_property("age", PropertyValue::Integer(30));
1557        let query = CypherQuery {
1558            reading_clauses: vec![ReadingClause::Match(MatchClause {
1559                patterns: vec![GraphPattern::Node(node)],
1560                optional: false,
1561                where_clause: None,
1562            })],
1563            post_with_reading_clauses: vec![],
1564            post_with_where_clause: None,
1565            further_with_stages: vec![],
1566            union_branches: Vec::new(),
1567            where_clause: None,
1568            with_clause: None,
1569            return_clause: ReturnClause {
1570                distinct: false,
1571                items: vec![],
1572            },
1573            limit: None,
1574            order_by: None,
1575            skip: None,
1576        };
1577
1578        let mut analyzer = SemanticAnalyzer::new(custom_config);
1579        let result = analyzer.analyze(&query, &HashMap::new()).unwrap();
1580        assert!(result
1581            .errors
1582            .iter()
1583            .any(|e| e.contains("Property 'age' not found on labels [\"Person\"]")));
1584    }
1585
1586    #[test]
1587    fn test_valid_length_range_ok() {
1588        let start = NodePattern::new(Some("a".to_string())).with_label("Person");
1589        let end = NodePattern::new(Some("b".to_string())).with_label("Person");
1590        let mut rel = RelationshipPattern::new(RelationshipDirection::Outgoing)
1591            .with_variable("r")
1592            .with_type("KNOWS");
1593        rel.length = Some(LengthRange {
1594            min: Some(2),
1595            max: Some(3),
1596        });
1597
1598        let path = PathPattern {
1599            start_node: start,
1600            segments: vec![PathSegment {
1601                relationship: rel,
1602                end_node: end,
1603            }],
1604            shortest: None,
1605            variable: None,
1606        };
1607
1608        let query = CypherQuery {
1609            reading_clauses: vec![ReadingClause::Match(MatchClause {
1610                patterns: vec![GraphPattern::Path(path)],
1611                optional: false,
1612                where_clause: None,
1613            })],
1614            post_with_reading_clauses: vec![],
1615            post_with_where_clause: None,
1616            further_with_stages: vec![],
1617            union_branches: Vec::new(),
1618            where_clause: None,
1619            with_clause: None,
1620            return_clause: ReturnClause {
1621                distinct: false,
1622                items: vec![],
1623            },
1624            limit: None,
1625            order_by: None,
1626            skip: None,
1627        };
1628
1629        let mut analyzer = SemanticAnalyzer::new(test_config());
1630        let result = analyzer.analyze(&query, &HashMap::new()).unwrap();
1631        assert!(result
1632            .errors
1633            .iter()
1634            .all(|e| !e.contains("Invalid path length range")));
1635    }
1636
1637    #[test]
1638    fn test_relationship_variable_metadata_merge_across_segments() {
1639        // Path with two segments sharing the same relationship variable 'r'
1640        // (a:Person)-[r:KNOWS {since: 2020}]->(b:Person)-[r:FRIEND {level: 1}]->(c:Person)
1641        let start = NodePattern::new(Some("a".to_string())).with_label("Person");
1642        let mid = NodePattern::new(Some("b".to_string())).with_label("Person");
1643        let end = NodePattern::new(Some("c".to_string())).with_label("Person");
1644
1645        let mut rel1 = RelationshipPattern::new(RelationshipDirection::Outgoing)
1646            .with_variable("r")
1647            .with_type("KNOWS")
1648            .with_property("since", PropertyValue::Integer(2020));
1649        rel1.length = None;
1650
1651        let mut rel2 = RelationshipPattern::new(RelationshipDirection::Outgoing)
1652            .with_variable("r")
1653            .with_type("FRIEND")
1654            .with_property("level", PropertyValue::Integer(1));
1655        rel2.length = None;
1656
1657        let path = PathPattern {
1658            start_node: start,
1659            segments: vec![
1660                PathSegment {
1661                    relationship: rel1,
1662                    end_node: mid,
1663                },
1664                PathSegment {
1665                    relationship: rel2,
1666                    end_node: end,
1667                },
1668            ],
1669            shortest: None,
1670            variable: None,
1671        };
1672
1673        // Custom config that knows both relationship types to avoid warnings muddying the assertion
1674        let custom_config = GraphConfig::builder()
1675            .node_table("Person", "id")
1676            .rel_table("KNOWS", "Person", "Person", "src_id", "dst_id")
1677            .rel_table("FRIEND", "Person", "Person", "src_id", "dst_id")
1678            .build()
1679            .unwrap();
1680
1681        let query = CypherQuery {
1682            reading_clauses: vec![ReadingClause::Match(MatchClause {
1683                patterns: vec![GraphPattern::Path(path)],
1684                optional: false,
1685                where_clause: None,
1686            })],
1687            post_with_reading_clauses: vec![],
1688            post_with_where_clause: None,
1689            further_with_stages: vec![],
1690            union_branches: Vec::new(),
1691            where_clause: None,
1692            with_clause: None,
1693            return_clause: ReturnClause {
1694                distinct: false,
1695                items: vec![],
1696            },
1697            limit: None,
1698            order_by: None,
1699            skip: None,
1700        };
1701
1702        let mut analyzer = SemanticAnalyzer::new(custom_config);
1703        let result = analyzer.analyze(&query, &HashMap::new()).unwrap();
1704        let r = result.variables.get("r").expect("variable r present");
1705        // Types merged
1706        assert!(r.labels.contains(&"KNOWS".to_string()));
1707        assert!(r.labels.contains(&"FRIEND".to_string()));
1708        // Properties unioned
1709        assert!(r.properties.contains("since"));
1710        assert!(r.properties.contains("level"));
1711    }
1712
1713    #[test]
1714    fn test_parameter_substitution() {
1715        // MATCH (n:Person) WHERE n.age > $min_age RETURN n
1716        let node = NodePattern::new(Some("n".to_string())).with_label("Person");
1717        let where_clause = WhereClause {
1718            expression: BooleanExpression::Comparison {
1719                left: ValueExpression::Property(PropertyRef::new("n", "age")),
1720                operator: crate::ast::ComparisonOperator::GreaterThan,
1721                right: ValueExpression::Parameter("min_age".to_string()),
1722            },
1723        };
1724        let query = CypherQuery {
1725            reading_clauses: vec![ReadingClause::Match(MatchClause {
1726                patterns: vec![GraphPattern::Node(node)],
1727                optional: false,
1728                where_clause: None,
1729            })],
1730            where_clause: Some(where_clause),
1731            with_clause: None,
1732            post_with_reading_clauses: vec![],
1733            post_with_where_clause: None,
1734            further_with_stages: vec![],
1735            union_branches: Vec::new(),
1736            return_clause: ReturnClause {
1737                distinct: false,
1738                items: vec![ReturnItem {
1739                    expression: ValueExpression::Variable("n".to_string()),
1740                    alias: None,
1741                }],
1742            },
1743            limit: None,
1744            order_by: None,
1745            skip: None,
1746        };
1747
1748        let mut parameters = HashMap::new();
1749        parameters.insert("min_age".to_string(), serde_json::json!(18));
1750
1751        let mut analyzer = SemanticAnalyzer::new(test_config());
1752        let result = analyzer
1753            .analyze(&query, &parameters)
1754            .expect("Analysis failed");
1755
1756        // Verify substitution in AST
1757        let where_clause = result.ast.where_clause.as_ref().unwrap();
1758        match &where_clause.expression {
1759            BooleanExpression::Comparison { right, .. } => match right {
1760                ValueExpression::Literal(PropertyValue::Integer(val)) => {
1761                    assert_eq!(*val, 18);
1762                }
1763                _ => panic!("Expected Integer literal, got {:?}", right),
1764            },
1765            _ => panic!("Expected Comparison expression"),
1766        }
1767    }
1768
1769    #[test]
1770    fn test_function_argument_undefined_variable_in_return() {
1771        // RETURN toUpper(m.name)
1772        let expr = ValueExpression::ScalarFunction {
1773            name: "toUpper".to_string(),
1774            args: vec![ValueExpression::Property(PropertyRef::new("m", "name"))],
1775        };
1776        let result = analyze_return_expr(expr).unwrap();
1777        assert!(result
1778            .errors
1779            .iter()
1780            .any(|e| e.contains("Undefined variable: 'm'")));
1781    }
1782
1783    #[test]
1784    fn test_function_argument_valid_variable_ok() {
1785        // MATCH (n:Person) RETURN toUpper(n.name)
1786        let expr = ValueExpression::ScalarFunction {
1787            name: "toUpper".to_string(),
1788            args: vec![ValueExpression::Property(PropertyRef::new("n", "name"))],
1789        };
1790        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1791        assert!(result.errors.is_empty());
1792    }
1793
1794    #[test]
1795    fn test_arithmetic_with_undefined_variable_in_return() {
1796        // RETURN x + 1
1797        let expr = ValueExpression::Arithmetic {
1798            left: Box::new(ValueExpression::Variable("x".to_string())),
1799            operator: ArithmeticOperator::Add,
1800            right: Box::new(ValueExpression::Literal(PropertyValue::Integer(1))),
1801        };
1802        let result = analyze_return_expr(expr).unwrap();
1803        assert!(result
1804            .errors
1805            .iter()
1806            .any(|e| e.contains("Undefined variable: 'x'")));
1807    }
1808
1809    #[test]
1810    fn test_arithmetic_with_defined_property_ok() {
1811        let expr = ValueExpression::Arithmetic {
1812            left: Box::new(ValueExpression::Literal(PropertyValue::Integer(1))),
1813            operator: ArithmeticOperator::Add,
1814            right: Box::new(ValueExpression::Property(PropertyRef::new("n", "age"))),
1815        };
1816        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1817        // Should not report undefined variable 'n'
1818        assert!(result
1819            .errors
1820            .iter()
1821            .all(|e| !e.contains("Undefined variable: 'n'")));
1822    }
1823
1824    #[test]
1825    fn test_count_with_multiple_args_fails_validation() {
1826        // COUNT(n.age, n.name) should fail semantic validation
1827        let expr = ValueExpression::AggregateFunction {
1828            name: "count".to_string(),
1829            args: vec![
1830                ValueExpression::Property(PropertyRef::new("n", "age")),
1831                ValueExpression::Property(PropertyRef::new("n", "name")),
1832            ],
1833            distinct: false,
1834        };
1835        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1836        assert!(
1837            result
1838                .errors
1839                .iter()
1840                .any(|e| e.contains("COUNT requires exactly 1 argument")),
1841            "Expected error about COUNT arity, got: {:?}",
1842            result.errors
1843        );
1844    }
1845
1846    #[test]
1847    fn test_count_with_zero_args_fails_validation() {
1848        // COUNT() with no arguments should fail
1849        let expr = ValueExpression::AggregateFunction {
1850            name: "count".to_string(),
1851            args: vec![],
1852            distinct: false,
1853        };
1854        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1855        assert!(
1856            result
1857                .errors
1858                .iter()
1859                .any(|e| e.contains("COUNT requires exactly 1 argument")),
1860            "Expected error about COUNT arity, got: {:?}",
1861            result.errors
1862        );
1863    }
1864
1865    #[test]
1866    fn test_count_with_one_arg_passes_validation() {
1867        // COUNT(n.age) should pass validation
1868        let expr = ValueExpression::AggregateFunction {
1869            name: "count".to_string(),
1870            args: vec![ValueExpression::Property(PropertyRef::new("n", "age"))],
1871            distinct: false,
1872        };
1873        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1874        assert!(
1875            result
1876                .errors
1877                .iter()
1878                .all(|e| !e.contains("COUNT requires exactly 1 argument")),
1879            "COUNT with 1 arg should not produce arity error, got: {:?}",
1880            result.errors
1881        );
1882    }
1883
1884    #[test]
1885    fn test_count_star_passes_validation() {
1886        // COUNT(*) should be allowed (special-cased in semantic analysis)
1887        let expr = ValueExpression::AggregateFunction {
1888            name: "count".to_string(),
1889            args: vec![ValueExpression::Variable("*".to_string())],
1890            distinct: false,
1891        };
1892        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1893        assert!(
1894            result.errors.is_empty(),
1895            "Expected COUNT(*) to pass semantic validation, got: {:?}",
1896            result.errors
1897        );
1898    }
1899
1900    #[test]
1901    fn test_unimplemented_scalar_function_fails_validation() {
1902        let expr = ValueExpression::ScalarFunction {
1903            name: "not_a_function".to_string(),
1904            args: vec![ValueExpression::Property(PropertyRef::new("n", "name"))],
1905        };
1906        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1907        // ScalarFunction with unknown name collects an error
1908        assert!(
1909            result
1910                .errors
1911                .iter()
1912                .any(|e| e.to_lowercase().contains("not implemented")),
1913            "Expected semantic validation to reject unimplemented function, got: {:?}",
1914            result.errors
1915        );
1916    }
1917
1918    #[test]
1919    fn test_sum_with_variable_fails_validation() {
1920        let expr = ValueExpression::AggregateFunction {
1921            name: "sum".to_string(),
1922            args: vec![ValueExpression::Variable("n".to_string())],
1923            distinct: false,
1924        };
1925        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1926        assert!(
1927            !result.errors.is_empty(),
1928            "Expected SUM(variable) to produce validation errors"
1929        );
1930        let has_sum_error = result
1931            .errors
1932            .iter()
1933            .any(|e| e.contains("SUM(n) is invalid") && e.contains("requires a property"));
1934        assert!(
1935            has_sum_error,
1936            "Expected error about SUM requiring property, got: {:?}",
1937            result.errors
1938        );
1939    }
1940
1941    #[test]
1942    fn test_avg_with_variable_fails_validation() {
1943        let expr = ValueExpression::AggregateFunction {
1944            name: "avg".to_string(),
1945            args: vec![ValueExpression::Variable("n".to_string())],
1946            distinct: false,
1947        };
1948        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1949        assert!(
1950            !result.errors.is_empty(),
1951            "Expected AVG(variable) to produce validation errors"
1952        );
1953        let has_avg_error = result
1954            .errors
1955            .iter()
1956            .any(|e| e.contains("AVG(n) is invalid") && e.contains("requires a property"));
1957        assert!(
1958            has_avg_error,
1959            "Expected error about AVG requiring property, got: {:?}",
1960            result.errors
1961        );
1962    }
1963
1964    #[test]
1965    fn test_sum_with_property_passes_validation() {
1966        let expr = ValueExpression::AggregateFunction {
1967            name: "sum".to_string(),
1968            args: vec![ValueExpression::Property(PropertyRef::new("n", "age"))],
1969            distinct: false,
1970        };
1971        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1972        assert!(
1973            result.errors.is_empty(),
1974            "SUM with property should pass validation, got errors: {:?}",
1975            result.errors
1976        );
1977    }
1978
1979    #[test]
1980    fn test_min_with_variable_fails_validation() {
1981        let expr = ValueExpression::AggregateFunction {
1982            name: "min".to_string(),
1983            args: vec![ValueExpression::Variable("n".to_string())],
1984            distinct: false,
1985        };
1986        let result = analyze_return_with_match("n", "Person", expr).unwrap();
1987        assert!(
1988            !result.errors.is_empty(),
1989            "Expected MIN(variable) to produce validation errors"
1990        );
1991        let has_min_error = result
1992            .errors
1993            .iter()
1994            .any(|e| e.contains("MIN(n) is invalid") && e.contains("requires a property"));
1995        assert!(
1996            has_min_error,
1997            "Expected error about MIN requiring property, got: {:?}",
1998            result.errors
1999        );
2000    }
2001
2002    #[test]
2003    fn test_max_with_variable_fails_validation() {
2004        let expr = ValueExpression::AggregateFunction {
2005            name: "max".to_string(),
2006            args: vec![ValueExpression::Variable("n".to_string())],
2007            distinct: false,
2008        };
2009        let result = analyze_return_with_match("n", "Person", expr).unwrap();
2010        assert!(
2011            !result.errors.is_empty(),
2012            "Expected MAX(variable) to produce validation errors"
2013        );
2014        let has_max_error = result
2015            .errors
2016            .iter()
2017            .any(|e| e.contains("MAX(n) is invalid") && e.contains("requires a property"));
2018        assert!(
2019            has_max_error,
2020            "Expected error about MAX requiring property, got: {:?}",
2021            result.errors
2022        );
2023    }
2024
2025    #[test]
2026    fn test_min_with_property_passes_validation() {
2027        let expr = ValueExpression::AggregateFunction {
2028            name: "min".to_string(),
2029            args: vec![ValueExpression::Property(PropertyRef::new("n", "age"))],
2030            distinct: false,
2031        };
2032        let result = analyze_return_with_match("n", "Person", expr).unwrap();
2033        assert!(
2034            result.errors.is_empty(),
2035            "MIN with property should pass validation, got errors: {:?}",
2036            result.errors
2037        );
2038    }
2039
2040    #[test]
2041    fn test_max_with_property_passes_validation() {
2042        let expr = ValueExpression::AggregateFunction {
2043            name: "max".to_string(),
2044            args: vec![ValueExpression::Property(PropertyRef::new("n", "age"))],
2045            distinct: false,
2046        };
2047        let result = analyze_return_with_match("n", "Person", expr).unwrap();
2048        assert!(
2049            result.errors.is_empty(),
2050            "MAX with property should pass validation, got errors: {:?}",
2051            result.errors
2052        );
2053    }
2054
2055    #[test]
2056    fn test_distinct_only_supported_on_count() {
2057        // SUM(DISTINCT n.age) should fail - DISTINCT only supported for COUNT
2058        let expr = ValueExpression::AggregateFunction {
2059            name: "sum".to_string(),
2060            args: vec![ValueExpression::Property(PropertyRef::new("n", "age"))],
2061            distinct: true,
2062        };
2063        let result = analyze_return_with_match("n", "Person", expr).unwrap();
2064        assert!(
2065            result
2066                .errors
2067                .iter()
2068                .any(|e| e.contains("DISTINCT is only supported with COUNT")),
2069            "Expected error about DISTINCT only for COUNT, got: {:?}",
2070            result.errors
2071        );
2072    }
2073
2074    #[test]
2075    fn test_count_distinct_star_rejected() {
2076        // COUNT(DISTINCT *) is semantically meaningless - should be rejected
2077        let expr = ValueExpression::AggregateFunction {
2078            name: "count".to_string(),
2079            args: vec![ValueExpression::Variable("*".to_string())],
2080            distinct: true,
2081        };
2082        let result = analyze_return_with_match("n", "Person", expr).unwrap();
2083        assert!(
2084            result
2085                .errors
2086                .iter()
2087                .any(|e| e.contains("COUNT(DISTINCT *)")),
2088            "Expected error about COUNT(DISTINCT *), got: {:?}",
2089            result.errors
2090        );
2091    }
2092
2093    #[test]
2094    fn test_count_distinct_passes_validation() {
2095        // COUNT(DISTINCT n.age) should pass
2096        let expr = ValueExpression::AggregateFunction {
2097            name: "count".to_string(),
2098            args: vec![ValueExpression::Property(PropertyRef::new("n", "age"))],
2099            distinct: true,
2100        };
2101        let result = analyze_return_with_match("n", "Person", expr).unwrap();
2102        assert!(
2103            result.errors.is_empty(),
2104            "COUNT(DISTINCT) should pass validation, got errors: {:?}",
2105            result.errors
2106        );
2107    }
2108
2109    #[test]
2110    fn test_arithmetic_with_non_numeric_literal_error() {
2111        // RETURN "x" + 1
2112        let expr = ValueExpression::Arithmetic {
2113            left: Box::new(ValueExpression::Literal(PropertyValue::String(
2114                "x".to_string(),
2115            ))),
2116            operator: ArithmeticOperator::Add,
2117            right: Box::new(ValueExpression::Literal(PropertyValue::Integer(1))),
2118        };
2119        let result = analyze_return_expr(expr).unwrap();
2120        // The semantic analyzer returns Ok with errors collected in the result
2121        assert!(result
2122            .errors
2123            .iter()
2124            .any(|e| e.contains("Type mismatch") && e.contains("Utf8") && e.contains("Int64")));
2125    }
2126
2127    #[test]
2128    fn test_arithmetic_with_numeric_literals_ok() {
2129        // RETURN 1 + 2.0
2130        let expr = ValueExpression::Arithmetic {
2131            left: Box::new(ValueExpression::Literal(PropertyValue::Integer(1))),
2132            operator: ArithmeticOperator::Add,
2133            right: Box::new(ValueExpression::Literal(PropertyValue::Float(2.0))),
2134        };
2135        let result = analyze_return_expr(expr);
2136        assert!(result.is_ok(), "Expected Ok but got {:?}", result);
2137        assert!(result.unwrap().errors.is_empty());
2138    }
2139
2140    #[test]
2141    fn test_vector_distance_with_property() {
2142        use crate::ast::DistanceMetric;
2143
2144        // MATCH (p:Person) RETURN vector_distance(p.embedding, p.embedding, l2)
2145        let expr = ValueExpression::VectorDistance {
2146            left: Box::new(ValueExpression::Property(PropertyRef {
2147                variable: "p".to_string(),
2148                property: "embedding".to_string(),
2149            })),
2150            right: Box::new(ValueExpression::Property(PropertyRef {
2151                variable: "p".to_string(),
2152                property: "embedding".to_string(),
2153            })),
2154            metric: DistanceMetric::L2,
2155        };
2156
2157        let result = analyze_return_with_match("p", "Person", expr);
2158        assert!(result.is_ok(), "Expected Ok but got {:?}", result);
2159        assert!(result.unwrap().errors.is_empty());
2160    }
2161
2162    #[test]
2163    fn test_vector_distance_without_property_fails() {
2164        use crate::ast::DistanceMetric;
2165
2166        // MATCH (p:Person) RETURN vector_distance(0.5, 0.3, l2) - both literals, should fail
2167        let expr = ValueExpression::VectorDistance {
2168            left: Box::new(ValueExpression::Literal(PropertyValue::Float(0.5))),
2169            right: Box::new(ValueExpression::Literal(PropertyValue::Float(0.3))),
2170            metric: DistanceMetric::L2,
2171        };
2172
2173        let result = analyze_return_with_match("p", "Person", expr);
2174        // Semantic analyzer returns Ok but with errors in the result
2175        assert!(
2176            result.is_ok(),
2177            "Analyzer should return Ok with errors, got {:?}",
2178            result
2179        );
2180        let semantic_result = result.unwrap();
2181        assert!(
2182            !semantic_result.errors.is_empty(),
2183            "Expected validation errors"
2184        );
2185        assert!(semantic_result
2186            .errors
2187            .iter()
2188            .any(|e| e.contains("requires at least one argument to be a property")));
2189    }
2190
2191    #[test]
2192    fn test_vector_similarity_with_property() {
2193        use crate::ast::DistanceMetric;
2194
2195        // MATCH (p:Person) RETURN vector_similarity(p.embedding, p.embedding, cosine)
2196        let expr = ValueExpression::VectorSimilarity {
2197            left: Box::new(ValueExpression::Property(PropertyRef {
2198                variable: "p".to_string(),
2199                property: "embedding".to_string(),
2200            })),
2201            right: Box::new(ValueExpression::Property(PropertyRef {
2202                variable: "p".to_string(),
2203                property: "embedding".to_string(),
2204            })),
2205            metric: DistanceMetric::Cosine,
2206        };
2207
2208        let result = analyze_return_with_match("p", "Person", expr);
2209        assert!(result.is_ok(), "Expected Ok but got {:?}", result);
2210        assert!(result.unwrap().errors.is_empty());
2211    }
2212
2213    #[test]
2214    fn test_vector_similarity_one_literal_ok() {
2215        use crate::ast::DistanceMetric;
2216
2217        // MATCH (p:Person) RETURN vector_similarity(p.embedding, 0.5, cosine)
2218        // One property reference is sufficient
2219        let expr = ValueExpression::VectorSimilarity {
2220            left: Box::new(ValueExpression::Property(PropertyRef {
2221                variable: "p".to_string(),
2222                property: "embedding".to_string(),
2223            })),
2224            right: Box::new(ValueExpression::Literal(PropertyValue::Float(0.5))),
2225            metric: DistanceMetric::Cosine,
2226        };
2227
2228        let result = analyze_return_with_match("p", "Person", expr);
2229        assert!(result.is_ok(), "Expected Ok but got {:?}", result);
2230        assert!(result.unwrap().errors.is_empty());
2231    }
2232
2233    #[test]
2234    fn test_vector_distance_all_metrics() {
2235        use crate::ast::DistanceMetric;
2236
2237        // Test all distance metrics are accepted
2238        for metric in [
2239            DistanceMetric::L2,
2240            DistanceMetric::Cosine,
2241            DistanceMetric::Dot,
2242        ] {
2243            let expr = ValueExpression::VectorDistance {
2244                left: Box::new(ValueExpression::Property(PropertyRef {
2245                    variable: "p".to_string(),
2246                    property: "embedding".to_string(),
2247                })),
2248                right: Box::new(ValueExpression::Property(PropertyRef {
2249                    variable: "p".to_string(),
2250                    property: "embedding".to_string(),
2251                })),
2252                metric: metric.clone(),
2253            };
2254
2255            let result = analyze_return_with_match("p", "Person", expr);
2256            assert!(
2257                result.is_ok(),
2258                "Expected Ok for metric {:?} but got {:?}",
2259                metric,
2260                result
2261            );
2262            assert!(result.unwrap().errors.is_empty());
2263        }
2264    }
2265}