use super::unique_claims::{seeded_with_unique_name, unique_fingerprint};
use super::*;
#[test]
fn a_rollback_while_a_reader_is_held_touches_neither_graph() {
use crate::graph::handle::make_dir_graph_mut;
use std::sync::Arc;
for (name, build) in [
("plain", seeded as fn() -> DirGraph),
("columnar", seeded_columnar as fn() -> DirGraph),
("indexed", seeded_indexed as fn() -> DirGraph),
("unique", seeded_with_unique_name as fn() -> DirGraph),
] {
let mut writer = Arc::new(build());
let reader = Arc::clone(&writer);
let claims_before = unique_fingerprint(&reader);
assert_eq!(
!claims_before.is_empty(),
name == "unique",
"{name}: only the unique arm may hold declared constraints, and it must — \
otherwise the claim assertions below are vacuous"
);
let reader_before = fingerprint(&mut (*reader).clone());
let writer_before = {
let graph = make_dir_graph_mut(&mut writer);
assert!(
graph.graph.is_forked(),
"{name}: precondition — a held reader must produce an overlay"
);
let before = fingerprint(&mut graph.clone());
expect_failure(
graph,
"CREATE (:Item {id: 4000, name: 'first'}), (:Blocked {id: 4001, name: 'second'})",
Some(&["Item"]),
);
before
};
assert_eq!(
fingerprint(&mut (*reader).clone()),
reader_before,
"{name}: the reader's graph must be untouched by a write it never \
asked for — a difference here means the undo journal reversed into \
the shared base instead of the overlay (D2 R3)"
);
assert_eq!(
fingerprint(&mut (*writer).clone()),
writer_before,
"{name}: the writer's failed statement must roll back exactly, \
overlay or not"
);
assert_eq!(
unique_fingerprint(&writer),
claims_before,
"{name}: the failed statement claimed 'first' and must have released it — \
a claim surviving a rollback rejects that value forever"
);
assert_eq!(
unique_fingerprint(&reader),
claims_before,
"{name}: the reader's occupancy map must be untouched by the writer's \
constraint bookkeeping"
);
}
}
#[test]
fn a_write_under_a_held_reader_after_a_delete_takes_the_clone_path() {
use crate::graph::handle::make_dir_graph_mut;
use crate::graph::storage::backend::{backend_clone_nodes, reset_backend_clone_count};
use std::sync::Arc;
let mut writer = Arc::new(seeded());
run(
Arc::make_mut(&mut writer),
"MATCH (n:Item {id: 3}) DETACH DELETE n",
);
let live_nodes = writer.graph.node_count();
let reader = Arc::clone(&writer);
let reader_before = fingerprint(&mut (*reader).clone());
reset_backend_clone_count();
let graph = make_dir_graph_mut(&mut writer);
assert!(
!graph.graph.is_forked(),
"a graph with a non-empty free list must NOT fork — the overlay hands out \
append indices petgraph would reuse from the free list, and the fold-back \
would then mis-key every DirGraph index recorded against them"
);
assert_eq!(
backend_clone_nodes(),
live_nodes,
"the fallback is a genuine deep copy of the base, not an Arc share"
);
run(graph, "CREATE (:Item {id: 4, name: 'd', qty: 40})");
run(graph, "CREATE (:Item {id: 3, name: 'c-again', qty: 33})");
assert_eq!(item_prop(graph, 4, "qty"), Some(Value::Int64(40)));
assert_eq!(item_prop(graph, 3, "qty"), Some(Value::Int64(33)));
assert_eq!(
graph.graph.node_count(),
live_nodes + 2,
"both creates must be live nodes"
);
reset_backend_clone_count();
run(graph, "CREATE (:Item {id: 5, name: 'e', qty: 50})");
assert_eq!(
backend_clone_nodes(),
0,
"the copy is paid once at the fork point — a per-statement copy here is the \
cliff the overlay exists to remove, wearing the fallback's hat"
);
assert_eq!(
fingerprint(&mut (*reader).clone()),
reader_before,
"the reader's snapshot must be untouched by every one of those writes"
);
let mut reader_now = (*reader).clone();
assert_eq!(
reader_now.lookup_by_id("Item", &Value::Int64(4)),
None,
"none of the writer's three creates may be visible through the reader — the \
observable half of the deep copy, in the direction a shared backend breaks"
);
assert_eq!(
reader_now.lookup_by_id("Item", &Value::Int64(3)),
None,
"and the id the writer re-created must still read as deleted here"
);
}
#[test]
fn forked_statements_copy_zero_nodes_except_one_flatten() {
use crate::graph::handle::make_dir_graph_mut;
use crate::graph::storage::backend::{backend_clone_nodes, reset_backend_clone_count};
use std::sync::Arc;
const OVERLAY_QUERIES: &[&str] = &[
"CREATE (:Item {id: 2000, name: 'x'})",
"MATCH (n:Item {id: 1}) SET n.qty = 11, n.name = 'renamed'",
"MATCH (n:Item {id: 2000}) SET n:Featured",
"MERGE (n:Item {id: 2001}) ON CREATE SET n.name = 'merged'",
];
const ADJACENCY_QUERY: &str =
"MATCH (a:Item {id: 1}), (b:Item {id: 3}) CREATE (a)-[:LINKS {weight: 2}]->(b)";
let mut writer = Arc::new(seeded());
let reader = Arc::clone(&writer);
let fixture_nodes = reader.graph.node_count();
let graph = make_dir_graph_mut(&mut writer);
assert!(graph.graph.is_forked(), "precondition: the write forked");
for &query in OVERLAY_QUERIES {
reset_backend_clone_count();
run(graph, query);
assert_eq!(
backend_clone_nodes(),
0,
"an overlay-expressible statement on a forked backend must copy no node: {query}"
);
assert!(
graph.graph.is_forked(),
"...and must leave the backend forked: {query}"
);
}
reset_backend_clone_count();
run(graph, ADJACENCY_QUERY);
assert_eq!(
backend_clone_nodes(),
fixture_nodes,
"the adjacency write flattens the overlay — exactly one copy of the base"
);
assert!(
!graph.graph.is_forked(),
"flattening must leave a plain backend, so the copy is paid once"
);
reset_backend_clone_count();
run(graph, "MATCH (n:Item {id: 2000}) DETACH DELETE n");
run(graph, "CREATE (:Item {id: 2002, name: 'after'})");
assert_eq!(
backend_clone_nodes(),
0,
"after flattening, later statements mutate in place — one copy per fork, \
not one per statement"
);
assert_eq!(reader.graph.node_count(), fixture_nodes);
}
#[test]
fn a_replace_write_on_a_forked_columnar_row_rolls_back_the_cells_it_nulled() {
use crate::graph::dir_graph::rollback::StatementCheckpoint;
use crate::graph::handle::make_dir_graph_mut;
use crate::graph::storage::GraphWrite;
use std::sync::Arc;
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"
);
let idx = graph
.lookup_by_id("Item", &Value::Int64(1))
.expect("the fixture must carry Item id 1");
let name = graph.interner.get_or_intern("name");
let fresh = graph.interner.get_or_intern("fresh");
let before = fingerprint(&mut graph.clone());
let checkpoint = StatementCheckpoint::open(graph);
GraphWrite::replace_node_properties(
&mut graph.graph,
idx,
vec![
(name, Value::String("replaced".into())),
(fresh, Value::Int64(42)),
],
);
let mid = fingerprint(&mut graph.clone());
assert_ne!(before, mid, "the replace must have changed the graph");
let row = graph
.graph
.node_weight(idx)
.and_then(|node| match &node.properties {
PropertyStorage::Columnar(row) => Some(row.row_id()),
_ => None,
})
.expect("the fixture node must be a columnar row");
let qty = graph.interner.get_or_intern("qty");
let store = graph.column_store("Item").expect("master store");
assert!(
store.get(row, qty).is_none(),
"precondition: the replace must have dropped the property it omitted, \
or this test cannot see whether the rollback restores it"
);
checkpoint.rollback(graph);
assert_eq!(
fingerprint(&mut graph.clone()),
before,
"a rolled-back replace must restore the whole row — the cells it wrote \
and the ones it nulled"
);
}
#[test]
fn an_edge_property_write_under_a_held_reader_reaches_traversal_reads() {
use crate::graph::handle::make_dir_graph_mut;
use crate::graph::session::execute::execute_read;
use crate::graph::storage::backend::{backend_clone_nodes, reset_backend_clone_count};
use std::sync::Arc;
const PAIR: &str = "MATCH (:Item {id: 1})-[r:LINKS]->(:Item {id: 2})";
for (verb, predicate) in [
("SET r.weight = 99", "r.weight = 99"),
("REMOVE r.weight", "r.weight IS NULL"),
] {
let mut writer = Arc::new(seeded());
let reader = Arc::clone(&writer);
let fixture_nodes = reader.graph.node_count();
let graph = make_dir_graph_mut(&mut writer);
assert!(
graph.graph.is_forked(),
"{verb}: precondition — the held reader must have forked the write"
);
reset_backend_clone_count();
run(graph, &format!("{PAIR} {verb}"));
assert_eq!(
backend_clone_nodes(),
fixture_nodes,
"{verb}: the edge-property write flattens the overlay — one copy of the base"
);
assert!(
!graph.graph.is_forked(),
"{verb}: flattening must leave a plain backend, so the copy is paid once"
);
reset_backend_clone_count();
run(
graph,
"MATCH (:Item {id: 2})-[r:LINKS]->(:Item {id: 3}) SET r.weight = 77",
);
assert_eq!(
backend_clone_nodes(),
0,
"{verb}: after flattening, a second edge-property write copies nothing"
);
let params = HashMap::new();
let opts = ExecuteOptions::new(¶ms);
let filtered = execute_read(
graph,
&format!("MATCH (:Item)-[r:LINKS]->(:Item) WHERE {predicate} RETURN count(r) AS c"),
&opts,
)
.unwrap_or_else(|e| panic!("{verb}: filter query failed: {e}"))
.result
.rows;
assert_eq!(
filtered,
vec![vec![Value::Int64(1)]],
"{verb}: a WHERE on the edge property must see the write, and match \
only the edge it touched"
);
let projected = execute_read(graph, &format!("{PAIR} RETURN r.weight AS w"), &opts)
.unwrap_or_else(|e| panic!("{verb}: projection query failed: {e}"))
.result
.rows;
let expected = if verb.starts_with("SET") {
Value::Int64(99)
} else {
Value::Null
};
assert_eq!(
projected,
vec![vec![expected]],
"{verb}: the projection must agree with the filter"
);
let held = execute_read(&reader, &format!("{PAIR} RETURN r.weight AS w"), &opts)
.unwrap_or_else(|e| panic!("{verb}: held-reader query failed: {e}"))
.result
.rows;
assert_eq!(
held,
vec![vec![Value::Int64(5)]],
"{verb}: the held snapshot must not observe the writer's edit"
);
}
}