use super::*;
#[test]
fn rollback_reuses_the_vacated_slots() {
let mut graph = seeded();
let slots_before: Vec<usize> = graph
.graph
.node_indices()
.map(|idx| idx.index())
.collect::<Vec<_>>();
expect_failure(
&mut graph,
"MATCH (n:Item) DETACH DELETE n CREATE (:Blocked {id: 1})",
Some(&["Item"]),
);
let slots_after: Vec<usize> = graph.graph.node_indices().map(|idx| idx.index()).collect();
assert_eq!(
slots_before, slots_after,
"restored nodes must land on the slots they vacated"
);
}
#[test]
fn successful_statement_leaves_no_journal_installed() {
let mut graph = seeded();
run(&mut graph, "CREATE (:Item {id: 1000})");
assert!(
graph.graph.take_undo().is_none(),
"a committed statement must uninstall its journal"
);
}
#[test]
fn failed_statement_leaves_no_journal_installed() {
let mut graph = seeded();
expect_failure(
&mut graph,
"CREATE (:Item {id: 1001}), (:Blocked {id: 1002})",
Some(&["Item"]),
);
assert!(
graph.graph.take_undo().is_none(),
"a rolled-back statement must uninstall its journal"
);
}
#[test]
fn a_second_statement_after_a_rollback_still_commits() {
let mut graph = seeded();
expect_failure(
&mut graph,
"MATCH (n:Item) DETACH DELETE n CREATE (:Blocked {id: 1})",
Some(&["Item"]),
);
run(&mut graph, "CREATE (:Item {id: 1100, name: 'after'})");
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
let out = execute_mut(&mut graph, "MATCH (n:Item) RETURN count(n) AS c", &opts)
.expect("read after rollback");
let rows = out.result.rows;
assert_eq!(rows.len(), 1);
assert_eq!(
format!("{:?}", rows[0][0]),
format!("{:?}", Value::Int64(4))
);
}
const ZERO_COPY_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",
"MATCH (a:Item {id: 1}), (b:Item {id: 3}) CREATE (a)-[:LINKS {weight: 2}]->(b)",
"MATCH (n:Item {id: 2000}) DETACH DELETE n",
"MERGE (n:Item {id: 2001}) ON CREATE SET n.name = 'merged'",
];
fn assert_statements_copy_zero_nodes(graph: &mut DirGraph, fixture: &str) {
use crate::graph::storage::backend::{backend_clone_nodes, reset_backend_clone_count};
for &query in ZERO_COPY_QUERIES {
reset_backend_clone_count();
run(graph, query);
assert_eq!(
backend_clone_nodes(),
0,
"statement must not copy any node on the {fixture} fixture: {query}"
);
}
}
#[test]
fn journalled_statements_copy_zero_nodes() {
assert_statements_copy_zero_nodes(&mut seeded(), "plain");
}
#[test]
fn journalled_statements_copy_zero_nodes_on_a_saved_graph() {
assert_statements_copy_zero_nodes(&mut seeded_columnar(), "columnar");
}
#[test]
fn journalled_statements_copy_zero_nodes_on_an_indexed_graph() {
assert_statements_copy_zero_nodes(&mut seeded_indexed(), "indexed");
}
#[test]
fn mapped_statements_copy_zero_nodes() {
let mut graph = seeded_mapped();
assert!(
graph.graph.node_count() > 0,
"fixture must have nodes or the counter proves nothing"
);
assert_statements_copy_zero_nodes(&mut graph, "mapped");
}
#[test]
fn mapped_rolls_back_completely() {
let mut graph = seeded_mapped();
assert_rolls_back(
&mut graph,
"MATCH (n:Item) DETACH DELETE n CREATE (:Blocked {id: 1})",
Some(&["Item"]),
);
}
#[test]
fn the_mapped_silent_write_path_records_nothing() {
use crate::graph::storage::GraphWrite;
let mut graph = seeded_mapped();
let idx = graph
.graph
.node_indices()
.next()
.expect("the fixture must have a node");
graph.graph.begin_undo();
GraphWrite::node_weight_mut(&mut graph.graph, idx).expect("node is live");
let recorded = graph
.graph
.take_undo()
.expect("begin_undo must install a journal on a mapped graph")
.into_replay_order()
.count();
assert_eq!(
recorded, 1,
"the recorded seam must capture a pre-image, or the silent arm below \
is comparing against an empty journal for the wrong reason"
);
graph.graph.begin_undo();
GraphWrite::node_weight_mut_silent(&mut graph.graph, idx).expect("node is live");
let silent = graph
.graph
.take_undo()
.expect("begin_undo must install a journal on a mapped graph")
.into_replay_order()
.count();
assert_eq!(
silent, 0,
"the mapped silent write path journalled {silent} entries; it must \
journal none, or the columnar detach/reattach and handle-refresh \
sweeps cost one pre-image per node of the type per chunk"
);
}
fn assert_rollback_copies_zero_nodes(graph: &mut DirGraph, fixture: &str) {
use crate::graph::storage::backend::{backend_clone_nodes, reset_backend_clone_count};
reset_backend_clone_count();
expect_failure(
graph,
"MATCH (n:Item) DETACH DELETE n CREATE (:Blocked {id: 1})",
Some(&["Item"]),
);
assert_eq!(
backend_clone_nodes(),
0,
"rollback must not copy any node on the {fixture} fixture"
);
}
#[test]
fn journalled_rollback_copies_zero_nodes() {
assert_rollback_copies_zero_nodes(&mut seeded(), "plain");
}
#[test]
fn journalled_rollback_copies_zero_nodes_on_a_saved_graph() {
assert_rollback_copies_zero_nodes(&mut seeded_columnar(), "columnar");
}
#[test]
fn journalled_rollback_copies_zero_nodes_on_an_indexed_graph() {
assert_rollback_copies_zero_nodes(&mut seeded_indexed(), "indexed");
}
#[test]
fn journalled_rollback_copies_zero_nodes_on_a_mapped_graph() {
assert_rollback_copies_zero_nodes(&mut seeded_mapped(), "mapped");
}
#[test]
fn the_columnar_fixture_writes_through_the_master_store() {
let mut graph = seeded_columnar();
let before = fingerprint(&mut graph);
run(&mut graph, "MATCH (n:Item {id: 1}) SET n.qty = 12345");
let after = fingerprint(&mut graph);
assert_ne!(
before.column_masters, after.column_masters,
"a successful columnar SET must land in the master store; if it does \
not, the columnar arms are exercising the per-node fallback"
);
assert_eq!(
before.columnar_rows, after.columnar_rows,
"a columnar SET must not move any node to a different row — it writes \
a cell of the store the backend owns, and the node's row identity is \
exactly what must stay put"
);
}