Pular para o conteúdo

Migrações

Com histórico no banco, não num arquivo — e nunca apagando dado sozinho.

A forma simples: a diferença#

O modelo descreve como a tabela deveria ser. diferenca() compara com como ela é:

dataforge
adopt Forge

db := Forge.conectar(":memory:")
U := Forge.modelo("Usuario", {
    "id": {"tipo": "Serial"},
    "nome": {"tipo": "Texto", "obrigatorio": yes}
}, {"conexao": db})
Forge.migrar_tudo(db)

// agora o modelo cresce
U2 := Forge.modelo("Usuario", {
    "id": {"tipo": "Serial"},
    "nome": {"tipo": "Texto", "obrigatorio": yes},
    "telefone": {"tipo": "Texto"}
}, {"conexao": db})

d := U2.diferenca()
assert d["colunas_faltando"] is ["telefone"]

U2.aplicar_diferenca()
assert U2.diferenca()["colunas_faltando"] is []

Comparar o modelo com a tabela de verdade é melhor que confiar num histórico de arquivos, que diverge assim que alguém mexe no banco à mão.

A forma completa: passos numerados#

Para o que a diferença não cobre — renomear uma coluna, migrar dados, criar um índice composto:

dataforge
adopt Forge

db := Forge.conectar(":memory:")
m := Forge.migracoes(db)

m.passo("001_cria_usuarios",
    lambda c => c.executar(
        "create table usuarios (id integer primary key, nome text)"),
    lambda c => c.executar("drop table usuarios"))

m.passo("002_acrescenta_email",
    lambda c => c.executar("alter table usuarios add column email text"),
    lambda c => c.executar("alter table usuarios drop column email"))

assert m.subir() is ["001_cria_usuarios", "002_acrescenta_email"]
assert m.pendentes() is []
assert m.subir() is []              // rodar de novo não repete

O histórico mora no banco#

Numa tabela _forge_migracoes. Guardá-lo ali, e não num arquivo, é o que faz duas máquinas concordarem sobre o estado: um arquivo versionado diz o que deveria ter rodado; a tabela diz o que rodou.

dataforge
adopt Forge

db := Forge.conectar(":memory:")
m := Forge.migracoes(db)
m.passo("001", lambda c => c.executar("create table a (id integer)"),
        lambda c => c.executar("drop table a"))
m.subir()

e := m.estado()
assert e["aplicadas"] is 1
assert e["pendentes"] is 0
assert e["passos"][0]["reversivel"] is yes

Falha para tudo#

Continuar depois de uma migração que falhou deixa o banco num estado que nenhuma migração previu:

dataforge
adopt Forge

db := Forge.conectar(":memory:")
m := Forge.migracoes(db)
m.passo("001", lambda c => c.executar("create table a (id integer)"))
m.passo("002", lambda c => c.executar("isto não é sql"))
m.passo("003", lambda c => c.executar("create table c (id integer)"))

monitor:
    m.subir()
handle MigrationError as e:
    assert "002" in e.message

// a 003 não rodou
assert not ("c" in Forge.tabelas(db))

Desfazer#

dataforge
adopt Forge

db := Forge.conectar(":memory:")
m := Forge.migracoes(db)
m.passo("001", lambda c => c.executar("create table a (id integer)"),
        lambda c => c.executar("drop table a"))
m.subir()

assert m.descer(1) is ["001"]
assert not ("a" in Forge.tabelas(db))

Só desce o que declarou como desfazer. Uma migração sem caminho de volta avisa em vez de tentar adivinhar.