Skip to main content

quiver_storage/
lib.rs

1use std::path::Path;
2use std::sync::Once;
3
4use anyhow::Context;
5use refinery::Migration;
6use rusqlite::Connection;
7
8pub mod embeddings;
9pub mod fts;
10pub mod pool;
11pub mod scores;
12pub mod sources;
13pub mod suggestions;
14pub mod tools;
15pub mod usage;
16
17const M001: &str = include_str!("../migrations/001_init.sql");
18const M002: &str = include_str!("../migrations/002_fts.sql");
19const M003: &str = include_str!("../migrations/003_vec.sql");
20const M004: &str = include_str!("../migrations/004_embeddings.sql");
21const M005: &str = include_str!("../migrations/005_usage_uuid.sql");
22const M006: &str = include_str!("../migrations/006_agent_suggestions.sql");
23
24fn migrations() -> anyhow::Result<Vec<Migration>> {
25    Ok(vec![
26        Migration::unapplied("V1__init", M001).context("parse V1__init")?,
27        Migration::unapplied("V2__fts", M002).context("parse V2__fts")?,
28        Migration::unapplied("V3__vec", M003).context("parse V3__vec")?,
29        Migration::unapplied("V4__embeddings", M004).context("parse V4__embeddings")?,
30        Migration::unapplied("V5__usage_uuid", M005).context("parse V5__usage_uuid")?,
31        Migration::unapplied("V6__agent_suggestions", M006)
32            .context("parse V6__agent_suggestions")?,
33    ])
34}
35
36static VEC_INIT: Once = Once::new();
37
38/// Register the sqlite-vec extension as an auto-extension so every
39/// `Connection::open` after this point loads `vec0`. Idempotent.
40pub fn ensure_vec_extension() {
41    VEC_INIT.call_once(|| {
42        // sqlite_vec::sqlite3_vec_init has the exact extension entry-point
43        // signature `unsafe extern "C" fn(*mut sqlite3, *mut *mut c_char,
44        // *const sqlite3_api_routines) -> c_int`. Cast through *const () to
45        // bridge potential c_char vs i8/u8 platform differences without
46        // dragging in libsqlite3-sys directly.
47        type ExtInit = unsafe extern "C" fn(
48            *mut rusqlite::ffi::sqlite3,
49            *mut *mut std::os::raw::c_char,
50            *const rusqlite::ffi::sqlite3_api_routines,
51        ) -> std::os::raw::c_int;
52        unsafe {
53            let init: ExtInit = std::mem::transmute(sqlite_vec::sqlite3_vec_init as *const ());
54            rusqlite::ffi::sqlite3_auto_extension(Some(init));
55        }
56    });
57}
58
59/// Open a SQLite DB at `path` and run all pending migrations (001 + 002).
60/// Migration 003 (`tools_vec`) is deferred until the `sqlite-vec` extension
61/// is wired — see PLAN.md §6 and §3.
62pub fn open(path: &Path) -> anyhow::Result<Connection> {
63    ensure_vec_extension();
64    let mut conn =
65        Connection::open(path).with_context(|| format!("open sqlite at {}", path.display()))?;
66    let migs = migrations()?;
67    refinery::Runner::new(&migs)
68        .run(&mut conn)
69        .context("run refinery migrations")?;
70    Ok(conn)
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    fn table_names(conn: &Connection) -> Vec<String> {
78        let mut stmt = conn
79            .prepare(
80                "SELECT name FROM sqlite_master \
81                 WHERE type IN ('table','view') ORDER BY name",
82            )
83            .unwrap();
84        stmt.query_map([], |row| row.get::<_, String>(0))
85            .unwrap()
86            .map(|r| r.unwrap())
87            .collect()
88    }
89
90    #[test]
91    fn open_creates_expected_tables() {
92        let dir = tempfile::tempdir().unwrap();
93        let conn = open(&dir.path().join("quiver.sqlite")).unwrap();
94        let names = table_names(&conn);
95        for expected in [
96            "tools",
97            "usage_events",
98            "tool_scores",
99            "sources",
100            "tools_fts",
101        ] {
102            assert!(
103                names.contains(&expected.to_string()),
104                "missing {expected} in {names:?}"
105            );
106        }
107    }
108
109    #[test]
110    fn open_is_idempotent() {
111        let dir = tempfile::tempdir().unwrap();
112        let path = dir.path().join("quiver.sqlite");
113        let _c1 = open(&path).unwrap();
114        let _c2 = open(&path).unwrap();
115    }
116}