use std::path::Path;
use tempfile::TempDir;
use graphforge_core::uuid::{Uuid, new_v7};
use graphforge_core::{OntologyMode, TypeId};
use graphforge_storage::adjacency::{
ALL_RELATIONS_STEM, Direction, build_adjacency_index, csr_path, read_csr,
};
use graphforge_storage::adjacency_delta::{apply_delta_segments, delta_path, read_delta_chain};
use graphforge_storage::{GraphWriter, delete_edges, read_topology_generation};
const TS: i64 = 1_700_000_000_000_000;
const PERSON: TypeId = TypeId(0);
fn write_edges(dir: &Path, pairs: &[(usize, usize)], node_count: usize) -> Vec<Uuid> {
let mut w = GraphWriter::open_at(dir, OntologyMode::Strict, TS).unwrap();
let uuids: Vec<Uuid> = (0..node_count).map(|_| new_v7()).collect();
for u in &uuids {
w.create_node(*u, PERSON).unwrap();
}
for &(s, d) in pairs {
w.create_edge(new_v7(), "KNOWS", &uuids[s], &uuids[d])
.unwrap();
}
w.flush().unwrap();
uuids
}
#[test]
fn pure_append_segment_overlay_equals_full_rebuild() {
let dir = TempDir::new().unwrap();
write_edges(dir.path(), &[(0, 1), (1, 2), (2, 0)], 3);
build_adjacency_index(dir.path(), TS).unwrap();
let base_gen = read_topology_generation(dir.path()).unwrap();
let mut w = GraphWriter::open_at(dir.path(), OntologyMode::Strict, TS).unwrap();
let extra: Vec<Uuid> = (0..1).map(|_| new_v7()).collect();
w.create_node(extra[0], PERSON).unwrap();
w.create_edge(new_v7(), "KNOWS", &extra[0], &extra[0])
.unwrap();
w.flush().unwrap();
let cur_gen = read_topology_generation(dir.path()).unwrap();
assert!(cur_gen > base_gen, "edge flush bumped the generation");
assert!(
delta_path(dir.path(), cur_gen).exists(),
"a segment was written at the new generation"
);
let chain = read_delta_chain(dir.path(), base_gen, cur_gen).expect("intact chain");
assert!(!chain.is_empty());
let mut overlaid = Vec::new();
for stem in [ALL_RELATIONS_STEM, "KNOWS"] {
for direction in [Direction::Out, Direction::In] {
let base = read_csr(&csr_path(dir.path(), stem, direction)).unwrap();
overlaid.push((
stem,
direction,
apply_delta_segments(&base, stem, direction, &chain),
));
}
}
build_adjacency_index(dir.path(), TS).unwrap();
for (stem, direction, got) in overlaid {
let expected = read_csr(&csr_path(dir.path(), stem, direction)).unwrap();
assert_eq!(
got, expected,
"overlay must equal rebuild for {stem} {direction:?}"
);
}
}
#[test]
fn node_only_flush_writes_empty_segment() {
let dir = TempDir::new().unwrap();
write_edges(dir.path(), &[(0, 1)], 2);
build_adjacency_index(dir.path(), TS).unwrap();
let base_gen = read_topology_generation(dir.path()).unwrap();
let mut w = GraphWriter::open_at(dir.path(), OntologyMode::Strict, TS).unwrap();
w.create_node(new_v7(), PERSON).unwrap();
w.flush().unwrap();
let cur_gen = read_topology_generation(dir.path()).unwrap();
assert!(cur_gen > base_gen);
let chain = read_delta_chain(dir.path(), base_gen, cur_gen).expect("contiguous chain");
assert_eq!(chain.len(), 1);
assert!(chain[0].edges.is_empty(), "node-only segment has no edges");
}
#[test]
fn no_segment_without_index() {
let dir = TempDir::new().unwrap();
write_edges(dir.path(), &[(0, 1)], 2);
let generation = read_topology_generation(dir.path()).unwrap();
assert!(!delta_path(dir.path(), generation).exists());
assert!(
!graphforge_storage::adjacency::adjacency_dir(dir.path())
.join("deltas")
.exists()
);
}
#[test]
fn delete_breaks_the_chain() {
use std::collections::HashSet;
let dir = TempDir::new().unwrap();
let uuids = write_edges(dir.path(), &[(0, 1), (1, 2)], 3);
build_adjacency_index(dir.path(), TS).unwrap();
let base_gen = read_topology_generation(dir.path()).unwrap();
let mut victims: HashSet<[u8; 16]> = HashSet::new();
victims.insert(*uuids[2].as_bytes());
let edge_uuids: HashSet<[u8; 16]> =
graphforge_storage::incident_edge_uuids(dir.path(), &victims)
.unwrap()
.into_iter()
.collect();
let removed = delete_edges(dir.path(), &edge_uuids).unwrap();
assert!(removed > 0, "an incident edge was deleted");
let cur_gen = read_topology_generation(dir.path()).unwrap();
assert!(cur_gen > base_gen, "delete bumped the generation");
assert!(
!delta_path(dir.path(), cur_gen).exists(),
"delete writes no segment"
);
assert!(
read_delta_chain(dir.path(), base_gen, cur_gen).is_none(),
"the chain is broken at the delete's generation ⇒ stale ⇒ rebuild"
);
}