use super::*;
use crate::graph::session::execute::{execute_mut, ExecuteOptions};
fn run(graph: &mut DirGraph, query: &str) {
let params = std::collections::HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
execute_mut(graph, query, &opts).unwrap_or_else(|e| panic!("setup query failed: {query}: {e}"));
}
fn bucket(graph: &DirGraph, node_type: &str) -> Vec<NodeIndex> {
graph
.type_indices
.get(node_type)
.map(|members| members.to_vec())
.unwrap_or_default()
}
fn seeded(count: i64) -> DirGraph {
let mut graph = DirGraph::new();
for id in 0..count {
run(&mut graph, &format!("CREATE (:Item {{id: {id}, v: {id}}})"));
}
graph.build_id_index("Item");
graph
}
fn delete(graph: &mut DirGraph, ids: &[i64]) -> (usize, usize) {
let doomed: HashSet<NodeIndex> = ids
.iter()
.map(|id| {
graph
.lookup_by_id_readonly("Item", &Value::Int64(*id))
.unwrap_or_else(|| panic!("id {id} must be indexed"))
})
.collect();
detach_delete_nodes(graph, &doomed)
}
#[test]
fn a_middle_delete_keeps_the_surviving_bucket_order() {
let mut graph = seeded(6);
let before = bucket(&graph, "Item");
let doomed = graph
.lookup_by_id_readonly("Item", &Value::Int64(2))
.unwrap();
assert_eq!(delete(&mut graph, &[2]), (1, 0));
let expected: Vec<NodeIndex> = before.into_iter().filter(|idx| *idx != doomed).collect();
assert_eq!(bucket(&graph, "Item"), expected);
}
#[test]
fn a_multi_delete_keeps_the_surviving_bucket_order() {
let mut graph = seeded(8);
let before = bucket(&graph, "Item");
let doomed: HashSet<NodeIndex> = [0i64, 3, 7]
.iter()
.map(|id| {
graph
.lookup_by_id_readonly("Item", &Value::Int64(*id))
.unwrap()
})
.collect();
assert_eq!(delete(&mut graph, &[0, 3, 7]), (3, 0));
let expected: Vec<NodeIndex> = before
.into_iter()
.filter(|idx| !doomed.contains(idx))
.collect();
assert_eq!(bucket(&graph, "Item"), expected);
for id in [1i64, 2, 4, 5, 6] {
assert!(graph
.lookup_by_id_readonly("Item", &Value::Int64(id))
.is_some());
}
for id in [0i64, 3, 7] {
assert!(graph
.lookup_by_id_readonly("Item", &Value::Int64(id))
.is_none());
}
}
#[test]
fn an_out_of_order_bucket_still_deletes_correctly() {
let mut graph = seeded(3);
assert_eq!(delete(&mut graph, &[1]), (1, 0));
run(&mut graph, "CREATE (:Item {id: 9, v: 9})");
let reused = graph
.lookup_by_id_readonly("Item", &Value::Int64(9))
.unwrap();
let before = bucket(&graph, "Item");
assert_eq!(
before.last(),
Some(&reused),
"the reused slot must be appended, not sorted in — otherwise this test \
is not exercising the fallback it exists for"
);
assert!(
before.windows(2).any(|pair| pair[0] > pair[1]),
"the bucket must be out of order for this test to mean anything"
);
assert_eq!(delete(&mut graph, &[9]), (1, 0));
let expected: Vec<NodeIndex> = before.into_iter().filter(|idx| *idx != reused).collect();
assert_eq!(bucket(&graph, "Item"), expected);
assert!(graph
.lookup_by_id_readonly("Item", &Value::Int64(9))
.is_none());
for id in [0i64, 2] {
assert!(graph
.lookup_by_id_readonly("Item", &Value::Int64(id))
.is_some());
}
}
#[test]
fn deleting_every_member_empties_the_bucket() {
let mut graph = seeded(4);
assert_eq!(delete(&mut graph, &[0, 1, 2, 3]), (4, 0));
assert!(bucket(&graph, "Item").is_empty());
assert_eq!(graph.graph.node_count(), 0);
}
#[test]
fn a_mass_delete_falls_back_to_the_retain_with_the_same_result() {
let mut graph = DirGraph::new();
let rows: Vec<Vec<Value>> = (0..2_000i64).map(|id| vec![Value::Int64(id)]).collect();
let frame = DataFrame::from_cypher_rows(vec!["id".to_string()], rows).unwrap();
add_nodes(
&mut graph,
frame,
"Item".to_string(),
"id".to_string(),
Some("id".to_string()),
None,
)
.unwrap();
let before = bucket(&graph, "Item");
assert!(before.len() > POSITIONAL_MIN_BUCKET);
let doomed_ids: Vec<i64> = (0..100).map(|i| i * 20).collect();
let doomed: HashSet<NodeIndex> = doomed_ids
.iter()
.map(|id| {
graph
.lookup_by_id_readonly("Item", &Value::Int64(*id))
.unwrap()
})
.collect();
assert!(
doomed.len().saturating_mul(POSITIONAL_MAX_SHARE) > before.len(),
"this delete must be above the crossover or it tests the other arm"
);
assert_eq!(detach_delete_nodes(&mut graph, &doomed), (100, 0));
let expected: Vec<NodeIndex> = before
.into_iter()
.filter(|idx| !doomed.contains(idx))
.collect();
assert_eq!(bucket(&graph, "Item"), expected);
assert!(graph
.lookup_by_id_readonly("Item", &Value::Int64(0))
.is_none());
assert!(graph
.lookup_by_id_readonly("Item", &Value::Int64(1))
.is_some());
}
#[test]
fn a_multi_type_delete_edits_each_bucket_independently() {
let mut graph = DirGraph::new();
run(
&mut graph,
"CREATE (:Item {id: 1}), (:Item {id: 2}), (:Tag {id: 10}), (:Tag {id: 11})",
);
graph.build_id_index("Item");
graph.build_id_index("Tag");
let item = graph
.lookup_by_id_readonly("Item", &Value::Int64(1))
.unwrap();
let tag = graph
.lookup_by_id_readonly("Tag", &Value::Int64(11))
.unwrap();
let item_survivor = graph
.lookup_by_id_readonly("Item", &Value::Int64(2))
.unwrap();
let tag_survivor = graph
.lookup_by_id_readonly("Tag", &Value::Int64(10))
.unwrap();
let doomed: HashSet<NodeIndex> = [item, tag].into_iter().collect();
assert_eq!(detach_delete_nodes(&mut graph, &doomed), (2, 0));
assert_eq!(bucket(&graph, "Item"), vec![item_survivor]);
assert_eq!(bucket(&graph, "Tag"), vec![tag_survivor]);
}