use super::functions::FunctionRegistry;
use super::matcher::{Bindings, PatternMatcher, edn_to_entity_id, edn_to_value};
use super::rules::RuleRegistry;
use super::types::{AttributeSpec, EdnValue, Pattern, Rule, WhereClause};
use crate::graph::FactStorage;
use crate::graph::types::{Fact, Value};
use crate::storage::index::encode_value;
use anyhow::{Result, anyhow};
use std::collections::BTreeSet;
use std::sync::{Arc, RwLock};
#[allow(dead_code)]
pub const DEFAULT_MAX_ITERATIONS: usize = 1000;
pub const DEFAULT_MAX_DERIVED_FACTS: usize = 100_000;
pub const DEFAULT_MAX_RESULTS: usize = 1_000_000;
pub struct RecursiveEvaluator {
storage: FactStorage,
rules: Arc<RwLock<RuleRegistry>>,
functions: Arc<RwLock<FunctionRegistry>>,
max_iterations: usize,
max_derived_facts: usize,
max_results: usize,
}
impl RecursiveEvaluator {
pub fn new(
storage: FactStorage,
rules: Arc<RwLock<RuleRegistry>>,
functions: Arc<RwLock<FunctionRegistry>>,
max_iterations: usize,
max_derived_facts: usize,
max_results: usize,
) -> Self {
RecursiveEvaluator {
storage,
rules,
functions,
max_iterations,
max_derived_facts,
max_results,
}
}
pub fn evaluate_recursive_rules(&self, predicates: &[String]) -> Result<FactStorage> {
let base_facts = self.storage.get_asserted_facts()?;
let derived = FactStorage::new();
for fact in &base_facts {
derived.transact(
vec![(fact.entity, fact.attribute.clone(), fact.value.clone())],
None,
)?;
}
let mut seen_facts: BTreeSet<(uuid::Uuid, String, Vec<u8>)> = base_facts
.iter()
.map(|f| {
let encoded = encode_value(&f.value);
(f.entity, f.attribute.clone(), encoded)
})
.collect();
let mut iteration = 0;
loop {
iteration += 1;
if iteration > self.max_iterations {
return Err(anyhow!(
"Max iterations ({}) exceeded. Possible infinite recursion or cycle in rules.",
self.max_iterations
));
}
let new_facts = self.evaluate_iteration(predicates, &derived)?;
if new_facts.len() > self.max_derived_facts {
return Err(anyhow!(
"Max derived facts per iteration ({}) exceeded. Rule may be generating too many facts.",
self.max_derived_facts
));
}
let mut delta = Vec::new();
for fact in new_facts {
let encoded = encode_value(&fact.value);
let key = (fact.entity, fact.attribute.clone(), encoded);
if !seen_facts.contains(&key) {
if seen_facts.len() >= self.max_results {
return Err(anyhow!(
"Max query results ({}) exceeded.",
self.max_results
));
}
seen_facts.insert(key);
delta.push(fact);
}
}
if delta.is_empty() {
break;
}
for fact in delta {
derived.transact(
vec![(fact.entity, fact.attribute.clone(), fact.value.clone())],
None,
)?;
}
}
Ok(derived)
}
fn evaluate_iteration(
&self,
predicates: &[String],
current_facts: &FactStorage,
) -> Result<Vec<Fact>> {
let mut new_facts = Vec::new();
let registry = self.rules.read().expect("lock poisoned");
for predicate in predicates {
let rules = registry.get_rules(predicate);
for rule in rules {
let derived = self.evaluate_rule(&rule, current_facts)?;
new_facts.extend(derived);
}
}
Ok(new_facts)
}
fn evaluate_rule(&self, rule: &Rule, current_facts: &FactStorage) -> Result<Vec<Fact>> {
let mut derived = Vec::new();
let mut patterns = Vec::new();
let mut expr_clauses: Vec<&WhereClause> = Vec::new();
for clause in &rule.body {
match clause {
WhereClause::Pattern(p) => {
patterns.push(p.clone());
}
WhereClause::RuleInvocation { predicate, args } => {
let list: Vec<EdnValue> = std::iter::once(EdnValue::Symbol(predicate.clone()))
.chain(args.iter().cloned())
.collect();
let pattern = self.rule_invocation_to_pattern(&list)?;
patterns.push(pattern);
}
WhereClause::Not(_) => {
return Err(anyhow!(
"WhereClause::Not in evaluate_rule: use StratifiedEvaluator for rules with negation"
));
}
WhereClause::NotJoin { .. } => {
return Err(anyhow!(
"WhereClause::NotJoin in evaluate_rule: use StratifiedEvaluator for rules with negation"
));
}
WhereClause::Expr { .. } => {
expr_clauses.push(clause);
}
WhereClause::Or(_) | WhereClause::OrJoin { .. } => {
return Err(anyhow!(
"WhereClause::Or/OrJoin in evaluate_rule: not yet implemented"
));
}
}
}
if patterns.is_empty() && expr_clauses.is_empty() {
return Ok(derived);
}
let matcher = PatternMatcher::new(current_facts.clone());
let bindings = if patterns.is_empty() {
vec![Bindings::new()]
} else {
matcher.match_patterns(&patterns)
};
let bindings = apply_expr_clauses_in_evaluator(
bindings,
&expr_clauses,
&self.functions.read().expect("lock poisoned"),
);
for binding in bindings {
let fact = self.instantiate_head(&rule.head, &binding)?;
derived.push(fact);
}
Ok(derived)
}
fn rule_invocation_to_pattern(&self, list: &[EdnValue]) -> Result<Pattern> {
if list.is_empty() {
return Err(anyhow!("Rule invocation cannot be empty"));
}
let predicate = match &list[0] {
EdnValue::Symbol(s) => s.clone(),
_ => {
return Err(anyhow!(
"Rule invocation must start with predicate name (symbol)"
));
}
};
let args: Vec<EdnValue> = list[1..].to_vec();
rule_invocation_to_pattern(&predicate, &args)
}
fn instantiate_head(&self, head: &[EdnValue], binding: &Bindings) -> Result<Fact> {
if head.len() < 2 {
return Err(anyhow!(
"Rule head must have at least 2 elements: (predicate ?arg1)"
));
}
let predicate = match &head[0] {
EdnValue::Symbol(s) => s.clone(),
_ => return Err(anyhow!("Rule head must start with predicate name (symbol)")),
};
let entity_edn = self.substitute_variable(&head[1], binding)?;
let entity = edn_to_entity_id(&entity_edn)
.map_err(|e| anyhow!("Failed to convert entity: {}", e))?;
let value = if head.len() >= 3 {
let value_edn = self.substitute_variable(&head[2], binding)?;
edn_to_value(&value_edn).map_err(|e| anyhow!("Failed to convert value: {}", e))?
} else {
crate::graph::types::Value::Boolean(true)
};
let attribute = format!(":{}", predicate);
Ok(Fact::new(entity, attribute, value, 0))
}
fn substitute_variable(&self, edn: &EdnValue, binding: &Bindings) -> Result<EdnValue> {
match edn {
EdnValue::Symbol(s) if s.starts_with('?') => {
if let Some(value) = binding.get(s) {
Ok(value_to_edn(value))
} else {
Err(anyhow!("Unbound variable in rule head: {}", s))
}
}
_ => Ok(edn.clone()), }
}
pub fn instantiate_head_public(&self, head: &[EdnValue], binding: &Bindings) -> Result<Fact> {
self.instantiate_head(head, binding)
}
}
pub fn value_to_edn(value: &Value) -> EdnValue {
match value {
Value::String(s) => EdnValue::String(s.clone()),
Value::Integer(i) => EdnValue::Integer(*i),
Value::Float(f) => EdnValue::Float(*f),
Value::Boolean(b) => EdnValue::Boolean(*b),
Value::Ref(uuid) => EdnValue::Uuid(*uuid),
Value::Keyword(k) => EdnValue::Keyword(k.clone()),
Value::Null => EdnValue::Symbol("nil".to_string()),
}
}
pub fn substitute_pattern(pattern: &Pattern, binding: &Bindings) -> Pattern {
let attribute = match &pattern.attribute {
AttributeSpec::Real(edn) => AttributeSpec::Real(substitute_value(edn, binding)),
AttributeSpec::Pseudo(p) => AttributeSpec::Pseudo(p.clone()),
};
Pattern {
entity: substitute_value(&pattern.entity, binding),
attribute,
value: substitute_value(&pattern.value, binding),
valid_from: pattern.valid_from,
valid_to: pattern.valid_to,
}
}
pub fn substitute_value(value: &EdnValue, binding: &Bindings) -> EdnValue {
if let Some(var) = value.as_variable() {
binding
.get(var)
.map(value_to_edn)
.unwrap_or_else(|| value.clone())
} else {
value.clone()
}
}
pub(crate) fn rule_invocation_to_pattern(predicate: &str, args: &[EdnValue]) -> Result<Pattern> {
match args.len() {
1 => Ok(Pattern::new(
args[0].clone(),
EdnValue::Keyword(format!(":{}", predicate)),
EdnValue::Symbol("?_rule_value".to_string()),
)),
2 => Ok(Pattern::new(
args[0].clone(),
EdnValue::Keyword(format!(":{}", predicate)),
args[1].clone(),
)),
n => Err(anyhow!(
"Rule invocation '{}' must have 1 or 2 arguments, got {}",
predicate,
n
)),
}
}
pub fn evaluate_not_join(
join_vars: &[String],
clauses: &[WhereClause],
binding: &Bindings,
storage: Arc<[Fact]>,
functions: &FunctionRegistry,
) -> bool {
let partial: Bindings = join_vars
.iter()
.filter_map(|v| binding.get(v.as_str()).map(|val| (v.clone(), val.clone())))
.collect();
let substituted: Vec<Pattern> = clauses
.iter()
.filter_map(|c| match c {
WhereClause::Pattern(p) => Some(substitute_pattern(p, &partial)),
WhereClause::RuleInvocation { predicate, args } => {
rule_invocation_to_pattern(predicate, args)
.ok()
.map(|p| substitute_pattern(&p, &partial))
}
_ => None,
})
.collect();
let expr_clauses: Vec<&WhereClause> = clauses
.iter()
.filter(|c| matches!(c, WhereClause::Expr { .. }))
.collect();
let matcher = PatternMatcher::from_slice(storage.clone());
let mut not_bindings: Vec<Bindings> = if substituted.is_empty() {
vec![partial.clone()]
} else {
matcher
.match_patterns(&substituted)
.into_iter()
.map(|mut nb| {
for (k, v) in &partial {
nb.entry(k.clone()).or_insert_with(|| v.clone());
}
nb
})
.collect()
};
not_bindings = apply_expr_clauses_in_evaluator(not_bindings, &expr_clauses, functions);
!not_bindings.is_empty()
}
fn apply_expr_clauses_in_evaluator(
bindings: Vec<Bindings>,
expr_clauses: &[&WhereClause],
registry: &FunctionRegistry,
) -> Vec<Bindings> {
use crate::query::datalog::executor::{eval_expr, is_truthy};
bindings
.into_iter()
.filter_map(|mut b| {
for clause in expr_clauses {
if let WhereClause::Expr { expr, binding: out } = clause {
match eval_expr(expr, &b, Some(registry)) {
Ok(value) => match out {
None => {
if !is_truthy(&value) {
return None;
}
}
Some(var) => {
b.insert(var.clone(), value);
}
},
Err(_) => return None,
}
}
}
Some(b)
})
.collect()
}
pub struct StratifiedEvaluator {
storage: FactStorage,
rules: Arc<RwLock<RuleRegistry>>,
functions: Arc<RwLock<FunctionRegistry>>,
max_iterations: usize,
max_derived_facts: usize,
max_results: usize,
}
impl StratifiedEvaluator {
pub fn new(
storage: FactStorage,
rules: Arc<RwLock<RuleRegistry>>,
functions: Arc<RwLock<FunctionRegistry>>,
max_iterations: usize,
max_derived_facts: usize,
max_results: usize,
) -> Self {
StratifiedEvaluator {
storage,
rules,
functions,
max_iterations,
max_derived_facts,
max_results,
}
}
pub fn evaluate(&self, predicates: &[String]) -> Result<FactStorage> {
use crate::query::datalog::stratification::DependencyGraph;
let registry = self.rules.read().expect("lock poisoned");
let graph = DependencyGraph::from_rules(®istry);
let strata = graph.stratify()?;
let mut all_preds: Vec<String> = predicates.to_vec();
{
let mut i = 0;
while i < all_preds.len() {
let pred = all_preds[i].clone();
for rule in registry.get_rules(&pred) {
for clause in &rule.body {
for dep in clause.rule_invocations() {
if !all_preds.contains(&dep.to_string()) {
all_preds.push(dep.to_string());
}
}
}
}
i += 1;
}
}
let max_stratum = all_preds
.iter()
.map(|p| *strata.get(p).unwrap_or(&0))
.max()
.unwrap_or(0);
drop(registry);
let accumulated = self.storage.clone();
for stratum in 0..=max_stratum {
let registry = self.rules.read().expect("lock poisoned");
let stratum_preds: Vec<String> = all_preds
.iter()
.filter(|p| *strata.get(*p).unwrap_or(&0) == stratum)
.cloned()
.collect();
if stratum_preds.is_empty() {
continue;
}
let mut positive_rules: Vec<(String, Rule)> = Vec::new();
let mut mixed_rules: Vec<(String, Rule)> = Vec::new();
for pred in &stratum_preds {
for rule in registry.get_rules(pred) {
let has_not = rule.body.iter().any(|c| {
matches!(
c,
WhereClause::Not(_)
| WhereClause::NotJoin { .. }
| WhereClause::Or(_)
| WhereClause::OrJoin { .. }
)
});
if has_not {
mixed_rules.push((pred.clone(), rule));
} else {
positive_rules.push((pred.clone(), rule));
}
}
}
drop(registry);
if !positive_rules.is_empty() {
let mut sub_registry = RuleRegistry::new();
for (pred, rule) in &positive_rules {
sub_registry.register_rule_unchecked(pred.clone(), rule.clone());
}
let sub_rules = Arc::new(RwLock::new(sub_registry));
let sub_eval = RecursiveEvaluator::new(
accumulated.clone(),
sub_rules,
self.functions.clone(),
self.max_iterations,
self.max_derived_facts,
self.max_results,
);
let derived = sub_eval.evaluate_recursive_rules(&stratum_preds)?;
let existing: Vec<(uuid::Uuid, String, Value)> = accumulated
.get_asserted_facts()?
.into_iter()
.map(|f| (f.entity, f.attribute, f.value))
.collect();
for fact in derived.get_asserted_facts()? {
let key = (fact.entity, fact.attribute.clone(), fact.value.clone());
if !existing.iter().any(|e| e == &key) {
let _ = accumulated.load_fact(fact);
}
}
accumulated.restore_tx_counter()?;
}
for (_pred, rule) in &mixed_rules {
let positive_patterns: Vec<Pattern> = rule
.body
.iter()
.filter_map(|c| match c {
WhereClause::Pattern(p) => Some(p.clone()),
WhereClause::RuleInvocation { predicate, args } => match args.len() {
1 => Some(Pattern::new(
args[0].clone(),
EdnValue::Keyword(format!(":{}", predicate)),
EdnValue::Symbol("?_rule_value".to_string()),
)),
2 => Some(Pattern::new(
args[0].clone(),
EdnValue::Keyword(format!(":{}", predicate)),
args[1].clone(),
)),
_ => None,
},
WhereClause::Not(_) | WhereClause::NotJoin { .. } => None,
WhereClause::Expr { .. } => None,
WhereClause::Or(_) | WhereClause::OrJoin { .. } => None, })
.collect();
let not_clauses: Vec<Vec<WhereClause>> = rule
.body
.iter()
.filter_map(|c| match c {
WhereClause::Not(inner) => Some(inner.clone()),
_ => None,
})
.collect();
let not_join_clauses: Vec<(Vec<String>, Vec<WhereClause>)> = rule
.body
.iter()
.filter_map(|c| match c {
WhereClause::NotJoin { join_vars, clauses } => {
Some((join_vars.clone(), clauses.clone()))
}
_ => None,
})
.collect();
let body_expr_clauses: Vec<&WhereClause> = rule
.body
.iter()
.filter(|c| matches!(c, WhereClause::Expr { .. }))
.collect();
let accumulated_facts: Arc<[Fact]> =
Arc::from(accumulated.get_asserted_facts().unwrap_or_default());
let matcher = PatternMatcher::from_slice(accumulated_facts.clone());
let raw_candidates = matcher.match_patterns(&positive_patterns);
let or_expanded = {
use crate::query::datalog::executor::apply_or_clauses;
use crate::query::datalog::functions::FunctionRegistry;
let registry_guard = self.rules.read().expect("lock poisoned");
let fn_registry = FunctionRegistry::with_builtins();
let expanded = apply_or_clauses(
&rule.body,
raw_candidates,
accumulated_facts.clone(),
®istry_guard,
None,
None,
&fn_registry,
)?;
drop(registry_guard);
expanded
};
let candidates = apply_expr_clauses_in_evaluator(
or_expanded,
&body_expr_clauses,
&self.functions.read().expect("lock poisoned"),
);
let temp_eval = RecursiveEvaluator::new(
accumulated.clone(),
Arc::clone(&self.rules),
Arc::clone(&self.functions),
1,
self.max_derived_facts,
self.max_results,
);
'binding: for binding in candidates {
for not_body in ¬_clauses {
let substituted: Vec<Pattern> = not_body
.iter()
.filter_map(|c| match c {
WhereClause::Pattern(p) => Some(substitute_pattern(p, &binding)),
WhereClause::RuleInvocation { predicate, args } => {
let subst_args: Vec<EdnValue> = args
.iter()
.map(|a| substitute_value(a, &binding))
.collect();
match subst_args.len() {
1 => Some(Pattern::new(
subst_args[0].clone(),
EdnValue::Keyword(format!(":{}", predicate)),
EdnValue::Symbol("?_rule_value".to_string()),
)),
2 => Some(Pattern::new(
subst_args[0].clone(),
EdnValue::Keyword(format!(":{}", predicate)),
subst_args[1].clone(),
)),
_ => None,
}
}
WhereClause::Not(_) | WhereClause::NotJoin { .. } => None,
WhereClause::Expr { .. } => None,
WhereClause::Or(_) | WhereClause::OrJoin { .. } => None, })
.collect();
let not_matcher = PatternMatcher::from_slice(accumulated_facts.clone());
let mut not_bindings: Vec<Bindings> = if substituted.is_empty() {
vec![binding.clone()]
} else {
not_matcher
.match_patterns(&substituted)
.into_iter()
.map(|mut nb| {
for (k, v) in &binding {
nb.entry(k.clone()).or_insert_with(|| v.clone());
}
nb
})
.collect()
};
let not_body_expr_clauses: Vec<&WhereClause> = not_body
.iter()
.filter(|c| matches!(c, WhereClause::Expr { .. }))
.collect();
not_bindings = apply_expr_clauses_in_evaluator(
not_bindings,
¬_body_expr_clauses,
&self.functions.read().expect("lock poisoned"),
);
if !not_bindings.is_empty() {
continue 'binding; }
}
for (join_vars, nj_clauses) in ¬_join_clauses {
if evaluate_not_join(
join_vars,
nj_clauses,
&binding,
accumulated_facts.clone(),
&self.functions.read().expect("lock poisoned"),
) {
continue 'binding;
}
}
if let Ok(fact) = temp_eval.instantiate_head_public(&rule.head, &binding) {
let _ = accumulated
.transact(vec![(fact.entity, fact.attribute, fact.value)], None);
}
}
}
}
Ok(accumulated)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::query::datalog::functions::FunctionRegistry;
use crate::query::datalog::parser::parse_datalog_command;
use crate::query::datalog::types::DatalogCommand;
use uuid::Uuid;
fn create_test_storage() -> FactStorage {
let storage = FactStorage::new();
let a = Uuid::new_v4();
let b = Uuid::new_v4();
let c = Uuid::new_v4();
storage
.transact(
vec![
(a, ":connected".to_string(), Value::Ref(b)),
(b, ":connected".to_string(), Value::Ref(c)),
],
None,
)
.unwrap();
storage
}
fn register_test_rule(rules: &Arc<RwLock<RuleRegistry>>, rule_str: &str) {
let cmd = parse_datalog_command(rule_str).unwrap();
if let DatalogCommand::Rule(rule) = cmd {
let predicate = match &rule.head[0] {
EdnValue::Symbol(s) => s.clone(),
_ => panic!("Expected symbol as predicate name"),
};
rules
.write()
.unwrap()
.register_rule(predicate, rule)
.unwrap();
} else {
panic!("Expected Rule command");
}
}
#[test]
fn test_evaluator_creation() {
let storage = FactStorage::new();
let rules = Arc::new(RwLock::new(RuleRegistry::new()));
let functions = Arc::new(RwLock::new(FunctionRegistry::with_builtins()));
let _evaluator = RecursiveEvaluator::new(
storage,
rules,
functions,
1000,
DEFAULT_MAX_DERIVED_FACTS,
DEFAULT_MAX_RESULTS,
);
}
#[test]
fn test_max_iterations_exceeded_returns_error() {
let storage = create_test_storage();
let rules = Arc::new(RwLock::new(RuleRegistry::new()));
register_test_rule(&rules, r#"(rule [(reachable ?x ?y) [?x :connected ?y]])"#);
register_test_rule(
&rules,
r#"(rule [(reachable ?x ?y) [?x :connected ?z] (reachable ?z ?y)])"#,
);
let functions = Arc::new(RwLock::new(FunctionRegistry::with_builtins()));
let evaluator = RecursiveEvaluator::new(
storage,
rules,
functions.clone(),
0,
DEFAULT_MAX_DERIVED_FACTS,
DEFAULT_MAX_RESULTS,
);
let result = evaluator.evaluate_recursive_rules(&["reachable".to_string()]);
assert!(result.is_err(), "should fail when max iterations exceeded");
}
#[test]
fn test_rule_invocation_empty_list_error() {
let storage = FactStorage::new();
let rules = Arc::new(RwLock::new(RuleRegistry::new()));
let functions = Arc::new(RwLock::new(FunctionRegistry::with_builtins()));
let evaluator = RecursiveEvaluator::new(
storage,
rules,
functions,
1000,
DEFAULT_MAX_DERIVED_FACTS,
DEFAULT_MAX_RESULTS,
);
let result = evaluator.rule_invocation_to_pattern(&[]);
assert!(
result.is_err(),
"empty rule invocation list should return error"
);
}
#[test]
fn test_head_requires_args() {
let storage = FactStorage::new();
let rules = Arc::new(RwLock::new(RuleRegistry::new()));
let functions = Arc::new(RwLock::new(FunctionRegistry::with_builtins()));
let evaluator = RecursiveEvaluator::new(
storage,
rules,
functions,
1000,
DEFAULT_MAX_DERIVED_FACTS,
DEFAULT_MAX_RESULTS,
);
let head = vec![EdnValue::Symbol("predicate".to_string())]; let binding = std::collections::HashMap::new();
let result = evaluator.instantiate_head(&head, &binding);
assert!(
result.is_err(),
"head with only predicate and no arg should fail"
);
}
#[test]
fn test_evaluate_rule_empty_body_returns_empty_derived() {
use crate::query::datalog::types::Rule;
let storage = FactStorage::new();
let mut registry = RuleRegistry::new();
let functions = Arc::new(RwLock::new(FunctionRegistry::with_builtins()));
let rule = Rule {
head: vec![
EdnValue::Symbol("empty-body-pred".to_string()),
EdnValue::Symbol("?x".to_string()),
],
body: vec![], };
registry.register_rule_unchecked("empty-body-pred".to_string(), rule);
let rules = Arc::new(RwLock::new(registry));
let evaluator = RecursiveEvaluator::new(
storage,
rules,
functions,
1000,
DEFAULT_MAX_DERIVED_FACTS,
DEFAULT_MAX_RESULTS,
);
let result = evaluator.evaluate_recursive_rules(&["empty-body-pred".to_string()]);
assert!(result.is_ok(), "empty body rule should not fail");
}
#[test]
fn test_evaluate_rule_expr_only_body() {
use crate::query::datalog::types::{Expr, Rule, WhereClause};
let storage = FactStorage::new();
let functions = Arc::new(RwLock::new(FunctionRegistry::with_builtins()));
let e1 = Uuid::new_v4();
storage
.transact(
vec![(e1, ":item/value".to_string(), Value::Integer(42))],
None,
)
.unwrap();
let mut registry = RuleRegistry::new();
let rule = Rule {
head: vec![
EdnValue::Symbol("expr-only-pred".to_string()),
EdnValue::Symbol("?x".to_string()),
],
body: vec![WhereClause::Expr {
expr: Expr::Lit(Value::Boolean(false)), binding: None,
}],
};
registry.register_rule_unchecked("expr-only-pred".to_string(), rule);
let rules = Arc::new(RwLock::new(registry));
let evaluator = RecursiveEvaluator::new(
storage,
rules,
functions,
1000,
DEFAULT_MAX_DERIVED_FACTS,
DEFAULT_MAX_RESULTS,
);
let result = evaluator.evaluate_recursive_rules(&["expr-only-pred".to_string()]);
assert!(result.is_ok(), "expr-only rule body should not fail");
let derived = result.unwrap();
let facts = derived.get_asserted_facts().unwrap();
let pred_facts: Vec<_> = facts
.iter()
.filter(|f| f.attribute == ":expr-only-pred")
.collect();
assert_eq!(
pred_facts.len(),
0,
"expr evaluating to false should derive no facts"
);
}
#[test]
fn test_evaluate_not_join_expr_only_body() {
use crate::graph::types::Value;
use crate::query::datalog::types::{BinOp, Expr, WhereClause};
let storage = FactStorage::new();
let e1 = Uuid::new_v4();
storage
.transact(vec![(e1, ":score".to_string(), Value::Integer(100))], None)
.unwrap();
let facts: Arc<[crate::graph::types::Fact]> =
Arc::from(storage.get_asserted_facts().unwrap().as_slice());
let mut binding = std::collections::HashMap::new();
binding.insert("?x".to_string(), Value::Integer(100));
let expr_clause = WhereClause::Expr {
expr: Expr::BinOp(
BinOp::Gt,
Box::new(Expr::Var("?x".to_string())),
Box::new(Expr::Lit(Value::Integer(50))),
),
binding: None,
};
let result = evaluate_not_join(
&["?x".to_string()],
&[expr_clause],
&binding,
facts,
&FunctionRegistry::with_builtins(),
);
assert!(
result,
"not-join with expr (100 > 50) should return true (reject)"
);
}
#[test]
fn test_stratified_evaluator_empty_stratum_skipped() {
let storage = FactStorage::new();
let e1 = Uuid::new_v4();
storage
.transact(
vec![(e1, ":x".to_string(), crate::graph::types::Value::Integer(1))],
None,
)
.unwrap();
let rules = Arc::new(RwLock::new(RuleRegistry::new()));
let functions = Arc::new(RwLock::new(FunctionRegistry::with_builtins()));
register_test_rule(&rules, r#"(rule [(a ?x) [?x :x ?v]])"#);
let evaluator = StratifiedEvaluator::new(
storage.clone(),
rules,
functions,
1000,
DEFAULT_MAX_DERIVED_FACTS,
DEFAULT_MAX_RESULTS,
);
let result = evaluator.evaluate(&["a".to_string(), "nonexistent".to_string()]);
assert!(
result.is_ok(),
"evaluation with missing predicate should succeed"
);
}
#[test]
fn test_stratified_evaluator_not_body_empty_patterns() {
let storage = FactStorage::new();
let e1 = Uuid::new_v4();
let e2 = Uuid::new_v4();
storage
.transact(
vec![
(
e1,
":val".to_string(),
crate::graph::types::Value::Integer(200),
),
(
e2,
":val".to_string(),
crate::graph::types::Value::Integer(50),
),
],
None,
)
.unwrap();
use crate::graph::types::Value;
use crate::query::datalog::types::{BinOp, Expr, Pattern, Rule, WhereClause};
let mut registry = RuleRegistry::new();
let rule = Rule {
head: vec![
EdnValue::Symbol("big".to_string()),
EdnValue::Symbol("?x".to_string()),
],
body: vec![
WhereClause::Pattern(Pattern::new(
EdnValue::Symbol("?x".to_string()),
EdnValue::Keyword(":val".to_string()),
EdnValue::Symbol("?v".to_string()),
)),
WhereClause::Not(vec![WhereClause::Expr {
expr: Expr::BinOp(
BinOp::Lt,
Box::new(Expr::Var("?v".to_string())),
Box::new(Expr::Lit(Value::Integer(100))),
),
binding: None,
}]),
],
};
registry.register_rule_unchecked("big".to_string(), rule);
let rules = Arc::new(RwLock::new(registry));
let functions = Arc::new(RwLock::new(FunctionRegistry::with_builtins()));
let evaluator = StratifiedEvaluator::new(
storage.clone(),
rules,
functions,
1000,
DEFAULT_MAX_DERIVED_FACTS,
DEFAULT_MAX_RESULTS,
);
let result = evaluator.evaluate(&["big".to_string()]);
assert!(
result.is_ok(),
"stratified evaluation with expr-only not body should succeed"
);
let derived = result.unwrap();
let facts = derived.get_asserted_facts().unwrap();
let big_facts: Vec<_> = facts.iter().filter(|f| f.attribute == ":big").collect();
assert_eq!(
big_facts.len(),
1,
"only entity with val=200 should be 'big'"
);
}
#[test]
fn test_stratified_max_results_limit() {
let storage = create_test_storage();
let functions = Arc::new(RwLock::new(FunctionRegistry::with_builtins()));
let rules = Arc::new(RwLock::new(RuleRegistry::new()));
register_test_rule(&rules, "(rule [(all ?x) [?x :connected _]])");
let evaluator = StratifiedEvaluator::new(
storage,
rules,
functions,
100,
DEFAULT_MAX_DERIVED_FACTS,
1, );
let result = evaluator.evaluate(&["all".to_string()]);
assert!(
result.is_err(),
"stratified should error when max_results exceeded"
);
}
#[test]
fn test_stratified_max_derived_facts_limit() {
let storage = create_test_storage();
let functions = Arc::new(RwLock::new(FunctionRegistry::with_builtins()));
let rules = Arc::new(RwLock::new(RuleRegistry::new()));
register_test_rule(&rules, "(rule [(all ?x) [?x :connected _]])");
let evaluator = StratifiedEvaluator::new(
storage,
rules,
functions,
100,
1, DEFAULT_MAX_RESULTS,
);
let result = evaluator.evaluate(&["all".to_string()]);
assert!(
result.is_err(),
"stratified should error when max_derived_facts exceeded"
);
}
}