Pular para conteúdo

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:

  1. Verifique se o banco local está atualizado (make db-reset)
  2. Confirme que o schema reflete exatamente o estado do banco
  3. Remova da migration o que não é parte da sua mudança