Files
fn_registry/functions/infra/migration_down.go
T
egutierrez 35a49174ca feat: funciones impuras migration_create, migration_up, migration_down, migration_status
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>
2026-04-13 02:01:40 +02:00

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()
}