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