use super::*;
use crate::graph::cow::{reset_schema_map_forks, schema_map_forks};
#[test]
fn a_failed_set_restores_a_grown_property_catalogue() {
let mut graph = seeded();
assert!(
!graph.node_type_metadata["Item"].contains_key("color"),
"the fixture must not already declare the property, or this is vacuous"
);
assert_rolls_back(
&mut graph,
"MATCH (n:Item {id: 1}) SET n.color = 'red' \
WITH n MATCH (m:Item {id: 2}) SET m.qty = duration({months: 2147483648})",
None,
);
assert!(
!graph.node_type_metadata["Item"].contains_key("color"),
"a rolled-back SET left `color` in Item's property catalogue"
);
}
#[test]
fn a_failed_columnar_set_restores_a_grown_property_catalogue() {
let mut graph = seeded_columnar();
assert_rolls_back(
&mut graph,
"MATCH (n:Item {id: 1}) SET n.color = 'red' \
WITH n MATCH (m:Item {id: 2}) SET m.qty = duration({months: 2147483648})",
None,
);
assert!(!graph.node_type_metadata["Item"].contains_key("color"));
}
#[test]
fn a_failed_create_restores_a_grown_type_catalogue() {
let mut graph = seeded();
assert!(!graph.node_type_metadata.contains_key("Widget"));
assert_rolls_back(
&mut graph,
"CREATE (w:Widget {id: 1, name: 'w', spin: 3}) \
WITH w MATCH (m:Item {id: 2}) SET m.qty = duration({months: 2147483648})",
None,
);
assert!(
!graph.node_type_metadata.contains_key("Widget"),
"a rolled-back CREATE left the type in the property catalogue"
);
assert!(
!graph.type_schemas.contains_key("Widget"),
"a rolled-back CREATE left the type's shared TypeSchema behind"
);
}
#[test]
fn a_failed_edge_write_restores_connection_metadata() {
let mut graph = seeded();
assert!(!graph.connection_type_metadata.contains_key("RELATES"));
assert!(
!graph.connection_type_metadata["LINKS"]
.property_types
.contains_key("note"),
"the fixture must not already declare the edge property"
);
assert_rolls_back(
&mut graph,
"MATCH (a:Item {id: 1}), (b:Item {id: 3}) \
CREATE (a)-[:RELATES {note: 'x'}]->(b) \
WITH a MATCH (m:Item {id: 2}) SET m.qty = duration({months: 2147483648})",
None,
);
assert!(
!graph.connection_type_metadata.contains_key("RELATES"),
"a rolled-back CREATE left the connection type behind"
);
assert_rolls_back(
&mut graph,
"MATCH (a:Item {id: 1})-[r:LINKS]->(b:Item {id: 2}) SET r.note = 'x' \
WITH a MATCH (m:Item {id: 2}) SET m.qty = duration({months: 2147483648})",
None,
);
assert!(
!graph.connection_type_metadata["LINKS"]
.property_types
.contains_key("note"),
"a rolled-back SET left an edge property in the connection catalogue"
);
}
#[test]
fn a_rolled_back_schema_growth_does_not_survive_save_and_load() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("rollback.kgl");
let path = path.to_str().expect("utf-8 path");
let mut graph = seeded();
expect_failure(
&mut graph,
"CREATE (w:Widget {id: 1, name: 'w'}) \
WITH w MATCH (n:Item {id: 1}) SET n.color = 'red' \
WITH n MATCH (m:Item {id: 2}) SET m.qty = duration({months: 2147483648})",
None,
);
let mut handle = std::sync::Arc::new(graph);
crate::graph::io::file::save_graph(&mut handle, path).expect("save");
let reloaded = crate::graph::io::file::load_file(path).expect("load");
assert!(
!reloaded.node_type_metadata.contains_key("Widget"),
"a rolled-back type was written into the .kgl"
);
assert!(
!reloaded.node_type_metadata["Item"].contains_key("color"),
"a rolled-back property was written into the .kgl"
);
}
#[test]
fn a_multi_row_set_records_a_new_property_once_and_correctly() {
let mut graph = wide_rows_into(DirGraph::new());
assert!(
!graph.node_type_metadata["Item"].contains_key("color"),
"precondition: the property must be new, or this is vacuous"
);
run(&mut graph, "MATCH (n:Item) SET n.color = 'red'");
assert_eq!(
graph.node_type_metadata["Item"]
.get("color")
.map(String::as_str),
Some("String"),
"a 200-row SET must declare the property it introduced"
);
let mut graph = wide_rows_into(DirGraph::new());
assert_rolls_back(
&mut graph,
"MATCH (n:Item) SET n.color = 'red' \
WITH n LIMIT 1 MATCH (m:Item {id: 2}) \
SET m.qty = duration({months: 2147483648})",
None,
);
assert!(
!graph.node_type_metadata["Item"].contains_key("color"),
"a rolled-back multi-row SET left the property in the catalogue"
);
}
#[test]
fn a_set_with_mixed_value_types_records_the_last_one() {
let mut graph = wide_rows_into(DirGraph::new());
run(
&mut graph,
"MATCH (n:Item) WITH n ORDER BY n.qty \
SET n.mixed = CASE WHEN n.qty < 100 THEN 1 ELSE 'late' END",
);
assert_eq!(
graph.node_type_metadata["Item"]
.get("mixed")
.map(String::as_str),
Some("String"),
"the catalogue must carry the type name of the last row written, which \
is what the per-row upsert this replaced recorded"
);
}
#[test]
fn the_shell_restores_the_alias_and_parent_maps() {
let mut graph = seeded();
graph
.id_field_aliases_mut()
.insert("Item".to_string(), "npdid".to_string());
graph
.title_field_aliases_mut()
.insert("Item".to_string(), "label".to_string());
graph
.parent_types_mut()
.insert("Tag".to_string(), "Item".to_string());
let shell = graph.schema_shell();
graph.id_field_aliases_mut().clear();
graph.title_field_aliases_mut().clear();
graph.parent_types_mut().clear();
graph
.node_type_metadata_mut()
.insert("Ghost".to_string(), HashMap::new());
graph.restore_schema_shell(shell);
assert_eq!(
graph.id_field_aliases.get("Item").map(String::as_str),
Some("npdid")
);
assert_eq!(
graph.title_field_aliases.get("Item").map(String::as_str),
Some("label")
);
assert_eq!(
graph.parent_types.get("Tag").map(String::as_str),
Some("Item")
);
assert!(!graph.node_type_metadata.contains_key("Ghost"));
}
fn wide_schema() -> DirGraph {
let mut graph = DirGraph::new();
for t in 0..200 {
let mut props: HashMap<String, String> = HashMap::new();
for c in 0..50 {
props.insert(format!("p{c}"), "Int64".to_string());
}
graph.upsert_node_type_metadata(&format!("T{t}"), props);
}
graph.rebuild_type_schemas();
graph
}
#[test]
fn a_statement_that_changes_no_schema_forks_no_map() {
let mut graph = wide_schema();
reset_schema_map_forks();
run(&mut graph, "MATCH (n:T0 {id: -1}) SET n.p0 = 7");
assert_eq!(
schema_map_forks(),
0,
"a statement that changes no schema must copy no schema map; the shell \
clone is a pointer copy"
);
let mut graph = seeded();
reset_schema_map_forks();
run(&mut graph, "MATCH (n:Item {id: 1}) SET n.qty = 99");
assert_eq!(
schema_map_forks(),
0,
"an upsert whose keys are all already declared must not fork the map"
);
assert_eq!(item_prop(&graph, 1, "qty"), Some(Value::Int64(99)));
}
#[test]
fn a_schema_growing_statement_forks_each_touched_map_once() {
let mut graph = wide_rows_into(DirGraph::new());
reset_schema_map_forks();
run(&mut graph, "MATCH (n:Item) SET n.color = 'red'");
let forks = schema_map_forks();
assert_eq!(
forks, 3,
"a 200-row SET that introduces one property must fork exactly the three \
pieces of schema state it changes — node_type_metadata, type_schemas, \
and the interner, which sees the name `color` for the first time — once \
each, not once per row; saw {forks}"
);
reset_schema_map_forks();
run(&mut graph, "MATCH (n:Item) SET n.color = 'blue'");
assert_eq!(
schema_map_forks(),
0,
"the second pass over the same property must fork no map"
);
}
#[test]
fn the_fork_counter_sees_a_real_copy() {
let mut graph = wide_schema();
reset_schema_map_forks();
let _second_handle = graph.node_type_metadata.clone();
graph
.node_type_metadata_mut()
.insert("New".to_string(), HashMap::new());
assert_eq!(
schema_map_forks(),
1,
"writing through a shared handle must copy, and must be counted"
);
}