verstak/internal/core/storage/storage.go

127 lines
2.6 KiB
Go

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, etc.
}
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(&currentVer); 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()
}