Pular para o conteúdo

Migracoes

O modulo wippy/migration fornece um framework de migracoes de banco de dados com uma pequena DSL para definir alteracoes de schema, um executor que descobre e executa as migracoes, e um bootloader que roda migracoes pendentes para cada target_db registrado no projeto.

As migracoes suportam SQLite, PostgreSQL e MySQL, com implementacoes up/down por driver definidas lado a lado.

Adicione o modulo ao seu projeto:

Terminal window
wippy add wippy/migration
wippy install

Declare a dependencia e o banco de dados da aplicacao que as migracoes 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 migracoes se registra em wippy/bootloader na ordem 20. Quando a aplicacao inicia, ele descobre cada entrada de migracao no registro, agrupa-as por meta.target_db e executa as migracoes pendentes em cada banco de dados.

Uma migracao e uma entrada function.lua com meta.type: migration. A entrada retorna uma funcao 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)
CampoObrigatorioDescricao
meta.typesimDeve ser "migration" para descoberta
meta.target_dbsimID no registro do banco de dados a ser executado
meta.timestampnaoTimestamp ISO-8601 usado para ordenacao quando varias migracoes alvejam o mesmo banco
meta.tagsnaoArray de tags; o executor pode filtrar migracoes por tag

As migracoes de um banco rodam em ordem crescente de meta.timestamp.

Dentro da funcao passada para migration.define, tres funcoes aninhadas estao disponiveis:

FuncaoDescricao
migration(description, fn)Abre uma nova migracao com uma descricao legivel
database(type, fn)Declara uma implementacao para "sqlite", "postgres" ou "mysql"
up(fn) / down(fn)Define funcoes de avanco e reversao
after(fn)Hook opcional pos-migracao (mesma transacao)

Cada funcao up/down/after recebe um objeto de transacao, nao uma conexao bruta. Todas as tres operacoes rodam em uma unica transacao que faz rollback em caso de erro.

local rows, err = db:query(sql, params) -- SELECT, returns array of rows
local result, err = db:execute(sql, params) -- INSERT/UPDATE/DDL, returns { 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" })

Chamar error(...) aborta a migracao e faz rollback da transacao. Envolva toda instrucao que possa falhar:

up(function(db)
local _, err = db:execute("CREATE TABLE ...")
if err then error(err) end
end)

O executor e exposto como biblioteca para uso programatico:

imports:
runner: wippy.migration:runner
local runner = require("runner").setup("app:app_db")
local result = runner:run() -- apply all pending migrations
local result = runner:run_next() -- apply the next pending migration
local result = runner:rollback({ id = "app:01_create_users_table" })
local status = runner:status() -- list applied + pending migrations

Aplica toda migracao pendente para o banco de dados configurado. Retorna um resumo:

{
status = "complete", -- "complete" or "error"
migrations_found = 3,
migrations_applied = 2,
migrations_skipped = 1,
migrations_failed = 0,
duration = 0.123,
migrations = { ... }, -- per-migration status
skipped_details = { ... },
}

Opcoes:

OpcaoDescricao
tagsArray de tags; apenas migracoes cujo meta.tags possui intersecao sao consideradas

Reverte uma unica migracao pelo id (obrigatorio):

runner:rollback({ id = "app:01_create_users_table" })

Retorna { applied = {...}, pending = {...} }, ordenados por applied_at e meta.timestamp respectivamente.

wippy.migration:registry oferece consultas diretas ao registro:

FuncaoDescricao
registry.find({ target_db, tags })Retorna todas as entradas de migracao que atendem aos criterios
registry.get(id)Retorna uma unica entrada de migracao pelo id
registry.get_target_dbs()Retorna cada meta.target_db unico presente nas migracoes
registry.get_tags()Retorna cada tag unica presente nas migracoes

O bootloader usa essas funcoes para descobrir o conjunto completo de bancos alvo na inicializacao.

O executor cria uma tabela wippy_migrations em cada banco alvo na primeira execucao. Migracoes aplicadas sao registradas por id, para que execucoes subsequentes as pulem. A tabela de rastreamento e criada automaticamente; nao escreva sua propria migracao para cria-la.

  • Uma mudanca logica por migracao - crie uma tabela, adicione uma coluna, crie um indice.
  • Escreva um down de verdade - se o rollback for impossivel (perda de dados), documente isso e lance um erro em vez de ter sucesso silenciosamente.
  • Prefira idempotencia - CREATE TABLE IF NOT EXISTS e DROP TABLE IF EXISTS sobrevivem a reexecucoes sem tratamento especial.
  • Mantenha DDL e DML separados - evite popular dados na mesma migracao que cria uma tabela, quando possivel.
  • Teste as duas direcoes - aplique a migracao, reverta, e verifique que o schema corresponde ao estado inicial.