use core_api::{Direction, GraphDb, Predicate, RuleDef, Value};
fn tmp(name: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("graphdb-any-{}-{}", name, std::process::id()));
let _ = std::fs::remove_dir_all(&d);
d
}
fn mk_tags(items: &[&str]) -> Value {
Value::List(items.iter().map(|s| Value::Str((*s).into())).collect())
}
#[test]
fn any_two_branch_overlap_or_numeric_derives_edges() {
let dir = tmp("two-branch");
let mut db = GraphDb::open(&dir).unwrap();
db.create_rule(RuleDef {
name: "any_test".into(),
src_label: "N".into(),
dst_label: "N".into(),
predicate: Predicate::Any(vec![
Predicate::Overlap {
field: "tags".into(),
min: 0.3,
},
Predicate::NumericWithin {
field: "year".into(),
tolerance: 5.0,
},
]),
edge_type: "ANY".into(),
weight_prop: Some("score".into()),
max_edges: None,
approximate: false,
via_label: None,
via_edge: None,
via_dir: None,
})
.unwrap();
db.insert_node(
"N",
"a",
vec![
("tags".into(), mk_tags(&["x", "y"])),
("year".into(), Value::Int(2000)),
],
)
.unwrap();
db.insert_node(
"N",
"b",
vec![
("tags".into(), mk_tags(&["y", "z"])),
("year".into(), Value::Int(2010)),
],
)
.unwrap();
db.insert_node(
"N",
"c",
vec![
("tags".into(), mk_tags(&["p", "q"])),
("year".into(), Value::Int(2003)),
],
)
.unwrap();
db.insert_node(
"N",
"d",
vec![
("tags".into(), mk_tags(&["p", "q"])),
("year".into(), Value::Int(2050)),
],
)
.unwrap();
let a_out: Vec<String> = db.neighbors("a", "ANY", Direction::Out).unwrap_or_default();
assert!(
a_out.contains(&"b".to_string()),
"a→b must exist (Overlap branch fires); got {a_out:?}"
);
assert!(
a_out.contains(&"c".to_string()),
"a→c must exist (NumericWithin branch fires); got {a_out:?}"
);
assert!(
!a_out.contains(&"d".to_string()),
"a→d must not exist (no branch fires); got {a_out:?}"
);
}
#[test]
fn any_nested_in_all_derives_edges() {
let dir = tmp("nested");
let mut db = GraphDb::open(&dir).unwrap();
db.create_rule(RuleDef {
name: "nested".into(),
src_label: "N".into(),
dst_label: "N".into(),
predicate: Predicate::All(vec![
Predicate::FieldEqual {
field: "ind".into(),
},
Predicate::Any(vec![
Predicate::Overlap {
field: "tags".into(),
min: 0.3,
},
Predicate::NumericWithin {
field: "year".into(),
tolerance: 5.0,
},
]),
]),
edge_type: "NESTED".into(),
weight_prop: Some("score".into()),
max_edges: None,
approximate: false,
via_label: None,
via_edge: None,
via_dir: None,
})
.unwrap();
db.insert_node(
"N",
"a",
vec![
("ind".into(), Value::Str("arch".into())),
("tags".into(), mk_tags(&["x", "y"])),
("year".into(), Value::Int(2000)),
],
)
.unwrap();
db.insert_node(
"N",
"b",
vec![
("ind".into(), Value::Str("arch".into())),
("tags".into(), mk_tags(&["y", "z"])),
("year".into(), Value::Int(2020)),
],
)
.unwrap();
db.insert_node(
"N",
"c",
vec![
("ind".into(), Value::Str("arch".into())),
("tags".into(), mk_tags(&["p", "q"])),
("year".into(), Value::Int(2003)),
],
)
.unwrap();
db.insert_node(
"N",
"d",
vec![
("ind".into(), Value::Str("law".into())),
("tags".into(), mk_tags(&["y", "z"])),
("year".into(), Value::Int(2001)),
],
)
.unwrap();
let a_out: Vec<String> = db
.neighbors("a", "NESTED", Direction::Out)
.unwrap_or_default();
assert!(
a_out.contains(&"b".to_string()),
"a→b: same ind + tag overlap → must exist; got {a_out:?}"
);
assert!(
a_out.contains(&"c".to_string()),
"a→c: same ind + year proximity → must exist; got {a_out:?}"
);
assert!(
!a_out.contains(&"d".to_string()),
"a→d: different ind → must not exist; got {a_out:?}"
);
}
#[test]
fn any_score_is_max_over_satisfied_branches() {
let dir = tmp("maxscore");
let mut db = GraphDb::open(&dir).unwrap();
db.create_rule(RuleDef {
name: "maxscore".into(),
src_label: "N".into(),
dst_label: "N".into(),
predicate: Predicate::Any(vec![
Predicate::FieldEqual {
field: "ind".into(),
},
Predicate::NumericWithin {
field: "year".into(),
tolerance: 3.0,
},
]),
edge_type: "MS".into(),
weight_prop: Some("score".into()),
max_edges: None,
approximate: false,
via_label: None,
via_edge: None,
via_dir: None,
})
.unwrap();
db.insert_node(
"N",
"a",
vec![
("ind".into(), Value::Str("arch".into())),
("year".into(), Value::Int(2000)),
],
)
.unwrap();
db.insert_node(
"N",
"b",
vec![
("ind".into(), Value::Str("arch".into())),
("year".into(), Value::Int(2001)),
],
)
.unwrap();
let explain = db.explain("a", "b").unwrap();
let entry = explain
.iter()
.find(|e| e.rule == "maxscore" && e.src_key == "a" && e.dst_key == "b")
.expect("a→b must have an explain entry for 'maxscore'");
let w = entry
.weight
.expect("weight must be present (weight_prop set)");
assert!(
(w - 1.0).abs() < 1e-9,
"Any score must be max(1.0, 2/3) = 1.0; got {w}"
);
db.insert_node(
"N",
"c",
vec![
("ind".into(), Value::Str("law".into())),
("year".into(), Value::Int(2002)),
],
)
.unwrap();
let explain_c = db.explain("a", "c").unwrap();
let entry_c = explain_c
.iter()
.find(|e| e.rule == "maxscore" && e.src_key == "a" && e.dst_key == "c")
.expect("a→c must have an explain entry");
let wc = entry_c.weight.expect("weight present");
assert!(
(wc - 1.0 / 3.0).abs() < 1e-9,
"Any score (only numeric branch fires, year diff=2, tol=3) must be 1/3; got {wc}"
);
}
#[test]
fn any_retraction_when_sole_branch_breaks() {
let dir = tmp("retract");
let mut db = GraphDb::open(&dir).unwrap();
db.create_rule(RuleDef {
name: "ret".into(),
src_label: "N".into(),
dst_label: "N".into(),
predicate: Predicate::Any(vec![
Predicate::FieldEqual {
field: "ind".into(),
},
Predicate::NumericWithin {
field: "year".into(),
tolerance: 2.0,
},
]),
edge_type: "RET".into(),
weight_prop: None,
max_edges: None,
approximate: false,
via_label: None,
via_edge: None,
via_dir: None,
})
.unwrap();
db.insert_node(
"N",
"a",
vec![
("ind".into(), Value::Str("arch".into())),
("year".into(), Value::Int(2000)),
],
)
.unwrap();
db.insert_node(
"N",
"b",
vec![
("ind".into(), Value::Str("law".into())),
("year".into(), Value::Int(2001)),
],
)
.unwrap();
let a_out = db.neighbors("a", "RET", Direction::Out).unwrap_or_default();
assert!(
a_out.contains(&"b".to_string()),
"a→b must exist initially (NumericWithin branch fires); got {a_out:?}"
);
db.set_prop("b", "year", Value::Int(2005)).unwrap();
let a_out2 = db.neighbors("a", "RET", Direction::Out).unwrap_or_default();
assert!(
!a_out2.contains(&"b".to_string()),
"a→b must be retracted after year change breaks the sole matching branch; got {a_out2:?}"
);
db.set_prop("b", "ind", Value::Str("arch".into())).unwrap();
let a_out3 = db.neighbors("a", "RET", Direction::Out).unwrap_or_default();
assert!(
a_out3.contains(&"b".to_string()),
"a→b must re-derive when FieldEqual branch fires; got {a_out3:?}"
);
}
#[test]
fn any_edge_retained_when_one_branch_holds() {
let dir = tmp("retain-one");
let mut db = GraphDb::open(&dir).unwrap();
db.create_rule(RuleDef {
name: "ret2".into(),
src_label: "N".into(),
dst_label: "N".into(),
predicate: Predicate::Any(vec![
Predicate::FieldEqual {
field: "ind".into(),
},
Predicate::NumericWithin {
field: "year".into(),
tolerance: 10.0,
},
]),
edge_type: "R2".into(),
weight_prop: Some("score".into()),
max_edges: None,
approximate: false,
via_label: None,
via_edge: None,
via_dir: None,
})
.unwrap();
db.insert_node(
"N",
"src",
vec![
("ind".into(), Value::Str("arch".into())),
("year".into(), Value::Int(2000)),
],
)
.unwrap();
db.insert_node(
"N",
"dst",
vec![
("ind".into(), Value::Str("arch".into())),
("year".into(), Value::Int(2004)),
],
)
.unwrap();
let out0 = db
.neighbors("src", "R2", Direction::Out)
.unwrap_or_default();
assert!(
out0.contains(&"dst".to_string()),
"src→dst must exist initially; got {out0:?}"
);
let explain0 = db.explain("src", "dst").unwrap();
let e0 = explain0
.iter()
.find(|e| e.rule == "ret2")
.expect("explain entry for ret2");
let w0 = e0.weight.expect("weight present");
assert!(
(w0 - 1.0).abs() < 1e-9,
"initial weight must be max(1.0, 0.6) = 1.0; got {w0}"
);
db.set_prop("dst", "ind", Value::Str("law".into())).unwrap();
let out1 = db
.neighbors("src", "R2", Direction::Out)
.unwrap_or_default();
assert!(
out1.contains(&"dst".to_string()),
"src→dst must be RETAINED after FieldEqual branch breaks (NumericWithin still holds); got {out1:?}"
);
let explain1 = db.explain("src", "dst").unwrap();
let e1 = explain1
.iter()
.find(|e| e.rule == "ret2")
.expect("explain entry for ret2 after branch-A break");
let w1 = e1.weight.expect("weight present after branch-A break");
assert!(
(w1 - 0.6).abs() < 1e-9,
"weight must update to branch-B score (0.6) after branch-A breaks; got {w1}"
);
db.set_prop("dst", "year", Value::Int(2050)).unwrap();
let out2 = db
.neighbors("src", "R2", Direction::Out)
.unwrap_or_default();
assert!(
!out2.contains(&"dst".to_string()),
"src→dst must be retracted after both branches break; got {out2:?}"
);
}
#[test]
fn any_with_max_edges_score_change_causes_evict_backfill() {
let dir = tmp("topk");
let mut db = GraphDb::open(&dir).unwrap();
db.create_rule(RuleDef {
name: "topk_any".into(),
src_label: "N".into(),
dst_label: "N".into(),
predicate: Predicate::Any(vec![
Predicate::FieldEqual {
field: "ind".into(),
},
Predicate::NumericWithin {
field: "year".into(),
tolerance: 10.0,
},
]),
edge_type: "TK".into(),
weight_prop: Some("score".into()),
max_edges: Some(1),
approximate: false,
via_label: None,
via_edge: None,
via_dir: None,
})
.unwrap();
db.insert_node(
"N",
"src",
vec![
("ind".into(), Value::Str("arch".into())),
("year".into(), Value::Int(2000)),
],
)
.unwrap();
db.insert_node(
"N",
"d_low",
vec![
("ind".into(), Value::Str("law".into())),
("year".into(), Value::Int(2009)),
],
)
.unwrap();
db.insert_node(
"N",
"d_high",
vec![
("ind".into(), Value::Str("arch".into())),
("year".into(), Value::Int(2020)),
],
)
.unwrap();
let top1: Vec<String> = db
.neighbors("src", "TK", Direction::Out)
.unwrap_or_default();
assert_eq!(
top1,
vec!["d_high"],
"top-1 must be d_high (score 1.0 > 0.1); got {top1:?}"
);
db.set_prop("d_high", "ind", Value::Str("law".into()))
.unwrap();
let top1_after: Vec<String> = db
.neighbors("src", "TK", Direction::Out)
.unwrap_or_default();
assert_eq!(
top1_after,
vec!["d_low"],
"d_low must backfill after d_high loses its only matching branch; got {top1_after:?}"
);
db.set_prop("d_high", "ind", Value::Str("arch".into()))
.unwrap();
let top1_restored: Vec<String> = db
.neighbors("src", "TK", Direction::Out)
.unwrap_or_default();
assert_eq!(
top1_restored,
vec!["d_high"],
"d_high must reclaim top-1 after ind restored; got {top1_restored:?}"
);
}
#[test]
fn any_snapshot_v4_roundtrip() {
let dir = tmp("snap");
{
let mut db = GraphDb::open(&dir).unwrap();
db.create_rule(RuleDef {
name: "any_snap".into(),
src_label: "N".into(),
dst_label: "N".into(),
predicate: Predicate::Any(vec![
Predicate::FieldEqual {
field: "ind".into(),
},
Predicate::NumericWithin {
field: "year".into(),
tolerance: 3.0,
},
]),
edge_type: "SNAP".into(),
weight_prop: Some("score".into()),
max_edges: None,
approximate: false,
via_label: None,
via_edge: None,
via_dir: None,
})
.unwrap();
db.insert_node(
"N",
"a",
vec![
("ind".into(), Value::Str("arch".into())),
("year".into(), Value::Int(2000)),
],
)
.unwrap();
db.insert_node(
"N",
"b",
vec![
("ind".into(), Value::Str("arch".into())),
("year".into(), Value::Int(2001)),
],
)
.unwrap();
db.snapshot().unwrap();
db.insert_node(
"N",
"c",
vec![
("ind".into(), Value::Str("law".into())),
("year".into(), Value::Int(2002)),
],
)
.unwrap();
}
let db = GraphDb::open(&dir).unwrap();
assert_eq!(db.rules().len(), 1, "rule must survive snapshot+WAL replay");
assert_eq!(db.rules()[0].name, "any_snap");
let a_out = db
.neighbors("a", "SNAP", Direction::Out)
.unwrap_or_default();
assert!(
a_out.contains(&"b".to_string()),
"a→b must survive snapshot round-trip; got {a_out:?}"
);
assert!(
a_out.contains(&"c".to_string()),
"a→c must be derived after WAL replay; got {a_out:?}"
);
}
#[test]
fn any_bincode_roundtrip_and_old_records_still_decode() {
let rule = RuleDef {
name: "bc".into(),
src_label: "A".into(),
dst_label: "B".into(),
predicate: Predicate::Any(vec![
Predicate::FieldEqual { field: "f".into() },
Predicate::Overlap {
field: "tags".into(),
min: 0.5,
},
]),
edge_type: "E".into(),
weight_prop: None,
max_edges: None,
approximate: false,
via_label: None,
via_edge: None,
via_dir: None,
};
let bytes = bincode::serialize(&rule).unwrap();
let decoded: RuleDef = bincode::deserialize(&bytes).unwrap();
assert_eq!(rule, decoded, "Any RuleDef must round-trip via bincode");
let old = RuleDef {
name: "r".into(),
src_label: "A".into(),
dst_label: "B".into(),
predicate: Predicate::VectorSimilar {
field: "emb".into(),
min: 0.9,
},
edge_type: "E".into(),
weight_prop: None,
max_edges: None,
approximate: false,
via_label: None,
via_edge: None,
via_dir: None,
};
let old_bytes = bincode::serialize(&old).unwrap();
let old_decoded: RuleDef = bincode::deserialize(&old_bytes).unwrap();
assert_eq!(
old, old_decoded,
"pre-Any VectorSimilar record must still decode"
);
}