WIP
This commit is contained in:
parent
02e2e45848
commit
fc66b8e393
30
README.md
30
README.md
@ -11,13 +11,16 @@ The chase algorithm is a fundamental technique in context of database theory and
|
|||||||
- Ontology-based data access (OBDA)
|
- Ontology-based data access (OBDA)
|
||||||
- Datalog with existential rules
|
- Datalog with existential rules
|
||||||
|
|
||||||
This implementation provides a **restricted chase** that guarantees termination even with existential rules by tracking applied triggers.
|
This implementation provides a restricted-chase style materialization with active-trigger checks.
|
||||||
|
The default entrypoints use a `10_000` step safeguard and report incomplete results with
|
||||||
|
`terminated == false` if that limit is reached.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
- **Core Data Types**: Terms, Atoms, Rules, and Instances
|
- **Core Data Types**: Terms, Atoms, Rules, and Instances
|
||||||
- **Existential Quantification**: Automatic generation of labeled nulls
|
- **Existential Quantification**: Automatic generation of labeled nulls
|
||||||
- **Restricted Chase**: Termination guarantees via trigger tracking
|
- **Restricted Chase Style**: Active-trigger checks for existential rules
|
||||||
|
- **Configurable Step Limit**: `chase_with_config` exposes the step bound
|
||||||
- **Fluent API**: `RuleBuilder` for readable rule construction
|
- **Fluent API**: `RuleBuilder` for readable rule construction
|
||||||
- **Interactive Frontends**: REPL, script runner, and a local GUI
|
- **Interactive Frontends**: REPL, script runner, and a local GUI
|
||||||
- **Zero Dependencies**: Pure Rust with no external runtime dependencies
|
- **Zero Dependencies**: Pure Rust with no external runtime dependencies
|
||||||
@ -65,6 +68,29 @@ assert!(result.terminated);
|
|||||||
println!("Derived {} facts", result.instance.len());
|
println!("Derived {} facts", result.instance.len());
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use `chase_with_config` to change the default step bound:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use chase_rs::{Atom, ChaseConfig, Instance, Term, chase_with_config};
|
||||||
|
use chase_rs::chase::rule::RuleBuilder;
|
||||||
|
|
||||||
|
let instance: Instance = vec![Atom::new("P", vec![Term::constant("a")])]
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
let rule = RuleBuilder::new()
|
||||||
|
.when("P", vec![Term::var("X")])
|
||||||
|
.then("Q", vec![Term::var("X")])
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let result = chase_with_config(
|
||||||
|
instance,
|
||||||
|
&[rule],
|
||||||
|
ChaseConfig { max_steps: 100 },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.terminated);
|
||||||
|
```
|
||||||
|
|
||||||
#### Existential Rules
|
#### Existential Rules
|
||||||
|
|
||||||
Rules with head-only variables (existential quantification) automatically generate fresh labeled nulls:
|
Rules with head-only variables (existential quantification) automatically generate fresh labeled nulls:
|
||||||
|
|||||||
@ -1,55 +1,99 @@
|
|||||||
//! A database instance is a set of ground atoms (facts).
|
//! A database instance is a set of ground atoms (facts).
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::error::Error;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use super::atom::Atom;
|
use super::atom::Atom;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum InstanceError {
|
||||||
|
NonGroundFact(Atom),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for InstanceError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
InstanceError::NonGroundFact(atom) => {
|
||||||
|
write!(f, "facts must be ground atoms: {}", atom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for InstanceError {}
|
||||||
|
|
||||||
/// A database instance containing ground atoms.
|
/// A database instance containing ground atoms.
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct Instance {
|
pub struct Instance {
|
||||||
facts: HashSet<Atom>,
|
facts_by_predicate: HashMap<String, HashSet<Atom>>,
|
||||||
|
len: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Instance {
|
impl Instance {
|
||||||
/// Create an empty instance.
|
/// Create an empty instance.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Instance {
|
Instance {
|
||||||
facts: HashSet::new(),
|
facts_by_predicate: HashMap::new(),
|
||||||
|
len: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Try to add a fact to the instance. Returns true if the fact was new.
|
||||||
|
pub fn try_add(&mut self, fact: Atom) -> Result<bool, InstanceError> {
|
||||||
|
if !fact.is_ground() {
|
||||||
|
return Err(InstanceError::NonGroundFact(fact));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bucket = self
|
||||||
|
.facts_by_predicate
|
||||||
|
.entry(fact.predicate.clone())
|
||||||
|
.or_default();
|
||||||
|
let inserted = bucket.insert(fact);
|
||||||
|
if inserted {
|
||||||
|
self.len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(inserted)
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a fact to the instance. Returns true if the fact was new.
|
/// Add a fact to the instance. Returns true if the fact was new.
|
||||||
|
///
|
||||||
|
/// Panics if the atom is not ground.
|
||||||
pub fn add(&mut self, fact: Atom) -> bool {
|
pub fn add(&mut self, fact: Atom) -> bool {
|
||||||
debug_assert!(fact.is_ground(), "Facts must be ground atoms");
|
self.try_add(fact).expect("facts must be ground atoms")
|
||||||
self.facts.insert(fact)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the instance contains a fact.
|
/// Check if the instance contains a fact.
|
||||||
pub fn contains(&self, fact: &Atom) -> bool {
|
pub fn contains(&self, fact: &Atom) -> bool {
|
||||||
self.facts.contains(fact)
|
self.facts_by_predicate
|
||||||
|
.get(&fact.predicate)
|
||||||
|
.is_some_and(|facts| facts.contains(fact))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the number of facts.
|
/// Get the number of facts.
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.facts.len()
|
self.len
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the instance is empty.
|
/// Check if the instance is empty.
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.facts.is_empty()
|
self.len == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over all facts.
|
/// Iterate over all facts.
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &Atom> {
|
pub fn iter(&self) -> impl Iterator<Item = &Atom> {
|
||||||
self.facts.iter()
|
self.facts_by_predicate
|
||||||
|
.values()
|
||||||
|
.flat_map(|facts| facts.iter())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all facts with a given predicate.
|
/// Get all facts with a given predicate.
|
||||||
pub fn facts_for_predicate(&self, predicate: &str) -> Vec<&Atom> {
|
pub fn facts_for_predicate(&self, predicate: &str) -> Vec<&Atom> {
|
||||||
self.facts
|
self.facts_by_predicate
|
||||||
.iter()
|
.get(predicate)
|
||||||
.filter(|a| a.predicate == predicate)
|
.into_iter()
|
||||||
|
.flat_map(|facts| facts.iter())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,7 +101,7 @@ impl Instance {
|
|||||||
impl fmt::Display for Instance {
|
impl fmt::Display for Instance {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(f, "Instance {{")?;
|
writeln!(f, "Instance {{")?;
|
||||||
for fact in &self.facts {
|
for fact in self.iter() {
|
||||||
writeln!(f, " {}", fact)?;
|
writeln!(f, " {}", fact)?;
|
||||||
}
|
}
|
||||||
write!(f, "}}")
|
write!(f, "}}")
|
||||||
@ -116,4 +160,15 @@ mod tests {
|
|||||||
assert_eq!(instance.facts_for_predicate("Person").len(), 1);
|
assert_eq!(instance.facts_for_predicate("Person").len(), 1);
|
||||||
assert_eq!(instance.facts_for_predicate("Other").len(), 0);
|
assert_eq!(instance.facts_for_predicate("Other").len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_try_add_rejects_non_ground_facts() {
|
||||||
|
let mut instance = Instance::new();
|
||||||
|
let fact = Atom::new("Parent", vec![Term::var("X"), Term::constant("bob")]);
|
||||||
|
|
||||||
|
let error = instance.try_add(fact).unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(error, InstanceError::NonGroundFact(_)));
|
||||||
|
assert!(instance.is_empty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,8 @@ pub mod term;
|
|||||||
mod engine;
|
mod engine;
|
||||||
|
|
||||||
pub use atom::Atom;
|
pub use atom::Atom;
|
||||||
pub use engine::{ChaseResult, chase};
|
pub use engine::{ChaseConfig, ChaseResult, chase, chase_with_config};
|
||||||
pub use instance::Instance;
|
pub use instance::{Instance, InstanceError};
|
||||||
pub use rule::Rule;
|
pub use rule::Rule;
|
||||||
pub use substitution::Substitution;
|
pub use substitution::Substitution;
|
||||||
pub use term::Term;
|
pub use term::Term;
|
||||||
|
|||||||
@ -53,6 +53,9 @@ pub fn parse_command(input: &str) -> Result<Command, String> {
|
|||||||
|
|
||||||
if let Some(rest) = strip_keyword(trimmed, "fact") {
|
if let Some(rest) = strip_keyword(trimmed, "fact") {
|
||||||
let atom = parse_atom(trim_suffix(rest, '.')?)?;
|
let atom = parse_atom(trim_suffix(rest, '.')?)?;
|
||||||
|
if !atom.is_ground() {
|
||||||
|
return Err("facts must be ground atoms".to_string());
|
||||||
|
}
|
||||||
return Ok(Command::Fact(atom));
|
return Ok(Command::Fact(atom));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,6 +349,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_fact_command_rejects_variables() {
|
||||||
|
let error = parse_command("fact Parent(?X, bob).").unwrap_err();
|
||||||
|
assert_eq!(error, "facts must be ground atoms");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_rule_command() {
|
fn parse_rule_command() {
|
||||||
let command = parse_command("rule P(?X), Q(?X, a) -> R(?X).").unwrap();
|
let command = parse_command("rule P(?X), Q(?X, a) -> R(?X).").unwrap();
|
||||||
|
|||||||
@ -3,9 +3,7 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crate::chase::substitution::unify_atom;
|
use crate::chase::substitution::unify_atom;
|
||||||
use crate::{Atom, ChaseResult, Instance, Rule, Substitution, Term};
|
use crate::{Atom, ChaseConfig, ChaseResult, Instance, Rule, Substitution, Term};
|
||||||
|
|
||||||
const DEFAULT_MAX_STEPS: usize = 10_000;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MaterializedState {
|
pub struct MaterializedState {
|
||||||
@ -76,12 +74,13 @@ pub fn materialize(base_instance: Instance, rules: &[Rule]) -> MaterializedState
|
|||||||
.cloned()
|
.cloned()
|
||||||
.map(|fact| (fact, Derivation::Input))
|
.map(|fact| (fact, Derivation::Input))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
let max_steps = ChaseConfig::default().max_steps;
|
||||||
let mut null_gen = NullGenerator::seeded_from(&instance, rules);
|
let mut null_gen = NullGenerator::seeded_from(&instance, rules);
|
||||||
let mut applied_triggers = HashSet::new();
|
let mut applied_triggers = HashSet::new();
|
||||||
let mut steps = 0;
|
let mut steps = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if steps >= DEFAULT_MAX_STEPS {
|
if steps >= max_steps {
|
||||||
return MaterializedState {
|
return MaterializedState {
|
||||||
result: ChaseResult {
|
result: ChaseResult {
|
||||||
instance,
|
instance,
|
||||||
|
|||||||
@ -41,7 +41,10 @@ impl Session {
|
|||||||
match command {
|
match command {
|
||||||
Command::Fact(atom) => {
|
Command::Fact(atom) => {
|
||||||
self.materialized = None;
|
self.materialized = None;
|
||||||
let inserted = self.base_instance.add(atom.clone());
|
let inserted = self
|
||||||
|
.base_instance
|
||||||
|
.try_add(atom.clone())
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
let action = if inserted {
|
let action = if inserted {
|
||||||
"Added"
|
"Added"
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -2,4 +2,7 @@ pub mod chase;
|
|||||||
pub mod frontend;
|
pub mod frontend;
|
||||||
|
|
||||||
// Re-export main types for convenience
|
// Re-export main types for convenience
|
||||||
pub use chase::{Atom, ChaseResult, Instance, Rule, Substitution, Term, chase};
|
pub use chase::{
|
||||||
|
Atom, ChaseConfig, ChaseResult, Instance, InstanceError, Rule, Substitution, Term, chase,
|
||||||
|
chase_with_config,
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user