//! SQLite schema + migration (schema_version in metadata). use anyhow::{Context, Result}; use rusqlite::Connection; const SCHEMA_VERSION: i64 = 2; /// Fresh-install schema at the current version. Everything is `IF NOT EXISTS` /// so it can be re-run safely (the old version re-ran it on every `version < /// SCHEMA_VERSION`, which broke the moment any table already existed). const SCHEMA: &str = r#" CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY, path TEXT NOT NULL UNIQUE, content_hash TEXT NOT NULL, language TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS symbols ( id INTEGER PRIMARY KEY, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, name TEXT NOT NULL, qualified_name TEXT NOT NULL, kind TEXT NOT NULL, signature TEXT, line_start INTEGER NOT NULL, line_end INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id); CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name); CREATE TABLE IF NOT EXISTS imports ( id INTEGER PRIMARY KEY, from_file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, to_file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, import_type TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_imports_from ON imports(from_file_id); CREATE INDEX IF NOT EXISTS idx_imports_to ON imports(to_file_id); CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5( name, qualified_name, kind, signature, content='symbols', content_rowid='id' ); CREATE TRIGGER IF NOT EXISTS symbols_ai AFTER INSERT ON symbols BEGIN INSERT INTO symbols_fts(rowid, name, qualified_name, kind, signature) VALUES (new.id, new.name, new.qualified_name, new.kind, new.signature); END; CREATE TRIGGER IF NOT EXISTS symbols_ad AFTER DELETE ON symbols BEGIN INSERT INTO symbols_fts(symbols_fts, rowid, name, qualified_name, kind, signature) VALUES ('delete', old.id, old.name, old.qualified_name, old.kind, old.signature); END; CREATE VIRTUAL TABLE IF NOT EXISTS content_fts USING fts5(path UNINDEXED, content); "#; /// Incremental migrations. `MIGRATIONS[i]` upgrades from schema version `i+1` /// to `i+2`; version 0 is a fresh database and is created via [`SCHEMA`] /// directly. const MIGRATIONS: &[&str] = &[ // v1 -> v2: the `mtime` column was written but never read (change detection // is content-hash based). Drop the dead column. "ALTER TABLE files DROP COLUMN mtime;", ]; /// Create schema if missing, or migrate to the current version. pub fn init(conn: &mut Connection) -> Result<()> { conn.execute_batch( "CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);", )?; let version: i64 = conn .query_row( "SELECT COALESCE(CAST(value AS INTEGER), 0) FROM metadata WHERE key='schema_version'", [], |r| r.get(0), ) .unwrap_or(0); if version == 0 { conn.execute_batch(SCHEMA)?; } else { for (i, migration) in MIGRATIONS.iter().enumerate() { // i-th migration upgrades version (i+1) -> (i+2). if version <= (i as i64) + 1 { conn.execute_batch(migration) .with_context(|| format!("migration to schema v{} failed", i + 2))?; } } } conn.execute( "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?1)", [SCHEMA_VERSION.to_string()], )?; Ok(()) }