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,122 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func openTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("cannot open test DB: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { db.Close() })
|
||||
return db
|
||||
}
|
||||
|
||||
func writeMigrationFile(t *testing.T, dir, filename, content string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("cannot write migration file %s: %v", filename, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrationUp(t *testing.T) {
|
||||
t.Run("base de datos vacia aplica todas las migraciones", func(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
dir := t.TempDir()
|
||||
|
||||
writeMigrationFile(t, dir, "001_create_users.sql",
|
||||
"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY, name TEXT NOT NULL);\n-- +down\nDROP TABLE IF EXISTS users;\n")
|
||||
writeMigrationFile(t, dir, "002_create_roles.sql",
|
||||
"-- +up\nCREATE TABLE roles (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS roles;\n")
|
||||
|
||||
applied, err := MigrationUp(db, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(applied) != 2 {
|
||||
t.Errorf("applied count: got %d, want 2", len(applied))
|
||||
}
|
||||
if applied[0].Version != 1 || applied[1].Version != 2 {
|
||||
t.Errorf("applied versions: got %v", []int{applied[0].Version, applied[1].Version})
|
||||
}
|
||||
|
||||
// Verify tables were created
|
||||
var count int
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err != nil {
|
||||
t.Errorf("users table not created: %v", err)
|
||||
}
|
||||
if err := db.QueryRow("SELECT COUNT(*) FROM roles").Scan(&count); err != nil {
|
||||
t.Errorf("roles table not created: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("migraciones ya aplicadas se omiten", func(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
dir := t.TempDir()
|
||||
|
||||
writeMigrationFile(t, dir, "001_create_users.sql",
|
||||
"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")
|
||||
writeMigrationFile(t, dir, "002_create_roles.sql",
|
||||
"-- +up\nCREATE TABLE roles (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS roles;\n")
|
||||
|
||||
// Apply all
|
||||
_, err := MigrationUp(db, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("first up failed: %v", err)
|
||||
}
|
||||
|
||||
// Apply again — should apply nothing
|
||||
applied, err := MigrationUp(db, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("second up failed: %v", err)
|
||||
}
|
||||
if len(applied) != 0 {
|
||||
t.Errorf("expected 0 applied on second run, got %d", len(applied))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("migracion con SQL invalido retorna error y deja las anteriores aplicadas", func(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
dir := t.TempDir()
|
||||
|
||||
writeMigrationFile(t, dir, "001_create_users.sql",
|
||||
"-- +up\nCREATE TABLE users (id TEXT PRIMARY KEY);\n-- +down\nDROP TABLE IF EXISTS users;\n")
|
||||
writeMigrationFile(t, dir, "002_bad_sql.sql",
|
||||
"-- +up\nTHIS IS NOT VALID SQL!!!;\n-- +down\n\n")
|
||||
|
||||
applied, err := MigrationUp(db, dir)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid SQL, got nil")
|
||||
}
|
||||
// Version 1 should be applied
|
||||
if len(applied) != 1 || applied[0].Version != 1 {
|
||||
t.Errorf("expected [1] applied before failure, got versions: %v", applied)
|
||||
}
|
||||
|
||||
// users table should still exist (migration 1 committed)
|
||||
var count int
|
||||
if err2 := db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count); err2 != nil {
|
||||
t.Errorf("users table not present after partial apply: %v", err2)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("directorio sin archivos sql no aplica nada", func(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
dir := t.TempDir()
|
||||
|
||||
applied, err := MigrationUp(db, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(applied) != 0 {
|
||||
t.Errorf("expected 0 applied for empty dir, got %d", len(applied))
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user