use std::collections::{HashMap, HashSet};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FactArg {
Symbol(String),
Integer(i64),
}
impl fmt::Display for FactArg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FactArg::Symbol(s) => write!(f, "{s}"),
FactArg::Integer(n) => write!(f, "{n}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Fact {
pub predicate: String,
pub args: Vec<FactArg>,
}
impl Fact {
pub fn new(predicate: impl Into<String>, args: Vec<FactArg>) -> Self {
Self {
predicate: predicate.into(),
args,
}
}
pub fn sym(predicate: impl Into<String>, args: &[&str]) -> Self {
Self {
predicate: predicate.into(),
args: args
.iter()
.map(|s| FactArg::Symbol(s.to_string()))
.collect(),
}
}
pub fn arity(&self) -> usize {
self.args.len()
}
}
impl fmt::Display for Fact {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}(", self.predicate)?;
for (i, a) in self.args.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{a}")?;
}
write!(f, ")")
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Term {
Variable(String),
Constant(FactArg),
}
impl Term {
pub fn var(name: impl Into<String>) -> Self {
Term::Variable(name.into())
}
pub fn sym(s: impl Into<String>) -> Self {
Term::Constant(FactArg::Symbol(s.into()))
}
pub fn int(n: i64) -> Self {
Term::Constant(FactArg::Integer(n))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Atom {
pub predicate: String,
pub terms: Vec<Term>,
}
impl Atom {
pub fn new(predicate: impl Into<String>, terms: Vec<Term>) -> Self {
Self {
predicate: predicate.into(),
terms,
}
}
}
#[derive(Debug, Clone)]
pub struct Rule {
pub head: Atom,
pub body: Vec<Atom>,
}
impl Rule {
pub fn new(head: Atom, body: Vec<Atom>) -> Self {
Self { head, body }
}
pub fn is_fact(&self) -> bool {
self.body.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct Relation {
facts: HashSet<Fact>,
}
impl Relation {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, fact: Fact) -> bool {
self.facts.insert(fact)
}
pub fn contains(&self, fact: &Fact) -> bool {
self.facts.contains(fact)
}
pub fn len(&self) -> usize {
self.facts.len()
}
pub fn is_empty(&self) -> bool {
self.facts.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Fact> {
self.facts.iter()
}
pub fn facts(&self) -> Vec<Fact> {
self.facts.iter().cloned().collect()
}
pub fn union(&self, other: &Relation) -> Relation {
let mut result = self.clone();
for f in other.facts.iter() {
result.facts.insert(f.clone());
}
result
}
pub fn difference(&self, other: &Relation) -> Relation {
Relation {
facts: self
.facts
.iter()
.filter(|f| !other.facts.contains(*f))
.cloned()
.collect(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Edb {
relations: HashMap<String, Relation>,
}
impl Edb {
pub fn new() -> Self {
Self::default()
}
pub fn add_fact(&mut self, fact: Fact) {
self.relations
.entry(fact.predicate.clone())
.or_default()
.insert(fact);
}
pub fn get_relation(&self, predicate: &str) -> Option<&Relation> {
self.relations.get(predicate)
}
pub fn relation_names(&self) -> Vec<String> {
self.relations.keys().cloned().collect()
}
pub fn total_facts(&self) -> usize {
self.relations.values().map(|r| r.len()).sum()
}
}
#[derive(Debug, Clone, Default)]
pub struct Idb {
relations: HashMap<String, Relation>,
}
impl Idb {
pub fn new() -> Self {
Self::default()
}
pub fn get_relation(&self, predicate: &str) -> Option<&Relation> {
self.relations.get(predicate)
}
pub fn insert(&mut self, predicate: &str, fact: Fact) -> bool {
self.relations
.entry(predicate.to_owned())
.or_default()
.insert(fact)
}
pub fn total_facts(&self) -> usize {
self.relations.values().map(|r| r.len()).sum()
}
pub fn all_facts(&self) -> Vec<Fact> {
self.relations
.values()
.flat_map(|r| r.facts.iter().cloned())
.collect()
}
}
#[derive(Debug, Default, Clone)]
pub struct EvalStats {
pub iterations: usize,
pub total_new_facts: usize,
pub facts_per_iteration: Vec<usize>,
}
#[derive(Debug)]
pub enum QueryError {
UnknownPredicate(String),
ArityMismatch {
predicate: String,
expected: usize,
got: usize,
},
EvaluationError(String),
}
impl fmt::Display for QueryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QueryError::UnknownPredicate(p) => write!(f, "unknown predicate: {p}"),
QueryError::ArityMismatch {
predicate,
expected,
got,
} => write!(
f,
"arity mismatch for predicate {predicate}: expected {expected}, got {got}"
),
QueryError::EvaluationError(msg) => write!(f, "evaluation error: {msg}"),
}
}
}
impl std::error::Error for QueryError {}
fn unify_term(term: &Term, arg: &FactArg, bindings: &mut HashMap<String, FactArg>) -> bool {
match term {
Term::Variable(name) => {
if let Some(existing) = bindings.get(name) {
existing == arg
} else {
bindings.insert(name.clone(), arg.clone());
true
}
}
Term::Constant(c) => c == arg,
}
}
fn ground_head(head: &Atom, bindings: &HashMap<String, FactArg>) -> Option<Fact> {
let mut args = Vec::with_capacity(head.terms.len());
for term in &head.terms {
let arg = match term {
Term::Variable(name) => bindings.get(name)?.clone(),
Term::Constant(c) => c.clone(),
};
args.push(arg);
}
Some(Fact::new(head.predicate.clone(), args))
}
fn eval_body_atoms<'a>(
atoms: &'a [Atom],
current_bindings: HashMap<String, FactArg>,
all_facts: &'a HashMap<String, Relation>,
delta: &'a HashMap<String, Relation>,
used_delta: bool,
) -> Vec<HashMap<String, FactArg>> {
if atoms.is_empty() {
if used_delta {
return vec![current_bindings];
} else {
return vec![];
}
}
let (head_atom, rest) = atoms.split_first().expect("atoms is non-empty");
let predicate = &head_atom.predicate;
let mut results: Vec<HashMap<String, FactArg>> = Vec::new();
let full_rel = all_facts.get(predicate.as_str());
let delta_rel = delta.get(predicate.as_str());
let try_relation = |rel: &Relation,
bindings: &HashMap<String, FactArg>,
is_delta: bool|
-> Vec<HashMap<String, FactArg>> {
let mut out = Vec::new();
for fact in rel.iter() {
if fact.terms_len() != head_atom.terms.len() {
continue;
}
let mut b = bindings.clone();
let mut ok = true;
for (term, arg) in head_atom.terms.iter().zip(fact.args.iter()) {
if !unify_term(term, arg, &mut b) {
ok = false;
break;
}
}
if ok {
let mut sub = eval_body_atoms(rest, b, all_facts, delta, used_delta || is_delta);
out.append(&mut sub);
}
}
out
};
if let Some(dr) = delta_rel {
let mut sub = try_relation(dr, ¤t_bindings, true);
results.append(&mut sub);
}
if let Some(fr) = full_rel {
let mut sub = try_relation(fr, ¤t_bindings, false);
results.append(&mut sub);
}
results
}
trait FactExt {
fn terms_len(&self) -> usize;
}
impl FactExt for Fact {
fn terms_len(&self) -> usize {
self.args.len()
}
}
pub struct SemiNaiveEvaluator {
rules: Vec<Rule>,
edb: Edb,
idb: Idb,
stats: EvalStats,
}
impl SemiNaiveEvaluator {
pub fn new(rules: Vec<Rule>, edb: Edb) -> Self {
Self {
rules,
edb,
idb: Idb::new(),
stats: EvalStats::default(),
}
}
fn all_facts_snapshot(&self) -> HashMap<String, Relation> {
let mut map: HashMap<String, Relation> = HashMap::new();
for (pred, rel) in &self.edb.relations {
map.entry(pred.clone())
.or_default()
.facts
.extend(rel.facts.iter().cloned());
}
for (pred, rel) in &self.idb.relations {
map.entry(pred.clone())
.or_default()
.facts
.extend(rel.facts.iter().cloned());
}
map
}
fn apply_rule(&self, rule: &Rule, delta: &HashMap<String, Relation>) -> Vec<Fact> {
if rule.is_fact() {
return vec![];
}
let all_facts = self.all_facts_snapshot();
let bindings: HashMap<String, FactArg> = HashMap::new();
let binding_sets = eval_body_atoms(&rule.body, bindings, &all_facts, delta, false);
let mut new_facts: Vec<Fact> = Vec::new();
for b in binding_sets {
if let Some(fact) = ground_head(&rule.head, &b) {
let already_known = self
.idb
.get_relation(&fact.predicate)
.map(|r| r.contains(&fact))
.unwrap_or(false);
if !already_known {
new_facts.push(fact);
}
}
}
new_facts.sort_unstable_by(|a, b| format!("{a}").cmp(&format!("{b}")));
new_facts.dedup();
new_facts
}
pub fn evaluate(&mut self) -> Result<&Idb, QueryError> {
for rule in &self.rules {
if rule.is_fact() {
if let Some(fact) = ground_head(&rule.head, &HashMap::new()) {
self.idb.insert(&fact.predicate.clone(), fact);
}
}
}
let mut delta: HashMap<String, Relation> = HashMap::new();
for (pred, rel) in &self.edb.relations {
delta
.entry(pred.clone())
.or_default()
.facts
.extend(rel.facts.iter().cloned());
}
for (pred, rel) in &self.idb.relations {
delta
.entry(pred.clone())
.or_default()
.facts
.extend(rel.facts.iter().cloned());
}
loop {
if delta.values().all(|r| r.is_empty()) {
break;
}
let mut new_delta: HashMap<String, Relation> = HashMap::new();
let mut iteration_count = 0usize;
for rule in &self.rules {
if rule.is_fact() {
continue;
}
let derived = self.apply_rule(rule, &delta);
for fact in derived {
let pred = fact.predicate.clone();
let is_new = self.idb.insert(&pred, fact.clone());
if is_new {
new_delta.entry(pred).or_default().insert(fact);
iteration_count += 1;
}
}
}
self.stats.iterations += 1;
self.stats.facts_per_iteration.push(iteration_count);
self.stats.total_new_facts += iteration_count;
delta = new_delta;
}
Ok(&self.idb)
}
pub fn stats(&self) -> &EvalStats {
&self.stats
}
pub fn idb(&self) -> &Idb {
&self.idb
}
pub fn edb(&self) -> &Edb {
&self.edb
}
}
pub struct IncrementalEvaluator {
evaluator: SemiNaiveEvaluator,
}
impl IncrementalEvaluator {
pub fn new(rules: Vec<Rule>, initial_edb: Edb) -> Result<Self, QueryError> {
let mut evaluator = SemiNaiveEvaluator::new(rules, initial_edb);
evaluator.evaluate()?;
Ok(Self { evaluator })
}
pub fn add_facts(&mut self, new_facts: Vec<Fact>) -> Result<EvalStats, QueryError> {
for fact in &new_facts {
self.evaluator.edb.add_fact(fact.clone());
}
let mut delta: HashMap<String, Relation> = HashMap::new();
for fact in new_facts {
delta
.entry(fact.predicate.clone())
.or_default()
.insert(fact);
}
let mut local_stats = EvalStats::default();
loop {
if delta.values().all(|r| r.is_empty()) {
break;
}
let mut new_delta: HashMap<String, Relation> = HashMap::new();
let mut iteration_count = 0usize;
for rule in &self.evaluator.rules {
if rule.is_fact() {
continue;
}
let derived = self.evaluator.apply_rule(rule, &delta);
for fact in derived {
let pred = fact.predicate.clone();
let is_new = self.evaluator.idb.insert(&pred, fact.clone());
if is_new {
new_delta.entry(pred).or_default().insert(fact);
iteration_count += 1;
}
}
}
local_stats.iterations += 1;
local_stats.facts_per_iteration.push(iteration_count);
local_stats.total_new_facts += iteration_count;
self.evaluator.stats.iterations += 1;
self.evaluator.stats.total_new_facts += iteration_count;
self.evaluator
.stats
.facts_per_iteration
.push(iteration_count);
delta = new_delta;
}
Ok(local_stats)
}
pub fn query(&self, predicate: &str) -> Vec<Fact> {
self.evaluator
.idb
.get_relation(predicate)
.map(|r| r.facts())
.unwrap_or_default()
}
pub fn total_derived_facts(&self) -> usize {
self.evaluator.idb.total_facts()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_parent_edb() -> Edb {
let mut edb = Edb::new();
edb.add_fact(Fact::sym("parent", &["alice", "bob"]));
edb.add_fact(Fact::sym("parent", &["bob", "carol"]));
edb
}
fn ancestor_rules() -> Vec<Rule> {
vec![
Rule::new(
Atom::new("ancestor", vec![Term::var("X"), Term::var("Y")]),
vec![Atom::new("parent", vec![Term::var("X"), Term::var("Y")])],
),
Rule::new(
Atom::new("ancestor", vec![Term::var("X"), Term::var("Z")]),
vec![
Atom::new("parent", vec![Term::var("X"), Term::var("Y")]),
Atom::new("ancestor", vec![Term::var("Y"), Term::var("Z")]),
],
),
]
}
#[test]
fn test_empty_edb_no_rules() {
let mut eval = SemiNaiveEvaluator::new(vec![], Edb::new());
let idb = eval.evaluate().expect("evaluation should succeed");
assert_eq!(idb.total_facts(), 0, "empty IDB expected");
}
#[test]
fn test_fact_rules_insert_directly() {
let rule = Rule::new(
Atom::new("foo", vec![Term::sym("bar")]),
vec![], );
let mut eval = SemiNaiveEvaluator::new(vec![rule], Edb::new());
let idb = eval.evaluate().expect("evaluation should succeed");
let facts = idb.get_relation("foo").expect("relation foo should exist");
assert_eq!(facts.len(), 1);
assert!(facts.contains(&Fact::sym("foo", &["bar"])));
}
#[test]
fn test_simple_ancestor_chain() {
let rule = Rule::new(
Atom::new("ancestor", vec![Term::var("X"), Term::var("Y")]),
vec![Atom::new("parent", vec![Term::var("X"), Term::var("Y")])],
);
let mut eval = SemiNaiveEvaluator::new(vec![rule], make_parent_edb());
let idb = eval.evaluate().expect("evaluation should succeed");
let derived = idb.get_relation("ancestor").expect("ancestor relation");
assert!(derived.contains(&Fact::sym("ancestor", &["alice", "bob"])));
assert!(derived.contains(&Fact::sym("ancestor", &["bob", "carol"])));
}
#[test]
fn test_recursive_transitive_closure() {
let mut eval = SemiNaiveEvaluator::new(ancestor_rules(), make_parent_edb());
let idb = eval.evaluate().expect("evaluation should succeed");
let derived = idb.get_relation("ancestor").expect("ancestor relation");
assert!(derived.contains(&Fact::sym("ancestor", &["alice", "carol"])));
assert_eq!(derived.len(), 3);
}
#[test]
fn test_fixpoint_terminates() {
let mut eval = SemiNaiveEvaluator::new(ancestor_rules(), make_parent_edb());
eval.evaluate().expect("evaluation should succeed");
let idb_after = eval.idb().total_facts();
assert_eq!(idb_after, 3);
}
#[test]
fn test_eval_stats_iterations() {
let mut eval = SemiNaiveEvaluator::new(ancestor_rules(), make_parent_edb());
eval.evaluate().expect("evaluation should succeed");
assert!(
eval.stats().iterations >= 2,
"expected >=2 iterations, got {}",
eval.stats().iterations
);
}
#[test]
fn test_eval_stats_total_new_facts() {
let mut eval = SemiNaiveEvaluator::new(ancestor_rules(), make_parent_edb());
eval.evaluate().expect("evaluation should succeed");
assert_eq!(eval.stats().total_new_facts, 3);
}
#[test]
fn test_relation_union() {
let mut r1 = Relation::new();
r1.insert(Fact::sym("foo", &["a"]));
let mut r2 = Relation::new();
r2.insert(Fact::sym("foo", &["b"]));
r2.insert(Fact::sym("foo", &["a"]));
let u = r1.union(&r2);
assert_eq!(u.len(), 2);
}
#[test]
fn test_relation_difference() {
let mut r1 = Relation::new();
r1.insert(Fact::sym("foo", &["a"]));
r1.insert(Fact::sym("foo", &["b"]));
let mut r2 = Relation::new();
r2.insert(Fact::sym("foo", &["a"]));
let diff = r1.difference(&r2);
assert_eq!(diff.len(), 1);
assert!(diff.contains(&Fact::sym("foo", &["b"])));
}
#[test]
fn test_edb_total_facts() {
let edb = make_parent_edb();
assert_eq!(edb.total_facts(), 2);
}
#[test]
fn test_idb_all_facts() {
let mut eval = SemiNaiveEvaluator::new(ancestor_rules(), make_parent_edb());
eval.evaluate().expect("evaluation should succeed");
let all = eval.idb().all_facts();
assert_eq!(all.len(), 3);
}
#[test]
fn test_two_body_atom_join() {
let mut edb = Edb::new();
edb.add_fact(Fact::sym("parent", &["alice", "bob"]));
edb.add_fact(Fact::sym("parent", &["alice", "carol"]));
let rule = Rule::new(
Atom::new("sibling", vec![Term::var("X"), Term::var("Z")]),
vec![
Atom::new("parent", vec![Term::var("Y"), Term::var("X")]),
Atom::new("parent", vec![Term::var("Y"), Term::var("Z")]),
],
);
let mut eval = SemiNaiveEvaluator::new(vec![rule], edb);
let idb = eval.evaluate().expect("evaluation should succeed");
let siblings = idb.get_relation("sibling").expect("sibling relation");
assert_eq!(siblings.len(), 4);
}
#[test]
fn test_constant_in_body_filters() {
let rule = Rule::new(
Atom::new("known_alice", vec![Term::var("Y")]),
vec![Atom::new(
"parent",
vec![Term::sym("alice"), Term::var("Y")],
)],
);
let mut eval = SemiNaiveEvaluator::new(vec![rule], make_parent_edb());
let idb = eval.evaluate().expect("evaluation should succeed");
let rel = idb.get_relation("known_alice").expect("known_alice");
assert_eq!(rel.len(), 1);
assert!(rel.contains(&Fact::new(
"known_alice",
vec![FactArg::Symbol("bob".to_owned())]
)));
}
#[test]
fn test_variable_reuse_equality() {
let mut edb = Edb::new();
edb.add_fact(Fact::sym("parent", &["alice", "bob"]));
edb.add_fact(Fact::sym("parent", &["self", "self"]));
let rule = Rule::new(
Atom::new("self_parent", vec![Term::var("X")]),
vec![Atom::new("parent", vec![Term::var("X"), Term::var("X")])],
);
let mut eval = SemiNaiveEvaluator::new(vec![rule], edb);
let idb = eval.evaluate().expect("evaluation should succeed");
let rel = idb.get_relation("self_parent").expect("self_parent");
assert_eq!(rel.len(), 1);
assert!(rel.contains(&Fact::new(
"self_parent",
vec![FactArg::Symbol("self".to_owned())]
)));
}
#[test]
fn test_incremental_add_facts() {
let edb = make_parent_edb(); let mut inc =
IncrementalEvaluator::new(ancestor_rules(), edb).expect("init should succeed");
assert_eq!(inc.total_derived_facts(), 3);
inc.add_facts(vec![Fact::sym("parent", &["carol", "dave"])])
.expect("add_facts should succeed");
let ancestors = inc.query("ancestor");
assert_eq!(ancestors.len(), 6, "expected 6 ancestor pairs");
}
#[test]
fn test_incremental_query() {
let edb = make_parent_edb();
let inc = IncrementalEvaluator::new(ancestor_rules(), edb).expect("init should succeed");
let ancestors = inc.query("ancestor");
assert!(!ancestors.is_empty());
let none = inc.query("no_such_predicate");
assert!(none.is_empty());
}
#[test]
fn test_semi_naive_no_redundant_recomputation() {
let edb = make_parent_edb();
let mut inc =
IncrementalEvaluator::new(ancestor_rules(), edb).expect("init should succeed");
let before = inc.total_derived_facts();
let stats = inc
.add_facts(vec![Fact::sym("parent", &["alice", "bob"])])
.expect("add_facts should succeed");
let after = inc.total_derived_facts();
assert_eq!(before, after, "no new derived facts expected");
assert_eq!(stats.total_new_facts, 0);
}
#[test]
fn test_fact_sym_constructor() {
let f = Fact::sym("edge", &["a", "b"]);
assert_eq!(f.predicate, "edge");
assert_eq!(f.arity(), 2);
assert_eq!(f.args[0], FactArg::Symbol("a".to_owned()));
assert_eq!(f.args[1], FactArg::Symbol("b".to_owned()));
}
#[test]
fn test_term_constructors() {
let v = Term::var("X");
let s = Term::sym("hello");
let n = Term::int(42);
assert!(matches!(v, Term::Variable(ref x) if x == "X"));
assert!(matches!(s, Term::Constant(FactArg::Symbol(ref x)) if x == "hello"));
assert!(matches!(n, Term::Constant(FactArg::Integer(42))));
}
#[test]
fn test_unknown_predicate_in_rule_body() {
let rule = Rule::new(
Atom::new("foo", vec![Term::var("X")]),
vec![Atom::new("no_such_pred", vec![Term::var("X")])],
);
let mut eval = SemiNaiveEvaluator::new(vec![rule], Edb::new());
let idb = eval.evaluate().expect("evaluation should not hard-fail");
assert_eq!(idb.total_facts(), 0);
let err = QueryError::UnknownPredicate("no_such_pred".to_owned());
assert!(err.to_string().contains("no_such_pred"));
}
#[test]
fn test_five_node_chain() {
let nodes = ["a", "b", "c", "d", "e"];
let mut edb = Edb::new();
for i in 0..nodes.len() - 1 {
edb.add_fact(Fact::sym("parent", &[nodes[i], nodes[i + 1]]));
}
let mut eval = SemiNaiveEvaluator::new(ancestor_rules(), edb);
let idb = eval.evaluate().expect("evaluation should succeed");
let derived = idb.get_relation("ancestor").expect("ancestor relation");
assert_eq!(derived.len(), 10);
}
#[test]
fn test_multiple_rules_same_head() {
let mut edb = Edb::new();
edb.add_fact(Fact::sym("edge_a", &["x", "y"]));
edb.add_fact(Fact::sym("edge_b", &["y", "z"]));
let rule1 = Rule::new(
Atom::new("reachable", vec![Term::var("X"), Term::var("Y")]),
vec![Atom::new("edge_a", vec![Term::var("X"), Term::var("Y")])],
);
let rule2 = Rule::new(
Atom::new("reachable", vec![Term::var("X"), Term::var("Y")]),
vec![Atom::new("edge_b", vec![Term::var("X"), Term::var("Y")])],
);
let mut eval = SemiNaiveEvaluator::new(vec![rule1, rule2], edb);
let idb = eval.evaluate().expect("evaluation should succeed");
let rel = idb.get_relation("reachable").expect("reachable relation");
assert_eq!(rel.len(), 2);
assert!(rel.contains(&Fact::sym("reachable", &["x", "y"])));
assert!(rel.contains(&Fact::sym("reachable", &["y", "z"])));
}
#[test]
fn test_integer_fact_args() {
let mut edb = Edb::new();
edb.add_fact(Fact::new(
"score",
vec![FactArg::Symbol("alice".to_owned()), FactArg::Integer(99)],
));
let rule = Rule::new(
Atom::new("high_scorer", vec![Term::var("X")]),
vec![Atom::new("score", vec![Term::var("X"), Term::int(99)])],
);
let mut eval = SemiNaiveEvaluator::new(vec![rule], edb);
let idb = eval.evaluate().expect("evaluation should succeed");
let rel = idb.get_relation("high_scorer").expect("high_scorer");
assert_eq!(rel.len(), 1);
}
}