package db import ( "database/sql" "fmt" "os" "path/filepath" "sort" "strings" _ "modernc.org/sqlite" ) type DB struct { *sql.DB } func (d *DB) SQLDB() *sql.DB { return d.DB } func Open(dbPath string) (*DB, error) { if err := os.MkdirAll(filepath.Dir(dbPath), 0700); err != nil { return nil, err } db, err := sql.Open("sqlite", dbPath+"?_journal_mode=WAL&_foreign_keys=ON&_busy_timeout=5000") if err != nil { return nil, fmt.Errorf("opening db: %w", err) } if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil { db.Close() return nil, fmt.Errorf("enabling foreign_keys: %w", err) } return &DB{db}, nil } func (db *DB) Close() error { return db.DB.Close() } func (db *DB) RunMigrations() error { return db.runMigrationsInternal(migrationsFS, "migrations") } func (db *DB) runMigrationsInternal(mfs embedFS, migrationsRoot string) error { entries, err := mfs.ReadDir(migrationsRoot) if err != nil { return fmt.Errorf("reading migrations dir: %w", err) } var names []string for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") { names = append(names, e.Name()) } } sort.Strings(names) if _, err := db.Exec(` CREATE TABLE IF NOT EXISTS schema_migrations ( version TEXT PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); err != nil { return fmt.Errorf("creating schema_migrations table: %w", err) } for _, name := range names { var applied bool row := db.QueryRow("SELECT 1 FROM schema_migrations WHERE version = ?", name) if err := row.Scan(&applied); err == nil { applied = true } if applied { continue } data, err := mfs.ReadFile(filepath.Join(migrationsRoot, name)) if err != nil { return fmt.Errorf("reading migration %s: %w", name, err) } if _, err := db.Exec(string(data)); err != nil { return fmt.Errorf("applying migration %s: %w", name, err) } if _, err := db.Exec("INSERT INTO schema_migrations (version) VALUES (?)", name); err != nil { return fmt.Errorf("recording migration %s: %w", name, err) } } return nil } type embedFS interface { ReadDir(name string) ([]os.DirEntry, error) ReadFile(name string) ([]byte, error) }