use super::*;
use crate::api::cypher::CypherResult;
use crate::graph::dir_graph::DirGraph;
use crate::graph::session::execute::{execute_mut, execute_read, ExecuteOptions};
use std::collections::HashMap;
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(graph: &DirGraph, query: &str) -> CypherResult {
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
execute_read(graph, query, &opts)
.unwrap_or_else(|e| panic!("query failed: {query}: {e}"))
.result
}
fn rows(result: &CypherResult) -> Vec<String> {
let mut out: Vec<String> = result.rows.iter().map(|row| format!("{:?}", row)).collect();
out.sort();
out
}
fn indexed_fixture() -> DirGraph {
let mut graph = DirGraph::new();
for i in 0..60u32 {
run(
&mut graph,
&format!(
"CREATE (:Item {{id: {i}, sku: 'sku-{i}', category: 'cat-{}', \
region: 'reg-{}', score: {i}}})",
i % 5,
i % 3
),
);
}
run(
&mut graph,
"CREATE CONSTRAINT item_sku FOR (n:Item) REQUIRE n.sku IS UNIQUE",
);
run(&mut graph, "CREATE INDEX FOR (n:Item) ON (n.category)");
run(&mut graph, "CREATE INDEX FOR (n:Item) ON (n.region)");
run(
&mut graph,
"CREATE INDEX FOR (n:Item) ON (n.category, n.region)",
);
run(&mut graph, "CREATE RANGE INDEX FOR (n:Item) ON (n.score)");
graph
}
fn write_fixture(dir: &std::path::Path) -> String {
let path = dir.join("indexed.kgl");
let mut arc = Arc::new(indexed_fixture());
prepare_save(&mut arc);
Arc::make_mut(&mut arc).enable_columnar();
write_kgl(&arc, path.to_str().unwrap()).unwrap();
path.to_str().unwrap().to_string()
}
fn load_eager(path: &str) -> Arc<DirGraph> {
load_file_with(path, &LoadOptions::new().with_defer_index_rebuild(false)).unwrap()
}
fn load_deferred(path: &str) -> Arc<DirGraph> {
let graph = load_file_with(path, &LoadOptions::new().with_defer_index_rebuild(true)).unwrap();
assert!(
graph.indexes_deferred(),
"the option must have produced a deferred load"
);
graph
}
const SHAPES: &[&str] = &[
"MATCH (n:Item {category: 'cat-2'}) RETURN n.sku AS sku",
"MATCH (n:Item) WHERE n.category = 'cat-3' RETURN n.sku AS sku",
"MATCH (n:Item {category: 'cat-1', region: 'reg-2'}) RETURN n.sku AS sku",
"MATCH (n:Item) WHERE n.category = 'cat-0' AND n.region = 'reg-1' RETURN n.sku AS sku",
"MATCH (n:Item) WHERE n.score > 40 RETURN n.sku AS sku",
"MATCH (n:Item) WHERE n.score >= 10 AND n.score < 20 RETURN n.sku AS sku",
"MATCH (n:Item {sku: 'sku-17'}) RETURN n.id AS id",
"MATCH (n:Item {category: 'nope'}) RETURN n.sku AS sku",
"MATCH (n:Item) WHERE n.category IN ['cat-1', 'cat-4'] RETURN n.sku AS sku",
"MATCH (n:Item) RETURN count(n) AS c",
];
#[test]
fn deferred_load_answers_every_indexed_shape_identically() {
let dir = tempfile::tempdir().unwrap();
let path = write_fixture(dir.path());
let eager = load_eager(&path);
let deferred = load_deferred(&path);
for query in SHAPES {
let expected = read(&eager, query);
let actual = read(&deferred, query);
assert_eq!(
rows(&expected),
rows(&actual),
"deferred load diverged from eager on: {query}"
);
if !query.contains("'nope'") {
assert!(
!expected.rows.is_empty(),
"fixture query returns no rows, making the comparison vacuous: {query}"
);
}
}
}
#[test]
fn deferred_load_builds_nothing_and_keeps_every_declaration() {
let dir = tempfile::tempdir().unwrap();
let path = write_fixture(dir.path());
let eager = load_eager(&path);
let deferred = load_deferred(&path);
assert!(
!eager.property_indices.is_empty()
&& !eager.composite_indices.is_empty()
&& !eager.range_indices.is_empty()
&& !eager.unique_indices.is_empty(),
"the eager control must actually build all four families"
);
assert!(
deferred.property_indices.is_empty()
&& deferred.composite_indices.is_empty()
&& deferred.range_indices.is_empty()
&& deferred.unique_indices.is_empty(),
"a deferred load must build nothing"
);
assert_eq!(deferred.property_index_keys, eager.property_index_keys);
assert_eq!(deferred.composite_index_keys, eager.composite_index_keys);
assert_eq!(deferred.range_index_keys, eager.range_index_keys);
let mut materialized = deferred;
assert!(Arc::make_mut(&mut materialized).materialize_indexes());
assert!(!materialized.indexes_deferred());
assert_eq!(
materialized.list_indexes().len(),
eager.list_indexes().len()
);
assert_eq!(
materialized.list_composite_indexes(),
eager.list_composite_indexes()
);
assert_eq!(
materialized.list_unique_constraints(),
eager.list_unique_constraints()
);
assert!(!Arc::make_mut(&mut materialized).materialize_indexes());
}
#[test]
fn deferred_state_never_claims_an_index_is_present() {
let dir = tempfile::tempdir().unwrap();
let path = write_fixture(dir.path());
let deferred = load_deferred(&path);
assert!(!deferred.has_index("Item", "category"));
assert!(!deferred.has_any_index("Item", "category"));
assert!(!deferred.has_composite_index("Item", &["category".to_string(), "region".to_string()]));
assert!(!deferred.has_unique_constraints());
assert!(!deferred.has_unique_constraint("Item", &["sku".to_string()]));
assert!(deferred
.lookup_by_index("Item", "category", &Value::String("cat-1".into()))
.is_none());
assert!(deferred
.lookup_by_composite_index(
"Item",
&["category".to_string(), "region".to_string()],
&[Value::String("cat-1".into()), Value::String("reg-1".into())]
)
.is_none());
}
#[test]
fn deferred_indexes_are_listed_with_their_state_while_predicates_stay_absent() {
use crate::graph::dir_graph::indexes::IndexState;
use crate::graph::introspection::schema_overview::collect_indexes_structured;
let dir = tempfile::tempdir().unwrap();
let path = write_fixture(dir.path());
let eager = load_eager(&path);
let deferred = load_deferred(&path);
let names = |graph: &DirGraph| {
let mut out: Vec<String> = graph
.list_indexes_with_state()
.into_iter()
.map(|(node_type, property, _)| format!("{node_type}.{property}"))
.collect();
out.sort();
out
};
assert_eq!(names(&deferred), names(&eager));
assert!(!names(&deferred).is_empty(), "the fixture declares indexes");
assert!(deferred
.list_indexes_with_state()
.iter()
.all(|(_, _, state)| *state == IndexState::Deferred));
assert!(eager
.list_indexes_with_state()
.iter()
.all(|(_, _, state)| *state == IndexState::Ready));
let composite = deferred.list_composite_indexes_with_state();
assert_eq!(composite.len(), 1);
assert_eq!(composite[0].2, IndexState::Deferred);
assert_eq!(
deferred.list_composite_indexes(),
eager.list_composite_indexes()
);
assert_eq!(
deferred.list_unique_constraints(),
eager.list_unique_constraints()
);
assert!(!deferred.list_unique_constraints().is_empty());
let rows = collect_indexes_structured(&deferred);
assert!(!rows.is_empty());
assert!(
rows.iter().all(|row| row.state == "DEFERRED"),
"every declared index must be listed as DEFERRED: {rows:?}"
);
assert!(collect_indexes_structured(&eager)
.iter()
.all(|row| row.state == "ONLINE"));
assert!(!deferred.has_index("Item", "category"));
assert!(!deferred.has_composite_index("Item", &["category".to_string(), "region".to_string()]));
assert!(deferred.indexes_deferred());
}
#[test]
fn write_before_first_indexed_read_leaves_indexes_correct() {
let dir = tempfile::tempdir().unwrap();
let path = write_fixture(dir.path());
let mut deferred = load_deferred(&path);
let graph = crate::graph::handle::make_dir_graph_mut(&mut deferred);
assert!(
!graph.indexes_deferred(),
"acquiring a mutable graph must materialize before the caller writes"
);
run(
graph,
"CREATE (:Item {id: 900, sku: 'sku-900', category: 'cat-2', \
region: 'reg-1', score: 99})",
);
run(
graph,
"MATCH (n:Item {sku: 'sku-16'}) SET n.category = 'cat-2'",
);
let hits = read(
&deferred,
"MATCH (n:Item {category: 'cat-2'}) RETURN n.sku AS sku",
);
let mut skus = rows(&hits);
skus.dedup();
assert_eq!(
skus.len(),
hits.rows.len(),
"an indexed lookup returned a node twice — the index was filed into mid-build"
);
let scanned = read(
&deferred,
"MATCH (n:Item) WHERE n.category = 'cat-2' RETURN n.sku AS sku",
);
assert_eq!(rows(&hits), rows(&scanned));
assert_eq!(hits.rows.len(), 14, "fixture drifted");
}
#[test]
fn deferred_load_still_enforces_unique_constraints() {
let dir = tempfile::tempdir().unwrap();
let path = write_fixture(dir.path());
let mut deferred = load_deferred(&path);
let graph = crate::graph::handle::make_dir_graph_mut(&mut deferred);
let params = HashMap::new();
let opts = ExecuteOptions::eager(¶ms);
let outcome = execute_mut(
graph,
"CREATE (:Item {id: 901, sku: 'sku-17', category: 'cat-0', region: 'reg-0', score: 1})",
&opts,
);
assert!(
outcome.is_err(),
"a duplicate `sku` was admitted after a deferred load — the unique \
constraint was not materialized before the write"
);
execute_mut(
graph,
"CREATE (:Item {id: 902, sku: 'sku-902', category: 'cat-0', region: 'reg-0', score: 1})",
&opts,
)
.unwrap();
}
#[test]
fn saving_a_deferred_graph_preserves_declarations_byte_for_byte() {
let dir = tempfile::tempdir().unwrap();
let path = write_fixture(dir.path());
let resave = |graph: Arc<DirGraph>, name: &str| -> Vec<u8> {
let out = dir.path().join(name);
let mut arc = graph;
prepare_save(&mut arc);
crate::graph::handle::make_dir_graph_mut_preserving_lineage(&mut arc).enable_columnar();
write_kgl(&arc, out.to_str().unwrap()).unwrap();
std::fs::read(&out).unwrap()
};
let from_eager = resave(load_eager(&path), "eager.kgl");
let deferred = load_deferred(&path);
let from_deferred = resave(deferred, "deferred.kgl");
assert_eq!(
from_eager, from_deferred,
"a deferred graph must re-save to the same bytes as an eager one"
);
let reloaded = load_eager(dir.path().join("deferred.kgl").to_str().unwrap());
assert!(reloaded.has_index("Item", "category"));
assert!(reloaded.has_unique_constraints());
assert_eq!(reloaded.list_unique_constraints().len(), 1);
}
#[test]
fn concurrent_first_readers_agree_with_eager() {
let dir = tempfile::tempdir().unwrap();
let path = write_fixture(dir.path());
let expected: Vec<Vec<String>> = {
let eager = load_eager(&path);
SHAPES.iter().map(|q| rows(&read(&eager, q))).collect()
};
let deferred = load_deferred(&path);
std::thread::scope(|scope| {
for _ in 0..8 {
let graph = Arc::clone(&deferred);
let expected = &expected;
scope.spawn(move || {
for (query, want) in SHAPES.iter().zip(expected.iter()) {
assert_eq!(&rows(&read(&graph, query)), want, "racing readers: {query}");
}
});
}
});
assert!(
deferred.indexes_deferred(),
"reads must not have materialized anything"
);
}
#[test]
fn materialization_happens_once_and_later_writes_are_indexed() {
let dir = tempfile::tempdir().unwrap();
let path = write_fixture(dir.path());
let mut graph = load_deferred(&path);
run(
crate::graph::handle::make_dir_graph_mut(&mut graph),
"CREATE (:Item {id: 903, sku: 'sku-903', category: 'cat-9', region: 'reg-0', score: 1})",
);
assert!(!graph.indexes_deferred());
run(
crate::graph::handle::make_dir_graph_mut(&mut graph),
"CREATE (:Item {id: 904, sku: 'sku-904', category: 'cat-9', region: 'reg-0', score: 2})",
);
let hits = read(
&graph,
"MATCH (n:Item {category: 'cat-9'}) RETURN n.sku AS sku",
);
assert_eq!(hits.rows.len(), 2);
}
#[test]
fn ddl_on_an_owned_deferred_graph_materializes_first() {
let dir = tempfile::tempdir().unwrap();
let path = write_fixture(dir.path());
let deferred = load_deferred(&path);
let mut owned = Arc::try_unwrap(deferred).unwrap_or_else(|arc| (*arc).clone());
assert!(owned.indexes_deferred());
assert_eq!(owned.drop_index("Item", "category"), Ok(true));
assert!(!owned.indexes_deferred());
assert!(
owned.has_index("Item", "region"),
"the untouched index survived"
);
}