use std::collections::{HashMap, HashSet, VecDeque};
use std::cell::RefCell;
use crate::graph::Graph;
use crate::storage::apply_ops;
use crate::transaction::operation::{Operation, PropertyTarget};
use crate::types::{ulid_encode, DbError, EdgeId, NodeId, Properties, Value};
use super::ast::*;
thread_local! {
static QUERY_PARAMS: RefCell<HashMap<String, Value>> = RefCell::new(HashMap::new());
}
pub type Row = HashMap<String, Value>;
#[derive(Debug, Clone)]
enum Binding {
Node(NodeId),
Edge(EdgeId),
EdgeList(Vec<EdgeId>),
Value(Value),
}
type Bindings = HashMap<String, Binding>;
pub fn execute(
stmt: Statement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<Vec<Row>, DbError> {
let (rows, _ops) = execute_capturing(stmt, graph, next_txn_id)?;
Ok(rows)
}
pub fn execute_capturing(
stmt: Statement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
match stmt {
Statement::Match(m) => Ok((execute_match(m, graph)?, vec![])),
Statement::OptionalMatch(m) => Ok((execute_optional_match(m, graph)?, vec![])),
Statement::MatchOptionalMatch(m) => Ok((execute_match_optional_match(m, graph)?, vec![])),
Statement::Unwind(u) => Ok((execute_unwind(u, graph)?, vec![])),
Statement::Union(u) => Ok((execute_union(u, graph, next_txn_id)?, vec![])),
Statement::MatchWith(mw) => Ok((execute_match_with(mw, graph)?, vec![])),
Statement::Insert(ins) => execute_insert(ins, graph, next_txn_id),
Statement::MatchInsert(mi) => execute_match_insert(mi, graph, next_txn_id),
Statement::Set(s) => execute_set(s, graph, next_txn_id),
Statement::Remove(r) => execute_remove(r, graph, next_txn_id),
Statement::Delete(d) => execute_delete(d, graph, next_txn_id),
Statement::CreateIndex(ci) => execute_create_index(ci, graph, next_txn_id),
Statement::DropIndex(di) => execute_drop_index(di, graph, next_txn_id),
Statement::ShowIndexes => Ok((execute_show_indexes(graph)?, vec![])),
Statement::Call(c) => Ok((execute_call(c, graph)?, vec![])),
Statement::CallPipeline(c) => Ok((execute_call_pipeline(c, graph)?, vec![])),
Statement::Truncate => execute_truncate(graph),
Statement::LoadCsvNodes(n) => execute_load_csv_nodes(n, graph, next_txn_id),
Statement::LoadCsvEdges(e) => execute_load_csv_edges(e, graph, next_txn_id),
Statement::UnwindInsert(u) => execute_unwind_insert(u, graph, next_txn_id),
Statement::Constraint(c) => execute_constraint(c, graph),
}
}
pub fn execute_capturing_with_params(
stmt: Statement,
graph: &mut Graph,
next_txn_id: &mut u64,
params: HashMap<String, Value>,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
QUERY_PARAMS.with(|p| *p.borrow_mut() = params);
let result = execute_capturing(stmt, graph, next_txn_id);
QUERY_PARAMS.with(|p| p.borrow_mut().clear());
result
}
fn execute_match(m: MatchStatement, graph: &Graph) -> Result<Vec<Row>, DbError> {
let bindings = cross_join_patterns(&m.patterns, graph, m.path_mode, m.where_clause.as_ref());
let filtered: Vec<Bindings> = bindings
.into_iter()
.filter(|b| {
if let Some(ref cond) = m.where_clause {
matches!(eval_expr(cond, b, graph), Ok(Value::Bool(true)))
} else {
true
}
})
.collect();
let has_aggregates = m.return_clause.items.iter().any(|it| contains_aggregate(&it.expr));
let mut rows: Vec<Row> = if has_aggregates {
let agg_rows = execute_aggregate(&filtered, &m.return_clause.items, graph)?;
let mut agg_rows = agg_rows;
if !m.return_clause.order_by.is_empty() {
let order = &m.return_clause.order_by;
agg_rows.sort_by(|a, b| {
for item in order {
let va = eval_expr_on_row(&item.expr, a).unwrap_or(Value::Null);
let vb = eval_expr_on_row(&item.expr, b).unwrap_or(Value::Null);
let cmp = va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal);
let cmp = if item.ascending { cmp } else { cmp.reverse() };
if cmp != std::cmp::Ordering::Equal {
return cmp;
}
}
std::cmp::Ordering::Equal
});
}
agg_rows
} else {
let mut filtered = filtered;
if !m.return_clause.order_by.is_empty() {
let order = &m.return_clause.order_by;
filtered.sort_by(|a, b| {
for item in order {
let va = eval_expr(&item.expr, a, graph).unwrap_or(Value::Null);
let vb = eval_expr(&item.expr, b, graph).unwrap_or(Value::Null);
let cmp = va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal);
let cmp = if item.ascending { cmp } else { cmp.reverse() };
if cmp != std::cmp::Ordering::Equal {
return cmp;
}
}
std::cmp::Ordering::Equal
});
}
filtered
.iter()
.map(|b| project_return(&m.return_clause.items, b, graph))
.collect::<Result<Vec<_>, _>>()?
};
if m.return_clause.distinct {
let mut seen = std::collections::HashSet::new();
rows.retain(|row| {
let mut keys: Vec<_> = row.keys().collect();
keys.sort();
let key: Vec<String> = keys.iter().map(|k| format!("{}={:?}", k, row[*k])).collect();
seen.insert(key.join(","))
});
}
let offset = match &m.return_clause.offset {
Some(expr) => match eval_expr_literal(expr)? {
Value::Int(n) => n.max(0) as usize,
_ => return Err(DbError::Query("OFFSET must be an integer".into())),
},
None => 0,
};
let limit = match &m.return_clause.limit {
Some(expr) => match eval_expr_literal(expr)? {
Value::Int(n) => Some(n.max(0) as usize),
_ => return Err(DbError::Query("LIMIT must be an integer".into())),
},
None => None,
};
let rows: Vec<Row> = rows
.into_iter()
.skip(offset)
.take(limit.unwrap_or(usize::MAX))
.collect();
Ok(rows)
}
fn execute_optional_match(m: MatchStatement, graph: &Graph) -> Result<Vec<Row>, DbError> {
let bindings = cross_join_patterns(&m.patterns, graph, m.path_mode, m.where_clause.as_ref());
let filtered: Vec<Bindings> = bindings
.into_iter()
.filter(|b| {
if let Some(ref cond) = m.where_clause {
matches!(eval_expr(cond, b, graph), Ok(Value::Bool(true)))
} else {
true
}
})
.collect();
if filtered.is_empty() {
let null_row: Vec<Bindings> = vec![HashMap::new()];
return apply_return_clause(null_row, &m.return_clause, graph);
}
apply_return_clause(filtered, &m.return_clause, graph)
}
fn execute_match_optional_match(
m: super::ast::MatchOptionalMatchStatement,
graph: &Graph,
) -> Result<Vec<Row>, DbError> {
let mut rows: Vec<Bindings> = cross_join_patterns(&m.patterns, graph, m.path_mode, m.where_clause.as_ref());
if let Some(ref cond) = m.where_clause {
rows.retain(|b| matches!(eval_expr(cond, b, graph), Ok(Value::Bool(true))));
}
for clause in m.optional_clauses {
rows = left_join_optional(rows, &clause.patterns, clause.where_clause.as_ref(), clause.path_mode, graph);
}
apply_return_clause(rows, &m.return_clause, graph)
}
fn left_join_optional(
existing: Vec<Bindings>,
patterns: &[super::ast::GraphPattern],
where_clause: Option<&Expr>,
path_mode: PathMode,
graph: &Graph,
) -> Vec<Bindings> {
let mut result = Vec::new();
for seed in existing {
let matches = cross_join_patterns_seeded(patterns, &seed, path_mode, where_clause, graph);
if matches.is_empty() {
result.push(seed);
} else {
result.extend(matches);
}
}
result
}
fn cross_join_patterns_seeded(
patterns: &[super::ast::GraphPattern],
seed: &Bindings,
mode: PathMode,
where_clause: Option<&Expr>,
graph: &Graph,
) -> Vec<Bindings> {
let mut result: Vec<Bindings> = vec![seed.clone()];
for pattern in patterns {
let mut next: Vec<Bindings> = Vec::new();
for existing in &result {
let matches = match_pattern_seeded(pattern, existing, graph, mode, where_clause);
next.extend(matches);
}
result = next;
}
result
}
fn match_pattern_seeded(
pattern: &super::ast::GraphPattern,
seed: &Bindings,
graph: &Graph,
path_mode: PathMode,
where_clause: Option<&Expr>,
) -> Vec<Bindings> {
let start_nodes: Vec<NodeId> = if let Some(ref var) = pattern.start.variable {
if let Some(Binding::Node(id)) = seed.get(var.as_str()) {
vec![*id]
} else {
get_candidate_start_nodes(pattern, graph, where_clause)
}
} else {
get_candidate_start_nodes(pattern, graph, where_clause)
};
let mut results: Vec<Bindings> = Vec::new();
for node_id in start_nodes {
let node = match graph.get_node(node_id) { Some(n) => n, None => continue };
if !check_node_constraints(&pattern.start, &node) { continue; }
let mut binding = seed.clone();
if let Some(ref var) = pattern.start.variable {
binding.insert(var.clone(), Binding::Node(node_id));
}
let mut visited_nodes: HashSet<NodeId> = HashSet::new();
visited_nodes.insert(node_id);
let visited_edges: HashSet<EdgeId> = HashSet::new();
let step_bindings = extend_steps(
&pattern.steps, node_id, binding, graph,
path_mode, &visited_edges, &visited_nodes,
);
results.extend(step_bindings);
}
if let Some(w) = where_clause {
results.retain(|b| matches!(eval_expr(w, b, graph), Ok(Value::Bool(true))));
}
results
}
fn get_candidate_start_nodes(
pattern: &super::ast::GraphPattern,
graph: &Graph,
where_clause: Option<&Expr>,
) -> Vec<NodeId> {
if let Some(primary) = pattern.start.labels.first() {
for pc in &pattern.start.properties {
if let Ok(val) = eval_expr_literal(&pc.value) {
if let Some(ids) = graph.lookup_by_property(primary, &pc.key, &val) {
return ids;
}
}
}
if let (Some(var), Some(where_expr)) = (&pattern.start.variable, where_clause) {
if let Some(ids) = range_index_candidates(graph, primary, var, where_expr) {
return ids;
}
}
return graph.nodes_by_label(primary);
}
graph.all_nodes().into_iter().map(|n| n.id).collect()
}
fn execute_unwind(u: UnwindStatement, graph: &Graph) -> Result<Vec<Row>, DbError> {
let empty: Bindings = HashMap::new();
let list_val = eval_expr(&u.expr, &empty, graph)?;
let items = match list_val {
Value::List(items) => items,
Value::Null => vec![],
other => return Err(DbError::Query(format!(
"UNWIND requires a list expression, got {:?}", other
))),
};
let mut all_bindings: Vec<Bindings> = Vec::new();
for item in items {
let mut b: Bindings = HashMap::new();
b.insert(u.variable.clone(), Binding::Value(item));
all_bindings.push(b);
}
apply_return_clause(all_bindings, &u.return_clause, graph)
}
fn execute_union(
u: UnionStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<Vec<Row>, DbError> {
let mut all_rows: Vec<Row> = Vec::new();
for branch in u.branches {
let (rows, _ops) = execute_capturing(branch, graph, next_txn_id)?;
all_rows.extend(rows);
}
if !u.all {
let mut seen = std::collections::HashSet::new();
all_rows.retain(|row| {
let mut keys: Vec<_> = row.keys().collect();
keys.sort();
let key: Vec<String> = keys.iter().map(|k| format!("{}={:?}", k, row[*k])).collect();
seen.insert(key.join(","))
});
}
Ok(all_rows)
}
fn execute_match_with(mw: MatchWithStatement, graph: &Graph) -> Result<Vec<Row>, DbError> {
let bindings = cross_join_patterns(&mw.patterns, graph, mw.path_mode, mw.where_clause.as_ref());
let filtered: Vec<Bindings> = bindings
.into_iter()
.filter(|b| {
if let Some(ref cond) = mw.where_clause {
matches!(eval_expr(cond, b, graph), Ok(Value::Bool(true)))
} else {
true
}
})
.collect();
let with_items = &mw.with_clause.items;
let has_agg = with_items.iter().any(|it| contains_aggregate(&it.expr));
let projected_rows: Vec<Row> = if has_agg {
execute_aggregate(&filtered, with_items, graph)?
} else {
filtered
.iter()
.map(|b| project_return(with_items, b, graph))
.collect::<Result<Vec<_>, _>>()?
};
let mut projected_rows = projected_rows;
if mw.with_clause.distinct {
let mut seen = std::collections::HashSet::new();
projected_rows.retain(|row| {
let mut keys: Vec<_> = row.keys().collect();
keys.sort();
let key: Vec<String> = keys.iter().map(|k| format!("{}={:?}", k, row[*k])).collect();
seen.insert(key.join(","))
});
}
let projected_rows: Vec<Row> = if let Some(ref cond) = mw.with_clause.where_clause {
projected_rows
.into_iter()
.filter(|row| matches!(eval_expr_on_row(cond, row), Ok(Value::Bool(true))))
.collect()
} else {
projected_rows
};
let with_bindings: Vec<Bindings> = projected_rows
.into_iter()
.map(|row| {
row.into_iter()
.map(|(k, v)| (k, Binding::Value(v)))
.collect()
})
.collect();
apply_return_clause(with_bindings, &mw.return_clause, graph)
}
fn apply_return_clause(
filtered: Vec<Bindings>,
rc: &ReturnClause,
graph: &Graph,
) -> Result<Vec<Row>, DbError> {
let has_aggregates = rc.items.iter().any(|it| contains_aggregate(&it.expr));
let mut rows: Vec<Row> = if has_aggregates {
let mut agg_rows = execute_aggregate(&filtered, &rc.items, graph)?;
if !rc.order_by.is_empty() {
let order = &rc.order_by;
agg_rows.sort_by(|a, b| {
for item in order {
let va = eval_expr_on_row(&item.expr, a).unwrap_or(Value::Null);
let vb = eval_expr_on_row(&item.expr, b).unwrap_or(Value::Null);
let cmp = va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal);
let cmp = if item.ascending { cmp } else { cmp.reverse() };
if cmp != std::cmp::Ordering::Equal { return cmp; }
}
std::cmp::Ordering::Equal
});
}
agg_rows
} else {
let mut filtered = filtered;
if !rc.order_by.is_empty() {
let order = &rc.order_by;
filtered.sort_by(|a, b| {
for item in order {
let va = eval_expr(&item.expr, a, graph).unwrap_or(Value::Null);
let vb = eval_expr(&item.expr, b, graph).unwrap_or(Value::Null);
let cmp = va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal);
let cmp = if item.ascending { cmp } else { cmp.reverse() };
if cmp != std::cmp::Ordering::Equal { return cmp; }
}
std::cmp::Ordering::Equal
});
}
filtered
.iter()
.map(|b| project_return(&rc.items, b, graph))
.collect::<Result<Vec<_>, _>>()?
};
if rc.distinct {
let mut seen = std::collections::HashSet::new();
rows.retain(|row| {
let mut keys: Vec<_> = row.keys().collect();
keys.sort();
let key: Vec<String> = keys.iter().map(|k| format!("{}={:?}", k, row[*k])).collect();
seen.insert(key.join(","))
});
}
let offset = match &rc.offset {
Some(expr) => match eval_expr_literal(expr)? {
Value::Int(n) => n.max(0) as usize,
_ => return Err(DbError::Query("OFFSET must be an integer".into())),
},
None => 0,
};
let limit = match &rc.limit {
Some(expr) => match eval_expr_literal(expr)? {
Value::Int(n) => Some(n.max(0) as usize),
_ => return Err(DbError::Query("LIMIT must be an integer".into())),
},
None => None,
};
Ok(rows.into_iter().skip(offset).take(limit.unwrap_or(usize::MAX)).collect())
}
const AGG_FUNCTIONS: &[&str] = &["count", "sum", "avg", "min", "max", "collect"];
fn is_agg_fn(name: &str) -> bool {
AGG_FUNCTIONS.contains(&name)
}
fn contains_aggregate(expr: &Expr) -> bool {
match expr {
Expr::Call(name, args) => {
if is_agg_fn(name) {
true
} else {
args.iter().any(contains_aggregate)
}
}
Expr::BinOp(l, _, r) => contains_aggregate(l) || contains_aggregate(r),
Expr::Not(e) => contains_aggregate(e),
Expr::IsNull(e, _) => contains_aggregate(e),
Expr::Property(e, _) => contains_aggregate(e.as_ref()),
_ => false,
}
}
fn eval_expr_agg(expr: &Expr, group: &[&Bindings], graph: &Graph) -> Result<Value, DbError> {
match expr {
Expr::Call(name, args) if is_agg_fn(name) => aggregate_fn(name, args, group, graph),
Expr::BinOp(l, op, r) => {
let lv = eval_expr_agg(l, group, graph)?;
let rv = eval_expr_agg(r, group, graph)?;
eval_binop(lv, *op, rv)
}
Expr::Not(e) => match eval_expr_agg(e, group, graph)? {
Value::Bool(b) => Ok(Value::Bool(!b)),
_ => Ok(Value::Null),
},
Expr::IsNull(e, is_not) => {
let v = eval_expr_agg(e, group, graph)?;
let is_null = v == Value::Null;
Ok(Value::Bool(if *is_not { !is_null } else { is_null }))
}
_ => match group.first() {
Some(b) => eval_expr(expr, b, graph),
None => Ok(Value::Null),
},
}
}
fn aggregate_fn(
name: &str,
args: &[Expr],
group: &[&Bindings],
graph: &Graph,
) -> Result<Value, DbError> {
match name {
"count" => {
let is_star = args.is_empty() || matches!(args.first(), Some(Expr::Star));
if is_star {
Ok(Value::Int(group.len() as i64))
} else {
let arg = &args[0];
let n = group
.iter()
.filter(|b| matches!(eval_expr(arg, b, graph), Ok(v) if v != Value::Null))
.count();
Ok(Value::Int(n as i64))
}
}
"sum" => {
let arg = args.first().ok_or_else(|| DbError::Query("sum() requires 1 arg".into()))?;
let mut int_sum: i64 = 0;
let mut float_sum: f64 = 0.0;
let mut has_float = false;
let mut any = false;
for b in group {
match eval_expr(arg, b, graph)? {
Value::Null => {}
Value::Int(i) => { int_sum += i; float_sum += i as f64; any = true; }
Value::Float(f) => { float_sum += f; has_float = true; any = true; }
v => return Err(DbError::Query(format!("sum() cannot aggregate {v:?}"))),
}
}
if !any { Ok(Value::Null) } else if has_float { Ok(Value::Float(float_sum)) } else { Ok(Value::Int(int_sum)) }
}
"avg" => {
let arg = args.first().ok_or_else(|| DbError::Query("avg() requires 1 arg".into()))?;
let mut sum: f64 = 0.0;
let mut count: usize = 0;
for b in group {
match eval_expr(arg, b, graph)? {
Value::Null => {}
Value::Int(i) => { sum += i as f64; count += 1; }
Value::Float(f) => { sum += f; count += 1; }
v => return Err(DbError::Query(format!("avg() cannot aggregate {v:?}"))),
}
}
if count == 0 { Ok(Value::Null) } else { Ok(Value::Float(sum / count as f64)) }
}
"min" => {
let arg = args.first().ok_or_else(|| DbError::Query("min() requires 1 arg".into()))?;
let mut result: Option<Value> = None;
for b in group {
let v = eval_expr(arg, b, graph)?;
if v == Value::Null { continue; }
result = Some(match result {
None => v,
Some(cur) => if v.partial_cmp(&cur) == Some(std::cmp::Ordering::Less) { v } else { cur },
});
}
Ok(result.unwrap_or(Value::Null))
}
"max" => {
let arg = args.first().ok_or_else(|| DbError::Query("max() requires 1 arg".into()))?;
let mut result: Option<Value> = None;
for b in group {
let v = eval_expr(arg, b, graph)?;
if v == Value::Null { continue; }
result = Some(match result {
None => v,
Some(cur) => if v.partial_cmp(&cur) == Some(std::cmp::Ordering::Greater) { v } else { cur },
});
}
Ok(result.unwrap_or(Value::Null))
}
"collect" => {
let arg = args.first().ok_or_else(|| DbError::Query("collect() requires 1 arg".into()))?;
let vals: Vec<Value> = group
.iter()
.filter_map(|b| eval_expr(arg, b, graph).ok())
.filter(|v| *v != Value::Null)
.collect();
Ok(Value::List(vals))
}
_ => Err(DbError::Query(format!("unknown aggregate function: {name}"))),
}
}
fn execute_aggregate(
filtered: &[Bindings],
items: &[ReturnItem],
graph: &Graph,
) -> Result<Vec<Row>, DbError> {
let key_items: Vec<&ReturnItem> = items.iter().filter(|it| !contains_aggregate(&it.expr)).collect();
let mut group_order: Vec<Vec<String>> = Vec::new();
let mut group_member_indices: Vec<Vec<usize>> = Vec::new();
let mut key_to_group: HashMap<Vec<String>, usize> = HashMap::new();
for (i, bindings) in filtered.iter().enumerate() {
let key: Vec<String> = key_items
.iter()
.map(|item| match eval_expr(&item.expr, bindings, graph) {
Ok(v) => format!("{v:?}"),
Err(_) => "null".to_string(),
})
.collect();
let next_idx = group_order.len();
let gidx = *key_to_group.entry(key.clone()).or_insert_with(|| {
group_order.push(key);
group_member_indices.push(Vec::new());
next_idx
});
group_member_indices[gidx].push(i);
}
if group_order.is_empty() && key_items.is_empty() {
group_order.push(vec![]);
group_member_indices.push(vec![]);
}
let mut rows = Vec::new();
for (gidx, _) in group_order.iter().enumerate() {
let group: Vec<&Bindings> = group_member_indices[gidx]
.iter()
.map(|&i| &filtered[i])
.collect();
let mut row = Row::new();
for item in items {
let col = item.alias.clone().unwrap_or_else(|| expr_display(&item.expr));
let val = eval_expr_agg(&item.expr, &group, graph)?;
row.insert(col, val);
}
rows.push(row);
}
Ok(rows)
}
fn match_pattern(
pattern: &GraphPattern,
graph: &Graph,
path_mode: PathMode,
where_clause: Option<&Expr>,
) -> Vec<Bindings> {
let start_nodes: Vec<NodeId> = 'candidates: {
if let Some(primary) = pattern.start.labels.first() {
for pc in &pattern.start.properties {
if let Ok(val) = eval_expr_literal(&pc.value) {
if let Some(ids) = graph.lookup_by_property(primary, &pc.key, &val) {
break 'candidates ids;
}
}
}
if let (Some(var), Some(where_expr)) = (&pattern.start.variable, where_clause) {
if let Some(ids) = range_index_candidates(graph, primary, var, where_expr) {
break 'candidates ids;
}
}
break 'candidates graph.nodes_by_label(primary);
}
graph.all_nodes().into_iter().map(|n| n.id).collect()
};
let mut results: Vec<Bindings> = Vec::new();
for node_id in start_nodes {
let node = match graph.get_node(node_id) {
Some(n) => n,
None => continue,
};
if !check_node_constraints(&pattern.start, &node) {
continue;
}
let mut binding: Bindings = HashMap::new();
if let Some(ref var) = pattern.start.variable {
binding.insert(var.clone(), Binding::Node(node_id));
}
let mut visited_nodes: HashSet<NodeId> = HashSet::new();
visited_nodes.insert(node_id);
let visited_edges: HashSet<EdgeId> = HashSet::new();
let step_bindings = extend_steps(
&pattern.steps, node_id, binding, graph,
path_mode, &visited_edges, &visited_nodes,
);
results.extend(step_bindings);
}
results
}
fn extend_steps(
steps: &[EdgePatternStep],
current_node: NodeId,
binding: Bindings,
graph: &Graph,
path_mode: PathMode,
visited_edges: &HashSet<EdgeId>,
visited_nodes: &HashSet<NodeId>,
) -> Vec<Bindings> {
let Some(step) = steps.first() else {
return vec![binding];
};
let rest = &steps[1..];
let mut results = Vec::new();
if let Some(quant) = &step.edge.quantifier {
let endpoints = traverse_quantified(
graph, current_node, &step.edge, quant, path_mode,
visited_edges, visited_nodes,
);
for (end_node, edge_path) in endpoints {
let end_node_data = match graph.get_node(end_node) {
Some(n) => n,
None => continue,
};
if !check_node_constraints(&step.node, &end_node_data) {
continue;
}
let mut new_binding = binding.clone();
if let Some(ref var) = step.edge.variable {
new_binding.insert(var.clone(), Binding::EdgeList(edge_path.clone()));
}
if let Some(ref var) = step.node.variable {
new_binding.insert(var.clone(), Binding::Node(end_node));
}
let mut new_visited_edges = visited_edges.clone();
let mut new_visited_nodes = visited_nodes.clone();
new_visited_edges.extend(edge_path.iter().copied());
new_visited_nodes.insert(end_node);
let extended = extend_steps(
rest, end_node, new_binding, graph,
path_mode, &new_visited_edges, &new_visited_nodes,
);
results.extend(extended);
}
} else {
let edge_ids: Vec<EdgeId> = match step.edge.direction {
EdgeDirection::Outgoing => graph.outgoing_edges(current_node),
EdgeDirection::Incoming => graph.incoming_edges(current_node),
EdgeDirection::Either => {
let mut ids = graph.outgoing_edges(current_node);
ids.extend(graph.incoming_edges(current_node));
ids.sort();
ids.dedup();
ids
}
};
for edge_id in edge_ids {
let edge = match graph.get_edge(edge_id) {
Some(e) => e,
None => continue,
};
if let Some(ref lbl) = step.edge.label {
if &edge.label != lbl {
continue;
}
}
if !check_property_constraints(&step.edge.properties, &edge.properties) {
continue;
}
if path_mode == PathMode::Trail && visited_edges.contains(&edge_id) {
continue;
}
let neighbour = match step.edge.direction {
EdgeDirection::Outgoing => edge.to_node,
EdgeDirection::Incoming => edge.from_node,
EdgeDirection::Either => {
if edge.from_node == current_node { edge.to_node } else { edge.from_node }
}
};
if path_mode == PathMode::Simple && visited_nodes.contains(&neighbour) {
continue;
}
let nbr_node = match graph.get_node(neighbour) {
Some(n) => n,
None => continue,
};
if !check_node_constraints(&step.node, &nbr_node) {
continue;
}
let mut new_binding = binding.clone();
if let Some(ref var) = step.edge.variable {
new_binding.insert(var.clone(), Binding::Edge(edge_id));
}
if let Some(ref var) = step.node.variable {
new_binding.insert(var.clone(), Binding::Node(neighbour));
}
let mut new_visited_edges = visited_edges.clone();
let mut new_visited_nodes = visited_nodes.clone();
new_visited_edges.insert(edge_id);
new_visited_nodes.insert(neighbour);
let extended = extend_steps(
rest, neighbour, new_binding, graph,
path_mode, &new_visited_edges, &new_visited_nodes,
);
results.extend(extended);
}
}
results
}
struct QState {
node: NodeId,
depth: u32,
edge_path: Vec<EdgeId>,
visited_edges: HashSet<EdgeId>,
visited_nodes: HashSet<NodeId>,
}
fn traverse_quantified(
graph: &Graph,
start: NodeId,
edge_pat: &EdgePattern,
quant: &PathQuantifier,
path_mode: PathMode,
prior_visited_edges: &HashSet<EdgeId>,
prior_visited_nodes: &HashSet<NodeId>,
) -> Vec<(NodeId, Vec<EdgeId>)> {
let effective_max: u32 = match quant.max {
Some(n) => n,
None => match path_mode {
PathMode::Walk => 25u32.min(graph.node_count() as u32 + 1),
PathMode::Trail => graph.edge_count() as u32,
PathMode::Simple => graph.node_count() as u32,
},
};
let min = quant.min;
let mut results: Vec<(NodeId, Vec<EdgeId>)> = Vec::new();
let mut initial_visited_nodes = prior_visited_nodes.clone();
initial_visited_nodes.insert(start);
let mut queue: VecDeque<QState> = VecDeque::new();
queue.push_back(QState {
node: start,
depth: 0,
edge_path: Vec::new(),
visited_edges: prior_visited_edges.clone(),
visited_nodes: initial_visited_nodes,
});
while let Some(state) = queue.pop_front() {
if state.depth >= min {
results.push((state.node, state.edge_path.clone()));
}
if state.depth >= effective_max {
continue;
}
let candidate_edges: Vec<EdgeId> = match edge_pat.direction {
EdgeDirection::Outgoing => graph.outgoing_edges(state.node),
EdgeDirection::Incoming => graph.incoming_edges(state.node),
EdgeDirection::Either => {
let mut ids = graph.outgoing_edges(state.node);
ids.extend(graph.incoming_edges(state.node));
ids.sort();
ids.dedup();
ids
}
};
for edge_id in candidate_edges {
let edge = match graph.get_edge(edge_id) {
Some(e) => e,
None => continue,
};
if let Some(ref lbl) = edge_pat.label {
if &edge.label != lbl {
continue;
}
}
if !check_property_constraints(&edge_pat.properties, &edge.properties) {
continue;
}
if path_mode == PathMode::Trail && state.visited_edges.contains(&edge_id) {
continue;
}
let neighbour = match edge_pat.direction {
EdgeDirection::Outgoing => edge.to_node,
EdgeDirection::Incoming => edge.from_node,
EdgeDirection::Either => {
if edge.from_node == state.node { edge.to_node } else { edge.from_node }
}
};
if path_mode == PathMode::Simple && state.visited_nodes.contains(&neighbour) {
continue;
}
let mut new_visited_edges = state.visited_edges.clone();
new_visited_edges.insert(edge_id);
let mut new_visited_nodes = state.visited_nodes.clone();
new_visited_nodes.insert(neighbour);
let mut new_path = state.edge_path.clone();
new_path.push(edge_id);
queue.push_back(QState {
node: neighbour,
depth: state.depth + 1,
edge_path: new_path,
visited_edges: new_visited_edges,
visited_nodes: new_visited_nodes,
});
}
}
results
}
fn check_node_constraints(pattern: &NodePattern, node: &crate::types::Node) -> bool {
for lbl in &pattern.labels {
if !node.labels.contains(lbl) {
return false;
}
}
check_property_constraints(&pattern.properties, &node.properties)
}
fn check_property_constraints(
constraints: &[PropertyConstraint],
properties: &Properties,
) -> bool {
for c in constraints {
let actual = properties.get(&c.key).cloned().unwrap_or(Value::Null);
let expected = match eval_expr_literal(&c.value) {
Ok(v) => v,
Err(_) => return false,
};
if actual != expected {
return false;
}
}
true
}
fn project_return(
items: &[ReturnItem],
bindings: &Bindings,
graph: &Graph,
) -> Result<Row, DbError> {
let mut row = Row::new();
for item in items {
match &item.expr {
Expr::Star => {
for (var, binding) in bindings {
match binding {
Binding::Node(id) => {
row.insert(var.clone(), Value::String(ulid_encode(id.0)));
}
Binding::Edge(id) => {
row.insert(var.clone(), Value::String(ulid_encode(id.0)));
}
Binding::EdgeList(ids) => {
row.insert(
var.clone(),
Value::List(
ids.iter()
.map(|id| Value::String(ulid_encode(id.0)))
.collect(),
),
);
}
Binding::Value(v) => {
row.insert(var.clone(), v.clone());
}
}
}
}
expr => {
let val = eval_expr(expr, bindings, graph)?;
let key = item
.alias
.clone()
.unwrap_or_else(|| expr_display(expr));
row.insert(key, val);
}
}
}
Ok(row)
}
fn expr_display(expr: &Expr) -> String {
match expr {
Expr::Var(v) => v.clone(),
Expr::Property(obj, key) => format!("{}.{}", expr_display(obj), key),
Expr::Call(name, args) => {
let arg_strs: Vec<String> = args.iter().map(expr_display).collect();
format!("{}({})", name, arg_strs.join(", "))
}
Expr::Literal(v) => format!("{v}"),
Expr::Star => "*".to_string(),
_ => "expr".to_string(),
}
}
fn eval_expr(expr: &Expr, bindings: &Bindings, graph: &Graph) -> Result<Value, DbError> {
match expr {
Expr::Literal(v) => Ok(v.clone()),
Expr::Var(name) => match bindings.get(name) {
Some(Binding::Node(id)) => Ok(Value::String(ulid_encode(id.0))),
Some(Binding::Edge(id)) => Ok(Value::String(ulid_encode(id.0))),
Some(Binding::EdgeList(ids)) => Ok(Value::List(
ids.iter().map(|id| Value::String(ulid_encode(id.0))).collect(),
)),
Some(Binding::Value(v)) => Ok(v.clone()),
None => Ok(Value::Null),
},
Expr::Property(obj_expr, key) => {
let obj_name = match obj_expr.as_ref() {
Expr::Var(v) => v.clone(),
_ => return Err(DbError::Query("property access on non-variable".into())),
};
match bindings.get(&obj_name) {
Some(Binding::Node(id)) => Ok(graph
.get_node(*id)
.and_then(|n| n.properties.get(key).cloned())
.unwrap_or(Value::Null)),
Some(Binding::Edge(id)) => Ok(graph
.get_edge(*id)
.and_then(|e| e.properties.get(key).cloned())
.unwrap_or(Value::Null)),
Some(Binding::EdgeList(ids)) => {
let values: Vec<Value> = ids
.iter()
.map(|id| {
graph
.get_edge(*id)
.and_then(|e| e.properties.get(key).cloned())
.unwrap_or(Value::Null)
})
.collect();
Ok(Value::List(values))
}
Some(Binding::Value(Value::Map(m))) => {
Ok(m.get(key).cloned().unwrap_or(Value::Null))
}
Some(Binding::Value(_)) | None => Ok(Value::Null),
}
}
Expr::List(items) => {
let vals: Result<Vec<Value>, DbError> =
items.iter().map(|e| eval_expr(e, bindings, graph)).collect();
Ok(Value::List(vals?))
}
Expr::BinOp(left, op, right) => {
let l = eval_expr(left, bindings, graph)?;
let r = eval_expr(right, bindings, graph)?;
eval_binop(l, *op, r)
}
Expr::Not(inner) => {
let v = eval_expr(inner, bindings, graph)?;
match v {
Value::Bool(b) => Ok(Value::Bool(!b)),
_ => Ok(Value::Null),
}
}
Expr::IsNull(inner, is_not) => {
let v = eval_expr(inner, bindings, graph)?;
let is_null = v == Value::Null;
Ok(Value::Bool(if *is_not { !is_null } else { is_null }))
}
Expr::Call(name, args) => eval_function(name, args, bindings, graph),
Expr::Star => Err(DbError::Query("* not valid in expression context".into())),
Expr::Param(name) => {
Ok(QUERY_PARAMS.with(|p| p.borrow().get(name).cloned().unwrap_or(Value::Null)))
}
}
}
fn eval_expr_literal(expr: &Expr) -> Result<Value, DbError> {
match expr {
Expr::Literal(v) => Ok(v.clone()),
Expr::BinOp(l, op, r) => {
let lv = eval_expr_literal(l)?;
let rv = eval_expr_literal(r)?;
eval_binop(lv, *op, rv)
}
Expr::Not(inner) => match eval_expr_literal(inner)? {
Value::Bool(b) => Ok(Value::Bool(!b)),
_ => Ok(Value::Null),
},
Expr::Param(name) => {
Ok(QUERY_PARAMS.with(|p| p.borrow().get(name).cloned().unwrap_or(Value::Null)))
}
_ => Err(DbError::Query(
"non-literal expression where literal required".into(),
)),
}
}
fn eval_expr_on_row(expr: &Expr, row: &Row) -> Result<Value, DbError> {
let display = expr_display(expr);
if display != "expr" {
if let Some(v) = row.get(&display) {
return Ok(v.clone());
}
}
match expr {
Expr::Literal(v) => Ok(v.clone()),
Expr::Var(name) => Ok(row.get(name).cloned().unwrap_or(Value::Null)),
Expr::Property(obj_expr, key) => {
let obj_name = match obj_expr.as_ref() {
Expr::Var(v) => v.clone(),
_ => return Ok(Value::Null),
};
let col = format!("{}.{}", obj_name, key);
Ok(row.get(&col).cloned().unwrap_or(Value::Null))
}
Expr::BinOp(l, op, r) => {
let lv = eval_expr_on_row(l, row)?;
let rv = eval_expr_on_row(r, row)?;
eval_binop(lv, *op, rv)
}
_ => Ok(Value::Null),
}
}
fn eval_binop(l: Value, op: BinOp, r: Value) -> Result<Value, DbError> {
match op {
BinOp::And => Ok(Value::Bool(
matches!(l, Value::Bool(true)) && matches!(r, Value::Bool(true)),
)),
BinOp::Or => Ok(Value::Bool(
matches!(l, Value::Bool(true)) || matches!(r, Value::Bool(true)),
)),
BinOp::Eq => Ok(Value::Bool(l == r)),
BinOp::Neq => Ok(Value::Bool(l != r)),
BinOp::Lt => Ok(Value::Bool(l.partial_cmp(&r) == Some(std::cmp::Ordering::Less))),
BinOp::Lte => Ok(Value::Bool(
matches!(l.partial_cmp(&r), Some(std::cmp::Ordering::Less) | Some(std::cmp::Ordering::Equal)),
)),
BinOp::Gt => Ok(Value::Bool(l.partial_cmp(&r) == Some(std::cmp::Ordering::Greater))),
BinOp::Gte => Ok(Value::Bool(
matches!(l.partial_cmp(&r), Some(std::cmp::Ordering::Greater) | Some(std::cmp::Ordering::Equal)),
)),
BinOp::Add => numeric_op(l, r, |a, b| a + b, |a, b| a + b),
BinOp::Sub => numeric_op(l, r, |a, b| a - b, |a, b| a - b),
BinOp::Mul => numeric_op(l, r, |a, b| a * b, |a, b| a * b),
BinOp::Div => {
let is_zero = match &r {
Value::Int(0) => true,
Value::Float(f) => *f == 0.0,
_ => false,
};
if is_zero {
return Err(DbError::Query("division by zero".into()));
}
numeric_op(l, r, |a, b| a / b, |a, b| a / b)
}
BinOp::Mod => {
if matches!(r, Value::Int(0)) {
return Err(DbError::Query("modulo by zero".into()));
}
numeric_op(l, r, |a, b| a % b, |a, b| a % b)
}
BinOp::In => {
let found = match &r {
Value::List(items) => items.contains(&l),
_ => return Err(DbError::Query(format!("IN requires a list on the right side, got {:?}", r))),
};
Ok(Value::Bool(found))
}
}
}
fn numeric_op(
l: Value,
r: Value,
int_op: impl Fn(i64, i64) -> i64,
float_op: impl Fn(f64, f64) -> f64,
) -> Result<Value, DbError> {
match (l, r) {
(Value::Int(a), Value::Int(b)) => Ok(Value::Int(int_op(a, b))),
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(float_op(a, b))),
(Value::Int(a), Value::Float(b)) => Ok(Value::Float(float_op(a as f64, b))),
(Value::Float(a), Value::Int(b)) => Ok(Value::Float(float_op(a, b as f64))),
(l, r) => Err(DbError::Query(format!("cannot apply numeric op to {l:?} and {r:?}"))),
}
}
fn eval_function(
name: &str,
args: &[Expr],
bindings: &Bindings,
graph: &Graph,
) -> Result<Value, DbError> {
match name {
"count" => Ok(Value::Int(1)),
"id" | "elementid" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("id() requires 1 arg".into()))?,
bindings, graph)?;
Ok(v)
}
"labels" => {
let arg = args.first().ok_or_else(|| DbError::Query("labels() requires 1 arg".into()))?;
let var = match arg {
Expr::Var(v) => v.clone(),
_ => return Err(DbError::Query("labels() requires a node variable".into())),
};
match bindings.get(&var) {
Some(Binding::Node(id)) => {
let labels = graph
.get_node(*id)
.map(|n| n.labels.iter().map(|l| Value::String(l.clone())).collect())
.unwrap_or_default();
Ok(Value::List(labels))
}
_ => Ok(Value::Null),
}
}
"type" => {
let arg = args.first().ok_or_else(|| DbError::Query("type() requires 1 arg".into()))?;
let var = match arg {
Expr::Var(v) => v.clone(),
_ => return Err(DbError::Query("type() requires an edge variable".into())),
};
match bindings.get(&var) {
Some(Binding::Edge(id)) => Ok(graph
.get_edge(*id)
.map(|e| Value::String(e.label.clone()))
.unwrap_or(Value::Null)),
_ => Ok(Value::Null),
}
}
"tostring" | "to_string" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("toString() requires 1 arg".into()))?,
bindings, graph)?;
Ok(Value::String(format!("{v}")))
}
"tointeger" | "to_integer" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("toInteger() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::Int(i) => Ok(Value::Int(i)),
Value::Float(f) => {
if !f.is_finite() {
return Err(DbError::Query(format!("cannot convert non-finite float {f} to integer")));
}
Ok(Value::Int(f as i64))
}
Value::String(s) => s.parse::<i64>().map(Value::Int)
.map_err(|_| DbError::Query(format!("cannot convert '{s}' to integer"))),
_ => Ok(Value::Null),
}
}
"size" | "length" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("size() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::String(s) => Ok(Value::Int(s.chars().count() as i64)),
Value::List(l) => Ok(Value::Int(l.len() as i64)),
_ => Ok(Value::Null),
}
}
"tolower" | "lowercase" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("toLower() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::String(s) => Ok(Value::String(s.to_lowercase())),
_ => Ok(Value::Null),
}
}
"toupper" | "uppercase" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("toUpper() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::String(s) => Ok(Value::String(s.to_uppercase())),
_ => Ok(Value::Null),
}
}
"trim" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("trim() requires 1 arg".into()))?,
bindings, graph)?;
match v { Value::String(s) => Ok(Value::String(s.trim().to_string())), _ => Ok(Value::Null) }
}
"ltrim" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("ltrim() requires 1 arg".into()))?,
bindings, graph)?;
match v { Value::String(s) => Ok(Value::String(s.trim_start().to_string())), _ => Ok(Value::Null) }
}
"rtrim" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("rtrim() requires 1 arg".into()))?,
bindings, graph)?;
match v { Value::String(s) => Ok(Value::String(s.trim_end().to_string())), _ => Ok(Value::Null) }
}
"split" => {
if args.len() < 2 { return Err(DbError::Query("split() requires 2 args".into())); }
let s = eval_expr(&args[0], bindings, graph)?;
let d = eval_expr(&args[1], bindings, graph)?;
match (s, d) {
(Value::String(s), Value::String(d)) => {
let parts = s.split(d.as_str()).map(|p| Value::String(p.to_string())).collect();
Ok(Value::List(parts))
}
_ => Ok(Value::Null),
}
}
"replace" => {
if args.len() < 3 { return Err(DbError::Query("replace() requires 3 args".into())); }
let s = eval_expr(&args[0], bindings, graph)?;
let from = eval_expr(&args[1], bindings, graph)?;
let to = eval_expr(&args[2], bindings, graph)?;
match (s, from, to) {
(Value::String(s), Value::String(f), Value::String(t)) => {
Ok(Value::String(s.replace(f.as_str(), &t)))
}
_ => Ok(Value::Null),
}
}
"substring" => {
if args.is_empty() { return Err(DbError::Query("substring() requires at least 2 args".into())); }
let s = eval_expr(&args[0], bindings, graph)?;
let start = eval_expr(args.get(1).ok_or_else(|| DbError::Query("substring() requires start".into()))?,
bindings, graph)?;
let length = if args.len() >= 3 {
Some(eval_expr(&args[2], bindings, graph)?)
} else {
None
};
match (s, start) {
(Value::String(s), Value::Int(start)) => {
let start = start.max(0) as usize;
let chars: Vec<char> = s.chars().collect();
let slice = match length {
Some(Value::Int(len)) => {
let len = len.max(0) as usize;
chars.get(start..start.saturating_add(len)).unwrap_or(&[])
}
_ => chars.get(start..).unwrap_or(&[]),
};
Ok(Value::String(slice.iter().collect()))
}
_ => Ok(Value::Null),
}
}
"left" => {
if args.len() < 2 { return Err(DbError::Query("left() requires 2 args".into())); }
let s = eval_expr(&args[0], bindings, graph)?;
let n = eval_expr(&args[1], bindings, graph)?;
match (s, n) {
(Value::String(s), Value::Int(n)) => {
let n = n.max(0) as usize;
Ok(Value::String(s.chars().take(n).collect()))
}
_ => Ok(Value::Null),
}
}
"right" => {
if args.len() < 2 { return Err(DbError::Query("right() requires 2 args".into())); }
let s = eval_expr(&args[0], bindings, graph)?;
let n = eval_expr(&args[1], bindings, graph)?;
match (s, n) {
(Value::String(s), Value::Int(n)) => {
let n = n.max(0) as usize;
let chars: Vec<char> = s.chars().collect();
let start = chars.len().saturating_sub(n);
Ok(Value::String(chars[start..].iter().collect()))
}
_ => Ok(Value::Null),
}
}
"startswith" => {
if args.len() < 2 { return Err(DbError::Query("startsWith() requires 2 args".into())); }
let s = eval_expr(&args[0], bindings, graph)?;
let p = eval_expr(&args[1], bindings, graph)?;
match (s, p) {
(Value::String(s), Value::String(p)) => Ok(Value::Bool(s.starts_with(p.as_str()))),
_ => Ok(Value::Null),
}
}
"endswith" => {
if args.len() < 2 { return Err(DbError::Query("endsWith() requires 2 args".into())); }
let s = eval_expr(&args[0], bindings, graph)?;
let p = eval_expr(&args[1], bindings, graph)?;
match (s, p) {
(Value::String(s), Value::String(p)) => Ok(Value::Bool(s.ends_with(p.as_str()))),
_ => Ok(Value::Null),
}
}
"contains" => {
if args.len() < 2 { return Err(DbError::Query("contains() requires 2 args".into())); }
let s = eval_expr(&args[0], bindings, graph)?;
let sub = eval_expr(&args[1], bindings, graph)?;
match (s, sub) {
(Value::String(s), Value::String(sub)) => Ok(Value::Bool(s.contains(sub.as_str()))),
_ => Ok(Value::Null),
}
}
"tofloat" | "to_float" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("toFloat() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::Float(f) => Ok(Value::Float(f)),
Value::Int(i) => Ok(Value::Float(i as f64)),
Value::String(s) => s.parse::<f64>().map(Value::Float)
.map_err(|_| DbError::Query(format!("cannot convert '{s}' to float"))),
_ => Ok(Value::Null),
}
}
"coalesce" => {
for arg in args {
let v = eval_expr(arg, bindings, graph)?;
if v != Value::Null { return Ok(v); }
}
Ok(Value::Null)
}
"abs" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("abs() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::Int(i) => Ok(Value::Int(i.abs())),
Value::Float(f) => Ok(Value::Float(f.abs())),
_ => Ok(Value::Null),
}
}
"ceil" | "ceiling" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("ceil() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::Float(f) => Ok(Value::Float(f.ceil())),
Value::Int(i) => Ok(Value::Int(i)),
_ => Ok(Value::Null),
}
}
"floor" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("floor() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::Float(f) => Ok(Value::Float(f.floor())),
Value::Int(i) => Ok(Value::Int(i)),
_ => Ok(Value::Null),
}
}
"round" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("round() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::Float(f) => Ok(Value::Float(f.round())),
Value::Int(i) => Ok(Value::Int(i)),
_ => Ok(Value::Null),
}
}
"sign" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("sign() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::Int(i) => Ok(Value::Int(i.signum())),
Value::Float(f) => Ok(Value::Float(f.signum())),
_ => Ok(Value::Null),
}
}
"sqrt" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("sqrt() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::Float(f) => Ok(Value::Float(f.sqrt())),
Value::Int(i) => Ok(Value::Float((i as f64).sqrt())),
_ => Ok(Value::Null),
}
}
"head" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("head() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::List(items) => Ok(items.into_iter().next().unwrap_or(Value::Null)),
_ => Ok(Value::Null),
}
}
"last" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("last() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::List(items) => Ok(items.into_iter().last().unwrap_or(Value::Null)),
_ => Ok(Value::Null),
}
}
"tail" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("tail() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::List(mut items) => {
if !items.is_empty() { items.remove(0); }
Ok(Value::List(items))
}
_ => Ok(Value::Null),
}
}
"reverse" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("reverse() requires 1 arg".into()))?,
bindings, graph)?;
match v {
Value::List(mut items) => { items.reverse(); Ok(Value::List(items)) }
Value::String(s) => Ok(Value::String(s.chars().rev().collect())),
_ => Ok(Value::Null),
}
}
"range" => {
if args.len() < 2 { return Err(DbError::Query("range() requires at least 2 args".into())); }
let start = match eval_expr(&args[0], bindings, graph)? {
Value::Int(i) => i,
_ => return Err(DbError::Query("range(): start must be an integer".into())),
};
let end = match eval_expr(&args[1], bindings, graph)? {
Value::Int(i) => i,
_ => return Err(DbError::Query("range(): end must be an integer".into())),
};
let step: i64 = if args.len() >= 3 {
match eval_expr(&args[2], bindings, graph)? {
Value::Int(i) if i != 0 => i,
_ => return Err(DbError::Query("range(): step must be a non-zero integer".into())),
}
} else if start <= end { 1 } else { -1 };
let mut result = Vec::new();
let mut i = start;
while (step > 0 && i <= end) || (step < 0 && i >= end) {
result.push(Value::Int(i));
i += step;
if result.len() > 1_000_000 { break; } }
Ok(Value::List(result))
}
"keys" => {
let arg = args.first().ok_or_else(|| DbError::Query("keys() requires 1 arg".into()))?;
let var = match arg { Expr::Var(v) => v.clone(),
_ => return Err(DbError::Query("keys() requires a node or edge variable".into())), };
match bindings.get(&var) {
Some(Binding::Node(id)) => {
let mut ks: Vec<Value> = graph.get_node(*id)
.map(|n| n.properties.keys().map(|k| Value::String(k.clone())).collect())
.unwrap_or_default();
ks.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
Ok(Value::List(ks))
}
Some(Binding::Edge(id)) => {
let mut ks: Vec<Value> = graph.get_edge(*id)
.map(|e| e.properties.keys().map(|k| Value::String(k.clone())).collect())
.unwrap_or_default();
ks.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
Ok(Value::List(ks))
}
_ => Ok(Value::Null),
}
}
"exists" => {
let arg = args.first().ok_or_else(|| DbError::Query("exists() requires 1 arg".into()))?;
let v = eval_expr(arg, bindings, graph)?;
Ok(Value::Bool(v != Value::Null))
}
"isnull" | "is_null" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("isNull() requires 1 arg".into()))?,
bindings, graph)?;
Ok(Value::Bool(v == Value::Null))
}
"not" => {
let v = eval_expr(args.first().ok_or_else(|| DbError::Query("not() requires 1 arg".into()))?,
bindings, graph)?;
match v { Value::Bool(b) => Ok(Value::Bool(!b)), _ => Ok(Value::Null) }
}
f => Err(DbError::Query(format!("unknown function: {f}"))),
}
}
fn find_matching_node(n: &InsertNode, graph: &Graph) -> Option<NodeId> {
if n.properties.is_empty() {
return None;
}
let expected: Vec<(String, Value)> = n
.properties
.iter()
.filter_map(|pa| eval_expr_literal(&pa.value).ok().map(|v| (pa.key.clone(), v)))
.collect();
let candidates: Vec<NodeId> = if !n.labels.is_empty() {
graph.nodes_by_label(&n.labels[0])
} else {
graph.all_nodes().into_iter().map(|nd| nd.id).collect()
};
'outer: for id in candidates {
if let Some(node) = graph.get_node(id) {
for label in &n.labels {
if !node.labels.contains(label) {
continue 'outer;
}
}
for (key, val) in &expected {
match node.properties.get(key) {
Some(v) if v == val => {}
_ => continue 'outer,
}
}
return Some(id);
}
}
None
}
fn find_matching_edge(from: NodeId, to: NodeId, label: &str, graph: &Graph) -> Option<EdgeId> {
for eid in graph.outgoing_edges(from) {
if let Some(edge) = graph.get_edge(eid) {
if edge.label == label && edge.to_node == to {
return Some(eid);
}
}
}
None
}
fn build_insert_ops(
elements: &[InsertElement],
var_to_node: &mut HashMap<String, NodeId>,
graph: &mut Graph,
) -> Result<Vec<Operation>, DbError> {
let mut ops: Vec<Operation> = Vec::new();
for element in elements {
match element {
InsertElement::Node(n) => {
let id = if let Some(existing) = find_matching_node(n, graph) {
existing
} else {
let new_id = graph.alloc_node_id();
let mut props = Properties::new();
for pa in &n.properties {
props.insert(pa.key.clone(), eval_expr_literal(&pa.value)?);
}
graph.check_node_constraints(&n.labels, &props, None)?;
let node = crate::types::Node::new(new_id, n.labels.clone(), props);
ops.push(Operation::CreateNode { node });
new_id
};
if let Some(ref var) = n.variable {
var_to_node.insert(var.clone(), id);
}
}
InsertElement::Edge(e) => {
let from = *var_to_node.get(&e.from_var).ok_or_else(|| {
DbError::Query(format!(
"INSERT edge: variable '{}' not bound — declare the node earlier in the same INSERT or use MATCH … INSERT",
e.from_var
))
})?;
let to = *var_to_node.get(&e.to_var).ok_or_else(|| {
DbError::Query(format!(
"INSERT edge: variable '{}' not bound — declare the node earlier in the same INSERT or use MATCH … INSERT",
e.to_var
))
})?;
if find_matching_edge(from, to, &e.label, graph).is_none() {
let id = graph.alloc_edge_id();
let mut props = Properties::new();
for pa in &e.properties {
props.insert(pa.key.clone(), eval_expr_literal(&pa.value)?);
}
let edge = crate::types::Edge::new(id, e.label.clone(), from, to, props, e.directed);
ops.push(Operation::CreateEdge { edge });
}
}
}
}
Ok(ops)
}
fn execute_insert(
ins: InsertStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
let mut var_to_node: HashMap<String, NodeId> = HashMap::new();
let ops = build_insert_ops(&ins.elements, &mut var_to_node, graph)?;
let txn_id = *next_txn_id;
*next_txn_id += 1;
apply_ops(graph, &ops)?;
let count = ops.len();
Ok((vec![summary_row(format!("Inserted {count} element(s) [txn {txn_id}]"))], ops))
}
fn cross_join_patterns(
patterns: &[GraphPattern],
graph: &Graph,
mode: PathMode,
where_clause: Option<&Expr>,
) -> Vec<Bindings> {
let mut result: Vec<Bindings> = vec![HashMap::new()];
for pattern in patterns {
let pattern_bindings = match_pattern(pattern, graph, mode, where_clause);
let mut next: Vec<Bindings> = Vec::new();
for existing in &result {
for new_b in &pattern_bindings {
let mut merged = existing.clone();
for (k, v) in new_b {
merged.insert(k.clone(), v.clone());
}
next.push(merged);
}
}
result = next;
}
result
}
fn range_index_candidates<'a>(
graph: &'a Graph,
label: &str,
var: &str,
where_expr: &'a Expr,
) -> Option<Vec<NodeId>> {
for idx_def in &graph.index_defs {
if idx_def.label != label {
continue;
}
let (lo, hi) = extract_prop_range(where_expr, var, &idx_def.property);
if lo.is_some() || hi.is_some() {
return graph.lookup_by_property_range(label, &idx_def.property, lo, hi);
}
}
None
}
fn extract_prop_range<'a>(
expr: &'a Expr,
var: &str,
prop: &str,
) -> (Option<(&'a Value, bool)>, Option<(&'a Value, bool)>) {
let mut lo: Option<(&Value, bool)> = None;
let mut hi: Option<(&Value, bool)> = None;
collect_bounds(expr, var, prop, &mut lo, &mut hi);
(lo, hi)
}
fn collect_bounds<'a>(
expr: &'a Expr,
var: &str,
prop: &str,
lo: &mut Option<(&'a Value, bool)>,
hi: &mut Option<(&'a Value, bool)>,
) {
match expr {
Expr::BinOp(l, BinOp::And, r) => {
collect_bounds(l, var, prop, lo, hi);
collect_bounds(r, var, prop, lo, hi);
}
Expr::BinOp(l, op, r)
if matches!(op, BinOp::Gt | BinOp::Gte) && is_var_prop(l, var, prop) =>
{
if let Expr::Literal(val) = r.as_ref() {
if lo.is_none() {
*lo = Some((val, *op == BinOp::Gte));
}
}
}
Expr::BinOp(l, op, r)
if matches!(op, BinOp::Lt | BinOp::Lte) && is_var_prop(r, var, prop) =>
{
if let Expr::Literal(val) = l.as_ref() {
if lo.is_none() {
*lo = Some((val, *op == BinOp::Lte));
}
}
}
Expr::BinOp(l, op, r)
if matches!(op, BinOp::Lt | BinOp::Lte) && is_var_prop(l, var, prop) =>
{
if let Expr::Literal(val) = r.as_ref() {
if hi.is_none() {
*hi = Some((val, *op == BinOp::Lte));
}
}
}
Expr::BinOp(l, op, r)
if matches!(op, BinOp::Gt | BinOp::Gte) && is_var_prop(r, var, prop) =>
{
if let Expr::Literal(val) = l.as_ref() {
if hi.is_none() {
*hi = Some((val, *op == BinOp::Gte));
}
}
}
_ => {}
}
}
fn is_var_prop(expr: &Expr, var: &str, prop: &str) -> bool {
matches!(
expr,
Expr::Property(base, p) if p == prop && matches!(base.as_ref(), Expr::Var(v) if v == var)
)
}
fn execute_match_insert(
mi: MatchInsertStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
let merged = cross_join_patterns(&mi.patterns, graph, PathMode::Walk, mi.where_clause.as_ref());
let filtered: Vec<Bindings> = merged
.into_iter()
.filter(|b| {
if let Some(ref cond) = mi.where_clause {
matches!(eval_expr(cond, b, graph), Ok(Value::Bool(true)))
} else {
true
}
})
.collect();
let mut all_ops: Vec<Operation> = Vec::new();
for binding in &filtered {
let mut var_to_node: HashMap<String, NodeId> = HashMap::new();
for (var, b) in binding {
if let Binding::Node(id) = b {
var_to_node.insert(var.clone(), *id);
}
}
let ops = build_insert_ops(&mi.elements, &mut var_to_node, graph)?;
all_ops.extend(ops);
}
let txn_id = *next_txn_id;
*next_txn_id += 1;
let count = all_ops.len();
apply_ops(graph, &all_ops)?;
Ok((vec![summary_row(format!("Inserted {count} element(s) [txn {txn_id}]"))], all_ops))
}
fn execute_set(
s: SetStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
let bindings = match_pattern(&s.match_pattern, graph, PathMode::Walk, s.where_clause.as_ref());
let filtered: Vec<Bindings> = bindings
.into_iter()
.filter(|b| {
if let Some(ref cond) = s.where_clause {
matches!(eval_expr(cond, b, graph), Ok(Value::Bool(true)))
} else {
true
}
})
.collect();
let mut ops: Vec<Operation> = Vec::new();
for binding in &filtered {
for item in &s.assignments {
match item {
SetItem::Property { variable, key, value } => {
let val = eval_expr(value, binding, graph)?;
match binding.get(variable) {
Some(Binding::Node(id)) => ops.push(Operation::SetProperty {
target: PropertyTarget::Node(*id),
key: key.clone(),
value: val,
}),
Some(Binding::Edge(id)) => ops.push(Operation::SetProperty {
target: PropertyTarget::Edge(*id),
key: key.clone(),
value: val,
}),
Some(Binding::EdgeList(_)) | Some(Binding::Value(_)) => {
return Err(DbError::Query(format!(
"cannot SET property on path variable '{variable}'; \
bind individual edges instead"
)))
}
None => {
return Err(DbError::Query(format!(
"variable '{variable}' not bound"
)))
}
}
}
SetItem::AddLabel { variable, label } => match binding.get(variable) {
Some(Binding::Node(id)) => ops.push(Operation::AddLabel {
node_id: *id,
label: label.clone(),
}),
_ => {
return Err(DbError::Query(format!(
"variable '{variable}' not a node"
)))
}
},
}
}
}
let count = ops.len();
let txn_id = *next_txn_id;
*next_txn_id += 1;
apply_ops(graph, &ops)?;
Ok((vec![summary_row(format!("Set {count} property/label operation(s) [txn {txn_id}]"))], ops))
}
fn execute_remove(
r: RemoveStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
let bindings = match_pattern(&r.match_pattern, graph, PathMode::Walk, r.where_clause.as_ref());
let filtered: Vec<Bindings> = bindings
.into_iter()
.filter(|b| {
if let Some(ref cond) = r.where_clause {
matches!(eval_expr(cond, b, graph), Ok(Value::Bool(true)))
} else {
true
}
})
.collect();
let mut ops: Vec<Operation> = Vec::new();
for binding in &filtered {
for item in &r.items {
match item {
RemoveItem::Property { variable, key } => match binding.get(variable) {
Some(Binding::Node(id)) => ops.push(Operation::RemoveProperty {
target: PropertyTarget::Node(*id),
key: key.clone(),
}),
Some(Binding::Edge(id)) => ops.push(Operation::RemoveProperty {
target: PropertyTarget::Edge(*id),
key: key.clone(),
}),
Some(Binding::EdgeList(_)) | Some(Binding::Value(_)) => {
return Err(DbError::Query(format!(
"cannot REMOVE property on path variable '{variable}'"
)))
}
None => return Err(DbError::Query(format!("variable '{variable}' not bound"))),
},
RemoveItem::Label { variable, label } => match binding.get(variable) {
Some(Binding::Node(id)) => ops.push(Operation::RemoveLabel {
node_id: *id,
label: label.clone(),
}),
_ => return Err(DbError::Query(format!("'{variable}' is not a node"))),
},
}
}
}
let count = ops.len();
let txn_id = *next_txn_id;
*next_txn_id += 1;
apply_ops(graph, &ops)?;
Ok((vec![summary_row(format!("Removed {count} property/label(s) [txn {txn_id}]"))], ops))
}
fn execute_delete(
d: DeleteStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
let bindings = match_pattern(&d.match_pattern, graph, PathMode::Walk, d.where_clause.as_ref());
let filtered: Vec<Bindings> = bindings
.into_iter()
.filter(|b| {
if let Some(ref cond) = d.where_clause {
matches!(eval_expr(cond, b, graph), Ok(Value::Bool(true)))
} else {
true
}
})
.collect();
let mut ops: Vec<Operation> = Vec::new();
for binding in &filtered {
for var in &d.variables {
match binding.get(var) {
Some(Binding::Node(id)) => {
if d.detach {
ops.push(Operation::DeleteNodeDetach { node_id: *id });
} else {
ops.push(Operation::DeleteNode { node_id: *id });
}
}
Some(Binding::Edge(id)) => {
ops.push(Operation::DeleteEdge { edge_id: *id });
}
Some(Binding::EdgeList(_)) | Some(Binding::Value(_)) => {
return Err(DbError::Query(format!(
"cannot DELETE path variable '{var}'; \
iterate individual edges instead"
)))
}
None => return Err(DbError::Query(format!("variable '{var}' not bound"))),
}
}
}
let count = ops.len();
let txn_id = *next_txn_id;
*next_txn_id += 1;
apply_ops(graph, &ops)?;
Ok((vec![summary_row(format!("Deleted {count} element(s) [txn {txn_id}]"))], ops))
}
fn execute_create_index(
ci: CreateIndexStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
let already = graph.has_property_index(&ci.label, &ci.property);
graph.apply_create_index(&ci.label, &ci.property);
let txn_id = *next_txn_id;
*next_txn_id += 1;
let msg = if already {
format!("Index ON :{}({}) already exists [txn {txn_id}]", ci.label, ci.property)
} else {
format!("Index created ON :{}({}) [txn {txn_id}]", ci.label, ci.property)
};
let op = Operation::CreateIndex { label: ci.label, property: ci.property };
Ok((vec![summary_row(msg)], vec![op]))
}
fn execute_drop_index(
di: DropIndexStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
let existed = graph.drop_property_index(&di.label, &di.property);
let txn_id = *next_txn_id;
*next_txn_id += 1;
let msg = if existed {
format!("Index ON :{}({}) dropped [txn {txn_id}]", di.label, di.property)
} else {
format!("No index ON :{}({}) [txn {txn_id}]", di.label, di.property)
};
let op = Operation::DropIndex { label: di.label, property: di.property };
Ok((vec![summary_row(msg)], if existed { vec![op] } else { vec![] }))
}
fn execute_show_indexes(graph: &Graph) -> Result<Vec<Row>, DbError> {
let indexes = graph.list_property_indexes();
if indexes.is_empty() {
return Ok(vec![summary_row("No indexes defined.".to_string())]);
}
Ok(indexes
.into_iter()
.map(|(label, property, count)| {
let mut row = Row::new();
row.insert("label".to_string(), Value::String(label));
row.insert("property".to_string(), Value::String(property));
row.insert("entries".to_string(), Value::Int(count as i64));
row
})
.collect())
}
fn execute_call(c: super::ast::CallStatement, graph: &Graph) -> Result<Vec<Row>, DbError> {
let params: HashMap<String, Value> = c
.params
.into_iter()
.map(|(k, expr)| eval_const_expr(&expr).map(|v| (k, v)))
.collect::<Result<_, _>>()?;
let mut rows = crate::algorithms::dispatch_call(graph, &c.name, ¶ms)?;
if let Some(cols) = c.yields {
let col_set: std::collections::HashSet<&str> =
cols.iter().map(String::as_str).collect();
for row in &mut rows {
row.retain(|k, _| col_set.contains(k.as_str()));
}
}
Ok(rows)
}
fn execute_call_pipeline(c: super::ast::CallPipelineStatement, graph: &Graph) -> Result<Vec<Row>, DbError> {
let params: HashMap<String, Value> = c
.params
.into_iter()
.map(|(k, expr)| eval_const_expr(&expr).map(|v| (k, v)))
.collect::<Result<_, _>>()?;
let algo_rows = crate::algorithms::dispatch_call(graph, &c.name, ¶ms)?;
let col_set: std::collections::HashSet<&str> =
c.yields.iter().map(String::as_str).collect();
let mut bindings: Vec<Bindings> = algo_rows
.into_iter()
.map(|row| {
row.into_iter()
.filter(|(k, _)| col_set.contains(k.as_str()))
.map(|(k, v)| (k, Binding::Value(v)))
.collect()
})
.collect();
if let Some(mc) = c.match_clause {
bindings = bindings
.into_iter()
.flat_map(|seed| {
cross_join_patterns_seeded(&mc.patterns, &seed, mc.path_mode, mc.where_clause.as_ref(), graph)
})
.collect();
}
apply_return_clause(bindings, &c.return_clause, graph)
}
fn eval_const_expr(expr: &Expr) -> Result<Value, DbError> {
match expr {
Expr::Literal(v) => Ok(v.clone()),
Expr::BinOp(l, op, r) => {
let lv = eval_const_expr(l)?;
let rv = eval_const_expr(r)?;
apply_binop(*op, &lv, &rv)
}
Expr::Not(e) => {
let v = eval_const_expr(e)?;
match v {
Value::Bool(b) => Ok(Value::Bool(!b)),
other => Err(DbError::Query(format!(
"CALL parameter: NOT requires a boolean, got {other:?}"
))),
}
}
other => Err(DbError::Query(format!(
"CALL parameters must be constant expressions (literals or arithmetic). \
Got: {other:?}. Use literal values like 0.85 or \"out\"."
))),
}
}
fn apply_binop(op: BinOp, l: &Value, r: &Value) -> Result<Value, DbError> {
use Value::*;
match (op, l, r) {
(BinOp::Add, Int(a), Int(b)) => Ok(Int(a + b)),
(BinOp::Sub, Int(a), Int(b)) => Ok(Int(a - b)),
(BinOp::Mul, Int(a), Int(b)) => Ok(Int(a * b)),
(BinOp::Div, Int(a), Int(b)) => {
if *b == 0 { Err(DbError::Query("division by zero".into())) }
else { Ok(Int(a / b)) }
}
(BinOp::Mod, Int(a), Int(b)) => {
if *b == 0 { Err(DbError::Query("modulo by zero".into())) }
else { Ok(Int(a % b)) }
}
(BinOp::Add, Float(a), Float(b)) => Ok(Float(a + b)),
(BinOp::Sub, Float(a), Float(b)) => Ok(Float(a - b)),
(BinOp::Mul, Float(a), Float(b)) => Ok(Float(a * b)),
(BinOp::Div, Float(a), Float(b)) => Ok(Float(a / b)),
(BinOp::Add, Int(a), Float(b)) => Ok(Float(*a as f64 + b)),
(BinOp::Add, Float(a), Int(b)) => Ok(Float(a + *b as f64)),
(BinOp::Sub, Int(a), Float(b)) => Ok(Float(*a as f64 - b)),
(BinOp::Sub, Float(a), Int(b)) => Ok(Float(a - *b as f64)),
(BinOp::Mul, Int(a), Float(b)) => Ok(Float(*a as f64 * b)),
(BinOp::Mul, Float(a), Int(b)) => Ok(Float(a * *b as f64)),
(BinOp::Div, Int(a), Float(b)) => Ok(Float(*a as f64 / b)),
(BinOp::Div, Float(a), Int(b)) => Ok(Float(a / *b as f64)),
_ => Err(DbError::Query(format!(
"unsupported constant operation {op:?} on {l:?} and {r:?}"
))),
}
}
fn summary_row(msg: String) -> Row {
let mut row = Row::new();
row.insert("result".to_string(), Value::String(msg));
row
}
fn execute_truncate(
graph: &mut Graph,
) -> Result<(Vec<Row>, Vec<crate::transaction::operation::Operation>), DbError> {
graph.clear()?;
let mut row = Row::new();
row.insert("cleared".into(), crate::types::Value::Bool(true));
Ok((vec![row], vec![]))
}
fn execute_load_csv_nodes(
stmt: super::ast::LoadCsvNodesStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<crate::transaction::operation::Operation>), DbError> {
use std::fs::File;
use crate::csv_import::load_nodes_csv;
let file = File::open(&stmt.path)
.map_err(|e| DbError::Storage(e))?;
let result = load_nodes_csv(file, graph, stmt.label.as_deref())?;
graph.csv_id_map = result.id_map;
*next_txn_id += 1;
let msg = format!("loaded {} nodes", result.inserted);
Ok((vec![summary_row(msg)], vec![]))
}
fn execute_load_csv_edges(
stmt: super::ast::LoadCsvEdgesStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<crate::transaction::operation::Operation>), DbError> {
use std::fs::File;
use crate::csv_import::load_edges_csv;
let file = File::open(&stmt.path)
.map_err(|e| DbError::Storage(e))?;
let id_map = graph.csv_id_map.clone();
let result = load_edges_csv(file, graph, &id_map, stmt.label.as_deref())?;
*next_txn_id += 1;
let msg = if result.skipped > 0 {
format!("loaded {} edges ({} skipped — unresolved IDs)", result.inserted, result.skipped)
} else {
format!("loaded {} edges", result.inserted)
};
Ok((vec![summary_row(msg)], vec![]))
}
fn execute_unwind_insert(
stmt: UnwindInsertStatement,
graph: &mut Graph,
next_txn_id: &mut u64,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
let empty: Bindings = HashMap::new();
let list = eval_expr(&stmt.expr, &empty, graph)?;
let items = match list {
Value::List(v) => v,
other => return Err(DbError::Query(
format!("UNWIND INSERT: expression must evaluate to a list, got {other}"),
)),
};
let mut all_ops: Vec<Operation> = Vec::new();
for item in items {
let mut bindings: Bindings = HashMap::new();
bindings.insert(stmt.variable.clone(), Binding::Value(item));
let mut var_to_node: HashMap<String, NodeId> = HashMap::new();
for element in &stmt.elements {
match element {
InsertElement::Node(n) => {
let new_id = graph.alloc_node_id();
let mut props = Properties::new();
for pa in &n.properties {
props.insert(pa.key.clone(), eval_expr(&pa.value, &bindings, graph)?);
}
graph.check_node_constraints(&n.labels, &props, None)?;
let node = crate::types::Node::new(new_id, n.labels.clone(), props);
all_ops.push(Operation::CreateNode { node });
if let Some(ref var) = n.variable {
var_to_node.insert(var.clone(), new_id);
}
}
InsertElement::Edge(e) => {
let from = *var_to_node.get(&e.from_var).ok_or_else(|| {
DbError::Query(format!("UNWIND INSERT edge: '{}' not bound", e.from_var))
})?;
let to = *var_to_node.get(&e.to_var).ok_or_else(|| {
DbError::Query(format!("UNWIND INSERT edge: '{}' not bound", e.to_var))
})?;
if find_matching_edge(from, to, &e.label, graph).is_none() {
let id = graph.alloc_edge_id();
let mut props = Properties::new();
for pa in &e.properties {
props.insert(pa.key.clone(), eval_expr(&pa.value, &bindings, graph)?);
}
let edge = crate::types::Edge::new(id, e.label.clone(), from, to, props, e.directed);
all_ops.push(Operation::CreateEdge { edge });
}
}
}
}
}
apply_ops(graph, &all_ops)?;
let txn_id = *next_txn_id;
*next_txn_id += 1;
let count = all_ops.len();
Ok((vec![summary_row(format!("Inserted {count} element(s) [txn {txn_id}]"))], all_ops))
}
fn execute_constraint(
stmt: ConstraintStatement,
graph: &mut Graph,
) -> Result<(Vec<Row>, Vec<Operation>), DbError> {
use crate::graph::constraints as gc;
fn to_graph_kind(k: &ConstraintKind) -> gc::ConstraintKind {
match k {
ConstraintKind::Unique => gc::ConstraintKind::Unique,
ConstraintKind::Type(vk) => gc::ConstraintKind::Type(match vk {
ValueKind::Integer => gc::ValueKind::Integer,
ValueKind::Float => gc::ValueKind::Float,
ValueKind::String => gc::ValueKind::String,
ValueKind::Boolean => gc::ValueKind::Boolean,
}),
}
}
match stmt.op {
ConstraintOp::Show => {
let rows = graph.list_constraints().iter().map(|c| {
let mut row = Row::new();
row.insert("label".into(), Value::String(c.label.clone()));
row.insert("property".into(), Value::String(c.property.clone()));
row.insert("kind".into(), Value::String(match &c.kind {
gc::ConstraintKind::Unique => "UNIQUE".to_string(),
gc::ConstraintKind::Type(vk) => format!("TYPE IS {}", vk),
}));
row
}).collect();
Ok((rows, vec![]))
}
ConstraintOp::Create { kind, label, property } => {
let def = gc::ConstraintDef {
kind: to_graph_kind(&kind),
label: label.clone(),
property: property.clone(),
};
let added = graph.add_constraint(def)?;
let msg = if added {
format!("constraint created on :{label}({property})")
} else {
format!("constraint already exists on :{label}({property})")
};
Ok((vec![summary_row(msg)], vec![]))
}
ConstraintOp::Drop { kind, label, property } => {
let graph_kind = to_graph_kind(&kind);
let removed = graph.remove_constraint(&label, &property, &graph_kind);
let msg = if removed {
format!("constraint dropped on :{label}({property})")
} else {
format!("constraint not found on :{label}({property})")
};
Ok((vec![summary_row(msg)], vec![]))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::Graph;
use crate::types::DbError;
fn run(graph: &mut Graph, txn: &mut u64, gql: &str) -> Result<Vec<Row>, DbError> {
let stmt = super::super::parser::parse(gql)?;
execute(stmt, graph, txn)
}
fn col(rows: &[Row], c: &str) -> Vec<String> {
rows.iter()
.map(|r| r.get(c).map(|v| format!("{v}")).unwrap_or_else(|| "null".to_string()))
.collect()
}
fn sorted_col(rows: &[Row], c: &str) -> Vec<String> {
let mut v = col(rows, c);
v.sort();
v
}
fn three_people() -> (Graph, u64) {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob", age: 25})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Carol", age: 35})"#).unwrap();
(g, txn)
}
#[test]
fn insert_edge_combined_statement() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:Person {name: "Alice"}), (b:Person {name: "Bob"}), (a)-[:KNOWS]->(b)"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (x)-[:KNOWS]->(y) RETURN x.name, y.name"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(col(&rows, "x.name"), vec!["\"Alice\""]);
assert_eq!(col(&rows, "y.name"), vec!["\"Bob\""]);
}
#[test]
fn insert_multiple_edges_in_one_statement() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:N {name: "A"}), (b:N {name: "B"}), (c:N {name: "C"}),
(a)-[:R]->(b), (b)-[:R]->(c)"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (x)-[:R]->(y) RETURN x.name, y.name"#).unwrap();
assert_eq!(rows.len(), 2);
}
#[test]
fn insert_edge_only_inserts_correct_endpoints() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (src:City {name: "NYC"}), (dst:City {name: "LA"}), (src)-[:FLIGHT]->(dst)"#,
)
.unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (x:City)-[:FLIGHT]->(y:City) RETURN x.name, y.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(col(&rows, "x.name"), vec!["\"NYC\""]);
assert_eq!(col(&rows, "y.name"), vec!["\"LA\""]);
}
#[test]
fn order_by_numeric_desc() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN n.name ORDER BY n.age DESC"#,
)
.unwrap();
assert_eq!(col(&rows, "n.name"), vec!["\"Carol\"", "\"Alice\"", "\"Bob\""]);
}
#[test]
fn order_by_numeric_asc() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN n.name ORDER BY n.age ASC"#,
)
.unwrap();
assert_eq!(col(&rows, "n.name"), vec!["\"Bob\"", "\"Alice\"", "\"Carol\""]);
}
#[test]
fn order_by_string_asc() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN n.name ORDER BY n.name ASC"#,
)
.unwrap();
assert_eq!(col(&rows, "n.name"), vec!["\"Alice\"", "\"Bob\"", "\"Carol\""]);
}
#[test]
fn order_by_with_where_clause() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.age > 28 RETURN n.name ORDER BY n.age DESC"#,
)
.unwrap();
assert_eq!(col(&rows, "n.name"), vec!["\"Carol\"", "\"Alice\""]);
}
#[test]
fn distinct_removes_duplicate_rows() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Tag {name: "rust"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Tag {name: "rust"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Tag {name: "go"})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (t:Tag) RETURN DISTINCT t.name"#).unwrap();
assert_eq!(rows.len(), 2, "DISTINCT should collapse the two 'rust' rows into one");
}
#[test]
fn distinct_all_unique_returns_same_count() {
let (mut g, mut txn) = three_people();
let all = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
let dedup = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN DISTINCT n.name"#).unwrap();
assert_eq!(all.len(), dedup.len(), "no duplicates to remove, counts should match");
}
#[test]
fn match_outgoing_edge() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:P {name: "A"}), (b:P {name: "B"}), (a)-[:KNOWS]->(b)"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (x)-[:KNOWS]->(y) RETURN x.name, y.name"#).unwrap();
assert_eq!(rows.len(), 1);
}
#[test]
fn match_incoming_edge() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:P {name: "src"}), (b:P {name: "dst"}), (a)-[:LINK]->(b)"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (y)<-[:LINK]-(x) RETURN x.name, y.name"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(col(&rows, "x.name"), vec!["\"src\""]);
assert_eq!(col(&rows, "y.name"), vec!["\"dst\""]);
}
#[test]
fn match_either_direction_edge() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:N {name: "A"}), (b:N {name: "B"}), (a)-[:R]->(b)"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (x)-[:R]-(y) RETURN x.name, y.name"#).unwrap();
assert_eq!(rows.len(), 2);
}
#[test]
fn match_edge_type_filter() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:N {name: "A"}), (b:N {name: "B"}), (c:N {name: "C"}),
(a)-[:KNOWS]->(b), (a)-[:LIKES]->(c)"#,
)
.unwrap();
let knows = run(&mut g, &mut txn, r#"MATCH (x)-[:KNOWS]->(y) RETURN y.name"#).unwrap();
assert_eq!(knows.len(), 1);
assert_eq!(col(&knows, "y.name"), vec!["\"B\""]);
let likes = run(&mut g, &mut txn, r#"MATCH (x)-[:LIKES]->(y) RETURN y.name"#).unwrap();
assert_eq!(likes.len(), 1);
assert_eq!(col(&likes, "y.name"), vec!["\"C\""]);
}
#[test]
fn set_property_updates_value() {
let (mut g, mut txn) = three_people();
run(&mut g, &mut txn, r#"MATCH (n:Person) WHERE n.name = "Alice" SET n.age = 99"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" RETURN n.age"#,
)
.unwrap();
assert_eq!(col(&rows, "n.age"), vec!["99"]);
}
#[test]
fn set_property_does_not_affect_other_nodes() {
let (mut g, mut txn) = three_people();
run(&mut g, &mut txn, r#"MATCH (n:Person) WHERE n.name = "Alice" SET n.age = 99"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Bob" RETURN n.age"#,
)
.unwrap();
assert_eq!(col(&rows, "n.age"), vec!["25"], "Bob's age should be unchanged");
}
#[test]
fn set_adds_new_property() {
let (mut g, mut txn) = three_people();
run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" SET n.email = "alice@example.com""#,
)
.unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" RETURN n.email"#,
)
.unwrap();
assert_eq!(col(&rows, "n.email"), vec!["\"alice@example.com\""]);
}
#[test]
fn set_label_adds_label() {
let (mut g, mut txn) = three_people();
run(&mut g, &mut txn, r#"MATCH (n:Person) WHERE n.name = "Alice" SET n:Manager"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Manager) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(col(&rows, "n.name"), vec!["\"Alice\""]);
}
#[test]
fn set_label_does_not_remove_original_label() {
let (mut g, mut txn) = three_people();
run(&mut g, &mut txn, r#"MATCH (n:Person) WHERE n.name = "Alice" SET n:Manager"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" RETURN n.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
}
#[test]
fn remove_property_makes_it_null() {
let (mut g, mut txn) = three_people();
run(&mut g, &mut txn, r#"MATCH (n:Person) WHERE n.name = "Bob" REMOVE n.age"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Bob" RETURN n.age"#,
)
.unwrap();
assert_eq!(col(&rows, "n.age"), vec!["null"]);
}
#[test]
fn remove_property_does_not_affect_other_nodes() {
let (mut g, mut txn) = three_people();
run(&mut g, &mut txn, r#"MATCH (n:Person) WHERE n.name = "Bob" REMOVE n.age"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" RETURN n.age"#,
)
.unwrap();
assert_eq!(col(&rows, "n.age"), vec!["30"], "Alice's age should be unchanged");
}
#[test]
fn remove_label_removes_from_index() {
let (mut g, mut txn) = three_people();
run(&mut g, &mut txn, r#"MATCH (n:Person) WHERE n.name = "Alice" SET n:Employee"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (n:Employee) REMOVE n:Employee"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Employee) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 0, "Employee label should have been removed");
}
#[test]
fn remove_label_keeps_other_labels() {
let (mut g, mut txn) = three_people();
run(&mut g, &mut txn, r#"MATCH (n:Person) WHERE n.name = "Alice" SET n:Employee"#).unwrap();
run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" REMOVE n:Employee"#,
)
.unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" RETURN n.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
}
#[test]
fn delete_removes_node() {
let (mut g, mut txn) = three_people();
run(&mut g, &mut txn, r#"MATCH (n:Person) WHERE n.name = "Alice" DELETE n"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 2);
assert!(!sorted_col(&rows, "n.name").contains(&"\"Alice\"".to_string()));
}
#[test]
fn delete_node_with_edges_fails() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:N {name: "A"}), (b:N {name: "B"}), (a)-[:R]->(b)"#,
)
.unwrap();
let result = run(&mut g, &mut txn, r#"MATCH (n:N) WHERE n.name = "A" DELETE n"#);
assert!(result.is_err(), "DELETE on a node with edges should fail without DETACH");
}
#[test]
fn detach_delete_removes_node_and_incident_edges() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:Person {name: "Alice"}), (b:Person {name: "Bob"}), (a)-[:KNOWS]->(b)"#,
)
.unwrap();
run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" DETACH DELETE n"#,
)
.unwrap();
let nodes = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(nodes.len(), 1);
assert_eq!(col(&nodes, "n.name"), vec!["\"Bob\""]);
let edges = run(&mut g, &mut txn, r#"MATCH (x)-[:KNOWS]->(y) RETURN x.name"#).unwrap();
assert_eq!(edges.len(), 0, "incident edge should have been removed");
}
#[test]
fn where_greater_than_filters() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.age > 28 RETURN n.name"#,
)
.unwrap();
assert_eq!(sorted_col(&rows, "n.name"), vec!["\"Alice\"", "\"Carol\""]);
}
#[test]
fn where_and_combines_conditions() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.age > 24 AND n.age < 35 RETURN n.name"#,
)
.unwrap();
assert_eq!(sorted_col(&rows, "n.name"), vec!["\"Alice\"", "\"Bob\""]);
}
#[test]
fn where_is_null() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Item {name: "has_age", age: 5})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Item {name: "no_age"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Item) WHERE n.age IS NULL RETURN n.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(col(&rows, "n.name"), vec!["\"no_age\""]);
}
#[test]
fn where_is_not_null() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Item {name: "has_age", age: 5})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Item {name: "no_age"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Item) WHERE n.age IS NOT NULL RETURN n.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(col(&rows, "n.name"), vec!["\"has_age\""]);
}
#[test]
fn limit_restricts_row_count() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN n.name ORDER BY n.name ASC LIMIT 2"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(col(&rows, "n.name"), vec!["\"Alice\"", "\"Bob\""]);
}
#[test]
fn offset_skips_leading_rows() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN n.name ORDER BY n.name ASC OFFSET 1"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(col(&rows, "n.name"), vec!["\"Bob\"", "\"Carol\""]);
}
#[test]
fn limit_and_offset_together() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN n.name ORDER BY n.name ASC LIMIT 1 OFFSET 1"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(col(&rows, "n.name"), vec!["\"Bob\""]);
}
#[test]
fn exact_two_hop_path() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:N {name: "A"}), (b:N {name: "B"}), (c:N {name: "C"}),
(a)-[:R]->(b), (b)-[:R]->(c)"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (x)-[*2]->(y) RETURN x.name, y.name"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(col(&rows, "x.name"), vec!["\"A\""]);
assert_eq!(col(&rows, "y.name"), vec!["\"C\""]);
}
#[test]
fn one_to_two_hop_range() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:N {name: "A"}), (b:N {name: "B"}), (c:N {name: "C"}),
(a)-[:R]->(b), (b)-[:R]->(c)"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (x)-[*1..2]->(y) RETURN x.name, y.name"#).unwrap();
assert_eq!(rows.len(), 3);
}
#[test]
fn plus_quantifier_finds_reachable_nodes() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:N {name: "A"}), (b:N {name: "B"}), (c:N {name: "C"}),
(a)-[:R]->(b), (b)-[:R]->(c)"#,
)
.unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (x:N {name: "A"})-[:R+]->(y) RETURN y.name"#,
)
.unwrap();
assert_eq!(sorted_col(&rows, "y.name"), vec!["\"B\"", "\"C\""]);
}
#[test]
fn trail_mode_prevents_edge_revisit() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:N {name: "A"}), (b:N {name: "B"}),
(a)-[:R]->(b), (b)-[:R]->(a)"#,
)
.unwrap();
let walk = run(&mut g, &mut txn, r#"MATCH WALK (x)-[*1..4]->(y) RETURN x.name, y.name"#).unwrap();
let trail = run(&mut g, &mut txn, r#"MATCH TRAIL (x)-[*1..4]->(y) RETURN x.name, y.name"#).unwrap();
assert!(trail.len() <= walk.len());
assert_eq!(trail.len(), 4);
}
#[test]
fn simple_mode_prevents_node_revisit() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:N {name: "A"}), (b:N {name: "B"}),
(a)-[:R]->(b), (b)-[:R]->(a)"#,
)
.unwrap();
let simple = run(
&mut g, &mut txn,
r#"MATCH SIMPLE (x)-[*1..4]->(y) RETURN x.name, y.name"#,
)
.unwrap();
assert_eq!(simple.len(), 2);
}
#[test]
fn match_insert_connects_existing_nodes() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob"})"#).unwrap();
let before = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(before.len(), 2);
run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"}), (b:Person {name: "Bob"}) INSERT (a)-[:KNOWS]->(b)"#,
)
.unwrap();
let after = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(after.len(), 2, "MATCH+INSERT must not create duplicate nodes");
let edges = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b:Person) RETURN b.name"#,
)
.unwrap();
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].get("b.name"), Some(&Value::String("Bob".into())));
}
#[test]
fn match_insert_with_where_clause() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Item {name: "A", active: true})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Item {name: "B", active: false})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Hub {name: "Hub"})"#).unwrap();
run(
&mut g, &mut txn,
r#"MATCH (i:Item), (h:Hub) WHERE i.active = true INSERT (i)-[:CONNECTED_TO]->(h)"#,
)
.unwrap();
let connected = run(
&mut g, &mut txn,
r#"MATCH (i:Item)-[:CONNECTED_TO]->(h:Hub) RETURN i.name"#,
)
.unwrap();
assert_eq!(connected.len(), 1);
assert_eq!(connected[0].get("i.name"), Some(&Value::String("A".into())));
}
#[test]
fn match_insert_creates_one_edge_per_binding() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Src {name: "S"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Dst {name: "D1"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Dst {name: "D2"})"#).unwrap();
run(
&mut g, &mut txn,
r#"MATCH (s:Src), (d:Dst) INSERT (s)-[:LINK]->(d)"#,
)
.unwrap();
let links = run(
&mut g, &mut txn,
r#"MATCH (s:Src)-[:LINK]->(d:Dst) RETURN d.name"#,
)
.unwrap();
assert_eq!(links.len(), 2);
}
#[test]
fn match_insert_can_create_new_node_and_edge() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"}) INSERT (t:Tag {name: "rust"}), (a)-[:TAGGED]->(t)"#,
)
.unwrap();
let tags = run(
&mut g, &mut txn,
r#"MATCH (a:Person)-[:TAGGED]->(t:Tag) RETURN t.name"#,
)
.unwrap();
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].get("t.name"), Some(&Value::String("rust".into())));
let nodes = run(&mut g, &mut txn, r#"MATCH (n) RETURN n.name"#).unwrap();
assert_eq!(nodes.len(), 2);
}
#[test]
fn insert_edge_unbound_variable_returns_error() {
let mut g = Graph::new();
let mut txn = 0u64;
let result = run(&mut g, &mut txn, r#"INSERT (a)-[:REL]->(b)"#);
assert!(result.is_err(), "expected error for unbound INSERT edge variables");
let msg = format!("{:?}", result.unwrap_err());
assert!(
msg.contains("not bound") || msg.contains("not defined"),
"error should mention unbound variable, got: {msg}"
);
}
#[test]
fn insert_node_with_properties_is_idempotent() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 1, "duplicate INSERT with same properties must not create a second node");
}
#[test]
fn insert_different_labels_creates_separate_nodes() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Robot {name: "Alice"})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 2);
}
#[test]
fn insert_combined_node_and_edge_is_idempotent() {
let mut g = Graph::new();
let mut txn = 0u64;
for _ in 0..2 {
run(
&mut g, &mut txn,
r#"INSERT (a:Person {name: "Alice"}), (b:Person {name: "Bob"}), (a)-[:KNOWS]->(b)"#,
)
.unwrap();
}
let nodes = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(nodes.len(), 2, "no duplicate nodes");
let edges = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b:Person) RETURN b.name"#,
)
.unwrap();
assert_eq!(edges.len(), 1, "no duplicate edges");
}
#[test]
fn insert_bare_label_always_creates_new_node() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Anon)"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Anon)"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Anon) RETURN n"#).unwrap();
assert_eq!(rows.len(), 2, "bare label INSERT should always create a new node");
}
fn readme_graph() -> (Graph, u64) {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob", age: 25})"#).unwrap();
run(
&mut g, &mut txn,
r#"INSERT (:Person:Employee {name: "Carol", age: 35})"#,
)
.unwrap();
run(
&mut g, &mut txn,
r#"INSERT (a:Person {name: "Alice"}), (b:Person {name: "Bob"}), (a)-[:KNOWS]->(b)"#,
)
.unwrap();
(g, txn)
}
#[test]
fn readme_basic_match_returns_all_persons() {
let (mut g, mut txn) = readme_graph();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name, n.age"#).unwrap();
assert_eq!(rows.len(), 3);
let names: Vec<&Value> = rows.iter().map(|r| r.get("n.name").unwrap()).collect();
assert!(names.contains(&&Value::String("Alice".into())));
assert!(names.contains(&&Value::String("Bob".into())));
assert!(names.contains(&&Value::String("Carol".into())));
}
#[test]
fn readme_where_order_returns_filtered_sorted() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.age > 28 RETURN n.name ORDER BY n.age DESC"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].get("n.name"), Some(&Value::String("Carol".into())));
assert_eq!(rows[1].get("n.name"), Some(&Value::String("Alice".into())));
}
#[test]
fn readme_limit_offset() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN n.name LIMIT 2 OFFSET 1"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
}
#[test]
fn readme_distinct() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN DISTINCT n.name"#,
)
.unwrap();
assert_eq!(rows.len(), 3);
}
#[test]
fn readme_edge_query_directed() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("a.name"), Some(&Value::String("Alice".into())));
assert_eq!(rows[0].get("b.name"), Some(&Value::String("Bob".into())));
}
#[test]
fn readme_edge_query_with_type_function() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Person)-[r]->(b) RETURN a.name, type(r), b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("type(r)"), Some(&Value::String("KNOWS".into())));
}
#[test]
fn readme_edge_query_reverse() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a)<-[:KNOWS]-(b) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("a.name"), Some(&Value::String("Bob".into())));
assert_eq!(rows[0].get("b.name"), Some(&Value::String("Alice".into())));
}
#[test]
fn readme_edge_query_undirected() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a)-[:KNOWS]-(b) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
}
#[test]
fn readme_variable_length_path() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a)-[*1..3]->(b) RETURN a.name, b.name"#,
)
.unwrap();
assert!(!rows.is_empty());
let has_alice_bob = rows.iter().any(|r| {
r.get("a.name") == Some(&Value::String("Alice".into()))
&& r.get("b.name") == Some(&Value::String("Bob".into()))
});
assert!(has_alice_bob);
}
#[test]
fn readme_variable_length_labeled() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a)-[:KNOWS*]->(b) RETURN a.name, b.name"#,
)
.unwrap();
assert!(!rows.is_empty());
}
#[test]
fn readme_exact_hops() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a)-[*2]->(b) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 0);
}
#[test]
fn readme_set_property() {
let (mut g, mut txn) = readme_graph();
run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" SET n.age = 31"#,
)
.unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person {name: "Alice"}) RETURN n.age"#,
)
.unwrap();
assert_eq!(rows[0].get("n.age"), Some(&Value::Int(31)));
}
#[test]
fn readme_set_label() {
let (mut g, mut txn) = readme_graph();
run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" SET n:Manager"#,
)
.unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Manager) RETURN n.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("n.name"), Some(&Value::String("Alice".into())));
}
#[test]
fn readme_remove_property() {
let (mut g, mut txn) = readme_graph();
run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Bob" REMOVE n.age"#,
)
.unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person {name: "Bob"}) RETURN n.age"#,
)
.unwrap();
assert_eq!(rows[0].get("n.age"), Some(&Value::Null));
}
#[test]
fn readme_remove_label() {
let (mut g, mut txn) = readme_graph();
run(
&mut g, &mut txn,
r#"MATCH (n:Employee) WHERE n.name = "Carol" REMOVE n:Employee"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Employee) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 0, "Carol should no longer be an Employee");
}
#[test]
fn readme_delete_node() {
let (mut g, mut txn) = readme_graph();
run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Carol" DELETE n"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 2, "Carol should be deleted; Alice and Bob remain");
}
#[test]
fn readme_detach_delete() {
let (mut g, mut txn) = readme_graph();
run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" DETACH DELETE n"#,
)
.unwrap();
let nodes = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(nodes.len(), 2, "Bob and Carol remain");
let edges = run(&mut g, &mut txn, r#"MATCH (a)-[:KNOWS]->(b) RETURN a.name"#).unwrap();
assert_eq!(edges.len(), 0, "KNOWS edge removed with Alice");
}
#[test]
fn readme_match_insert_connects_existing() {
let (mut g, mut txn) = readme_graph();
run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"}), (b:Person {name: "Bob"}) INSERT (a)-[:KNOWS]->(b)"#,
)
.unwrap();
let edges = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b) RETURN b.name"#,
)
.unwrap();
assert_eq!(edges.len(), 1, "edge must not be duplicated");
}
#[test]
fn readme_is_null_expression() {
let (mut g, mut txn) = readme_graph();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Dave"})"#).unwrap();
let null_rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.age IS NULL RETURN n.name"#,
)
.unwrap();
assert_eq!(null_rows.len(), 1);
assert_eq!(null_rows[0].get("n.name"), Some(&Value::String("Dave".into())));
let not_null_rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.age IS NOT NULL RETURN n.name"#,
)
.unwrap();
assert_eq!(not_null_rows.len(), 3); }
#[test]
fn readme_arithmetic_expression() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person {name: "Alice"}) RETURN n.age * 2 + 1 AS adjusted"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("adjusted"), Some(&Value::Int(61)));
}
#[test]
fn multi_pattern_match_cross_joins() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Color {name: "red"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Color {name: "blue"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Size {name: "small"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Size {name: "large"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (c:Color), (s:Size) RETURN c.name, s.name"#,
)
.unwrap();
assert_eq!(rows.len(), 4);
let pairs: Vec<(String, String)> = rows
.iter()
.map(|r| {
let c = match r.get("c.name").unwrap() { Value::String(s) => s.clone(), _ => panic!() };
let s = match r.get("s.name").unwrap() { Value::String(s) => s.clone(), _ => panic!() };
(c, s)
})
.collect();
assert!(pairs.contains(&("red".into(), "small".into())));
assert!(pairs.contains(&("red".into(), "large".into())));
assert!(pairs.contains(&("blue".into(), "small".into())));
assert!(pairs.contains(&("blue".into(), "large".into())));
}
#[test]
fn multi_pattern_match_with_where() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob", age: 25})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Carol", age: 35})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Person), (b:Person) WHERE a.age > b.age RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 3);
}
#[test]
fn multi_pattern_self_join_excludes_same_node() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:T {name: "X"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:T {name: "Y"})"#).unwrap();
let all = run(&mut g, &mut txn, r#"MATCH (a:T), (b:T) RETURN a.name, b.name"#).unwrap();
assert_eq!(all.len(), 4);
let diff = run(
&mut g, &mut txn,
r#"MATCH (a:T), (b:T) WHERE a.name <> b.name RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(diff.len(), 2);
}
#[test]
fn multi_pattern_mixes_path_and_node() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:Person {name: "Alice"}), (b:Person {name: "Bob"}), (a)-[:KNOWS]->(b)"#,
)
.unwrap();
run(&mut g, &mut txn, r#"INSERT (:Tag {name: "rust"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Person)-[:KNOWS]->(b:Person), (t:Tag) RETURN a.name, b.name, t.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("t.name"), Some(&Value::String("rust".into())));
}
#[test]
fn multi_pattern_three_patterns() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:A {name: "a1"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:A {name: "a2"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:B {name: "b1"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:C {name: "c1"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:A), (b:B), (c:C) RETURN a.name, b.name, c.name"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
}
#[test]
fn single_pattern_match_unchanged() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Item {name: "x"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Item {name: "y"})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Item) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 2);
}
#[test]
fn multi_pattern_match_order_by() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:N {name: "B"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:N {name: "A"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:M {val: 1})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:M {val: 2})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:N), (m:M) RETURN n.name, m.val ORDER BY n.name ASC, m.val DESC"#,
)
.unwrap();
assert_eq!(rows.len(), 4);
assert_eq!(rows[0].get("n.name"), Some(&Value::String("A".into())));
assert_eq!(rows[0].get("m.val"), Some(&Value::Int(2)));
assert_eq!(rows[3].get("n.name"), Some(&Value::String("B".into())));
assert_eq!(rows[3].get("m.val"), Some(&Value::Int(1)));
}
fn three_numbers() -> (Graph, u64) {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Num {val: 10})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Num {val: 20})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Num {val: 30})"#).unwrap();
(g, txn)
}
#[test]
fn count_star_returns_total() {
let (mut g, mut txn) = three_numbers();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Num) RETURN count(*)"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("count(*)"), Some(&Value::Int(3)));
}
#[test]
fn count_star_empty_returns_zero() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, r#"MATCH (n:Num) RETURN count(*)"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("count(*)"), Some(&Value::Int(0)));
}
#[test]
fn count_expr_skips_nulls() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:X {v: 1})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:X {v: 2})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:X)"#).unwrap(); let rows = run(&mut g, &mut txn, r#"MATCH (n:X) RETURN count(n.v)"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("count(n.v)"), Some(&Value::Int(2)));
}
#[test]
fn sum_integers() {
let (mut g, mut txn) = three_numbers();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Num) RETURN sum(n.val)"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("sum(n.val)"), Some(&Value::Int(60)));
}
#[test]
fn avg_integers() {
let (mut g, mut txn) = three_numbers();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Num) RETURN avg(n.val)"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("avg(n.val)"), Some(&Value::Float(20.0)));
}
#[test]
fn min_max_integers() {
let (mut g, mut txn) = three_numbers();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Num) RETURN min(n.val), max(n.val)"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("min(n.val)"), Some(&Value::Int(10)));
assert_eq!(rows[0].get("max(n.val)"), Some(&Value::Int(30)));
}
#[test]
fn collect_returns_list() {
let (mut g, mut txn) = three_numbers();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Num) RETURN collect(n.val)"#).unwrap();
assert_eq!(rows.len(), 1);
let list = match rows[0].get("collect(n.val)").unwrap() {
Value::List(l) => l.clone(),
other => panic!("expected List, got {other:?}"),
};
assert_eq!(list.len(), 3);
assert!(list.contains(&Value::Int(10)));
assert!(list.contains(&Value::Int(20)));
assert!(list.contains(&Value::Int(30)));
}
#[test]
fn count_grouped_by_label() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {dept: "eng", name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {dept: "eng", name: "Bob"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {dept: "hr", name: "Carol"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN n.dept, count(*) ORDER BY n.dept ASC"#,
)
.unwrap();
assert_eq!(rows.len(), 2, "two groups: eng and hr");
assert_eq!(rows[0].get("n.dept"), Some(&Value::String("eng".into())));
assert_eq!(rows[0].get("count(*)"), Some(&Value::Int(2)));
assert_eq!(rows[1].get("n.dept"), Some(&Value::String("hr".into())));
assert_eq!(rows[1].get("count(*)"), Some(&Value::Int(1)));
}
#[test]
fn sum_grouped_by_category() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Item {cat: "A", price: 10})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Item {cat: "A", price: 20})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Item {cat: "B", price: 5})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Item) RETURN n.cat, sum(n.price) ORDER BY n.cat ASC"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].get("sum(n.price)"), Some(&Value::Int(30)));
assert_eq!(rows[1].get("sum(n.price)"), Some(&Value::Int(5)));
}
#[test]
fn count_star_with_where() {
let (mut g, mut txn) = three_numbers();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Num) WHERE n.val > 10 RETURN count(*)"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("count(*)"), Some(&Value::Int(2)));
}
#[test]
fn order_by_aggregate_column() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:P {dept: "eng", name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {dept: "eng", name: "Bob"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {dept: "hr", name: "Carol"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:P) RETURN n.dept, count(*) ORDER BY count(*) DESC"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].get("n.dept"), Some(&Value::String("eng".into())));
assert_eq!(rows[0].get("count(*)"), Some(&Value::Int(2)));
}
#[test]
fn count_star_with_alias() {
let (mut g, mut txn) = three_numbers();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Num) RETURN count(*) AS total"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("total"), Some(&Value::Int(3)));
}
fn five_node_chain() -> (Graph, u64) {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:Stop {name: "A"}), (b:Stop {name: "B"}), (c:Stop {name: "C"}),
(d:Stop {name: "D"}), (e:Stop {name: "E"}),
(a)-[:NEXT]->(b), (b)-[:NEXT]->(c), (c)-[:NEXT]->(d), (d)-[:NEXT]->(e)"#,
)
.unwrap();
(g, txn)
}
#[test]
fn four_hop_exact_match() {
let (mut g, mut txn) = five_node_chain();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Stop)-[*4]->(b:Stop) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("a.name"), Some(&Value::String("A".into())));
assert_eq!(rows[0].get("b.name"), Some(&Value::String("E".into())));
}
#[test]
fn three_hop_exact_match() {
let (mut g, mut txn) = five_node_chain();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Stop)-[*3]->(b:Stop) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
let pairs: Vec<(String, String)> = rows
.iter()
.map(|r| {
let a = r.get("a.name").and_then(|v| if let Value::String(s) = v { Some(s.clone()) } else { None }).unwrap_or_default();
let b = r.get("b.name").and_then(|v| if let Value::String(s) = v { Some(s.clone()) } else { None }).unwrap_or_default();
(a, b)
})
.collect();
assert!(pairs.contains(&("A".into(), "D".into())));
assert!(pairs.contains(&("B".into(), "E".into())));
}
#[test]
fn one_to_four_hop_range() {
let (mut g, mut txn) = five_node_chain();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Stop)-[*1..4]->(b:Stop) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 10);
}
#[test]
fn two_or_more_hops() {
let (mut g, mut txn) = five_node_chain();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Stop)-[*2..]->(b:Stop) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 6);
let has_ab = rows.iter().any(|r| {
r.get("a.name") == Some(&Value::String("A".into()))
&& r.get("b.name") == Some(&Value::String("B".into()))
});
assert!(!has_ab, "A→B is only 1 hop and must not appear in *2.. results");
}
#[test]
fn path_variable_length_four_hops() {
let (mut g, mut txn) = five_node_chain();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Stop)-[r*4]->(b:Stop) RETURN a.name, b.name, length(r)"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("length(r)"), Some(&Value::Int(4)));
}
#[test]
fn plus_quantifier_with_label_filter() {
let (mut g, mut txn) = five_node_chain();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Stop)-[:NEXT+]->(b:Stop) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 10);
let ae = rows.iter().any(|r| {
r.get("a.name") == Some(&Value::String("A".into()))
&& r.get("b.name") == Some(&Value::String("E".into()))
});
assert!(ae, "A must reach E via NEXT+");
}
#[test]
fn readme_brace_quantifier_with_length() {
let (mut g, mut txn) = five_node_chain();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Stop)-[r:NEXT{1,2}]->(b:Stop) RETURN a.name, length(r), b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 7);
for row in &rows {
let len = row.get("length(r)").unwrap();
assert!(
*len == Value::Int(1) || *len == Value::Int(2),
"length must be 1 or 2, got {len:?}"
);
}
}
#[test]
fn repeated_insert_same_properties_no_duplicate() {
let mut g = Graph::new();
let mut txn = 0u64;
for _ in 0..3 {
run(&mut g, &mut txn, r#"INSERT (:User {name: "alice", role: "admin"})"#).unwrap();
}
let rows = run(&mut g, &mut txn, r#"MATCH (n:User) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 1, "three identical INSERTs must yield exactly one node");
}
#[test]
fn inserts_with_distinct_properties_create_separate_nodes() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:User {name: "alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:User {name: "bob"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:User {name: "carol"})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:User) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 3, "three users with distinct names must be separate nodes");
}
#[test]
fn repeated_edge_insert_no_duplicate() {
let mut g = Graph::new();
let mut txn = 0u64;
for _ in 0..3 {
run(
&mut g, &mut txn,
r#"INSERT (a:W {name: "X"}), (b:W {name: "Y"}), (a)-[:LINK]->(b)"#,
)
.unwrap();
}
let rows = run(&mut g, &mut txn, r#"MATCH (a)-[:LINK]->(b) RETURN a.name, b.name"#).unwrap();
assert_eq!(rows.len(), 1, "three identical edge INSERTs must yield exactly one edge");
let nodes = run(&mut g, &mut txn, r#"MATCH (n:W) RETURN n.name"#).unwrap();
assert_eq!(nodes.len(), 2, "X and Y must not be duplicated");
}
#[test]
fn repeated_match_insert_edge_no_duplicate() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:P {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {name: "Bob"})"#).unwrap();
for _ in 0..3 {
run(
&mut g, &mut txn,
r#"MATCH (a:P {name: "Alice"}), (b:P {name: "Bob"}) INSERT (a)-[:FRIENDS]->(b)"#,
)
.unwrap();
}
let edges = run(&mut g, &mut txn, r#"MATCH (a)-[:FRIENDS]->(b) RETURN a.name"#).unwrap();
assert_eq!(edges.len(), 1, "running MATCH+INSERT three times must not duplicate the edge");
}
#[test]
fn chain_built_from_separate_inserts_no_node_duplication() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Chain {id: 1, name: "n1"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Chain {id: 2, name: "n2"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Chain {id: 3, name: "n3"})"#).unwrap();
run(
&mut g, &mut txn,
r#"MATCH (a:Chain {name: "n1"}), (b:Chain {name: "n2"}) INSERT (a)-[:NEXT]->(b)"#,
)
.unwrap();
run(
&mut g, &mut txn,
r#"MATCH (a:Chain {name: "n2"}), (b:Chain {name: "n3"}) INSERT (a)-[:NEXT]->(b)"#,
)
.unwrap();
run(
&mut g, &mut txn,
r#"MATCH (a:Chain {name: "n1"}), (b:Chain {name: "n2"}) INSERT (a)-[:NEXT]->(b)"#,
)
.unwrap();
let nodes = run(&mut g, &mut txn, r#"MATCH (n:Chain) RETURN n.name"#).unwrap();
assert_eq!(nodes.len(), 3, "re-running MATCH+INSERT must not duplicate nodes");
let edges = run(&mut g, &mut txn, r#"MATCH (a)-[:NEXT]->(b) RETURN a.name"#).unwrap();
assert_eq!(edges.len(), 2, "re-running MATCH+INSERT must not duplicate edges");
}
#[test]
fn different_edge_labels_create_separate_edges() {
let mut g = Graph::new();
let mut txn = 0u64;
run(
&mut g, &mut txn,
r#"INSERT (a:V {name: "X"}), (b:V {name: "Y"}),
(a)-[:KNOWS]->(b), (a)-[:LIKES]->(b)"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (a)-[r]->(b) RETURN type(r)"#).unwrap();
assert_eq!(rows.len(), 2, "KNOWS and LIKES are two distinct edges");
let types: Vec<&Value> = rows.iter().map(|r| r.get("type(r)").unwrap()).collect();
assert!(types.contains(&&Value::String("KNOWS".into())));
assert!(types.contains(&&Value::String("LIKES".into())));
}
#[test]
fn readme_match_insert_where_boolean_property() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Item {name: "widget", active: true})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Item {name: "gizmo", active: false})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Hub {name: "hub1"})"#).unwrap();
run(
&mut g, &mut txn,
r#"MATCH (i:Item), (h:Hub) WHERE i.active = true INSERT (i)-[:CONNECTED_TO]->(h)"#,
)
.unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (i)-[:CONNECTED_TO]->(h) RETURN i.name"#).unwrap();
assert_eq!(rows.len(), 1, "only the active item connects to the hub");
assert_eq!(rows[0].get("i.name"), Some(&Value::String("widget".into())));
}
#[test]
fn multi_label_node_matches_both_labels() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person:Employee {name: "Carol", age: 35})"#).unwrap();
let as_person = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(as_person.len(), 1, "Carol is a Person");
let as_emp = run(&mut g, &mut txn, r#"MATCH (n:Employee) RETURN n.name"#).unwrap();
assert_eq!(as_emp.len(), 1, "Carol is an Employee");
let label_rows = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN labels(n)"#).unwrap();
assert_eq!(label_rows.len(), 1);
if let Some(Value::List(ls)) = label_rows[0].get("labels(n)") {
assert!(ls.contains(&Value::String("Person".into())));
assert!(ls.contains(&Value::String("Employee".into())));
} else {
panic!("labels(n) should return a list");
}
}
#[test]
fn multi_label_insert_is_idempotent() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person:Employee {name: "Carol", age: 35})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person:Employee {name: "Carol", age: 35})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Employee) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 1, "re-inserting the same multi-label node must not duplicate it");
}
#[test]
fn readme_where_and_with_neq() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.age > 28 AND n.name <> "Bob" RETURN n.name ORDER BY n.age ASC"#,
)
.unwrap();
assert_eq!(rows.len(), 2, "Alice(30) and Carol(35) pass; Bob(25) excluded by age");
assert_eq!(rows[0].get("n.name"), Some(&Value::String("Alice".into())));
assert_eq!(rows[1].get("n.name"), Some(&Value::String("Carol".into())));
}
#[test]
fn readme_order_by_multi_column() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:P {name: "Alice", age: 30})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {name: "Aaron", age: 30})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {name: "Bob", age: 25})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:P) RETURN n.name, n.age ORDER BY n.age DESC, n.name ASC"#,
)
.unwrap();
assert_eq!(rows.len(), 3);
assert_eq!(rows[0].get("n.name"), Some(&Value::String("Aaron".into())));
assert_eq!(rows[1].get("n.name"), Some(&Value::String("Alice".into())));
assert_eq!(rows[2].get("n.name"), Some(&Value::String("Bob".into())));
}
#[test]
fn size_of_labels() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:A:B:C {name: "triple"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:A) RETURN size(labels(n)) AS lcount"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("lcount"), Some(&Value::Int(3)));
}
#[test]
fn to_string_function() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person {name: "Alice"}) RETURN toString(n.age) AS s"#,
)
.unwrap();
assert_eq!(rows[0].get("s"), Some(&Value::String("30".into())));
}
#[test]
fn to_integer_function() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:X {code: "42"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:X) RETURN toInteger(n.code) AS i"#,
)
.unwrap();
assert_eq!(rows[0].get("i"), Some(&Value::Int(42)));
}
#[test]
fn where_not_condition() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE NOT n.name = "Alice" RETURN n.name"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
let names = sorted_col(&rows, "n.name");
assert!(!names.contains(&"\"Alice\"".to_string()));
}
#[test]
fn where_or_condition() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WHERE n.name = "Alice" OR n.name = "Bob" RETURN n.name"#,
)
.unwrap();
assert_eq!(rows.len(), 2);
let names = sorted_col(&rows, "n.name");
assert!(names.contains(&"\"Alice\"".to_string()));
assert!(names.contains(&"\"Bob\"".to_string()));
}
#[test]
fn readme_agg_min_max_ages() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN min(n.age), max(n.age)"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("min(n.age)"), Some(&Value::Int(25)));
assert_eq!(rows[0].get("max(n.age)"), Some(&Value::Int(35)));
}
#[test]
fn readme_agg_collect_names() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN collect(n.name)"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
if let Some(Value::List(names)) = rows[0].get("collect(n.name)") {
assert_eq!(names.len(), 3);
assert!(names.contains(&Value::String("Alice".into())));
assert!(names.contains(&Value::String("Bob".into())));
assert!(names.contains(&Value::String("Carol".into())));
} else {
panic!("collect(n.name) should return a list");
}
}
#[test]
fn readme_agg_count_star() {
let (mut g, mut txn) = readme_graph();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN count(*)"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("count(*)"), Some(&Value::Int(3)));
}
#[test]
fn readme_match_insert_new_node_and_edge() {
let (mut g, mut txn) = readme_graph();
run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"}) INSERT (t:Tag {name: "rust"}), (a)-[:TAGGED]->(t)"#,
)
.unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"})-[:TAGGED]->(t:Tag) RETURN t.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("t.name"), Some(&Value::String("rust".into())));
run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"}) INSERT (t:Tag {name: "rust"}), (a)-[:TAGGED]->(t)"#,
)
.unwrap();
let tags = run(&mut g, &mut txn, r#"MATCH (n:Tag) RETURN n.name"#).unwrap();
assert_eq!(tags.len(), 1, "tag must not be duplicated on second run");
}
#[test]
fn simple_mode_on_long_chain() {
let (mut g, mut txn) = five_node_chain();
let rows = run(
&mut g, &mut txn,
r#"MATCH SIMPLE (a:Stop)-[*1..10]->(b:Stop) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 10);
}
#[test]
fn trail_mode_on_long_chain() {
let (mut g, mut txn) = five_node_chain();
let rows = run(
&mut g, &mut txn,
r#"MATCH TRAIL (a:Stop)-[*1..10]->(b:Stop) RETURN a.name, b.name"#,
)
.unwrap();
assert_eq!(rows.len(), 10);
}
#[test]
fn id_function_stable() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Node {name: "stable"})"#).unwrap();
let rows1 = run(&mut g, &mut txn, r#"MATCH (n:Node) RETURN id(n) AS nid"#).unwrap();
let rows2 = run(&mut g, &mut txn, r#"MATCH (n:Node) RETURN id(n) AS nid"#).unwrap();
assert_eq!(rows1[0].get("nid"), rows2[0].get("nid"), "id must be stable across queries");
}
#[test]
fn id_is_ulid_format() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:X {v: 1})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:X) RETURN id(n) AS nid"#).unwrap();
assert_eq!(rows.len(), 1);
let id_val = match rows[0].get("nid").unwrap() {
Value::String(s) => s.clone(),
other => panic!("expected String, got {other:?}"),
};
assert_eq!(id_val.len(), 26, "ULID must be 26 characters");
let valid_chars = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
for ch in id_val.chars() {
assert!(valid_chars.contains(ch), "invalid Crockford Base32 char: {ch}");
}
}
#[test]
fn ids_are_unique_per_node() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:X {v: 1})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:X {v: 2})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:X {v: 3})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:X) RETURN id(n) AS nid"#).unwrap();
assert_eq!(rows.len(), 3);
let ids: std::collections::HashSet<String> = rows
.iter()
.map(|r| match r.get("nid").unwrap() {
Value::String(s) => s.clone(),
_ => panic!("expected String"),
})
.collect();
assert_eq!(ids.len(), 3, "all three node IDs must be distinct");
}
#[test]
fn ulid_ids_are_lexicographically_ordered() {
use crate::types::ulid_new;
let a = crate::types::ulid_encode(ulid_new());
let b = crate::types::ulid_encode(ulid_new());
let c = crate::types::ulid_encode(ulid_new());
assert!(a < b, "ULIDs must be monotonically increasing: {a} < {b}");
assert!(b < c, "ULIDs must be monotonically increasing: {b} < {c}");
}
#[test]
fn edge_id_is_ulid_format() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:A {n: 1})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:B {n: 2})"#).unwrap();
run(
&mut g, &mut txn,
r#"MATCH (a:A), (b:B) INSERT (a)-[:LINK]->(b)"#,
)
.unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (a:A)-[r:LINK]->(b:B) RETURN id(r) AS eid"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
let eid = match rows[0].get("eid").unwrap() {
Value::String(s) => s.clone(),
other => panic!("expected String, got {other:?}"),
};
assert_eq!(eid.len(), 26, "edge ULID must be 26 characters");
}
#[test]
fn upsert_preserves_ulid() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:P {name: "Alice"})"#).unwrap();
let rows1 = run(
&mut g, &mut txn,
r#"MATCH (n:P {name: "Alice"}) RETURN id(n) AS nid"#,
)
.unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {name: "Alice"})"#).unwrap();
let rows2 = run(
&mut g, &mut txn,
r#"MATCH (n:P {name: "Alice"}) RETURN id(n) AS nid"#,
)
.unwrap();
assert_eq!(rows1.len(), 1);
assert_eq!(rows2.len(), 1);
assert_eq!(
rows1[0].get("nid"),
rows2[0].get("nid"),
"upsert must preserve the original ULID"
);
}
#[test]
fn arithmetic_div_and_mod() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:X {v: 17})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:X) RETURN n.v / 5 AS q, n.v % 5 AS r"#,
)
.unwrap();
assert_eq!(rows[0].get("q"), Some(&Value::Int(3)));
assert_eq!(rows[0].get("r"), Some(&Value::Int(2)));
}
#[test]
fn limit_zero_returns_no_rows() {
let (mut g, mut txn) = readme_graph();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name LIMIT 0"#).unwrap();
assert_eq!(rows.len(), 0);
}
#[test]
fn offset_beyond_end_returns_empty() {
let (mut g, mut txn) = readme_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) RETURN n.name OFFSET 100"#,
)
.unwrap();
assert_eq!(rows.len(), 0);
}
fn inserted_count(rows: &[Row]) -> u64 {
let msg = match rows[0].get("result") {
Some(Value::String(s)) => s.clone(),
_ => panic!("expected summary row"),
};
msg.split_whitespace()
.nth(1)
.unwrap()
.parse::<u64>()
.expect("count must be a number")
}
#[test]
fn readme_sequence_element_counts() {
let mut g = Graph::new();
let mut txn = 0u64;
let r0 = run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
assert_eq!(inserted_count(&r0), 1, "txn 0: Alice");
let r1 = run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob", age: 25})"#).unwrap();
assert_eq!(inserted_count(&r1), 1, "txn 1: Bob");
let r2 = run(
&mut g, &mut txn,
r#"INSERT (:Person:Employee {name: "Carol", age: 35})"#,
)
.unwrap();
assert_eq!(inserted_count(&r2), 1, "txn 2: Carol");
let r3 = run(
&mut g, &mut txn,
r#"INSERT (a:Person {name: "Alice"}), (b:Person {name: "Bob"}), (a)-[:KNOWS]->(b)"#,
)
.unwrap();
assert_eq!(inserted_count(&r3), 1, "txn 3: KNOWS edge only");
let r3b = run(
&mut g, &mut txn,
r#"INSERT (a:Person {name: "Alice"}), (b:Person {name: "Bob"}), (a)-[:KNOWS]->(b)"#,
)
.unwrap();
assert_eq!(inserted_count(&r3b), 0, "txn 3 repeat: already exists");
let r4 = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"}), (b:Person:Employee {name: "Carol"}) INSERT (a)-[:MANAGES]->(b)"#,
)
.unwrap();
assert_eq!(inserted_count(&r4), 1, "txn 4: MANAGES edge");
let r4b = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"}), (b:Person:Employee {name: "Carol"}) INSERT (a)-[:MANAGES]->(b)"#,
)
.unwrap();
assert_eq!(inserted_count(&r4b), 0, "txn 4 repeat: already exists");
let persons =
run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(persons.len(), 3, "exactly 3 Person nodes");
let knows = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"})-[:KNOWS]->(b) RETURN b.name"#,
)
.unwrap();
assert_eq!(knows.len(), 1);
assert_eq!(knows[0].get("b.name"), Some(&Value::String("Bob".into())));
let manages = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"})-[:MANAGES]->(b) RETURN b.name"#,
)
.unwrap();
assert_eq!(manages.len(), 1);
assert_eq!(manages[0].get("b.name"), Some(&Value::String("Carol".into())));
}
#[test]
fn readme_match_insert_manages_edge() {
let (mut g, mut txn) = readme_graph();
let r = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"}), (b:Person:Employee {name: "Carol"}) INSERT (a)-[:MANAGES]->(b)"#,
)
.unwrap();
assert_eq!(inserted_count(&r), 1, "first run inserts the MANAGES edge");
let r2 = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"}), (b:Person:Employee {name: "Carol"}) INSERT (a)-[:MANAGES]->(b)"#,
)
.unwrap();
assert_eq!(inserted_count(&r2), 0, "second run is a no-op");
let edges = run(
&mut g, &mut txn,
r#"MATCH (a:Person {name: "Alice"})-[:MANAGES]->(b) RETURN b.name"#,
)
.unwrap();
assert_eq!(edges.len(), 1, "exactly one MANAGES edge");
assert_eq!(edges[0].get("b.name"), Some(&Value::String("Carol".into())));
}
#[test]
fn create_index_returns_summary() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "CREATE INDEX ON :Person(name)").unwrap();
assert_eq!(rows.len(), 1);
let msg = format!("{}", rows[0].get("result").unwrap());
assert!(msg.contains("Index created"), "got: {msg}");
}
#[test]
fn create_index_idempotent() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE INDEX ON :Person(name)").unwrap();
let rows = run(&mut g, &mut txn, "CREATE INDEX ON :Person(name)").unwrap();
let msg = format!("{}", rows[0].get("result").unwrap());
assert!(msg.contains("already exists"), "got: {msg}");
}
#[test]
fn show_indexes_empty_then_populated() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "SHOW INDEXES").unwrap();
assert_eq!(rows.len(), 1);
let msg = format!("{}", rows[0].get("result").unwrap());
assert!(msg.contains("No indexes"), "got: {msg}");
run(&mut g, &mut txn, "CREATE INDEX ON :Person(name)").unwrap();
run(&mut g, &mut txn, "CREATE INDEX ON :Person(age)").unwrap();
let rows = run(&mut g, &mut txn, "SHOW INDEXES").unwrap();
assert_eq!(rows.len(), 2);
let mut labels: Vec<String> = rows
.iter()
.map(|r| format!("{}", r.get("label").unwrap()))
.collect();
labels.sort();
assert_eq!(labels, vec!["\"Person\"", "\"Person\""]);
}
#[test]
fn drop_index_removes_index() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE INDEX ON :Person(name)").unwrap();
let rows = run(&mut g, &mut txn, "DROP INDEX ON :Person(name)").unwrap();
let msg = format!("{}", rows[0].get("result").unwrap());
assert!(msg.contains("dropped"), "got: {msg}");
let rows = run(&mut g, &mut txn, "SHOW INDEXES").unwrap();
let msg = format!("{}", rows[0].get("result").unwrap());
assert!(msg.contains("No indexes"), "got: {msg}");
}
#[test]
fn drop_nonexistent_index_returns_no_index_message() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "DROP INDEX ON :Person(name)").unwrap();
let msg = format!("{}", rows[0].get("result").unwrap());
assert!(msg.contains("No index"), "got: {msg}");
}
#[test]
fn index_used_for_exact_lookup() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE INDEX ON :Person(name)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob", age: 25})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Carol", age: 35})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person {name: "Alice"}) RETURN n.age"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("n.age"), Some(&Value::Int(30)));
}
#[test]
fn index_maintained_after_set_property() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE INDEX ON :Person(name)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (n:Person {name: "Alice"}) SET n.name = "Alicia""#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person {name: "Alicia"}) RETURN n.name"#,
)
.unwrap();
assert_eq!(rows.len(), 1);
let rows2 = run(
&mut g, &mut txn,
r#"MATCH (n:Person {name: "Alice"}) RETURN n.name"#,
)
.unwrap();
assert_eq!(rows2.len(), 0);
}
#[test]
fn index_entry_count_in_show_indexes() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE INDEX ON :Person(name)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob"})"#).unwrap();
let rows = run(&mut g, &mut txn, "SHOW INDEXES").unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("entries"), Some(&Value::Int(2)));
}
#[test]
fn execute_capturing_returns_ops() {
let mut g = Graph::new();
let mut txn = 0u64;
let stmt = super::super::parser::parse(r#"INSERT (:Person {name: "Alice"})"#).unwrap();
let (rows, ops) = execute_capturing(stmt, &mut g, &mut txn).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(ops.len(), 1, "one CreateNode op");
let found = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(found.len(), 1);
}
#[test]
fn execute_capturing_read_returns_empty_ops() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
let stmt =
super::super::parser::parse(r#"MATCH (n:Person) RETURN n.name"#).unwrap();
let (_rows, ops) = execute_capturing(stmt, &mut g, &mut txn).unwrap();
assert!(ops.is_empty(), "MATCH produces no ops");
}
#[test]
fn rollback_restores_graph() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
let snapshot = g.clone();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Carol"})"#).unwrap();
g = snapshot;
let rows = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(col(&rows, "n.name"), vec!["\"Alice\""]);
}
fn make_age_graph() -> (Graph, u64) {
let dir = tempfile::tempdir().unwrap().into_path();
let mut g = Graph::open(&dir).unwrap();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE INDEX ON :Person(age)").unwrap();
for (name, age) in &[("Alice", 30i64), ("Bob", 25), ("Carol", 35), ("Dave", 20)] {
run(
&mut g,
&mut txn,
&format!(r#"INSERT (:Person {{name: "{name}", age: {age}}})"#),
)
.unwrap();
}
(g, txn)
}
#[test]
fn range_gt_exclusive() {
let (mut g, mut txn) = make_age_graph();
let rows =
run(&mut g, &mut txn, "MATCH (n:Person) WHERE n.age > 25 RETURN n.name").unwrap();
let mut names: Vec<String> = col(&rows, "n.name");
names.sort();
assert_eq!(names, vec![r#""Alice""#, r#""Carol""#]);
}
#[test]
fn range_gte_inclusive() {
let (mut g, mut txn) = make_age_graph();
let rows =
run(&mut g, &mut txn, "MATCH (n:Person) WHERE n.age >= 25 RETURN n.name").unwrap();
let mut names: Vec<String> = col(&rows, "n.name");
names.sort();
assert_eq!(names, vec![r#""Alice""#, r#""Bob""#, r#""Carol""#]);
}
#[test]
fn range_lt_exclusive() {
let (mut g, mut txn) = make_age_graph();
let rows =
run(&mut g, &mut txn, "MATCH (n:Person) WHERE n.age < 30 RETURN n.name").unwrap();
let mut names: Vec<String> = col(&rows, "n.name");
names.sort();
assert_eq!(names, vec![r#""Bob""#, r#""Dave""#]);
}
#[test]
fn range_closed_interval() {
let (mut g, mut txn) = make_age_graph();
let rows = run(
&mut g,
&mut txn,
"MATCH (n:Person) WHERE n.age >= 25 AND n.age <= 30 RETURN n.name",
)
.unwrap();
let mut names: Vec<String> = col(&rows, "n.name");
names.sort();
assert_eq!(names, vec![r#""Alice""#, r#""Bob""#]);
}
#[test]
fn range_no_index_falls_back_correctly() {
let dir = tempfile::tempdir().unwrap().into_path();
let mut g = Graph::open(&dir).unwrap();
let mut txn = 0u64;
for (name, age) in &[("Alice", 30i64), ("Bob", 25), ("Carol", 35)] {
run(
&mut g,
&mut txn,
&format!(r#"INSERT (:Person {{name: "{name}", age2: {age}}})"#),
)
.unwrap();
}
let rows =
run(&mut g, &mut txn, "MATCH (n:Person) WHERE n.age2 > 25 RETURN n.name").unwrap();
let mut names: Vec<String> = col(&rows, "n.name");
names.sort();
assert_eq!(names, vec![r#""Alice""#, r#""Carol""#]);
}
#[test]
fn range_negative_integers() {
let dir = tempfile::tempdir().unwrap().into_path();
let mut g = Graph::open(&dir).unwrap();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE INDEX ON :Temp(val)").unwrap();
for v in &[-100i64, -10, 0, 10, 100] {
run(&mut g, &mut txn, &format!("INSERT (:Temp {{val: {v}}})")).unwrap();
}
let rows =
run(&mut g, &mut txn, "MATCH (n:Temp) WHERE n.val >= -10 AND n.val < 10 RETURN n.val")
.unwrap();
let mut vals: Vec<String> = col(&rows, "n.val");
vals.sort();
assert_eq!(vals, vec!["-10", "0"]);
}
#[test]
fn unwind_basic() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "UNWIND [1, 2, 3] AS x RETURN x").unwrap();
assert_eq!(rows.len(), 3);
assert_eq!(col(&rows, "x"), vec!["1", "2", "3"]);
}
#[test]
fn unwind_string_list() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(
&mut g, &mut txn,
r#"UNWIND ["Alice", "Bob"] AS name RETURN name"#,
).unwrap();
assert_eq!(rows.len(), 2);
let mut names = col(&rows, "name");
names.sort();
assert_eq!(names, vec!["\"Alice\"", "\"Bob\""]);
}
#[test]
fn unwind_empty_list() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "UNWIND [] AS x RETURN x").unwrap();
assert_eq!(rows.len(), 0);
}
#[test]
fn optional_match_found() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"OPTIONAL MATCH (n:Person {name: "Alice"}) RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(col(&rows, "n.name"), vec!["\"Alice\""]);
}
#[test]
fn optional_match_not_found_returns_null_row() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(
&mut g, &mut txn,
r#"OPTIONAL MATCH (n:Person) RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("n.name"), Some(&Value::Null));
}
#[test]
fn union_combines_results() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person {name: "Alice"}) RETURN n.name
UNION
MATCH (n:Person {name: "Bob"}) RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 2);
let mut names = col(&rows, "n.name");
names.sort();
assert_eq!(names, vec!["\"Alice\"", "\"Bob\""]);
}
#[test]
fn union_all_keeps_duplicates() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person {name: "Alice"}) RETURN n.name
UNION ALL
MATCH (n:Person {name: "Alice"}) RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 2, "UNION ALL keeps both copies");
}
#[test]
fn union_deduplicates_identical_rows() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person {name: "Alice"}) RETURN n.name
UNION
MATCH (n:Person {name: "Alice"}) RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 1, "UNION removes duplicates");
}
#[test]
fn match_with_return() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WITH n.name AS name WHERE name <> "Bob" RETURN name"#,
).unwrap();
assert_eq!(rows.len(), 2);
let mut names = col(&rows, "name");
names.sort();
assert_eq!(names, vec!["\"Alice\"", "\"Carol\""]);
}
#[test]
fn match_with_aggregation() {
let (mut g, mut txn) = three_people();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:Person) WITH count(n) AS cnt RETURN cnt"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("cnt"), Some(&Value::Int(3)));
}
#[test]
fn list_literal_in_unwind() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "UNWIND [10, 20, 30] AS n RETURN n").unwrap();
let vals = col(&rows, "n");
assert_eq!(vals, vec!["10", "20", "30"]);
}
#[test]
fn string_functions() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:T {name: " hello "})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:T) RETURN toLower(n.name) AS lo, toUpper(n.name) AS hi, trim(n.name) AS tr"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("lo"), Some(&Value::String(" hello ".to_string())));
assert_eq!(rows[0].get("hi"), Some(&Value::String(" HELLO ".to_string())));
assert_eq!(rows[0].get("tr"), Some(&Value::String("hello".to_string())));
}
#[test]
fn math_functions() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:T {val: -3.7})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:T) RETURN abs(n.val) AS a, ceil(n.val) AS c, floor(n.val) AS f"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("a"), Some(&Value::Float(3.7)));
assert_eq!(rows[0].get("c"), Some(&Value::Float(-3.0)));
assert_eq!(rows[0].get("f"), Some(&Value::Float(-4.0)));
}
#[test]
fn range_function() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "UNWIND range(1, 3) AS n RETURN n").unwrap();
assert_eq!(col(&rows, "n"), vec!["1", "2", "3"]);
}
#[test]
fn optional_match_missing_label_returns_null() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
let rows = run(&mut g, &mut txn, "OPTIONAL MATCH (n:Ghost) RETURN n.name").unwrap();
assert_eq!(rows.len(), 1, "should get exactly one null row");
assert_eq!(rows[0].get("n.name"), Some(&Value::Null));
}
#[test]
fn optional_match_count_expr_on_no_match() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "OPTIONAL MATCH (n:Ghost) RETURN count(n) AS c").unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("c"), Some(&Value::Int(0)));
}
#[test]
fn optional_match_count_star_on_no_match() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "OPTIONAL MATCH (n:Ghost) RETURN count(*) AS c").unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("c"), Some(&Value::Int(1)));
}
#[test]
fn delete_nonexistent_node_errors() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "MATCH (n:Ghost) DELETE n").unwrap();
assert_eq!(rows.len(), 1);
let msg = format!("{}", rows[0].get("result").unwrap());
assert!(msg.contains("Deleted 0"), "got: {msg}");
}
#[test]
fn set_on_no_match_is_noop() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (n:Person) WHERE n.name = "Nobody" SET n.age = 99"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.age"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("n.age"), Some(&Value::Int(30)));
}
#[test]
fn self_loop_edge() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Node {name: "X"})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (n:Node) INSERT (n)-[:SELF]->(n)"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (a)-[:SELF]->(b) RETURN a.name, b.name"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("a.name"), Some(&Value::String("X".into())));
assert_eq!(rows[0].get("b.name"), Some(&Value::String("X".into())));
}
#[test]
fn unwind_empty_list_no_rows() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "UNWIND [] AS x RETURN x").unwrap();
assert_eq!(rows.len(), 0);
}
#[test]
fn union_all_preserves_duplicates() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:A {v: 1})"#).unwrap();
let rows = run(
&mut g, &mut txn,
"MATCH (n:A) RETURN n.v UNION ALL MATCH (n:A) RETURN n.v",
).unwrap();
assert_eq!(rows.len(), 2, "UNION ALL should keep duplicates");
}
#[test]
fn union_deduplicates() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:A {v: 1})"#).unwrap();
let rows = run(
&mut g, &mut txn,
"MATCH (n:A) RETURN n.v UNION MATCH (n:A) RETURN n.v",
).unwrap();
assert_eq!(rows.len(), 1, "UNION should deduplicate");
}
#[test]
fn where_missing_property_is_null_falsy() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:P {name: "Alice"})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:P) WHERE n.age > 20 RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 0);
}
#[test]
fn count_star_on_empty_label() {
let mut g = Graph::new();
let mut txn = 0u64;
let rows = run(&mut g, &mut txn, "MATCH (n:Empty) RETURN count(*) AS c").unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("c"), Some(&Value::Int(0)));
}
#[test]
fn property_index_updated_on_detach_delete() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"CREATE INDEX ON :P(name)"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {name: "Bob"})"#).unwrap();
run(
&mut g, &mut txn,
r#"MATCH (a:P {name: "Alice"}), (b:P {name: "Bob"}) INSERT (a)-[:KNOWS]->(b)"#,
).unwrap();
run(&mut g, &mut txn, r#"MATCH (n:P {name: "Alice"}) DETACH DELETE n"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:P {name: "Alice"}) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 0, "index should report no entries after detach-delete");
let rows2 = run(&mut g, &mut txn, r#"MATCH (n:P {name: "Bob"}) RETURN n.name"#).unwrap();
assert_eq!(rows2.len(), 1);
}
#[test]
fn match_with_having_style_filter() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:P {dept: "eng", salary: 100})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {dept: "eng", salary: 200})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {dept: "hr", salary: 50})"#).unwrap();
let rows = run(
&mut g, &mut txn,
"MATCH (n:P) WITH n.dept AS dept, sum(n.salary) AS total WHERE total > 100 RETURN dept, total",
).unwrap();
assert_eq!(rows.len(), 1, "only eng dept has total > 100");
assert_eq!(rows[0].get("dept"), Some(&Value::String("eng".into())));
assert_eq!(rows[0].get("total"), Some(&Value::Int(300)));
}
#[test]
fn multi_label_match_all_required() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person:Employee {name: "Bob"})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Person:Employee) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("n.name"), Some(&Value::String("Bob".into())));
}
#[test]
fn multi_label_partial_match() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person:Employee {name: "Bob"})"#).unwrap();
let rows = run(&mut g, &mut txn, r#"MATCH (n:Person) RETURN n.name"#).unwrap();
assert_eq!(rows.len(), 1);
}
#[test]
fn star_quantifier_includes_zero_hop() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:A {v: 1})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:A {v: 2})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (a:A {v:1})-[:X*]->(b:A {v:2}) RETURN a.v, b.v"#).unwrap();
}
#[test]
fn coalesce_returns_first_nonnull() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:T {a: 1})"#).unwrap();
let rows = run(
&mut g, &mut txn,
"MATCH (n:T) RETURN coalesce(n.missing, n.a, 99) AS v",
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("v"), Some(&Value::Int(1)));
}
#[test]
fn string_predicates() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:T {name: "hello world"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:T) WHERE startsWith(n.name, "hello") RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 1);
let rows2 = run(
&mut g, &mut txn,
r#"MATCH (n:T) WHERE endsWith(n.name, "xyz") RETURN n.name"#,
).unwrap();
assert_eq!(rows2.len(), 0);
}
#[test]
fn in_operator_string_found() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:T {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:T {name: "Bob"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:T) WHERE n.name IN ["Alice", "Carol"] RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("n.name"), Some(&Value::String("Alice".into())));
}
#[test]
fn in_operator_string_not_found() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:T {name: "Alice"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:T) WHERE n.name IN ["Bob", "Carol"] RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 0);
}
#[test]
fn in_operator_integer_list() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:T {val: 10})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:T {val: 20})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:T {val: 30})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:T) WHERE n.val IN [10, 30] RETURN n.val ORDER BY n.val"#,
).unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].get("n.val"), Some(&Value::Int(10)));
assert_eq!(rows[1].get("n.val"), Some(&Value::Int(30)));
}
#[test]
fn in_operator_empty_list_matches_nothing() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:T {name: "Alice"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n:T) WHERE n.name IN [] RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 0);
}
#[test]
fn in_operator_with_labels_function() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Company {name: "Acme"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n) WHERE "Person" IN labels(n) RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("n.name"), Some(&Value::String("Alice".into())));
}
#[test]
fn in_operator_with_multiple_label_types() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Company {name: "Acme"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Animal {name: "Rex"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (n) WHERE "Company" IN labels(n) RETURN n.name"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("n.name"), Some(&Value::String("Acme".into())));
let rows2 = run(
&mut g, &mut txn,
r#"MATCH (n) WHERE "Animal" IN labels(n) RETURN n.name"#,
).unwrap();
assert_eq!(rows2.len(), 1);
assert_eq!(rows2[0].get("n.name"), Some(&Value::String("Rex".into())));
}
fn social_graph() -> (Graph, u64) {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Carol"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Company {name: "Acme"})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (a:Person {name:"Alice"}), (c:Company {name:"Acme"}) INSERT (a)-[:WORKS_AT]->(c)"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (b:Person {name:"Bob"}), (c:Company {name:"Acme"}) INSERT (b)-[:WORKS_AT]->(c)"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (a:Person {name:"Alice"}), (b:Person {name:"Bob"}) INSERT (a)-[:KNOWS]->(b)"#).unwrap();
(g, txn)
}
#[test]
fn match_optional_match_all_found() {
let (mut g, mut txn) = social_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (p:Person {name:"Alice"}) OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company) RETURN p.name, c.name"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("p.name"), Some(&Value::String("Alice".into())));
assert_eq!(rows[0].get("c.name"), Some(&Value::String("Acme".into())));
}
#[test]
fn match_optional_match_not_found_gives_null() {
let (mut g, mut txn) = social_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (p:Person {name:"Carol"}) OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company) RETURN p.name, c.name"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("p.name"), Some(&Value::String("Carol".into())));
assert!(matches!(rows[0].get("c.name"), Some(&Value::Null) | None));
}
#[test]
fn match_optional_match_mixed_results() {
let (mut g, mut txn) = social_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (p:Person) OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company) RETURN p.name, c.name ORDER BY p.name"#,
).unwrap();
assert_eq!(rows.len(), 3, "one row per person");
let names: Vec<_> = rows.iter().map(|r| r.get("p.name").cloned().unwrap()).collect();
assert_eq!(names, vec![
Value::String("Alice".into()),
Value::String("Bob".into()),
Value::String("Carol".into()),
]);
assert!(matches!(rows[2].get("c.name"), Some(&Value::Null) | None));
assert_eq!(rows[0].get("c.name"), Some(&Value::String("Acme".into())));
assert_eq!(rows[1].get("c.name"), Some(&Value::String("Acme".into())));
}
#[test]
fn match_optional_match_does_not_duplicate_mandatory_rows() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Company {name: "Acme"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Company {name: "Globex"})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (a:Person {name:"Alice"}), (c:Company {name:"Acme"}) INSERT (a)-[:WORKS_AT]->(c)"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (a:Person {name:"Alice"}), (c:Company {name:"Globex"}) INSERT (a)-[:WORKS_AT]->(c)"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (p:Person) OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company) RETURN p.name, c.name ORDER BY c.name"#,
).unwrap();
assert_eq!(rows.len(), 2);
}
#[test]
fn match_optional_match_where_on_optional() {
let (mut g, mut txn) = social_graph();
let rows = run(
&mut g, &mut txn,
r#"MATCH (p:Person) OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company) WHERE c.name = "Acme" RETURN p.name, c.name ORDER BY p.name"#,
).unwrap();
assert_eq!(rows.len(), 3);
assert!(matches!(rows[2].get("c.name"), Some(&Value::Null) | None));
}
#[test]
fn match_two_optional_match_clauses() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Company {name: "Acme"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:City {name: "NY"})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (a:Person {name:"Alice"}), (c:Company {name:"Acme"}) INSERT (a)-[:WORKS_AT]->(c)"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"MATCH (p:Person) OPTIONAL MATCH (p)-[:WORKS_AT]->(c:Company) OPTIONAL MATCH (p)-[:LIVES_IN]->(city:City) RETURN p.name, c.name, city.name"#,
).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].get("c.name"), Some(&Value::String("Acme".into())));
assert!(matches!(rows[0].get("city.name"), Some(&Value::Null) | None));
}
#[test]
fn call_yield_projects_correct_columns() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"CALL pageRank(iterations: 5, dampingFactor: 0.85) YIELD node, score"#,
).unwrap();
assert_eq!(rows.len(), 2, "one row per node");
for r in &rows {
assert!(r.contains_key("node"), "must have 'node' column");
assert!(r.contains_key("score"), "must have 'score' column");
}
}
#[test]
fn call_yield_partial_projection() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:X {v: 1})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:X {v: 2})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"CALL pageRank() YIELD node"#,
).unwrap();
assert_eq!(rows.len(), 2);
for r in &rows {
assert!(r.contains_key("node"));
assert!(!r.contains_key("score"), "score not in YIELD — must be absent");
}
}
#[test]
fn call_degree_centrality_correct_key_names() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:A {n: "hub"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:B {n: "spoke1"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:B {n: "spoke2"})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (a:A), (b:B {n:"spoke1"}) INSERT (a)-[:E]->(b)"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (a:A), (b:B {n:"spoke2"}) INSERT (a)-[:E]->(b)"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"CALL degreeCentrality() YIELD node, degree, in_degree, out_degree"#,
).unwrap();
assert!(!rows.is_empty());
for r in &rows {
assert!(r.contains_key("degree"), "must have 'degree'");
assert!(r.contains_key("in_degree"), "must have 'in_degree'");
assert!(r.contains_key("out_degree"), "must have 'out_degree'");
assert!(!r.contains_key("totalDegree"), "'totalDegree' must not exist");
assert!(!r.contains_key("inDegree"), "'inDegree' must not exist");
assert!(!r.contains_key("outDegree"), "'outDegree' must not exist");
}
let hub = rows.iter().max_by_key(|r| {
if let Some(Value::Int(d)) = r.get("out_degree") { *d } else { 0 }
}).unwrap();
assert_eq!(hub.get("out_degree"), Some(&Value::Int(2)));
assert_eq!(hub.get("in_degree"), Some(&Value::Int(0)));
}
#[test]
fn call_wcc_yields_node_and_component() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:X)"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:X)"#).unwrap(); let rows = run(&mut g, &mut txn, r#"CALL wcc() YIELD node, component"#).unwrap();
assert_eq!(rows.len(), 2);
for r in &rows {
assert!(r.contains_key("node"));
assert!(r.contains_key("component"));
}
}
#[test]
fn call_pipeline_yield_return_basic() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:P {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P {name: "Bob"})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"CALL pageRank(iterations: 5, dampingFactor: 0.85) YIELD node, score RETURN node, score ORDER BY score DESC"#,
).unwrap();
assert_eq!(rows.len(), 2, "one row per node");
for r in &rows {
assert!(r.contains_key("node"));
assert!(r.contains_key("score"));
}
let s0 = if let Some(Value::Float(f)) = rows[0].get("score") { *f } else { 0.0 };
let s1 = if let Some(Value::Float(f)) = rows[1].get("score") { *f } else { 0.0 };
assert!(s0 >= s1, "ORDER BY score DESC: first score must be >= second");
}
#[test]
fn call_pipeline_yield_return_with_limit() {
let mut g = Graph::new();
let mut txn = 0u64;
for i in 0..5 {
run(&mut g, &mut txn, &format!(r#"INSERT (:N {{v: {}}})"#, i)).unwrap();
}
let rows = run(
&mut g, &mut txn,
r#"CALL degreeCentrality() YIELD node, degree RETURN node, degree LIMIT 2"#,
).unwrap();
assert_eq!(rows.len(), 2);
}
#[test]
fn call_pipeline_yield_return_aggregation() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:P)"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P)"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:P)"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"CALL wcc() YIELD node, component RETURN component, count(*) AS sz ORDER BY sz DESC"#,
).unwrap();
assert_eq!(rows.len(), 3);
for r in &rows {
assert_eq!(r.get("sz"), Some(&Value::Int(1)));
}
}
#[test]
fn call_pipeline_match_filters_by_label() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Company {name: "Acme"})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (a:Person {name:"Alice"}), (b:Person {name:"Bob"}) INSERT (a)-[:KNOWS]->(b)"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"CALL pageRank(iterations: 5, dampingFactor: 0.85) YIELD node, score MATCH (n) WHERE n = node AND "Person" IN labels(n) RETURN n.name, score ORDER BY n.name"#,
).unwrap();
assert_eq!(rows.len(), 2, "only Person nodes returned");
let names: Vec<_> = rows.iter()
.map(|r| r.get("n.name").cloned().unwrap())
.collect();
assert!(names.contains(&Value::String("Alice".into())));
assert!(names.contains(&Value::String("Bob".into())));
for r in &rows {
assert_ne!(r.get("n.name"), Some(&Value::String("Acme".into())));
}
}
#[test]
fn call_pipeline_match_returns_node_properties() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob", age: 25})"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"CALL degreeCentrality() YIELD node, degree MATCH (n) WHERE n = node AND "Person" IN labels(n) RETURN n.name, n.age, degree ORDER BY n.name"#,
).unwrap();
assert_eq!(rows.len(), 2);
assert!(rows[0].contains_key("n.name"));
assert!(rows[0].contains_key("n.age"));
assert!(rows[0].contains_key("degree"));
}
#[test]
fn call_pipeline_match_order_by_score() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Carol"})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (a:Person {name:"Alice"}), (b:Person {name:"Bob"}) INSERT (a)-[:KNOWS]->(b)"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (b:Person {name:"Bob"}), (c:Person {name:"Carol"}) INSERT (b)-[:KNOWS]->(c)"#).unwrap();
let rows = run(
&mut g, &mut txn,
r#"CALL pageRank(iterations: 20, dampingFactor: 0.85) YIELD node, score MATCH (n) WHERE n = node AND "Person" IN labels(n) RETURN n.name, round(score * 1000) / 1000 AS pagerank ORDER BY pagerank DESC LIMIT 5"#,
).unwrap();
assert_eq!(rows.len(), 3, "all 3 Person nodes returned");
for r in &rows {
assert!(r.contains_key("n.name"), "must have n.name");
assert!(r.contains_key("pagerank"), "must have pagerank alias");
}
for r in &rows {
if let Some(Value::Float(f)) = r.get("pagerank") {
assert!(*f >= 0.0, "pagerank score must be non-negative");
}
}
}
#[test]
fn load_csv_nodes_gql() {
use std::io::Write;
let mut g = Graph::new();
let mut txn = 0u64;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nodes.csv");
{
let mut f = std::fs::File::create(&path).unwrap();
writeln!(f, ":ID,name,age,:LABEL").unwrap();
writeln!(f, "1,Alice,30,Person").unwrap();
writeln!(f, "2,Bob,25,Person").unwrap();
}
let gql = format!("LOAD CSV NODES FROM '{}'", path.display());
let rows = run(&mut g, &mut txn, &gql).unwrap();
assert_eq!(rows.len(), 1);
assert!(rows[0]["result"].to_string().contains("2"), "summary should say 2 nodes");
assert_eq!(g.node_count(), 2);
assert_eq!(g.csv_id_map.len(), 2);
}
#[test]
fn load_csv_nodes_with_label_override() {
use std::io::Write;
let mut g = Graph::new();
let mut txn = 0u64;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nodes.csv");
{
let mut f = std::fs::File::create(&path).unwrap();
writeln!(f, ":ID,name").unwrap();
writeln!(f, "1,Alice").unwrap();
}
let gql = format!("LOAD CSV NODES FROM '{}' LABEL Employee", path.display());
run(&mut g, &mut txn, &gql).unwrap();
let nid = *g.csv_id_map.values().next().unwrap();
let node = g.get_node(nid).unwrap();
assert_eq!(node.labels, vec!["Employee"]);
}
#[test]
fn load_csv_edges_gql() {
use std::io::Write;
let mut g = Graph::new();
let mut txn = 0u64;
let dir = tempfile::tempdir().unwrap();
let npath = dir.path().join("nodes.csv");
let epath = dir.path().join("edges.csv");
{
let mut f = std::fs::File::create(&npath).unwrap();
writeln!(f, ":ID,name,:LABEL").unwrap();
writeln!(f, "1,Alice,Person").unwrap();
writeln!(f, "2,Bob,Person").unwrap();
}
{
let mut f = std::fs::File::create(&epath).unwrap();
writeln!(f, ":START_ID,:END_ID,:TYPE,weight").unwrap();
writeln!(f, "1,2,KNOWS,0.9").unwrap();
}
run(&mut g, &mut txn, &format!("LOAD CSV NODES FROM '{}'", npath.display())).unwrap();
let rows = run(&mut g, &mut txn, &format!("LOAD CSV EDGES FROM '{}'", epath.display())).unwrap();
assert_eq!(g.edge_count(), 1);
assert!(rows[0]["result"].to_string().contains("1"));
}
#[test]
fn call_pipeline_match_with_limit() {
let mut g = Graph::new();
let mut txn = 0u64;
for i in 0..6 {
run(&mut g, &mut txn, &format!(r#"INSERT (:Person {{name: "P{}"}})"#, i)).unwrap();
}
let rows = run(
&mut g, &mut txn,
r#"CALL pageRank() YIELD node, score MATCH (n) WHERE n = node AND "Person" IN labels(n) RETURN n.name, score ORDER BY score DESC LIMIT 3"#,
).unwrap();
assert_eq!(rows.len(), 3, "LIMIT 3 must cap results");
}
#[test]
fn truncate_clears_all_nodes_and_edges() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name:"Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name:"Bob"})"#).unwrap();
let rows = run(&mut g, &mut txn, "MATCH (a:Person), (b:Person) WHERE a.name <> b.name INSERT (a)-[:KNOWS]->(b)").unwrap();
let _ = rows;
let n = run(&mut g, &mut txn, "MATCH (n) RETURN count(n) AS c").unwrap();
assert!(n[0]["c"].to_string().parse::<u64>().unwrap() > 0);
let result = run(&mut g, &mut txn, "TRUNCATE").unwrap();
assert_eq!(result[0]["cleared"], crate::types::Value::Bool(true));
let n = run(&mut g, &mut txn, "MATCH (n) RETURN count(n) AS c").unwrap();
assert_eq!(n[0]["c"], crate::types::Value::Int(0));
let e = run(&mut g, &mut txn, "MATCH ()-[r]->() RETURN count(r) AS c").unwrap();
assert_eq!(e[0]["c"], crate::types::Value::Int(0));
}
#[test]
fn truncate_is_idempotent_on_empty_graph() {
let mut g = Graph::new();
let mut txn = 0u64;
let result = run(&mut g, &mut txn, "TRUNCATE").unwrap();
assert_eq!(result[0]["cleared"], crate::types::Value::Bool(true));
let n = run(&mut g, &mut txn, "MATCH (n) RETURN count(n) AS c").unwrap();
assert_eq!(n[0]["c"], crate::types::Value::Int(0));
}
#[test]
fn truncate_preserves_index_definitions() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE INDEX ON :Person(name)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name:"Alice"})"#).unwrap();
run(&mut g, &mut txn, "TRUNCATE").unwrap();
let rows = run(&mut g, &mut txn, "SHOW INDEXES").unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["label"], crate::types::Value::String("Person".into()));
}
#[test]
fn constraint_create_show_drop() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Person(email)").unwrap();
let rows = run(&mut g, &mut txn, "SHOW CONSTRAINTS").unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["label"], crate::types::Value::String("Person".into()));
assert_eq!(rows[0]["property"], crate::types::Value::String("email".into()));
assert_eq!(rows[0]["kind"], crate::types::Value::String("UNIQUE".into()));
run(&mut g, &mut txn, "DROP CONSTRAINT UNIQUE ON :Person(email)").unwrap();
let rows2 = run(&mut g, &mut txn, "SHOW CONSTRAINTS").unwrap();
assert_eq!(rows2.len(), 0);
}
#[test]
fn constraint_type_integer() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT TYPE IS INTEGER ON :Person(age)").unwrap();
let rows = run(&mut g, &mut txn, "SHOW CONSTRAINTS").unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["kind"], crate::types::Value::String("TYPE IS INTEGER".into()));
}
#[test]
fn constraint_unique_enforced() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Person(email)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", email: "a@b.com"})"#).unwrap();
let err = run(&mut g, &mut txn, r#"INSERT (:Person {name: "Eve", email: "a@b.com"})"#);
assert!(err.is_err(), "expected constraint violation but got Ok");
let msg = format!("{}", err.unwrap_err());
assert!(msg.contains("Constraint violation"), "expected constraint violation, got: {msg}");
}
#[test]
fn constraint_type_enforced() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT TYPE IS INTEGER ON :Person(age)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {age: 30})"#).unwrap();
let err = run(&mut g, &mut txn, r#"INSERT (:Person {age: "not-a-number"})"#);
assert!(err.is_err());
let msg = format!("{}", err.unwrap_err());
assert!(msg.contains("Constraint violation"), "expected constraint violation, got: {msg}");
}
#[test]
fn constraint_allows_self_update() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Person(email)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {email: "a@b.com"})"#).unwrap();
run(&mut g, &mut txn, r#"MATCH (n:Person {email: "a@b.com"}) SET n.email = "a@b.com""#).unwrap();
}
#[test]
fn param_basic_literal() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
let mut params = std::collections::HashMap::new();
params.insert("target".to_string(), crate::types::Value::String("Alice".to_string()));
let stmt = super::super::parser::parse(r#"MATCH (n:Person {name: $target}) RETURN n.name"#).unwrap();
let (rows, _) = execute_capturing_with_params(stmt, &mut g, &mut txn, params).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["n.name"], crate::types::Value::String("Alice".into()));
}
#[test]
fn unwind_insert_from_literal_list() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn,
r#"UNWIND ["Alice", "Bob", "Carol"] AS name INSERT (:Person {name: name})"#
).unwrap();
let rows = run(&mut g, &mut txn, "MATCH (n:Person) RETURN n.name ORDER BY n.name").unwrap();
assert_eq!(rows.len(), 3);
let names = col(&rows, "n.name");
assert!(names.contains(&"\"Alice\"".to_string()));
assert!(names.contains(&"\"Bob\"".to_string()));
assert!(names.contains(&"\"Carol\"".to_string()));
}
#[test]
fn unwind_insert_from_param() {
let mut g = Graph::new();
let mut txn = 0u64;
let mut params = std::collections::HashMap::new();
params.insert("names".to_string(), crate::types::Value::List(vec![
crate::types::Value::String("Dave".into()),
crate::types::Value::String("Eve".into()),
]));
let stmt = super::super::parser::parse(
"UNWIND $names AS name INSERT (:Person {name: name})"
).unwrap();
let (rows, _) = execute_capturing_with_params(stmt, &mut g, &mut txn, params).unwrap();
assert!(rows[0]["result"].to_string().contains("Inserted 2"));
let qrows = run(&mut g, &mut txn, "MATCH (n:Person) RETURN n.name ORDER BY n.name").unwrap();
assert_eq!(qrows.len(), 2);
}
#[test]
fn constraint_show_multiple() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Person(email)").unwrap();
run(&mut g, &mut txn, "CREATE CONSTRAINT TYPE IS INTEGER ON :Person(age)").unwrap();
run(&mut g, &mut txn, "CREATE CONSTRAINT TYPE IS STRING ON :Company(name)").unwrap();
let rows = run(&mut g, &mut txn, "SHOW CONSTRAINTS").unwrap();
assert_eq!(rows.len(), 3);
let labels: Vec<_> = rows.iter()
.map(|r| r["label"].to_string().trim_matches('"').to_string())
.collect();
assert!(labels.contains(&"Person".to_string()));
assert!(labels.contains(&"Company".to_string()));
}
#[test]
fn constraint_type_float() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT TYPE IS FLOAT ON :Sensor(reading)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Sensor {reading: 3.14})"#).unwrap();
let err = run(&mut g, &mut txn, r#"INSERT (:Sensor {reading: 42})"#);
assert!(err.is_err(), "integer should violate FLOAT constraint");
}
#[test]
fn constraint_type_string() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT TYPE IS STRING ON :Tag(name)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Tag {name: "rust"})"#).unwrap();
let err = run(&mut g, &mut txn, r#"INSERT (:Tag {name: 99})"#);
assert!(err.is_err(), "integer should violate STRING constraint");
}
#[test]
fn constraint_type_boolean() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT TYPE IS BOOLEAN ON :Feature(enabled)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Feature {enabled: true})"#).unwrap();
let err = run(&mut g, &mut txn, r#"INSERT (:Feature {enabled: "yes"})"#);
assert!(err.is_err(), "string should violate BOOLEAN constraint");
}
#[test]
fn constraint_unique_missing_property_allowed() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Person(email)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob"})"#).unwrap();
let rows = run(&mut g, &mut txn, "MATCH (n:Person) RETURN count(n) AS c").unwrap();
assert_eq!(rows[0]["c"], crate::types::Value::Int(2));
}
#[test]
fn constraint_unique_different_labels_allowed() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Person(email)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {email: "a@b.com"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Robot {email: "a@b.com"})"#).unwrap();
let rows = run(&mut g, &mut txn, "MATCH (n) RETURN count(n) AS c").unwrap();
assert_eq!(rows[0]["c"], crate::types::Value::Int(2));
}
#[test]
fn constraint_set_triggers_unique() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Person(email)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", email: "alice@x.com"})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob", email: "bob@x.com"})"#).unwrap();
let err = run(&mut g, &mut txn,
r#"MATCH (n:Person {name: "Bob"}) SET n.email = "alice@x.com""#);
assert!(err.is_err(), "SET should be blocked by UNIQUE constraint");
let msg = format!("{}", err.unwrap_err());
assert!(msg.contains("Constraint violation"));
}
#[test]
fn constraint_set_triggers_type() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT TYPE IS INTEGER ON :Person(age)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
let err = run(&mut g, &mut txn,
r#"MATCH (n:Person {name: "Alice"}) SET n.age = "thirty""#);
assert!(err.is_err(), "SET should be blocked by TYPE constraint");
}
#[test]
fn constraint_duplicate_create_is_noop() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Person(email)").unwrap();
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Person(email)").unwrap();
let rows = run(&mut g, &mut txn, "SHOW CONSTRAINTS").unwrap();
assert_eq!(rows.len(), 1);
}
#[test]
fn constraint_drop_removes_enforcement() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Person(email)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", email: "a@b.com"})"#).unwrap();
run(&mut g, &mut txn, "DROP CONSTRAINT UNIQUE ON :Person(email)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Eve", email: "a@b.com"})"#).unwrap();
let rows = run(&mut g, &mut txn, "MATCH (n:Person) RETURN count(n) AS c").unwrap();
assert_eq!(rows[0]["c"], crate::types::Value::Int(2));
}
#[test]
fn unwind_insert_respects_constraints() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :Tag(name)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:Tag {name: "rust"})"#).unwrap();
let err = run(&mut g, &mut txn,
r#"UNWIND ["go", "rust"] AS n INSERT (:Tag {name: n})"#);
assert!(err.is_err(), "UNWIND INSERT should propagate constraint violation");
}
#[test]
fn param_missing_evaluates_to_null() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
let stmt = super::super::parser::parse(
r#"MATCH (n:Person) WHERE n.name = $target RETURN n.name"#
).unwrap();
let (rows, _) = execute_capturing_with_params(stmt, &mut g, &mut txn,
std::collections::HashMap::new()).unwrap();
assert_eq!(rows.len(), 0, "null param should match nothing");
}
#[test]
fn param_integer_in_where() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice", age: 30})"#).unwrap();
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Bob", age: 20})"#).unwrap();
let mut params = std::collections::HashMap::new();
params.insert("min_age".into(), crate::types::Value::Int(25));
let stmt = super::super::parser::parse(
"MATCH (n:Person) WHERE n.age >= $min_age RETURN n.name"
).unwrap();
let (rows, _) = execute_capturing_with_params(stmt, &mut g, &mut txn, params).unwrap();
assert_eq!(rows.len(), 1);
assert_eq!(rows[0]["n.name"], crate::types::Value::String("Alice".into()));
}
#[test]
fn param_used_as_limit() {
let mut g = Graph::new();
let mut txn = 0u64;
for i in 0..10i64 {
run(&mut g, &mut txn, &format!(r#"INSERT (:Item {{n: {i}}})"#)).unwrap();
}
let mut params = std::collections::HashMap::new();
params.insert("k".into(), crate::types::Value::Int(3));
let stmt = super::super::parser::parse(
"MATCH (x:Item) RETURN x.n ORDER BY x.n LIMIT $k"
).unwrap();
let (rows, _) = execute_capturing_with_params(stmt, &mut g, &mut txn, params).unwrap();
assert_eq!(rows.len(), 3);
}
#[test]
fn params_do_not_leak_between_calls() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, r#"INSERT (:Person {name: "Alice"})"#).unwrap();
let mut p = std::collections::HashMap::new();
p.insert("target".into(), crate::types::Value::String("Alice".into()));
let stmt1 = super::super::parser::parse(
r#"MATCH (n:Person) WHERE n.name = $target RETURN n.name"#
).unwrap();
let (rows, _) = execute_capturing_with_params(stmt1, &mut g, &mut txn, p).unwrap();
assert_eq!(rows.len(), 1);
let stmt2 = super::super::parser::parse(
r#"MATCH (n:Person) WHERE n.name = $target RETURN n.name"#
).unwrap();
let (rows2, _) = execute_capturing_with_params(stmt2, &mut g, &mut txn,
std::collections::HashMap::new()).unwrap();
assert_eq!(rows2.len(), 0, "params must be cleared after each call");
}
#[test]
fn unwind_insert_map_items() {
let mut g = Graph::new();
let mut txn = 0u64;
let mut params = std::collections::HashMap::new();
params.insert("people".into(), crate::types::Value::List(vec![
{
let mut m = std::collections::HashMap::new();
m.insert("name".into(), crate::types::Value::String("Alice".into()));
m.insert("age".into(), crate::types::Value::Int(30));
crate::types::Value::Map(m)
},
{
let mut m = std::collections::HashMap::new();
m.insert("name".into(), crate::types::Value::String("Bob".into()));
m.insert("age".into(), crate::types::Value::Int(25));
crate::types::Value::Map(m)
},
]));
let stmt = super::super::parser::parse(
"UNWIND $people AS p INSERT (:Person {name: p.name, age: p.age})"
).unwrap();
let (rows, _) = execute_capturing_with_params(stmt, &mut g, &mut txn, params).unwrap();
assert!(rows[0]["result"].to_string().contains("Inserted 2"));
let qrows = run(&mut g, &mut txn,
"MATCH (n:Person) RETURN n.name, n.age ORDER BY n.name").unwrap();
assert_eq!(qrows.len(), 2);
assert_eq!(qrows[0]["n.name"], crate::types::Value::String("Alice".into()));
assert_eq!(qrows[0]["n.age"], crate::types::Value::Int(30));
assert_eq!(qrows[1]["n.name"], crate::types::Value::String("Bob".into()));
}
#[test]
fn param_and_constraint_interact_correctly() {
let mut g = Graph::new();
let mut txn = 0u64;
run(&mut g, &mut txn, "CREATE CONSTRAINT UNIQUE ON :User(username)").unwrap();
run(&mut g, &mut txn, r#"INSERT (:User {username: "alice"})"#).unwrap();
let mut p = std::collections::HashMap::new();
p.insert("uname".into(), crate::types::Value::String("alice".into()));
let stmt = super::super::parser::parse(
r#"INSERT (:User {username: $uname})"#
).unwrap();
let stmt2 = super::super::parser::parse(
r#"INSERT (:User {username: $uname, display: "imposter"})"#
).unwrap();
let err = execute_capturing_with_params(stmt2, &mut g, &mut txn, p);
assert!(err.is_err(), "parameterized INSERT should be blocked by UNIQUE constraint");
}
}