Skip to main content

cypherlite_query/semantic/
mod.rs

1// Semantic analysis: variable scope validation, label/type resolution
2
3use crate::parser::ast::{
4    Clause, CreateClause, DeleteClause, Expression, MatchClause, MergeClause, NodePattern, Pattern,
5    PatternElement, RelationshipPattern, RemoveItem, ReturnClause, SetItem, UnwindClause,
6    WithClause,
7};
8use cypherlite_core::LabelRegistry;
9
10/// Variable scope tracking for semantic analysis.
11pub mod symbol_table;
12use symbol_table::{SymbolTable, VariableKind};
13
14/// Semantic errors found during analysis.
15#[derive(Debug, Clone, PartialEq)]
16pub struct SemanticError {
17    /// Human-readable error description.
18    pub message: String,
19}
20
21impl std::fmt::Display for SemanticError {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        write!(f, "Semantic error: {}", self.message)
24    }
25}
26
27impl std::error::Error for SemanticError {}
28
29/// Walks the AST to validate variable scoping and resolve names via a `LabelRegistry`.
30pub struct SemanticAnalyzer<'a> {
31    registry: &'a mut dyn LabelRegistry,
32    symbols: SymbolTable,
33}
34
35impl<'a> SemanticAnalyzer<'a> {
36    /// Create a new analyzer backed by the given registry.
37    pub fn new(registry: &'a mut dyn LabelRegistry) -> Self {
38        Self {
39            registry,
40            symbols: SymbolTable::new(),
41        }
42    }
43
44    // @MX:ANCHOR: [AUTO] Central semantic validation — called by CypherLite API and Planner
45    // @MX:REASON: fan_in >= 3; validates all queries before execution
46    /// Analyze a query, resolving names and checking variable scoping.
47    /// Returns the symbol table on success.
48    pub fn analyze(
49        &mut self,
50        query: &crate::parser::ast::Query,
51    ) -> Result<SymbolTable, SemanticError> {
52        for clause in &query.clauses {
53            self.analyze_clause(clause)?;
54        }
55        Ok(self.symbols.clone())
56    }
57
58    fn analyze_clause(&mut self, clause: &Clause) -> Result<(), SemanticError> {
59        match clause {
60            Clause::Match(m) => self.analyze_match(m),
61            Clause::Create(c) => self.analyze_create(c),
62            Clause::Merge(m) => self.analyze_merge(m),
63            Clause::Return(r) => self.analyze_return(r),
64            Clause::With(w) => self.analyze_with(w),
65            Clause::Set(s) => self.analyze_set(s),
66            Clause::Delete(d) => self.analyze_delete(d),
67            Clause::Remove(r) => self.analyze_remove(r),
68            Clause::Unwind(u) => self.analyze_unwind(u),
69            // DDL clauses: no variable scope validation needed
70            Clause::CreateIndex(_) | Clause::DropIndex(_) => Ok(()),
71            #[cfg(feature = "subgraph")]
72            Clause::CreateSnapshot(_) => Ok(()), // TODO: semantic analysis for snapshot
73            #[cfg(feature = "hypergraph")]
74            Clause::CreateHyperedge(hc) => {
75                // Register the hyperedge variable if present
76                if let Some(ref var) = hc.variable {
77                    self.symbols
78                        .define(var.clone(), VariableKind::Expression)
79                        .map_err(|msg| SemanticError { message: msg })?;
80                }
81                Ok(())
82            }
83            #[cfg(feature = "hypergraph")]
84            Clause::MatchHyperedge(mhc) => {
85                // Register the hyperedge variable in scope
86                if let Some(ref var) = mhc.variable {
87                    self.symbols
88                        .define(var.clone(), VariableKind::Expression)
89                        .map_err(|msg| SemanticError { message: msg })?;
90                }
91                Ok(())
92            }
93        }
94    }
95
96    // --- Pattern-defining clauses ---
97
98    fn analyze_match(&mut self, m: &MatchClause) -> Result<(), SemanticError> {
99        self.analyze_pattern_define_with_nullable(&m.pattern, m.optional)?;
100        // Validate temporal predicate expressions if present
101        if let Some(ref tp) = m.temporal_predicate {
102            match tp {
103                crate::parser::ast::TemporalPredicate::AsOf(expr) => {
104                    self.analyze_expression_refs(expr)?;
105                }
106                crate::parser::ast::TemporalPredicate::Between(start, end) => {
107                    self.analyze_expression_refs(start)?;
108                    self.analyze_expression_refs(end)?;
109                }
110            }
111        }
112        if let Some(ref where_expr) = m.where_clause {
113            self.analyze_expression_refs(where_expr)?;
114        }
115        Ok(())
116    }
117
118    fn analyze_create(&mut self, c: &CreateClause) -> Result<(), SemanticError> {
119        self.analyze_pattern_define(&c.pattern)
120    }
121
122    fn analyze_merge(&mut self, m: &MergeClause) -> Result<(), SemanticError> {
123        self.analyze_pattern_define(&m.pattern)?;
124        // Validate ON MATCH SET items reference defined variables
125        for item in &m.on_match {
126            match item {
127                SetItem::Property { target, value } => {
128                    self.analyze_expression_refs(target)?;
129                    self.analyze_expression_refs(value)?;
130                }
131            }
132        }
133        // Validate ON CREATE SET items reference defined variables
134        for item in &m.on_create {
135            match item {
136                SetItem::Property { target, value } => {
137                    self.analyze_expression_refs(target)?;
138                    self.analyze_expression_refs(value)?;
139                }
140            }
141        }
142        Ok(())
143    }
144
145    // --- Expression-referencing clauses ---
146
147    fn analyze_return(&mut self, r: &ReturnClause) -> Result<(), SemanticError> {
148        for item in &r.items {
149            self.analyze_expression_refs(&item.expr)?;
150        }
151        if let Some(ref order_items) = r.order_by {
152            for oi in order_items {
153                self.analyze_expression_refs(&oi.expr)?;
154            }
155        }
156        if let Some(ref skip) = r.skip {
157            self.analyze_expression_refs(skip)?;
158        }
159        if let Some(ref limit) = r.limit {
160            self.analyze_expression_refs(limit)?;
161        }
162        Ok(())
163    }
164
165    fn analyze_with(&mut self, w: &WithClause) -> Result<(), SemanticError> {
166        // First, validate all WITH expressions against current scope
167        for item in &w.items {
168            self.analyze_expression_refs(&item.expr)?;
169        }
170
171        // Determine surviving variables after scope reset.
172        // Each WITH item produces a variable: alias if present, or variable name from expression.
173        let survivors: Vec<(String, VariableKind)> = w
174            .items
175            .iter()
176            .filter_map(|item| {
177                let name = match &item.alias {
178                    Some(alias) => alias.clone(),
179                    None => match &item.expr {
180                        Expression::Variable(v) => v.clone(),
181                        _ => return None,
182                    },
183                };
184                // If the expression is a plain variable and it already has a kind, preserve it.
185                // Otherwise, it becomes an Expression kind.
186                let kind = if item.alias.is_none() {
187                    if let Expression::Variable(v) = &item.expr {
188                        self.symbols
189                            .get(v)
190                            .map(|info| info.kind)
191                            .unwrap_or(VariableKind::Expression)
192                    } else {
193                        VariableKind::Expression
194                    }
195                } else {
196                    VariableKind::Expression
197                };
198                Some((name, kind))
199            })
200            .collect();
201
202        // Reset scope: only projected variables survive
203        self.symbols.reset_scope(&survivors);
204
205        // Validate WITH WHERE against the new scope (after reset)
206        if let Some(ref where_expr) = w.where_clause {
207            self.analyze_expression_refs(where_expr)?;
208        }
209
210        Ok(())
211    }
212
213    fn analyze_set(&mut self, s: &crate::parser::ast::SetClause) -> Result<(), SemanticError> {
214        for item in &s.items {
215            match item {
216                SetItem::Property { target, value } => {
217                    self.analyze_expression_refs(target)?;
218                    self.analyze_expression_refs(value)?;
219                }
220            }
221        }
222        Ok(())
223    }
224
225    fn analyze_delete(&mut self, d: &DeleteClause) -> Result<(), SemanticError> {
226        for expr in &d.exprs {
227            self.analyze_expression_refs(expr)?;
228        }
229        Ok(())
230    }
231
232    fn analyze_remove(
233        &mut self,
234        r: &crate::parser::ast::RemoveClause,
235    ) -> Result<(), SemanticError> {
236        for item in &r.items {
237            match item {
238                RemoveItem::Property(expr) => {
239                    self.analyze_expression_refs(expr)?;
240                }
241                RemoveItem::Label { variable, label } => {
242                    if !self.symbols.is_defined(variable) {
243                        return Err(SemanticError {
244                            message: format!("undefined variable '{}'", variable),
245                        });
246                    }
247                    self.registry.get_or_create_label(label);
248                }
249            }
250        }
251        Ok(())
252    }
253
254    fn analyze_unwind(&mut self, u: &UnwindClause) -> Result<(), SemanticError> {
255        self.analyze_expression_refs(&u.expr)?;
256        self.symbols
257            .define(u.variable.clone(), VariableKind::Expression)
258            .map_err(|msg| SemanticError { message: msg })?;
259        Ok(())
260    }
261
262    // --- Pattern definition (defines variables and resolves labels/types) ---
263
264    fn analyze_pattern_define(&mut self, pattern: &Pattern) -> Result<(), SemanticError> {
265        self.analyze_pattern_define_with_nullable(pattern, false)
266    }
267
268    fn analyze_pattern_define_with_nullable(
269        &mut self,
270        pattern: &Pattern,
271        nullable: bool,
272    ) -> Result<(), SemanticError> {
273        for chain in &pattern.chains {
274            for element in &chain.elements {
275                match element {
276                    PatternElement::Node(node) => {
277                        self.analyze_node_pattern(node, nullable)?;
278                    }
279                    PatternElement::Relationship(rel) => {
280                        self.analyze_rel_pattern(rel, nullable)?;
281                    }
282                }
283            }
284        }
285        Ok(())
286    }
287
288    fn analyze_node_pattern(
289        &mut self,
290        node: &NodePattern,
291        nullable: bool,
292    ) -> Result<(), SemanticError> {
293        // Define variable if present.
294        if let Some(ref var) = node.variable {
295            self.symbols
296                .define_with_nullable(var.clone(), VariableKind::Node, nullable)
297                .map_err(|msg| SemanticError { message: msg })?;
298        }
299        // Resolve labels.
300        for label in &node.labels {
301            self.registry.get_or_create_label(label);
302        }
303        // Resolve property keys in map literal.
304        if let Some(ref props) = node.properties {
305            for (key, value) in props {
306                self.registry.get_or_create_prop_key(key);
307                self.analyze_expression_refs(value)?;
308            }
309        }
310        Ok(())
311    }
312
313    fn analyze_rel_pattern(
314        &mut self,
315        rel: &RelationshipPattern,
316        nullable: bool,
317    ) -> Result<(), SemanticError> {
318        // Define variable if present.
319        if let Some(ref var) = rel.variable {
320            self.symbols
321                .define_with_nullable(var.clone(), VariableKind::Relationship, nullable)
322                .map_err(|msg| SemanticError { message: msg })?;
323        }
324        // Resolve relationship types.
325        for rt in &rel.rel_types {
326            self.registry.get_or_create_rel_type(rt);
327        }
328        // Validate variable-length path bounds.
329        if let (Some(min), Some(max)) = (rel.min_hops, rel.max_hops) {
330            if max < min {
331                return Err(SemanticError {
332                    message: format!("max_hops ({}) must be >= min_hops ({})", max, min),
333                });
334            }
335        }
336        // Configurable max hop limit (default 10).
337        const MAX_HOP_LIMIT: u32 = 10;
338        if let Some(max) = rel.max_hops {
339            if max > MAX_HOP_LIMIT {
340                return Err(SemanticError {
341                    message: format!(
342                        "max_hops ({}) exceeds configurable limit ({})",
343                        max, MAX_HOP_LIMIT
344                    ),
345                });
346            }
347        }
348        // Resolve property keys in map literal.
349        if let Some(ref props) = rel.properties {
350            for (key, value) in props {
351                self.registry.get_or_create_prop_key(key);
352                self.analyze_expression_refs(value)?;
353            }
354        }
355        Ok(())
356    }
357
358    // --- Expression reference checking ---
359
360    fn analyze_expression_refs(&self, expr: &Expression) -> Result<(), SemanticError> {
361        match expr {
362            Expression::Variable(name) => {
363                if !self.symbols.is_defined(name) {
364                    return Err(SemanticError {
365                        message: format!("undefined variable '{}'", name),
366                    });
367                }
368                Ok(())
369            }
370            Expression::Property(inner, _prop_key) => {
371                // We check the inner expression for variable references.
372                // Property key resolution is deferred to planner/executor.
373                self.analyze_expression_refs(inner)
374            }
375            Expression::BinaryOp(_, lhs, rhs) => {
376                self.analyze_expression_refs(lhs)?;
377                self.analyze_expression_refs(rhs)
378            }
379            Expression::UnaryOp(_, operand) => self.analyze_expression_refs(operand),
380            Expression::FunctionCall { args, .. } => {
381                for arg in args {
382                    self.analyze_expression_refs(arg)?;
383                }
384                Ok(())
385            }
386            Expression::IsNull(inner, _) => self.analyze_expression_refs(inner),
387            Expression::ListLiteral(elements) => {
388                for elem in elements {
389                    self.analyze_expression_refs(elem)?;
390                }
391                Ok(())
392            }
393            Expression::Literal(_) | Expression::Parameter(_) | Expression::CountStar => Ok(()),
394            #[cfg(feature = "hypergraph")]
395            Expression::TemporalRef { node, timestamp } => {
396                self.analyze_expression_refs(node)?;
397                self.analyze_expression_refs(timestamp)
398            }
399        }
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use crate::parser::ast::*;
407    use std::collections::HashMap;
408
409    // --- MockCatalog implementing LabelRegistry ---
410
411    #[derive(Default)]
412    struct MockCatalog {
413        labels: HashMap<String, u32>,
414        rel_types: HashMap<String, u32>,
415        prop_keys: HashMap<String, u32>,
416        next_label: u32,
417        next_rel: u32,
418        next_prop: u32,
419    }
420
421    impl LabelRegistry for MockCatalog {
422        fn get_or_create_label(&mut self, name: &str) -> u32 {
423            if let Some(&id) = self.labels.get(name) {
424                return id;
425            }
426            let id = self.next_label;
427            self.next_label += 1;
428            self.labels.insert(name.to_string(), id);
429            id
430        }
431        fn label_id(&self, name: &str) -> Option<u32> {
432            self.labels.get(name).copied()
433        }
434        fn label_name(&self, _id: u32) -> Option<&str> {
435            None // Not needed for tests.
436        }
437        fn get_or_create_rel_type(&mut self, name: &str) -> u32 {
438            if let Some(&id) = self.rel_types.get(name) {
439                return id;
440            }
441            let id = self.next_rel;
442            self.next_rel += 1;
443            self.rel_types.insert(name.to_string(), id);
444            id
445        }
446        fn rel_type_id(&self, name: &str) -> Option<u32> {
447            self.rel_types.get(name).copied()
448        }
449        fn rel_type_name(&self, _id: u32) -> Option<&str> {
450            None
451        }
452        fn get_or_create_prop_key(&mut self, name: &str) -> u32 {
453            if let Some(&id) = self.prop_keys.get(name) {
454                return id;
455            }
456            let id = self.next_prop;
457            self.next_prop += 1;
458            self.prop_keys.insert(name.to_string(), id);
459            id
460        }
461        fn prop_key_id(&self, name: &str) -> Option<u32> {
462            self.prop_keys.get(name).copied()
463        }
464        fn prop_key_name(&self, _id: u32) -> Option<&str> {
465            None
466        }
467    }
468
469    // --- Helper: build a simple node pattern ---
470
471    fn node(var: Option<&str>, labels: &[&str], props: Option<MapLiteral>) -> PatternElement {
472        PatternElement::Node(NodePattern {
473            variable: var.map(|s| s.to_string()),
474            labels: labels.iter().map(|s| s.to_string()).collect(),
475            properties: props,
476        })
477    }
478
479    fn rel(
480        var: Option<&str>,
481        types: &[&str],
482        dir: RelDirection,
483        props: Option<MapLiteral>,
484    ) -> PatternElement {
485        PatternElement::Relationship(RelationshipPattern {
486            variable: var.map(|s| s.to_string()),
487            rel_types: types.iter().map(|s| s.to_string()).collect(),
488            direction: dir,
489            properties: props,
490            min_hops: None,
491            max_hops: None,
492        })
493    }
494
495    fn pattern(chains: Vec<Vec<PatternElement>>) -> Pattern {
496        Pattern {
497            chains: chains
498                .into_iter()
499                .map(|elements| PatternChain { elements })
500                .collect(),
501        }
502    }
503
504    fn var_expr(name: &str) -> Expression {
505        Expression::Variable(name.to_string())
506    }
507
508    fn prop_expr(var_name: &str, prop: &str) -> Expression {
509        Expression::Property(Box::new(var_expr(var_name)), prop.to_string())
510    }
511
512    fn return_clause(items: Vec<ReturnItem>) -> ReturnClause {
513        ReturnClause {
514            distinct: false,
515            items,
516            order_by: None,
517            skip: None,
518            limit: None,
519        }
520    }
521
522    fn return_item(expr: Expression) -> ReturnItem {
523        ReturnItem { expr, alias: None }
524    }
525
526    // === TASK-039 Tests ===
527
528    // Valid: MATCH (n:Person) RETURN n.name -- n is defined, name resolves
529    #[test]
530    fn test_valid_match_return_property() {
531        let mut catalog = MockCatalog::default();
532        let query = Query {
533            clauses: vec![
534                Clause::Match(MatchClause {
535                    optional: false,
536                    pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
537                    temporal_predicate: None,
538                    where_clause: None,
539                }),
540                Clause::Return(return_clause(vec![return_item(prop_expr("n", "name"))])),
541            ],
542        };
543
544        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
545        let result = analyzer.analyze(&query);
546        assert!(result.is_ok());
547
548        let symbols = result.unwrap();
549        assert!(symbols.is_defined("n"));
550        assert_eq!(symbols.get("n").unwrap().kind, VariableKind::Node);
551
552        // Verify label was resolved in catalog.
553        assert!(catalog.label_id("Person").is_some());
554    }
555
556    // Valid: MATCH (a)-[r:KNOWS]->(b) RETURN b.name -- a, r, b all defined
557    #[test]
558    fn test_valid_match_relationship_pattern() {
559        let mut catalog = MockCatalog::default();
560        let query = Query {
561            clauses: vec![
562                Clause::Match(MatchClause {
563                    optional: false,
564                    pattern: pattern(vec![vec![
565                        node(Some("a"), &[], None),
566                        rel(Some("r"), &["KNOWS"], RelDirection::Outgoing, None),
567                        node(Some("b"), &[], None),
568                    ]]),
569                    temporal_predicate: None,
570                    where_clause: None,
571                }),
572                Clause::Return(return_clause(vec![return_item(prop_expr("b", "name"))])),
573            ],
574        };
575
576        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
577        let result = analyzer.analyze(&query);
578        assert!(result.is_ok());
579
580        let symbols = result.unwrap();
581        assert!(symbols.is_defined("a"));
582        assert!(symbols.is_defined("r"));
583        assert!(symbols.is_defined("b"));
584        assert_eq!(symbols.get("r").unwrap().kind, VariableKind::Relationship);
585
586        // Verify relationship type was resolved.
587        assert!(catalog.rel_type_id("KNOWS").is_some());
588    }
589
590    // Invalid: MATCH (n:Person) RETURN m.name -- m is undefined -> SemanticError
591    #[test]
592    fn test_invalid_undefined_variable_in_return() {
593        let mut catalog = MockCatalog::default();
594        let query = Query {
595            clauses: vec![
596                Clause::Match(MatchClause {
597                    optional: false,
598                    pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
599                    temporal_predicate: None,
600                    where_clause: None,
601                }),
602                Clause::Return(return_clause(vec![return_item(prop_expr("m", "name"))])),
603            ],
604        };
605
606        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
607        let result = analyzer.analyze(&query);
608        assert!(result.is_err());
609
610        let err = result.unwrap_err();
611        assert!(
612            err.message.contains("undefined variable 'm'"),
613            "expected undefined variable error, got: {}",
614            err.message
615        );
616    }
617
618    // Invalid: RETURN n.name without MATCH -- n undefined
619    #[test]
620    fn test_invalid_return_without_match() {
621        let mut catalog = MockCatalog::default();
622        let query = Query {
623            clauses: vec![Clause::Return(return_clause(vec![return_item(prop_expr(
624                "n", "name",
625            ))]))],
626        };
627
628        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
629        let result = analyzer.analyze(&query);
630        assert!(result.is_err());
631
632        let err = result.unwrap_err();
633        assert!(err.message.contains("undefined variable 'n'"));
634    }
635
636    // Valid: CREATE (n:Person {name: "Alice"}) RETURN n -- n defined in CREATE
637    #[test]
638    fn test_valid_create_with_properties_and_return() {
639        let mut catalog = MockCatalog::default();
640        let props = vec![(
641            "name".to_string(),
642            Expression::Literal(Literal::String("Alice".to_string())),
643        )];
644
645        let query = Query {
646            clauses: vec![
647                Clause::Create(CreateClause {
648                    pattern: pattern(vec![vec![node(Some("n"), &["Person"], Some(props))]]),
649                }),
650                Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
651            ],
652        };
653
654        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
655        let result = analyzer.analyze(&query);
656        assert!(result.is_ok());
657
658        let symbols = result.unwrap();
659        assert!(symbols.is_defined("n"));
660
661        // Verify label and prop key were resolved.
662        assert!(catalog.label_id("Person").is_some());
663        assert!(catalog.prop_key_id("name").is_some());
664    }
665
666    // Valid: MATCH (n) WHERE n.age > 30 RETURN n -- WHERE refs n
667    #[test]
668    fn test_valid_where_references_defined_variable() {
669        let mut catalog = MockCatalog::default();
670        let where_expr = Expression::BinaryOp(
671            BinaryOp::Gt,
672            Box::new(prop_expr("n", "age")),
673            Box::new(Expression::Literal(Literal::Integer(30))),
674        );
675
676        let query = Query {
677            clauses: vec![
678                Clause::Match(MatchClause {
679                    optional: false,
680                    pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
681                    temporal_predicate: None,
682                    where_clause: Some(where_expr),
683                }),
684                Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
685            ],
686        };
687
688        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
689        let result = analyzer.analyze(&query);
690        assert!(result.is_ok());
691    }
692
693    // Invalid: MATCH (n) WHERE m.age > 30 RETURN n -- m undefined in WHERE
694    #[test]
695    fn test_invalid_undefined_variable_in_where() {
696        let mut catalog = MockCatalog::default();
697        let where_expr = Expression::BinaryOp(
698            BinaryOp::Gt,
699            Box::new(prop_expr("m", "age")),
700            Box::new(Expression::Literal(Literal::Integer(30))),
701        );
702
703        let query = Query {
704            clauses: vec![
705                Clause::Match(MatchClause {
706                    optional: false,
707                    pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
708                    temporal_predicate: None,
709                    where_clause: Some(where_expr),
710                }),
711                Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
712            ],
713        };
714
715        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
716        let result = analyzer.analyze(&query);
717        assert!(result.is_err());
718
719        let err = result.unwrap_err();
720        assert!(
721            err.message.contains("undefined variable 'm'"),
722            "expected undefined variable error, got: {}",
723            err.message
724        );
725    }
726
727    // Additional: anonymous node patterns (no variable) are allowed
728    #[test]
729    fn test_valid_anonymous_node_pattern() {
730        let mut catalog = MockCatalog::default();
731        let query = Query {
732            clauses: vec![Clause::Match(MatchClause {
733                optional: false,
734                pattern: pattern(vec![vec![node(None, &["Person"], None)]]),
735                temporal_predicate: None,
736                where_clause: None,
737            })],
738        };
739
740        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
741        let result = analyzer.analyze(&query);
742        assert!(result.is_ok());
743        assert!(catalog.label_id("Person").is_some());
744    }
745
746    // Additional: redefining a node variable with the same kind across patterns is ok
747    #[test]
748    fn test_valid_redefine_same_kind() {
749        let mut catalog = MockCatalog::default();
750        let query = Query {
751            clauses: vec![
752                Clause::Match(MatchClause {
753                    optional: false,
754                    pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
755                    temporal_predicate: None,
756                    where_clause: None,
757                }),
758                Clause::Match(MatchClause {
759                    optional: false,
760                    pattern: pattern(vec![vec![node(Some("n"), &["Company"], None)]]),
761                    temporal_predicate: None,
762                    where_clause: None,
763                }),
764            ],
765        };
766
767        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
768        let result = analyzer.analyze(&query);
769        assert!(result.is_ok());
770    }
771
772    // Additional: defining a node variable then redefining as relationship is an error
773    #[test]
774    fn test_invalid_redefine_different_kind() {
775        let mut catalog = MockCatalog::default();
776        let query = Query {
777            clauses: vec![
778                Clause::Match(MatchClause {
779                    optional: false,
780                    pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
781                    temporal_predicate: None,
782                    where_clause: None,
783                }),
784                Clause::Match(MatchClause {
785                    optional: false,
786                    pattern: pattern(vec![vec![rel(
787                        Some("n"),
788                        &["KNOWS"],
789                        RelDirection::Outgoing,
790                        None,
791                    )]]),
792                    temporal_predicate: None,
793                    where_clause: None,
794                }),
795            ],
796        };
797
798        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
799        let result = analyzer.analyze(&query);
800        assert!(result.is_err());
801        assert!(result.unwrap_err().message.contains("already defined as"));
802    }
803
804    // Additional: SET clause checks variable references
805    #[test]
806    fn test_valid_set_clause() {
807        let mut catalog = MockCatalog::default();
808        let query = Query {
809            clauses: vec![
810                Clause::Match(MatchClause {
811                    optional: false,
812                    pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
813                    temporal_predicate: None,
814                    where_clause: None,
815                }),
816                Clause::Set(SetClause {
817                    items: vec![SetItem::Property {
818                        target: prop_expr("n", "age"),
819                        value: Expression::Literal(Literal::Integer(42)),
820                    }],
821                }),
822            ],
823        };
824
825        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
826        assert!(analyzer.analyze(&query).is_ok());
827    }
828
829    // Additional: DELETE clause checks variable references
830    #[test]
831    fn test_valid_delete_clause() {
832        let mut catalog = MockCatalog::default();
833        let query = Query {
834            clauses: vec![
835                Clause::Match(MatchClause {
836                    optional: false,
837                    pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
838                    temporal_predicate: None,
839                    where_clause: None,
840                }),
841                Clause::Delete(DeleteClause {
842                    detach: true,
843                    exprs: vec![var_expr("n")],
844                }),
845            ],
846        };
847
848        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
849        assert!(analyzer.analyze(&query).is_ok());
850    }
851
852    // Additional: DELETE with undefined variable fails
853    #[test]
854    fn test_invalid_delete_undefined_variable() {
855        let mut catalog = MockCatalog::default();
856        let query = Query {
857            clauses: vec![Clause::Delete(DeleteClause {
858                detach: false,
859                exprs: vec![var_expr("n")],
860            })],
861        };
862
863        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
864        let result = analyzer.analyze(&query);
865        assert!(result.is_err());
866        assert!(result
867            .unwrap_err()
868            .message
869            .contains("undefined variable 'n'"));
870    }
871
872    // Additional: MERGE defines variables like CREATE
873    #[test]
874    fn test_valid_merge_defines_variables() {
875        let mut catalog = MockCatalog::default();
876        let query = Query {
877            clauses: vec![
878                Clause::Merge(MergeClause {
879                    pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
880                    on_match: vec![],
881                    on_create: vec![],
882                }),
883                Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
884            ],
885        };
886
887        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
888        assert!(analyzer.analyze(&query).is_ok());
889    }
890
891    // TASK-085: MERGE with ON MATCH SET / ON CREATE SET validates variable refs
892    #[test]
893    fn test_valid_merge_on_match_set() {
894        let mut catalog = MockCatalog::default();
895        let query = Query {
896            clauses: vec![
897                Clause::Merge(MergeClause {
898                    pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
899                    on_match: vec![SetItem::Property {
900                        target: prop_expr("n", "seen"),
901                        value: Expression::Literal(Literal::Bool(true)),
902                    }],
903                    on_create: vec![],
904                }),
905                Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
906            ],
907        };
908
909        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
910        assert!(analyzer.analyze(&query).is_ok());
911    }
912
913    // TASK-085: MERGE ON CREATE SET with undefined variable fails
914    #[test]
915    fn test_invalid_merge_on_create_set_undefined_var() {
916        let mut catalog = MockCatalog::default();
917        let query = Query {
918            clauses: vec![Clause::Merge(MergeClause {
919                pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
920                on_match: vec![],
921                on_create: vec![SetItem::Property {
922                    target: prop_expr("m", "created"),
923                    value: Expression::Literal(Literal::Bool(true)),
924                }],
925            })],
926        };
927
928        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
929        let result = analyzer.analyze(&query);
930        assert!(result.is_err());
931        assert!(result
932            .unwrap_err()
933            .message
934            .contains("undefined variable 'm'"));
935    }
936
937    // Additional: function calls with variable arguments are checked
938    #[test]
939    fn test_valid_function_call_in_return() {
940        let mut catalog = MockCatalog::default();
941        let query = Query {
942            clauses: vec![
943                Clause::Match(MatchClause {
944                    optional: false,
945                    pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
946                    temporal_predicate: None,
947                    where_clause: None,
948                }),
949                Clause::Return(return_clause(vec![return_item(Expression::FunctionCall {
950                    name: "count".to_string(),
951                    distinct: false,
952                    args: vec![var_expr("n")],
953                })])),
954            ],
955        };
956
957        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
958        assert!(analyzer.analyze(&query).is_ok());
959    }
960
961    // === TASK-060 Tests: WITH clause scope reset ===
962
963    fn with_clause(items: Vec<ReturnItem>, where_clause: Option<Expression>) -> WithClause {
964        WithClause {
965            distinct: false,
966            items,
967            where_clause,
968        }
969    }
970
971    // MATCH (n:Person)-[r:KNOWS]->(m:Person) WITH n RETURN n
972    // After WITH n, only 'n' survives; 'm' and 'r' become inaccessible
973    #[test]
974    fn test_with_scope_reset_projected_variable_survives() {
975        let mut catalog = MockCatalog::default();
976        let query = Query {
977            clauses: vec![
978                Clause::Match(MatchClause {
979                    optional: false,
980                    pattern: pattern(vec![vec![
981                        node(Some("n"), &["Person"], None),
982                        rel(Some("r"), &["KNOWS"], RelDirection::Outgoing, None),
983                        node(Some("m"), &["Person"], None),
984                    ]]),
985                    temporal_predicate: None,
986                    where_clause: None,
987                }),
988                Clause::With(with_clause(vec![return_item(var_expr("n"))], None)),
989                Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
990            ],
991        };
992
993        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
994        let result = analyzer.analyze(&query);
995        assert!(result.is_ok());
996    }
997
998    // MATCH (n:Person)-[r:KNOWS]->(m:Person) WITH n RETURN m
999    // Error: 'm' not in WITH projection, so it is undefined after WITH
1000    #[test]
1001    fn test_with_scope_reset_non_projected_variable_error() {
1002        let mut catalog = MockCatalog::default();
1003        let query = Query {
1004            clauses: vec![
1005                Clause::Match(MatchClause {
1006                    optional: false,
1007                    pattern: pattern(vec![vec![
1008                        node(Some("n"), &["Person"], None),
1009                        rel(Some("r"), &["KNOWS"], RelDirection::Outgoing, None),
1010                        node(Some("m"), &["Person"], None),
1011                    ]]),
1012                    temporal_predicate: None,
1013                    where_clause: None,
1014                }),
1015                Clause::With(with_clause(vec![return_item(var_expr("n"))], None)),
1016                Clause::Return(return_clause(vec![return_item(var_expr("m"))])),
1017            ],
1018        };
1019
1020        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1021        let result = analyzer.analyze(&query);
1022        assert!(result.is_err());
1023        assert!(result
1024            .unwrap_err()
1025            .message
1026            .contains("undefined variable 'm'"));
1027    }
1028
1029    // MATCH (n:Person) WITH n.name AS name RETURN name
1030    // The alias 'name' is available after WITH, but 'n' is not
1031    #[test]
1032    fn test_with_alias_creates_new_scope() {
1033        let mut catalog = MockCatalog::default();
1034        let query = Query {
1035            clauses: vec![
1036                Clause::Match(MatchClause {
1037                    optional: false,
1038                    pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
1039                    temporal_predicate: None,
1040                    where_clause: None,
1041                }),
1042                Clause::With(with_clause(
1043                    vec![ReturnItem {
1044                        expr: prop_expr("n", "name"),
1045                        alias: Some("name".to_string()),
1046                    }],
1047                    None,
1048                )),
1049                Clause::Return(return_clause(vec![return_item(var_expr("name"))])),
1050            ],
1051        };
1052
1053        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1054        let result = analyzer.analyze(&query);
1055        assert!(result.is_ok());
1056    }
1057
1058    // MATCH (n:Person) WITH n.name AS name RETURN n
1059    // Error: 'n' is not in WITH projection, only 'name' alias is
1060    #[test]
1061    fn test_with_alias_original_variable_inaccessible() {
1062        let mut catalog = MockCatalog::default();
1063        let query = Query {
1064            clauses: vec![
1065                Clause::Match(MatchClause {
1066                    optional: false,
1067                    pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
1068                    temporal_predicate: None,
1069                    where_clause: None,
1070                }),
1071                Clause::With(with_clause(
1072                    vec![ReturnItem {
1073                        expr: prop_expr("n", "name"),
1074                        alias: Some("name".to_string()),
1075                    }],
1076                    None,
1077                )),
1078                Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
1079            ],
1080        };
1081
1082        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1083        let result = analyzer.analyze(&query);
1084        assert!(result.is_err());
1085        assert!(result
1086            .unwrap_err()
1087            .message
1088            .contains("undefined variable 'n'"));
1089    }
1090
1091    // MATCH (n:Person) WITH n WHERE n.age > 30 RETURN n
1092    // WITH WHERE should be able to reference projected variables
1093    #[test]
1094    fn test_with_where_references_projected_variable() {
1095        let mut catalog = MockCatalog::default();
1096        let query = Query {
1097            clauses: vec![
1098                Clause::Match(MatchClause {
1099                    optional: false,
1100                    pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
1101                    temporal_predicate: None,
1102                    where_clause: None,
1103                }),
1104                Clause::With(with_clause(
1105                    vec![return_item(var_expr("n"))],
1106                    Some(Expression::BinaryOp(
1107                        BinaryOp::Gt,
1108                        Box::new(prop_expr("n", "age")),
1109                        Box::new(Expression::Literal(Literal::Integer(30))),
1110                    )),
1111                )),
1112                Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
1113            ],
1114        };
1115
1116        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1117        let result = analyzer.analyze(&query);
1118        assert!(result.is_ok());
1119    }
1120
1121    // === TASK-074 Tests: OPTIONAL MATCH semantic analysis ===
1122
1123    // OPTIONAL MATCH variables should be marked as nullable
1124    #[test]
1125    fn test_optional_match_variables_are_nullable() {
1126        let mut catalog = MockCatalog::default();
1127        let query = Query {
1128            clauses: vec![
1129                Clause::Match(MatchClause {
1130                    optional: false,
1131                    pattern: pattern(vec![vec![node(Some("a"), &["Person"], None)]]),
1132                    temporal_predicate: None,
1133                    where_clause: None,
1134                }),
1135                Clause::Match(MatchClause {
1136                    optional: true,
1137                    pattern: pattern(vec![vec![
1138                        node(Some("a"), &[], None),
1139                        rel(Some("r"), &["KNOWS"], RelDirection::Outgoing, None),
1140                        node(Some("b"), &[], None),
1141                    ]]),
1142                    temporal_predicate: None,
1143                    where_clause: None,
1144                }),
1145                Clause::Return(return_clause(vec![
1146                    return_item(prop_expr("a", "name")),
1147                    return_item(prop_expr("b", "name")),
1148                ])),
1149            ],
1150        };
1151
1152        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1153        let result = analyzer.analyze(&query);
1154        assert!(result.is_ok());
1155
1156        let symbols = result.unwrap();
1157        // 'a' from regular MATCH is not nullable
1158        assert!(!symbols.get("a").unwrap().nullable);
1159        // 'b' from OPTIONAL MATCH is nullable
1160        assert!(symbols.get("b").unwrap().nullable);
1161        // 'r' from OPTIONAL MATCH is nullable
1162        assert!(symbols.get("r").unwrap().nullable);
1163    }
1164
1165    // OPTIONAL MATCH can reference variables from earlier MATCH
1166    #[test]
1167    fn test_optional_match_references_earlier_match_variable() {
1168        let mut catalog = MockCatalog::default();
1169        let query = Query {
1170            clauses: vec![
1171                Clause::Match(MatchClause {
1172                    optional: false,
1173                    pattern: pattern(vec![vec![node(Some("a"), &["Person"], None)]]),
1174                    temporal_predicate: None,
1175                    where_clause: None,
1176                }),
1177                Clause::Match(MatchClause {
1178                    optional: true,
1179                    pattern: pattern(vec![vec![
1180                        node(Some("a"), &[], None),
1181                        rel(None, &["WORKS_AT"], RelDirection::Outgoing, None),
1182                        node(Some("c"), &["Company"], None),
1183                    ]]),
1184                    temporal_predicate: None,
1185                    where_clause: None,
1186                }),
1187                Clause::Return(return_clause(vec![
1188                    return_item(var_expr("a")),
1189                    return_item(var_expr("c")),
1190                ])),
1191            ],
1192        };
1193
1194        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1195        let result = analyzer.analyze(&query);
1196        assert!(result.is_ok());
1197
1198        let symbols = result.unwrap();
1199        // 'a' was first defined in regular MATCH (not nullable), then re-referenced in OPTIONAL MATCH.
1200        // The non-nullable definition should be preserved.
1201        assert!(!symbols.get("a").unwrap().nullable);
1202        // 'c' is from OPTIONAL MATCH, so nullable
1203        assert!(symbols.get("c").unwrap().nullable);
1204    }
1205
1206    // OPTIONAL MATCH with WHERE clause
1207    #[test]
1208    fn test_optional_match_with_where() {
1209        let mut catalog = MockCatalog::default();
1210        let where_expr = Expression::BinaryOp(
1211            BinaryOp::Gt,
1212            Box::new(prop_expr("b", "age")),
1213            Box::new(Expression::Literal(Literal::Integer(20))),
1214        );
1215
1216        let query = Query {
1217            clauses: vec![
1218                Clause::Match(MatchClause {
1219                    optional: false,
1220                    pattern: pattern(vec![vec![node(Some("a"), &["Person"], None)]]),
1221                    temporal_predicate: None,
1222                    where_clause: None,
1223                }),
1224                Clause::Match(MatchClause {
1225                    optional: true,
1226                    pattern: pattern(vec![vec![
1227                        node(Some("a"), &[], None),
1228                        rel(None, &["KNOWS"], RelDirection::Outgoing, None),
1229                        node(Some("b"), &[], None),
1230                    ]]),
1231                    temporal_predicate: None,
1232                    where_clause: Some(where_expr),
1233                }),
1234                Clause::Return(return_clause(vec![
1235                    return_item(var_expr("a")),
1236                    return_item(var_expr("b")),
1237                ])),
1238            ],
1239        };
1240
1241        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1242        let result = analyzer.analyze(&query);
1243        assert!(result.is_ok());
1244    }
1245
1246    // === TASK-069 Tests: UNWIND clause semantic analysis ===
1247
1248    // UNWIND [1,2,3] AS x RETURN x -- x should be defined after UNWIND
1249    #[test]
1250    fn test_unwind_defines_variable() {
1251        let mut catalog = MockCatalog::default();
1252        let query = Query {
1253            clauses: vec![
1254                Clause::Unwind(UnwindClause {
1255                    expr: Expression::ListLiteral(vec![
1256                        Expression::Literal(Literal::Integer(1)),
1257                        Expression::Literal(Literal::Integer(2)),
1258                    ]),
1259                    variable: "x".to_string(),
1260                }),
1261                Clause::Return(return_clause(vec![return_item(var_expr("x"))])),
1262            ],
1263        };
1264
1265        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1266        let result = analyzer.analyze(&query);
1267        assert!(result.is_ok());
1268
1269        let symbols = result.unwrap();
1270        assert!(symbols.is_defined("x"));
1271        assert_eq!(symbols.get("x").unwrap().kind, VariableKind::Expression);
1272    }
1273
1274    // MATCH (n) UNWIND n.hobbies AS h RETURN h -- UNWIND expr references n
1275    #[test]
1276    fn test_unwind_references_prior_variables() {
1277        let mut catalog = MockCatalog::default();
1278        let query = Query {
1279            clauses: vec![
1280                Clause::Match(MatchClause {
1281                    optional: false,
1282                    pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
1283                    temporal_predicate: None,
1284                    where_clause: None,
1285                }),
1286                Clause::Unwind(UnwindClause {
1287                    expr: prop_expr("n", "hobbies"),
1288                    variable: "h".to_string(),
1289                }),
1290                Clause::Return(return_clause(vec![return_item(var_expr("h"))])),
1291            ],
1292        };
1293
1294        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1295        let result = analyzer.analyze(&query);
1296        assert!(result.is_ok());
1297    }
1298
1299    // UNWIND m.items AS x RETURN x -- m is undefined -> error
1300    #[test]
1301    fn test_unwind_undefined_variable_in_expr() {
1302        let mut catalog = MockCatalog::default();
1303        let query = Query {
1304            clauses: vec![
1305                Clause::Unwind(UnwindClause {
1306                    expr: prop_expr("m", "items"),
1307                    variable: "x".to_string(),
1308                }),
1309                Clause::Return(return_clause(vec![return_item(var_expr("x"))])),
1310            ],
1311        };
1312
1313        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1314        let result = analyzer.analyze(&query);
1315        assert!(result.is_err());
1316        assert!(result
1317            .unwrap_err()
1318            .message
1319            .contains("undefined variable 'm'"));
1320    }
1321
1322    // Additional: SemanticError Display implementation
1323    #[test]
1324    fn test_semantic_error_display() {
1325        let err = SemanticError {
1326            message: "test error".to_string(),
1327        };
1328        assert_eq!(format!("{}", err), "Semantic error: test error");
1329    }
1330
1331    // -- TASK-103: Variable-length path semantic validation --
1332
1333    #[test]
1334    fn test_var_length_path_valid() {
1335        let mut catalog = MockCatalog::default();
1336        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1337        let query = Query {
1338            clauses: vec![Clause::Match(MatchClause {
1339                optional: false,
1340                pattern: pattern(vec![vec![
1341                    node(Some("a"), &["Person"], None),
1342                    rel(None, &["KNOWS"], RelDirection::Outgoing, None),
1343                    node(Some("b"), &[], None),
1344                ]]),
1345                temporal_predicate: None,
1346                where_clause: None,
1347            })],
1348        };
1349        // Modify the relationship to have variable-length
1350        let mut q = query;
1351        if let Clause::Match(ref mut mc) = q.clauses[0] {
1352            if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
1353                rp.min_hops = Some(1);
1354                rp.max_hops = Some(3);
1355            }
1356        }
1357        assert!(analyzer.analyze(&q).is_ok());
1358    }
1359
1360    #[test]
1361    fn test_var_length_path_max_less_than_min() {
1362        let mut catalog = MockCatalog::default();
1363        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1364        let mut query = Query {
1365            clauses: vec![Clause::Match(MatchClause {
1366                optional: false,
1367                pattern: pattern(vec![vec![
1368                    node(Some("a"), &[], None),
1369                    rel(None, &[], RelDirection::Outgoing, None),
1370                    node(Some("b"), &[], None),
1371                ]]),
1372                temporal_predicate: None,
1373                where_clause: None,
1374            })],
1375        };
1376        if let Clause::Match(ref mut mc) = query.clauses[0] {
1377            if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
1378                rp.min_hops = Some(5);
1379                rp.max_hops = Some(2);
1380            }
1381        }
1382        let result = analyzer.analyze(&query);
1383        assert!(result.is_err());
1384        assert!(result
1385            .expect_err("should fail")
1386            .message
1387            .contains("max_hops"));
1388    }
1389
1390    #[test]
1391    fn test_var_length_path_max_exceeds_limit() {
1392        let mut catalog = MockCatalog::default();
1393        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1394        let mut query = Query {
1395            clauses: vec![Clause::Match(MatchClause {
1396                optional: false,
1397                pattern: pattern(vec![vec![
1398                    node(Some("a"), &[], None),
1399                    rel(None, &[], RelDirection::Outgoing, None),
1400                    node(Some("b"), &[], None),
1401                ]]),
1402                temporal_predicate: None,
1403                where_clause: None,
1404            })],
1405        };
1406        if let Clause::Match(ref mut mc) = query.clauses[0] {
1407            if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
1408                rp.min_hops = Some(1);
1409                rp.max_hops = Some(100);
1410            }
1411        }
1412        let result = analyzer.analyze(&query);
1413        assert!(result.is_err());
1414        assert!(result.expect_err("should fail").message.contains("exceeds"));
1415    }
1416
1417    #[test]
1418    fn test_var_length_path_unbounded_ok() {
1419        let mut catalog = MockCatalog::default();
1420        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1421        let mut query = Query {
1422            clauses: vec![Clause::Match(MatchClause {
1423                optional: false,
1424                pattern: pattern(vec![vec![
1425                    node(Some("a"), &[], None),
1426                    rel(None, &[], RelDirection::Outgoing, None),
1427                    node(Some("b"), &[], None),
1428                ]]),
1429                temporal_predicate: None,
1430                where_clause: None,
1431            })],
1432        };
1433        if let Clause::Match(ref mut mc) = query.clauses[0] {
1434            if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
1435                rp.min_hops = Some(1);
1436                rp.max_hops = None; // unbounded is OK - planner will cap it
1437            }
1438        }
1439        assert!(analyzer.analyze(&query).is_ok());
1440    }
1441
1442    #[test]
1443    fn test_var_length_path_min_zero_ok() {
1444        let mut catalog = MockCatalog::default();
1445        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1446        let mut query = Query {
1447            clauses: vec![Clause::Match(MatchClause {
1448                optional: false,
1449                pattern: pattern(vec![vec![
1450                    node(Some("a"), &[], None),
1451                    rel(None, &[], RelDirection::Outgoing, None),
1452                    node(Some("b"), &[], None),
1453                ]]),
1454                temporal_predicate: None,
1455                where_clause: None,
1456            })],
1457        };
1458        if let Clause::Match(ref mut mc) = query.clauses[0] {
1459            if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
1460                rp.min_hops = Some(0);
1461                rp.max_hops = Some(1);
1462            }
1463        }
1464        assert!(analyzer.analyze(&query).is_ok());
1465    }
1466
1467    #[test]
1468    fn test_var_length_path_equal_min_max_ok() {
1469        let mut catalog = MockCatalog::default();
1470        let mut analyzer = SemanticAnalyzer::new(&mut catalog);
1471        let mut query = Query {
1472            clauses: vec![Clause::Match(MatchClause {
1473                optional: false,
1474                pattern: pattern(vec![vec![
1475                    node(Some("a"), &[], None),
1476                    rel(None, &[], RelDirection::Outgoing, None),
1477                    node(Some("b"), &[], None),
1478                ]]),
1479                temporal_predicate: None,
1480                where_clause: None,
1481            })],
1482        };
1483        if let Clause::Match(ref mut mc) = query.clauses[0] {
1484            if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
1485                rp.min_hops = Some(3);
1486                rp.max_hops = Some(3);
1487            }
1488        }
1489        assert!(analyzer.analyze(&query).is_ok());
1490    }
1491}