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).
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: sqlite_apply_versioned_migrations
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func ApplyVersionedMigrations(conn *sql.DB, fsys fs.FS, dir string) error"
|
||||
description: "Aplica migraciones SQLite pendientes desde un fs.FS con tracking explicito de versiones en schema_migrations. Cada migracion corre en su propia transaccion; si falla se hace rollback y se retorna el error sin avanzar la version."
|
||||
tags: [sqlite, migrations, schema, versioned, transactional, embed, infra]
|
||||
uses_functions: []
|
||||
uses_types: [error_go_core]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports:
|
||||
- "database/sql"
|
||||
- "io/fs"
|
||||
- "fmt"
|
||||
- "path"
|
||||
- "sort"
|
||||
- "strconv"
|
||||
- "strings"
|
||||
- "time"
|
||||
tested: true
|
||||
tests:
|
||||
- "aplica todas desde cero y registra schema_migrations"
|
||||
- "idempotente por version, no vuelve a aplicar"
|
||||
- "migracion intermedia falla, version anterior no avanza"
|
||||
- "archivos sin prefijo numerico se ignoran"
|
||||
- "dir vacio no error y no crea schema_migrations"
|
||||
test_file_path: "functions/infra/sqlite_apply_versioned_migrations_test.go"
|
||||
file_path: "functions/infra/sqlite_apply_versioned_migrations.go"
|
||||
params:
|
||||
- name: conn
|
||||
desc: "Conexion SQLite abierta. Debe apuntar a la base de datos donde se gestionaran las migraciones."
|
||||
- name: fsys
|
||||
desc: "Sistema de archivos (embed.FS, os.DirFS, fstest.MapFS, etc.) que contiene el directorio de migraciones."
|
||||
- name: dir
|
||||
desc: "Ruta del directorio dentro de fsys que contiene los archivos .sql (ej. 'migrations')."
|
||||
output: "nil si todas las migraciones pendientes se aplicaron correctamente; error descriptivo con el nombre del archivo que fallo en caso contrario."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
func openDB(path string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", path+"?_foreign_keys=on&_journal_mode=WAL")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := infra.ApplyVersionedMigrations(db, migrationsFS, "migrations"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("migrations: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Diferencias vs sqlite_apply_migrations_go_infra (naive)
|
||||
|
||||
| Aspecto | `sqlite_apply_versioned_migrations` (esta) | `sqlite_apply_migrations` (naive) |
|
||||
|---|---|---|
|
||||
| Tracking | Tabla `schema_migrations` — sabe exactamente que versiones estan aplicadas | Sin tabla de tracking — reaplica todo cada vez |
|
||||
| Idempotencia | Por numero de version (`version <= current` se salta) | Por error — ignora "duplicate column / already exists" |
|
||||
| Transacciones | Una transaccion por archivo — rollback limpio si falla | Sin transacciones — sentencias sueltas |
|
||||
| Parsing SQL | Confia en SQLite multi-statement (`tx.Exec` del contenido completo) | Split manual por `;` (fragil con strings) |
|
||||
| Uso ideal | Apps con `operations.db` propias, BDs con datos vivos, deploy multi-PC | Bootstrap rapido, scripts de seed, migraciones sin estado persistente |
|
||||
|
||||
**Regla practica:** usa `sqlite_apply_versioned_migrations` cuando necesites saber que se aplico, cuando, y garantizar que un fallo no deja la BD a medio migrar. Usa `sqlite_apply_migrations` para scripts de seed o inicializacion que no importa repetir.
|
||||
|
||||
## Notas
|
||||
|
||||
- La funcion esta adaptada directamente de `fn_operations/migrate.go` — el patron probado en produccion del registry.
|
||||
- `schema_migrations` guarda `version` (INTEGER PK), `name` (filename), `applied_at` (RFC3339 UTC).
|
||||
- El SQL de cada archivo se ejecuta con una sola llamada `tx.Exec(content)` sin split por `;`. Esto funciona correctamente con el driver `go-sqlite3` (CGO) que soporta multi-statement. No usar con drivers pure-Go que no soporten multi-statement.
|
||||
- Archivos sin prefijo numerico parseable o sin extension `.sql` se ignoran silenciosamente.
|
||||
- Compatible con `embed.FS`, `os.DirFS`, y `fstest.MapFS` (util en tests).
|
||||
Reference in New Issue
Block a user