//! Integration tests for the current rule-engine core. use query_engine::chase::rule::RuleBuilder; use query_engine::{Atom, Instance, Term, chase}; #[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); } #[test] fn stratified_negation_derives_isolated_nodes() { use query_engine::chase::{ChaseConfig, chase_stratified}; let instance: Instance = vec![ Atom::new("Node", vec![Term::constant("a")]), Atom::new("Node", vec![Term::constant("b")]), Atom::new("Node", vec![Term::constant("c")]), Atom::new("Edge", vec![Term::constant("a"), Term::constant("b")]), ] .into_iter() .collect(); // Stratum 0: Edge(X, Y) -> Connected(X) let rule1 = RuleBuilder::new() .when("Edge", vec![Term::var("X"), Term::var("Y")]) .then("Connected", vec![Term::var("X")]) .build(); // Stratum 1: Node(X), NOT Connected(X) -> Isolated(X) let rule2 = RuleBuilder::new() .when("Node", vec![Term::var("X")]) .when_not("Connected", vec![Term::var("X")]) .then("Isolated", vec![Term::var("X")]) .build(); let result = chase_stratified(instance, &[rule1, rule2], ChaseConfig::default()); assert!(result.terminated); let connected = result.instance.facts_for_predicate("Connected"); assert_eq!(connected.len(), 1); // only "a" has an outgoing edge let isolated = result.instance.facts_for_predicate("Isolated"); assert_eq!(isolated.len(), 2); // "b" and "c" } #[test] fn negation_without_stratification_fires_in_single_round() { // A simple case: negation refers to a predicate that is never derived. let instance: Instance = vec![ Atom::new("Person", vec![Term::constant("alice")]), Atom::new("Person", vec![Term::constant("bob")]), Atom::new("Fired", vec![Term::constant("bob")]), ] .into_iter() .collect(); // Person(X), NOT Fired(X) -> Active(X) let rule = RuleBuilder::new() .when("Person", vec![Term::var("X")]) .when_not("Fired", vec![Term::var("X")]) .then("Active", vec![Term::var("X")]) .build(); let result = chase(instance, &[rule]); assert!(result.terminated); let active = result.instance.facts_for_predicate("Active"); assert_eq!(active.len(), 1); assert_eq!(active[0].terms[0], Term::constant("alice")); }