Pular para o conteúdo

Banco e contratos

Runtime.Data implementa escrita EF Core, leitura Dapper, tenant, soft delete, unidade de trabalho e auditoria log. A spec alimenta entidades, mapeamentos, queries e migrations.

Campos json/jsonb usam os tipos nativos do PostgreSQL e JsonElement em C#. EF grava o valor estruturado; Dapper lê e a API devolve JSON sem codificá-lo como texto. A importação do banco mantém o tipo original e os defaults estruturados. Veja a declaração na spec.

Janela do terminal
dotnet builder db verify --connection "$SIL_BUILDER_TEST_POSTGRES"

Use uma instância descartável com permissão CREATEDB. O comando cria um banco temporário, executa cada migration duas vezes e valida schema e queries com PREPARE/EXPLAIN. --baseline arquivo.sql é opcional. Isso não certifica joins com duas organizações nem o ambiente de produção.

Um índice único criado à mão numa migration, sem declaração na spec, é BLD6013: sua violação não tem erro gerado. Declare-o em builder.entity.indexes (formato). O spec apply grava CREATE UNIQUE INDEX IF NOT EXISTS … WHERE … na migration da tarefa e no snapshot. O db verify confere colunas e unicidade (BLD6013) e o predicado (BLD6014), ignorando os casts e os parênteses que o PostgreSQL acrescenta; o IN da spec equivale ao = ANY (ARRAY[…]) que o PostgreSQL devolve. Mudar ou remover um índice aplicado exige migration revisada (BLD2002).

Um CHECK ou EXCLUDE criado à mão numa migration, sem declaração na spec, é BLD6012 (“not in the spec”): sua violação não tem erro gerado. Declare-o em builder.entity.constraints (formato). O spec apply grava na migration da tarefa um bloco DO por restrição, que só a cria se ela não existe, e CREATE EXTENSION IF NOT EXISTS btree_gist antes de um exclude; o snapshot guarda a declaração. O db verify e o db diff leem o check e a definição que o pg_get_constraintdef devolve com o mesmo parser e comparam as duas árvores: os casts, os parênteses redundantes e a caixa que o PostgreSQL acrescenta não contam, mas o agrupamento conta, e reagrupar os termos é BLD6012. O exclude segue a comparação textual. Mudar ou remover uma restrição aplicada exige migration revisada (BLD2002). A extensão btree_gist precisa estar disponível no servidor; desde o PostgreSQL 13 ela é confiável e o dono do banco a cria sem superusuário.

Uma tabela que nenhuma spec gera (log particionado, lote com limite de itens) entra no db verify por um snapshot em .builder/schema/<schema>/<tabela>.json, no formato dos snapshots gerados. Além dos campos de coluna e índice, ele aceita:

{ "name": "lot_total", "type": "integer", "nullable": false, "checkExpression": "lot_total >= 1 AND lot_total <= 500" }
"primaryKey": ["lgi_id", "lgi_incdat"],
"partitionBy": { "type": "range", "column": "lgi_incdat" },

checkExpression é o SQL de um CHECK de intervalo ou comparação, escrito como o PostgreSQL o imprime (BETWEEN vira >= AND <=); parênteses, casts e caixa não contam. check continua sendo a lista de valores de um enum, e os dois juntos são recusados. primaryKey lista as colunas da chave na ordem do banco e partitionBy.type é range, list ou hash; o PostgreSQL exige a coluna da partição na chave. As partições (log_integracao_202610) são filhas da tabela declarada: não aparecem como tabelas órfãs (BLD6006) nem têm constraints avaliadas. Um snapshot que o builder não consegue ler é BLD6020, com arquivo, linha e motivo, em vez de deixar a tabela sem declaração.

Janela do terminal
dotnet builder db diff features/dock/2026-10-03-dock-docas --connection "$SIL_BUILDER_TEST_POSTGRES"
dotnet builder spec lint --db "$SIL_BUILDER_TEST_POSTGRES"

A comparação lê o catálogo em transação somente leitura. Aplicar migrations em produção segue o processo de banco existente; o builder não tem db apply ou db status.

Janela do terminal
dotnet builder contract export --from-specs
dotnet builder contract lint --input .builder/openapi.json
dotnet builder contract hoppscotch
dotnet builder contract diff --input .builder/openapi.json --base /caminho/openapi-anterior.json

O OpenAPI nasce do modelo tipado das specs. O spec apply também atualiza os contratos derivados. BLD4002 detecta artefatos velhos; BLD4003–4005 apontam colisões, spec ausente e artefatos sem produtor.

As permissões geram constantes, .builder/permissions.json e uma proposta de seed fora de .migrations. A autorização real é configurada pelo consumidor. Códigos de erro simbólicos aguardam a integração com o registro Shipyard.

Declare builder.entity.key: bigint-identity para gerar <prefixo>_id bigint generated always as identity e <prefixo>_ideapi uuid com índice único. O modo omitido continua uuid, preservando consumidores existentes. Na entidade de organização, use tenant: none; nas entidades com tenant, organization: Organization aponta para essa feature global do mesmo módulo, também numérica.

O EF grava PKs e FKs numéricas. Os repositórios resolvem o GUID da organização e das referências; as views geradas <tabela>_api projetam os GUIDs para as consultas Dapper. ITenantContext, rotas, DTOs e object_id continuam usando GUIDs. As referências numéricas exigem destinos numéricos aplicados primeiro.

Na plataforma, *_ideapi é um código e código é varchar(50) (PTB-99): ger_org_ideapi é varchar(50) NOT NULL DEFAULT uuidv7()::varchar. Declare builder.entity.ideapiStorage: varchar (padrão uuid) para seguir esse padrão. Só vale com key: bigint-identity (BLD5012); numa entidade externa descreve a coluna existente.

  • A tabela gerada recebe <prefixo>_ideapi varchar(50) NOT NULL com o mesmo índice único. A aplicação grava o GUID (Guid.CreateVersion7()) no formato canônico D minúsculo, o mesmo de uuidv7()::varchar.
  • O C# continua com Guid em domínio, DTOs, rotas, object_id e ITenantContext. O EF converte a propriedade (UseSilNumericIdentity(…, SilIdeapiStorage.Varchar)); a view _api projeta <prefixo>_ideapi::uuid AS <prefixo>_id e ::uuid nas referências e na organização varchar.
  • Toda resolução GUID → id compara a coluna com o texto canônico (ideapi = @guid com o valor em texto), nunca ideapi::uuid = @guid, para usar o índice: repositórios, tenant e referências no EF, e a busca por id e o filtro de tenant do Dapper, que usam as colunas de texto que a view acrescenta no fim (<prefixo>_ideapi e <prefixo>_orgid_ideapi, via .TextIdeapi(…) da listagem). Filtros por campo reference comparam o ::uuid da view, sem índice no destino.
  • Leitura tolerante, busca exata: o valor lido aceita maiúsculas e espaços nas pontas (o char(36) antigo tinha preenchimento), mas a busca só encontra o texto canônico. Normalize valores legados antes (UPDATE … SET x_ideapi = lower(btrim(x_ideapi)); a PTB-99 já fez o btrim).
  • A auditoria continua com record_id e organization_id em uuid, gravados a partir do Guid de domínio, não do texto da coluna.
  • Uma entidade key: uuid não referencia um destino varchar: a FK seria uuid → varchar (BLD5019). Converta os consumidores para bigint-identity antes, mantendo o destino em uuid, e só depois troque o destino para varchar.

Para converter um banco UUID, altere apenas a identidade, começando pela organização e pelas entidades referenciadas. spec sync mostra o delta; spec apply com task/autor/link grava a migration. Ela preserva os UUIDs em ideapi, cria as identidades, traduz vínculos e recria PKs/FKs/índices. A conversão falha sem alterar dados se algum vínculo não puder ser resolvido. Revise e valide com db verify antes de seguir o processo de banco do consumidor.

Com ideapiStorage: varchar na spec convertida, a coluna renomeada vira varchar(50) (USING <coluna>::text, os GUIDs no formato canônico), e as referências e a organização varchar são traduzidas comparando o texto do destino. Uma entidade já numérica com uuid passa a varchar sozinha no passo (ALTER COLUMN … TYPE varchar(50) USING …::text): a migration remove e recria views simples que leem a coluna com (coluna)::uuid, preservando o tipo das colunas. Depois reaplique os consumidores (spec sync aponta a view deles); a volta para uuid exige migration revisada.

Quando o alvo migra antes de um consumidor que ainda tem chave uuid (por exemplo equipe antes de equipe_membro), a FK do consumidor sobre o GUID público não pode voltar, porque o PostgreSQL não liga uuid a varchar(50). A migration do alvo remove essa FK e avisa com um NOTICE; a conversão do consumidor resolve o vínculo pelo texto do GUID, recusa órfãos e cria a FK numérica para o id interno. Migre o consumidor na mesma entrega para não deixar o vínculo sem FK.

A conversão automática para varchar verifica todas as views afetadas antes do primeiro DROP. Ela recusa views com outro proprietário, grants (inclusive por coluna), defaults de coluna, opções, comentários, regras, triggers, rótulos de segurança ou views dependentes, além de views materializadas. Também recusa recriação sob privilégios padrão customizados, que poderiam acrescentar permissões novas. Isso vale para a _api gerada: uma customização no banco não pode desaparecer na regeneração. Nesses casos, prepare uma migration revisada que preserve metadados e dependências; não remova as permissões para contornar a recusa. O db verify só enxerga os objetos presentes nas migrations e no baseline usado; inclua as customizações reais nessa validação.

UseSilNumericIdentity(builder, column, apiColumn, primaryKey) continua disponível com a assinatura original e armazena o GUID em uuid. O overload com SilIdeapiStorage seleciona o armazenamento explicitamente, preservando chamadas de consumidores já compilados contra a assinatura anterior.

A auditoria passa a ter PK numérica e GUID do evento em ideapi. record_id e organization_id preservam os GUIDs públicos; record_internal_id e organization_internal_id guardam as chaves internas. O delta preenche o histórico de registros ainda existentes; registros já removidos fisicamente mantêm o GUID, com chave interna nula. A reversão de identidade exige uma migration revisada.

Quando a organização é da plataforma, como base.organizacao (PK ger_org_id, GUID público ger_org_ideapi), declare-a como entidade externa. O builder gera só o mapeamento EF de leitura e nunca cria DDL, migration, view, auditoria, handler ou endpoint para ela.

builder:
schema: 1
module: ForcaVendas
entity: { name: Organization, plural: Organizations, table: base.organizacao, prefix: org, tenant: none, key: bigint-identity,
ideapiStorage: varchar, softDelete: false, audit: none, external: { id: ger_org_id, ideapi: ger_org_ideapi },
label: { singular: organização, plural: organizações, gender: f } }
fields:
- { name: Name, json: nome, type: string, max: 100, column: ger_org_nome }
  • external exige key: bigint-identity, tenant: none, audit: none, softDelete: false e column em todo campo; api, rules, acceptance, unique e reference são recusados (BLD5012).
  • As entidades com organization: Organization recebem a FK REFERENCES base.organizacao (ger_org_id), e a view _api faz o JOIN por ger_org_id/ger_org_ideapi. Um campo reference para a entidade externa continua recusado (BLD5019).
  • ideapiStorage: varchar descreve o ger_org_ideapi varchar(50) da plataforma: o EF converte o GUID e a resolução do tenant compara o texto canônico. Sem ela, a coluna é tratada como uuid e o PostgreSQL recusa a comparação (operator does not exist: character varying = uuid).
  • O estado aplicado fica em .builder/external/<módulo>/<entidade>.json, fora de .builder/schema. O runtime recusa gravar a entidade (UseSilReadOnly).
  • Os testes gerados criam a tabela mínima quando ela não existe, com o tipo de ideapiStorage, e incluem a organização pelas colunas externas (em varchar, o texto canônico). O db verify não cria a tabela; passe um baseline com ela:
CREATE SCHEMA base;
CREATE TABLE base.organizacao (
ger_org_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
ger_org_ideapi varchar(50) NOT NULL UNIQUE,
ger_org_nome varchar(100)
);
Janela do terminal
dotnet builder db verify --connection "$SIL_BUILDER_TEST_POSTGRES" --baseline db/baseline.sql

Sem esse baseline, a migration da primeira entidade com tenant falha em BLD6003, porque a FK aponta para uma tabela que não existe.