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
38pub fn ensure_vec_extension() {
41 VEC_INIT.call_once(|| {
42 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
59pub 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}