use std::collections::HashMap;
use crate::error::StorageError;
use crate::gql::ast::{
CreateClause, DeleteClause, DetachDeleteClause, EdgeDirection, Expression, ForeachClause,
ForeachMutation, MatchClause, MergeClause, MutationClause, MutationQuery, Pattern,
PatternElement, RemoveClause, ReturnClause, ReturnItem, SetClause, SetItem, Statement,
WhereClause,
};
use crate::gql::error::CompileError;
use crate::schema::{
validate_edge, validate_property_update, validate_vertex, GraphSchema, ValidationMode,
ValidationResult,
};
use crate::storage::{GraphStorage, GraphStorageMut};
use crate::value::{EdgeId, Value, VertexId};
#[derive(Debug, thiserror::Error)]
pub enum MutationError {
#[error("Compilation error: {0}")]
Compile(#[from] CompileError),
#[error("Storage error: {0}")]
Storage(#[from] StorageError),
#[error("Unbound variable '{0}': variable was not bound by MATCH or CREATE")]
UnboundVariable(String),
#[error("Cannot delete vertex {0:?}: vertex has connected edges. Use DETACH DELETE to remove edges automatically.")]
VertexHasEdges(VertexId),
#[error("Invalid element type for {operation}: expected {expected}, got {actual}")]
InvalidElementType {
operation: &'static str,
expected: &'static str,
actual: String,
},
#[error("CREATE requires a label for new vertices")]
MissingLabel,
#[error("CREATE edge requires both source and target vertices")]
IncompleteEdge,
#[error("Schema validation error: {0}")]
Schema(#[from] crate::schema::SchemaError),
}
#[derive(Debug)]
pub struct MutationContext<'s, S: GraphStorage + GraphStorageMut> {
storage: &'s mut S,
vertex_bindings: HashMap<String, VertexId>,
edge_bindings: HashMap<String, EdgeId>,
value_bindings: HashMap<String, Value>,
schema: Option<&'s GraphSchema>,
}
impl<'s, S: GraphStorage + GraphStorageMut> MutationContext<'s, S> {
pub fn new(storage: &'s mut S) -> Self {
Self {
storage,
vertex_bindings: HashMap::new(),
edge_bindings: HashMap::new(),
value_bindings: HashMap::new(),
schema: None,
}
}
pub fn with_schema(storage: &'s mut S, schema: Option<&'s GraphSchema>) -> Self {
Self {
storage,
vertex_bindings: HashMap::new(),
edge_bindings: HashMap::new(),
value_bindings: HashMap::new(),
schema,
}
}
pub fn schema(&self) -> Option<&GraphSchema> {
self.schema
}
pub fn bind_vertex(&mut self, variable: &str, id: VertexId) {
self.vertex_bindings.insert(variable.to_string(), id);
}
pub fn bind_edge(&mut self, variable: &str, id: EdgeId) {
self.edge_bindings.insert(variable.to_string(), id);
}
pub fn bind_value(&mut self, variable: &str, value: Value) {
self.value_bindings.insert(variable.to_string(), value);
}
pub fn get_vertex(&self, variable: &str) -> Option<VertexId> {
self.vertex_bindings.get(variable).copied()
}
pub fn get_edge(&self, variable: &str) -> Option<EdgeId> {
self.edge_bindings.get(variable).copied()
}
pub fn get_value(&self, variable: &str) -> Option<&Value> {
self.value_bindings.get(variable)
}
pub fn get_element(&self, variable: &str) -> Option<Element> {
if let Some(vid) = self.vertex_bindings.get(variable) {
return Some(Element::Vertex(*vid));
}
if let Some(eid) = self.edge_bindings.get(variable) {
return Some(Element::Edge(*eid));
}
None
}
pub fn is_bound(&self, variable: &str) -> bool {
self.vertex_bindings.contains_key(variable)
|| self.edge_bindings.contains_key(variable)
|| self.value_bindings.contains_key(variable)
}
pub fn storage_mut(&mut self) -> &mut S {
self.storage
}
pub fn storage(&self) -> &S {
self.storage
}
pub fn clear_bindings(&mut self) {
self.vertex_bindings.clear();
self.edge_bindings.clear();
self.value_bindings.clear();
}
pub fn clone_bindings(&self) -> (HashMap<String, VertexId>, HashMap<String, EdgeId>) {
(self.vertex_bindings.clone(), self.edge_bindings.clone())
}
pub fn clone_all_bindings(
&self,
) -> (
HashMap<String, VertexId>,
HashMap<String, EdgeId>,
HashMap<String, Value>,
) {
(
self.vertex_bindings.clone(),
self.edge_bindings.clone(),
self.value_bindings.clone(),
)
}
pub fn restore_bindings(
&mut self,
vertex_bindings: HashMap<String, VertexId>,
edge_bindings: HashMap<String, EdgeId>,
) {
self.vertex_bindings = vertex_bindings;
self.edge_bindings = edge_bindings;
}
pub fn remove_value_binding(&mut self, variable: &str) {
self.value_bindings.remove(variable);
}
}
#[derive(Debug, Clone, Copy)]
pub enum Element {
Vertex(VertexId),
Edge(EdgeId),
}
pub fn execute_mutation<S: GraphStorage + GraphStorageMut>(
stmt: &Statement,
storage: &mut S,
) -> Result<Vec<Value>, MutationError> {
match stmt {
Statement::Mutation(mutation) => execute_mutation_query(mutation.as_ref(), storage),
Statement::Query(_) | Statement::Union { .. } => Err(MutationError::Compile(
CompileError::UnsupportedFeature("Expected mutation statement, got read query".into()),
)),
Statement::Ddl(_) => Err(MutationError::Compile(CompileError::UnsupportedFeature(
"Expected mutation statement, got DDL statement. Use execute_ddl() instead.".into(),
))),
}
}
pub fn execute_mutation_with_schema<S: GraphStorage + GraphStorageMut>(
stmt: &Statement,
storage: &mut S,
schema: Option<&GraphSchema>,
) -> Result<Vec<Value>, MutationError> {
match stmt {
Statement::Mutation(mutation) => {
execute_mutation_query_with_schema(mutation.as_ref(), storage, schema)
}
Statement::Query(_) | Statement::Union { .. } => Err(MutationError::Compile(
CompileError::UnsupportedFeature("Expected mutation statement, got read query".into()),
)),
Statement::Ddl(_) => Err(MutationError::Compile(CompileError::UnsupportedFeature(
"Expected mutation statement, got DDL statement. Use execute_ddl() instead.".into(),
))),
}
}
pub fn execute_mutation_query<S: GraphStorage + GraphStorageMut>(
query: &MutationQuery,
storage: &mut S,
) -> Result<Vec<Value>, MutationError> {
execute_mutation_query_with_schema(query, storage, None)
}
pub fn execute_mutation_query_with_schema<S: GraphStorage + GraphStorageMut>(
query: &MutationQuery,
storage: &mut S,
schema: Option<&GraphSchema>,
) -> Result<Vec<Value>, MutationError> {
let mut ctx = MutationContext::with_schema(storage, schema);
let mut results = Vec::new();
if let Some(match_clause) = &query.match_clause {
let match_results = execute_match(&ctx, match_clause, &query.where_clause)?;
if match_results.is_empty() {
return Ok(vec![]);
}
for bindings in match_results {
ctx.vertex_bindings = bindings.vertices;
ctx.edge_bindings = bindings.edges;
for mutation in &query.mutations {
execute_mutation_clause(&mut ctx, mutation)?;
}
for foreach_clause in &query.foreach_clauses {
execute_foreach(&mut ctx, foreach_clause)?;
}
if let Some(return_clause) = &query.return_clause {
if let Some(result) = evaluate_return(&ctx, return_clause)? {
results.push(result);
}
}
}
} else {
for mutation in &query.mutations {
execute_mutation_clause(&mut ctx, mutation)?;
}
for foreach_clause in &query.foreach_clauses {
execute_foreach(&mut ctx, foreach_clause)?;
}
if let Some(return_clause) = &query.return_clause {
if let Some(result) = evaluate_return(&ctx, return_clause)? {
results.push(result);
}
}
}
Ok(results)
}
#[derive(Debug, Clone, Default)]
struct MatchBindings {
vertices: HashMap<String, VertexId>,
edges: HashMap<String, EdgeId>,
}
fn execute_match<S: GraphStorage + GraphStorageMut>(
ctx: &MutationContext<S>,
match_clause: &MatchClause,
where_clause: &Option<WhereClause>,
) -> Result<Vec<MatchBindings>, MutationError> {
let mut all_bindings = Vec::new();
for pattern in &match_clause.patterns {
let pattern_bindings = match_pattern(ctx, pattern)?;
let filtered = if let Some(where_cl) = where_clause {
pattern_bindings
.into_iter()
.filter(|b| evaluate_where_filter(ctx, where_cl, b))
.collect()
} else {
pattern_bindings
};
all_bindings.extend(filtered);
}
Ok(all_bindings)
}
fn match_pattern<S: GraphStorage + GraphStorageMut>(
ctx: &MutationContext<S>,
pattern: &Pattern,
) -> Result<Vec<MatchBindings>, MutationError> {
if pattern.elements.is_empty() {
return Ok(vec![]);
}
let first_element = &pattern.elements[0];
let PatternElement::Node(first_node) = first_element else {
return Err(MutationError::Compile(
CompileError::PatternMustStartWithNode,
));
};
let mut current_bindings: Vec<MatchBindings> = Vec::new();
for vertex in ctx.storage().all_vertices() {
if !first_node.labels.is_empty() && !first_node.labels.contains(&vertex.label) {
continue;
}
let mut matches = true;
for (key, lit) in &first_node.properties {
let expected: Value = lit.clone().into();
match vertex.properties.get(key) {
Some(actual) if *actual == expected => {}
_ => {
matches = false;
break;
}
}
}
if matches {
let mut bindings = MatchBindings::default();
if let Some(var) = &first_node.variable {
bindings.vertices.insert(var.clone(), vertex.id);
}
current_bindings.push(bindings);
}
}
let mut element_iter = pattern.elements.iter().skip(1).peekable();
while let Some(element) = element_iter.next() {
match element {
PatternElement::Edge(edge) => {
let Some(PatternElement::Node(target_node)) = element_iter.next() else {
continue; };
let mut new_bindings = Vec::new();
for binding in ¤t_bindings {
let source_id = binding
.vertices
.values()
.last()
.copied()
.ok_or_else(|| MutationError::UnboundVariable("source".to_string()))?;
let edges: Vec<_> = match edge.direction {
EdgeDirection::Outgoing => ctx.storage().out_edges(source_id).collect(),
EdgeDirection::Incoming => ctx.storage().in_edges(source_id).collect(),
EdgeDirection::Both => {
let mut all = ctx.storage().out_edges(source_id).collect::<Vec<_>>();
all.extend(ctx.storage().in_edges(source_id));
all
}
};
for edge_ref in edges {
if !edge.labels.is_empty() && !edge.labels.contains(&edge_ref.label) {
continue;
}
let mut edge_matches = true;
for (key, lit) in &edge.properties {
let expected: Value = lit.clone().into();
match edge_ref.properties.get(key) {
Some(actual) if *actual == expected => {}
_ => {
edge_matches = false;
break;
}
}
}
if !edge_matches {
continue;
}
let target_id = match edge.direction {
EdgeDirection::Outgoing => edge_ref.dst,
EdgeDirection::Incoming => edge_ref.src,
EdgeDirection::Both => {
if edge_ref.src == source_id {
edge_ref.dst
} else {
edge_ref.src
}
}
};
let Some(target_vertex) = ctx.storage().get_vertex(target_id) else {
continue;
};
if !target_node.labels.is_empty()
&& !target_node.labels.contains(&target_vertex.label)
{
continue;
}
let mut target_matches = true;
for (key, lit) in &target_node.properties {
let expected: Value = lit.clone().into();
match target_vertex.properties.get(key) {
Some(actual) if *actual == expected => {}
_ => {
target_matches = false;
break;
}
}
}
if !target_matches {
continue;
}
let mut new_binding = binding.clone();
if let Some(var) = &edge.variable {
new_binding.edges.insert(var.clone(), edge_ref.id);
}
if let Some(var) = &target_node.variable {
new_binding.vertices.insert(var.clone(), target_id);
}
new_bindings.push(new_binding);
}
}
current_bindings = new_bindings;
}
PatternElement::Node(_) => {
}
}
}
Ok(current_bindings)
}
fn evaluate_where_filter<S: GraphStorage + GraphStorageMut>(
ctx: &MutationContext<S>,
where_clause: &WhereClause,
bindings: &MatchBindings,
) -> bool {
evaluate_predicate(ctx, &where_clause.expression, bindings)
}
fn evaluate_predicate<S: GraphStorage + GraphStorageMut>(
ctx: &MutationContext<S>,
expr: &Expression,
bindings: &MatchBindings,
) -> bool {
match evaluate_expression(ctx, expr, bindings) {
Value::Bool(b) => b,
Value::Null => false,
_ => true, }
}
fn evaluate_expression<S: GraphStorage + GraphStorageMut>(
ctx: &MutationContext<S>,
expr: &Expression,
bindings: &MatchBindings,
) -> Value {
use crate::gql::ast::BinaryOperator;
match expr {
Expression::Literal(lit) => lit.clone().into(),
Expression::Variable(var) => {
if let Some(vid) = bindings.vertices.get(var) {
Value::Vertex(*vid)
} else if let Some(eid) = bindings.edges.get(var) {
Value::Edge(*eid)
} else if let Some(value) = ctx.get_value(var) {
value.clone()
} else {
Value::Null
}
}
Expression::Property { variable, property } => {
if let Some(vid) = bindings.vertices.get(variable) {
if let Some(vertex) = ctx.storage().get_vertex(*vid) {
return vertex
.properties
.get(property)
.cloned()
.unwrap_or(Value::Null);
}
} else if let Some(eid) = bindings.edges.get(variable) {
if let Some(edge) = ctx.storage().get_edge(*eid) {
return edge
.properties
.get(property)
.cloned()
.unwrap_or(Value::Null);
}
}
Value::Null
}
Expression::BinaryOp { left, op, right } => {
let left_val = evaluate_expression(ctx, left, bindings);
let right_val = evaluate_expression(ctx, right, bindings);
match op {
BinaryOperator::Eq => Value::Bool(left_val == right_val),
BinaryOperator::Neq => Value::Bool(left_val != right_val),
BinaryOperator::Lt => Value::Bool(
compare_values(&left_val, &right_val) == Some(std::cmp::Ordering::Less),
),
BinaryOperator::Lte => Value::Bool(matches!(
compare_values(&left_val, &right_val),
Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
)),
BinaryOperator::Gt => Value::Bool(
compare_values(&left_val, &right_val) == Some(std::cmp::Ordering::Greater),
),
BinaryOperator::Gte => Value::Bool(matches!(
compare_values(&left_val, &right_val),
Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal)
)),
BinaryOperator::And => Value::Bool(
left_val.as_bool().unwrap_or(false) && right_val.as_bool().unwrap_or(false),
),
BinaryOperator::Or => Value::Bool(
left_val.as_bool().unwrap_or(false) || right_val.as_bool().unwrap_or(false),
),
BinaryOperator::Add => {
apply_arithmetic(&left_val, &right_val, |a, b| a + b, |a, b| a + b)
}
BinaryOperator::Sub => {
apply_arithmetic(&left_val, &right_val, |a, b| a - b, |a, b| a - b)
}
BinaryOperator::Mul => {
apply_arithmetic(&left_val, &right_val, |a, b| a * b, |a, b| a * b)
}
BinaryOperator::Div => apply_arithmetic(
&left_val,
&right_val,
|a, b| if b != 0 { a / b } else { 0 },
|a, b| if b != 0.0 { a / b } else { f64::NAN },
),
BinaryOperator::Mod => apply_arithmetic(
&left_val,
&right_val,
|a, b| if b != 0 { a % b } else { 0 },
|a, b| if b != 0.0 { a % b } else { f64::NAN },
),
_ => Value::Null, }
}
Expression::UnaryOp { op, expr } => {
use crate::gql::ast::UnaryOperator;
let val = evaluate_expression(ctx, expr, bindings);
match op {
UnaryOperator::Not => match val {
Value::Bool(b) => Value::Bool(!b),
Value::Null => Value::Null,
_ => Value::Bool(false),
},
UnaryOperator::Neg => match val {
Value::Int(n) => Value::Int(-n),
Value::Float(f) => Value::Float(-f),
_ => Value::Null,
},
}
}
Expression::IsNull { expr, negated } => {
let val = evaluate_expression(ctx, expr, bindings);
let is_null = matches!(val, Value::Null);
Value::Bool(if *negated { !is_null } else { is_null })
}
Expression::InList {
expr,
list,
negated,
} => {
let val = evaluate_expression(ctx, expr, bindings);
let in_list = list.iter().any(|item| {
let item_val = evaluate_expression(ctx, item, bindings);
val == item_val
});
Value::Bool(if *negated { !in_list } else { in_list })
}
Expression::List(items) => {
let evaluated: Vec<Value> = items
.iter()
.map(|item| evaluate_expression(ctx, item, bindings))
.collect();
Value::List(evaluated)
}
_ => Value::Null,
}
}
fn compare_values(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
match (left, right) {
(Value::Int(a), Value::Int(b)) => Some(a.cmp(b)),
(Value::Float(a), Value::Float(b)) => a.partial_cmp(b),
(Value::Int(a), Value::Float(b)) => (*a as f64).partial_cmp(b),
(Value::Float(a), Value::Int(b)) => a.partial_cmp(&(*b as f64)),
(Value::String(a), Value::String(b)) => Some(a.cmp(b)),
_ => None,
}
}
fn apply_arithmetic<F1, F2>(left: &Value, right: &Value, int_op: F1, float_op: F2) -> Value
where
F1: Fn(i64, i64) -> i64,
F2: Fn(f64, f64) -> f64,
{
match (left, right) {
(Value::Int(a), Value::Int(b)) => Value::Int(int_op(*a, *b)),
(Value::Float(a), Value::Float(b)) => Value::Float(float_op(*a, *b)),
(Value::Int(a), Value::Float(b)) => Value::Float(float_op(*a as f64, *b)),
(Value::Float(a), Value::Int(b)) => Value::Float(float_op(*a, *b as f64)),
_ => Value::Null,
}
}
fn check_validation_results(
results: &[ValidationResult],
mode: ValidationMode,
) -> Result<(), MutationError> {
for result in results {
if let ValidationResult::Error(err) = result {
if mode != ValidationMode::Warn {
return Err(MutationError::Schema(err.clone()));
}
}
}
Ok(())
}
fn execute_mutation_clause<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
mutation: &MutationClause,
) -> Result<(), MutationError> {
match mutation {
MutationClause::Create(create) => execute_create(ctx, create),
MutationClause::Set(set) => execute_set(ctx, set),
MutationClause::Remove(remove) => execute_remove(ctx, remove),
MutationClause::Delete(delete) => execute_delete(ctx, delete),
MutationClause::DetachDelete(detach) => execute_detach_delete(ctx, detach),
MutationClause::Merge(merge) => execute_merge(ctx, merge),
}
}
fn execute_create<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
create: &CreateClause,
) -> Result<(), MutationError> {
for pattern in &create.patterns {
create_pattern(ctx, pattern)?;
}
Ok(())
}
fn create_pattern<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
pattern: &Pattern,
) -> Result<(), MutationError> {
let mut prev_vertex_id: Option<VertexId> = None;
let mut prev_vertex_label: Option<String> = None;
let mut pending_edge: Option<(&crate::gql::ast::EdgePattern, VertexId, String)> = None;
for element in &pattern.elements {
match element {
PatternElement::Node(node) => {
let (vertex_id, vertex_label) = if let Some(var) = &node.variable {
if let Some(existing_id) = ctx.get_vertex(var) {
let label = ctx
.storage()
.get_vertex(existing_id)
.map(|v| v.label.clone())
.unwrap_or_default();
(existing_id, label)
} else {
let label = node.labels.first().ok_or(MutationError::MissingLabel)?;
let properties: HashMap<String, Value> = node
.properties
.iter()
.map(|(k, v)| (k.clone(), v.clone().into()))
.collect();
if let Some(schema) = ctx.schema() {
let results = validate_vertex(schema, label, &properties)?;
check_validation_results(&results, schema.mode)?;
}
let id = ctx.storage_mut().add_vertex(label, properties);
ctx.bind_vertex(var, id);
(id, label.to_string())
}
} else {
let label = node.labels.first().ok_or(MutationError::MissingLabel)?;
let properties: HashMap<String, Value> = node
.properties
.iter()
.map(|(k, v)| (k.clone(), v.clone().into()))
.collect();
if let Some(schema) = ctx.schema() {
let results = validate_vertex(schema, label, &properties)?;
check_validation_results(&results, schema.mode)?;
}
let id = ctx.storage_mut().add_vertex(label, properties);
(id, label.to_string())
};
if let Some((edge_pattern, from_id, from_label)) = pending_edge.take() {
let to_id = vertex_id;
let to_label = &vertex_label;
let (src, dst, src_label, dst_label) = match edge_pattern.direction {
EdgeDirection::Outgoing => (from_id, to_id, &from_label, to_label),
EdgeDirection::Incoming => (to_id, from_id, to_label, &from_label),
EdgeDirection::Both => (from_id, to_id, &from_label, to_label), };
let edge_label = edge_pattern
.labels
.first()
.map(|s| s.as_str())
.unwrap_or("edge");
let edge_properties: HashMap<String, Value> = edge_pattern
.properties
.iter()
.map(|(k, v)| (k.clone(), v.clone().into()))
.collect();
if let Some(schema) = ctx.schema() {
let results = validate_edge(
schema,
edge_label,
src_label,
dst_label,
&edge_properties,
)?;
check_validation_results(&results, schema.mode)?;
}
let edge_id =
ctx.storage_mut()
.add_edge(src, dst, edge_label, edge_properties)?;
if let Some(var) = &edge_pattern.variable {
ctx.bind_edge(var, edge_id);
}
}
prev_vertex_id = Some(vertex_id);
prev_vertex_label = Some(vertex_label);
}
PatternElement::Edge(edge) => {
let from_id = prev_vertex_id.ok_or(MutationError::IncompleteEdge)?;
let from_label = prev_vertex_label
.clone()
.ok_or(MutationError::IncompleteEdge)?;
pending_edge = Some((edge, from_id, from_label));
}
}
}
if pending_edge.is_some() {
return Err(MutationError::IncompleteEdge);
}
Ok(())
}
fn execute_set<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
set: &SetClause,
) -> Result<(), MutationError> {
for item in &set.items {
execute_set_item(ctx, item)?;
}
Ok(())
}
fn execute_set_item<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
item: &SetItem,
) -> Result<(), MutationError> {
let element = ctx
.get_element(&item.target.variable)
.ok_or_else(|| MutationError::UnboundVariable(item.target.variable.clone()))?;
let (vertices, edges) = ctx.clone_bindings();
let bindings = MatchBindings { vertices, edges };
let value = evaluate_expression(ctx, &item.value, &bindings);
match element {
Element::Vertex(vid) => {
if let Some(schema) = ctx.schema() {
if let Some(vertex) = ctx.storage().get_vertex(vid) {
let results = validate_property_update(
schema,
&vertex.label,
&item.target.property,
&value,
true, )?;
check_validation_results(&results, schema.mode)?;
}
}
ctx.storage_mut()
.set_vertex_property(vid, &item.target.property, value)?;
}
Element::Edge(eid) => {
if let Some(schema) = ctx.schema() {
if let Some(edge) = ctx.storage().get_edge(eid) {
let results = validate_property_update(
schema,
&edge.label,
&item.target.property,
&value,
false, )?;
check_validation_results(&results, schema.mode)?;
}
}
ctx.storage_mut()
.set_edge_property(eid, &item.target.property, value)?;
}
}
Ok(())
}
fn execute_remove<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
remove: &RemoveClause,
) -> Result<(), MutationError> {
for prop_ref in &remove.properties {
let element = ctx
.get_element(&prop_ref.variable)
.ok_or_else(|| MutationError::UnboundVariable(prop_ref.variable.clone()))?;
match element {
Element::Vertex(vid) => {
ctx.storage_mut()
.set_vertex_property(vid, &prop_ref.property, Value::Null)?;
}
Element::Edge(eid) => {
ctx.storage_mut()
.set_edge_property(eid, &prop_ref.property, Value::Null)?;
}
}
}
Ok(())
}
fn execute_delete<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
delete: &DeleteClause,
) -> Result<(), MutationError> {
for var in &delete.variables {
let element = ctx
.get_element(var)
.ok_or_else(|| MutationError::UnboundVariable(var.clone()))?;
match element {
Element::Vertex(vid) => {
let has_edges = ctx.storage().out_edges(vid).next().is_some()
|| ctx.storage().in_edges(vid).next().is_some();
if has_edges {
return Err(MutationError::VertexHasEdges(vid));
}
ctx.storage_mut().remove_vertex(vid)?;
}
Element::Edge(eid) => {
ctx.storage_mut().remove_edge(eid)?;
}
}
}
Ok(())
}
fn execute_detach_delete<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
detach: &DetachDeleteClause,
) -> Result<(), MutationError> {
for var in &detach.variables {
let element = ctx
.get_element(var)
.ok_or_else(|| MutationError::UnboundVariable(var.clone()))?;
match element {
Element::Vertex(vid) => {
ctx.storage_mut().remove_vertex(vid)?;
}
Element::Edge(eid) => {
ctx.storage_mut().remove_edge(eid)?;
}
}
}
Ok(())
}
fn execute_merge<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
merge: &MergeClause,
) -> Result<(), MutationError> {
let match_results = match_pattern(ctx, &merge.pattern)?;
if match_results.is_empty() {
create_pattern(ctx, &merge.pattern)?;
if let Some(on_create) = &merge.on_create {
for item in on_create {
execute_set_item(ctx, item)?;
}
}
} else {
let first_match = &match_results[0];
for (var, vid) in &first_match.vertices {
ctx.bind_vertex(var, *vid);
}
for (var, eid) in &first_match.edges {
ctx.bind_edge(var, *eid);
}
if let Some(on_match) = &merge.on_match {
for item in on_match {
execute_set_item(ctx, item)?;
}
}
}
Ok(())
}
fn execute_foreach<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
foreach_clause: &ForeachClause,
) -> Result<(), MutationError> {
let bindings = MatchBindings {
vertices: ctx.vertex_bindings.clone(),
edges: ctx.edge_bindings.clone(),
};
let list_value = evaluate_expression(ctx, &foreach_clause.list, &bindings);
let items = match list_value {
Value::List(items) => items,
Value::Null => {
return Ok(());
}
other => {
return Err(MutationError::Compile(CompileError::foreach_not_list(
&foreach_clause.variable,
value_type_name(&other),
)));
}
};
let saved_vertex = ctx.vertex_bindings.get(&foreach_clause.variable).copied();
let saved_edge = ctx.edge_bindings.get(&foreach_clause.variable).copied();
let saved_value = ctx.get_value(&foreach_clause.variable).cloned();
for item in items {
match &item {
Value::Vertex(vid) => {
ctx.bind_vertex(&foreach_clause.variable, *vid);
ctx.remove_value_binding(&foreach_clause.variable);
}
Value::Edge(eid) => {
ctx.bind_edge(&foreach_clause.variable, *eid);
ctx.remove_value_binding(&foreach_clause.variable);
}
_ => {
ctx.bind_value(&foreach_clause.variable, item.clone());
}
}
for mutation in &foreach_clause.mutations {
execute_foreach_mutation(ctx, mutation)?;
}
}
if let Some(vid) = saved_vertex {
ctx.bind_vertex(&foreach_clause.variable, vid);
} else {
ctx.vertex_bindings.remove(&foreach_clause.variable);
}
if let Some(eid) = saved_edge {
ctx.bind_edge(&foreach_clause.variable, eid);
} else {
ctx.edge_bindings.remove(&foreach_clause.variable);
}
if let Some(val) = saved_value {
ctx.bind_value(&foreach_clause.variable, val);
} else {
ctx.remove_value_binding(&foreach_clause.variable);
}
Ok(())
}
fn execute_foreach_mutation<S: GraphStorage + GraphStorageMut>(
ctx: &mut MutationContext<S>,
mutation: &ForeachMutation,
) -> Result<(), MutationError> {
match mutation {
ForeachMutation::Set(set) => execute_set(ctx, set),
ForeachMutation::Remove(remove) => execute_remove(ctx, remove),
ForeachMutation::Delete(delete) => execute_delete(ctx, delete),
ForeachMutation::DetachDelete(detach) => execute_detach_delete(ctx, detach),
ForeachMutation::Create(create) => execute_create(ctx, create),
ForeachMutation::Foreach(nested) => execute_foreach(ctx, nested),
}
}
fn value_type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Int(_) => "integer",
Value::Float(_) => "float",
Value::String(_) => "string",
Value::List(_) => "list",
Value::Map(_) => "map",
Value::Vertex(_) => "vertex",
Value::Edge(_) => "edge",
Value::Point(_) => "point",
Value::Polygon(_) => "polygon",
}
}
fn evaluate_return<S: GraphStorage + GraphStorageMut>(
ctx: &MutationContext<S>,
return_clause: &ReturnClause,
) -> Result<Option<Value>, MutationError> {
let bindings = MatchBindings {
vertices: ctx.vertex_bindings.clone(),
edges: ctx.edge_bindings.clone(),
};
if return_clause.items.is_empty() {
return Ok(None);
}
if return_clause.items.len() == 1 {
let item = &return_clause.items[0];
let value = evaluate_return_item(ctx, item, &bindings)?;
Ok(Some(value))
} else {
let mut map = crate::value::ValueMap::new();
for item in &return_clause.items {
let key = get_return_item_key(item);
let value = evaluate_return_item(ctx, item, &bindings)?;
map.insert(key, value);
}
Ok(Some(Value::Map(map)))
}
}
fn get_return_item_key(item: &ReturnItem) -> String {
if let Some(alias) = &item.alias {
alias.clone()
} else {
match &item.expression {
Expression::Variable(var) => var.clone(),
Expression::Property { variable, property } => format!("{}.{}", variable, property),
_ => "expr".to_string(),
}
}
}
fn evaluate_return_item<S: GraphStorage + GraphStorageMut>(
ctx: &MutationContext<S>,
item: &ReturnItem,
bindings: &MatchBindings,
) -> Result<Value, MutationError> {
Ok(evaluate_expression(ctx, &item.expression, bindings))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gql::parse_statement;
use crate::storage::Graph;
#[test]
fn test_create_single_vertex() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt = parse_statement("CREATE (n:Person {name: 'Alice', age: 30})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
assert_eq!(storage.vertex_count(), 1);
let vertex = storage.all_vertices().next().unwrap();
assert_eq!(vertex.label, "Person");
assert_eq!(
vertex.properties.get("name"),
Some(&Value::String("Alice".into()))
);
assert_eq!(vertex.properties.get("age"), Some(&Value::Int(30)));
}
#[test]
fn test_create_multiple_vertices() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt =
parse_statement("CREATE (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
assert_eq!(storage.vertex_count(), 2);
}
#[test]
fn test_create_edge() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt = parse_statement(
"CREATE (a:Person {name: 'Alice'})-[:KNOWS {since: 2020}]->(b:Person {name: 'Bob'})",
)
.unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
assert_eq!(storage.vertex_count(), 2);
assert_eq!(storage.edge_count(), 1);
let edge = storage.all_edges().next().unwrap();
assert_eq!(edge.label, "KNOWS");
assert_eq!(edge.properties.get("since"), Some(&Value::Int(2020)));
}
#[test]
fn test_create_with_return() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt = parse_statement("CREATE (n:Person {name: 'Alice'}) RETURN n").unwrap();
let results = execute_mutation(&stmt, &mut storage).unwrap();
assert_eq!(results.len(), 1);
assert!(matches!(results[0], Value::Vertex(_)));
}
#[test]
fn test_match_set() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt = parse_statement("CREATE (n:Person {name: 'Alice', age: 30})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let stmt = parse_statement("MATCH (n:Person {name: 'Alice'}) SET n.age = 31").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let vertex = storage.all_vertices().next().unwrap();
assert_eq!(vertex.properties.get("age"), Some(&Value::Int(31)));
}
#[test]
fn test_match_delete_edge() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt =
parse_statement("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
.unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
assert_eq!(storage.edge_count(), 1);
let stmt = parse_statement("MATCH (a:Person)-[r:KNOWS]->() DELETE r").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
assert_eq!(storage.edge_count(), 0);
assert_eq!(storage.vertex_count(), 2); }
#[test]
fn test_delete_vertex_with_edges_fails() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt =
parse_statement("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
.unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let stmt = parse_statement("MATCH (a:Person {name: 'Alice'}) DELETE a").unwrap();
let result = execute_mutation(&stmt, &mut storage);
assert!(matches!(result, Err(MutationError::VertexHasEdges(_))));
}
#[test]
fn test_detach_delete_vertex() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt =
parse_statement("CREATE (a:Person {name: 'Alice'})-[:KNOWS]->(b:Person {name: 'Bob'})")
.unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
assert_eq!(storage.vertex_count(), 2);
assert_eq!(storage.edge_count(), 1);
let stmt = parse_statement("MATCH (a:Person {name: 'Alice'}) DETACH DELETE a").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
assert_eq!(storage.vertex_count(), 1); assert_eq!(storage.edge_count(), 0); }
#[test]
fn test_merge_creates_when_not_exists() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt =
parse_statement("MERGE (n:Person {name: 'Alice'}) ON CREATE SET n.created = true")
.unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
assert_eq!(storage.vertex_count(), 1);
let vertex = storage.all_vertices().next().unwrap();
assert_eq!(
vertex.properties.get("name"),
Some(&Value::String("Alice".into()))
);
assert_eq!(vertex.properties.get("created"), Some(&Value::Bool(true)));
}
#[test]
fn test_merge_matches_when_exists() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt = parse_statement("CREATE (n:Person {name: 'Alice', version: 1})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let stmt =
parse_statement("MERGE (n:Person {name: 'Alice'}) ON MATCH SET n.version = 2").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
assert_eq!(storage.vertex_count(), 1);
let vertex = storage.all_vertices().next().unwrap();
assert_eq!(vertex.properties.get("version"), Some(&Value::Int(2)));
}
#[test]
fn test_remove_property() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt = parse_statement("CREATE (n:Person {name: 'Alice', temp: 'value'})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let stmt = parse_statement("MATCH (n:Person) REMOVE n.temp").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let vertex = storage.all_vertices().next().unwrap();
assert_eq!(vertex.properties.get("temp"), Some(&Value::Null));
}
#[test]
fn test_match_with_where() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt = parse_statement("CREATE (a:Person {name: 'Alice', age: 30})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let stmt = parse_statement("CREATE (b:Person {name: 'Bob', age: 25})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let stmt = parse_statement("MATCH (n:Person) WHERE n.age > 26 SET n.adult = true").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let mut updated_count = 0;
for vertex in storage.all_vertices() {
if vertex.properties.get("adult") == Some(&Value::Bool(true)) {
assert_eq!(
vertex.properties.get("name"),
Some(&Value::String("Alice".into()))
);
updated_count += 1;
}
}
assert_eq!(updated_count, 1);
}
#[test]
fn test_foreach_basic_set() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt = parse_statement("CREATE (a:Person {name: 'Alice'})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let stmt = parse_statement("CREATE (b:Person {name: 'Bob'})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let vertex_ids: Vec<_> = storage.all_vertices().map(|v| v.id).collect();
assert_eq!(vertex_ids.len(), 2);
let foreach_clause = ForeachClause {
variable: "n".to_string(),
list: Expression::List(vec![
Expression::Literal(crate::gql::ast::Literal::Int(1)),
Expression::Literal(crate::gql::ast::Literal::Int(2)),
]),
mutations: vec![],
};
let mut ctx = MutationContext::new(&mut storage);
execute_foreach(&mut ctx, &foreach_clause).unwrap();
}
#[test]
fn test_foreach_with_vertex_list() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt = parse_statement("CREATE (a:Person {name: 'Alice', visited: false})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let stmt = parse_statement("CREATE (b:Person {name: 'Bob', visited: false})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let vertex_ids: Vec<_> = storage.all_vertices().map(|v| v.id).collect();
let foreach_clause = ForeachClause {
variable: "n".to_string(),
list: Expression::List(
vertex_ids
.iter()
.map(|id| Expression::Literal(crate::gql::ast::Literal::Int(id.0 as i64)))
.collect(),
),
mutations: vec![],
};
let mut ctx = MutationContext::new(&mut storage);
execute_foreach(&mut ctx, &foreach_clause).unwrap();
assert_eq!(storage.vertex_count(), 2);
}
#[test]
fn test_foreach_not_list_error() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let foreach_clause = ForeachClause {
variable: "n".to_string(),
list: Expression::Literal(crate::gql::ast::Literal::Int(42)),
mutations: vec![],
};
let mut ctx = MutationContext::new(&mut storage);
let result = execute_foreach(&mut ctx, &foreach_clause);
assert!(matches!(
result,
Err(MutationError::Compile(CompileError::ForeachNotList { .. }))
));
}
#[test]
fn test_foreach_null_list_is_noop() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let foreach_clause = ForeachClause {
variable: "n".to_string(),
list: Expression::Literal(crate::gql::ast::Literal::Null),
mutations: vec![],
};
let mut ctx = MutationContext::new(&mut storage);
execute_foreach(&mut ctx, &foreach_clause).unwrap();
}
#[test]
fn test_foreach_empty_list() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let foreach_clause = ForeachClause {
variable: "n".to_string(),
list: Expression::List(vec![]),
mutations: vec![],
};
let mut ctx = MutationContext::new(&mut storage);
execute_foreach(&mut ctx, &foreach_clause).unwrap();
}
#[test]
fn test_foreach_nested() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let inner_foreach = ForeachClause {
variable: "y".to_string(),
list: Expression::List(vec![
Expression::Literal(crate::gql::ast::Literal::Int(1)),
Expression::Literal(crate::gql::ast::Literal::Int(2)),
]),
mutations: vec![],
};
let outer_foreach = ForeachClause {
variable: "x".to_string(),
list: Expression::List(vec![
Expression::Literal(crate::gql::ast::Literal::Int(10)),
Expression::Literal(crate::gql::ast::Literal::Int(20)),
]),
mutations: vec![ForeachMutation::Foreach(Box::new(inner_foreach))],
};
let mut ctx = MutationContext::new(&mut storage);
execute_foreach(&mut ctx, &outer_foreach).unwrap();
}
#[test]
fn test_foreach_set_with_iteration_variable() {
let graph = Graph::new();
let mut storage = graph.as_storage_mut();
let stmt = parse_statement("CREATE (a:Person {name: 'Alice'})").unwrap();
execute_mutation(&stmt, &mut storage).unwrap();
let vertex_id = storage.all_vertices().next().unwrap().id;
let foreach_clause = ForeachClause {
variable: "i".to_string(),
list: Expression::List(vec![
Expression::Literal(crate::gql::ast::Literal::Int(1)),
Expression::Literal(crate::gql::ast::Literal::Int(2)),
Expression::Literal(crate::gql::ast::Literal::Int(3)),
]),
mutations: vec![ForeachMutation::Set(SetClause {
items: vec![SetItem {
target: crate::gql::ast::PropertyRef {
variable: "p".to_string(),
property: "counter".to_string(),
},
value: Expression::Variable("i".to_string()),
}],
})],
};
let mut ctx = MutationContext::new(&mut storage);
ctx.bind_vertex("p", vertex_id);
execute_foreach(&mut ctx, &foreach_clause).unwrap();
let vertex = storage.get_vertex(vertex_id).unwrap();
assert_eq!(
vertex.properties.get("counter"),
Some(&Value::Int(3)),
"Counter should be 3 (last value)"
);
}
}