use std::collections::HashMap;
use std::sync::Arc;
use crate::datatypes::{DataFrame, Value};
use crate::graph::dir_graph::DirGraph;
use crate::graph::schema::{InternedKey, PropertyStorage};
use crate::graph::session::{execute_mut, execute_read, ExecuteOptions};
use crate::graph::storage::column_store::ColumnStore;
use crate::graph::storage::GraphRead;
use petgraph::graph::NodeIndex;
const N: i64 = 4;
fn run(graph: &mut DirGraph, query: &str) {
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
execute_mut(graph, query, &opts).unwrap_or_else(|e| panic!("setup query failed: {query}: {e}"));
}
fn read_one(graph: &DirGraph, query: &str) -> Value {
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
let out = execute_read(graph, query, &opts).unwrap_or_else(|e| panic!("{query}: {e}"));
out.result
.rows
.first()
.and_then(|r| r.first())
.cloned()
.unwrap_or(Value::Null)
}
fn sized_rows(n: i64) -> DirGraph {
let mut g = DirGraph::new();
let rows: Vec<Vec<Value>> = (1..=n)
.map(|i| {
vec![
Value::Int64(i),
Value::String(format!("t{i}")),
Value::String(format!("c0-{i}")),
Value::Int64(i * 10),
]
})
.collect();
let df = DataFrame::from_cypher_rows(
vec![
"id".to_string(),
"title".to_string(),
"c0".to_string(),
"c1".to_string(),
],
rows,
)
.unwrap();
crate::graph::mutation::maintain::add_nodes(
&mut g,
df,
"Item".to_string(),
"id".to_string(),
Some("title".to_string()),
None,
)
.unwrap();
g
}
fn docs_fixture() -> DirGraph {
sized_rows(N)
}
fn sized_columnar(n: i64) -> DirGraph {
let mut g = sized_rows(n);
g.enable_columnar();
assert!(
g.column_store_count() > 0,
"fixture must own a master column store, or every arm below is vacuous"
);
assert!(
node_row_id(&g, node_of(&g, 1)).is_some(),
"fixture nodes must be columnar rows, or the ownership arms are vacuous"
);
g
}
fn seeded_columnar() -> DirGraph {
sized_columnar(N)
}
fn node_of(graph: &DirGraph, id: i64) -> NodeIndex {
graph
.graph
.node_indices()
.find(|&i| graph.graph.get_node_id(i) == Some(Value::Int64(id)))
.unwrap_or_else(|| panic!("no Item with id {id}"))
}
fn node_row_id(graph: &DirGraph, idx: NodeIndex) -> Option<u32> {
match graph.graph.node_weight(idx).map(|n| &n.properties) {
Some(PropertyStorage::Columnar(row)) => Some(row.row_id()),
_ => None,
}
}
fn master_is_uniquely_owned(graph: &DirGraph) -> bool {
graph
.column_store("Item")
.is_some_and(|master| Arc::strong_count(master) == 1)
}
fn diverge_master(graph: &mut DirGraph, idx: NodeIndex, key: &str, value: Value) -> InternedKey {
let row_id = node_row_id(graph, idx).expect("columnar node");
let ikey = graph.interner.get_or_intern(key);
let master = Arc::make_mut(graph.column_store_mut("Item").expect("master store"));
assert!(
master.set(row_id, ikey, &value, None),
"master write must land"
);
ikey
}
fn extract_json_c0(json: &str) -> Value {
let obj = json
.split('{')
.find(|chunk| chunk.contains("\"id\":1,"))
.unwrap_or("");
match obj.split("\"c0\":").nth(1) {
Some(rest) => {
let raw = rest
.split([',', '}'])
.next()
.unwrap_or("")
.trim()
.trim_matches('"');
Value::String(raw.to_string())
}
None => Value::Null,
}
}
fn all_read_surfaces(
graph: &mut DirGraph,
idx: NodeIndex,
ikey: InternedKey,
) -> Vec<(&'static str, Value)> {
let reader = graph.property_reader("Item", "c0");
let graph = &*graph;
vec![
(
"GraphRead::node_view",
graph
.node_view(idx)
.and_then(|v| v.get_property("c0"))
.map(|c| c.into_owned())
.unwrap_or(Value::Null),
),
(
"GraphRead::get_node_property",
graph
.graph
.get_node_property(idx, ikey)
.unwrap_or(Value::Null),
),
(
"GraphRead::node_row_properties",
graph
.graph
.node_row_properties(idx)
.into_iter()
.find(|(k, _)| *k == ikey)
.map(|(_, v)| v)
.unwrap_or(Value::Null),
),
(
"DirGraph::read_indexed (index build funnel)",
graph.read_indexed(&reader, idx).unwrap_or(Value::Null),
),
(
"Cypher RETURN n.c0",
read_one(graph, "MATCH (n:Item) WHERE n.id = 1 RETURN n.c0"),
),
(
"Cypher RETURN n (whole-node projection)",
match read_one(graph, "MATCH (n:Item) WHERE n.id = 1 RETURN n") {
Value::Node(nv) => nv.properties.get("c0").cloned().unwrap_or(Value::Null),
other => panic!("expected a node value, got {other:?}"),
},
),
(
"Cypher properties(n)",
match read_one(graph, "MATCH (n:Item) WHERE n.id = 1 RETURN properties(n)") {
Value::Map(m) => m.get("c0").cloned().unwrap_or(Value::Null),
other => panic!("expected a map, got {other:?}"),
},
),
("D3-JSON export", {
let json = crate::graph::io::export::to_d3_json(graph, None).unwrap();
extract_json_c0(&json)
}),
]
}
#[test]
fn all_public_reads_agree_without_divergence() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let ikey = InternedKey::from_str("c0");
let stored = Value::String("c0-1".into());
for (surface, got) in all_read_surfaces(&mut graph, idx, ikey) {
assert_eq!(got, stored, "{surface} disagreed with the stored value");
}
}
#[test]
fn all_public_reads_agree_under_master_node_divergence() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let ikey = diverge_master(&mut graph, idx, "c0", Value::String("MASTER".into()));
let surfaces = all_read_surfaces(&mut graph, idx, ikey);
let (first_name, first) = surfaces[0].clone();
for (surface, got) in &surfaces[1..] {
assert_eq!(
got, &first,
"{surface} resolved {got:?} but {first_name} resolved {first:?} — \
two public reads of the same property must never disagree"
);
}
assert_eq!(
first,
Value::String("MASTER".into()),
"{first_name} agreed with the others on {first:?}; all surfaces \
returning Null is agreement without a read"
);
}
#[test]
fn the_backend_store_is_the_only_read_route() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let ikey = diverge_master(&mut graph, idx, "c0", Value::String("MASTER".into()));
assert_eq!(
graph.graph.get_node_property(idx, ikey),
Some(Value::String("MASTER".into())),
"a write into the backend's store must be what a read returns — there \
is no second replica left to shadow it"
);
}
#[test]
fn set_leaves_the_master_uniquely_owned() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let ikey = diverge_master(&mut graph, idx, "c0", Value::String("MASTER".into()));
run(
&mut graph,
"MATCH (n:Item) WHERE n.id = 1 SET n.c0 = 'WRITTEN'",
);
assert!(
master_is_uniquely_owned(&graph),
"a committed columnar SET must leave the master uniquely owned — the \
journal's pre-image is released at commit"
);
let want = Value::String("WRITTEN".into());
for (surface, got) in all_read_surfaces(&mut graph, idx, ikey) {
assert_eq!(got, want, "{surface} did not observe the SET");
}
}
#[test]
fn remove_leaves_the_master_uniquely_owned() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let ikey = diverge_master(&mut graph, idx, "c0", Value::String("MASTER".into()));
run(&mut graph, "MATCH (n:Item) WHERE n.id = 1 REMOVE n.c0");
assert!(
master_is_uniquely_owned(&graph),
"a committed columnar REMOVE must leave the master uniquely owned"
);
for (surface, got) in all_read_surfaces(&mut graph, idx, ikey) {
assert_eq!(got, Value::Null, "{surface} still sees a removed property");
}
}
#[test]
fn merge_key_read_matches_the_public_read() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let _ = diverge_master(&mut graph, idx, "c0", Value::String("MASTER".into()));
let before = graph.graph.node_count();
let observed = read_one(&graph, "MATCH (n:Item) WHERE n.id = 1 RETURN n.c0");
let observed_str = match &observed {
Value::String(s) => s.clone(),
other => panic!("expected a string, got {other:?}"),
};
run(
&mut graph,
&format!("MERGE (n:Item {{id: 1, c0: '{observed_str}'}})"),
);
assert_eq!(
graph.graph.node_count(),
before,
"MERGE on the value the public read reports must match the existing row, not create one"
);
}
#[test]
fn rollback_restores_every_read_surface() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let ikey = InternedKey::from_str("c0");
let before = graph.graph.get_node_property(idx, ikey);
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
let err = execute_mut(
&mut graph,
"MATCH (n:Item) WHERE n.id = 1 SET n.c0 = 'DOOMED', \
n.c1 = duration({months: 2147483648})",
&opts,
);
assert!(err.is_err(), "the fixture statement must fail to roll back");
assert_eq!(
graph.graph.get_node_property(idx, ikey),
before,
"a rolled-back columnar SET must restore the pre-statement value"
);
for (surface, got) in all_read_surfaces(&mut graph, idx, ikey) {
assert_eq!(
Some(got),
before.clone(),
"{surface} kept a rolled-back value"
);
}
}
#[test]
fn save_and_reload_round_trips_the_observed_value() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let _ = diverge_master(&mut graph, idx, "c0", Value::String("MASTER".into()));
run(
&mut graph,
"MATCH (n:Item) WHERE n.id = 1 SET n.c0 = 'PERSISTED'",
);
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("g.kgl");
let mut arc = Arc::new(graph);
crate::graph::io::file::prepare_save(&mut arc);
Arc::make_mut(&mut arc).enable_columnar();
crate::graph::io::file::write_kgl(&arc, path.to_str().unwrap()).unwrap();
let loaded = crate::graph::io::file::load_file(path.to_str().unwrap()).unwrap();
assert_eq!(
read_one(&loaded, "MATCH (n:Item) WHERE n.id = 1 RETURN n.c0"),
Value::String("PERSISTED".into()),
"the saved file must carry the value the reads reported"
);
}
#[test]
fn spill_reclaims_the_heap_it_materialises() {
let mut graph = seeded_columnar();
let dir = tempfile::tempdir().unwrap();
graph.spill_dir = Some(dir.path().to_path_buf());
graph.memory_limit = Some(0);
assert!(
master_is_uniquely_owned(&graph),
"precondition: nothing but the backend owns the store before the spill"
);
let heap_before = graph
.column_store("Item")
.expect("master store")
.heap_bytes();
assert!(heap_before > 0, "precondition: the store holds heap data");
graph.maybe_spill_columns();
let master = graph.column_store("Item").expect("master store");
assert!(
master.is_mapped(),
"the spill must have materialised the master to files, or this test proves nothing"
);
assert!(
master.heap_bytes() < heap_before,
"the spill must reclaim heap: got {} bytes, was {heap_before}. Before D1 \
Phase 3 `make_mut` forked and this number never moved.",
master.heap_bytes()
);
assert!(
master_is_uniquely_owned(&graph),
"and the spilled store must still be the uniquely-owned one — a fork \
here would mean the reclaimed copy is not what reads resolve"
);
assert_eq!(
read_one(&graph, "MATCH (n:Item) WHERE n.id = 1 RETURN n.c0"),
Value::String("c0-1".into()),
"a spill must never change what a read returns"
);
}
#[test]
fn describe_reports_columnar_properties() {
use crate::graph::introspection::{ConnectionDetail, CypherDetail, FluentDetail};
let graph = seeded_columnar();
let xml = crate::graph::introspection::describe::compute_description(
&graph,
None,
&ConnectionDetail::Off,
&CypherDetail::Off,
&FluentDetail::Off,
None,
None,
None,
)
.unwrap();
assert!(
xml.contains("c0"),
"describe() lost a columnar property: {xml}"
);
assert!(
xml.contains("c0-1"),
"describe()'s node sample lost a columnar property value: {xml}"
);
}
#[test]
fn property_stats_count_columnar_rows() {
let graph = seeded_columnar();
let stats = crate::graph::introspection::schema_overview::compute_property_stats(
&graph, "Item", 32, None,
)
.expect("property stats");
let c0 = stats
.iter()
.find(|p| p.property_name == "c0")
.expect("c0 must appear in the property stats");
assert_eq!(
c0.non_null, N as usize,
"columnar rows contributed no values to the property stats"
);
assert_eq!(
c0.unique, N as usize,
"columnar rows contributed no distinct values"
);
}
#[test]
fn property_ndv_counts_columnar_rows() {
let graph = seeded_columnar();
assert_eq!(
graph.property_ndv("Item", "c0"),
Some(N as usize),
"property_ndv must see a columnar type's distinct values"
);
}
fn poison_row(
graph: &mut DirGraph,
node_type: &str,
edit: impl FnOnce(&mut ColumnStore),
) -> PoisonGuard {
let mut replacement: ColumnStore = (**graph
.column_store(node_type)
.expect("type must be columnar, or the poison is a no-op"))
.clone();
edit(&mut replacement);
graph.install_column_store(node_type, Arc::new(replacement));
PoisonGuard
}
struct PoisonGuard;
fn poison_property(
graph: &mut DirGraph,
node_type: &str,
row_id: u32,
key: &str,
value: Value,
) -> PoisonGuard {
let ikey = graph.interner.get_or_intern(key);
poison_row(graph, node_type, move |store| {
assert!(
store.set(row_id, ikey, &value, None),
"poison write must land, or the swap proves nothing"
);
})
}
fn poison_title(graph: &mut DirGraph, node_type: &str, row_id: u32, value: Value) -> PoisonGuard {
poison_row(graph, node_type, move |store| {
assert!(
store.set_title(row_id, &value),
"poison title write must land, or the swap proves nothing"
);
})
}
fn poisoned_fixture() -> (DirGraph, NodeIndex, PoisonGuard) {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let row_id = node_row_id(&graph, idx).expect("columnar node");
let guard = poison_property(
&mut graph,
"Item",
row_id,
"c0",
Value::String("TRUTH".into()),
);
(graph, idx, guard)
}
#[test]
fn poison_installs_a_distinct_store_that_reads_resolve() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let before = Arc::as_ptr(graph.column_store("Item").expect("master"));
let row_id = node_row_id(&graph, idx).unwrap();
let _guard = poison_property(
&mut graph,
"Item",
row_id,
"c0",
Value::String("TRUTH".into()),
);
let after = Arc::as_ptr(graph.column_store("Item").expect("master"));
assert!(
!std::ptr::eq(before, after),
"the poison must install a distinct allocation, or a caller holding the \
old one would be indistinguishable from one reading the new"
);
assert_eq!(
graph.node_view(idx).unwrap().get_property_value("c0"),
Some(Value::String("TRUTH".into())),
"and the read route must resolve the newly installed store"
);
assert_eq!(
node_row_id(&graph, idx),
Some(row_id),
"the node's row identity must be untouched — the swap is of the store, \
not of the node"
);
}
#[test]
fn r1_matcher_property_filter_reads_the_authoritative_store() {
let (graph, _idx, _guard) = poisoned_fixture();
assert_eq!(
read_one(&graph, "MATCH (n:Item {c0: 'TRUTH'}) RETURN n.id"),
Value::Int64(1),
"the matcher's property filter must see the authoritative value"
);
assert_eq!(
read_one(&graph, "MATCH (n:Item {c0: 'c0-1'}) RETURN n.id"),
Value::Null,
"the matcher must not match the stale replica"
);
}
#[test]
fn r3_where_clause_reads_the_authoritative_store() {
let (graph, _idx, _guard) = poisoned_fixture();
assert_eq!(
read_one(&graph, "MATCH (n:Item) WHERE n.c0 = 'TRUTH' RETURN n.id"),
Value::Int64(1)
);
assert_eq!(
read_one(&graph, "MATCH (n:Item) WHERE n.id = 1 RETURN n.c0"),
Value::String("TRUTH".into())
);
}
#[test]
fn r4_whole_node_projection_reads_the_authoritative_store() {
let (graph, _idx, _guard) = poisoned_fixture();
match read_one(&graph, "MATCH (n:Item) WHERE n.id = 1 RETURN n") {
Value::Node(nv) => assert_eq!(
nv.properties.get("c0"),
Some(&Value::String("TRUTH".into())),
"RETURN n must carry the authoritative value"
),
other => panic!("expected a node value, got {other:?}"),
}
}
#[test]
fn r8_property_index_build_reads_the_authoritative_store() {
let (mut graph, idx, _guard) = poisoned_fixture();
graph.create_index("Item", "c0");
let bucket = graph
.property_indices
.get(&("Item".to_string(), "c0".to_string()))
.expect("index must exist");
assert_eq!(
bucket.get(&Value::String("TRUTH".into())),
Some(&vec![idx]),
"the built index must bucket the row under its authoritative value"
);
assert!(
!bucket.contains_key(&Value::String("c0-1".into())),
"the built index must not carry the stale replica's value"
);
}
#[test]
fn r9_incremental_index_maintenance_reads_the_authoritative_store() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let row_id = node_row_id(&graph, idx).unwrap();
graph.create_index("Item", "c0");
let _guard = poison_property(
&mut graph,
"Item",
row_id,
"c0",
Value::String("TRUTH".into()),
);
graph.update_property_indices_for_add("Item", idx);
let bucket = graph
.property_indices
.get(&("Item".to_string(), "c0".to_string()))
.expect("index must exist");
assert!(
bucket
.get(&Value::String("TRUTH".into()))
.is_some_and(|members| members.contains(&idx)),
"incremental maintenance must file the row under its authoritative \
value, or it disagrees with a rebuilt index"
);
}
#[test]
fn r11_unique_constraint_gate_reads_the_authoritative_store() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let row_id = node_row_id(&graph, idx).unwrap();
let _guard = poison_property(
&mut graph,
"Item",
row_id,
"c0",
Value::String("c0-2".into()),
);
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
let result = execute_mut(
&mut graph,
"CREATE CONSTRAINT FOR (i:Item) REQUIRE i.c0 IS UNIQUE",
&opts,
);
assert!(
result.is_err(),
"the constraint gate must see the authoritative duplicate and reject; \
reading the stale node handles would show four distinct values"
);
}
#[test]
fn r12_property_ndv_reads_the_authoritative_store() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let row_id = node_row_id(&graph, idx).unwrap();
let _guard = poison_property(
&mut graph,
"Item",
row_id,
"c0",
Value::String("c0-2".into()),
);
assert_eq!(
graph.property_ndv("Item", "c0"),
Some(N as usize - 1),
"property_ndv must count the authoritative values; reading the stale \
node handles would still report {N} distinct"
);
}
#[test]
fn r13_export_reads_the_authoritative_store() {
let (graph, _idx, _guard) = poisoned_fixture();
let json = crate::graph::io::export::to_d3_json(&graph, None).unwrap();
assert_eq!(
extract_json_c0(&json),
Value::String("TRUTH".into()),
"D3-JSON export must carry the authoritative value"
);
}
#[test]
fn r13_property_stats_read_the_authoritative_store() {
let (graph, _idx, _guard) = poisoned_fixture();
let stats = crate::graph::introspection::schema_overview::compute_property_stats(
&graph, "Item", 32, None,
)
.expect("property stats");
let c0 = stats
.iter()
.find(|p| p.property_name == "c0")
.expect("c0 must appear in the property stats");
let values = c0.values.as_ref().expect("small-cardinality values");
assert!(
values.contains(&Value::String("TRUTH".into())),
"property stats must observe the authoritative value; got {values:?}"
);
assert!(
!values.contains(&Value::String("c0-1".into())),
"property stats must not observe the stale replica; got {values:?}"
);
}
#[test]
fn r14_resolve_noderefs_reads_the_authoritative_store() {
let mut graph = seeded_columnar();
let idx = node_of(&graph, 1);
let row_id = node_row_id(&graph, idx).unwrap();
let _guard = poison_title(
&mut graph,
"Item",
row_id,
Value::String("TRUE-TITLE".into()),
);
let mut rows = vec![vec![Value::NodeRef(idx.index() as u32)]];
crate::graph::session::resolve_noderefs(&graph.graph, &mut rows);
assert_eq!(
rows[0][0],
Value::String("TRUE-TITLE".into()),
"resolve_noderefs must resolve the authoritative title"
);
}
const NODE_HANDLE_ESCAPE_SITES: &[(&str, usize)] = &[];
#[test]
fn no_code_reaches_a_node_held_column_store_handle() {
use std::collections::BTreeMap;
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut found: BTreeMap<String, usize> = BTreeMap::new();
let read_escape = concat!(".node_", "handle()");
let write_escape = concat!(".re", "point(");
fn walk(
dir: &std::path::Path,
root: &std::path::Path,
needles: (&str, &str),
found: &mut BTreeMap<String, usize>,
) {
for entry in std::fs::read_dir(dir).expect("readable source dir") {
let path = entry.expect("dir entry").path();
if path.is_dir() {
walk(&path, root, needles, found);
} else if path.extension().is_some_and(|e| e == "rs") {
let text = std::fs::read_to_string(&path).expect("readable source file");
let hits = text.matches(needles.0).count() + text.matches(needles.1).count();
if hits > 0 {
let rel = path
.strip_prefix(root)
.expect("under src")
.to_string_lossy()
.replace('\\', "/");
*found.entry(rel).or_insert(0) += hits;
}
}
}
}
walk(&src, &src, (read_escape, write_escape), &mut found);
let expected: BTreeMap<String, usize> = NODE_HANDLE_ESCAPE_SITES
.iter()
.map(|(f, n)| ((*f).to_string(), *n))
.collect();
assert_eq!(
found, expected,
"\nA node-held column-store handle is reachable again.\n\
D1 Phase 3 made the storage backend the sole owner: a node carries a \
row id, and the store is resolved by `GraphRead::column_store`. Read \
through `NodeView` / `GraphRead` and write through \
`GraphWrite::set_node_property` instead of re-introducing a per-node \
handle.\n"
);
}
#[test]
fn saving_a_freshly_built_graph_skips_the_rebuild() {
use crate::graph::dir_graph::COLUMNAR_REBUILDS;
let rebuilds = || COLUMNAR_REBUILDS.with(|c| c.get());
let mut graph = docs_fixture();
assert!(
graph.column_store_count() > 0,
"construction must already be columnar, or this test measures nothing"
);
let before = rebuilds();
graph.enable_columnar();
graph.enable_columnar();
assert_eq!(
rebuilds(),
before,
"a save of an unmodified graph must take the fast path; a rebuild here \
means construction and the saved shape disagree, or the idempotence \
guard regressed, and every save pays O(N)"
);
run(&mut graph, "MATCH (n:Item) WHERE n.id = 1 DELETE n");
graph.enable_columnar();
assert_eq!(
rebuilds(),
before + 1,
"an orphaned row must still be detected as drift and rebuild"
);
}
#[test]
fn a_row_appended_out_of_index_order_is_detected_as_drift() {
use crate::graph::dir_graph::COLUMNAR_REBUILDS;
use crate::graph::schema::PropertyStorage;
let rebuilds = || COLUMNAR_REBUILDS.with(|c| c.get());
let mut graph = docs_fixture();
run(&mut graph, "MATCH (n:Item) WHERE n.id = 1 DETACH DELETE n");
graph.enable_columnar();
let settled = rebuilds();
graph.enable_columnar();
assert_eq!(
rebuilds(),
settled,
"precondition: the graph must be settled"
);
run(&mut graph, "CREATE (:Item {id: 99, title: 'late'})");
let out_of_order = graph
.graph
.node_indices()
.filter_map(|idx| graph.graph.node_weight(idx))
.filter_map(|node| match &node.properties {
PropertyStorage::Columnar(row) => Some(row.row_id()),
_ => None,
})
.enumerate()
.any(|(position, row_id)| row_id as usize != position);
assert!(
out_of_order,
"precondition: the create must have produced an out-of-order row, or \
this test cannot see whether the drift check catches one"
);
graph.enable_columnar();
assert_eq!(
rebuilds(),
settled + 1,
"an out-of-order row must be detected as drift; without the rebuild the \
save writes every row against the wrong node"
);
let ordered = graph
.graph
.node_indices()
.filter_map(|idx| graph.graph.node_weight(idx))
.filter_map(|node| match &node.properties {
PropertyStorage::Columnar(row) => Some(row.row_id()),
_ => None,
})
.enumerate()
.all(|(position, row_id)| row_id as usize == position);
assert!(ordered, "the rebuild must restore ascending row order");
}
#[test]
fn a_one_row_columnar_set_leaves_every_other_row_untouched() {
for n in [20i64, 200] {
let mut graph = sized_columnar(n);
let before: Vec<Option<Value>> = (0..n)
.map(|i| {
graph
.column_store("Item")
.unwrap()
.get(i as u32, InternedKey::from_str("c0"))
})
.collect();
run(&mut graph, "MATCH (n:Item) WHERE n.id = 1 SET n.c0 = 'ONE'");
let after: Vec<Option<Value>> = (0..n)
.map(|i| {
graph
.column_store("Item")
.unwrap()
.get(i as u32, InternedKey::from_str("c0"))
})
.collect();
let changed: Vec<usize> = before
.iter()
.zip(&after)
.enumerate()
.filter(|(_, (b, a))| b != a)
.map(|(i, _)| i)
.collect();
assert_eq!(
changed.len(),
1,
"N={n}: a one-row SET must change exactly one row, changed {changed:?}"
);
assert!(
master_is_uniquely_owned(&graph),
"N={n}: and must leave the master uniquely owned, so the next write \
mutates in place rather than copying the store"
);
}
}
fn replace_item_one_with_c0_only(graph: &mut DirGraph) {
let df = DataFrame::from_cypher_rows(
vec!["id".to_string(), "c0".to_string()],
vec![vec![Value::Int64(1), Value::String("replaced".into())]],
)
.unwrap();
crate::graph::mutation::maintain::add_nodes(
graph,
df,
"Item".to_string(),
"id".to_string(),
None,
Some("replace".to_string()),
)
.unwrap();
}
#[test]
fn a_forked_columnar_replace_drops_the_properties_it_omits() {
use crate::graph::handle::make_dir_graph_mut;
const C1: &str = "MATCH (n:Item) WHERE n.id = 1 RETURN n.c1";
const C0: &str = "MATCH (n:Item) WHERE n.id = 1 RETURN n.c0";
let mut control = seeded_columnar();
assert_eq!(
read_one(&control, C1),
Value::Int64(10),
"precondition: the fixture row carries the property the batch will omit"
);
replace_item_one_with_c0_only(&mut control);
assert_eq!(
read_one(&control, C0),
Value::String("replaced".into()),
"control: the batch's own property is written"
);
assert_eq!(
read_one(&control, C1),
Value::Null,
"control: replace-mode drops a property the batch omits"
);
let mut writer = Arc::new(seeded_columnar());
let reader = Arc::clone(&writer);
let graph = make_dir_graph_mut(&mut writer);
assert!(
graph.graph.is_forked(),
"precondition: a held view must fork the writer, or this is a second \
run of the control"
);
replace_item_one_with_c0_only(graph);
assert!(
graph.graph.is_forked(),
"precondition: the replace must land on the overlay, not after a flatten"
);
assert_eq!(
read_one(graph, C0),
Value::String("replaced".into()),
"the batch's own property is written on the overlay too"
);
assert_eq!(
read_one(graph, C1),
Value::Null,
"a replace on a forked columnar row must drop the properties the batch \
omits, exactly as the unforked control does"
);
assert_eq!(
read_one(&reader, C1),
Value::Int64(10),
"and the held view keeps the row it was forked with"
);
}
fn sized_columnar_in_mode(
n: i64,
mode: crate::graph::storage::mode::StorageMode,
path: Option<&std::path::Path>,
) -> DirGraph {
let mut g = crate::graph::storage::mode::new_dir_graph_in_mode(mode, path)
.expect("fixture backend must be constructible");
let rows: Vec<Vec<Value>> = (1..=n)
.map(|i| {
vec![
Value::Int64(i),
Value::String(format!("t{i}")),
Value::String(format!("c0-{i}")),
Value::Int64(i * 10),
]
})
.collect();
let df = DataFrame::from_cypher_rows(
vec![
"id".to_string(),
"title".to_string(),
"c0".to_string(),
"c1".to_string(),
],
rows,
)
.unwrap();
crate::graph::mutation::maintain::add_nodes(
&mut g,
df,
"Item".to_string(),
"id".to_string(),
Some("title".to_string()),
None,
)
.unwrap();
g.enable_columnar();
assert_eq!(
crate::graph::storage::mode::live_storage_mode(&g),
mode,
"fixture must be on the backend it names, or the arm tests Memory twice"
);
assert!(
g.column_store_count() > 0,
"fixture must own a master column store, or every arm below is vacuous"
);
g
}
fn mapped_columnar() -> DirGraph {
sized_columnar_in_mode(N, crate::graph::storage::mode::StorageMode::Mapped, None)
}
#[test]
fn the_backend_store_is_the_only_read_route_on_mapped() {
let mut graph = mapped_columnar();
let idx = node_of(&graph, 1);
let row_id = node_row_id(&graph, idx).expect("columnar node");
let _guard = poison_property(
&mut graph,
"Item",
row_id,
"c0",
Value::String("TRUTH".into()),
);
assert_eq!(
graph.node_view(idx).unwrap().get_property_value("c0"),
Some(Value::String("TRUTH".into())),
"a mapped graph must read the store its backend owns"
);
assert_eq!(
read_one(&graph, "MATCH (n:Item) WHERE n.id = 1 RETURN n.c0"),
Value::String("TRUTH".into()),
"and the Cypher read route must resolve the same store"
);
}
#[test]
fn set_leaves_the_master_uniquely_owned_on_mapped() {
let mut graph = mapped_columnar();
let idx = node_of(&graph, 1);
let ikey = graph.interner.get_or_intern("c0");
let before = Arc::as_ptr(graph.column_store("Item").expect("master"));
run(
&mut graph,
"MATCH (n:Item) WHERE n.id = 1 SET n.c0 = 'WRITTEN'",
);
assert!(
std::ptr::eq(
before,
Arc::as_ptr(graph.column_store("Item").expect("master"))
),
"a columnar SET on Mapped must mutate the master in place — a changed \
allocation means the statement deep-copied the type's columns"
);
assert!(
master_is_uniquely_owned(&graph),
"a committed columnar SET on Mapped must leave the master uniquely owned"
);
for (surface, got) in all_read_surfaces(&mut graph, idx, ikey) {
assert_eq!(
got,
Value::String("WRITTEN".into()),
"{surface} did not observe the SET on Mapped"
);
}
}
#[test]
fn the_backend_store_is_the_only_read_route_on_disk() {
let dir = tempfile::tempdir().expect("temp dir");
let mut graph = sized_columnar_in_mode(
N,
crate::graph::storage::mode::StorageMode::Disk,
Some(dir.path()),
);
let (idx, row_id) = {
let _query = graph.graph.begin_query();
let idx = node_of(&graph, 1);
let row_id = node_row_id(&graph, idx).expect("columnar node");
(idx, row_id)
};
let _guard = poison_property(
&mut graph,
"Item",
row_id,
"c0",
Value::String("TRUTH".into()),
);
{
let _query = graph.graph.begin_query();
assert_eq!(
graph.node_view(idx).unwrap().get_property_value("c0"),
Some(Value::String("TRUTH".into())),
"a disk graph must read the store its backend owns"
);
}
assert_eq!(
read_one(&graph, "MATCH (n:Item) WHERE n.id = 1 RETURN n.c0"),
Value::String("TRUTH".into()),
"and the Cypher read route must resolve the same store"
);
}
#[test]
fn the_spill_trigger_converges_on_the_unspillable_floor() {
let mut graph = seeded_columnar();
let dir = tempfile::tempdir().unwrap();
graph.spill_dir = Some(dir.path().to_path_buf());
graph.memory_limit = Some(0);
graph.maybe_spill_columns();
let master = graph.column_store("Item").expect("master store");
assert!(
master.is_mapped(),
"precondition: the store must actually have spilled"
);
assert_eq!(
master.spillable_heap_bytes(),
0,
"everything a spill can move is file-backed, so the trigger must now \
read zero against its zero limit"
);
assert!(
master.heap_bytes() > 0,
"precondition: the unspillable floor is still there — without it this \
test cannot distinguish convergence from an empty store"
);
assert!(
!master.may_have_grown_spillable_heap(),
"and a completed spill must clear the growth flag, or every later \
statement re-walks every type to rediscover the floor"
);
}
#[test]
fn a_statement_that_grows_spillable_heap_still_triggers_the_spill() {
let mut graph = seeded_columnar();
let dir = tempfile::tempdir().unwrap();
graph.spill_dir = Some(dir.path().to_path_buf());
graph.memory_limit = Some(0);
graph.maybe_spill_columns();
assert_eq!(
graph
.column_store("Item")
.expect("master store")
.spillable_heap_bytes(),
0,
"precondition: the fixture starts converged"
);
run(&mut graph, "MATCH (n:Item) SET n.fresh = 7");
let master = graph.column_store("Item").expect("master store");
assert_eq!(
master.spillable_heap_bytes(),
0,
"the appended `fresh` column is spillable heap over a zero limit; the \
statement that created it must have re-run the spill pass"
);
assert_eq!(
read_one(&graph, "MATCH (n:Item) WHERE n.id = 1 RETURN n.fresh"),
Value::Int64(7),
"and the value must survive the spill it triggered"
);
}