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>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user