Files
fn_registry/functions/infra/sqlite_apply_versioned_migrations_test.go
egutierrez 618c07a12c feat(infra): sqlite_apply_versioned_migrations + dedup fn_operations + registry
Promueve patron versionado (schema_migrations + tx por archivo) al registry
como sqlite_apply_versioned_migrations_go_infra. Migra fn_operations/migrate.go
y registry/migrate.go al consumirla. ~200 LOC duplicadas eliminadas.

- functions/infra/sqlite_apply_versioned_migrations.{go,md,_test.go}: nueva,
  5/5 tests pass. Generaliza fs.FS + dir param (fn_operations usaba embed.FS
  hardcoded). Distinta de sqlite_apply_migrations_go_infra (naive split-by-`;`,
  idempotent-by-error) — esta hace tracking explicito + transactions.
- fn_operations/migrate.go: 111 LOC -> 17. Wrapper sobre infra.ApplyVersionedMigrations.
- registry/migrate.go: idem. Mismo patron copy-paste, ahora unificado.

Smoke: ./fn ops init crea operations.db con schema_migrations poblada.
fn_operations + registry tests: PASS. fn index registra nueva fn (1091 total).
2026-05-09 12:50:51 +02:00

159 lines
4.9 KiB
Go

package infra
import (
"database/sql"
"testing"
"testing/fstest"
_ "github.com/mattn/go-sqlite3"
)
func newTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("open db: %v", err)
}
t.Cleanup(func() { db.Close() })
return db
}
func TestApplyVersionedMigrations(t *testing.T) {
t.Run("aplica todas desde cero y registra schema_migrations", func(t *testing.T) {
db := newTestDB(t)
fsys := fstest.MapFS{
"migrations/001_init.sql": {Data: []byte("CREATE TABLE users (id INTEGER PRIMARY KEY);")},
"migrations/002_add_email.sql": {Data: []byte("ALTER TABLE users ADD COLUMN email TEXT;")},
}
if err := ApplyVersionedMigrations(db, fsys, "migrations"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify schema_migrations has 2 rows
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations").Scan(&count); err != nil {
t.Fatalf("count schema_migrations: %v", err)
}
if count != 2 {
t.Errorf("schema_migrations rows: got %d, want 2", count)
}
// Verify MAX(version) == 2
var maxV int
if err := db.QueryRow("SELECT MAX(version) FROM schema_migrations").Scan(&maxV); err != nil {
t.Fatalf("max version: %v", err)
}
if maxV != 2 {
t.Errorf("max version: got %d, want 2", maxV)
}
// Verify the table from migration 001 exists
if _, err := db.Exec("INSERT INTO users (id) VALUES (1)"); err != nil {
t.Errorf("users table not created: %v", err)
}
})
t.Run("idempotente por version, no vuelve a aplicar", func(t *testing.T) {
db := newTestDB(t)
fsys := fstest.MapFS{
"migrations/001_init.sql": {Data: []byte("CREATE TABLE things (id INTEGER PRIMARY KEY);")},
}
// First run
if err := ApplyVersionedMigrations(db, fsys, "migrations"); err != nil {
t.Fatalf("first run: %v", err)
}
// Second run — must not error even though CREATE TABLE would fail normally
if err := ApplyVersionedMigrations(db, fsys, "migrations"); err != nil {
t.Fatalf("second run: %v", err)
}
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations").Scan(&count); err != nil {
t.Fatalf("count: %v", err)
}
if count != 1 {
t.Errorf("expected 1 row in schema_migrations, got %d", count)
}
})
t.Run("migracion intermedia falla, version anterior no avanza", func(t *testing.T) {
db := newTestDB(t)
fsys := fstest.MapFS{
"migrations/001_init.sql": {Data: []byte("CREATE TABLE ok (id INTEGER PRIMARY KEY);")},
"migrations/002_bad.sql": {Data: []byte("THIS IS NOT VALID SQL !!!;")},
"migrations/003_more.sql": {Data: []byte("CREATE TABLE more (id INTEGER PRIMARY KEY);")},
}
err := ApplyVersionedMigrations(db, fsys, "migrations")
if err == nil {
t.Fatal("expected error from bad migration, got nil")
}
// Only version 1 should be recorded
var maxV int
if err2 := db.QueryRow("SELECT COALESCE(MAX(version), 0) FROM schema_migrations").Scan(&maxV); err2 != nil {
t.Fatalf("read version: %v", err2)
}
if maxV != 1 {
t.Errorf("max version after failure: got %d, want 1", maxV)
}
// Migration 003 table must NOT exist
if _, err2 := db.Exec("INSERT INTO more (id) VALUES (1)"); err2 == nil {
t.Error("table 'more' should not exist after failed migration 002")
}
})
t.Run("archivos sin prefijo numerico se ignoran", func(t *testing.T) {
db := newTestDB(t)
fsys := fstest.MapFS{
"migrations/README.sql": {Data: []byte("-- this should be ignored")},
"migrations/init.sql": {Data: []byte("CREATE TABLE bad (id INTEGER PRIMARY KEY);")},
"migrations/001_good.sql": {Data: []byte("CREATE TABLE good (id INTEGER PRIMARY KEY);")},
}
if err := ApplyVersionedMigrations(db, fsys, "migrations"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations").Scan(&count); err != nil {
t.Fatalf("count: %v", err)
}
if count != 1 {
t.Errorf("expected 1 migration applied, got %d", count)
}
// Only 'good' table should exist
if _, err := db.Exec("INSERT INTO good (id) VALUES (1)"); err != nil {
t.Errorf("table 'good' should exist: %v", err)
}
if _, err := db.Exec("INSERT INTO bad (id) VALUES (1)"); err == nil {
t.Error("table 'bad' should NOT exist")
}
})
t.Run("dir vacio no error y no crea schema_migrations", func(t *testing.T) {
db := newTestDB(t)
fsys := fstest.MapFS{
"migrations/.keep": {Data: []byte("")},
}
if err := ApplyVersionedMigrations(db, fsys, "migrations"); err != nil {
t.Fatalf("unexpected error on empty dir: %v", err)
}
// schema_migrations IS created (the CREATE TABLE IF NOT EXISTS runs regardless)
// but it must be empty
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM schema_migrations").Scan(&count); err != nil {
t.Fatalf("count schema_migrations: %v", err)
}
if count != 0 {
t.Errorf("expected 0 rows in schema_migrations, got %d", count)
}
})
}