35a49174ca
Fase 2 del issue 0015. MigrationCreate (crea archivo .sql template con version auto-calculada), MigrationUp (aplica migraciones pendientes en transacciones individuales), MigrationDown (revierte ultimas N via down_sql de _migrations), MigrationGetStatus (cruza disco con BD, detecta orphaned). Tests de integracion: ciclo completo create->up->status->down->status. 26 tests, todos pasan. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
80 lines
2.3 KiB
Go
80 lines
2.3 KiB
Go
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()
|
|
}
|