//! Integration tests for the chase algorithm. 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); }