Migrações
Mudar o schema de um banco que tem dado dentro — com ida, volta e histórico.
Num sistema em produção o banco tem dado dentro. Trocar o create_table no código não muda a tabela que já existe, e apagar e recriar perde tudo.
A lista, com ida e volta#
steady MIGRACOES := [
{
"version": 1,
"description": "clientes",
"up": """
CREATE TABLE clientes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL
);
""",
"down": "DROP TABLE clientes;"
},
{
"version": 2,
"description": "e-mail do cliente",
"up": "ALTER TABLE clientes ADD COLUMN email TEXT;",
"down": "ALTER TABLE clientes DROP COLUMN email;"
}
]
Banco.migrate(db, MIGRACOES) // aplica o que falta; devolve quantas| Chamada | Faz |
|---|---|
Banco.migrate(db, lista) | aplica as que faltam, em ordem de versão |
Banco.migrations_applied(db) | versão, descrição e quando |
Banco.rollback_migration(db, lista) | desfaz uma |
Banco.rollback_migration(db, lista, ate := 3) | desfaz até a 3, que fica |
Banco.schema_sql(db) | o schema como o SQLite o guarda |
É idempotente, e isso é o ponto#
migrate grava numa tabela _migrations o que já aplicou, e pula essas. Rodar de novo devolve 0. É o que permite chamá-lo no começo de todo processo:
db := Banco.connect("dados.db")
Banco.migrate(db, MIGRACOES)
V.subir(porta := 8501)Sem isso, subir o servidor duas vezes quebraria na segunda — e alguém acabaria escrevendo um script de migração que se roda à mão, que é o mesmo problema com mais passos.
Toda migração deveria ter `down`#
Desfazer acontece no meio de um incidente — que é quando ninguém tem paciência para editar o banco à mão. Duas escolhas deliberadas:
O dado sobrevive#
Banco.insert(db, "clientes", {"nome": "Ana", "email": "ana@exemplo.br"})
steady MAIS := [...MIGRACOES, {
"version": 3, "description": "telefone",
"up": "ALTER TABLE clientes ADD COLUMN telefone TEXT DEFAULT '';",
"down": "ALTER TABLE clientes DROP COLUMN telefone;"
}]
Banco.migrate(db, MAIS) // uma aplicada
Banco.count(db, "clientes") // o dado continua láAs migrações que exigem cuidado#
| Mudança | Como fazer |
|---|---|
| acrescentar coluna | ALTER TABLE … ADD COLUMN — barato, e com DEFAULT não trava |
| renomear coluna | ALTER TABLE … RENAME COLUMN (SQLite 3.25+) |
| mudar tipo de coluna | tabela nova, INSERT … SELECT, DROP, RENAME — o SQLite não altera tipo |
acrescentar NOT NULL sem DEFAULT | impossível com dado existente: preencha antes, em duas migrações |
| acrescentar índice numa tabela grande | trava a escrita enquanto constrói; faça na janela de manutenção |
| apagar coluna | DROP COLUMN (3.35+), e é irreversível — o down não recupera o dado |
Antes de migrar em produção#
Banco.backup(db, $"antes-da-v{proxima}.db")
Banco.migrate(db, MIGRACOES)
Banco.integrity(db)backup faz uma cópia consistente com o banco em uso — é a API de backup do SQLite, não um cp. Um cp de um banco sendo escrito copia um arquivo pela metade.
E o SQLite não desfaz DDL dentro de transação de forma confiável em todas as versões: o backup é a rede de segurança real, não o rollback.
Comparar dois ambientes#
// em produção
IO.write("schema-prod.sql", Banco.schema_sql(prod))
// no seu
IO.write("schema-dev.sql", Banco.schema_sql(dev))Um diff entre os dois mostra o que falta migrar. É o jeito mais direto de descobrir que alguém alterou o banco à mão.