package migrate
import (
"database/sql"
"errors"
"fmt"
"log/slog"
"strings"
)
func getDownMigration(migration string) (string, bool) {
_, downComment, hasDownMigration := strings.Cut(string(migration), "\n-- DOWN:")
if !hasDownMigration {
return "", false
}
downMigration := strings.Join(strings.Split(downComment, "\n-- "), "\n")
return downMigration, true
}
func Migrate(dbname string, db *sql.DB, steps [][]byte, log *slog.Logger) error {
var version int
var dirty bool
row := db.QueryRow("SELECT version, dirty FROM version;")
err := row.Scan(&version, &dirty)
if err != nil {
if log != nil {
log.Debug("could not get initial migration version", "db_name", dbname, "err", err)
}
}
if dirty {
return fmt.Errorf("migration dirty: on version %d", version)
}
if len(steps) == version+1 {
if log != nil {
log.Info("no steps to run", "db_name", dbname, "version", version)
}
return nil
}
if version+1 > len(steps)-1 {
return fmt.Errorf("unknown future schema version: observed: %d, last_in_code: %d", version, len(steps)-1)
}
for i, migration := range steps[version+1:] {
if log != nil {
log.Info("running migration", "db_name", dbname, "migration_char_len", len(migration), "version", i+1)
}
_, err := db.Exec(string(migration))
if err != nil {
_, errDirty := db.Exec(`UPDATE version SET version=$1, dirty=true`, i+1)
if downMigration, ok := getDownMigration(string(migration)); ok {
log.Error("failed to run migration, you can run a down migration by manually executing the following on the db:", "db_name", dbname, "err", err, "err_dirty", errDirty)
fmt.Println(downMigration)
}
return errors.Join(err, errDirty)
}
}
return nil
}