Para trabalhar com migrations SQLx em Rust, instale o sqlx-cli, mantenha os arquivos SQL versionados no repositório e execute sqlx migrate run contra um banco controlado antes do deploy em produção. O fluxo mínimo é simples, mas uma estratégia segura também precisa considerar rollback, locks, compatibilidade entre versões da aplicação e testes em banco vazio.
cargo install sqlx-cli --no-default-features --features rustls,postgres
sqlx migrate add -r criar_usuarios
sqlx migrate run
sqlx migrate info
O SQLx registra quais migrations já foram aplicadas e executa somente as pendentes. Isso evita scripts manuais diferentes por ambiente e aproxima a evolução do schema do mesmo processo de revisão usado para o código Rust. Para APIs com Axum, SQLx e PostgreSQL, essa disciplina é parte central de um deploy previsível.
Resposta rápida: comandos essenciais do SQLx CLI
| Objetivo | Comando |
|---|---|
| Instalar a CLI para PostgreSQL | cargo install sqlx-cli --no-default-features --features rustls,postgres |
| Criar uma migration simples | sqlx migrate add criar_usuarios |
| Criar migration reversível | sqlx migrate add -r criar_usuarios |
| Aplicar pendências | sqlx migrate run |
| Ver o estado das migrations | sqlx migrate info |
| Reverter a última migration reversível | sqlx migrate revert |
| Criar o banco configurado | sqlx database create |
| Apagar o banco configurado | sqlx database drop |
| Recriar banco e aplicar migrations | sqlx database reset |
Confirme os argumentos com sqlx --help e sqlx migrate --help na versão homologada pelo projeto. A CLI evolui junto com o ecossistema, portanto pipelines devem fixar uma versão conhecida em vez de instalar silenciosamente qualquer release nova.
O que é uma migration no SQLx
Uma migration é uma alteração versionada no schema do banco. Ela pode criar uma tabela, adicionar um índice, mudar uma restrição, popular uma coluna ou preparar a remoção de uma estrutura antiga. No SQLx, os arquivos ficam normalmente no diretório migrations/ e fazem parte do repositório.
Ao executar as migrations, o SQLx mantém uma tabela interna de controle no banco. Essa tabela registra informações como a versão aplicada e o checksum do arquivo. O histórico permite distinguir uma migration já executada de uma pendente e também ajuda a detectar alterações indevidas em arquivos antigos.
A regra prática é: depois que uma migration chegou a um ambiente compartilhado, não edite o arquivo como se ele ainda fosse um rascunho. Crie uma nova migration corretiva. Alterar o passado pode fazer ambientes divergirem e quebrar a verificação por checksum.
Esse modelo complementa as queries verificadas em compile-time explicadas no guia de SQLx Rust. As macros validam o código contra um schema conhecido; migrations tornam a construção desse schema reproduzível.
Preparando SQLx CLI e PostgreSQL
Em um projeto PostgreSQL, instale a CLI somente com os recursos necessários:
cargo install sqlx-cli \
--no-default-features \
--features rustls,postgres
O uso de --no-default-features evita compilar suporte a bancos que o projeto não utiliza. A feature de TLS pode variar conforme a política do ambiente; confira a documentação e as opções da versão instalada.
Configure a URL em um arquivo .env local que não seja versionado:
DATABASE_URL=postgres://app:senha_local@localhost:5432/app_dev
Não coloque credenciais reais em migrations, exemplos do repositório ou logs de CI. Em produção, injete DATABASE_URL pelo gerenciador de segredos da plataforma.
Para subir um PostgreSQL descartável durante o desenvolvimento, uma opção é Docker:
docker run --name sqlx-postgres \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=senha_local \
-e POSTGRES_DB=app_dev \
-p 5432:5432 \
-d postgres:17
Depois, valide a conexão e o estado:
sqlx database create
sqlx migrate info
Se você usa uma composição completa com API e banco, o tutorial de deploy de Axum, Docker Compose e PostgreSQL mostra como separar serviços e health checks.
Criando a primeira migration
Crie uma migration reversível para a tabela de usuários:
sqlx migrate add -r criar_usuarios
A opção -r cria um par de arquivos: um para aplicar a mudança (up) e outro para revertê-la (down). Os nomes incluem uma versão temporal e uma descrição, preservando a ordem.
No arquivo up, escreva:
CREATE TABLE usuarios (
id BIGSERIAL PRIMARY KEY,
nome TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
ativo BOOLEAN NOT NULL DEFAULT TRUE,
criado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_usuarios_ativos
ON usuarios (criado_em DESC)
WHERE ativo = TRUE;
No arquivo down:
DROP TABLE usuarios;
Agora aplique e confira:
sqlx migrate run
sqlx migrate info
O arquivo down é útil no desenvolvimento, mas não transforma toda alteração em um rollback seguro. Se a migration up apagar dados, o down não conseguirá recriá-los. Reversibilidade sintática e recuperação real são conceitos diferentes.
Integrando migrations ao código Rust
Também é possível incorporar as migrations no binário com a macro sqlx::migrate!:
use sqlx::PgPool;
pub async fn executar_migrations(pool: &PgPool) -> Result<(), sqlx::migrate::MigrateError> {
sqlx::migrate!("./migrations")
.run(pool)
.await
}
Uma aplicação pequena pode chamar essa função antes de iniciar o servidor:
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let database_url = std::env::var("DATABASE_URL")?;
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(10)
.connect(&database_url)
.await?;
executar_migrations(&pool).await?;
// Criar o Router do Axum e iniciar o servidor.
Ok(())
}
A abordagem reduz a chance de publicar um binário sem os arquivos necessários. Por outro lado, executar mudanças de schema durante o startup mistura duas responsabilidades. Se dez réplicas iniciarem juntas, todas tentarão verificar migrations; mesmo com controle transacional e de concorrência do mecanismo, o startup passa a depender da duração e do sucesso de uma operação administrativa.
Para produção, uma etapa única costuma ser mais clara:
- construir e testar o artefato;
- executar um job de migration com identidade e permissões próprias;
- verificar o resultado;
- liberar gradualmente a nova versão da aplicação;
- observar erros, latência e conexões.
Como testar migrations na CI
O teste mais básico começa com um PostgreSQL vazio. Um pipeline deve:
- iniciar o serviço de banco;
- esperar o health check;
- criar um banco isolado;
- executar todas as migrations;
- rodar testes de integração;
- descartar o ambiente.
Exemplo conceitual de comandos:
export DATABASE_URL=postgres://app:[email protected]:5432/app_ci
sqlx database create
sqlx migrate run
cargo test --locked
O banco vazio detecta migrations fora de ordem, SQL inválido e dependências implícitas. Porém, ele não encontra todos os problemas de produção. Uma alteração pode funcionar em uma tabela vazia e falhar ao converter milhões de linhas.
Para mudanças relevantes, adicione um segundo cenário:
- restaure um dump sanitizado ou gere dados representativos;
- aplique a versão anterior do schema;
- execute a nova migration;
- meça tempo, locks e crescimento de armazenamento;
- inicie a versão antiga e a nova da aplicação contra o estado intermediário quando houver deploy gradual.
A suíte de testes em Rust deve incluir tanto a lógica de domínio quanto a integração com o banco. Mocks não revelam constraints, tipos PostgreSQL, isolamento de transações ou comportamento real de índices.
Migrations e queries offline no SQLx
Migrations e preparação offline resolvem problemas relacionados, mas diferentes:
- migrations constroem e evoluem o schema;
- metadados offline permitem verificar macros de query sem conectar o compilador ao banco durante todo build.
Um fluxo comum é aplicar as migrations em um banco de desenvolvimento e depois preparar os metadados usados pela verificação offline:
sqlx migrate run
cargo sqlx prepare --workspace
Versione o artefato de metadados esperado pela versão atual do SQLx e faça a CI verificar se ele está atualizado conforme o procedimento do projeto. Isso é especialmente importante em Cargo workspaces com vários serviços e crates de acesso a dados.
Não confunda preparação offline com execução de migrations no ambiente final. Um build bem-sucedido não prova que o banco de produção já está no schema esperado.
Estratégia segura: expandir, migrar e contrair
Mudar uma coluna em uso por várias réplicas exige compatibilidade entre versões. O padrão expandir e contrair evita depender de uma troca instantânea.
Imagine que usuarios.nome será dividido em nome e sobrenome.
1. Expandir
Adicione a nova coluna sem remover a antiga:
ALTER TABLE usuarios ADD COLUMN sobrenome TEXT;
Publique uma versão da aplicação capaz de conviver com os dois formatos. Quando necessário, faça dual write temporário ou mantenha fallback de leitura.
2. Migrar os dados
Preencha a coluna em lotes, fora de uma transação gigantesca quando o volume justificar. Monitore duração, locks, I/O e réplicas.
3. Contrair
Depois que todas as versões antigas saírem de circulação e os dados forem validados, crie outra migration para adicionar constraints ou remover a estrutura obsoleta.
Esse método é mais trabalhoso do que um ALTER TABLE agressivo, mas reduz o risco em rolling deploys. Ele também permite rollback da aplicação sem exigir que o schema volte no tempo.
Locks e migrations em tabelas grandes
Operações de DDL podem adquirir locks e bloquear queries. O impacto depende do PostgreSQL, da operação, do tamanho da tabela e do tráfego. Antes de executar uma mudança grande:
- leia o plano operacional da versão do PostgreSQL usada;
- teste com volume semelhante ao real;
- configure timeouts apropriados para não esperar indefinidamente;
- evite reescrever uma tabela inteira durante o horário de pico;
- crie índices grandes com a estratégia adequada ao ambiente;
- acompanhe sessões bloqueadas e latência;
- tenha um procedimento de interrupção e recuperação.
Não coloque CREATE INDEX CONCURRENTLY ou outras operações especiais em uma migration sem verificar as regras transacionais da ferramenta e do PostgreSQL. Algumas operações não podem rodar dentro de uma transação comum; confirme como a versão do SQLx CLI trata o arquivo e teste o procedimento completo antes da produção.
Rollback: aplicação e banco não voltam juntos
sqlx migrate revert é conveniente no ambiente local:
sqlx migrate revert
Em produção, o melhor rollback muitas vezes é republicar a versão anterior da aplicação mantendo o schema expandido. Isso só funciona quando a migration foi desenhada para compatibilidade.
Evite depender de rollback destrutivo para situações como:
- remoção de coluna com dados;
- conversão irreversível de formato;
- consolidação de tabelas;
- deleção de registros;
- alteração que a versão antiga não consegue ler.
Para esses casos, use backup validado, migrations corretivas e rollout gradual. O objetivo não é fazer toda migration possuir um down elegante; é preservar dados e permitir recuperação previsível.
Boas práticas para equipes
Uma mudança lógica por migration
Arquivos menores são mais fáceis de revisar, medir e corrigir. Não misture criação de tabela, backfill massivo e remoção de estrutura antiga sem necessidade.
SQL explícito e revisado
Migration é código de produção. Exija revisão de constraints, defaults, nulabilidade, índices, foreign keys e impacto operacional.
Nunca use dados secretos no arquivo
Migrations ficam no Git e circulam entre ambientes. Segredos devem vir do ambiente, e dados sensíveis não devem aparecer em fixtures públicas.
Fixe versões da ferramenta
Uma imagem de CI ou etapa de setup deve instalar a versão homologada do sqlx-cli. Combine isso com cargo test --locked e revisão do Cargo.lock.
Separe permissões
A aplicação normalmente precisa ler e modificar dados; o job de migration pode precisar alterar schema. Usar papéis diferentes reduz a superfície de privilégio do processo web.
Observe o deploy
Registre início, fim, versão e resultado da migration. Não exponha DATABASE_URL em logs. Integre a etapa à observabilidade descrita no guia de Tracing em Rust.
Checklist antes de executar em produção
- A migration foi aplicada do zero em CI.
- O upgrade a partir do schema anterior foi testado.
- O SQL recebeu revisão de outra pessoa.
- O arquivo antigo não foi alterado depois de aplicado.
- O impacto de locks foi medido com dados representativos.
- A versão antiga da aplicação tolera o schema expandido.
- Existe backup recente e recuperação testada quando a mudança envolve dados.
- O job usa o banco e as credenciais do ambiente correto.
- Logs não exibem segredos.
- Há observabilidade e responsável disponível durante a execução.
- O plano de rollback distingue aplicação, schema e dados.
SQLx migrations ou migration de outro ORM?
Se o projeto usa SQLx diretamente, o sistema integrado é uma escolha natural: SQL versionado, CLI simples e pouca abstração. Diesel e SeaORM também possuem fluxos próprios, mais alinhados aos modelos e ferramentas desses ecossistemas.
A comparação SQLx vs Diesel vs SeaORM ajuda a escolher a camada de acesso a dados. Depois da escolha, evite misturar três gerenciadores de migration sem uma responsabilidade clara. O banco precisa de uma única ordem confiável de evolução.
Conclusão
SQLx migrations dão a projetos Rust uma forma direta de versionar PostgreSQL, reproduzir ambientes e integrar mudanças de schema à CI. O começo cabe em três comandos: criar a migration, executar pendências e consultar o estado.
A maturidade aparece depois: migrations pequenas, testes com dados reais, deploy separado, expansão compatível, atenção a locks e rollback que prioriza preservação de dados. Em uma API Axum com SQLx, esse processo é tão importante quanto escrever handlers rápidos ou queries tipadas.
Comece criando um banco descartável, aplique todo o diretório migrations/ e automatize esse caminho na CI. Quando o mesmo procedimento funciona de forma repetível do notebook ao ambiente de produção, o schema deixa de ser uma sequência de comandos manuais e passa a ser parte verificável do software.