1use 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
10pub mod symbol_table;
12use symbol_table::{SymbolTable, VariableKind};
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct SemanticError {
17 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
29pub struct SemanticAnalyzer<'a> {
31 registry: &'a mut dyn LabelRegistry,
32 symbols: SymbolTable,
33}
34
35impl<'a> SemanticAnalyzer<'a> {
36 pub fn new(registry: &'a mut dyn LabelRegistry) -> Self {
38 Self {
39 registry,
40 symbols: SymbolTable::new(),
41 }
42 }
43
44 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 Clause::CreateIndex(_) | Clause::DropIndex(_) => Ok(()),
71 #[cfg(feature = "subgraph")]
72 Clause::CreateSnapshot(_) => Ok(()), #[cfg(feature = "hypergraph")]
74 Clause::CreateHyperedge(hc) => {
75 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 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 fn analyze_match(&mut self, m: &MatchClause) -> Result<(), SemanticError> {
99 self.analyze_pattern_define_with_nullable(&m.pattern, m.optional)?;
100 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 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 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 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 for item in &w.items {
168 self.analyze_expression_refs(&item.expr)?;
169 }
170
171 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 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 self.symbols.reset_scope(&survivors);
204
205 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 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 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 for label in &node.labels {
301 self.registry.get_or_create_label(label);
302 }
303 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 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 for rt in &rel.rel_types {
326 self.registry.get_or_create_rel_type(rt);
327 }
328 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 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 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 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 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 #[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 }
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 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 #[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 assert!(catalog.label_id("Person").is_some());
554 }
555
556 #[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 assert!(catalog.rel_type_id("KNOWS").is_some());
588 }
589
590 #[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 #[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 #[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 assert!(catalog.label_id("Person").is_some());
663 assert!(catalog.prop_key_id("name").is_some());
664 }
665
666 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 assert!(!symbols.get("a").unwrap().nullable);
1159 assert!(symbols.get("b").unwrap().nullable);
1161 assert!(symbols.get("r").unwrap().nullable);
1163 }
1164
1165 #[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 assert!(!symbols.get("a").unwrap().nullable);
1202 assert!(symbols.get("c").unwrap().nullable);
1204 }
1205
1206 #[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 #[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 #[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 #[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 #[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 #[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 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; }
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}