grit-core 0.2.4

Embedded, bi-temporal property graph for agent memory: one SQLite file, in-process, deterministic
Documentation
//! Plan pinning: `EXPLAIN QUERY PLAN` snapshots on core queries. At the
//! ≤10ms/≤50ms latency budget an index regression is a correctness bug, so a
//! full table scan of `edges` or `nodes` in the hot paths fails CI.

use std::sync::Arc;

use grit_core::{GraphOp, Grit, ManualClock, Options};
use serde_json::json;

/// Create a real database file (so the planner sees the real schema), then
/// inspect plans over a plain rusqlite connection.
fn plan_of(sql: &str, params_count: usize) -> String {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("plan.db");
    {
        let clock = Arc::new(ManualClock::new(1_000));
        let g = Grit::open(&path, Options::new("dev").clock(clock)).unwrap();
        // A little data so the planner has something to chew on.
        let a = g.new_id();
        let b = g.new_id();
        for (id, name) in [(a, "left"), (b, "right")] {
            g.apply(GraphOp::AddNode {
                id,
                kind: "k".into(),
                name: name.into(),
                summary: String::new(),
                attrs: json!({}),
                group_id: String::new(),
            })
            .unwrap();
        }
        g.apply(GraphOp::AddEdge {
            id: g.new_id(),
            src: a,
            dst: b,
            rel: "R".into(),
            fact: "left relates to right".into(),
            attrs: json!({}),
            group_id: String::new(),
            valid_at: None,
            invalid_at: None,
        })
        .unwrap();
    }

    let conn = rusqlite::Connection::open(&path).unwrap();
    let mut stmt = conn.prepare(&format!("EXPLAIN QUERY PLAN {sql}")).unwrap();
    let dummy: Vec<Box<dyn rusqlite::types::ToSql>> = (0..params_count)
        .map(|_| Box::new(1i64) as Box<dyn rusqlite::types::ToSql>)
        .collect();
    let refs: Vec<&dyn rusqlite::types::ToSql> = dummy.iter().map(|b| b.as_ref()).collect();
    let rows = stmt
        .query_map(refs.as_slice(), |r| r.get::<_, String>(3))
        .unwrap()
        .collect::<Result<Vec<_>, _>>()
        .unwrap();
    rows.join("\n")
}

#[test]
fn traversal_expansion_uses_edge_indexes() {
    for (name, sql) in [
        ("current", grit_core::EXPAND_SQL_CURRENT),
        ("as_at", grit_core::EXPAND_SQL_AS_AT),
    ] {
        let plan = plan_of(sql, 4);
        assert!(
            plan.contains("idx_edges_src") && plan.contains("idx_edges_dst"),
            "BFS expansion ({name}) must probe both edge indexes (OR-optimized), got:\n{plan}"
        );
        assert!(
            !plan.lines().any(|l| l.trim().starts_with("SCAN e")),
            "BFS expansion ({name}) must not full-scan edges, got:\n{plan}"
        );
    }
}

#[test]
fn incident_edge_lookup_uses_indexes() {
    let plan = plan_of("SELECT id FROM edges WHERE src = ?1 OR dst = ?1", 1);
    assert!(
        plan.contains("idx_edges_src") && plan.contains("idx_edges_dst"),
        "incident-edge lookup must be index-driven, got:\n{plan}"
    );
}

#[test]
fn node_point_lookup_uses_primary_key() {
    let plan = plan_of("SELECT name FROM nodes WHERE id = ?1", 1);
    assert!(
        plan.contains("USING INDEX sqlite_autoindex_nodes_1")
            || plan.contains("USING INTEGER PRIMARY KEY"),
        "node lookup must use the primary key, got:\n{plan}"
    );
}

/// The group filter on the vector legs must be consumed by vec0's
/// xBestIndex (partition-key pushdown), not applied by SQLite as a residual
/// filter after a global KNN — the residual shape silently reintroduces the
/// small-group recall bug the partition key exists to fix. vec0 encodes
/// consumed constraints in its idxstr, so pushdown ⇔ the plans differ.
#[test]
fn vec_knn_group_filter_is_pushed_into_the_scan() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("vecplan.db");
    {
        let clock = Arc::new(ManualClock::new(1_000));
        let g = Grit::open(&path, Options::new("dev").clock(clock)).unwrap();
        g.register_embedding_model("m", 4, "1").unwrap();
        let n = g.new_id();
        g.apply(GraphOp::AddNode {
            id: n,
            kind: "k".into(),
            name: "probe".into(),
            summary: String::new(),
            attrs: json!({}),
            group_id: "g".into(),
        })
        .unwrap();
        g.set_node_embedding(n, vec![1.0, 0.0, 0.0, 0.0]).unwrap();
    }

    // vec0 is available on plain connections via the auto-extension the
    // Grit open above registered.
    let conn = rusqlite::Connection::open(&path).unwrap();
    let plan = |sql: &str, params_count: usize| -> String {
        let mut stmt = conn.prepare(&format!("EXPLAIN QUERY PLAN {sql}")).unwrap();
        let dummy: Vec<Box<dyn rusqlite::types::ToSql>> = (0..params_count)
            .map(|_| Box::new(1i64) as Box<dyn rusqlite::types::ToSql>)
            .collect();
        let refs: Vec<&dyn rusqlite::types::ToSql> = dummy.iter().map(|b| b.as_ref()).collect();
        let rows = stmt
            .query_map(refs.as_slice(), |r| r.get::<_, String>(3))
            .unwrap()
            .collect::<Result<Vec<_>, _>>()
            .unwrap();
        rows.join("\n")
    };
    let filtered = plan(
        "SELECT id FROM vec_nodes
         WHERE embedding MATCH ?1 AND k = ?2 AND group_id = ?3",
        3,
    );
    let unfiltered = plan(
        "SELECT id FROM vec_nodes WHERE embedding MATCH ?1 AND k = ?2",
        2,
    );
    assert!(
        filtered.contains("VIRTUAL TABLE INDEX"),
        "filtered KNN must plan through vec0, got:\n{filtered}"
    );
    assert_ne!(
        filtered, unfiltered,
        "identical plans mean the group constraint was NOT consumed by \
         vec0's xBestIndex and would run as a post-scan residual filter"
    );
}