use std::collections::{BTreeMap, HashMap, HashSet};
use marsdb_graph::{AdjEntry, Direction, EdgeId, GraphStore, NodeId, PropertyValue, Txn, WriteTransaction};
use crate::aggregate::{property_value_hash_key, value_hash_key, AggAcc, HashKey};
use crate::ast::{
is_aggregate_name, CompareOp, Expr, Literal, Pattern, PropAccess, QueryPart, RelDirection, ReturnExpr,
ReturnItem, SortDir, Statement, Tail, WithClause, WithExpr,
};
use crate::error::QueryError;
use crate::ir::{ExpandDirection, LogicalPlan};
use crate::planner::{build_match_plan, pattern_all_vars, pattern_new_vars};
use crate::result::QueryResult;
use crate::value::Value;
const OPTIONAL_SEED_IDX_KEY: &str = "__seed_idx";
#[derive(Debug, Clone)]
enum Binding {
Node(NodeId),
Edge(EdgeId),
Value(PropertyValue),
List(Vec<Value>),
}
type BindingRow = HashMap<String, Binding>;
const VAR_EXPAND_DEPTH_CAP: u32 = 30;
pub struct Executor<'a> {
store: &'a GraphStore,
}
impl<'a> Executor<'a> {
pub fn new(store: &'a GraphStore) -> Self {
Self { store }
}
pub fn execute(&self, stmt: &Statement) -> Result<QueryResult, QueryError> {
if is_read_only(stmt) {
let read_txn = self.store.begin_read()?;
let Statement::Match {
parts,
tail,
order_by,
limit,
} = stmt
else {
unreachable!("is_read_only only returns true for Statement::Match")
};
return self.execute_match(Txn::Read(&read_txn), parts, tail, order_by, *limit);
}
let write_txn = self.store.begin_write()?;
let outcome = match stmt {
Statement::Create(patterns) => self.execute_create(&write_txn, patterns),
Statement::Match {
parts,
tail,
order_by,
limit,
} => self.execute_match(Txn::Write(&write_txn), parts, tail, order_by, *limit),
};
match outcome {
Ok(result) => {
GraphStore::commit(write_txn)?;
Ok(result)
}
Err(e) => {
let _ = GraphStore::abort(write_txn);
Err(e)
}
}
}
fn execute_create(&self, write_txn: &WriteTransaction, patterns: &[Pattern]) -> Result<QueryResult, QueryError> {
for pattern in patterns {
let start_labels = pattern_labels(&pattern.start.labels);
let start_props = literal_props_to_values(&pattern.start.props);
let mut prev_id = GraphStore::create_node_in_txn(write_txn, &start_labels, start_props)?;
for (rel, node) in &pattern.hops {
if rel.hop_range.is_some() {
return Err(QueryError::Parse(
"CREATE doesn't support variable-length relationship patterns (e.g. [:TYPE*1..3])".into(),
));
}
let labels = pattern_labels(&node.labels);
let props = literal_props_to_values(&node.props);
let node_id = GraphStore::create_node_in_txn(write_txn, &labels, props)?;
let rel_label = rel.rel_type.clone().unwrap_or_else(|| "REL".to_string());
let rel_props = literal_props_to_values(&rel.props);
let (src, dst) = match rel.direction {
RelDirection::Right => (prev_id, node_id),
RelDirection::Left => (node_id, prev_id),
RelDirection::Either => {
return Err(QueryError::Parse(
"CREATE requires a directed relationship (-> or <-), not an undirected pattern".into(),
))
}
};
GraphStore::create_edge_in_txn(write_txn, &rel_label, src, dst, rel_props)?;
prev_id = node_id;
}
}
Ok(QueryResult {
columns: vec![],
rows: vec![],
})
}
fn execute_match(
&self,
txn: Txn,
parts: &[QueryPart],
tail: &Tail,
order_by: &Option<Vec<(ReturnExpr, SortDir)>>,
limit: Option<i64>,
) -> Result<QueryResult, QueryError> {
let mut carried_vars: HashSet<String> = HashSet::new();
let mut current_rows: Vec<BindingRow> = vec![BindingRow::new()];
for part in parts {
let plan = build_match_plan(&part.pattern, &part.where_clause, &carried_vars)?;
current_rows = if part.optional {
let new_vars = pattern_new_vars(&part.pattern, &carried_vars);
self.eval_optional_part(txn, &plan, ¤t_rows, &new_vars)?
} else {
self.eval_plan(txn, &plan, ¤t_rows)?
};
if let Some(with) = &part.with {
current_rows = self.materialize_with(txn, with, ¤t_rows)?;
if let Some(with_order_by) = &with.order_by {
current_rows = self.apply_order_by_bindings(txn, current_rows, with_order_by)?;
}
if let Some(with_limit) = with.limit {
current_rows.truncate(with_limit.max(0) as usize);
}
carried_vars = with.items.iter().enumerate().map(with_item_output_name).collect();
} else {
carried_vars.extend(pattern_all_vars(&part.pattern));
}
}
if order_by.is_none() {
if let Some(count) = limit {
current_rows.truncate(count.max(0) as usize);
}
}
let mut result = match tail {
Tail::Return(items) => self.materialize_return(txn, items, ¤t_rows)?,
Tail::Delete(vars) => {
self.materialize_delete(require_write_txn(txn), vars, ¤t_rows, false)?
}
Tail::DetachDelete(vars) => {
self.materialize_delete(require_write_txn(txn), vars, ¤t_rows, true)?
}
Tail::Set(items) => self.materialize_set(require_write_txn(txn), items, ¤t_rows)?,
};
if let Some(order_by) = order_by {
result.rows = apply_order_by(result.rows, &result.columns, order_by)?;
if let Some(count) = limit {
result.rows.truncate(count.max(0) as usize);
}
}
Ok(result)
}
fn materialize_with(
&self,
txn: Txn,
with: &WithClause,
rows: &[BindingRow],
) -> Result<Vec<BindingRow>, QueryError> {
let mut out = if !has_aggregate(&with.items) {
let mut out = Vec::with_capacity(rows.len());
for row in rows {
let mut new_row = BindingRow::new();
for (i, item) in with.items.iter().enumerate() {
let name = with_item_output_name((i, item));
let binding = self.item_binding(txn, &item.expr, row)?;
new_row.insert(name, binding);
}
out.push(new_row);
}
out
} else {
validate_return_items(&with.items)?;
let grouped = self.resolve_grouped_rows(txn, &with.items, rows)?;
grouped
.into_iter()
.map(|bindings| {
with.items
.iter()
.enumerate()
.zip(bindings)
.map(|((i, item), b)| (with_item_output_name((i, item)), b))
.collect()
})
.collect()
};
if let Some(where_clause) = &with.where_clause {
let mut filtered = Vec::with_capacity(out.len());
for row in out {
if self.eval_with_expr(txn, where_clause, &row)? {
filtered.push(row);
}
}
out = filtered;
}
Ok(out)
}
fn item_binding(&self, txn: Txn, expr: &ReturnExpr, row: &BindingRow) -> Result<Binding, QueryError> {
match expr {
ReturnExpr::Var(v) => row.get(v).cloned().ok_or_else(|| QueryError::UnboundVariable(v.clone())),
other => {
let value = self.eval_return_expr(txn, other, row)?;
Ok(Binding::Value(value_to_property_value(&value)))
}
}
}
fn apply_order_by_bindings(
&self,
txn: Txn,
rows: Vec<BindingRow>,
order_by: &[(ReturnExpr, SortDir)],
) -> Result<Vec<BindingRow>, QueryError> {
let mut keyed: Vec<(Vec<Value>, BindingRow)> = Vec::with_capacity(rows.len());
for row in rows {
let value_map = self.binding_row_to_value_map(txn, &row)?;
let keys = order_by
.iter()
.map(|(expr, _)| eval_projected_expr(expr, &value_map))
.collect::<Result<Vec<_>, _>>()?;
keyed.push((keys, row));
}
keyed.sort_by(|(ka, _), (kb, _)| {
for (i, (_, dir)) in order_by.iter().enumerate() {
let ord = compare_with_dir(&ka[i], &kb[i], *dir);
if ord != std::cmp::Ordering::Equal {
return ord;
}
}
std::cmp::Ordering::Equal
});
Ok(keyed.into_iter().map(|(_, row)| row).collect())
}
fn binding_row_to_value_map(
&self,
txn: Txn,
row: &BindingRow,
) -> Result<HashMap<String, Value>, QueryError> {
let mut map = HashMap::with_capacity(row.len());
for (k, binding) in row {
map.insert(k.clone(), self.binding_to_value(txn, binding)?);
}
Ok(map)
}
fn binding_to_value(&self, txn: Txn, b: &Binding) -> Result<Value, QueryError> {
Ok(match b {
Binding::Node(id) => Value::Node(
GraphStore::get_node_in_txn(txn, *id)?
.expect("bound node exists within this statement's transaction"),
),
Binding::Edge(id) => Value::Edge(
GraphStore::get_edge_in_txn(txn, *id)?
.expect("bound edge exists within this statement's transaction"),
),
Binding::Value(PropertyValue::Null) => Value::Null,
Binding::Value(pv) => Value::Property(pv.clone()),
Binding::List(items) => Value::List(items.clone()),
})
}
fn resolve_grouped_rows(
&self,
txn: Txn,
items: &[ReturnItem],
rows: &[BindingRow],
) -> Result<Vec<Vec<Binding>>, QueryError> {
struct Group {
key_bindings: Vec<Option<Binding>>,
accs: Vec<Option<AggAcc>>,
row_count: i64,
}
fn fresh_accs(items: &[ReturnItem]) -> Vec<Option<AggAcc>> {
items
.iter()
.map(|item| match &item.expr {
ReturnExpr::Call { name, distinct, .. } if is_aggregate_name(name) => {
Some(AggAcc::identity(name, *distinct))
}
_ => None,
})
.collect()
}
let mut groups: Vec<Group> = Vec::new();
let mut group_index: HashMap<Vec<Option<HashKey>>, usize> = HashMap::new();
for row in rows {
let mut key_bindings = Vec::with_capacity(items.len());
for item in items {
key_bindings.push(if is_top_level_aggregate(&item.expr) {
None
} else {
Some(self.item_binding(txn, &item.expr, row)?)
});
}
let hash_key: Vec<Option<HashKey>> =
key_bindings.iter().map(|b| b.as_ref().map(binding_hash_key)).collect();
let group_idx = *group_index.entry(hash_key).or_insert_with(|| {
groups.push(Group {
key_bindings: key_bindings.clone(),
accs: fresh_accs(items),
row_count: 0,
});
groups.len() - 1
});
let group = &mut groups[group_idx];
group.row_count += 1;
for (i, item) in items.iter().enumerate() {
let ReturnExpr::Call { args, .. } = &item.expr else { continue };
if !is_top_level_aggregate(&item.expr) {
continue;
}
let value = self.eval_return_expr(txn, &args[0], row)?;
if !matches!(value, Value::Null) {
if let Some(acc) = &mut group.accs[i] {
acc.fold(&value)?;
}
}
}
}
let no_key_items = items.iter().all(|item| is_top_level_aggregate(&item.expr));
if groups.is_empty() && no_key_items {
groups.push(Group {
key_bindings: vec![None; items.len()],
accs: fresh_accs(items),
row_count: 0,
});
}
let mut out = Vec::with_capacity(groups.len());
for mut group in groups {
let mut row_out = Vec::with_capacity(items.len());
for (i, item) in items.iter().enumerate() {
let binding = if matches!(item.expr, ReturnExpr::CountStar) {
Binding::Value(PropertyValue::Int(group.row_count))
} else if is_top_level_aggregate(&item.expr) {
let value = group.accs[i]
.take()
.expect("aggregate item must have an accumulator")
.finish();
value_to_binding(value)
} else {
group.key_bindings[i].clone().expect("non-aggregate item must have a key binding")
};
row_out.push(binding);
}
out.push(row_out);
}
Ok(out)
}
fn eval_with_expr(&self, txn: Txn, expr: &WithExpr, row: &BindingRow) -> Result<bool, QueryError> {
Ok(match expr {
WithExpr::And(l, r) => self.eval_with_expr(txn, l, row)? && self.eval_with_expr(txn, r, row)?,
WithExpr::Or(l, r) => self.eval_with_expr(txn, l, row)? || self.eval_with_expr(txn, r, row)?,
WithExpr::Not(e) => !self.eval_with_expr(txn, e, row)?,
WithExpr::Compare(lhs, op, lit) => {
let value = self.eval_return_expr(txn, lhs, row)?;
compare_value(&value, *op, lit)
}
})
}
fn eval_optional_part(
&self,
txn: Txn,
plan: &LogicalPlan,
outer_rows: &[BindingRow],
new_vars: &HashSet<String>,
) -> Result<Vec<BindingRow>, QueryError> {
let tagged: Vec<BindingRow> = outer_rows
.iter()
.enumerate()
.map(|(i, row)| {
let mut r = row.clone();
r.insert(OPTIONAL_SEED_IDX_KEY.to_string(), Binding::Value(PropertyValue::Int(i as i64)));
r
})
.collect();
let results = self.eval_plan(txn, plan, &tagged)?;
let mut by_idx: HashMap<i64, Vec<BindingRow>> = HashMap::new();
for mut row in results {
let idx = match row.remove(OPTIONAL_SEED_IDX_KEY) {
Some(Binding::Value(PropertyValue::Int(i))) => i,
other => unreachable!("__seed_idx tagged internally as Binding::Value(Int), got {other:?}"),
};
by_idx.entry(idx).or_default().push(row);
}
let mut out = Vec::with_capacity(outer_rows.len());
for (i, outer_row) in outer_rows.iter().enumerate() {
match by_idx.remove(&(i as i64)) {
Some(matches) => out.extend(matches),
None => {
let mut padded = outer_row.clone();
for var in new_vars {
padded.insert(var.clone(), Binding::Value(PropertyValue::Null));
}
out.push(padded);
}
}
}
Ok(out)
}
fn eval_plan(
&self,
txn: Txn,
plan: &LogicalPlan,
seed: &[BindingRow],
) -> Result<Vec<BindingRow>, QueryError> {
match plan {
LogicalPlan::Seed { var } => {
debug_assert!(
seed.first().is_none_or(|row| row.contains_key(var)),
"Seed{{var: {var:?}}} planned for a var not present in the carried-forward rows"
);
Ok(seed.to_vec())
}
LogicalPlan::AllNodesScan { var } => self.scan(txn, var, None),
LogicalPlan::NodeByLabelScan { var, label } => self.scan(txn, var, Some(label)),
LogicalPlan::Expand {
input,
from_var,
to_var,
rel_var,
rel_label,
direction,
} => {
let base_rows = self.eval_plan(txn, input, seed)?;
let mut out = Vec::new();
for row in base_rows {
let Some(Binding::Node(from_id)) = row.get(from_var).cloned() else {
return Err(QueryError::UnboundVariable(from_var.clone()));
};
let entries = neighbors_for_direction(txn, from_id, *direction, rel_label.as_deref())?;
for entry in entries {
let mut new_row = row.clone();
new_row.insert(to_var.clone(), Binding::Node(entry.other));
if let Some(rv) = rel_var {
new_row.insert(rv.clone(), Binding::Edge(entry.edge_id));
}
out.push(new_row);
}
}
Ok(out)
}
LogicalPlan::VarExpand {
input,
from_var,
to_var,
rel_label,
direction,
min_hops,
max_hops,
} => {
let base_rows = self.eval_plan(txn, input, seed)?;
let mut out = Vec::new();
let unbounded = max_hops.is_none();
let effective_max = max_hops.unwrap_or(VAR_EXPAND_DEPTH_CAP);
for row in base_rows {
let Some(Binding::Node(start_id)) = row.get(from_var).cloned() else {
return Err(QueryError::UnboundVariable(from_var.clone()));
};
let mut visited = HashSet::new();
visited.insert(start_id);
if *min_hops == 0 {
let mut new_row = row.clone();
new_row.insert(to_var.clone(), Binding::Node(start_id));
out.push(new_row);
}
let mut frontier = vec![start_id];
let mut depth = 0u32;
while depth < effective_max && !frontier.is_empty() {
depth += 1;
let mut next_frontier = Vec::new();
for node in frontier {
let entries = neighbors_for_direction(txn, node, *direction, rel_label.as_deref())?;
for entry in entries {
if visited.insert(entry.other) {
next_frontier.push(entry.other);
if depth >= *min_hops {
let mut new_row = row.clone();
new_row.insert(to_var.clone(), Binding::Node(entry.other));
out.push(new_row);
}
}
}
}
frontier = next_frontier;
if depth == effective_max && unbounded && !frontier.is_empty() {
return Err(QueryError::Parse(format!(
"variable-length traversal exceeded the safety depth cap ({VAR_EXPAND_DEPTH_CAP} \
hops) — likely a cyclic graph or unexpectedly large fanout; narrow the pattern or \
add an explicit upper bound (e.g. *0..10)"
)));
}
}
}
Ok(out)
}
LogicalPlan::Filter { input, predicate } => {
let rows = self.eval_plan(txn, input, seed)?;
let mut out = Vec::with_capacity(rows.len());
for row in rows {
if self.eval_expr(txn, predicate, &row)? {
out.push(row);
}
}
Ok(out)
}
}
}
fn scan(&self, txn: Txn, var: &str, label: Option<&str>) -> Result<Vec<BindingRow>, QueryError> {
let nodes = GraphStore::all_nodes_in_txn(txn, label)?;
Ok(nodes
.into_iter()
.map(|n| {
let mut row = BindingRow::new();
row.insert(var.to_string(), Binding::Node(n.id));
row
})
.collect())
}
fn eval_expr(&self, txn: Txn, expr: &Expr, row: &BindingRow) -> Result<bool, QueryError> {
Ok(match expr {
Expr::And(l, r) => self.eval_expr(txn, l, row)? && self.eval_expr(txn, r, row)?,
Expr::Or(l, r) => self.eval_expr(txn, l, row)? || self.eval_expr(txn, r, row)?,
Expr::Not(e) => !self.eval_expr(txn, e, row)?,
Expr::Compare(pa, op, lit) => {
let prop_value = self.lookup_prop(txn, pa, row)?;
compare(&prop_value, *op, lit)
}
Expr::HasLabel(var, label) => {
let binding = row.get(var).ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
let Binding::Node(id) = binding else {
return Err(QueryError::UnboundVariable(var.clone()));
};
let node = GraphStore::get_node_in_txn(txn, *id)?;
node.is_some_and(|n| n.labels.iter().any(|l| l == label))
}
Expr::VarEq(a, b) => {
let ba = row.get(a).ok_or_else(|| QueryError::UnboundVariable(a.clone()))?;
let bb = row.get(b).ok_or_else(|| QueryError::UnboundVariable(b.clone()))?;
match (ba, bb) {
(Binding::Node(x), Binding::Node(y)) => x == y,
(Binding::Edge(x), Binding::Edge(y)) => x == y,
_ => false,
}
}
})
}
fn lookup_prop(
&self,
txn: Txn,
pa: &PropAccess,
row: &BindingRow,
) -> Result<Option<PropertyValue>, QueryError> {
let binding = row
.get(&pa.var)
.ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
match binding {
Binding::Node(id) => {
let node = GraphStore::get_node_in_txn(txn, *id)?;
Ok(node.and_then(|n| n.props.get(&pa.prop).cloned()))
}
Binding::Edge(id) => {
let edge = GraphStore::get_edge_in_txn(txn, *id)?;
Ok(edge.and_then(|e| e.props.get(&pa.prop).cloned()))
}
Binding::Value(_) | Binding::List(_) => Ok(None),
}
}
fn materialize_return(
&self,
txn: Txn,
items: &[ReturnItem],
rows: &[BindingRow],
) -> Result<QueryResult, QueryError> {
let columns = items
.iter()
.enumerate()
.map(|(i, item)| item.alias.clone().unwrap_or_else(|| default_column_name(&item.expr, i)))
.collect();
let out_rows = if !has_aggregate(items) {
let mut out_rows = Vec::with_capacity(rows.len());
for row in rows {
let mut out_row = Vec::with_capacity(items.len());
for item in items {
out_row.push(self.eval_return_expr(txn, &item.expr, row)?);
}
out_rows.push(out_row);
}
out_rows
} else {
validate_return_items(items)?;
let grouped = self.resolve_grouped_rows(txn, items, rows)?;
grouped
.into_iter()
.map(|bindings| {
bindings
.iter()
.map(|b| self.binding_to_value(txn, b))
.collect::<Result<Vec<_>, _>>()
})
.collect::<Result<Vec<_>, _>>()?
};
Ok(QueryResult {
columns,
rows: out_rows,
})
}
fn eval_return_expr(
&self,
txn: Txn,
expr: &ReturnExpr,
row: &BindingRow,
) -> Result<Value, QueryError> {
match expr {
ReturnExpr::Var(var) => {
let binding = row.get(var).ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
self.binding_to_value(txn, binding)
}
ReturnExpr::Prop(pa) => {
let value = self.lookup_prop(txn, pa, row)?;
Ok(match value {
Some(PropertyValue::Null) | None => Value::Null,
Some(pv) => Value::Property(pv),
})
}
ReturnExpr::Lit(lit) => Ok(match lit {
Literal::Null => Value::Null,
other => Value::Literal(other.clone()),
}),
ReturnExpr::Call { name, args, .. } => {
if is_aggregate_name(name) {
return Err(QueryError::Parse(format!(
"aggregate function '{name}' can only be used as a return item's top-level expression"
)));
}
let arg_values = args
.iter()
.map(|a| self.eval_return_expr(txn, a, row))
.collect::<Result<Vec<_>, _>>()?;
call_builtin(name, &arg_values)
}
ReturnExpr::CountStar => Err(QueryError::Parse(
"count(*) can only be used as a return item's top-level expression".into(),
)),
ReturnExpr::Case { test, whens, else_ } => {
let test_value = match test {
Some(t) => Some(self.eval_return_expr(txn, t, row)?),
None => None,
};
for (when, then) in whens {
let when_value = self.eval_return_expr(txn, when, row)?;
let matched = match &test_value {
Some(tv) => value_eq(tv, &when_value),
None => matches!(when_value, Value::Literal(Literal::Bool(true))),
};
if matched {
return self.eval_return_expr(txn, then, row);
}
}
match else_ {
Some(e) => self.eval_return_expr(txn, e, row),
None => Ok(Value::Null),
}
}
}
}
fn materialize_delete(
&self,
write_txn: &WriteTransaction,
vars: &[String],
rows: &[BindingRow],
detach: bool,
) -> Result<QueryResult, QueryError> {
let mut deleted_nodes = HashSet::new();
let mut deleted_edges = HashSet::new();
for row in rows {
for var in vars {
let binding = row.get(var).ok_or_else(|| QueryError::UnboundVariable(var.clone()))?;
match binding {
Binding::Node(id) => {
if deleted_nodes.insert(*id) {
GraphStore::delete_node_in_txn(write_txn, *id, detach)?;
}
}
Binding::Edge(id) => {
if deleted_edges.insert(*id) {
GraphStore::delete_edge_in_txn(write_txn, *id)?;
}
}
Binding::Value(_) | Binding::List(_) => {
return Err(QueryError::UnboundVariable(format!(
"'{var}' is a WITH-projected scalar, not a node/edge — DELETE needs a graph binding"
)))
}
}
}
}
Ok(QueryResult {
columns: vec![],
rows: vec![],
})
}
fn materialize_set(
&self,
write_txn: &WriteTransaction,
items: &[(PropAccess, Literal)],
rows: &[BindingRow],
) -> Result<QueryResult, QueryError> {
for row in rows {
for (pa, lit) in items {
let binding = row.get(&pa.var).ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
let value = literal_to_value(lit);
match binding {
Binding::Node(id) => {
GraphStore::set_node_prop_in_txn(write_txn, *id, &pa.prop, value)?;
}
Binding::Edge(id) => {
GraphStore::set_edge_prop_in_txn(write_txn, *id, &pa.prop, value)?;
}
Binding::Value(_) | Binding::List(_) => {
return Err(QueryError::UnboundVariable(format!(
"'{}' is a WITH-projected scalar, not a node/edge — SET needs a graph binding",
pa.var
)))
}
}
}
}
Ok(QueryResult {
columns: vec![],
rows: vec![],
})
}
}
fn is_read_only(stmt: &Statement) -> bool {
matches!(stmt, Statement::Match { tail: Tail::Return(_), .. })
}
fn require_write_txn(txn: Txn<'_>) -> &WriteTransaction {
let Txn::Write(write_txn) = txn else {
unreachable!(
"materialize_delete/materialize_set only reached via the write-dispatch path in \
Executor::execute — is_read_only(stmt) is false for any statement with a Delete/ \
DetachDelete/Set tail, so execute always opens a WriteTransaction for these"
)
};
write_txn
}
fn default_column_name(expr: &ReturnExpr, idx: usize) -> String {
match expr {
ReturnExpr::Var(v) => v.clone(),
ReturnExpr::Prop(pa) => format!("{}.{}", pa.var, pa.prop),
ReturnExpr::Lit(_) => format!("col{idx}"),
ReturnExpr::Call { name, .. } => format!("{name}(...)"),
ReturnExpr::CountStar => "count(*)".to_string(),
ReturnExpr::Case { .. } => format!("case{idx}"),
}
}
fn with_item_output_name((i, item): (usize, &ReturnItem)) -> String {
item.alias.clone().unwrap_or_else(|| default_column_name(&item.expr, i))
}
fn is_top_level_aggregate(expr: &ReturnExpr) -> bool {
match expr {
ReturnExpr::CountStar => true,
ReturnExpr::Call { name, .. } => is_aggregate_name(name),
_ => false,
}
}
fn contains_aggregate(expr: &ReturnExpr) -> bool {
match expr {
ReturnExpr::CountStar => true,
ReturnExpr::Call { name, args, .. } => is_aggregate_name(name) || args.iter().any(contains_aggregate),
ReturnExpr::Case { test, whens, else_ } => {
test.as_deref().is_some_and(contains_aggregate)
|| whens.iter().any(|(w, t)| contains_aggregate(w) || contains_aggregate(t))
|| else_.as_deref().is_some_and(contains_aggregate)
}
ReturnExpr::Var(_) | ReturnExpr::Prop(_) | ReturnExpr::Lit(_) => false,
}
}
fn has_aggregate(items: &[ReturnItem]) -> bool {
items.iter().any(|item| is_top_level_aggregate(&item.expr))
}
fn validate_return_items(items: &[ReturnItem]) -> Result<(), QueryError> {
for item in items {
match &item.expr {
ReturnExpr::CountStar => {}
ReturnExpr::Call { name, args, .. } if is_aggregate_name(name) => {
if args.len() != 1 {
return Err(QueryError::Parse(format!(
"{name}() takes exactly one argument (use count(*) for a row count with no argument)"
)));
}
if contains_aggregate(&args[0]) {
return Err(QueryError::Parse(format!(
"aggregate function '{name}' can't take another aggregate as an argument"
)));
}
}
other => {
if contains_aggregate(other) {
return Err(QueryError::Parse(
"an aggregate function must be a return item's entire expression, not nested inside \
another expression"
.into(),
));
}
}
}
}
Ok(())
}
fn binding_hash_key(b: &Binding) -> HashKey {
match b {
Binding::Node(id) => HashKey::Node(*id),
Binding::Edge(id) => HashKey::Edge(*id),
Binding::Value(pv) => property_value_hash_key(pv),
Binding::List(items) => HashKey::List(items.iter().map(value_hash_key).collect()),
}
}
fn value_to_binding(v: Value) -> Binding {
match v {
Value::List(items) => Binding::List(items),
other => Binding::Value(value_to_property_value(&other)),
}
}
fn compare_value(value: &Value, op: CompareOp, lit: &Literal) -> bool {
let prop = match value {
Value::Null => None,
Value::Property(pv) => Some(pv.clone()),
Value::Literal(l) => Some(literal_to_value(l)),
Value::Node(_) | Value::Edge(_) | Value::List(_) => None,
};
compare(&prop, op, lit)
}
fn value_to_property_value(v: &Value) -> PropertyValue {
match v {
Value::Null => PropertyValue::Null,
Value::Property(pv) => pv.clone(),
Value::Literal(lit) => literal_to_value(lit),
Value::Node(_) | Value::Edge(_) | Value::List(_) => PropertyValue::Null,
}
}
fn literal_to_value(lit: &Literal) -> PropertyValue {
match lit {
Literal::Int(i) => PropertyValue::Int(*i),
Literal::Float(f) => PropertyValue::Float(*f),
Literal::String(s) => PropertyValue::String(s.clone()),
Literal::Bool(b) => PropertyValue::Bool(*b),
Literal::Null => PropertyValue::Null,
Literal::Param(name) => {
unreachable!("param ${name} must be substituted before execution — see params::substitute_params")
}
}
}
fn literal_props_to_values(props: &[(String, Literal)]) -> BTreeMap<String, PropertyValue> {
props.iter().map(|(k, v)| (k.clone(), literal_to_value(v))).collect()
}
fn pattern_labels(labels: &[String]) -> Vec<&str> {
if labels.is_empty() {
vec!["Node"]
} else {
labels.iter().map(|s| s.as_str()).collect()
}
}
fn neighbors_for_direction(
txn: Txn,
node: NodeId,
direction: ExpandDirection,
rel_label: Option<&str>,
) -> Result<Vec<AdjEntry>, QueryError> {
Ok(match direction {
ExpandDirection::Out => GraphStore::neighbors_in_txn(txn, node, Direction::Out, rel_label)?,
ExpandDirection::In => GraphStore::neighbors_in_txn(txn, node, Direction::In, rel_label)?,
ExpandDirection::Either => {
let mut out = GraphStore::neighbors_in_txn(txn, node, Direction::Out, rel_label)?;
let inbound = GraphStore::neighbors_in_txn(txn, node, Direction::In, rel_label)?;
let seen: HashSet<EdgeId> = out.iter().map(|e| e.edge_id).collect();
out.extend(inbound.into_iter().filter(|e| !seen.contains(&e.edge_id)));
out
}
})
}
fn compare(prop: &Option<PropertyValue>, op: CompareOp, lit: &Literal) -> bool {
let Some(prop) = prop else { return false };
match (prop, lit) {
(PropertyValue::Int(a), Literal::Int(b)) => cmp_f64(op, *a as f64, *b as f64),
(PropertyValue::Int(a), Literal::Float(b)) => cmp_f64(op, *a as f64, *b),
(PropertyValue::Float(a), Literal::Float(b)) => cmp_f64(op, *a, *b),
(PropertyValue::Float(a), Literal::Int(b)) => cmp_f64(op, *a, *b as f64),
(PropertyValue::String(a), Literal::String(b)) => cmp_ord(op, a.as_str(), b.as_str()),
(PropertyValue::Bool(a), Literal::Bool(b)) => match op {
CompareOp::Eq => a == b,
CompareOp::Ne => a != b,
_ => false,
},
(PropertyValue::Null, Literal::Null) => matches!(op, CompareOp::Eq),
_ => false,
}
}
fn cmp_f64(op: CompareOp, a: f64, b: f64) -> bool {
match op {
CompareOp::Eq => a == b,
CompareOp::Ne => a != b,
CompareOp::Lt => a < b,
CompareOp::Le => a <= b,
CompareOp::Gt => a > b,
CompareOp::Ge => a >= b,
}
}
fn cmp_ord<T: PartialOrd>(op: CompareOp, a: T, b: T) -> bool {
match op {
CompareOp::Eq => a == b,
CompareOp::Ne => a != b,
CompareOp::Lt => a < b,
CompareOp::Le => a <= b,
CompareOp::Gt => a > b,
CompareOp::Ge => a >= b,
}
}
pub(crate) fn value_eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Null, Value::Null) => true,
(Value::Null, _) | (_, Value::Null) => false,
(Value::Property(pa), Value::Property(pb)) => pa == pb,
(Value::Literal(la), Value::Literal(lb)) => la == lb,
(Value::Property(pa), Value::Literal(lb)) => *pa == literal_to_value(lb),
(Value::Literal(la), Value::Property(pb)) => literal_to_value(la) == *pb,
(Value::Node(na), Value::Node(nb)) => na.id == nb.id,
(Value::Edge(ea), Value::Edge(eb)) => ea.id == eb.id,
(Value::List(la), Value::List(lb)) => la.len() == lb.len() && la.iter().zip(lb).all(|(x, y)| value_eq(x, y)),
_ => false,
}
}
fn call_builtin(name: &str, args: &[Value]) -> Result<Value, QueryError> {
match name.to_ascii_lowercase().as_str() {
"coalesce" => Ok(args
.iter()
.find(|v| !matches!(v, Value::Null))
.cloned()
.unwrap_or(Value::Null)),
"tointeger" => Ok(args.first().map(to_integer).unwrap_or(Value::Null)),
other => Err(QueryError::Parse(format!("unknown function: {other}"))),
}
}
fn to_integer(v: &Value) -> Value {
let as_str_parse = |s: &str| match s.trim().parse::<i64>() {
Ok(i) => Value::Property(PropertyValue::Int(i)),
Err(_) => Value::Null,
};
match v {
Value::Property(PropertyValue::Int(i)) => Value::Property(PropertyValue::Int(*i)),
Value::Property(PropertyValue::Float(f)) => Value::Property(PropertyValue::Int(*f as i64)),
Value::Property(PropertyValue::String(s)) => as_str_parse(s),
Value::Literal(Literal::Int(i)) => Value::Property(PropertyValue::Int(*i)),
Value::Literal(Literal::Float(f)) => Value::Property(PropertyValue::Int(*f as i64)),
Value::Literal(Literal::String(s)) => as_str_parse(s),
_ => Value::Null,
}
}
fn apply_order_by(
rows: Vec<Vec<Value>>,
columns: &[String],
order_by: &[(ReturnExpr, SortDir)],
) -> Result<Vec<Vec<Value>>, QueryError> {
let mut keyed: Vec<(Vec<Value>, Vec<Value>)> = Vec::with_capacity(rows.len());
for row in rows {
let row_map: HashMap<String, Value> = columns.iter().cloned().zip(row.iter().cloned()).collect();
let keys = order_by
.iter()
.map(|(expr, _)| eval_projected_expr(expr, &row_map))
.collect::<Result<Vec<_>, _>>()?;
keyed.push((keys, row));
}
keyed.sort_by(|(ka, _), (kb, _)| {
for (i, (_, dir)) in order_by.iter().enumerate() {
let ord = compare_with_dir(&ka[i], &kb[i], *dir);
if ord != std::cmp::Ordering::Equal {
return ord;
}
}
std::cmp::Ordering::Equal
});
Ok(keyed.into_iter().map(|(_, row)| row).collect())
}
fn eval_projected_expr(expr: &ReturnExpr, row: &HashMap<String, Value>) -> Result<Value, QueryError> {
match expr {
ReturnExpr::Var(name) => row
.get(name)
.cloned()
.ok_or_else(|| QueryError::UnboundVariable(name.clone())),
ReturnExpr::Prop(pa) => {
let base = row
.get(&pa.var)
.ok_or_else(|| QueryError::UnboundVariable(pa.var.clone()))?;
let pv = match base {
Value::Node(n) => n.props.get(&pa.prop).cloned(),
Value::Edge(e) => e.props.get(&pa.prop).cloned(),
_ => None,
};
Ok(match pv {
Some(PropertyValue::Null) | None => Value::Null,
Some(v) => Value::Property(v),
})
}
ReturnExpr::Lit(lit) => Ok(match lit {
Literal::Null => Value::Null,
other => Value::Literal(other.clone()),
}),
ReturnExpr::Call { name, args, .. } => {
if is_aggregate_name(name) {
return Err(QueryError::Parse(format!(
"aggregate function '{name}' can only be used as a return item's top-level expression"
)));
}
let arg_values = args
.iter()
.map(|a| eval_projected_expr(a, row))
.collect::<Result<Vec<_>, _>>()?;
call_builtin(name, &arg_values)
}
ReturnExpr::CountStar => Err(QueryError::Parse(
"count(*) can only be used as a return item's top-level expression".into(),
)),
ReturnExpr::Case { test, whens, else_ } => {
let test_value = match test {
Some(t) => Some(eval_projected_expr(t, row)?),
None => None,
};
for (when, then) in whens {
let when_value = eval_projected_expr(when, row)?;
let matched = match &test_value {
Some(tv) => value_eq(tv, &when_value),
None => matches!(when_value, Value::Literal(Literal::Bool(true))),
};
if matched {
return eval_projected_expr(then, row);
}
}
match else_ {
Some(e) => eval_projected_expr(e, row),
None => Ok(Value::Null),
}
}
}
}
fn compare_with_dir(a: &Value, b: &Value, dir: SortDir) -> std::cmp::Ordering {
use std::cmp::Ordering;
let a_null = matches!(a, Value::Null);
let b_null = matches!(b, Value::Null);
match (a_null, b_null) {
(true, true) => return Ordering::Equal,
(true, false) => return Ordering::Greater,
(false, true) => return Ordering::Less,
(false, false) => {}
}
let ord = compare_non_null(a, b);
if dir == SortDir::Desc {
ord.reverse()
} else {
ord
}
}
fn compare_non_null(a: &Value, b: &Value) -> std::cmp::Ordering {
use std::cmp::Ordering;
let pa = value_to_comparable(a);
let pb = value_to_comparable(b);
match (pa, pb) {
(Some(PropertyValue::Int(x)), Some(PropertyValue::Int(y))) => x.cmp(&y),
(Some(PropertyValue::Int(x)), Some(PropertyValue::Float(y))) => {
(x as f64).partial_cmp(&y).unwrap_or(Ordering::Equal)
}
(Some(PropertyValue::Float(x)), Some(PropertyValue::Int(y))) => {
x.partial_cmp(&(y as f64)).unwrap_or(Ordering::Equal)
}
(Some(PropertyValue::Float(x)), Some(PropertyValue::Float(y))) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
(Some(PropertyValue::String(x)), Some(PropertyValue::String(y))) => x.cmp(&y),
(Some(PropertyValue::Bool(x)), Some(PropertyValue::Bool(y))) => x.cmp(&y),
_ => Ordering::Equal,
}
}
fn value_to_comparable(v: &Value) -> Option<PropertyValue> {
match v {
Value::Property(pv) => Some(pv.clone()),
Value::Literal(lit) => Some(literal_to_value(lit)),
_ => None,
}
}
pub(crate) fn comparable_ordering(a: &Value, b: &Value) -> Option<std::cmp::Ordering> {
use std::cmp::Ordering;
let pa = value_to_comparable(a)?;
let pb = value_to_comparable(b)?;
Some(match (pa, pb) {
(PropertyValue::Int(x), PropertyValue::Int(y)) => x.cmp(&y),
(PropertyValue::Int(x), PropertyValue::Float(y)) => (x as f64).partial_cmp(&y).unwrap_or(Ordering::Equal),
(PropertyValue::Float(x), PropertyValue::Int(y)) => x.partial_cmp(&(y as f64)).unwrap_or(Ordering::Equal),
(PropertyValue::Float(x), PropertyValue::Float(y)) => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
(PropertyValue::String(x), PropertyValue::String(y)) => x.cmp(&y),
(PropertyValue::Bool(x), PropertyValue::Bool(y)) => x.cmp(&y),
_ => return None,
})
}