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,106 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MigrationGetStatus crosses migration files on disk with records in _migrations,
|
||||
// returning a sorted list of MigrationStatus entries ordered by version ascending.
|
||||
// Migrations present in _migrations but not on disk are included with Applied=true
|
||||
// and marked as orphaned in their Name (suffix " (orphaned)").
|
||||
// If _migrations does not exist yet, all file-based migrations are returned as pending.
|
||||
func MigrationGetStatus(db *sql.DB, dir string) ([]MigrationStatus, error) {
|
||||
// Load files from disk (non-fatal if dir is empty or missing)
|
||||
fileMigrations, err := loadMigrationsFromDir(dir)
|
||||
if err != nil && !strings.Contains(err.Error(), "cannot read migrations directory") {
|
||||
return nil, fmt.Errorf("migration_status: %w", err)
|
||||
}
|
||||
if err != nil {
|
||||
// Directory doesn't exist — treat as empty
|
||||
fileMigrations = nil
|
||||
}
|
||||
|
||||
// Build a map from version to file migration
|
||||
fileMap := make(map[int]Migration, len(fileMigrations))
|
||||
for _, m := range fileMigrations {
|
||||
fileMap[m.Version] = m
|
||||
}
|
||||
|
||||
// Load applied migrations from DB (_migrations may not exist yet)
|
||||
dbMap, err := loadAppliedMigrations(db)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migration_status: %w", err)
|
||||
}
|
||||
|
||||
// Merge: collect all known versions
|
||||
allVersions := make(map[int]struct{})
|
||||
for v := range fileMap {
|
||||
allVersions[v] = struct{}{}
|
||||
}
|
||||
for v := range dbMap {
|
||||
allVersions[v] = struct{}{}
|
||||
}
|
||||
|
||||
// Build status list
|
||||
var statuses []MigrationStatus
|
||||
for v := range allVersions {
|
||||
dbEntry, inDB := dbMap[v]
|
||||
fileEntry, inFile := fileMap[v]
|
||||
|
||||
name := fileEntry.Name
|
||||
if !inFile && inDB {
|
||||
// Orphaned: applied but no file on disk
|
||||
name = dbEntry.Name + " (orphaned)"
|
||||
}
|
||||
|
||||
statuses = append(statuses, MigrationStatus{
|
||||
Version: v,
|
||||
Name: name,
|
||||
Applied: inDB,
|
||||
AppliedAt: dbEntry.AppliedAt, // zero value if not in DB
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(statuses, func(i, j int) bool {
|
||||
return statuses[i].Version < statuses[j].Version
|
||||
})
|
||||
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
// migrationDBRecord holds data from the _migrations table.
|
||||
type migrationDBRecord struct {
|
||||
Name string
|
||||
AppliedAt time.Time
|
||||
}
|
||||
|
||||
// loadAppliedMigrations queries _migrations and returns a map of version -> record.
|
||||
// If the table does not exist, returns an empty map without error.
|
||||
func loadAppliedMigrations(db *sql.DB) (map[int]migrationDBRecord, error) {
|
||||
result := make(map[int]migrationDBRecord)
|
||||
|
||||
rows, err := db.Query("SELECT version, name, applied_at FROM _migrations ORDER BY version ASC")
|
||||
if err != nil {
|
||||
// Table doesn't exist yet — return empty map
|
||||
if strings.Contains(err.Error(), "no such table") {
|
||||
return result, nil
|
||||
}
|
||||
return nil, fmt.Errorf("query _migrations: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var version int
|
||||
var name, appliedAtStr string
|
||||
if err := rows.Scan(&version, &name, &appliedAtStr); err != nil {
|
||||
return nil, fmt.Errorf("scan _migrations row: %w", err)
|
||||
}
|
||||
appliedAt, _ := time.Parse("2006-01-02 15:04:05", appliedAtStr)
|
||||
result[version] = migrationDBRecord{Name: name, AppliedAt: appliedAt}
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user