SQLx Migrations em Rust: PostgreSQL, CI e Deploy

Aprenda a criar, executar e reverter migrations SQLx em Rust com PostgreSQL. Guia prático sobre CLI, testes, CI, deploy, locks e rollback de schema seguro.

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

ObjetivoComando
Instalar a CLI para PostgreSQLcargo install sqlx-cli --no-default-features --features rustls,postgres
Criar uma migration simplessqlx migrate add criar_usuarios
Criar migration reversívelsqlx migrate add -r criar_usuarios
Aplicar pendênciassqlx migrate run
Ver o estado das migrationssqlx migrate info
Reverter a última migration reversívelsqlx migrate revert
Criar o banco configuradosqlx database create
Apagar o banco configuradosqlx database drop
Recriar banco e aplicar migrationssqlx 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:

  1. construir e testar o artefato;
  2. executar um job de migration com identidade e permissões próprias;
  3. verificar o resultado;
  4. liberar gradualmente a nova versão da aplicação;
  5. 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:

  1. iniciar o serviço de banco;
  2. esperar o health check;
  3. criar um banco isolado;
  4. executar todas as migrations;
  5. rodar testes de integração;
  6. 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.