fastskill_core/storage/
vector_index.rs1use rusqlite::{Connection, Result};
4use std::path::Path;
5
6const SCHEMA_VERSION: i32 = 2;
8
9pub fn initialize_database(db_path: &Path) -> Result<()> {
11 if let Some(parent) = db_path.parent() {
13 if let Err(e) = std::fs::create_dir_all(parent) {
14 return Err(rusqlite::Error::FromSqlConversionFailure(
15 0,
16 rusqlite::types::Type::Text,
17 Box::new(e),
18 ));
19 }
20 }
21
22 let conn = Connection::open(db_path)?;
23
24 conn.execute(
26 "CREATE TABLE IF NOT EXISTS schema_version (
27 version INTEGER PRIMARY KEY
28 )",
29 [],
30 )?;
31
32 let current_version: Option<i32> = conn
34 .query_row(
35 "SELECT version FROM schema_version ORDER BY version DESC LIMIT 1",
36 [],
37 |row| row.get(0),
38 )
39 .ok();
40
41 if current_version.is_none_or(|v| v < SCHEMA_VERSION) {
42 apply_schema_updates(&conn)?;
44
45 conn.execute(
47 "INSERT OR REPLACE INTO schema_version (version) VALUES (?)",
48 [SCHEMA_VERSION],
49 )?;
50 }
51
52 Ok(())
53}
54
55fn apply_schema_updates(conn: &Connection) -> Result<()> {
57 let current_version: i32 = conn
59 .query_row(
60 "SELECT version FROM schema_version ORDER BY version DESC LIMIT 1",
61 [],
62 |row| row.get(0),
63 )
64 .unwrap_or(0);
65
66 if current_version < 1 {
68 conn.execute(
70 "CREATE TABLE IF NOT EXISTS skills (
71 id TEXT PRIMARY KEY,
72 skill_path TEXT NOT NULL,
73 frontmatter_json TEXT NOT NULL,
74 embedding_json TEXT NOT NULL,
75 file_hash TEXT NOT NULL,
76 updated_at TEXT NOT NULL
77 )",
78 [],
79 )?;
80
81 conn.execute(
83 "CREATE INDEX IF NOT EXISTS idx_updated_at ON skills(updated_at)",
84 [],
85 )?;
86
87 conn.execute(
88 "CREATE INDEX IF NOT EXISTS idx_file_hash ON skills(file_hash)",
89 [],
90 )?;
91 }
92
93 if current_version < 2 {
94 conn.execute("ALTER TABLE skills ADD COLUMN source_url TEXT", [])?;
96
97 conn.execute("ALTER TABLE skills ADD COLUMN source_type TEXT", [])?;
98
99 conn.execute("ALTER TABLE skills ADD COLUMN source_branch TEXT", [])?;
100
101 conn.execute("ALTER TABLE skills ADD COLUMN source_tag TEXT", [])?;
102
103 conn.execute("ALTER TABLE skills ADD COLUMN source_subdir TEXT", [])?;
104
105 conn.execute("ALTER TABLE skills ADD COLUMN installed_from TEXT", [])?;
106
107 conn.execute("ALTER TABLE skills ADD COLUMN version TEXT", [])?;
108
109 conn.execute("ALTER TABLE skills ADD COLUMN commit_hash TEXT", [])?;
110
111 conn.execute("ALTER TABLE skills ADD COLUMN fetched_at TEXT", [])?;
112
113 conn.execute("ALTER TABLE skills ADD COLUMN editable INTEGER", [])?;
114
115 conn.execute(
117 "CREATE INDEX IF NOT EXISTS idx_source_url ON skills(source_url)",
118 [],
119 )?;
120
121 conn.execute(
122 "CREATE INDEX IF NOT EXISTS idx_source_type ON skills(source_type)",
123 [],
124 )?;
125
126 conn.execute(
127 "CREATE INDEX IF NOT EXISTS idx_installed_from ON skills(installed_from)",
128 [],
129 )?;
130
131 conn.execute(
132 "CREATE INDEX IF NOT EXISTS idx_fetched_at ON skills(fetched_at)",
133 [],
134 )?;
135 }
136
137 Ok(())
138}
139
140pub struct VectorIndexConnection {
142 conn: Connection,
143}
144
145impl VectorIndexConnection {
146 pub fn open(db_path: &Path) -> Result<Self> {
148 initialize_database(db_path)?;
149 let conn = Connection::open(db_path)?;
150 Ok(Self { conn })
151 }
152
153 pub fn conn(&self) -> &Connection {
155 &self.conn
156 }
157
158 pub fn conn_mut(&mut self) -> &mut Connection {
160 &mut self.conn
161 }
162}
163
164#[cfg(test)]
165#[allow(clippy::unwrap_used)]
166mod tests {
167 use super::*;
168 use tempfile::NamedTempFile;
169
170 #[test]
171 fn test_database_initialization() {
172 let temp_file = NamedTempFile::new().unwrap();
173 let db_path = temp_file.path();
174
175 initialize_database(db_path).unwrap();
177
178 let conn = Connection::open(db_path).unwrap();
180
181 let table_count: i32 = conn
183 .query_row(
184 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='skills'",
185 [],
186 |row| row.get(0),
187 )
188 .unwrap();
189
190 assert_eq!(table_count, 1);
191
192 let version_count: i32 = conn
194 .query_row(
195 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_version'",
196 [],
197 |row| row.get(0),
198 )
199 .unwrap();
200
201 assert_eq!(version_count, 1);
202 }
203}