use mushroomdb::{AggFn, Direction, GraphDb, Predicate, RuleDef, Value, ViewDef, ViewSource};
use std::path::PathBuf;
type Db = GraphDb<core_storage::fs::RealFs>;
fn tmp(tag: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("reader-views-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
d
}
fn deg_view() -> ViewDef {
ViewDef {
name: "deg".into(),
label: "A".into(),
view_prop: "deg".into(),
source: ViewSource::Degree {
edge_type: "E".into(),
direction: Direction::Out,
},
}
}
fn agg_view(name: &str, agg: AggFn) -> ViewDef {
ViewDef {
name: name.into(),
label: "A".into(),
view_prop: name.into(),
source: ViewSource::NeighborAgg {
edge_type: "E".into(),
direction: Direction::Out,
agg,
prop: "score".into(),
},
}
}
fn assert_reader_agrees(db: &Db, keys: &[&str], when: &str) {
let reader = db.reader();
for key in keys {
let live = db.node_info(key).map(|n| n.props);
let read = reader.node_info(key).map(|n| n.props);
assert_eq!(read, live, "{when}: node {key}");
}
}
fn scored(db: &mut Db, key: &str, score: f64) {
db.insert_node("A", key, vec![("score".into(), Value::Float(score))])
.unwrap();
}
#[test]
fn a_degree_view_is_current_in_a_reader_after_an_edge_is_added() {
let dir = tmp("deg-add");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(deg_view()).unwrap();
for k in ["a", "b", "c"] {
db.insert_node("A", k, vec![]).unwrap();
}
db.insert_edge("E", "a", "b").unwrap();
db.insert_edge("E", "a", "c").unwrap();
let read = db.reader().node_info("a").unwrap().props;
assert_eq!(read.get("deg"), Some(&Value::Int(2)));
assert_reader_agrees(&db, &["a", "b", "c"], "two edges added");
}
#[test]
fn a_degree_view_is_current_in_a_reader_after_an_edge_is_removed() {
let dir = tmp("deg-del");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(deg_view()).unwrap();
for k in ["a", "b", "c"] {
db.insert_node("A", k, vec![]).unwrap();
}
db.insert_edge("E", "a", "b").unwrap();
db.insert_edge("E", "a", "c").unwrap();
db.delete_edge("E", "a", "b").unwrap();
let read = db.reader().node_info("a").unwrap().props;
assert_eq!(read.get("deg"), Some(&Value::Int(1)));
assert_reader_agrees(&db, &["a", "b", "c"], "one edge removed");
}
#[test]
fn an_aggregate_view_follows_a_neighbours_property_in_a_reader() {
let dir = tmp("agg-prop");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(agg_view("total", AggFn::Sum)).unwrap();
db.create_view(agg_view("avg", AggFn::Avg)).unwrap();
scored(&mut db, "a", 0.0);
scored(&mut db, "b", 3.0);
scored(&mut db, "c", 4.0);
db.insert_edge("E", "a", "b").unwrap();
db.insert_edge("E", "a", "c").unwrap();
db.set_prop("c", "score", Value::Float(10.0)).unwrap();
let read = db.reader().node_info("a").unwrap().props;
assert_eq!(read.get("total"), Some(&Value::Float(13.0)));
assert_eq!(read.get("avg"), Some(&Value::Float(6.5)));
assert_reader_agrees(&db, &["a", "b", "c"], "a neighbour's score changed");
}
#[test]
fn an_average_that_becomes_absent_after_a_snapshot_is_absent_in_a_reader() {
let dir = tmp("avg-absent");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(agg_view("avg", AggFn::Avg)).unwrap();
scored(&mut db, "a", 0.0);
scored(&mut db, "b", 3.0);
db.insert_edge("E", "a", "b").unwrap();
db.snapshot().unwrap();
db.delete_edge("E", "a", "b").unwrap();
assert_eq!(db.node_info("a").unwrap().props.get("avg"), None);
assert_eq!(db.reader().node_info("a").unwrap().props.get("avg"), None);
assert_reader_agrees(&db, &["a", "b"], "the only neighbour unlinked");
}
#[test]
fn a_view_created_since_the_last_fold_has_its_values_in_a_reader() {
let dir = tmp("create");
let mut db: Db = GraphDb::open(&dir).unwrap();
for k in ["a", "b", "c"] {
db.insert_node("A", k, vec![]).unwrap();
}
db.insert_edge("E", "a", "b").unwrap();
db.create_view(deg_view()).unwrap();
let read = db.reader().node_info("a").unwrap().props;
assert_eq!(read.get("deg"), Some(&Value::Int(1)));
assert_reader_agrees(&db, &["a", "b", "c"], "view created");
}
#[test]
fn a_deleted_views_property_is_gone_from_a_reader() {
let dir = tmp("delete");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(deg_view()).unwrap();
for k in ["a", "b"] {
db.insert_node("A", k, vec![]).unwrap();
}
db.insert_edge("E", "a", "b").unwrap();
db.snapshot().unwrap();
db.delete_view("deg").unwrap();
assert_eq!(db.reader().node_info("a").unwrap().props.get("deg"), None);
assert_reader_agrees(&db, &["a", "b"], "view deleted after a snapshot");
}
#[test]
fn a_view_over_a_derived_edge_is_current_in_a_reader() {
let dir = tmp("derived");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_rule(RuleDef {
name: "same_kind".into(),
src_label: "A".into(),
dst_label: "A".into(),
predicate: Predicate::FieldEqual {
field: "kind".into(),
},
edge_type: "E".into(),
weight_prop: None,
max_edges: None,
approximate: false,
via_label: None,
via_edge: None,
via_dir: None,
namespace: None,
})
.unwrap();
db.create_view(deg_view()).unwrap();
for k in ["a", "b", "c"] {
db.insert_node("A", k, vec![("kind".into(), Value::Str("x".into()))])
.unwrap();
}
db.set_prop("c", "kind", Value::Str("y".into())).unwrap();
assert_reader_agrees(&db, &["a", "b", "c"], "a rule's edges came and went");
let read = db.reader().node_info("a").unwrap().props;
assert_eq!(read.get("deg"), db.node_info("a").unwrap().props.get("deg"));
assert!(read.contains_key("deg"));
}
#[test]
fn a_reader_taken_earlier_keeps_the_view_value_of_its_own_commit() {
let dir = tmp("isolation");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(deg_view()).unwrap();
for k in ["a", "b", "c"] {
db.insert_node("A", k, vec![]).unwrap();
}
db.insert_edge("E", "a", "b").unwrap();
let earlier = db.reader();
db.insert_edge("E", "a", "c").unwrap();
let then = earlier.node_info("a").unwrap().props;
assert_eq!(then.get("deg"), Some(&Value::Int(1)));
let now = db.reader().node_info("a").unwrap().props;
assert_eq!(now.get("deg"), Some(&Value::Int(2)));
}
#[test]
fn a_reader_agrees_with_the_handle_through_a_run_of_writes_and_a_snapshot() {
let dir = tmp("run");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(deg_view()).unwrap();
db.create_view(agg_view("total", AggFn::Sum)).unwrap();
db.create_view(agg_view("low", AggFn::Min)).unwrap();
let keys = ["n0", "n1", "n2", "n3", "n4"];
for (i, k) in keys.iter().enumerate() {
scored(&mut db, k, i as f64);
}
let mut step = 7usize;
for round in 0..40 {
step = step.wrapping_mul(31).wrapping_add(11);
let a = keys[step % 5];
let b = keys[(step / 5) % 5];
match step % 7 {
0..=2 => {
let _ = db.insert_edge("E", a, b);
}
3 => {
let _ = db.delete_edge("E", a, b);
}
4 => {
let _ = db.set_prop(b, "score", Value::Float(round as f64));
}
5 => {
let _ = db.remove_prop(b, "score");
}
_ => {
if db.node_info(a).is_some() {
db.delete_node(a).unwrap();
} else {
scored(&mut db, a, 1.5);
}
}
}
if round == 15 {
db.snapshot().unwrap();
}
assert_reader_agrees(&db, &keys, &format!("round {round}"));
}
}
#[test]
fn view_values_moved_by_one_batch_are_current_in_a_reader() {
let dir = tmp("batch");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(deg_view()).unwrap();
db.create_view(agg_view("total", AggFn::Sum)).unwrap();
scored(&mut db, "a", 0.0);
scored(&mut db, "gone", 9.0);
db.insert_edge("E", "a", "gone").unwrap();
{
let mut b = db.batch();
b.insert_node("A", "b", vec![("score".into(), Value::Float(2.0))]);
b.insert_node("A", "c", vec![("score".into(), Value::Float(5.0))]);
b.insert_edge("E", "a", "b");
b.insert_edge("E", "a", "c");
b.delete_edge("E", "a", "gone");
b.set_prop("c", "score", Value::Float(6.0));
b.commit().unwrap();
}
let read = db.reader().node_info("a").unwrap().props;
assert_eq!(read.get("deg"), Some(&Value::Int(2)));
assert_eq!(read.get("total"), Some(&Value::Float(8.0)));
assert_reader_agrees(&db, &["a", "b", "c", "gone"], "one batch");
}
#[test]
fn a_neighbours_view_is_current_in_a_reader_after_a_snapshotted_node_is_deleted() {
let dir = tmp("delete-node");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(deg_view()).unwrap();
db.create_view(agg_view("avg", AggFn::Avg)).unwrap();
scored(&mut db, "a", 0.0);
scored(&mut db, "b", 3.0);
db.insert_edge("E", "a", "b").unwrap();
db.snapshot().unwrap();
db.delete_node("b").unwrap();
let read = db.reader().node_info("a").unwrap().props;
assert_eq!(read.get("deg"), Some(&Value::Int(0)));
assert_eq!(read.get("avg"), None);
assert_reader_agrees(&db, &["a", "b"], "the neighbour deleted");
}
#[test]
fn a_commit_that_moves_a_view_on_thousands_of_nodes_is_current_in_a_reader() {
let dir = tmp("hub");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(ViewDef {
name: "fans".into(),
label: "A".into(),
view_prop: "fans".into(),
source: ViewSource::Degree {
edge_type: "E".into(),
direction: Direction::In,
},
})
.unwrap();
db.insert_node("A", "hub", vec![]).unwrap();
let spokes: Vec<String> = (0..1500).map(|i| format!("s{i}")).collect();
{
let mut b = db.batch();
for s in &spokes {
b.insert_node("A", s, vec![]);
b.insert_edge("E", "hub", s);
}
b.commit().unwrap();
}
let reader = db.reader();
for s in [&spokes[0], &spokes[777], &spokes[1499]] {
let read = reader.node_info(s).unwrap().props;
assert_eq!(
read.get("fans"),
Some(&Value::Int(1)),
"{s} after the batch"
);
}
db.delete_node("hub").unwrap();
let reader = db.reader();
for s in [&spokes[0], &spokes[777], &spokes[1499]] {
let read = reader.node_info(s).unwrap().props;
assert_eq!(
read.get("fans"),
Some(&Value::Int(0)),
"{s} after the delete"
);
assert_eq!(db.node_info(s).unwrap().props, read);
}
}
fn load_in_batches(db: &mut Db, label: &str, batches: usize, per_batch: usize) {
for batch in 0..batches {
let mut b = db.batch();
for i in 0..per_batch {
b.insert_node(label, &format!("{label}-{batch}-{i}"), vec![]);
}
b.commit().unwrap();
}
}
#[test]
fn a_bulk_load_of_a_label_no_view_is_on_does_not_fold_on_every_batch() {
let dir = tmp("bulk-other");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(deg_view()).unwrap();
load_in_batches(&mut db, "N", 5, 1100);
assert_eq!(db.reader().deltas.len(), 5, "one delta for each batch");
}
#[test]
fn a_bulk_load_of_a_label_a_view_is_on_does_not_fold_on_every_batch() {
let dir = tmp("bulk-viewed");
let mut db: Db = GraphDb::open(&dir).unwrap();
db.create_view(deg_view()).unwrap();
load_in_batches(&mut db, "A", 5, 1100);
let reader = db.reader();
assert_eq!(reader.deltas.len(), 5, "one delta for each batch");
for key in ["A-0-0", "A-2-555", "A-4-1099"] {
let read = reader.node_info(key).unwrap().props;
assert_eq!(read.get("deg"), Some(&Value::Int(0)), "{key}");
}
}