package infra import ( "database/sql" "fmt" "time" ) // MigrationDown reverts the last n applied migrations by executing their down_sql // from the _migrations table. Migrations are reverted in reverse version order // (highest version first). Each reversion runs in its own transaction. // Returns the list of reverted migrations. If n <= 0, no migrations are reverted. func MigrationDown(db *sql.DB, n int) ([]Migration, error) { if n <= 0 { return nil, nil } // Fetch last n applied migrations in descending order const query = ` SELECT version, name, up_sql, down_sql, applied_at FROM _migrations ORDER BY version DESC LIMIT ?` rows, err := db.Query(query, n) if err != nil { return nil, fmt.Errorf("migration_down: query _migrations: %w", err) } defer rows.Close() var toRevert []Migration for rows.Next() { var m Migration var appliedAtStr string if err := rows.Scan(&m.Version, &m.Name, &m.UpSQL, &m.DownSQL, &appliedAtStr); err != nil { return nil, fmt.Errorf("migration_down: scan row: %w", err) } m.AppliedAt, _ = time.Parse("2006-01-02 15:04:05", appliedAtStr) toRevert = append(toRevert, m) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("migration_down: rows error: %w", err) } // Revert each migration in its own transaction (already in DESC order) var reverted []Migration for _, m := range toRevert { if err := revertMigration(db, m); err != nil { return reverted, fmt.Errorf("migration_down: reverting version %d (%s): %w", m.Version, m.Name, err) } reverted = append(reverted, m) } return reverted, nil } // revertMigration executes a migration's DownSQL within a transaction and removes it // from _migrations. If DownSQL is empty, only the record is removed. func revertMigration(db *sql.DB, m Migration) error { tx, err := db.Begin() if err != nil { return fmt.Errorf("begin transaction: %w", err) } defer tx.Rollback() //nolint:errcheck // Execute the down SQL if present if m.DownSQL != "" { if _, err := tx.Exec(m.DownSQL); err != nil { return fmt.Errorf("exec down_sql: %w", err) } } // Remove the migration record if _, err := tx.Exec("DELETE FROM _migrations WHERE version = ?", m.Version); err != nil { return fmt.Errorf("delete from _migrations: %w", err) } return tx.Commit() }