Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions graph/src/planner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,113 @@ pub fn subtree_contains(
.any(|n| predicate(n.data()))
}

/// Returns true if a QueryExpr tree contains any non-deterministic function call.
fn expr_has_non_deterministic(expr: &DynTree<ExprIR<Variable>>) -> bool {
expr.root()
.walk_with(&mut Traversal.bfs().over_nodes())
.any(|n| matches!(n.data(), ExprIR::FuncInvocation(func) if func.non_deterministic))
}

/// Returns true if a SetItem references any non-deterministic expression.
fn set_item_has_non_deterministic(item: &SetItem<Arc<String>, Variable>) -> bool {
match item {
SetItem::Attribute { target, value, .. } => {
expr_has_non_deterministic(target) || expr_has_non_deterministic(value)
}
SetItem::Label { .. } => false,
}
}

/// Returns true if a QueryGraph (CREATE/MERGE pattern) contains non-deterministic expressions.
fn query_graph_has_non_deterministic(qg: &QueryGraph<Arc<String>, Arc<String>, Variable>) -> bool {
for node in qg.nodes() {
if expr_has_non_deterministic(&node.attrs) {
return true;
}
}
for rel in qg.relationships() {
if expr_has_non_deterministic(&rel.attrs) {
return true;
}
}
false
}

/// Returns true if an IndexQuery tree contains any non-deterministic function call.
fn index_query_has_non_deterministic(query: &IndexQuery<QueryExpr<Variable>>) -> bool {
match query {
IndexQuery::Range { min, max, .. } => {
min.as_ref().is_some_and(|e| expr_has_non_deterministic(e))
|| max.as_ref().is_some_and(|e| expr_has_non_deterministic(e))
}
IndexQuery::And(queries) | IndexQuery::Or(queries) => {
queries.iter().any(index_query_has_non_deterministic)
}
IndexQuery::Point { point, radius, .. } => {
expr_has_non_deterministic(point) || expr_has_non_deterministic(radius)
}
IndexQuery::InList { list, .. } => expr_has_non_deterministic(list),
IndexQuery::Equal { value, .. } | IndexQuery::ArrayContains { value, .. } => {
expr_has_non_deterministic(value)
}
}
}

/// Returns true if the execution plan contains any non-deterministic function call.
#[must_use]
pub fn plan_is_non_deterministic(plan: &DynTree<IR>) -> bool {
plan.root()
.walk_with(&mut Traversal.bfs().over_nodes())
.any(|node| match node.data() {
IR::Create(qg) => query_graph_has_non_deterministic(qg),
IR::Merge {
pattern,
on_create,
on_match,
} => {
query_graph_has_non_deterministic(pattern)
|| on_create.iter().any(set_item_has_non_deterministic)
|| on_match.iter().any(set_item_has_non_deterministic)
}
IR::Set(items) => items.iter().any(set_item_has_non_deterministic),
IR::Remove(exprs) | IR::Delete { exprs, .. } => {
exprs.iter().any(|e| expr_has_non_deterministic(e))
}
IR::Unwind { expr, .. }
| IR::Filter(expr)
| IR::Skip(expr)
| IR::Limit(expr)
| IR::ForEach { list: expr, .. } => expr_has_non_deterministic(expr),
IR::Sort(exprs) => exprs.iter().any(|(e, _)| expr_has_non_deterministic(e)),
IR::Project { exprs, .. } => exprs.iter().any(|(_, e)| expr_has_non_deterministic(e)),
IR::Aggregate {
keys, aggregations, ..
} => {
keys.iter().any(|(_, e)| expr_has_non_deterministic(e))
|| aggregations
.iter()
.any(|(_, e)| expr_has_non_deterministic(e))
}
IR::ProcedureCall { args, .. } => args.iter().any(|e| expr_has_non_deterministic(e)),
IR::LoadCsv {
file_path,
delimiter,
..
} => expr_has_non_deterministic(file_path) || expr_has_non_deterministic(delimiter),
IR::ValueHashJoin { lhs_exp, rhs_exp } => {
expr_has_non_deterministic(lhs_exp) || expr_has_non_deterministic(rhs_exp)
}
IR::NodeByIndexScan { query, .. } => index_query_has_non_deterministic(query),
IR::NodeByFulltextScan { label, query, .. } => {
expr_has_non_deterministic(label) || expr_has_non_deterministic(query)
}
IR::NodeByLabelAndIdScan { filter, .. } | IR::NodeByIdSeek { filter, .. } => {
filter.iter().any(|(e, _)| expr_has_non_deterministic(e))
}
_ => false,
})
}

/// Formats a relationship for CondTraverse/ExpandInto display.
/// Shows node labels and hides anonymous edge aliases.
fn fmt_rel_with_labels(rel: &QueryRelationship<Arc<String>, Arc<String>, Variable>) -> String {
Expand Down
2 changes: 2 additions & 0 deletions graph/src/runtime/functions/aggregation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ pub fn register(funcs: &mut Functions) {
"percentileCont",
percentile,
false,
false,
vec![
Type::Union(vec![Type::Int, Type::Float, Type::Null]),
Type::Union(vec![Type::Int, Type::Float]),
Expand Down Expand Up @@ -307,6 +308,7 @@ pub fn register(funcs: &mut Functions) {
"stDevP",
stdev,
false,
false,
vec![Type::Union(vec![Type::Int, Type::Float, Type::Null])],
FnType::Aggregation {
initial: Value::List(Arc::new(thin_vec![
Expand Down
6 changes: 5 additions & 1 deletion graph/src/runtime/functions/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
use super::{FnType, Functions, Type};
use crate::runtime::{runtime::Runtime, value::Value};
use std::sync::Arc;
use thin_vec::{ThinVec, thin_vec};
use thin_vec::ThinVec;

pub fn register(funcs: &mut Functions) {
cypher_fn!(funcs, "tointeger",
Expand Down Expand Up @@ -84,6 +84,7 @@ pub fn register(funcs: &mut Functions) {
"toIntegerOrNull",
value_to_integer,
false,
false,
vec![Type::Any],
FnType::Function,
Type::Union(vec![Type::Int, Type::Null]),
Expand All @@ -105,6 +106,7 @@ pub fn register(funcs: &mut Functions) {
"toFloatOrNull",
value_to_float,
false,
false,
vec![Type::Any],
FnType::Function,
Type::Union(vec![Type::Float, Type::Null]),
Expand Down Expand Up @@ -140,6 +142,7 @@ pub fn register(funcs: &mut Functions) {
"tostringornull",
value_to_string,
false,
false,
vec![Type::Any],
FnType::Function,
Type::Union(vec![Type::String, Type::Null]),
Expand Down Expand Up @@ -197,6 +200,7 @@ pub fn register(funcs: &mut Functions) {
"toBooleanOrNull",
to_boolean,
false,
false,
vec![Type::Any],
FnType::Function,
Type::Union(vec![Type::Bool, Type::Null]),
Expand Down
2 changes: 2 additions & 0 deletions graph/src/runtime/functions/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ pub fn register(funcs: &mut Functions) {
cypher_fn!(funcs, "randomUUID",
args: [],
ret: Type::String,
non_deterministic,
fn random_uuid(_, _args) {
// Generate 16 random bytes (128 bits)
let mut rng = rand::rng();
Expand Down Expand Up @@ -184,6 +185,7 @@ pub fn register(funcs: &mut Functions) {
cypher_fn!(funcs, "rand",
args: [],
ret: Type::Float,
non_deterministic,
#[allow(clippy::needless_pass_by_value)]
fn rand(_, args) {
debug_assert!(args.is_empty());
Expand Down
70 changes: 70 additions & 0 deletions graph/src/runtime/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,58 @@
/// | `procedure:` | read-only procedure | `Procedure(yields)` |
/// | `write procedure:` | write procedure | `Procedure(yields)` |
macro_rules! cypher_fn {
// ── Non-deterministic scalar function (fixed args) ──
($funcs:ident, $name:expr,
args: [$($arg:expr),* $(,)?],
ret: $ret:expr,
non_deterministic,
$(#[$attr:meta])*
fn $fn_name:ident($rt:pat, $args:pat) $body:block
) => {
$(#[$attr])*
fn $fn_name(
$rt: &Runtime,
$args: ThinVec<Value>,
) -> Result<Value, String>
$body

$funcs.add(
$name,
$fn_name,
false,
true,
vec![$($arg),*],
FnType::Function,
$ret,
);
};

// ── Non-deterministic variable-length argument function ──
($funcs:ident, $name:expr,
var_arg: $arg_type:expr,
ret: $ret:expr,
non_deterministic,
$(#[$attr:meta])*
fn $fn_name:ident($rt:pat, $args:pat) $body:block
) => {
$(#[$attr])*
fn $fn_name(
$rt: &Runtime,
$args: ThinVec<Value>,
) -> Result<Value, String>
$body

$funcs.add_var_len(
$name,
$fn_name,
false,
true,
$arg_type,
FnType::Function,
$ret,
);
};

// ── Scalar function (FnType::Function, write=false, fixed args) ──
($funcs:ident, $name:expr,
args: [$($arg:expr),* $(,)?],
Expand All @@ -90,6 +142,7 @@ macro_rules! cypher_fn {
$name,
$fn_name,
false,
false,
vec![$($arg),*],
FnType::Function,
$ret,
Expand All @@ -114,6 +167,7 @@ macro_rules! cypher_fn {
$name,
$fn_name,
false,
false,
$arg_type,
FnType::Function,
$ret,
Expand All @@ -139,6 +193,7 @@ macro_rules! cypher_fn {
$name,
$fn_name,
false,
false,
vec![$($arg),*],
FnType::Aggregation { initial: $init, finalizer: None },
$ret,
Expand All @@ -165,6 +220,7 @@ macro_rules! cypher_fn {
$name,
$fn_name,
false,
false,
vec![$($arg),*],
FnType::Aggregation { initial: $init, finalizer: Some(Box::new($finalizer)) },
$ret,
Expand All @@ -190,6 +246,7 @@ macro_rules! cypher_fn {
$name,
$fn_name,
false,
false,
vec![$($arg),*],
FnType::Internal,
$ret,
Expand All @@ -215,6 +272,7 @@ macro_rules! cypher_fn {
$name,
$fn_name,
false,
false,
vec![$($arg),*],
FnType::Procedure(vec![$(String::from($yield_col)),*]),
$ret,
Expand All @@ -240,6 +298,7 @@ macro_rules! cypher_fn {
$name,
$fn_name,
true,
false,
vec![$($arg),*],
FnType::Procedure(vec![$(String::from($yield_col)),*]),
$ret,
Expand Down Expand Up @@ -457,6 +516,7 @@ pub struct GraphFn {
pub name: String,
pub func: RuntimeFn,
pub write: bool,
pub non_deterministic: bool,
pub args_type: FnArguments,
pub fn_type: FnType,
pub ret_type: Type,
Expand All @@ -470,6 +530,7 @@ impl Debug for GraphFn {
f.debug_struct("GraphFn")
.field("name", &self.name)
.field("write", &self.write)
.field("non_deterministic", &self.non_deterministic)
.field("args_type", &self.args_type)
.field("fn_type", &self.fn_type)
.field("ret_type", &self.ret_type)
Expand All @@ -483,6 +544,7 @@ impl GraphFn {
name: &str,
func: fn(&Runtime, ThinVec<Value>) -> Result<Value, String>,
write: bool,
non_deterministic: bool,
args_type: FnArguments,
fn_type: FnType,
ret_type: Type,
Expand All @@ -491,6 +553,7 @@ impl GraphFn {
name: String::from(name),
func: Arc::new(func),
write,
non_deterministic,
args_type,
fn_type,
ret_type,
Expand All @@ -506,6 +569,7 @@ impl GraphFn {
crate::udf::js_context::call_udf_bridge(&udf_name, rt, &args)
}),
write: false,
non_deterministic: false,
args_type: FnArguments::VarLength(Type::Any),
fn_type: FnType::Udf,
ret_type: Type::Any,
Expand Down Expand Up @@ -622,11 +686,13 @@ impl Functions {
Self::default()
}

#[allow(clippy::too_many_arguments)]
pub fn add(
&mut self,
name: &str,
func: fn(&Runtime, ThinVec<Value>) -> Result<Value, String>,
write: bool,
non_deterministic: bool,
args_type: Vec<Type>,
fn_type: FnType,
ret_type: Type,
Expand All @@ -640,18 +706,21 @@ impl Functions {
name,
func,
write,
non_deterministic,
FnArguments::Fixed(args_type),
fn_type,
ret_type,
));
self.functions.insert(lower_name, graph_fn);
}

#[allow(clippy::too_many_arguments)]
pub fn add_var_len(
&mut self,
name: &str,
func: fn(&Runtime, ThinVec<Value>) -> Result<Value, String>,
write: bool,
non_deterministic: bool,
arg_type: Type,
fn_type: FnType,
ret_type: Type,
Expand All @@ -665,6 +734,7 @@ impl Functions {
&name,
func,
write,
non_deterministic,
FnArguments::VarLength(arg_type),
fn_type,
ret_type,
Expand Down
Loading
Loading