use crate::parser::ast::{
Clause, CreateClause, DeleteClause, Expression, MatchClause, MergeClause, NodePattern, Pattern,
PatternElement, RelationshipPattern, RemoveItem, ReturnClause, SetItem, UnwindClause,
WithClause,
};
use cypherlite_core::LabelRegistry;
pub mod symbol_table;
use symbol_table::{SymbolTable, VariableKind};
#[derive(Debug, Clone, PartialEq)]
pub struct SemanticError {
pub message: String,
}
impl std::fmt::Display for SemanticError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Semantic error: {}", self.message)
}
}
impl std::error::Error for SemanticError {}
pub struct SemanticAnalyzer<'a> {
registry: &'a mut dyn LabelRegistry,
symbols: SymbolTable,
}
impl<'a> SemanticAnalyzer<'a> {
pub fn new(registry: &'a mut dyn LabelRegistry) -> Self {
Self {
registry,
symbols: SymbolTable::new(),
}
}
pub fn analyze(
&mut self,
query: &crate::parser::ast::Query,
) -> Result<SymbolTable, SemanticError> {
for clause in &query.clauses {
self.analyze_clause(clause)?;
}
Ok(self.symbols.clone())
}
fn analyze_clause(&mut self, clause: &Clause) -> Result<(), SemanticError> {
match clause {
Clause::Match(m) => self.analyze_match(m),
Clause::Create(c) => self.analyze_create(c),
Clause::Merge(m) => self.analyze_merge(m),
Clause::Return(r) => self.analyze_return(r),
Clause::With(w) => self.analyze_with(w),
Clause::Set(s) => self.analyze_set(s),
Clause::Delete(d) => self.analyze_delete(d),
Clause::Remove(r) => self.analyze_remove(r),
Clause::Unwind(u) => self.analyze_unwind(u),
Clause::CreateIndex(_) | Clause::DropIndex(_) => Ok(()),
#[cfg(feature = "subgraph")]
Clause::CreateSnapshot(_) => Ok(()), #[cfg(feature = "hypergraph")]
Clause::CreateHyperedge(hc) => {
if let Some(ref var) = hc.variable {
self.symbols
.define(var.clone(), VariableKind::Expression)
.map_err(|msg| SemanticError { message: msg })?;
}
Ok(())
}
#[cfg(feature = "hypergraph")]
Clause::MatchHyperedge(mhc) => {
if let Some(ref var) = mhc.variable {
self.symbols
.define(var.clone(), VariableKind::Expression)
.map_err(|msg| SemanticError { message: msg })?;
}
Ok(())
}
}
}
fn analyze_match(&mut self, m: &MatchClause) -> Result<(), SemanticError> {
self.analyze_pattern_define_with_nullable(&m.pattern, m.optional)?;
if let Some(ref tp) = m.temporal_predicate {
match tp {
crate::parser::ast::TemporalPredicate::AsOf(expr) => {
self.analyze_expression_refs(expr)?;
}
crate::parser::ast::TemporalPredicate::Between(start, end) => {
self.analyze_expression_refs(start)?;
self.analyze_expression_refs(end)?;
}
}
}
if let Some(ref where_expr) = m.where_clause {
self.analyze_expression_refs(where_expr)?;
}
Ok(())
}
fn analyze_create(&mut self, c: &CreateClause) -> Result<(), SemanticError> {
self.analyze_pattern_define(&c.pattern)
}
fn analyze_merge(&mut self, m: &MergeClause) -> Result<(), SemanticError> {
self.analyze_pattern_define(&m.pattern)?;
for item in &m.on_match {
match item {
SetItem::Property { target, value } => {
self.analyze_expression_refs(target)?;
self.analyze_expression_refs(value)?;
}
}
}
for item in &m.on_create {
match item {
SetItem::Property { target, value } => {
self.analyze_expression_refs(target)?;
self.analyze_expression_refs(value)?;
}
}
}
Ok(())
}
fn analyze_return(&mut self, r: &ReturnClause) -> Result<(), SemanticError> {
for item in &r.items {
self.analyze_expression_refs(&item.expr)?;
}
if let Some(ref order_items) = r.order_by {
for oi in order_items {
self.analyze_expression_refs(&oi.expr)?;
}
}
if let Some(ref skip) = r.skip {
self.analyze_expression_refs(skip)?;
}
if let Some(ref limit) = r.limit {
self.analyze_expression_refs(limit)?;
}
Ok(())
}
fn analyze_with(&mut self, w: &WithClause) -> Result<(), SemanticError> {
for item in &w.items {
self.analyze_expression_refs(&item.expr)?;
}
let survivors: Vec<(String, VariableKind)> = w
.items
.iter()
.filter_map(|item| {
let name = match &item.alias {
Some(alias) => alias.clone(),
None => match &item.expr {
Expression::Variable(v) => v.clone(),
_ => return None,
},
};
let kind = if item.alias.is_none() {
if let Expression::Variable(v) = &item.expr {
self.symbols
.get(v)
.map(|info| info.kind)
.unwrap_or(VariableKind::Expression)
} else {
VariableKind::Expression
}
} else {
VariableKind::Expression
};
Some((name, kind))
})
.collect();
self.symbols.reset_scope(&survivors);
if let Some(ref where_expr) = w.where_clause {
self.analyze_expression_refs(where_expr)?;
}
Ok(())
}
fn analyze_set(&mut self, s: &crate::parser::ast::SetClause) -> Result<(), SemanticError> {
for item in &s.items {
match item {
SetItem::Property { target, value } => {
self.analyze_expression_refs(target)?;
self.analyze_expression_refs(value)?;
}
}
}
Ok(())
}
fn analyze_delete(&mut self, d: &DeleteClause) -> Result<(), SemanticError> {
for expr in &d.exprs {
self.analyze_expression_refs(expr)?;
}
Ok(())
}
fn analyze_remove(
&mut self,
r: &crate::parser::ast::RemoveClause,
) -> Result<(), SemanticError> {
for item in &r.items {
match item {
RemoveItem::Property(expr) => {
self.analyze_expression_refs(expr)?;
}
RemoveItem::Label { variable, label } => {
if !self.symbols.is_defined(variable) {
return Err(SemanticError {
message: format!("undefined variable '{}'", variable),
});
}
self.registry.get_or_create_label(label);
}
}
}
Ok(())
}
fn analyze_unwind(&mut self, u: &UnwindClause) -> Result<(), SemanticError> {
self.analyze_expression_refs(&u.expr)?;
self.symbols
.define(u.variable.clone(), VariableKind::Expression)
.map_err(|msg| SemanticError { message: msg })?;
Ok(())
}
fn analyze_pattern_define(&mut self, pattern: &Pattern) -> Result<(), SemanticError> {
self.analyze_pattern_define_with_nullable(pattern, false)
}
fn analyze_pattern_define_with_nullable(
&mut self,
pattern: &Pattern,
nullable: bool,
) -> Result<(), SemanticError> {
for chain in &pattern.chains {
for element in &chain.elements {
match element {
PatternElement::Node(node) => {
self.analyze_node_pattern(node, nullable)?;
}
PatternElement::Relationship(rel) => {
self.analyze_rel_pattern(rel, nullable)?;
}
}
}
}
Ok(())
}
fn analyze_node_pattern(
&mut self,
node: &NodePattern,
nullable: bool,
) -> Result<(), SemanticError> {
if let Some(ref var) = node.variable {
self.symbols
.define_with_nullable(var.clone(), VariableKind::Node, nullable)
.map_err(|msg| SemanticError { message: msg })?;
}
for label in &node.labels {
self.registry.get_or_create_label(label);
}
if let Some(ref props) = node.properties {
for (key, value) in props {
self.registry.get_or_create_prop_key(key);
self.analyze_expression_refs(value)?;
}
}
Ok(())
}
fn analyze_rel_pattern(
&mut self,
rel: &RelationshipPattern,
nullable: bool,
) -> Result<(), SemanticError> {
if let Some(ref var) = rel.variable {
self.symbols
.define_with_nullable(var.clone(), VariableKind::Relationship, nullable)
.map_err(|msg| SemanticError { message: msg })?;
}
for rt in &rel.rel_types {
self.registry.get_or_create_rel_type(rt);
}
if let (Some(min), Some(max)) = (rel.min_hops, rel.max_hops) {
if max < min {
return Err(SemanticError {
message: format!("max_hops ({}) must be >= min_hops ({})", max, min),
});
}
}
const MAX_HOP_LIMIT: u32 = 10;
if let Some(max) = rel.max_hops {
if max > MAX_HOP_LIMIT {
return Err(SemanticError {
message: format!(
"max_hops ({}) exceeds configurable limit ({})",
max, MAX_HOP_LIMIT
),
});
}
}
if let Some(ref props) = rel.properties {
for (key, value) in props {
self.registry.get_or_create_prop_key(key);
self.analyze_expression_refs(value)?;
}
}
Ok(())
}
fn analyze_expression_refs(&self, expr: &Expression) -> Result<(), SemanticError> {
match expr {
Expression::Variable(name) => {
if !self.symbols.is_defined(name) {
return Err(SemanticError {
message: format!("undefined variable '{}'", name),
});
}
Ok(())
}
Expression::Property(inner, _prop_key) => {
self.analyze_expression_refs(inner)
}
Expression::BinaryOp(_, lhs, rhs) => {
self.analyze_expression_refs(lhs)?;
self.analyze_expression_refs(rhs)
}
Expression::UnaryOp(_, operand) => self.analyze_expression_refs(operand),
Expression::FunctionCall { args, .. } => {
for arg in args {
self.analyze_expression_refs(arg)?;
}
Ok(())
}
Expression::IsNull(inner, _) => self.analyze_expression_refs(inner),
Expression::ListLiteral(elements) => {
for elem in elements {
self.analyze_expression_refs(elem)?;
}
Ok(())
}
Expression::Literal(_) | Expression::Parameter(_) | Expression::CountStar => Ok(()),
#[cfg(feature = "hypergraph")]
Expression::TemporalRef { node, timestamp } => {
self.analyze_expression_refs(node)?;
self.analyze_expression_refs(timestamp)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::ast::*;
use std::collections::HashMap;
#[derive(Default)]
struct MockCatalog {
labels: HashMap<String, u32>,
rel_types: HashMap<String, u32>,
prop_keys: HashMap<String, u32>,
next_label: u32,
next_rel: u32,
next_prop: u32,
}
impl LabelRegistry for MockCatalog {
fn get_or_create_label(&mut self, name: &str) -> u32 {
if let Some(&id) = self.labels.get(name) {
return id;
}
let id = self.next_label;
self.next_label += 1;
self.labels.insert(name.to_string(), id);
id
}
fn label_id(&self, name: &str) -> Option<u32> {
self.labels.get(name).copied()
}
fn label_name(&self, _id: u32) -> Option<&str> {
None }
fn get_or_create_rel_type(&mut self, name: &str) -> u32 {
if let Some(&id) = self.rel_types.get(name) {
return id;
}
let id = self.next_rel;
self.next_rel += 1;
self.rel_types.insert(name.to_string(), id);
id
}
fn rel_type_id(&self, name: &str) -> Option<u32> {
self.rel_types.get(name).copied()
}
fn rel_type_name(&self, _id: u32) -> Option<&str> {
None
}
fn get_or_create_prop_key(&mut self, name: &str) -> u32 {
if let Some(&id) = self.prop_keys.get(name) {
return id;
}
let id = self.next_prop;
self.next_prop += 1;
self.prop_keys.insert(name.to_string(), id);
id
}
fn prop_key_id(&self, name: &str) -> Option<u32> {
self.prop_keys.get(name).copied()
}
fn prop_key_name(&self, _id: u32) -> Option<&str> {
None
}
}
fn node(var: Option<&str>, labels: &[&str], props: Option<MapLiteral>) -> PatternElement {
PatternElement::Node(NodePattern {
variable: var.map(|s| s.to_string()),
labels: labels.iter().map(|s| s.to_string()).collect(),
properties: props,
})
}
fn rel(
var: Option<&str>,
types: &[&str],
dir: RelDirection,
props: Option<MapLiteral>,
) -> PatternElement {
PatternElement::Relationship(RelationshipPattern {
variable: var.map(|s| s.to_string()),
rel_types: types.iter().map(|s| s.to_string()).collect(),
direction: dir,
properties: props,
min_hops: None,
max_hops: None,
})
}
fn pattern(chains: Vec<Vec<PatternElement>>) -> Pattern {
Pattern {
chains: chains
.into_iter()
.map(|elements| PatternChain { elements })
.collect(),
}
}
fn var_expr(name: &str) -> Expression {
Expression::Variable(name.to_string())
}
fn prop_expr(var_name: &str, prop: &str) -> Expression {
Expression::Property(Box::new(var_expr(var_name)), prop.to_string())
}
fn return_clause(items: Vec<ReturnItem>) -> ReturnClause {
ReturnClause {
distinct: false,
items,
order_by: None,
skip: None,
limit: None,
}
}
fn return_item(expr: Expression) -> ReturnItem {
ReturnItem { expr, alias: None }
}
#[test]
fn test_valid_match_return_property() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Return(return_clause(vec![return_item(prop_expr("n", "name"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
let symbols = result.unwrap();
assert!(symbols.is_defined("n"));
assert_eq!(symbols.get("n").unwrap().kind, VariableKind::Node);
assert!(catalog.label_id("Person").is_some());
}
#[test]
fn test_valid_match_relationship_pattern() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![
node(Some("a"), &[], None),
rel(Some("r"), &["KNOWS"], RelDirection::Outgoing, None),
node(Some("b"), &[], None),
]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Return(return_clause(vec![return_item(prop_expr("b", "name"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
let symbols = result.unwrap();
assert!(symbols.is_defined("a"));
assert!(symbols.is_defined("r"));
assert!(symbols.is_defined("b"));
assert_eq!(symbols.get("r").unwrap().kind, VariableKind::Relationship);
assert!(catalog.rel_type_id("KNOWS").is_some());
}
#[test]
fn test_invalid_undefined_variable_in_return() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Return(return_clause(vec![return_item(prop_expr("m", "name"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.message.contains("undefined variable 'm'"),
"expected undefined variable error, got: {}",
err.message
);
}
#[test]
fn test_invalid_return_without_match() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![Clause::Return(return_clause(vec![return_item(prop_expr(
"n", "name",
))]))],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message.contains("undefined variable 'n'"));
}
#[test]
fn test_valid_create_with_properties_and_return() {
let mut catalog = MockCatalog::default();
let props = vec![(
"name".to_string(),
Expression::Literal(Literal::String("Alice".to_string())),
)];
let query = Query {
clauses: vec![
Clause::Create(CreateClause {
pattern: pattern(vec![vec![node(Some("n"), &["Person"], Some(props))]]),
}),
Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
let symbols = result.unwrap();
assert!(symbols.is_defined("n"));
assert!(catalog.label_id("Person").is_some());
assert!(catalog.prop_key_id("name").is_some());
}
#[test]
fn test_valid_where_references_defined_variable() {
let mut catalog = MockCatalog::default();
let where_expr = Expression::BinaryOp(
BinaryOp::Gt,
Box::new(prop_expr("n", "age")),
Box::new(Expression::Literal(Literal::Integer(30))),
);
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
temporal_predicate: None,
where_clause: Some(where_expr),
}),
Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
}
#[test]
fn test_invalid_undefined_variable_in_where() {
let mut catalog = MockCatalog::default();
let where_expr = Expression::BinaryOp(
BinaryOp::Gt,
Box::new(prop_expr("m", "age")),
Box::new(Expression::Literal(Literal::Integer(30))),
);
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
temporal_predicate: None,
where_clause: Some(where_expr),
}),
Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.message.contains("undefined variable 'm'"),
"expected undefined variable error, got: {}",
err.message
);
}
#[test]
fn test_valid_anonymous_node_pattern() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(None, &["Person"], None)]]),
temporal_predicate: None,
where_clause: None,
})],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
assert!(catalog.label_id("Person").is_some());
}
#[test]
fn test_valid_redefine_same_kind() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &["Company"], None)]]),
temporal_predicate: None,
where_clause: None,
}),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
}
#[test]
fn test_invalid_redefine_different_kind() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![rel(
Some("n"),
&["KNOWS"],
RelDirection::Outgoing,
None,
)]]),
temporal_predicate: None,
where_clause: None,
}),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("already defined as"));
}
#[test]
fn test_valid_set_clause() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Set(SetClause {
items: vec![SetItem::Property {
target: prop_expr("n", "age"),
value: Expression::Literal(Literal::Integer(42)),
}],
}),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
assert!(analyzer.analyze(&query).is_ok());
}
#[test]
fn test_valid_delete_clause() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Delete(DeleteClause {
detach: true,
exprs: vec![var_expr("n")],
}),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
assert!(analyzer.analyze(&query).is_ok());
}
#[test]
fn test_invalid_delete_undefined_variable() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![Clause::Delete(DeleteClause {
detach: false,
exprs: vec![var_expr("n")],
})],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_err());
assert!(result
.unwrap_err()
.message
.contains("undefined variable 'n'"));
}
#[test]
fn test_valid_merge_defines_variables() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Merge(MergeClause {
pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
on_match: vec![],
on_create: vec![],
}),
Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
assert!(analyzer.analyze(&query).is_ok());
}
#[test]
fn test_valid_merge_on_match_set() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Merge(MergeClause {
pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
on_match: vec![SetItem::Property {
target: prop_expr("n", "seen"),
value: Expression::Literal(Literal::Bool(true)),
}],
on_create: vec![],
}),
Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
assert!(analyzer.analyze(&query).is_ok());
}
#[test]
fn test_invalid_merge_on_create_set_undefined_var() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![Clause::Merge(MergeClause {
pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
on_match: vec![],
on_create: vec![SetItem::Property {
target: prop_expr("m", "created"),
value: Expression::Literal(Literal::Bool(true)),
}],
})],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_err());
assert!(result
.unwrap_err()
.message
.contains("undefined variable 'm'"));
}
#[test]
fn test_valid_function_call_in_return() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Return(return_clause(vec![return_item(Expression::FunctionCall {
name: "count".to_string(),
distinct: false,
args: vec![var_expr("n")],
})])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
assert!(analyzer.analyze(&query).is_ok());
}
fn with_clause(items: Vec<ReturnItem>, where_clause: Option<Expression>) -> WithClause {
WithClause {
distinct: false,
items,
where_clause,
}
}
#[test]
fn test_with_scope_reset_projected_variable_survives() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![
node(Some("n"), &["Person"], None),
rel(Some("r"), &["KNOWS"], RelDirection::Outgoing, None),
node(Some("m"), &["Person"], None),
]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::With(with_clause(vec![return_item(var_expr("n"))], None)),
Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
}
#[test]
fn test_with_scope_reset_non_projected_variable_error() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![
node(Some("n"), &["Person"], None),
rel(Some("r"), &["KNOWS"], RelDirection::Outgoing, None),
node(Some("m"), &["Person"], None),
]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::With(with_clause(vec![return_item(var_expr("n"))], None)),
Clause::Return(return_clause(vec![return_item(var_expr("m"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_err());
assert!(result
.unwrap_err()
.message
.contains("undefined variable 'm'"));
}
#[test]
fn test_with_alias_creates_new_scope() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::With(with_clause(
vec![ReturnItem {
expr: prop_expr("n", "name"),
alias: Some("name".to_string()),
}],
None,
)),
Clause::Return(return_clause(vec![return_item(var_expr("name"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
}
#[test]
fn test_with_alias_original_variable_inaccessible() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::With(with_clause(
vec![ReturnItem {
expr: prop_expr("n", "name"),
alias: Some("name".to_string()),
}],
None,
)),
Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_err());
assert!(result
.unwrap_err()
.message
.contains("undefined variable 'n'"));
}
#[test]
fn test_with_where_references_projected_variable() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &["Person"], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::With(with_clause(
vec![return_item(var_expr("n"))],
Some(Expression::BinaryOp(
BinaryOp::Gt,
Box::new(prop_expr("n", "age")),
Box::new(Expression::Literal(Literal::Integer(30))),
)),
)),
Clause::Return(return_clause(vec![return_item(var_expr("n"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
}
#[test]
fn test_optional_match_variables_are_nullable() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("a"), &["Person"], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Match(MatchClause {
optional: true,
pattern: pattern(vec![vec![
node(Some("a"), &[], None),
rel(Some("r"), &["KNOWS"], RelDirection::Outgoing, None),
node(Some("b"), &[], None),
]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Return(return_clause(vec![
return_item(prop_expr("a", "name")),
return_item(prop_expr("b", "name")),
])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
let symbols = result.unwrap();
assert!(!symbols.get("a").unwrap().nullable);
assert!(symbols.get("b").unwrap().nullable);
assert!(symbols.get("r").unwrap().nullable);
}
#[test]
fn test_optional_match_references_earlier_match_variable() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("a"), &["Person"], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Match(MatchClause {
optional: true,
pattern: pattern(vec![vec![
node(Some("a"), &[], None),
rel(None, &["WORKS_AT"], RelDirection::Outgoing, None),
node(Some("c"), &["Company"], None),
]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Return(return_clause(vec![
return_item(var_expr("a")),
return_item(var_expr("c")),
])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
let symbols = result.unwrap();
assert!(!symbols.get("a").unwrap().nullable);
assert!(symbols.get("c").unwrap().nullable);
}
#[test]
fn test_optional_match_with_where() {
let mut catalog = MockCatalog::default();
let where_expr = Expression::BinaryOp(
BinaryOp::Gt,
Box::new(prop_expr("b", "age")),
Box::new(Expression::Literal(Literal::Integer(20))),
);
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("a"), &["Person"], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Match(MatchClause {
optional: true,
pattern: pattern(vec![vec![
node(Some("a"), &[], None),
rel(None, &["KNOWS"], RelDirection::Outgoing, None),
node(Some("b"), &[], None),
]]),
temporal_predicate: None,
where_clause: Some(where_expr),
}),
Clause::Return(return_clause(vec![
return_item(var_expr("a")),
return_item(var_expr("b")),
])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
}
#[test]
fn test_unwind_defines_variable() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Unwind(UnwindClause {
expr: Expression::ListLiteral(vec![
Expression::Literal(Literal::Integer(1)),
Expression::Literal(Literal::Integer(2)),
]),
variable: "x".to_string(),
}),
Clause::Return(return_clause(vec![return_item(var_expr("x"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
let symbols = result.unwrap();
assert!(symbols.is_defined("x"));
assert_eq!(symbols.get("x").unwrap().kind, VariableKind::Expression);
}
#[test]
fn test_unwind_references_prior_variables() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![node(Some("n"), &[], None)]]),
temporal_predicate: None,
where_clause: None,
}),
Clause::Unwind(UnwindClause {
expr: prop_expr("n", "hobbies"),
variable: "h".to_string(),
}),
Clause::Return(return_clause(vec![return_item(var_expr("h"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_ok());
}
#[test]
fn test_unwind_undefined_variable_in_expr() {
let mut catalog = MockCatalog::default();
let query = Query {
clauses: vec![
Clause::Unwind(UnwindClause {
expr: prop_expr("m", "items"),
variable: "x".to_string(),
}),
Clause::Return(return_clause(vec![return_item(var_expr("x"))])),
],
};
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let result = analyzer.analyze(&query);
assert!(result.is_err());
assert!(result
.unwrap_err()
.message
.contains("undefined variable 'm'"));
}
#[test]
fn test_semantic_error_display() {
let err = SemanticError {
message: "test error".to_string(),
};
assert_eq!(format!("{}", err), "Semantic error: test error");
}
#[test]
fn test_var_length_path_valid() {
let mut catalog = MockCatalog::default();
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let query = Query {
clauses: vec![Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![
node(Some("a"), &["Person"], None),
rel(None, &["KNOWS"], RelDirection::Outgoing, None),
node(Some("b"), &[], None),
]]),
temporal_predicate: None,
where_clause: None,
})],
};
let mut q = query;
if let Clause::Match(ref mut mc) = q.clauses[0] {
if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
rp.min_hops = Some(1);
rp.max_hops = Some(3);
}
}
assert!(analyzer.analyze(&q).is_ok());
}
#[test]
fn test_var_length_path_max_less_than_min() {
let mut catalog = MockCatalog::default();
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let mut query = Query {
clauses: vec![Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![
node(Some("a"), &[], None),
rel(None, &[], RelDirection::Outgoing, None),
node(Some("b"), &[], None),
]]),
temporal_predicate: None,
where_clause: None,
})],
};
if let Clause::Match(ref mut mc) = query.clauses[0] {
if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
rp.min_hops = Some(5);
rp.max_hops = Some(2);
}
}
let result = analyzer.analyze(&query);
assert!(result.is_err());
assert!(result
.expect_err("should fail")
.message
.contains("max_hops"));
}
#[test]
fn test_var_length_path_max_exceeds_limit() {
let mut catalog = MockCatalog::default();
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let mut query = Query {
clauses: vec![Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![
node(Some("a"), &[], None),
rel(None, &[], RelDirection::Outgoing, None),
node(Some("b"), &[], None),
]]),
temporal_predicate: None,
where_clause: None,
})],
};
if let Clause::Match(ref mut mc) = query.clauses[0] {
if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
rp.min_hops = Some(1);
rp.max_hops = Some(100);
}
}
let result = analyzer.analyze(&query);
assert!(result.is_err());
assert!(result.expect_err("should fail").message.contains("exceeds"));
}
#[test]
fn test_var_length_path_unbounded_ok() {
let mut catalog = MockCatalog::default();
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let mut query = Query {
clauses: vec![Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![
node(Some("a"), &[], None),
rel(None, &[], RelDirection::Outgoing, None),
node(Some("b"), &[], None),
]]),
temporal_predicate: None,
where_clause: None,
})],
};
if let Clause::Match(ref mut mc) = query.clauses[0] {
if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
rp.min_hops = Some(1);
rp.max_hops = None; }
}
assert!(analyzer.analyze(&query).is_ok());
}
#[test]
fn test_var_length_path_min_zero_ok() {
let mut catalog = MockCatalog::default();
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let mut query = Query {
clauses: vec![Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![
node(Some("a"), &[], None),
rel(None, &[], RelDirection::Outgoing, None),
node(Some("b"), &[], None),
]]),
temporal_predicate: None,
where_clause: None,
})],
};
if let Clause::Match(ref mut mc) = query.clauses[0] {
if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
rp.min_hops = Some(0);
rp.max_hops = Some(1);
}
}
assert!(analyzer.analyze(&query).is_ok());
}
#[test]
fn test_var_length_path_equal_min_max_ok() {
let mut catalog = MockCatalog::default();
let mut analyzer = SemanticAnalyzer::new(&mut catalog);
let mut query = Query {
clauses: vec![Clause::Match(MatchClause {
optional: false,
pattern: pattern(vec![vec![
node(Some("a"), &[], None),
rel(None, &[], RelDirection::Outgoing, None),
node(Some("b"), &[], None),
]]),
temporal_predicate: None,
where_clause: None,
})],
};
if let Clause::Match(ref mut mc) = query.clauses[0] {
if let PatternElement::Relationship(ref mut rp) = mc.pattern.chains[0].elements[1] {
rp.min_hops = Some(3);
rp.max_hops = Some(3);
}
}
assert!(analyzer.analyze(&query).is_ok());
}
}