use roaring::RoaringBitmap;
use selene_core::{DbString, EdgeId, GraphId, LabelSet, NodeId, PropertyMap, Value, db_string};
use smallvec::smallvec;
use crate::adjacency::AdjacencyEdge;
use crate::graph::{CompositePropertyIndexEntry, PropertyIndexEntry, SeleneGraph};
use crate::{CompositeTypedIndex, SharedGraph, TypedIndex, TypedIndexKind};
fn label(name: &str) -> DbString {
db_string(name).unwrap()
}
fn consistent_graph() -> SeleneGraph {
let shared = SharedGraph::new(GraphId::new(1));
let person = label("consistency.person");
let age = label("consistency.age");
let knows = label("consistency.knows");
let (a, b) = {
let mut txn = shared.begin_write();
let (a, b);
{
let mut mutator = txn.mutator();
a = mutator
.create_node(
LabelSet::single(person.clone()),
PropertyMap::from_pairs([(age, Value::Int(30))]).unwrap(),
)
.unwrap();
b = mutator
.create_node(LabelSet::single(person), PropertyMap::new())
.unwrap();
mutator
.create_edge(knows, a, b, PropertyMap::new())
.unwrap();
}
txn.commit().unwrap();
(a, b)
};
let _ = (a, b);
(*shared.read()).clone()
}
#[test]
fn consistent_graph_passes() {
let graph = consistent_graph();
assert!(graph.assert_indexes_consistent().is_ok());
}
#[test]
fn stray_label_bitmap_row_is_caught() {
let mut graph = consistent_graph();
let person = label("consistency.person");
graph.idx_label.entry(person).or_default().insert(999);
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(err.contains("node label index"), "got: {err}");
}
#[test]
fn empty_label_bitmap_is_caught() {
let mut graph = consistent_graph();
let ghost = label("consistency.ghost");
graph.idx_label.insert(ghost, RoaringBitmap::new());
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(err.contains("present-but-empty"), "got: {err}");
}
#[test]
fn missing_label_bitmap_key_is_caught() {
let mut graph = consistent_graph();
let person = label("consistency.person");
graph.idx_label.remove(&person);
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(
err.contains("missing from the maintained index"),
"got: {err}"
);
}
#[test]
fn stray_edge_label_bitmap_row_is_caught() {
let mut graph = consistent_graph();
let knows = label("consistency.knows");
graph.idx_edge_label.entry(knows).or_default().insert(777);
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(err.contains("edge label index"), "got: {err}");
}
#[test]
fn drifted_property_index_is_caught() {
let mut graph = consistent_graph();
let person = label("consistency.person");
let age = label("consistency.age");
let index = crate::property_index::build_property_index_lenient(
&graph,
person.clone(),
age.clone(),
TypedIndexKind::I64,
)
.unwrap();
graph.property_index.insert(
(person.clone(), age.clone()),
PropertyIndexEntry::new(index, None),
);
assert!(graph.assert_indexes_consistent().is_ok());
let mut corrupt = TypedIndex::new(TypedIndexKind::I64);
corrupt.insert(&Value::Int(30), 0).unwrap();
corrupt.insert(&Value::Int(30), 5).unwrap(); graph
.property_index
.insert((person, age), PropertyIndexEntry::new(corrupt, None));
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(err.contains("property index"), "got: {err}");
assert!(err.contains("drifted"), "got: {err}");
}
#[test]
fn empty_property_index_bucket_is_caught() {
use std::collections::BTreeMap;
let mut graph = consistent_graph();
let person = label("consistency.person");
let age = label("consistency.age");
let mut buckets: BTreeMap<i64, RoaringBitmap> = BTreeMap::new();
let mut populated = RoaringBitmap::new();
populated.insert(0);
buckets.insert(30, populated);
buckets.insert(99, RoaringBitmap::new()); let leaky = TypedIndex::I64(buckets);
graph
.property_index
.insert((person, age), PropertyIndexEntry::new(leaky, None));
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(err.contains("present-but-empty"), "got: {err}");
}
#[test]
fn drifted_composite_index_is_caught() {
let mut graph = consistent_graph();
let person = label("consistency.person");
let age = label("consistency.age");
let name = label("consistency.name");
let props: smallvec::SmallVec<[DbString; 4]> = smallvec![age, name];
let kinds: smallvec::SmallVec<[TypedIndexKind; 4]> =
smallvec![TypedIndexKind::I64, TypedIndexKind::String];
let index = crate::composite_property_index::build_composite_property_index_lenient(
&graph,
person.clone(),
props.clone(),
kinds.clone(),
)
.unwrap();
let key = crate::graph::composite_property_key(&props);
graph.composite_property_index.insert(
(person.clone(), key.clone()),
CompositePropertyIndexEntry::new(index, props.clone(), None),
);
assert!(graph.assert_indexes_consistent().is_ok());
let mut corrupt = CompositeTypedIndex::new(kinds);
let v_age = Value::Int(30);
let v_name = Value::String(label("consistency.bogus"));
corrupt.insert(&[&v_age, &v_name], 0).unwrap();
graph.composite_property_index.insert(
(person, key),
CompositePropertyIndexEntry::new(corrupt, props, None),
);
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(err.contains("composite index"), "got: {err}");
}
#[test]
fn dangling_adjacency_to_missing_edge_is_caught() {
let mut graph = consistent_graph();
let knows = label("consistency.knows");
graph
.adjacency_out
.entry(NodeId::new(1))
.or_default()
.add(AdjacencyEdge {
label: knows,
neighbor: NodeId::new(2),
edge_id: EdgeId::new(900),
});
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(err.contains("adjacency"), "got: {err}");
}
#[test]
fn empty_adjacency_entry_is_caught() {
let mut graph = consistent_graph();
graph
.adjacency_out
.insert(NodeId::new(2), crate::adjacency::AdjacencyEntry::new());
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(err.contains("present-but-empty"), "got: {err}");
}
#[test]
fn out_of_range_alive_node_is_caught() {
let mut graph = consistent_graph();
graph.node_store.alive_mut().insert(500);
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(err.contains("out-of-range"), "got: {err}");
}
#[test]
fn column_length_mismatch_is_caught() {
let mut graph = consistent_graph();
graph
.node_store
.labels
.push(LabelSet::single(label("consistency.extra")));
let err = graph.assert_indexes_consistent().unwrap_err();
assert!(err.contains("column length mismatch"), "got: {err}");
}