iridium-db 0.2.0

A high-performance vector-graph hybrid storage and indexing engine
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(_)));
}