use selene_core::{
DbString, GraphId, LabelSet, NodeId, PropertyMap, PropertyValueType, Value, db_string,
};
use smallvec::{SmallVec, smallvec};
use super::compact_core;
use crate::store::RowIndex;
use crate::{
EdgeEndpointDef, EdgeTypeDef, GraphTypeDef, NodeTypeDef, PropertyTypeDef, SeleneGraph,
SharedGraph, TypedIndexKind, ValidationMode,
};
fn prop(key: &str, value: Value) -> PropertyMap {
PropertyMap::from_pairs([(db_string(key).unwrap(), value)]).unwrap()
}
fn graph_with_indexed_deletion() -> (SharedGraph, DbString, DbString) {
let shared = SharedGraph::new(GraphId::new(1));
let la = db_string("cmp.idx").unwrap();
let name = db_string("name").unwrap();
shared
.create_property_index(la.clone(), name.clone(), TypedIndexKind::I64)
.unwrap();
let mut txn = shared.begin_write();
let n2 = {
let mut m = txn.mutator();
m.create_node(LabelSet::single(la.clone()), prop("name", Value::Int(11)))
.unwrap();
let n2 = m
.create_node(LabelSet::single(la.clone()), prop("name", Value::Int(22)))
.unwrap();
m.create_node(LabelSet::single(la.clone()), prop("name", Value::Int(33)))
.unwrap();
m.delete_node(n2).unwrap();
n2
};
txn.commit().unwrap();
assert_eq!(n2, NodeId::new(2));
(shared, la, name)
}
#[test]
fn compaction_rebuilds_property_index_dropping_reclaimed_rows() {
let (shared, la, name) = graph_with_indexed_deletion();
let before = shared.read();
let compacted = compact_core(&before).unwrap();
let g = &compacted.graph;
assert!(
g.property_index_for(&la, &name).is_some(),
"the property-index registration must survive compaction"
);
let resolve = |graph: &SeleneGraph, v: i64| -> Vec<NodeId> {
graph
.nodes_with_property_eq(&la, &name, &Value::Int(v))
.map(|rows| {
rows.iter()
.map(|r| graph.node_id_for_row(RowIndex::new(r)).unwrap())
.collect()
})
.unwrap_or_default()
};
assert_eq!(resolve(g, 11), vec![NodeId::new(1)]);
assert_eq!(resolve(g, 33), vec![NodeId::new(3)]);
assert!(
resolve(g, 22).is_empty(),
"reclaimed node 2's indexed value must not survive the rebuild"
);
assert!(resolve(&before, 22).is_empty());
}
fn graph_with_composite_indexed_deletion() -> (SharedGraph, DbString, SmallVec<[DbString; 4]>) {
let shared = SharedGraph::new(GraphId::new(1));
let la = db_string("cmp.cidx").unwrap();
let zone = db_string("zone").unwrap();
let rank = db_string("rank").unwrap();
let props: SmallVec<[DbString; 4]> = smallvec![zone.clone(), rank.clone()];
let mk = |z: &str, r: i64| {
PropertyMap::from_pairs([
(zone.clone(), Value::String(db_string(z).unwrap())),
(rank.clone(), Value::Int(r)),
])
.unwrap()
};
let mut txn = shared.begin_write();
let n2 = {
let mut m = txn.mutator();
m.create_composite_property_index_named(
la.clone(),
props.clone(),
smallvec![TypedIndexKind::String, TypedIndexKind::I64],
None,
)
.unwrap();
m.create_node(LabelSet::single(la.clone()), mk("north", 1))
.unwrap(); let n2 = m
.create_node(LabelSet::single(la.clone()), mk("south", 2))
.unwrap(); m.create_node(LabelSet::single(la.clone()), mk("north", 3))
.unwrap(); m.delete_node(n2).unwrap();
n2
};
txn.commit().unwrap();
assert_eq!(n2, NodeId::new(2));
(shared, la, props)
}
#[test]
fn compaction_rebuilds_composite_property_index() {
let (shared, la, props) = graph_with_composite_indexed_deletion();
let before = shared.read();
let compacted = compact_core(&before).unwrap();
let g = &compacted.graph;
let entry = g
.composite_property_index_entry_for(&la, &props)
.expect("composite registration must survive compaction");
assert_eq!(entry.declared_properties.as_slice(), props.as_slice());
let resolve = |graph: &SeleneGraph, z: &str, r: i64| -> Vec<NodeId> {
let Some(entry) = graph.composite_property_index_entry_for(&la, &props) else {
return Vec::new();
};
let zone_v = Value::String(db_string(z).unwrap());
let rank_v = Value::Int(r);
let refs: Vec<&Value> = vec![&zone_v, &rank_v];
let Ok(key) = entry.index.key_from_values(&refs) else {
return Vec::new();
};
entry
.index
.lookup_key(&key)
.map(|bitmap| {
bitmap
.iter()
.map(|row| graph.node_id_for_row(RowIndex::new(row)).unwrap())
.collect()
})
.unwrap_or_default()
};
assert_eq!(resolve(g, "north", 1), vec![NodeId::new(1)]);
assert_eq!(resolve(g, "north", 3), vec![NodeId::new(3)]);
assert!(
resolve(g, "south", 2).is_empty(),
"reclaimed node 2's composite key must not survive the rebuild"
);
}
fn person_only_graph_type() -> GraphTypeDef {
GraphTypeDef {
name: db_string("cmp.closed.graph").unwrap(),
node_types: vec![NodeTypeDef {
name: db_string("cmp.closed.person").unwrap(),
key_labels: LabelSet::single(db_string("Person").unwrap()),
properties: vec![PropertyTypeDef {
name: db_string("name").unwrap(),
value_type: PropertyValueType::String,
list_element_type: None,
required: true,
default: None,
immutable: false,
unique: false,
decimal_type: None,
character_string_type: None,
byte_string_type: None,
record_field_types: None,
}],
validation_mode: ValidationMode::Strict,
}],
edge_types: vec![EdgeTypeDef {
name: db_string("cmp.closed.knows").unwrap(),
label: db_string("KNOWS").unwrap(),
source_node_type: EdgeEndpointDef::NodeType(0),
target_node_type: EdgeEndpointDef::NodeType(0),
properties: vec![],
validation_mode: ValidationMode::Strict,
}],
}
}
#[test]
fn compaction_preserves_closed_graph_binding_and_revalidates() {
let shared = SharedGraph::builder(GraphId::new(77))
.bound_to(person_only_graph_type())
.unwrap()
.build()
.unwrap();
let person = db_string("Person").unwrap();
let knows = db_string("KNOWS").unwrap();
let name = db_string("name").unwrap();
let mk = |n: &str| {
PropertyMap::from_pairs([(name.clone(), Value::String(db_string(n).unwrap()))]).unwrap()
};
let mut txn = shared.begin_write();
{
let mut m = txn.mutator();
let p1 = m
.create_node(LabelSet::single(person.clone()), mk("ann"))
.unwrap();
let p2 = m
.create_node(LabelSet::single(person.clone()), mk("bob"))
.unwrap();
let p3 = m
.create_node(LabelSet::single(person.clone()), mk("cy"))
.unwrap();
m.create_edge(knows.clone(), p1, p3, PropertyMap::new())
.unwrap(); m.create_edge(knows, p1, p2, PropertyMap::new()).unwrap(); m.delete_node(p2).unwrap();
assert_eq!(
(p1, p2, p3),
(NodeId::new(1), NodeId::new(2), NodeId::new(3))
);
}
txn.commit().unwrap();
let before = shared.read();
let compacted = compact_core(&before).unwrap();
assert!(compacted.graph.meta.bound_type.is_some());
assert_eq!(
compacted
.graph
.meta
.bound_type
.as_ref()
.map(|t| t.name.clone()),
before.meta.bound_type.as_ref().map(|t| t.name.clone()),
);
let republished = SharedGraph::from_graph(compacted.graph);
{
let mut txn = republished.begin_write();
{
let mut m = txn.mutator();
m.create_node(LabelSet::single(person.clone()), mk("dot"))
.unwrap();
}
txn.commit().expect("a conforming insert must still commit");
}
{
let mut txn = republished.begin_write();
{
let mut m = txn.mutator();
let _ = m.create_node(LabelSet::single(person), PropertyMap::new());
}
assert!(
txn.commit().is_err(),
"the bound type must still reject a non-conforming node after compaction"
);
}
let g = republished.read();
assert!(g.is_node_alive(NodeId::new(1)));
assert!(g.is_node_alive(NodeId::new(3)));
assert!(g.row_for_node_id(NodeId::new(2)).is_none());
}