Problemas Conhecidos¶
Comportamentos do PostgreSQL, Supabase CLI, e SQLAlchemy que já nos pegaram. Consulte antes de escrever migrations ou queries com patterns não triviais.
Supabase CLI / db diff¶
CREATE DOMAIN não é capturado pelo diff¶
O supabase db diff não detecta criação de domínios customizados. Se você adicionar um novo typed ID domain em 02_domains.sql, a migration gerada não vai incluir o CREATE DOMAIN.
Fix: Adicione manualmente ao arquivo de migration gerado:
-- adicionar manualmente após rodar db-diff
CREATE DOMAIN "contact_tag_id" AS "text"
CHECK (VALUE ~ '^ctag_[0-9A-Z]{26}$');
O mesmo vale para: comentários (COMMENT ON), particionamento, e algumas extensões.
ALTER TYPE ... ADD VALUE não pode estar em transação¶
Adicionar um valor a um enum existente falha se executado dentro de um bloco de transação:
-- ❌ Falha se o Supabase aplicar em transação implícita
ALTER TYPE "subscription_status" ADD VALUE 'trial_expired';
Fix: Use a cláusula IF NOT EXISTS e verifique se o ambiente de deploy aplica cada migration em transação separada. Se necessário, crie uma migration separada apenas para o ALTER TYPE.
PostgreSQL: tabelas particionadas¶
PK deve incluir a coluna de particionamento¶
Em tabelas PARTITION BY RANGE (created_at), a PK precisa incluir created_at:
-- ❌ Falha
CREATE TABLE "audit_events" (
"id" audit_event_id PRIMARY KEY,
"created_at" timestamptz NOT NULL,
...
) PARTITION BY RANGE (created_at);
-- ✅ Correto
CREATE TABLE "audit_events" (
"id" audit_event_id NOT NULL,
"created_at" timestamptz NOT NULL,
...
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
Não use ADD CONSTRAINT ... PRIMARY KEY após criação¶
Para tabelas particionadas, declare a PK inline no CREATE TABLE. O ALTER TABLE ... ADD CONSTRAINT ... PRIMARY KEY depois da criação falha em tabelas particionadas.
FKs de/para tabelas particionadas¶
PostgreSQL não suporta FK referenciando uma tabela particionada como origem — apenas como destino. Se a tabela particionada precisar referenciar outra tabela, trate a integridade referencial na camada de aplicação ou via triggers.
PostgreSQL: CTEs data-modifying¶
INSERT + UPDATE no mesmo CTE não se enxergam¶
-- ❌ O UPDATE não vê as linhas inseridas pelo INSERT na mesma query
WITH inserted AS (
INSERT INTO items (id, tenant_id, status)
VALUES (:id, :tenant_id, 'pending')
RETURNING id
)
UPDATE items SET status = 'active'
WHERE id IN (SELECT id FROM inserted);
O PostgreSQL resolve CTEs usando um snapshot consistente — o UPDATE vê o estado antes do INSERT.
Fix: Use duas statements separadas:
INSERT INTO items (id, tenant_id, status) VALUES (:id, :tenant_id, 'pending');
UPDATE items SET status = 'active' WHERE id = :id;
PostgreSQL: CHECK constraints com NOT VALID¶
Se você adicionar um CHECK constraint com NOT VALID (para não bloquear a tabela durante o alter), valide em uma migration separada após o backfill:
-- Migration 1: adicionar constraint sem validar dados existentes
ALTER TABLE "documents"
ADD CONSTRAINT "chk_language_not_empty" CHECK (language != '') NOT VALID;
-- Migration 2 (após backfill): validar dados existentes
ALTER TABLE "documents"
VALIDATE CONSTRAINT "chk_language_not_empty";
Aplicar VALIDATE CONSTRAINT antes do backfill fazer lock na tabela ou falhar se houver dados inválidos.
SQLAlchemy + asyncpg¶
JSONB não aceita dict direto — use json.dumps() + CAST¶
O asyncpg não converte automaticamente dict Python para jsonb. Passa como string JSON com cast explícito:
# ❌ Falha com asyncpg
await session.execute(
"INSERT INTO items (data) VALUES (:data)",
{"data": {"key": "value"}}
)
# ✅ Correto
import json
await session.execute(
"INSERT INTO items (data) VALUES (CAST(:data AS jsonb))",
{"data": json.dumps({"key": "value"})}
)
Nota: Não use ::jsonb (cast estilo PostgreSQL) com named params do SQLAlchemy — o : no ::jsonb conflita com o prefixo de parâmetros. Use sempre CAST(:param AS jsonb).
ON CONFLICT em PK composta precisa de todas as colunas¶
Para tabelas com PK composta (ex: tabelas particionadas com PRIMARY KEY (id, created_at)):
# ❌ Falha — PK composta precisa de todas as colunas
query = """
INSERT INTO audit_events (id, created_at, ...)
VALUES (:id, :created_at, ...)
ON CONFLICT (id) DO UPDATE SET ...
"""
# ✅ Correto
query = """
INSERT INTO audit_events (id, created_at, ...)
VALUES (:id, :created_at, ...)
ON CONFLICT (id, created_at) DO UPDATE SET ...
"""
Ordem de migrations¶
DML após DDL¶
Migrations de dados (backfill) devem ter timestamp posterior à DDL que cria as colunas/tabelas que elas populam:
20260310000000_add_language_column.sql ← DDL (cria coluna)
20260310000001_backfill_language.sql ← DML (popula coluna)
20260310000002_add_not_null_constraint.sql ← DDL (adiciona constraint depois do backfill)
Se o timestamp do DML for anterior, o Supabase vai tentar populá-lo antes da coluna existir.
Detecção de mudanças não relacionadas no diff¶
O supabase db diff às vezes inclui mudanças não relacionadas com o que você editou — especialmente quando há divergência entre o schema declarativo e o estado local (ex: uma renomeação de coluna que estava no schema mas não foi migrada).
Sempre revise o arquivo gerado antes de commitar. Se aparecer algo inesperado:
- Verifique se o banco local está atualizado (
make db-reset) - Confirme que o schema reflete exatamente o estado do banco
- Remova da migration o que não é parte da sua mudança