package main import ( "database/sql" "embed" "fmt" "io/fs" "sort" "strings" _ "github.com/mattn/go-sqlite3" ) //go:embed migrations/*.sql var migrationsFS embed.FS // openDB opens (or creates) the SQLite database and applies migrations. func openDB(path string) (*sql.DB, error) { dsn := fmt.Sprintf("file:%s?_journal=WAL&_foreign_keys=on&_busy_timeout=5000", path) conn, err := sql.Open("sqlite3", dsn) if err != nil { return nil, fmt.Errorf("open: %w", err) } if err := conn.Ping(); err != nil { return nil, fmt.Errorf("ping: %w", err) } if err := applyMigrations(conn); err != nil { conn.Close() return nil, fmt.Errorf("migrations: %w", err) } return conn, nil } func applyMigrations(conn *sql.DB) error { files, err := fs.Glob(migrationsFS, "migrations/*.sql") if err != nil { return err } sort.Strings(files) for _, f := range files { b, err := migrationsFS.ReadFile(f) if err != nil { return err } if _, err := conn.Exec(string(b)); err != nil { msg := err.Error() // Idempotent ignores for ADD COLUMN re-runs etc. if strings.Contains(msg, "duplicate column") || strings.Contains(msg, "already exists") { continue } return fmt.Errorf("%s: %w", f, err) } } return nil }