Migrações
O módulo wippy/migration fornece um framework de migrações de banco de dados com uma pequena DSL para definir alterações de schema, um executor que descobre e executa as migrações, e um bootloader que roda migrações pendentes para cada target_db registrado no projeto.
As migrações suportam SQLite, PostgreSQL e MySQL, com implementações up/down por driver definidas lado a lado.
Configuração
Adicione o módulo ao seu projeto:
wippy add wippy/migration
wippy install
Declare a dependência e o banco de dados da aplicação que as migrações devem alvejar:
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: "*"
O bootloader de migrações se registra em wippy/bootloader na ordem 20. Quando a aplicação inicia, ele descobre cada entrada de migração no registro, agrupa-as por meta.target_db e executa as migrações pendentes em cada banco de dados.
Definindo uma Migração
Uma migração é uma entrada function.lua com meta.type: migration. A entrada retorna uma função produzida 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)
Metadados Obrigatórios
| Campo | Obrigatório | Descrição |
|---|---|---|
meta.type |
sim | Deve ser "migration" para descoberta |
meta.target_db |
sim | ID no registro do banco de dados a ser executado |
meta.timestamp |
não | Timestamp ISO-8601 usado para ordenação quando várias migrações alvejam o mesmo banco |
meta.tags |
não | Array de tags; o executor pode filtrar migrações por tag |
As migrações de um banco rodam em ordem crescente de meta.timestamp. meta.timestamp é opcional; o id completo da entrada é o critério de desempate, então migrações com timestamps iguais ou ausentes ainda rodam em uma ordem estável e determinística.
DSL
Dentro da função passada para migration.define, as seguintes funções aninhadas estão disponíveis:
| Função | Descrição |
|---|---|
migration(description, fn) |
Abre uma nova migração com uma descrição legível |
database(type, fn) |
Declara uma implementação para "sqlite", "postgres" ou "mysql" |
up(fn) / down(fn) |
Define funções de avanço e reversão |
after(fn) |
Hook opcional pós-migração (mesma transação) |
Cada função up/down/after recebe um objeto de transação, não uma conexão bruta. Todas as três operações rodam em uma única transação que faz rollback em caso de erro.
Métodos da Transação
local rows, err = db:query(sql, params) -- SELECT, retorna array de linhas
local result, err = db:execute(sql, params) -- INSERT/UPDATE/DDL, retorna { rows_affected, last_insert_id }
local stmt, err = db:prepare(sql) -- prepared statement
Sempre use consultas parametrizadas:
db:execute("INSERT INTO users (name, email) VALUES (?, ?)", { "Alice", "alice@example.com" })
Tratamento de Erros
Chamar error(...) aborta a migração e faz rollback da transação. Envolva toda instrução que possa falhar:
up(function(db)
local _, err = db:execute("CREATE TABLE ...")
if err then error(err) end
end)
API do Executor
O executor é exposto como biblioteca para uso programático:
imports:
runner: wippy.migration:runner
local runner = require("runner").setup("app:app_db")
local result = runner:run() -- aplica todas as migrações pendentes
local result = runner:run_next() -- aplica a próxima migração pendente
local result = runner:rollback() -- reverte a migração aplicada mais recentemente
local status = runner:status() -- lista migrações aplicadas + pendentes
runner:run(options)
Aplica toda migração pendente para o banco de dados configurado. Retorna um resumo:
{
status = "complete", -- "complete" ou "error"
migrations_found = 3,
migrations_applied = 2,
migrations_skipped = 1,
migrations_failed = 0,
duration = 0.123,
migrations = { ... }, -- status por migração
skipped_details = { ... },
}
Opções:
| Opção | Descrição |
|---|---|
tags |
Array de tags; apenas migrações cujo meta.tags possui interseção são consideradas |
runner:rollback(options)
Reverte migrações aplicadas na ordem inversa da aplicação. Sem opções, reverte apenas a migração aplicada mais recentemente:
runner:rollback() -- reverte a última migração
runner:rollback({ count = 3 }) -- reverte as últimas 3
runner:rollback({ allowed_ids = { "app:01_create_users_table" } }) -- restringe a ids específicos
Opções:
| Opção | Descrição |
|---|---|
count |
Número de migrações a reverter; padrão 1 |
allowed_ids |
Array de ids de migração; apenas estes são elegíveis para rollback |
runner:status(options)
Retorna um relatório de status descrevendo cada migração do banco de dados:
{
database_id = "app:app_db",
db_type = "sqlite",
total_migrations = 3,
applied_migrations = 2,
pending_migrations = 1,
migrations = {
{ id = "app:01_...", description = "...", timestamp = "...",
tags = {}, status = "applied", applied_at = ... },
-- ...
},
}
As migrações aplicadas são listadas primeiro (ordenadas por applied_at), seguidas pelas pendentes (ordenadas por meta.timestamp e depois por id).
API do Registro
wippy.migration:registry oferece consultas diretas ao registro:
| Função | Descrição |
|---|---|
registry.find({ target_db, tags }) |
Retorna todas as entradas de migração que atendem aos critérios |
registry.get(id) |
Retorna uma única entrada de migração pelo id |
registry.get_target_dbs() |
Retorna cada meta.target_db único presente nas migrações |
registry.get_tags() |
Retorna cada tag única presente nas migrações |
O bootloader usa essas funções para descobrir o conjunto completo de bancos alvo na inicialização.
Rastreamento de Migrações
O executor cria uma tabela _migrations em cada banco alvo na primeira execução. Migrações aplicadas são registradas por id, para que execuções subsequentes as pulem. A tabela de rastreamento é criada automaticamente; não escreva sua própria migração para criá-la.
Boas Práticas
- Uma mudança lógica por migração - crie uma tabela, adicione uma coluna, crie um índice.
- Escreva um
downde verdade - se o rollback for impossível (perda de dados), documente isso e lance um erro em vez de ter sucesso silenciosamente. - Prefira idempotência -
CREATE TABLE IF NOT EXISTSeDROP TABLE IF EXISTSsobrevivem a reexecuções sem tratamento especial. - Mantenha DDL e DML separados - evite popular dados na mesma migração que cria uma tabela, quando possível.
- Teste as duas direções - aplique a migração, reverta, e verifique que o schema corresponde ao estado inicial.
Veja Também
- Driver SQL - Configuração de recurso de banco de dados
- Bootloader - Ordenação e hooks do bootloader
- Visão Geral do Framework - Uso dos módulos do framework