Migraciones
El modulo wippy/migration proporciona un framework de migracion de bases de datos con un pequeno DSL para definir cambios de esquema, un runner que las descubre y ejecuta, y un bootloader que ejecuta las migraciones pendientes para cada target_db registrado en el proyecto.
Las migraciones soportan SQLite, PostgreSQL y MySQL, con implementaciones up/down por driver definidas lado a lado.
Configuracion
Agrega el modulo a tu proyecto:
wippy add wippy/migration
wippy install
Declara la dependencia y la base de datos de la aplicacion a la que se dirigen las migraciones:
version: "1.0"
namespace: app
entries:
- name: app_db
kind: db.sql.sqlite
path: ./data/app.db
- name: dep.migration
kind: ns.dependency
component: wippy/migration
version: "*"
El bootloader de migracion se registra con wippy/bootloader en el orden 20. Cuando la aplicacion inicia, descubre cada entrada de migracion en el registro, las agrupa por meta.target_db y ejecuta las migraciones pendientes contra cada base de datos.
Definir una Migracion
Una migracion es una entrada function.lua con meta.type: migration. La entrada retorna una funcion producida por migration.define(...).
entries:
- name: 01_create_users_table
kind: function.lua
meta:
type: migration
target_db: app:app_db
timestamp: "2025-01-15T10:00:00Z"
source: file://01_create_users_table.lua
imports:
migration: wippy.migration:migration
return require("migration").define(function()
migration("Create users table", function()
database("sqlite", function()
up(function(db)
local ok, err = db:execute([[
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)
]])
if err then error(err) end
end)
down(function(db)
db:execute("DROP TABLE IF EXISTS users")
end)
end)
database("postgres", function()
up(function(db)
db:execute([[
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
)
]])
end)
down(function(db)
db:execute("DROP TABLE IF EXISTS users")
end)
end)
end)
end)
Metadatos Requeridos
| Campo | Requerido | Descripcion |
|---|---|---|
meta.type |
si | Debe ser "migration" para el descubrimiento |
meta.target_db |
si | ID de registro de la base de datos contra la que ejecutar |
meta.timestamp |
no | Marca de tiempo ISO-8601 usada para ordenar cuando multiples migraciones apuntan a la misma base de datos |
meta.tags |
no | Array de etiquetas; el runner puede filtrar migraciones por etiqueta |
Las migraciones para una base de datos se ejecutan en orden ascendente de meta.timestamp.
DSL
Dentro de la funcion pasada a migration.define, hay tres funciones anidadas disponibles:
| Funcion | Descripcion |
|---|---|
migration(description, fn) |
Abrir una nueva migracion con una descripcion legible |
database(type, fn) |
Declarar una implementacion para "sqlite", "postgres" o "mysql" |
up(fn) / down(fn) |
Definir funciones de avance y reversion |
after(fn) |
Hook opcional post-migracion (misma transaccion) |
Cada funcion up/down/after recibe un objeto de transaccion, no una conexion cruda. Las tres operaciones se ejecutan en una sola transaccion que se revierte en caso de error.
Metodos de Transaccion
local rows, err = db:query(sql, params) -- SELECT, retorna array de filas
local result, err = db:execute(sql, params) -- INSERT/UPDATE/DDL, retorna { rows_affected, last_insert_id }
local stmt, err = db:prepare(sql) -- sentencia preparada
Usa siempre consultas parametrizadas:
db:execute("INSERT INTO users (name, email) VALUES (?, ?)", { "Alice", "alice@example.com" })
Manejo de Errores
Llamar a error(...) aborta la migracion y revierte la transaccion. Envuelve cada sentencia que pueda fallar:
up(function(db)
local _, err = db:execute("CREATE TABLE ...")
if err then error(err) end
end)
API del Runner
El runner se expone como una biblioteca para uso programatico:
imports:
runner: wippy.migration:runner
local runner = require("runner").setup("app:app_db")
local result = runner:run() -- aplicar todas las migraciones pendientes
local result = runner:run_next() -- aplicar la siguiente migracion pendiente
local result = runner:rollback({ id = "app:01_create_users_table" })
local status = runner:status() -- listar migraciones aplicadas + pendientes
runner:run(options)
Aplica cada migracion pendiente para la base de datos configurada. Retorna un resumen:
{
status = "complete", -- "complete" o "error"
migrations_found = 3,
migrations_applied = 2,
migrations_skipped = 1,
migrations_failed = 0,
duration = 0.123,
migrations = { ... }, -- estado por migracion
skipped_details = { ... },
}
Opciones:
| Opcion | Descripcion |
|---|---|
tags |
Array de etiquetas; solo se consideran las migraciones cuyos meta.tags se intersecten |
runner:rollback(options)
Revierte una sola migracion por id (requerido):
runner:rollback({ id = "app:01_create_users_table" })
runner:status(options)
Retorna { applied = {...}, pending = {...} }, ordenado por applied_at y meta.timestamp respectivamente.
API de Registry
wippy.migration:registry ofrece consultas directas al registro:
| Funcion | Descripcion |
|---|---|
registry.find({ target_db, tags }) |
Retorna todas las entradas de migracion que coinciden con los criterios |
registry.get(id) |
Retorna una sola entrada de migracion por id |
registry.get_target_dbs() |
Retorna cada meta.target_db unico presente en las migraciones |
registry.get_tags() |
Retorna cada etiqueta unica presente en las migraciones |
El bootloader las usa para descubrir el conjunto completo de bases de datos destino al inicio.
Seguimiento de Migraciones
El runner crea una tabla wippy_migrations en cada base de datos destino en la primera ejecucion. Las migraciones aplicadas se registran por id para que las ejecuciones posteriores las omitan. La tabla de seguimiento se crea automaticamente; no escribas tu propia migracion para crearla.
Buenas Practicas
- Un cambio logico por migracion - crear una tabla, agregar una columna, crear un indice.
- Escribe un
downreal - si la reversion es imposible (perdida de datos), documentalo y lanza un error en lugar de tener exito silenciosamente. - Prefiere la idempotencia -
CREATE TABLE IF NOT EXISTSyDROP TABLE IF EXISTSsobreviven a re-ejecuciones sin manejo especial. - Mantener DDL y DML separados - no siembres datos en la misma migracion que crea una tabla cuando puedas evitarlo.
- Probar ambas direcciones - aplica la migracion, revierte y verifica que el esquema coincida con el estado inicial.
Ver Tambien
- SQL Driver - Configuracion del recurso de base de datos
- Bootloader - Ordenamiento y hooks del bootloader
- Vision General del Framework - Uso de modulos del framework