package database import ( "database/sql" "embed" "fmt" "log/slog" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/golang-migrate/migrate/v4/source/iofs" _ "github.com/mattn/go-sqlite3" ) //go:embed migrations/*.sql var migrations embed.FS func Open(dbPath string) (*sql.DB, error) { connStr := dbPath + "?_journal_mode=WAL&_foreign_keys=1" migrationDb, err := sql.Open("sqlite3", connStr) if err != nil { return nil, fmt.Errorf("open db for migrations: %w", err) } if err := migrationDb.Ping(); err != nil { migrationDb.Close() return nil, fmt.Errorf("ping db: %w", err) } if err := runMigrations(migrationDb); err != nil { migrationDb.Close() return nil, fmt.Errorf("run migrations: %w", err) } migrationDb.Close() db, err := sql.Open("sqlite3", connStr) if err != nil { return nil, fmt.Errorf("open db: %w", err) } if err := db.Ping(); err != nil { db.Close() return nil, fmt.Errorf("ping db: %w", err) } return db, nil } func runMigrations(db *sql.DB) error { sourceDriver, err := iofs.New(migrations, "migrations") if err != nil { return fmt.Errorf("create migration source: %w", err) } driver, err := sqlite3.WithInstance(db, &sqlite3.Config{}) if err != nil { return fmt.Errorf("create database driver: %w", err) } m, err := migrate.NewWithInstance("iofs", sourceDriver, "sqlite3", driver) if err != nil { return fmt.Errorf("create migrate instance: %w", err) } defer m.Close() currentVersion, _, err := m.Version() if err != nil && err != migrate.ErrNilVersion { return fmt.Errorf("get current migration version: %w", err) } if err == migrate.ErrNilVersion { slog.Info("running database migrations", "currentVersion", "none") } else { slog.Info("running database migrations", "currentVersion", currentVersion) } upErr := m.Up() if upErr != nil && upErr != migrate.ErrNoChange { return fmt.Errorf("run migrations: %w", upErr) } newVersion, dirty, versionErr := m.Version() if versionErr != nil && versionErr != migrate.ErrNilVersion { return fmt.Errorf("get migration version after execution: %w", versionErr) } if upErr == migrate.ErrNoChange { slog.Info("no pending migrations", "currentVersion", currentVersion) } else if dirty { slog.Warn("migration completed but database is in dirty state", "version", newVersion) } else { if err == migrate.ErrNilVersion { slog.Info("migrations completed", "appliedVersion", newVersion) } else if currentVersion != newVersion { slog.Info("migrations completed", "fromVersion", currentVersion, "toVersion", newVersion) } else { slog.Info("migrations completed", "version", newVersion) } } return nil }