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 }