Skip to main content

fastskill_core/storage/
vector_index.rs

1//! SQLite database operations for vector index
2
3use rusqlite::{Connection, Result};
4use std::path::Path;
5
6/// Database schema version
7const SCHEMA_VERSION: i32 = 2;
8
9/// Initialize the vector index database
10pub fn initialize_database(db_path: &Path) -> Result<()> {
11    // Create parent directory if it doesn't exist
12    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    // Create schema version table
25    conn.execute(
26        "CREATE TABLE IF NOT EXISTS schema_version (
27            version INTEGER PRIMARY KEY
28        )",
29        [],
30    )?;
31
32    // Check current schema version
33    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
43        apply_schema_updates(&conn)?;
44
45        // Update schema version
46        conn.execute(
47            "INSERT OR REPLACE INTO schema_version (version) VALUES (?)",
48            [SCHEMA_VERSION],
49        )?;
50    }
51
52    Ok(())
53}
54
55/// Apply database schema updates
56fn apply_schema_updates(conn: &Connection) -> Result<()> {
57    // Get current schema version (default to 0 if no version table exists yet)
58    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    // Apply migrations based on current version
67    if current_version < 1 {
68        // Schema version 1: Initial schema
69        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        // Create indexes for better performance
82        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        // Schema version 2: Add source tracking columns
95        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        // Create indexes for the new columns
116        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
140/// Database connection wrapper with proper error handling
141pub struct VectorIndexConnection {
142    conn: Connection,
143}
144
145impl VectorIndexConnection {
146    /// Open a database connection and ensure schema is initialized
147    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    /// Get a reference to the underlying connection
154    pub fn conn(&self) -> &Connection {
155        &self.conn
156    }
157
158    /// Get a mutable reference to the underlying connection
159    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
176        initialize_database(db_path).unwrap();
177
178        // Verify schema was created
179        let conn = Connection::open(db_path).unwrap();
180
181        // Check if skills table exists
182        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        // Check if schema_version table exists
193        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}