O erro “cannot execute CREATE TABLE in a read-only transaction” do PostgreSQL está causando falhas em migrações para muitas equipes, deixando esquemas parcialmente aplicados e tabelas de histórico de migração travadas. A falha geralmente ocorre quando uma operação de escrita cai em uma réplica em vez do nó primário, e interrompe qualquer ferramenta que dependa de alterações de esquema — Flyway, Liquibase, Django ORM e similares.
Por que o erro ocorre
O PostgreSQL desabilita todas as escritas, alterações de esquema e atualizações de sequências quando uma transação é marcada como somente leitura (read-only). As formas mais comuns de uma migração acabar nesse estado são:
- Poolers de conexão (ex: PgBouncer) que, inadvertidamente, enviam a conexão de migração para uma réplica de leitura.
- Roles que possuem o parâmetro
default_transaction_read_onlydefinido como on por padrão. - Endpoints de nuvem que expõem URLs separadas para leitura (reader) e escrita (writer); usar a URL de leitura (como é comum com AWS RDS ou Aurora) direciona as escritas para uma réplica.
Quando qualquer uma dessas condições se aplica, o processo de migração pode tentar criar tabelas, adicionar colunas ou atualizar sequências apenas para encontrar um obstáculo, deixando o banco de dados em um estado de migração parcial.
Correção imediata dentro da transação
Se você já encontrou o erro, pode sobrescrever a flag de somente leitura para a transação atual sem afetar o restante da sessão — o que é crucial quando um pool de conexões reutiliza a mesma sessão para outros trabalhos.
BEGIN;
SET LOCAL default_transaction_read_only = off;
SET TRANSACTION READ WRITE;
CREATE TABLE orders (id SERIAL PRIMARY KEY, total NUMERIC);
COMMIT;
SET LOCAL altera a configuração apenas durante a duração da transação. Usar apenas SET faria com que a alteração persistisse por toda a sessão, o que pode quebrar outras operações que legitimamente precisam de um padrão de somente leitura.
Medidas preventivas
1. Verifique se você está no nó primário
Adicione uma verificação rápida antes de qualquer execução de migração:
SELECT CASE WHEN pg_is_in_recovery() THEN 'REPLICA' ELSE 'PRIMARY' END;
Se o resultado for REPLICA, aborte a migração. A função pg_is_in_recovery() retorna true em um servidor standby, garantindo que você não esteja tentando escrever em uma cópia de somente leitura.
2. Use roles de migração dedicadas
Crie uma role cujo modo de transação padrão permita escrita e conceda a ela apenas as permissões necessárias:
CREATEno banco de dados de destino.CONNECTpara permitir que a ferramenta de migração abra uma sessão.
Evite atribuir a esta role o atributo default_transaction_read_only = on, que às vezes é definido para usuários de propósito geral.
3. Aponte a infraestrutura para o endpoint de escrita (writer)
Em scripts de IaC (Terraform, CloudFormation, etc.), configure os executores de migração para usar o endpoint de escrita (writer) do cluster, não o endpoint de leitura (reader). O endpoint de escrita resolve para o nó primário, enquanto o endpoint de leitura resolve para uma réplica que rejeitará as escritas.
4. Adicione um gate de CI/CD
Insira uma etapa de shell em seu pipeline (GitHub Actions, GitLab CI, etc.) que execute a consulta pg_is_in_recovery(). Se ela retornar true, encerre o job com um status diferente de zero para interromper o deployment precocemente.
if psql $DATABASE_URL -c "SELECT pg_is_in_recovery()" | grep -q t; then
echo "Connected to replica – aborting migration"
exit 1
fi
5. Ajuste as tentativas de repetição (retries) da ferramenta de migração
Ferramentas como o Flyway costumam tentar reconectar automaticamente. Para o Flyway 9+, defina flyway.connectRetries=0. Isso evita que a ferramenta atinja repetidamente uma réplica, o que, de outra forma, poderia aumentar o lag de replicação e desperdiçar recursos.
O que monitorar a seguir
- Métricas de lag de replicação: Um aumento no lag pode indicar que uma migração visou inadvertidamente uma réplica, fazendo com que as tentativas de escrita ficassem na fila.
- Regras de roteamento do pooler de conexão: Certifique-se de que as configurações do pooler direcionem explicitamente o tráfego de migração para o host primário.
- Padrões de roles após upgrades: Upgrades de banco de dados às vezes resetam os parâmetros das roles; realize uma nova auditoria do
default_transaction_read_onlyapós mudanças de versão principais.
Em resumo: um erro de transação somente leitura raramente é um bug do PostgreSQL; é um sintoma de tráfego sendo enviado para o nó errado ou de uma role sendo configurada incorretamente. Ao verificar o papel do nó, usar contas de migração dedicadas e reforçar seu pipeline de CI/CD, você pode manter as migrações funcionando sem problemas e evitar esquemas parcialmente aplicados que prejudicam o desenvolvimento subsequente.
