package db import ( "database/sql" "embed" "fmt" "io/fs" "sort" "strings" _ "modernc.org/sqlite" ) //go:embed migrations var migrationsFS embed.FS func Open(path string) (*sql.DB, error) { db, err := sql.Open("sqlite", path) if err != nil { return nil, err } // SQLite does not support concurrent writers; a single connection avoids locking errors. db.SetMaxOpenConns(1) if _, err := db.Exec("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;"); err != nil { db.Close() return nil, fmt.Errorf("set pragmas: %w", err) } if err := db.Ping(); err != nil { db.Close() return nil, fmt.Errorf("ping: %w", err) } return db, nil } func Migrate(db *sql.DB) error { 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("create schema_migrations: %w", err) } entries, err := fs.ReadDir(migrationsFS, "migrations") if err != nil { return fmt.Errorf("read migrations dir: %w", err) } var files []string for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".sql") { files = append(files, e.Name()) } } sort.Strings(files) for _, name := range files { var count int if err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations WHERE version = ?", name).Scan(&count); err != nil { return fmt.Errorf("check migration %s: %w", name, err) } if count > 0 { continue } data, err := migrationsFS.ReadFile("migrations/" + name) if err != nil { return fmt.Errorf("read migration %s: %w", name, err) } if _, err := db.Exec(string(data)); err != nil { return fmt.Errorf("apply migration %s: %w", name, err) } if _, err := db.Exec("INSERT INTO schema_migrations (version) VALUES (?)", name); err != nil { return fmt.Errorf("record migration %s: %w", name, err) } } return nil }