8 min

Verificação de esquema PostgreSQL antes da primeira migração

A verificação de esquema PostgreSQL detecta mapeamentos errados, restrições fracas, índices ausentes e alterações inseguras antes que a primeira migração toque nos dados.

Verificação de esquema PostgreSQL antes da primeira migração

Um gerador com IA pode produzir PostgreSQL válido e ainda assim deduzir o banco de dados errado. A sintaxe é a parte fácil. Os erros perigosos parecem plausíveis: uma relação opcional se torna obrigatória, uma string de status recebe uma restrição CHECK incompleta, uma exclusão se propaga por registros que deveriam sobreviver ou uma migração recria uma tabela e perde discretamente uma coluna.

Por isso, a verificação de esquema PostgreSQL precisa testar separadamente o significado, o comportamento da migração e a recuperação. Só aprovo um esquema deduzido depois que ele passa por um conjunto de dados conhecido, invariantes explícitas, consultas representativas, revisão de alterações destrutivas e um ensaio de restauração. Se faltar qualquer um desses pontos, a migração continua sendo uma proposta.

A primeira migração merece esse rigor mesmo quando o banco de produção está vazio. Erros iniciais de esquema se consolidam rapidamente, porque o código da aplicação, os dados iniciais, os relatórios e as migrações posteriores passam a depender deles. Uma revisão de quinze minutos antes da primeira execução costuma custar menos do que explicar seis meses depois por que dois conceitos diferentes compartilham uma coluna de texto que aceita nulos.

Um esquema deduzido é uma especificação não confiável

Trate o esquema deduzido como um rascunho de especificação, não como uma verdade executável. O gerador viu prompts, telas de exemplo, registros importados ou código de aplicação gerado. Ele não acompanhou todas as exceções de negócio, regras de retenção, importações em massa, correções do suporte e pagamentos recusados que o banco acabará armazenando.

Comece separando três perguntas que as equipes costumam misturar. A correção do esquema pergunta se tabelas e restrições representam o domínio. A segurança da migração pergunta se as operações propostas preservam os dados existentes e mantêm o banco utilizável enquanto executam. A prontidão para recuperação pergunta se é possível voltar a um estado conhecido após uma alteração parcial ou semanticamente errada. Passar em uma delas diz pouco sobre as outras duas.

Uma instrução CREATE TABLE pode descrever a estrutura final desejada e ainda chegar a ela por operações inseguras. Imagine que o gerador transforme customer_name text em customer_id bigint. A chave estrangeira final pode fazer sentido, mas uma migração que elimina a coluna de nome antes de associar nomes históricos aos clientes destrói a única evidência necessária para essa associação. A revisão de esquema aprova o destino; a revisão da migração examina o caminho.

Leia o modelo proposto em voz alta na linguagem do domínio. Diga que cada fatura pertence a exatamente um cliente legal, em vez de dizer que invoices.customer_id referencia customers.id. A primeira frase convida a objeções úteis: rascunhos podem existir antes da seleção do cliente, faturas importadas podem apontar para clientes arquivados e registros legais podem precisar manter congelado o nome do cliente no momento da emissão. O vocabulário SQL pode ocultar essas divergências.

Exijo uma nota de premissas ao lado de cada tabela deduzida. Ela deve explicar o que uma linha significa, como a linha é identificada, quem a possui, se pode existir sem seu aparente pai e o que significa excluí-la. Se a equipe não consegue responder a esses pontos, o gerador adivinhou um banco de dados que a equipe não projetou.

Registros conhecidos revelam mapeamentos de tabela errados

Um conjunto de dados conhecido deve conter registros escolhidos pela cobertura semântica, porque uma grande amostra aleatória muitas vezes repete o mesmo caso fácil. Dez registros selecionados com cuidado podem revelar mais do que dez mil linhas quase idênticas no caminho feliz.

Monte uma matriz de mapeamento antes de executar o DDL. Cada linha da matriz deve acompanhar um conceito de origem até o destino proposto e registrar a contagem ou o valor esperado. Para uma aplicação de pedidos, o artefato pode ser assim:

Fato conhecidoDestino propostoResultado esperado
O pedido A tem dois itensorders e order_itemsUma linha de pedido e duas linhas-filhas
O pedido B não tem conta atribuídaorders.account_idUma linha com conta NULL
Duas pessoas compartilham um emailcontacts.emailAs duas linhas sobrevivem, a menos que unicidade seja uma regra declarada
O código do produto tem zeros à esquerdaproducts.codeO valor de texto 00417 permanece inalterado
Pedido cancelado mantém cobrançasorders e chargesAs linhas de cobrança permanecem após o cancelamento

Isso captura erros de mapeamento de tabelas antes que os detalhes das restrições distraiam a revisão. Geradores com IA frequentemente normalizam objetos repetidos em tabelas separadas, o que costuma ser sensato, mas repetição não prova identidade. Dois endereços de entrega com o mesmo texto podem ser registros históricos, não referências a uma única linha de endereço editável. Mesclá-los faz uma edição posterior de endereço reescrever a história.

O erro oposto também acontece. Um gerador pode copiar campos do cliente para todos os pedidos porque a tela os exibe juntos. Alguns valores pertencem ao cliente, enquanto outros devem permanecer como um retrato do pedido. O projeto correto pode conter tanto customer_id quanto campos do documento emitido, como billing_name. Chamar isso de duplicação e excluir um lado perde a identidade atual ou a verdade histórica.

Carregue o conjunto conhecido em um banco descartável pelo mesmo caminho de importação ou dados iniciais que a aplicação usará. Em seguida, escreva verificações contra fatos, não apenas contagens de linhas:

SELECT
    (SELECT count(*) FROM orders WHERE external_id = 'ORDER-A') AS order_a,
    (SELECT count(*) FROM order_items i
       JOIN orders o ON o.id = i.order_id
      WHERE o.external_id = 'ORDER-A') AS order_a_items,
    (SELECT account_id IS NULL FROM orders
      WHERE external_id = 'ORDER-B') AS order_b_unassigned;

Um resultado aprovado deve ser explícito:

 order_a | order_a_items | order_b_unassigned
---------+---------------+---------------------
       1 |             2 | t

Não aceite uma diferença sem explicação porque a aplicação gerada continua sendo exibida. Uma interface pode esconder pais duplicados, filhos descartados, códigos truncados e padrões inventados. Reconcilie cada caso de teste intencional antes de discutir a implantação em produção.

As restrições precisam representar verdades do domínio

Uma restrição do banco deve rejeitar um estado que é sempre inválido, independentemente da tela, API, importação ou script de correção que grave a linha. Se uma regra tem exceções ou depende de fatos externos mutáveis, forçá-la em uma restrição simples costuma gerar trabalho bloqueado ou dados desonestos.

Chaves primárias identificam linhas, mas não fornecem automaticamente uma identidade de negócio significativa. Um ID bigint interno pode coexistir com um número de pedido único no escopo do locatário. Se o negócio diz que números de pedido são únicos por locatário, UNIQUE (tenant_id, order_number) expressa essa regra. Uma restrição global de unicidade rejeitaria registros legítimos, enquanto a falta de restrição permitiria ambiguidade durante tentativas repetidas.

Restrições CHECK servem para fatos estáveis da linha, como quantity > 0 ou finished_at >= started_at. O manual do PostgreSQL explica que o banco presume que uma expressão CHECK é imutável durante toda a vida da restrição. Por isso, uma CHECK que chama uma função cujo comportamento muda depois pode deixar linhas antigas violando a regra aparente. Use uma expressão fixa para uma verdade fixa. Coloque políticas mutáveis, como um conjunto atualmente permitido controlado por administradores, em uma tabela referenciada ou no fluxo da aplicação.

Restrições de status geradas exigem desconfiança. Um gerador pode examinar os exemplos atuais e emitir:

status text NOT NULL
    CHECK (status IN ('draft', 'active', 'closed'))

Isso só estará correto se esses forem todos os estados duradouros. Pergunte sobre registros com falha, cancelados, suspensos, importados e legados desconhecidos. Se a máquina de estados ainda muda, uma tabela de consulta pode tornar inclusões explícitas, mas não substitui a validação das transições. Uma linha autorizada a conter closed não diz nada sobre ela poder passar diretamente de draft para closed.

Use unicidade deliberadamente. O PostgreSQL implementa uma restrição única com um índice B-tree único, mas um índice único parcial expressa uma regra diferente. A exclusão lógica costuma exigir unicidade apenas entre linhas ativas:

CREATE UNIQUE INDEX users_tenant_email_live_uq
    ON users (tenant_id, lower(email))
    WHERE deleted_at IS NULL;

Isso não é equivalente a UNIQUE (tenant_id, email, deleted_at). O PostgreSQL trata valores NULL conforme suas regras de unicidade, e incluir o carimbo de exclusão altera a identidade que está sendo imposta. Revise os casos exatos de duplicidade com casos de teste, em vez de deduzir o comportamento pela lista de colunas.

A nulidade é uma decisão de negócio

Defina uma coluna como NOT NULL somente quando o domínio exigir um valor para toda linha legítima e todos os caminhos de gravação puderem fornecê-lo. O desenho da tela é uma evidência fraca. Um campo obrigatório no formulário atual não diz nada sobre importações, rascunhos, linhas geradas pelo sistema ou registros históricos.

Revise quatro estados separadamente: a origem omitiu o campo, a origem enviou null explicitamente, a origem enviou um valor vazio e a origem forneceu um valor significativo. APIs JSON, formulários, importações CSV e PostgreSQL podem tratar esses estados de forma diferente. Se a aplicação agrupa os quatro antes da inserção, a revisão do esquema deve deixar essa decisão visível, em vez de fingir que o banco a resolveu.

Valores padrão merecem a mesma atenção. Um padrão fornece um valor quando um INSERT omite a coluna; ele não corrige um NULL explícito nem prova que o valor é verdadeiro. country_code DEFAULT 'US' é perigoso se um país desconhecido for possível. A linha passa a conter uma mentira convincente na qual relatórios e lógica de conformidade podem confiar.

Uma migração gerada comum adiciona uma coluna obrigatória em uma única instrução:

ALTER TABLE customers
    ADD COLUMN account_type text NOT NULL DEFAULT 'standard';

A instrução pode ser executada, mas todo cliente histórico se torna padrão sem evidência. Uma sequência mais segura adiciona a coluna anulável, deriva valores de dados conhecidos, mede as linhas não resolvidas, impede novas omissões nas gravações da aplicação e só então adiciona NOT NULL se o domínio justificar. Se o desconhecido continuar sendo legítimo, mantenha NULL e defina como consultas e interfaces o exibem.

O PostgreSQL oferece uma separação útil para algumas restrições. Uma CHECK ou chave estrangeira pode ser adicionada como NOT VALID, evitando validar todas as linhas existentes durante sua criação, e depois verificada com VALIDATE CONSTRAINT. O manual documenta isso como uma forma de adiar a varredura inicial da tabela. Isso não autoriza ignorar violações antigas: novas gravações sofrem a imposição, e a etapa de validação ainda precisa passar antes da aprovação.

Antes de tornar a nulidade mais rígida, execute uma consulta de distribuição que mostre as categorias reais:

SELECT
    count(*) AS total,
    count(*) FILTER (WHERE account_type IS NULL) AS nulls,
    count(*) FILTER (WHERE account_type = '') AS empty_strings,
    count(*) FILTER (WHERE account_type NOT IN
        ('standard', 'partner', 'internal')) AS unexpected
FROM customers;

Um padrão gerado por IA pode fazer essa consulta parecer limpa depois da migração. Execute-a também antes do preenchimento e preserve o resultado. Caso contrário, você perde a evidência necessária para distinguir valores derivados de valores inventados.

Os índices devem atender a padrões de acesso observados

Preserve um caminho de volta
Mantenha o rollback disponível quando uma alteração gerada precisar voltar a um estado conhecido da aplicação.

Aprove um índice quando ele atende a uma consulta conhecida, impõe uma regra de unicidade declarada ou viabiliza uma necessidade operacional. Indexar toda coluna com formato de identificador desperdiça armazenamento e trabalho de gravação, enquanto a falta de um índice composto importante pode transformar uma página de lista rotineira em uma varredura crescente.

Comece pelas consultas que a aplicação gerada realmente faz. Registre as colunas de filtro, o limite do locatário, as colunas de junção, a ordenação e o tamanho esperado do resultado. Para uma página de pedidos recentes, o formato desta consulta importa mais do que o diagrama da tabela:

SELECT id, order_number, status, created_at
FROM orders
WHERE tenant_id = $1
  AND status = $2
ORDER BY created_at DESC
LIMIT 50;

Um índice apenas em tenant_id ainda pode examinar muitas linhas do locatário e ordená-las. Um índice em (tenant_id, status, created_at DESC) corresponde mais de perto a esse padrão de acesso. A ordem das colunas não é uma disputa de popularidade; ela segue condições de igualdade, condições de intervalo, ordenação e seletividade na consulta real.

Execute EXPLAIN (ANALYZE, BUFFERS) com dados representativos, mas não trate um único caso de teste pequeno como prova de desempenho. O PostgreSQL pode preferir corretamente uma varredura sequencial em uma tabela pequena. A verificação deve confirmar que o índice pretendido existe e que um ensaio com tamanho próximo ao de produção oferece ao planejador uma escolha realista. Nunca desative varreduras sequenciais para fabricar uma varredura de índice e aprová-la.

Chaves estrangeiras trazem outra surpresa comum: o PostgreSQL indexa as colunas primárias ou únicas referenciadas, mas não cria automaticamente um índice nas colunas-filhas referenciadoras. Excluir ou atualizar um pai pode, portanto, varrer a tabela-filha para verificar referências. Junções da filha para o pai também podem precisar desse índice filho. Examine cada relação com base nas leituras esperadas e nas alterações dos pais.

Rejeite índices duplicados e não usados na proposta inicial. (tenant_id, status) pode ser redundante se já existir um índice adequado em (tenant_id, status, created_at), embora detalhes da carga de trabalho possam mudar esse julgamento. Compare definições, não nomes. Geradores com IA costumam criar um índice por recurso e não percebem que vários recursos pediram as mesmas colunas iniciais.

Em bancos existentes e movimentados, lembre-se de que CREATE INDEX CONCURRENTLY não pode ser executado dentro de um bloco de transação, exige mais trabalho e pode deixar um índice inválido após falhar. O manual do PostgreSQL explica essas diferenças operacionais. Uma ferramenta de migração que envolve toda migração em uma transação precisa de uma exceção explícita e de um procedimento de limpeza, não de uma substituição otimista de palavra-chave.

Chaves estrangeiras precisam de propriedade e regras de exclusão

Uma chave estrangeira só está correta depois que a equipe decide se a relação expressa propriedade, referência, contexto opcional ou atribuição histórica. Colunas de aparência semelhante podem exigir comportamentos opostos para exclusão.

Considere projects.owner_user_id, invoices.customer_id e audit_events.actor_user_id. Um projeto pode transferir de proprietário. Uma fatura pode precisar sobreviver ao encerramento da conta do cliente. Um evento de auditoria pode preservar o identificador anterior do ator mesmo depois que os dados de identidade forem removidos. Aplicar ON DELETE CASCADE aos três porque referenciam users codificaria uma ficção destrutiva.

Use CASCADE quando o filho não tiver significado sem o pai e excluir o pai realmente significar excluir todo o agregado. Itens de pedido costumam se encaixar nisso. Registros de pagamento, documentos emitidos, importações, logs e evidências de moderação muitas vezes não. Para eles, rejeição, arquivamento, anonimização controlada ou uma referência anulável acompanhada de campos de retrato preservados podem ser melhores opções.

SET NULL também exige revisão semântica. Ele preserva a linha-filha, mas apaga a relação direta. Se a equipe depois precisar explicar qual conta criou um relatório, uma referência nula pode não bastar. Manter um token histórico não identificável ou um retrato pode preservar a responsabilização sem reter todos os dados pessoais, mas a escolha exata de retenção pertence à política do produto, não a uma suposição da IA.

Verifique a cardinalidade nas duas direções. Um gerador pode modelar um para um ao colocar uma chave estrangeira sem uma restrição única, permitindo silenciosamente várias linhas-filhas. Ele também pode impor unicidade quando o histórico exige várias versões. Escreva casos de teste para um pai com zero, um e vários filhos, e declare quais inserções devem passar.

Restrições adiáveis precisam de um motivo específico. Elas podem ajudar quando uma transação deve violar temporariamente a ordem de referência ou atualizar linhas mutuamente dependentes, mas adiar por padrão toda chave estrangeira desloca os erros para o commit e torna as falhas mais difíceis de localizar. Mantenha a imposição imediata, salvo quando uma sequência real de transação exigir adiamento.

Examine o catálogo depois de aplicar a migração ao banco de ensaio:

SELECT
    conname,
    contype,
    convalidated,
    pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'public.orders'::regclass
ORDER BY conname;

Uma saída representativa tem este formato:

       conname        | contype | convalidated | definition
----------------------+---------+--------------+---------------------------------------
 orders_pkey          | p       | t            | PRIMARY KEY (id)
 orders_customer_fk   | f       | t            | FOREIGN KEY (customer_id) REFERENCES customers(id)
 orders_total_check   | c       | t            | CHECK ((total_cents >= 0))

Compare as definições retornadas com as regras de propriedade aprovadas. O êxito da migração por si só não revela uma ação ausente, um adiamento inesperado ou uma restrição não validada.

Alterações destrutivas se escondem em SQL razoável

Examine o caminho dos dados
Exporte o código-fonte quando uma migração gerada precisar de uma revisão mais atenta de conversões e preenchimentos.

Revise a migração como uma transformação de dados, porque DDL aparentemente organizado pode descartar significado sem usar um DROP TABLE óbvio. Procure primeiro a destruição direta; depois examine conversões, preenchimentos, regravações, renomeações e substituição de restrições.

Uma renomeação e uma exclusão são operacionalmente diferentes, mesmo quando o esquema final parece idêntico. Se surname se torna family_name, uma renomeação preserva os dados e as dependências com mais fidelidade. Excluir a coluna antiga e adicionar a nova produz o mesmo diagrama, mas esvazia todos os valores. Migrações geradas frequentemente deduzem o estado final sem compreender a continuidade.

Alterações de tipo precisam de conversões de exemplo e casos de rejeição. Transformar identificadores de texto em inteiros pode remover zeros à esquerda ou rejeitar identificadores mistos. Reduzir a precisão numérica pode arredondar valores. Converter timestamps requer uma premissa explícita sobre fuso horário. Teste a expressão USING real com valores mínimo, máximo, nulo, malformado e historicamente incomum antes de alterar a coluna.

Trate estas operações como exigindo justificativa escrita: excluir uma tabela ou coluna, mudar um tipo por uma conversão com perda, substituir uma coluna preenchida, adicionar CASCADE, definir NOT NULL após um preenchimento gerado e reconstruir a unicidade com colunas diferentes. Examine também SQL bruto embutido em funções geradas ou callbacks de migração. A busca no texto é um filtro inicial, não a revisão inteira.

Um padrão de falha aparece repetidamente. Os dados conhecidos contêm contatos com empresas opcionais, mas a tela de exemplo mostra apenas contatos empresariais. O gerador torna contacts.company_id NOT NULL e insere uma empresa gerada chamada Unknown para linhas sem correspondência. A migração passa, as contagens conferem e toda chave estrangeira é validada. Os dados continuam errados: contatos individuais agora parecem pertencer a uma empresa, relatórios agrupam pessoas sem relação e excluir o espaço reservado pode se propagar para contatos reais.

O reparo não é outro padrão. Restaure o estado de origem, torne a relação anulável, migre apenas correspondências apoiadas por evidência e adicione uma verificação de que o conjunto sem correspondência é igual aos contatos individuais conhecidos. Por isso, casos de teste semânticos precisam registrar relações esperadas, não apenas contagens esperadas de linhas.

Ferramentas de diff de esquema ajudam, mas sou contra aprovar apenas com base em um diff. A recomendação é popular porque um diff é compacto e fácil de revisar. Ele não deve ser o único bloqueio porque mostra mudança estrutural, não a origem dos valores preenchidos, os limites de transação, o comportamento de bloqueio ou a verdade após a migração.

Um ensaio precisa provar resultados e comportamento de falha

Execute a migração completa sobre uma restauração descartável do conjunto de dados conhecido e teste tanto o resultado pretendido quanto um caminho interrompido ou rejeitado. Um banco novo e vazio ajuda a encontrar erros de ordenação, mas não pode revelar conversão com perda, linhas históricas inválidas ou validação lenta.

Use esta sequência de ensaio como artefato de lançamento:

  1. Restaure o conjunto de dados anterior à alteração em um banco isolado e registre contagens de linhas e verificações semânticas.
  2. Capture o esquema atual, aplique o artefato exato da migração e salve toda saída com tempos e limites de transação.
  3. Execute verificações de catálogo, verificações de mapeamento, testes de rejeição de restrições e consultas representativas da aplicação.
  4. Compare valores importantes com as expectativas registradas, incluindo categorias sem correspondência e nulas.
  5. Execute o método de recuperação documentado e repita as verificações anteriores à migração no banco recuperado.

Capture um dump somente de esquema antes e depois:

pg_dump --schema-only --no-owner --no-privileges \
  --dbname "$DATABASE_URL" > schema.sql

Revise tabelas, sequências, índices, restrições, funções, gatilhos, extensões e privilégios relevantes para a aplicação. Um diff de modelo ORM pode omitir objetos do banco que a aplicação não modela, sobretudo gatilhos, índices de expressão, índices parciais e funções instaladas manualmente.

Adicione testes negativos que comprovem que as restrições rejeitam estados inválidos. Uma transação de teste pode tentar uma inserção inválida e fazer rollback independentemente do resultado:

BEGIN;

INSERT INTO order_items (order_id, quantity, unit_price_cents)
VALUES (1001, 0, 2500);

ROLLBACK;

A saída esperada deve indicar a restrição violada, por exemplo:

ERROR:  new row for relation "order_items" violates check constraint "order_items_quantity_check"
DETAIL:  Failing row contains (..., 0, 2500, ...).

Não compare o texto inteiro do erro em todos os ambientes, porque os detalhes podem variar. Em testes automatizados, verifique o SQLSTATE ou a identidade da restrição e mantenha uma saída legível para o revisor.

Meça bloqueios e duração em um conjunto de dados grande o bastante para se parecer com a implantação pretendida. Uma operação que termina instantaneamente em cinquenta linhas pode bloquear gravações ao validar milhões. Em um banco inicial de produção vazio, o risco imediato é menor, mas o ensaio ainda testa dados iniciais importados e cria uma referência para alterações futuras.

A recuperação precisa de mais do que uma migração reversa

Crie a partir de regras revisadas
Transforme regras de domínio aprovadas em uma aplicação web, de servidor ou móvel pelo chat.

A recuperação só é confiável quando restaura os dados e a compatibilidade da aplicação dentro do tempo que o serviço pode tolerar. Uma migração reversa que recria colunas excluídas não recupera seus valores anteriores.

Escolha a unidade de recuperação antes da execução. Para um banco inicial vazio, excluir e recriar o banco pode ser aceitável se nenhuma gravação de usuário tiver começado. Quando já existem gravações reais, a recuperação pode exigir um snapshot do banco, um backup lógico, colunas antigas mantidas ou um reparo posterior. O método certo depende de quantos dados novos podem chegar durante e depois da migração.

Teste os comandos de restauração e as credenciais antes de confiar neles. Um backup que existe, mas não pode ser restaurado pelo operador de implantação, não é um plano de recuperação. Restaure em um banco separado, verifique propriedade e extensões e execute as mesmas verificações conhecidas usadas antes da migração.

Snapshots e rollback de transação resolvem falhas diferentes. Uma transação pode desfazer instruções quando uma migração falha antes do commit, desde que toda operação participe dessa transação. Um snapshot pode retornar o banco inteiro a um estado anterior, mas isso pode descartar gravações legítimas feitas depois dele. Nenhum dos dois mecanismos reconcilia automaticamente essas gravações.

Prefira alterações aditivas quando houver incerteza. Adicione a nova coluna ou tabela, copie os dados com regras mensuráveis, use os dois caminhos de código por um período controlado se necessário e remova a estrutura antiga somente após verificar. Essa abordagem de expandir e contrair exige trabalho extra, mas preserva evidências. Manter uma coluna antiga renomeada por uma versão costuma custar menos do que reconstruí-la a partir de logs.

Escreva antecipadamente os gatilhos de recuperação. Exemplos incluem qualquer verificação semântica que falhe, registros inesperados sem correspondência, uma restrição inválida, uma migração que exceda sua janela aprovada de bloqueio ou erros da aplicação causados por incompatibilidade de versão. O operador não deve inventar a decisão enquanto os usuários esperam.

Registre o ponto depois do qual restaurar o banco antigo também exige restaurar a aplicação antiga. Uma aplicação nova pode depender de uma coluna nova, enquanto uma aplicação antiga pode rejeitar um novo valor de enum ou gravar o formato antigo. A recuperação do banco e da aplicação precisa usar versões compatíveis.

A aprovação exige evidência, não confiança

Aprove a primeira migração somente quando outra pessoa puder reproduzir, a partir dos artefatos armazenados, por que ela é segura. A confiança gerada por uma revisão de código limpa ou por uma interface gerada bem acabada não resiste à primeira discrepância de dados sem explicação.

O registro de aprovação deve conter as premissas deduzidas, a matriz de mapeamento das tabelas, a identidade do conjunto de dados conhecido, o diff do esquema, a migração exata, consultas e resultados de validação, testes de entradas rejeitadas, justificativas dos índices, justificativas de operações destrutivas e o procedimento de recuperação testado. Identifique o revisor e preserve decisões pendentes como bloqueios, em vez de enterrá-las no histórico do chat.

Quando uma aplicação for gerada no Koder.ai, use o modo de planejamento para registrar essas decisões de esquema antes de permitir a migração, exporte o código-fonte para revisão e trate snapshots e rollback como ferramentas de recuperação que ainda precisam de um ensaio com o conjunto de dados conhecido.

Não deixe o gerador aprovar a própria inferência regenerando código até os testes passarem. Esse ciclo pode fazer a aplicação se adaptar a um esquema errado em vez de corrigir o modelo. Uma pessoa precisa decidir se o banco corresponde ao domínio, sobretudo em identidade, exclusão, retenção e valores desconhecidos.

A consulta final de aprovação deve ser simples. Cada fato conhecido corresponde a um resultado esperado, cada restrição rejeita o contraexemplo pretendido, toda operação destrutiva tem um motivo e a restauração reproduz as verificações anteriores à migração. Se a evidência precisar de uma explicação persuasiva para justificar uma divergência, interrompa a migração. O PostgreSQL aplicará o esquema com precisão, inclusive as partes que o gerador adivinhou errado.

Perguntas frequentes

O que devo verificar em um esquema PostgreSQL gerado por IA?

Verifique o DDL gerado, as operações da migração e as premissas por trás dos dois. Um esquema final correto ainda pode ser alcançado por uma migração que apaga dados, bloqueia gravações ou inventa padrões enganosos.

Qual deve ser o tamanho de um conjunto de dados para verificar o esquema?

Use um conjunto pequeno de dados que contenha linhas comuns, valores-limite, relações ausentes, duplicatas, nulos, strings vazias e casos históricos incomuns. O objetivo não é volume, e sim refutar as premissas do gerador antes que os dados de produção façam isso.

Uma migração de teste bem-sucedida prova que o esquema é seguro?

Não. Uma migração bem-sucedida prova que o PostgreSQL aceitou as instruções naquele estado do banco. Ela não prova que o mapeamento das tabelas está correto, que os dados mantiveram seu significado, que os índices atendem às consultas reais ou que a recuperação funciona.

Quando uma coluna PostgreSQL deve ser NOT NULL?

Um campo deve ser NOT NULL somente se todo registro legítimo tiver um valor e se a aplicação puder fornecer esse valor em todos os caminhos de gravação. Não use um valor padrão inventado apenas para cumprir a restrição, pois isso troca dados ausentes visíveis por dados falsos e plausíveis.

Devo usar uma restrição única ou um índice único?

Uma restrição única expressa uma regra que outros objetos do banco podem referenciar, e o PostgreSQL a sustenta com um índice. Um índice único é útil quando a unicidade se aplica apenas a linhas ou expressões selecionadas, como registros não excluídos ou endereços de email normalizados.

As chaves estrangeiras do PostgreSQL criam índices automaticamente?

Indexe colunas usadas para localizar linhas-pai, filtrar consultas comuns, unir tabelas grandes ou impor unicidade. O PostgreSQL não cria automaticamente um índice no lado referenciador de uma chave estrangeira, então examine as colunas-filhas separadamente em vez de supor que a restrição já cuidou disso.

Quando ON DELETE CASCADE é seguro?

Use CASCADE somente quando a linha-filha não tiver significado independente depois que sua linha-pai desaparecer. Se a exclusão for uma decisão de negócio ou a filha for uma evidência, como uma fatura ou registro de auditoria, rejeite ou processe explicitamente a exclusão.

Como detectar alterações destrutivas em uma migração?

Trate todo DROP, conversão com perda, regravação de tabela, nova coluna obrigatória e restrição substituída como potencialmente destrutivos. Pesquise o texto da migração, mas também examine funções geradas e SQL bruto, pois o comportamento destrutivo pode estar escondido neles.

Qual é a forma mais segura de testar a recuperação de uma migração?

Restaure o banco anterior à alteração em um local separado, execute a migração nele, rode consultas de verificação semântica e compare os resultados com as expectativas registradas. Testar apenas a migração reversa não recupera dados apagados e pode dar uma falsa sensação de segurança.

Que evidências devo guardar depois de aprovar um esquema?

Mantenha juntos o DDL gerado, o texto da migração, o diff do esquema, as consultas e resultados de verificação, o procedimento de recuperação e a identidade do revisor. Esse registro explica o que foi aprovado e ajuda a próxima revisão de migração a detectar premissas que mudaram.

Related posts