use std::sync::Arc;
use grit_core::{GraphOp, Grit, ManualClock, Options};
use serde_json::json;
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();
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}"
);
}
#[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();
}
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"
);
}