Schemas mudam. Planeje pensando nisso.
A primeira versão do seu schema nunca é a última. Colunas vão sendo adicionadas, tabelas acabam sendo divididas, índices precisam ser repensados. A questão não é se o seu schema vai mudar — é se essa mudança vai chegar de forma limpa em todo notebook, servidor e dispositivo de usuário que já tem uma cópia mais antiga do banco.
É exatamente para isso que servem as migrations: uma sequência de scripts pequenos e ordenados que levam o banco da versão N para a N+1. Rode na ordem certa e qualquer banco se atualiza até a versão atual. Se você abrir mão dessa disciplina, vai cair naquele clássico "na minha máquina funciona" e perder a tarde inteira investigando o problema.
Para fazer esse versionamento de schema no SQLite, você tem uma única ferramenta nativa: PRAGMA user_version. É um inteiro de 32 bits que o banco guarda para você, e que o próprio SQLite nunca toca. O significado dele é você quem define.
Um banco recém-criado começa em 0. Defina esse valor para o número da última migration que você aplicou. Na inicialização, basta ler esse número para saber em que ponto o schema está.
Um loop mínimo de migrations
O modelo mental é simples: cada migration é um script SQL numerado. Sua aplicação lê o user_version atual, executa em ordem todos os scripts com número maior e atualiza o user_version ao final de cada um.
Veja a migration 1, que cria o schema inicial:
Repare em duas coisas. Tudo está envolvido por BEGIN; ... COMMIT;, ou seja, é atômico — se o CREATE TABLE falhar, o user_version não avança e você pode corrigir e rodar de novo. E o PRAGMA user_version = 1 é a última instrução antes do commit, então a versão só muda se todo o resto deu certo.
Agora imagine que você precisa adicionar uma coluna created_at. Essa é a migration 2:
Um banco na versão 0 roda os dois. Um banco na versão 1 roda só o segundo. Um banco na versão 2 não roda nada. A ordem é o contrato.
O que dá e o que não dá fazer com ALTER TABLE
O ALTER TABLE do SQLite é propositalmente limitado. Ele aceita apenas:
ADD COLUMN— adiciona uma nova coluna, com valor padrão opcional.DROP COLUMN— remove uma coluna (a partir da versão 3.35).RENAME COLUMN— renomeia uma coluna (a partir da 3.25).RENAME TO— renomeia a própria tabela.
E é só isso. Não dá pra mudar o tipo de uma coluna, alterar um NOT NULL, mexer numa restrição CHECK nem adicionar uma FOREIGN KEY numa coluna que já existe.
-- Não suportado:
ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(255);
ALTER TABLE users ADD CONSTRAINT users_email_check CHECK (email LIKE '%@%');
Quando você precisa fazer uma alteração que o SQLite não suporta diretamente, a receita oficial é "reconstruir a tabela". É mais verboso, mas funciona sempre.
Reconstruindo a tabela para mudanças maiores
O padrão é o seguinte: crie uma nova tabela com o formato desejado, copie os dados, descarte a antiga e renomeie a nova no lugar dela. Tudo dentro de uma transação.
A documentação oficial do SQLite chama isso de 12-step recipe (receita de 12 passos) e ainda traz alertas extras sobre triggers, views e referências de chave estrangeira — vale a pena ler com calma antes de aplicar num schema em produção. Para a maioria dos casos, a versão de quatro passos acima já dá conta.
Um aviso importante: se existirem chaves estrangeiras apontando para a tabela que você está reconstruindo, execute PRAGMA foreign_keys = OFF antes da migration e PRAGMA foreign_keys = ON depois. Caso contrário, o DROP TABLE pode quebrar a integridade referencial no meio do caminho.
Rodando migrations sqlite a partir da sua aplicação
O controle de versão é simples o bastante para você escrever na mão. Em Python, usando só a biblioteca padrão:
Os princípios fundamentais:
- As migrations são numeradas em sequência, começando do 1. Sem buracos, sem reordenação.
- Cada migration roda dentro de uma transação, junto com o
PRAGMA user_version = Nque incrementa a versão. - Depois que uma migration foi commitada e entrou em produção, ela é intocável. Mudança nova vira migration nova.
Essa última regra é justamente a que mais cai por terra nos times. Se você editar a migration 3 depois que o banco de um colega já rodou ela, o banco dele fica silenciosamente fora de sincronia com o seu — e para sempre.
Mantendo um histórico de auditoria
O PRAGMA user_version te diz em que ponto o banco está, mas não te conta quando cada passo rodou nem o que ele fez. Uma tabelinha simples de controle resolve isso:
Agora você tem uma linha por migration, com nome e timestamp — útil pra hora de debugar aquele clássico "por que esse banco tem uma coluna que o código não espera?"
O PRAGMA user_version continua sendo a fonte da verdade pro loop; a tabela existe pros humanos.
Rollback de migration no SQLite: o que a transação resolve (e o que não resolve)
O DDL do SQLite é transacional. Se a migration 5 começa criando uma tabela, copiando dados e atualizando o user_version, e a cópia falha no meio do caminho, o ROLLBACK desfaz tudo — inclusive o CREATE TABLE. O banco volta exatamente ao estado anterior ao BEGIN.
Isso cobre as migrations que falharam. Mas não resolve o caso das migrations que foram comitadas com sucesso e das quais você se arrependeu depois. Para essas, você escreve uma down-migration separada — um script que desfaz a mudança. O SQLite não tem reversão automática. Se a migration 7 adicionou uma coluna, a versão "down" dropa essa coluna. Se a migration 7 dropou uma coluna, a versão "down" não consegue recuperar os dados; o máximo que dá pra fazer é recriar a coluna vazia.
Na prática, muitos projetos pequenos ignoram down-migrations e dependem de backups como mecanismo de "desfazer". É uma escolha válida — desde que você realmente faça os backups.
Alguns hábitos que evitam dor de cabeça depois
- Uma migration por mudança lógica. Uma migration que adiciona três colunas sem relação entre si é mais difícil de revisar e de reverter do que três migrations separadas.
- Teste migrations em uma cópia de produção. Mudanças de schema podem ser lentas em tabelas grandes; descobrir isso direto em produção não é nada legal.
- Nunca edite uma migration que já foi publicada. Crie uma nova.
- Faça backup antes. Um
.backuprápido no CLI ou uma cópia do arquivo com o banco fechado é um seguro barato antes de qualquer migration não-trivial. - Cuidado com o
PRAGMA foreign_keys. Desligue durante reconstruções de tabela e religue depois.
Em projetos maiores, vale usar uma ferramenta dedicada — Alembic com SQLAlchemy, golang-migrate, Knex, Flyway. Elas cuidam da ordenação, de execuções concorrentes e de convenções de time que, do contrário, você reinventaria do zero. Os princípios são os mesmos do loop que mostramos acima; a ferramenta só elimina o boilerplate.
A seguir: modo WAL e concorrência
Migrations normalmente rodam com a aplicação offline ou segurando um lock exclusivo. No resto do tempo, seu banco está atendendo leituras e escritas de várias conexões ao mesmo tempo — e o journal mode padrão do SQLite nem sempre é a melhor opção. A próxima página fala sobre o modo WAL, o que ele muda e quando vale a pena migrar para ele.
Perguntas frequentes
Como versionar o schema de um banco SQLite?
O SQLite já oferece um inteiro de 32 bits por banco chamado user_version, acessado via PRAGMA user_version. Na inicialização da aplicação, leia esse valor, compare com o número da última migration que o seu código conhece e rode em ordem o que estiver faltando. Não precisa de tabela extra — embora muita gente crie uma só para ter histórico de auditoria.
Dá para fazer rollback de uma migration no SQLite?
Envolva cada migration em BEGIN; ... COMMIT;. Se algo falhar no meio, o ROLLBACK desfaz tudo — tanto mudanças de schema quanto de dados, já que o DDL do SQLite é transacional. Agora, para reverter uma migration que já foi commitada, você precisa de um script de "down" escrito por você mesmo: o SQLite não gera isso automaticamente.
Por que o ALTER TABLE do SQLite é tão limitado?
O SQLite suporta ALTER TABLE ADD COLUMN, RENAME TABLE, RENAME COLUMN e DROP COLUMN, mas não permite mudanças arbitrárias como alterar o tipo ou as constraints de uma coluna. A saída é a famosa receita de 12 passos: criar uma tabela nova com o formato desejado, fazer INSERT INTO new_table SELECT ... FROM old_table, dropar a antiga e renomear a nova.
Vale a pena usar uma ferramenta de migration ou escrever a minha?
Para projetos pequenos, um loop caseiro lendo arquivos .sql numerados, controlado por PRAGMA user_version, dá uns 30 linhas de código e resolve numa boa. Já em projetos maiores, ferramentas como Alembic (Python), golang-migrate (Go) ou Knex (Node) cuidam de ordenação, locks e fluxo em equipe — coisas que você acabaria reinventando do zero.