use super::*;
use selene_core::{
EdgeId, GraphId, LabelSet, NodeId, PropertyMap, PropertyValueType, Value, db_string,
};
use crate::store::RowIndex;
use crate::{GraphTypeDef, NodeTypeDef, PropertyTypeDef, ValidationMode};
fn prop(key: &str, value: Value) -> PropertyMap {
PropertyMap::from_pairs([(db_string(key).unwrap(), value)]).unwrap()
}
fn churned_graph() -> SharedGraph {
let shared = SharedGraph::new(GraphId::new(1));
let la = db_string("c4c.node").unwrap();
let mut txn = shared.begin_write();
{
let mut m = txn.mutator();
for i in 1..=5 {
m.create_node(LabelSet::single(la.clone()), prop("n", Value::Int(i)))
.unwrap();
}
m.delete_node(NodeId::new(2)).unwrap();
m.delete_node(NodeId::new(4)).unwrap();
}
txn.commit().unwrap();
shared
}
#[test]
fn compact_densifies_live_graph_and_reclaims_dead_rows() {
let shared = churned_graph();
{
let before = shared.read();
assert_eq!(before.node_store.len(), 5, "dead rows still occupy RAM");
assert_eq!(before.node_count(), 3);
}
let report = shared.compact().unwrap();
assert_eq!(
report.reclaimed_nodes, 2,
"the two deleted rows are reclaimed"
);
let g = shared.read();
assert_eq!(g.node_store.len(), 3, "store densified in place");
assert_eq!(g.node_count(), 3);
assert_eq!(g.row_for_node_id(NodeId::new(1)), Some(RowIndex::new(0)));
assert_eq!(g.row_for_node_id(NodeId::new(3)), Some(RowIndex::new(1)));
assert_eq!(g.row_for_node_id(NodeId::new(5)), Some(RowIndex::new(2)));
assert!(g.row_for_node_id(NodeId::new(2)).is_none());
assert!(g.row_for_node_id(NodeId::new(4)).is_none());
assert_eq!(g.meta.next_node_id, 6);
}
#[test]
fn compaction_stats_track_delete_pressure_and_dense_rebuild() {
let shared = churned_graph();
let before = shared.compaction_stats();
assert_eq!(before.allocated_nodes, 5);
assert_eq!(before.live_nodes, 3);
assert_eq!(before.reclaimable_nodes, 2);
assert_eq!(before.allocated_edges, 0);
assert_eq!(before.live_edges, 0);
assert_eq!(before.reclaimable_edges, 0);
assert_eq!(before.allocated_rows(), 5);
assert_eq!(before.live_rows(), 3);
assert_eq!(before.reclaimable_rows(), 2);
assert!(!before.is_dense());
let report = shared.compact().unwrap();
assert_eq!(report.reclaimed_nodes, before.reclaimable_nodes);
assert_eq!(report.reclaimed_edges, before.reclaimable_edges);
let after = shared.compaction_stats();
assert_eq!(after.allocated_nodes, 3);
assert_eq!(after.live_nodes, 3);
assert_eq!(after.reclaimable_nodes, 0);
assert_eq!(after.allocated_rows(), 3);
assert_eq!(after.live_rows(), 3);
assert_eq!(after.reclaimable_rows(), 0);
assert!(after.is_dense());
}
#[test]
fn compact_preserves_observable_reads() {
let shared = churned_graph();
let before = shared.read();
shared.compact().unwrap();
let g = shared.read();
for id in [NodeId::new(1), NodeId::new(3), NodeId::new(5)] {
assert_eq!(g.node_labels(id), before.node_labels(id), "labels {id}");
assert_eq!(
g.node_properties(id),
before.node_properties(id),
"properties {id}"
);
}
}
#[test]
fn create_after_compact_appends_without_rebloat() {
let shared = churned_graph();
shared.compact().unwrap();
let new_id = {
let mut txn = shared.begin_write();
let id = txn
.mutator()
.create_node(
LabelSet::single(db_string("c4c.node").unwrap()),
PropertyMap::new(),
)
.unwrap();
txn.commit().unwrap();
id
};
let g = shared.read();
assert_eq!(new_id, NodeId::new(6), "preserved high-water, no reuse");
assert_eq!(
g.row_for_node_id(new_id),
Some(RowIndex::new(3)),
"appended at the dense end, not the high-water arith row"
);
assert_eq!(g.node_store.len(), 4, "dense, no re-bloat");
assert!(g.is_node_alive(NodeId::new(1)) && g.is_node_alive(NodeId::new(6)));
assert!(g.row_for_node_id(NodeId::new(2)).is_none());
}
#[test]
fn compact_on_a_dense_graph_is_a_noop() {
let shared = SharedGraph::new(GraphId::new(1));
let la = db_string("c4c.dense").unwrap();
{
let mut txn = shared.begin_write();
{
let mut m = txn.mutator();
m.create_node(LabelSet::single(la.clone()), PropertyMap::new())
.unwrap();
m.create_node(LabelSet::single(la), PropertyMap::new())
.unwrap();
}
txn.commit().unwrap();
}
let report = shared.compact().unwrap();
assert_eq!(report.reclaimed_nodes, 0);
assert_eq!(report.reclaimed_edges, 0);
let g = shared.read();
assert_eq!(g.node_store.len(), 2);
assert_eq!(g.row_for_node_id(NodeId::new(1)), Some(RowIndex::new(0)));
assert_eq!(g.row_for_node_id(NodeId::new(2)), Some(RowIndex::new(1)));
}
#[test]
fn compact_preserves_edges_and_adjacency() {
let shared = SharedGraph::new(GraphId::new(1));
let la = db_string("c4c.n").unwrap();
let el = db_string("c4c.e").unwrap();
let (n1, n3, n4) = {
let mut txn = shared.begin_write();
let ids = {
let mut m = txn.mutator();
let n1 = m
.create_node(LabelSet::single(la.clone()), PropertyMap::new())
.unwrap();
let n2 = m
.create_node(LabelSet::single(la.clone()), PropertyMap::new())
.unwrap();
let n3 = m
.create_node(LabelSet::single(la.clone()), PropertyMap::new())
.unwrap();
let n4 = m
.create_node(LabelSet::single(la), PropertyMap::new())
.unwrap();
m.create_edge(el.clone(), n1, n2, PropertyMap::new())
.unwrap(); m.create_edge(el.clone(), n3, n4, PropertyMap::new())
.unwrap(); m.create_edge(el, n1, n4, PropertyMap::new()).unwrap(); m.delete_node(n2).unwrap(); (n1, n3, n4)
};
txn.commit().unwrap();
ids
};
let before = shared.compaction_stats();
assert_eq!(before.reclaimable_nodes, 1);
assert_eq!(before.reclaimable_edges, 1);
assert_eq!(before.reclaimable_rows(), 2);
let report = shared.compact().unwrap();
assert_eq!(report.reclaimed_nodes, before.reclaimable_nodes);
assert_eq!(report.reclaimed_edges, before.reclaimable_edges);
let g = shared.read();
assert_eq!(g.edge_endpoints(EdgeId::new(2)), Some((n3, n4)));
assert_eq!(g.edge_endpoints(EdgeId::new(3)), Some((n1, n4)));
assert!(
g.row_for_edge_id(EdgeId::new(1)).is_none(),
"the cascade-deleted edge was reclaimed"
);
assert!(
g.outgoing_edges(n1)
.unwrap()
.iter()
.any(|e| e.edge_id == EdgeId::new(3) && e.neighbor == n4)
);
let n4_incoming: Vec<_> = g
.incoming_edges(n4)
.unwrap()
.iter()
.map(|e| e.edge_id)
.collect();
assert!(n4_incoming.contains(&EdgeId::new(2)) && n4_incoming.contains(&EdgeId::new(3)));
}
fn person_graph_type() -> GraphTypeDef {
GraphTypeDef {
name: db_string("c4c.closed").unwrap(),
node_types: vec![NodeTypeDef {
name: db_string("c4c.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::new(),
}
}
#[test]
fn compact_preserves_closed_graph_binding_on_the_live_path() {
let shared = SharedGraph::builder(GraphId::new(9))
.bound_to(person_graph_type())
.unwrap()
.build()
.unwrap();
let person = db_string("Person").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();
m.create_node(LabelSet::single(person.clone()), mk("ann"))
.unwrap();
let bob = m
.create_node(LabelSet::single(person.clone()), mk("bob"))
.unwrap();
m.create_node(LabelSet::single(person.clone()), mk("cy"))
.unwrap();
m.delete_node(bob).unwrap();
}
txn.commit().unwrap();
}
shared.compact().unwrap();
{
let g = shared.read();
assert!(g.meta.bound_type.is_some(), "GG02 binding survived densify");
assert!(g.is_node_alive(NodeId::new(1)));
assert!(g.row_for_node_id(NodeId::new(2)).is_none(), "bob reclaimed");
}
{
let mut txn = shared.begin_write();
{
let mut m = txn.mutator();
m.create_node(LabelSet::single(person.clone()), mk("dot"))
.unwrap();
}
txn.commit()
.expect("conforming insert commits post-compact");
}
{
let mut txn = shared.begin_write();
{
let mut m = txn.mutator();
let _ = m.create_node(LabelSet::single(person), PropertyMap::new());
}
assert!(
txn.commit().is_err(),
"the bound type still rejects a non-conforming node after live compaction"
);
}
}