package storage import ( "database/sql" "fmt" "os" "path/filepath" "strings" "time" _ "github.com/mattn/go-sqlite3" ) // DB wraps sql.DB with Verstak-specific operations. type DB struct { *sql.DB path string } // Open opens or creates a SQLite database at path and runs pending migrations. func Open(path string) (*DB, error) { dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0o750); err != nil { return nil, fmt.Errorf("create db dir: %w", err) } db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?mode=rwc", path)) if err != nil { return nil, fmt.Errorf("open sqlite: %w", err) } db.SetMaxOpenConns(1) w := &DB{DB: db, path: path} if err := w.runInitialSchema(); err != nil { db.Close() return nil, err } if err := w.runMigrations(); err != nil { db.Close() return nil, err } return w, nil } // Path returns the on-disk path of the database file. func (db *DB) Path() string { return db.path } // --- internals --- const schemaVersionDDL = ` CREATE TABLE IF NOT EXISTS _schema_ver ( version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL ); ` var migrationFiles = map[int]string{ 1: migration001, 2: migration002, 3: migration003, 4: migration004, 5: migration005, 6: migration006, // 7: migration007 (FTS5) — created lazily by search.Rebuild() 8: migration008, 9: migration009, 10: migration010, 11: migration011, } func (db *DB) runInitialSchema() error { _, err := db.Exec(schemaVersionDDL) return err } func (db *DB) runMigrations() error { var currentVer int if err := db.QueryRow("SELECT COALESCE(MAX(version), 0) FROM _schema_ver").Scan(¤tVer); err != nil { return err } // Build sorted version list. versions := make([]int, 0, len(migrationFiles)) for v := range migrationFiles { versions = append(versions, v) } for i := 0; i < len(versions); i++ { for j := i + 1; j < len(versions); j++ { if versions[j] < versions[i] { versions[i], versions[j] = versions[j], versions[i] } } } for _, v := range versions { if v <= currentVer { continue } if err := db.applyMigration(v, migrationFiles[v]); err != nil { return fmt.Errorf("migrate v%d: %w", v, err) } } return nil } func (db *DB) applyMigration(version int, raw string) error { tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() for _, stmt := range strings.Split(raw, ";") { stmt = strings.TrimSpace(stmt) if stmt == "" { continue } if _, err := tx.Exec(stmt); err != nil { return fmt.Errorf("exec stat: %w; sql=%.200s", err, stmt) } } if _, err := tx.Exec("INSERT INTO _schema_ver (version, applied_at) VALUES (?, ?)", version, time.Now().UTC().Format(time.RFC3339)); err != nil { return err } return tx.Commit() }