use std::collections::{HashMap, HashSet};
use crate::types::*;
use super::helpers::*;
use super::*;
pub(in crate::cypher::planner) fn validate_return_variables(
items: &[ReturnItem],
scope_vars: &HashSet<String>,
) -> crate::types::Result<()> {
for item in items {
if matches!(item.expr.kind, ExprKind::Star) {
if scope_vars.is_empty() {
return Err(GraphError::syntax(
"RETURN * is not allowed when there are no variables in scope".to_string(),
));
}
continue;
}
check_expr_variables(&item.expr, scope_vars)?;
}
Ok(())
}
pub(in crate::cypher::planner) fn check_duplicate_columns(
items: &[ReturnItem],
) -> crate::types::Result<()> {
let mut seen: HashSet<String> = HashSet::new();
for item in items {
if matches!(item.expr.kind, ExprKind::Star) {
continue;
}
let col = item
.alias
.clone()
.unwrap_or_else(|| crate::cypher::eval::expr_to_column_name(&item.expr));
if !seen.insert(col.clone()) {
return Err(GraphError::syntax(format!(
"Multiple result columns with the same name are not supported: '{col}'"
)));
}
}
Ok(())
}
pub(in crate::cypher::planner) fn validate_expr_types(
expr: &Expr,
var_types: &HashMap<String, VarKind>,
) -> crate::types::Result<()> {
match &expr.kind {
ExprKind::FunctionCall { name, args, .. } => {
let name_lower = name.to_ascii_lowercase();
if !crate::cypher::eval::is_known_function(&name_lower) {
return Err(unknown_function_error(name, expr.span));
}
if let Some((min, max)) = function_arity(&name_lower) {
let got = args.len();
if got < min || got > max {
let expected = if min == max {
format!("{min}")
} else {
format!("{min} or {max}")
};
return Err(GraphError::Query(crate::types::QueryError::ArgumentError {
phase: crate::types::QueryPhase::SemanticAnalysis,
code: ErrorCode::InvalidNumberOfArguments,
message: format!("{name}() expected {expected} argument(s) but got {got}"),
hint: None,
span: Some(expr.span),
}));
}
}
if let Some(Expr {
kind: ExprKind::Variable(var),
..
}) = args.first()
{
if let Some(kind) = var_types.get(var) {
match name_lower.as_str() {
"type" if *kind == VarKind::Node => {
return Err(GraphError::type_error(
crate::types::QueryPhase::SemanticAnalysis,
"type() requires a relationship".to_string(),
)
.with_code(ErrorCode::InvalidArgumentType));
}
"length" if *kind == VarKind::Node || *kind == VarKind::Relationship => {
return Err(GraphError::type_error(
crate::types::QueryPhase::SemanticAnalysis,
"length() requires a path, string, or list".to_string(),
)
.with_code(ErrorCode::InvalidArgumentType));
}
"size" if *kind == VarKind::Path => {
return Err(GraphError::type_error(
crate::types::QueryPhase::SemanticAnalysis,
"size() requires a string or list".to_string(),
)
.with_code(ErrorCode::InvalidArgumentType));
}
"toboolean" | "tointeger" | "tofloat" | "tostring"
if *kind == VarKind::Node || *kind == VarKind::Relationship =>
{
return Err(GraphError::type_error(
crate::types::QueryPhase::SemanticAnalysis,
format!("{name}() cannot convert a {kind:?}"),
)
.with_code(ErrorCode::InvalidArgumentValue));
}
_ => {}
}
}
}
if name_lower == "score" {
if args.len() != 1 {
return Err(GraphError::Query(crate::types::QueryError::ArgumentError {
phase: crate::types::QueryPhase::SemanticAnalysis,
code: ErrorCode::InvalidArgumentValue,
message: format!("score() requires exactly 1 argument, got {}", args.len()),
hint: Some("usage: score(node_variable)".to_string()),
span: Some(expr.span),
}));
}
if !matches!(&args[0].kind, ExprKind::Variable(_)) {
return Err(GraphError::Query(crate::types::QueryError::ArgumentError {
phase: crate::types::QueryPhase::SemanticAnalysis,
code: ErrorCode::InvalidArgumentValue,
message: "score() argument must be a variable".to_string(),
hint: Some(
"usage: score(n) where n is an FTS-matched node variable".to_string(),
),
span: Some(expr.span),
}));
}
}
for arg in args {
validate_expr_types(arg, var_types)?;
}
}
ExprKind::Property(var, _) => {
if let Some(kind) = var_types.get(var) {
if *kind == VarKind::Path {
return Err(GraphError::type_error(
crate::types::QueryPhase::SemanticAnalysis,
"property access on a path is not allowed".to_string(),
)
.with_code(ErrorCode::InvalidArgumentType));
}
}
}
ExprKind::BinaryOp { left, right, .. } => {
validate_expr_types(left, var_types)?;
validate_expr_types(right, var_types)?;
}
ExprKind::Not(inner) | ExprKind::IsNull(inner) | ExprKind::IsNotNull(inner) => {
validate_expr_types(inner, var_types)?;
}
ExprKind::Quantifier {
list_expr,
variable,
predicate,
..
} => {
if let ExprKind::List(items) = &list_expr.as_ref().kind {
if !items.is_empty() {
let all_strings = items.iter().all(|e| {
matches!(
e.kind,
ExprKind::Literal(crate::cypher::ast::LiteralValue::String(_))
)
});
let all_booleans = items.iter().all(|e| {
matches!(
e.kind,
ExprKind::Literal(crate::cypher::ast::LiteralValue::Bool(_))
)
});
if (all_strings || all_booleans)
&& predicate_uses_arithmetic_on(predicate, variable)
{
let elem_type = if all_strings { "String" } else { "Boolean" };
return Err(GraphError::type_error(
crate::types::QueryPhase::SemanticAnalysis,
format!("{elem_type} is not a valid argument type for arithmetic operations"),
).with_code(ErrorCode::InvalidArgumentType));
}
}
}
validate_expr_types(list_expr, var_types)?;
}
_ => {}
}
Ok(())
}
pub(in crate::cypher::planner) fn predicate_uses_arithmetic_on(expr: &Expr, var: &str) -> bool {
match &expr.kind {
ExprKind::BinaryOp { left, op, right } => {
let is_arith = matches!(
op,
BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow
);
if is_arith && (expr_references_var(left, var) || expr_references_var(right, var)) {
return true;
}
predicate_uses_arithmetic_on(left, var) || predicate_uses_arithmetic_on(right, var)
}
ExprKind::Not(inner) | ExprKind::IsNull(inner) | ExprKind::IsNotNull(inner) => {
predicate_uses_arithmetic_on(inner, var)
}
ExprKind::FunctionCall { args, .. } => {
args.iter().any(|a| predicate_uses_arithmetic_on(a, var))
}
_ => false,
}
}
pub(in crate::cypher::planner) fn expr_references_var(expr: &Expr, var: &str) -> bool {
match &expr.kind {
ExprKind::Variable(v) => v == var,
ExprKind::Property(v, _) => v == var,
ExprKind::BinaryOp { left, right, .. } => {
expr_references_var(left, var) || expr_references_var(right, var)
}
ExprKind::Not(inner) | ExprKind::IsNull(inner) | ExprKind::IsNotNull(inner) => {
expr_references_var(inner, var)
}
ExprKind::FunctionCall { args, .. } => args.iter().any(|a| expr_references_var(a, var)),
_ => false,
}
}
pub(in crate::cypher::planner) fn check_expr_variables(
expr: &Expr,
scope: &HashSet<String>,
) -> crate::types::Result<()> {
let mut props = Vec::new();
collect_property_refs(expr, &mut props);
check_expr_variables_inner(expr, scope, &props)
}
pub(in crate::cypher::planner) fn check_expr_variables_inner(
expr: &Expr,
scope: &HashSet<String>,
seen_props: &[(String, String)],
) -> crate::types::Result<()> {
match &expr.kind {
ExprKind::Variable(var) => {
if !scope.contains(var) {
return Err(undefined_variable_error_with_props(
var, scope, seen_props, expr.span,
));
}
}
ExprKind::Property(var, _) => {
if !scope.contains(var) {
return Err(undefined_variable_error_with_props(
var, scope, seen_props, expr.span,
));
}
}
ExprKind::BinaryOp { left, right, .. } => {
check_expr_variables_inner(left, scope, seen_props)?;
check_expr_variables_inner(right, scope, seen_props)?;
}
ExprKind::Not(inner) | ExprKind::IsNull(inner) | ExprKind::IsNotNull(inner) => {
check_expr_variables_inner(inner, scope, seen_props)?;
}
ExprKind::FunctionCall { args, .. } => {
for arg in args {
check_expr_variables_inner(arg, scope, seen_props)?;
}
}
ExprKind::Case {
operand,
alternatives,
default,
} => {
if let Some(o) = operand {
check_expr_variables_inner(o, scope, seen_props)?;
}
for (cond, result) in alternatives {
check_expr_variables_inner(cond, scope, seen_props)?;
check_expr_variables_inner(result, scope, seen_props)?;
}
if let Some(d) = default {
check_expr_variables_inner(d, scope, seen_props)?;
}
}
ExprKind::List(items) => {
for item in items {
check_expr_variables_inner(item, scope, seen_props)?;
}
}
ExprKind::MapLiteral(pairs) => {
for (_, v) in pairs {
check_expr_variables_inner(v, scope, seen_props)?;
}
}
ExprKind::Index { expr, index } => {
check_expr_variables_inner(expr, scope, seen_props)?;
check_expr_variables_inner(index, scope, seen_props)?;
}
ExprKind::Slice { expr, start, end } => {
check_expr_variables_inner(expr, scope, seen_props)?;
if let Some(s) = start {
check_expr_variables_inner(s, scope, seen_props)?;
}
if let Some(e) = end {
check_expr_variables_inner(e, scope, seen_props)?;
}
}
ExprKind::Literal(_) | ExprKind::Parameter(_) | ExprKind::Star => {}
ExprKind::ListComprehension { list_expr, .. } => {
check_expr_variables_inner(list_expr, scope, seen_props)?;
}
ExprKind::Quantifier { list_expr, .. } => {
check_expr_variables_inner(list_expr, scope, seen_props)?;
}
ExprKind::Exists { .. }
| ExprKind::ExistsSubquery(_)
| ExprKind::PatternPredicate(_)
| ExprKind::PatternComprehension { .. } => {}
ExprKind::DotAccess { expr, .. } => {
check_expr_variables_inner(expr, scope, seen_props)?;
}
ExprKind::HasLabel(var, _) => {
if !scope.contains(var) {
return Err(
GraphError::syntax(var.to_string()).with_code(ErrorCode::UndefinedVariable)
);
}
}
}
Ok(())
}
pub(in crate::cypher::planner) fn validate_pattern_predicate_vars(
expr: &Expr,
scope: &HashSet<String>,
) -> crate::types::Result<()> {
match &expr.kind {
ExprKind::PatternPredicate(pattern) => {
if pattern.elements.len() == 1 {
if let PatternElement::Node(n) = &pattern.elements[0] {
if n.variable.as_ref().is_some_and(|v| scope.contains(v)) {
return Err(GraphError::type_error(
crate::types::QueryPhase::SemanticAnalysis,
"a single node pattern is not a valid predicate".to_string(),
)
.with_code(ErrorCode::InvalidArgumentType));
}
}
}
for elem in &pattern.elements {
match elem {
PatternElement::Node(n) => {
if let Some(ref var) = n.variable {
if !scope.contains(var) {
return Err(GraphError::syntax(var.to_string())
.with_code(ErrorCode::UndefinedVariable));
}
}
}
PatternElement::Relationship(r) => {
if let Some(ref var) = r.variable {
if !scope.contains(var) {
return Err(GraphError::syntax(var.to_string())
.with_code(ErrorCode::UndefinedVariable));
}
}
}
}
}
}
ExprKind::BinaryOp { left, right, .. } => {
validate_pattern_predicate_vars(left, scope)?;
validate_pattern_predicate_vars(right, scope)?;
}
ExprKind::Not(inner) | ExprKind::IsNull(inner) | ExprKind::IsNotNull(inner) => {
validate_pattern_predicate_vars(inner, scope)?;
}
_ => {}
}
Ok(())
}
pub(in crate::cypher::planner) fn reject_pattern_predicates(
expr: &Expr,
) -> crate::types::Result<()> {
match &expr.kind {
ExprKind::PatternPredicate(_) => {
return Err(
GraphError::syntax("pattern expressions are not allowed here".to_string())
.with_code(ErrorCode::UnexpectedSyntax),
);
}
ExprKind::FunctionCall { args, .. } => {
for arg in args {
reject_pattern_predicates(arg)?;
}
}
ExprKind::BinaryOp { left, right, .. } => {
reject_pattern_predicates(left)?;
reject_pattern_predicates(right)?;
}
ExprKind::Not(inner) | ExprKind::IsNull(inner) | ExprKind::IsNotNull(inner) => {
reject_pattern_predicates(inner)?;
}
ExprKind::List(items) => {
for item in items {
reject_pattern_predicates(item)?;
}
}
ExprKind::Index { expr, index } => {
reject_pattern_predicates(expr)?;
reject_pattern_predicates(index)?;
}
ExprKind::DotAccess { expr, .. } => {
reject_pattern_predicates(expr)?;
}
ExprKind::Case {
operand,
alternatives,
default,
} => {
if let Some(o) = operand {
reject_pattern_predicates(o)?;
}
for (cond, result) in alternatives {
reject_pattern_predicates(cond)?;
reject_pattern_predicates(result)?;
}
if let Some(d) = default {
reject_pattern_predicates(d)?;
}
}
_ => {}
}
Ok(())
}
pub(in crate::cypher::planner) fn collect_pattern_variables(
patterns: &[Pattern],
) -> HashSet<String> {
let mut vars = HashSet::new();
for pattern in patterns {
if let Some(ref path_var) = pattern.path_variable {
vars.insert(path_var.clone());
}
for elem in &pattern.elements {
match elem {
PatternElement::Node(n) => {
if let Some(ref var) = n.variable {
vars.insert(var.clone());
}
}
PatternElement::Relationship(r) => {
if let Some(ref var) = r.variable {
vars.insert(var.clone());
}
}
}
}
}
vars
}
pub(in crate::cypher::planner) fn validate_create_patterns(
patterns: &[Pattern],
scope: &HashSet<String>,
) -> crate::types::Result<()> {
let mut local_scope: HashSet<String> = scope.clone();
for pattern in patterns {
for elem in &pattern.elements {
match elem {
PatternElement::Node(n) => {
for expr in n.properties.values() {
check_expr_variables(expr, &local_scope)?;
}
if let Some(ref var) = n.variable {
local_scope.insert(var.clone());
}
}
PatternElement::Relationship(rel) => {
if rel.rel_types.is_empty() {
return Err(GraphError::syntax(
"a relationship must have exactly one type in CREATE",
)
.with_code(ErrorCode::NoSingleRelationshipType));
}
if rel.rel_types.len() > 1 {
return Err(GraphError::syntax(
"a relationship must have exactly one type in CREATE",
)
.with_code(ErrorCode::NoSingleRelationshipType));
}
if rel.direction == RelDirection::Undirected {
return Err(GraphError::syntax(
"only directed relationships are supported in CREATE",
)
.with_code(ErrorCode::RequiresDirectedRelationship));
}
for expr in rel.properties.values() {
check_expr_variables(expr, &local_scope)?;
}
if let Some(ref var) = rel.variable {
local_scope.insert(var.clone());
}
}
}
}
}
Ok(())
}
pub(in crate::cypher::planner) fn validate_merge_pattern(
pattern: &Pattern,
scope: &HashSet<String>,
on_create: &[SetItem],
on_match: &[SetItem],
) -> crate::types::Result<()> {
let mut merge_vars = scope.clone();
for elem in &pattern.elements {
match elem {
PatternElement::Node(n) => {
if let Some(ref var) = n.variable {
merge_vars.insert(var.clone());
}
}
PatternElement::Relationship(rel) => {
if rel.rel_types.is_empty() {
return Err(GraphError::syntax(
"a relationship must have exactly one type in MERGE",
)
.with_code(ErrorCode::NoSingleRelationshipType));
}
if rel.rel_types.len() > 1 {
return Err(GraphError::syntax(
"a relationship must have exactly one type in MERGE",
)
.with_code(ErrorCode::NoSingleRelationshipType));
}
if let Some(ref var) = rel.variable {
merge_vars.insert(var.clone());
}
}
}
}
for item in on_create.iter().chain(on_match.iter()) {
match item {
SetItem::Property(a) => {
if !merge_vars.contains(&a.variable) {
return Err(GraphError::syntax(a.variable.to_string())
.with_code(ErrorCode::UndefinedVariable));
}
check_expr_variables(&a.value, &merge_vars)?;
}
SetItem::Label { variable, .. } => {
if !merge_vars.contains(variable) {
return Err(GraphError::syntax(variable.to_string())
.with_code(ErrorCode::UndefinedVariable));
}
}
SetItem::MapOverwrite { variable, value } | SetItem::MapMerge { variable, value } => {
if !merge_vars.contains(variable) {
return Err(GraphError::syntax(variable.to_string())
.with_code(ErrorCode::UndefinedVariable));
}
check_expr_variables(value, &merge_vars)?;
}
}
}
Ok(())
}
pub(in crate::cypher::planner) fn validate_set_variables(
items: &[SetItem],
scope: &HashSet<String>,
) -> crate::types::Result<()> {
for item in items {
match item {
SetItem::Property(a) => {
if !scope.contains(&a.variable) {
return Err(GraphError::syntax(a.variable.to_string())
.with_code(ErrorCode::UndefinedVariable));
}
check_expr_variables(&a.value, scope)?;
reject_pattern_predicates(&a.value)?;
}
SetItem::Label { variable, .. } => {
if !scope.contains(variable) {
return Err(GraphError::syntax(variable.to_string())
.with_code(ErrorCode::UndefinedVariable));
}
}
SetItem::MapOverwrite { variable, value } | SetItem::MapMerge { variable, value } => {
if !scope.contains(variable) {
return Err(GraphError::syntax(variable.to_string())
.with_code(ErrorCode::UndefinedVariable));
}
check_expr_variables(value, scope)?;
}
}
}
Ok(())
}
pub(in crate::cypher::planner) fn validate_delete_exprs(
exprs: &[crate::cypher::ast::Expr],
scope: &HashSet<String>,
) -> crate::types::Result<()> {
use crate::cypher::ast::ExprKind;
for expr in exprs {
match &expr.kind {
ExprKind::Variable(var) => {
if !scope.contains(var) {
return Err(
GraphError::syntax(var.to_string()).with_code(ErrorCode::UndefinedVariable)
);
}
}
ExprKind::HasLabel(..) => {
return Err(
GraphError::syntax("cannot delete a label predicate expression")
.with_code(ErrorCode::InvalidDelete),
);
}
ExprKind::Literal(_) => {
return Err(
GraphError::syntax("DELETE requires a node, relationship, or path")
.with_code(ErrorCode::InvalidArgumentType),
);
}
ExprKind::BinaryOp { .. } => {
return Err(
GraphError::syntax("DELETE requires a node, relationship, or path")
.with_code(ErrorCode::InvalidArgumentType),
);
}
_ => {
if let Some(root) = extract_root_variable(expr) {
if !scope.contains(&root) {
return Err(GraphError::syntax(root.to_string())
.with_code(ErrorCode::UndefinedVariable));
}
}
}
}
}
Ok(())
}
pub(in crate::cypher::planner) fn extract_root_variable(
expr: &crate::cypher::ast::Expr,
) -> Option<String> {
use crate::cypher::ast::ExprKind;
match &expr.kind {
ExprKind::Variable(v) => Some(v.clone()),
ExprKind::Property(v, _) => Some(v.clone()),
ExprKind::Index { expr: base, .. } => extract_root_variable(base),
_ => None,
}
}
pub(in crate::cypher::planner) fn validate_no_aggregation_in_order_by(
order_by: &[SortItem],
) -> crate::types::Result<()> {
for item in order_by {
if is_aggregate_fn(&item.expr) {
return Err(GraphError::syntax(
"aggregation functions are not allowed in ORDER BY when there is no aggregation in the preceding WITH/RETURN"
).with_code(ErrorCode::InvalidAggregation));
}
}
Ok(())
}
pub(in crate::cypher::planner) fn validate_return_order_by_with_aggregates(
return_items: &[ReturnItem],
order_by: &[SortItem],
) -> crate::types::Result<()> {
let mut returned_cols: HashSet<String> = HashSet::new();
for item in return_items {
returned_cols.insert(crate::cypher::eval::expr_to_column_name(&item.expr));
if let Some(ref alias) = item.alias {
returned_cols.insert(alias.clone());
}
}
for sort_item in order_by {
if is_aggregate_fn(&sort_item.expr) {
let mut non_agg_leaves = Vec::new();
collect_non_aggregate_leaves(&sort_item.expr, &mut non_agg_leaves);
for leaf in &non_agg_leaves {
let col = crate::cypher::eval::expr_to_column_name(leaf);
if !returned_cols.contains(&col) {
return Err(
GraphError::syntax("ORDER BY references a variable not in RETURN")
.with_code(ErrorCode::UndefinedVariable),
);
}
}
}
}
Ok(())
}
pub(in crate::cypher::planner) fn validate_distinct_order_by(
return_items: &[ReturnItem],
order_by: &[SortItem],
distinct: bool,
) -> crate::types::Result<()> {
if !distinct || order_by.is_empty() {
return Ok(());
}
let mut projected: HashSet<String> = HashSet::new();
let mut returned_vars: HashSet<String> = HashSet::new();
for item in return_items {
if let Some(ref alias) = item.alias {
projected.insert(alias.clone());
}
projected.insert(crate::cypher::eval::expr_to_column_name(&item.expr));
if let ExprKind::Variable(var) = &item.expr.kind {
returned_vars.insert(var.clone());
}
}
for sort_item in order_by {
let col = crate::cypher::eval::expr_to_column_name(&sort_item.expr);
if projected.contains(&col) {
continue;
}
if let ExprKind::Property(var, _) = &sort_item.expr.kind {
if returned_vars.contains(var) {
continue;
}
}
return Err(
GraphError::syntax("ORDER BY references a variable not in RETURN DISTINCT")
.with_code(ErrorCode::UndefinedVariable),
);
}
Ok(())
}
pub(in crate::cypher::planner) fn validate_with_order_by_scope_input(
input_scope: &HashSet<String>,
with_items: &[ReturnItem],
order_by: &[SortItem],
) -> crate::types::Result<()> {
let mut scope = input_scope.clone();
for item in with_items {
if let Some(ref alias) = item.alias {
scope.insert(alias.clone());
}
if matches!(item.expr.kind, ExprKind::Star) {
return Ok(()); }
}
for sort_item in order_by {
validate_expr_in_scope(&sort_item.expr, &scope)?;
}
Ok(())
}
pub(in crate::cypher::planner) fn validate_agg_order_by_scope(
with_items: &[ReturnItem],
order_by: &[SortItem],
) -> crate::types::Result<()> {
let mut scope = HashSet::new();
for item in with_items {
if let Some(ref alias) = item.alias {
scope.insert(alias.clone());
}
if !is_aggregate_fn(&item.expr) {
collect_variables_from_expr(&item.expr, &mut scope);
}
}
for sort_item in order_by {
validate_non_agg_leaves_in_scope(&sort_item.expr, &scope)?;
}
Ok(())
}
pub(in crate::cypher::planner) fn collect_variables_from_expr(
expr: &Expr,
vars: &mut HashSet<String>,
) {
match &expr.kind {
ExprKind::Variable(name) => {
vars.insert(name.clone());
}
ExprKind::Property(var, _) => {
vars.insert(var.clone());
}
ExprKind::BinaryOp { left, right, .. } => {
collect_variables_from_expr(left, vars);
collect_variables_from_expr(right, vars);
}
ExprKind::FunctionCall { args, .. } => {
for arg in args {
collect_variables_from_expr(arg, vars);
}
}
ExprKind::Not(inner) | ExprKind::IsNull(inner) | ExprKind::IsNotNull(inner) => {
collect_variables_from_expr(inner, vars);
}
_ => {}
}
}
pub(in crate::cypher::planner) fn validate_non_agg_leaves_in_scope(
expr: &Expr,
scope: &HashSet<String>,
) -> crate::types::Result<()> {
match &expr.kind {
ExprKind::FunctionCall { name, args, .. } => {
if matches!(
name.to_ascii_lowercase().as_str(),
"count"
| "sum"
| "avg"
| "min"
| "max"
| "collect"
| "percentiledisc"
| "percentilecont"
| "stdev"
| "stdevp"
) {
return Ok(());
}
for arg in args {
validate_non_agg_leaves_in_scope(arg, scope)?;
}
}
ExprKind::Variable(name) if !scope.contains(name) => {
return Err(undefined_variable_error(name, scope, expr.span));
}
ExprKind::Property(var, _) if !scope.contains(var) => {
return Err(undefined_variable_error(var, scope, expr.span));
}
ExprKind::BinaryOp { left, right, .. } => {
validate_non_agg_leaves_in_scope(left, scope)?;
validate_non_agg_leaves_in_scope(right, scope)?;
}
ExprKind::Not(inner) | ExprKind::IsNull(inner) | ExprKind::IsNotNull(inner) => {
validate_non_agg_leaves_in_scope(inner, scope)?;
}
_ => {} }
Ok(())
}
pub(in crate::cypher::planner) fn validate_expr_in_scope(
expr: &Expr,
scope: &HashSet<String>,
) -> crate::types::Result<()> {
match &expr.kind {
ExprKind::Variable(name) if !scope.contains(name) => {
return Err(undefined_variable_error(name, scope, expr.span));
}
ExprKind::Property(var, _) if !scope.contains(var) => {
return Err(undefined_variable_error(var, scope, expr.span));
}
ExprKind::BinaryOp { left, right, .. } => {
validate_expr_in_scope(left, scope)?;
validate_expr_in_scope(right, scope)?;
}
ExprKind::FunctionCall { args, .. } => {
for arg in args {
validate_expr_in_scope(arg, scope)?;
}
}
ExprKind::Not(inner) | ExprKind::IsNull(inner) | ExprKind::IsNotNull(inner) => {
validate_expr_in_scope(inner, scope)?;
}
ExprKind::List(items) => {
for item in items {
validate_expr_in_scope(item, scope)?;
}
}
_ => {} }
Ok(())
}
pub(in crate::cypher::planner) fn validate_no_aggregation_in_list_comp(
expr: &Expr,
) -> crate::types::Result<()> {
match &expr.kind {
ExprKind::ListComprehension {
map_expr,
filter,
list_expr,
..
} => {
if let Some(ref me) = map_expr {
if is_aggregate_fn(me) {
return Err(GraphError::syntax(
"aggregation functions are not allowed in list comprehension",
)
.with_code(ErrorCode::InvalidAggregation));
}
validate_no_aggregation_in_list_comp(me)?;
}
if let Some(ref fe) = filter {
validate_no_aggregation_in_list_comp(fe)?;
}
validate_no_aggregation_in_list_comp(list_expr)?;
}
ExprKind::FunctionCall { args, .. } => {
for arg in args {
validate_no_aggregation_in_list_comp(arg)?;
}
}
ExprKind::BinaryOp { left, right, .. } => {
validate_no_aggregation_in_list_comp(left)?;
validate_no_aggregation_in_list_comp(right)?;
}
ExprKind::Not(inner) | ExprKind::IsNull(inner) | ExprKind::IsNotNull(inner) => {
validate_no_aggregation_in_list_comp(inner)?;
}
ExprKind::List(items) => {
for item in items {
validate_no_aggregation_in_list_comp(item)?;
}
}
_ => {}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::cypher::planner) enum WithValueKind {
Scalar, Node, }
pub(in crate::cypher::planner) fn infer_with_value_kind(expr: &Expr) -> WithValueKind {
match &expr.kind {
ExprKind::Literal(_) => WithValueKind::Scalar,
ExprKind::MapLiteral(_) => WithValueKind::Scalar,
ExprKind::Property(_, _) => WithValueKind::Scalar,
ExprKind::BinaryOp { .. } => WithValueKind::Scalar,
ExprKind::List(_) => WithValueKind::Scalar,
_ => WithValueKind::Node, }
}
pub(in crate::cypher::planner) fn validate_merge_variable_rebinding(
pattern: &Pattern,
scope: &HashSet<String>,
) -> crate::types::Result<()> {
if pattern.elements.len() == 1 {
if let PatternElement::Node(n) = &pattern.elements[0] {
if let Some(ref var) = n.variable {
if scope.contains(var) {
return Err(
GraphError::syntax(format!("variable `{var}` already bound"))
.with_code(ErrorCode::VariableAlreadyBound),
);
}
}
}
}
if pattern.elements.len() == 3 {
for elem in &pattern.elements {
if let PatternElement::Relationship(rel) = elem {
if let Some(ref var) = rel.variable {
if scope.contains(var) {
return Err(
GraphError::syntax(format!("variable `{var}` already bound"))
.with_code(ErrorCode::VariableAlreadyBound),
);
}
}
}
if let PatternElement::Node(n) = elem {
if let Some(ref var) = n.variable {
if scope.contains(var) && !n.labels.is_empty() {
return Err(
GraphError::syntax(format!("variable `{var}` already bound"))
.with_code(ErrorCode::VariableAlreadyBound),
);
}
}
}
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(in crate::cypher::planner) enum VarKind {
Node,
Relationship,
Path,
}
pub(in crate::cypher::planner) fn validate_variable_types(
patterns: &[Pattern],
) -> crate::types::Result<HashMap<String, VarKind>> {
let mut types: HashMap<String, VarKind> = HashMap::new();
validate_variable_types_with_map(patterns, &mut types)?;
Ok(types)
}
pub(in crate::cypher::planner) fn validate_variable_types_with_map(
patterns: &[Pattern],
types: &mut std::collections::HashMap<String, VarKind>,
) -> crate::types::Result<()> {
let prior_rels: HashSet<String> = types
.iter()
.filter(|(_, &k)| k == VarKind::Relationship)
.map(|(n, _)| n.clone())
.collect();
for pattern in patterns {
if let Some(ref path_var) = pattern.path_variable {
if let Some(&existing) = types.get(path_var) {
if existing != VarKind::Path {
return Err(GraphError::Query(crate::types::QueryError::SyntaxError {
phase: crate::types::QueryPhase::SemanticAnalysis,
message: format!(
"variable '{path_var}' already defined with a different type"
),
code: ErrorCode::Other,
hint: None,
span: None,
}));
}
} else {
types.insert(path_var.clone(), VarKind::Path);
}
}
for elem in &pattern.elements {
match elem {
PatternElement::Node(n) => {
if let Some(ref var) = n.variable {
if let Some(&existing) = types.get(var) {
if existing != VarKind::Node {
return Err(GraphError::Query(
crate::types::QueryError::SyntaxError {
phase: crate::types::QueryPhase::SemanticAnalysis,
message: format!(
"variable '{var}' already defined with a different type"
),
code: ErrorCode::Other,
hint: None,
span: None,
},
));
}
} else {
types.insert(var.clone(), VarKind::Node);
}
}
}
PatternElement::Relationship(r) => {
if let Some(ref var) = r.variable {
if let Some(&existing) = types.get(var) {
if existing != VarKind::Relationship {
return Err(GraphError::Query(
crate::types::QueryError::SyntaxError {
phase: crate::types::QueryPhase::SemanticAnalysis,
message: format!(
"variable '{var}' already defined with a different type"
),
code: ErrorCode::Other,
hint: None,
span: None,
},
));
}
if !prior_rels.contains(var) {
return Err(GraphError::syntax(format!(
"cannot use relationship variable '{var}' more than once in a pattern"
)).with_code(ErrorCode::VariableAlreadyBound));
}
} else {
types.insert(var.clone(), VarKind::Relationship);
}
}
}
}
}
}
Ok(())
}
#[cfg(test)]
mod score_validation_tests {
use crate::cypher::parser::parse;
use crate::cypher::planner::plan;
use crate::types::{ErrorCode, GraphError, QueryError};
use rusqlite::Connection;
fn plan_cypher(q: &str) -> crate::types::Result<crate::cypher::ir::LogicalOp> {
let conn = Connection::open_in_memory().unwrap();
crate::schema::init_schema(&conn).unwrap();
let stmt = parse(q)?;
plan(&conn, &stmt)
}
#[test]
fn score_with_zero_args_errors_at_plan_time() {
let err = plan_cypher("MATCH (n:Person) RETURN score()").unwrap_err();
match err {
GraphError::Query(QueryError::ArgumentError {
code: ErrorCode::InvalidArgumentValue,
message,
..
}) => {
assert!(message.contains("score"), "message was: {message}");
}
other => panic!("expected ArgumentError(InvalidArgumentValue), got {other:?}"),
}
}
#[test]
fn score_with_two_args_errors_at_plan_time() {
let err = plan_cypher("MATCH (n:Person), (m:Person) RETURN score(n, m)").unwrap_err();
match err {
GraphError::Query(QueryError::ArgumentError {
code: ErrorCode::InvalidArgumentValue,
..
}) => {}
other => panic!("expected ArgumentError(InvalidArgumentValue), got {other:?}"),
}
}
#[test]
fn score_with_literal_arg_errors_at_plan_time() {
let err = plan_cypher("MATCH (n:Person) RETURN score(123)").unwrap_err();
match err {
GraphError::Query(QueryError::ArgumentError {
code: ErrorCode::InvalidArgumentValue,
message,
..
}) => {
assert!(
message.contains("variable") || message.contains("Variable"),
"message was: {message}"
);
}
other => panic!("expected ArgumentError(InvalidArgumentValue), got {other:?}"),
}
}
#[test]
fn score_with_property_access_arg_errors_at_plan_time() {
let err = plan_cypher("MATCH (n:Person) RETURN score(n.bio)").unwrap_err();
match err {
GraphError::Query(QueryError::ArgumentError {
code: ErrorCode::InvalidArgumentValue,
..
}) => {}
other => panic!("expected ArgumentError(InvalidArgumentValue), got {other:?}"),
}
}
#[test]
fn score_with_variable_arg_plans_successfully() {
plan_cypher("MATCH (n:Person) RETURN score(n)").expect("score(n) should plan");
}
}