2026-03-09 09:59:10 +01:00
|
|
|
//! Integration tests for the chase algorithm.
|
2026-03-06 10:52:32 +00:00
|
|
|
|
2026-03-09 09:59:10 +01:00
|
|
|
use chase_rs::chase::rule::RuleBuilder;
|
|
|
|
|
use chase_rs::{chase, Atom, Instance, Term};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_transitive_closure() {
|
|
|
|
|
// Build a chain: a -> b -> c -> d
|
|
|
|
|
let instance: Instance = vec![
|
|
|
|
|
Atom::new("Edge", vec![Term::constant("a"), Term::constant("b")]),
|
|
|
|
|
Atom::new("Edge", vec![Term::constant("b"), Term::constant("c")]),
|
|
|
|
|
Atom::new("Edge", vec![Term::constant("c"), Term::constant("d")]),
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Edge(X, Y) -> Path(X, Y)
|
|
|
|
|
let rule1 = RuleBuilder::new()
|
|
|
|
|
.when("Edge", vec![Term::var("X"), Term::var("Y")])
|
|
|
|
|
.then("Path", vec![Term::var("X"), Term::var("Y")])
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
// Path(X, Y), Edge(Y, Z) -> Path(X, Z)
|
|
|
|
|
let rule2 = RuleBuilder::new()
|
|
|
|
|
.when("Path", vec![Term::var("X"), Term::var("Y")])
|
|
|
|
|
.when("Edge", vec![Term::var("Y"), Term::var("Z")])
|
|
|
|
|
.then("Path", vec![Term::var("X"), Term::var("Z")])
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
let result = chase(instance, &[rule1, rule2]);
|
|
|
|
|
|
|
|
|
|
assert!(result.terminated);
|
|
|
|
|
|
|
|
|
|
// Should have 6 paths: a->b, b->c, c->d, a->c, b->d, a->d
|
|
|
|
|
let paths = result.instance.facts_for_predicate("Path");
|
|
|
|
|
assert_eq!(paths.len(), 6);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_existential_rule_generates_nulls() {
|
|
|
|
|
// Every employee must have a department
|
|
|
|
|
let instance: Instance = vec![
|
|
|
|
|
Atom::new("Employee", vec![Term::constant("alice")]),
|
|
|
|
|
Atom::new("Employee", vec![Term::constant("bob")]),
|
|
|
|
|
Atom::new("Employee", vec![Term::constant("carol")]),
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Employee(X) -> WorksIn(X, Y) where Y is existential
|
|
|
|
|
let rule = RuleBuilder::new()
|
|
|
|
|
.when("Employee", vec![Term::var("X")])
|
|
|
|
|
.then("WorksIn", vec![Term::var("X"), Term::var("Dept")])
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
let result = chase(instance, &[rule]);
|
|
|
|
|
|
|
|
|
|
assert!(result.terminated);
|
|
|
|
|
|
|
|
|
|
let works_in = result.instance.facts_for_predicate("WorksIn");
|
|
|
|
|
assert_eq!(works_in.len(), 3);
|
|
|
|
|
|
|
|
|
|
// Each should have a unique null
|
|
|
|
|
let nulls: Vec<_> = works_in
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|f| match &f.terms[1] {
|
|
|
|
|
Term::Null(id) => Some(*id),
|
|
|
|
|
_ => None,
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
assert_eq!(nulls.len(), 3);
|
|
|
|
|
|
|
|
|
|
// All nulls should be unique
|
|
|
|
|
let mut unique_nulls = nulls.clone();
|
|
|
|
|
unique_nulls.sort();
|
|
|
|
|
unique_nulls.dedup();
|
|
|
|
|
assert_eq!(unique_nulls.len(), 3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_multiple_head_atoms() {
|
|
|
|
|
let instance: Instance = vec![Atom::new("Person", vec![Term::constant("alice")])]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Person(X) -> HasName(X, N), HasAge(X, A)
|
|
|
|
|
let rule = RuleBuilder::new()
|
|
|
|
|
.when("Person", vec![Term::var("X")])
|
|
|
|
|
.then("HasName", vec![Term::var("X"), Term::var("N")])
|
|
|
|
|
.then("HasAge", vec![Term::var("X"), Term::var("A")])
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
let result = chase(instance, &[rule]);
|
|
|
|
|
|
|
|
|
|
assert!(result.terminated);
|
|
|
|
|
assert_eq!(result.instance.facts_for_predicate("HasName").len(), 1);
|
|
|
|
|
assert_eq!(result.instance.facts_for_predicate("HasAge").len(), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_chase_with_constants_in_rules() {
|
|
|
|
|
let instance: Instance = vec![
|
|
|
|
|
Atom::new("Status", vec![Term::constant("alice"), Term::constant("active")]),
|
|
|
|
|
Atom::new("Status", vec![Term::constant("bob"), Term::constant("inactive")]),
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Only active users get access: Status(X, "active") -> HasAccess(X)
|
|
|
|
|
let rule = RuleBuilder::new()
|
|
|
|
|
.when(
|
|
|
|
|
"Status",
|
|
|
|
|
vec![Term::var("X"), Term::constant("active")],
|
|
|
|
|
)
|
|
|
|
|
.then("HasAccess", vec![Term::var("X")])
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
let result = chase(instance, &[rule]);
|
|
|
|
|
|
|
|
|
|
assert!(result.terminated);
|
|
|
|
|
|
|
|
|
|
let access = result.instance.facts_for_predicate("HasAccess");
|
|
|
|
|
assert_eq!(access.len(), 1);
|
|
|
|
|
|
|
|
|
|
// Only alice should have access
|
|
|
|
|
let fact = access[0];
|
|
|
|
|
assert_eq!(fact.terms[0], Term::constant("alice"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_chase_reaches_fixpoint() {
|
|
|
|
|
// Test that applying the same rule multiple times doesn't create duplicates
|
|
|
|
|
let instance: Instance = vec![Atom::new("Fact", vec![Term::constant("x")])]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// Fact(X) -> Derived(X)
|
|
|
|
|
let rule = RuleBuilder::new()
|
|
|
|
|
.when("Fact", vec![Term::var("X")])
|
|
|
|
|
.then("Derived", vec![Term::var("X")])
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
let result = chase(instance, &[rule]);
|
|
|
|
|
|
|
|
|
|
assert!(result.terminated);
|
|
|
|
|
assert_eq!(result.instance.facts_for_predicate("Derived").len(), 1);
|
|
|
|
|
assert_eq!(result.steps, 1); // Should complete in one step
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_self_join_rule() {
|
|
|
|
|
// Find pairs of people with the same manager
|
|
|
|
|
let instance: Instance = vec![
|
|
|
|
|
Atom::new(
|
|
|
|
|
"ManagedBy",
|
|
|
|
|
vec![Term::constant("alice"), Term::constant("eve")],
|
|
|
|
|
),
|
|
|
|
|
Atom::new(
|
|
|
|
|
"ManagedBy",
|
|
|
|
|
vec![Term::constant("bob"), Term::constant("eve")],
|
|
|
|
|
),
|
|
|
|
|
Atom::new(
|
|
|
|
|
"ManagedBy",
|
|
|
|
|
vec![Term::constant("carol"), Term::constant("frank")],
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
.into_iter()
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// ManagedBy(X, M), ManagedBy(Y, M) -> SameTeam(X, Y)
|
|
|
|
|
let rule = RuleBuilder::new()
|
|
|
|
|
.when("ManagedBy", vec![Term::var("X"), Term::var("M")])
|
|
|
|
|
.when("ManagedBy", vec![Term::var("Y"), Term::var("M")])
|
|
|
|
|
.then("SameTeam", vec![Term::var("X"), Term::var("Y")])
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
let result = chase(instance, &[rule]);
|
|
|
|
|
|
|
|
|
|
assert!(result.terminated);
|
|
|
|
|
|
|
|
|
|
// Should have: (alice, alice), (alice, bob), (bob, alice), (bob, bob), (carol, carol)
|
|
|
|
|
let same_team = result.instance.facts_for_predicate("SameTeam");
|
|
|
|
|
assert_eq!(same_team.len(), 5);
|
|
|
|
|
}
|