use super::*;
#[test]
fn collect_planner_stats_returns_valid_snapshot() {
let _guard = planner_test_lock().lock().expect("planner lock");
let base = temp_dir("collect_planner_stats_valid");
let handle = storage_api::open_store(storage_api::StorageConfig {
buffer_pool_pages: 8,
wal_dir: base.join("wal"),
wal_segment_max_bytes: 1 << 20,
manifest_path: base.join("ir.manifest"),
sstable_dir: base.join("sst"),
})
.unwrap();
let snapshot = collect_planner_stats(&handle, &ExecuteParams::default());
assert_eq!(snapshot.schema_version, 1);
assert_eq!(snapshot.ttl_millis, 60_000);
assert!(snapshot.vector_selectivity_ppm <= 1_000_000);
assert!(snapshot.graph_selectivity_ppm <= 1_000_000);
assert!(snapshot.stats_version > 0);
}
#[test]
fn collect_planner_stats_arms_cbo() {
let _guard = planner_test_lock().lock().expect("planner lock");
clear_planner_stats();
let base = temp_dir("collect_planner_stats_arms_cbo");
let handle = storage_api::open_store(storage_api::StorageConfig {
buffer_pool_pages: 8,
wal_dir: base.join("wal"),
wal_segment_max_bytes: 1 << 20,
manifest_path: base.join("ir.manifest"),
sstable_dir: base.join("sst"),
})
.unwrap();
let snapshot = collect_planner_stats(&handle, &ExecuteParams::default());
set_planner_stats(snapshot).unwrap();
let ast = parse("MATCH (n) WHERE vector.cosine(n.embedding, $vec) > 0.5 RETURN n").unwrap();
let typed = validate(&ast, &Catalog).unwrap();
let plan = explain(&typed).unwrap();
assert_eq!(plan.planner_mode, "cbo");
assert!(plan.stats_version.is_some());
clear_planner_stats();
}
#[test]
fn explain_uses_vector_first_when_vector_predicate_present() {
let _guard = planner_test_lock().lock().expect("planner lock");
clear_planner_stats();
let ast =
parse("MATCH (n) WHERE vector.cosine(n.legal_risk_emb, $vec) > 0.8 RETURN n LIMIT 10")
.unwrap();
let typed = validate(&ast, &Catalog).unwrap();
let plan = explain(&typed).unwrap();
assert_eq!(plan.strategy, "vector-first");
assert_eq!(plan.planner_mode, "heuristic");
assert_eq!(plan.physical_ops.first(), Some(&PhysicalOp::VectorScan));
assert!(plan.physical_ops.contains(&PhysicalOp::Limit));
}
#[test]
fn explain_caches_inline_vector_predicate_values() {
let _guard = planner_test_lock().lock().expect("planner lock");
clear_planner_stats();
let ast =
parse("MATCH (n) WHERE vector.cosine(n.embedding, $q:1:0:-2) > 0.8 RETURN n LIMIT 10")
.unwrap();
let typed = validate(&ast, &Catalog).unwrap();
let plan = explain(&typed).unwrap();
let predicate = plan.predicate.expect("vector predicate");
assert_eq!(predicate.param, "$q:1:0:-2");
assert_eq!(
predicate.inline_vector.as_deref(),
Some(&[1.0, 0.0, -2.0][..])
);
}
#[test]
fn explain_uses_graph_first_without_where_predicate() {
let _guard = planner_test_lock().lock().expect("planner lock");
clear_planner_stats();
let ast = parse("MATCH (n) RETURN n").unwrap();
let typed = validate(&ast, &Catalog).unwrap();
let plan = explain(&typed).unwrap();
assert_eq!(plan.strategy, "graph-first");
assert_eq!(plan.planner_mode, "heuristic");
assert_eq!(plan.physical_ops.first(), Some(&PhysicalOp::NodeScan));
}
#[test]
fn explain_uses_bitmap_first_when_bitmap_predicate_present() {
let _guard = planner_test_lock().lock().expect("planner lock");
clear_planner_stats();
let ast =
parse("MATCH (n) WHERE bitmap.contains(idx_country, US) = 1 RETURN n LIMIT 10").unwrap();
let typed = validate(&ast, &Catalog).unwrap();
let plan = explain(&typed).unwrap();
assert_eq!(plan.strategy, "bitmap-first");
assert_eq!(plan.physical_ops.first(), Some(&PhysicalOp::BitmapScan));
assert!(plan.bitmap_predicate.is_some());
}
#[test]
fn explain_includes_with_and_multi_return_items() {
let _guard = planner_test_lock().lock().expect("planner lock");
clear_planner_stats();
let ast = parse("MATCH (n), (m) WITH n, count(m) AS cnt RETURN n, cnt LIMIT 5").unwrap();
let typed = validate(&ast, &Catalog).unwrap();
let plan = explain(&typed).unwrap();
assert_eq!(plan.logical_ops[0], "Match(n, m)");
assert!(plan
.logical_ops
.iter()
.any(|op| op == "With(n, count(m) AS cnt)"));
assert!(plan.logical_ops.iter().any(|op| op == "Return(n, cnt)"));
}
#[test]
fn explain_uses_cbo_when_fresh_stats_available() {
let _guard = planner_test_lock().lock().expect("planner lock");
clear_planner_stats();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
.min(u64::MAX as u128) as u64;
set_planner_stats(PlannerStatsSnapshot {
schema_version: 1,
stats_version: 42,
collected_at_millis: now,
ttl_millis: 60_000,
node_scan_base_cost: 200,
vector_scan_base_cost: 500,
filter_base_cost: 20,
vector_selectivity_ppm: 800_000,
graph_selectivity_ppm: 100_000,
skew_penalty_cost: 10,
})
.unwrap();
let ast = parse("MATCH (n) WHERE vector.cosine(n.embedding, $vec) > 0.5 RETURN n").unwrap();
let typed = validate(&ast, &Catalog).unwrap();
let plan = explain(&typed).unwrap();
assert_eq!(plan.planner_mode, "cbo");
assert_eq!(plan.stats_version, Some(42));
assert_eq!(plan.strategy, "graph-first");
clear_planner_stats();
}
#[test]
fn explain_falls_back_to_heuristic_when_stats_are_stale() {
let _guard = planner_test_lock().lock().expect("planner lock");
clear_planner_stats();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
.min(u64::MAX as u128) as u64;
set_planner_stats(PlannerStatsSnapshot {
schema_version: 1,
stats_version: 7,
collected_at_millis: now.saturating_sub(120_000),
ttl_millis: 1_000,
node_scan_base_cost: 50,
vector_scan_base_cost: 50,
filter_base_cost: 10,
vector_selectivity_ppm: 100_000,
graph_selectivity_ppm: 900_000,
skew_penalty_cost: 10,
})
.unwrap();
let ast = parse("MATCH (n) WHERE vector.cosine(n.embedding, $vec) > 0.5 RETURN n").unwrap();
let typed = validate(&ast, &Catalog).unwrap();
let plan = explain(&typed).unwrap();
assert_eq!(plan.planner_mode, "heuristic");
assert_eq!(plan.stats_version, None);
assert_eq!(plan.strategy, "vector-first");
clear_planner_stats();
}
#[test]
fn set_planner_stats_rejects_invalid_schema() {
let _guard = planner_test_lock().lock().expect("planner lock");
clear_planner_stats();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
.min(u64::MAX as u128) as u64;
let err = set_planner_stats(PlannerStatsSnapshot {
schema_version: 99,
stats_version: 1,
collected_at_millis: now,
ttl_millis: 1_000,
node_scan_base_cost: 100,
vector_scan_base_cost: 100,
filter_base_cost: 10,
vector_selectivity_ppm: 500_000,
graph_selectivity_ppm: 500_000,
skew_penalty_cost: 0,
})
.unwrap_err();
assert!(matches!(err, ExplainError::InvalidPlan(_)));
}