Banco e contratos
Persistência de ponta a ponta
Seção intitulada “Persistência de ponta a ponta”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.
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.
Índices declarados
Seção intitulada “Índices declarados”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).
Restrições declaradas
Seção intitulada “Restrições declaradas”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.
Tabelas escritas à mão
Seção intitulada “Tabelas escritas à mão”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.
Compare sem escrever
Seção intitulada “Compare sem escrever”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.
Gere e revise a API
Seção intitulada “Gere e revise a API”dotnet builder contract export --from-specsdotnet builder contract lint --input .builder/openapi.jsondotnet builder contract hoppscotchdotnet builder contract diff --input .builder/openapi.json --base /caminho/openapi-anterior.jsonO 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.
Identidade numérica e GUID público
Seção intitulada “Identidade numérica e GUID público”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.
GUID público em varchar(50) (ideapiStorage)
Seção intitulada “GUID público em varchar(50) (ideapiStorage)”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 NULLcom o mesmo índice único. A aplicação grava o GUID (Guid.CreateVersion7()) no formato canônicoDminúsculo, o mesmo deuuidv7()::varchar. - O C# continua com
Guidem domínio, DTOs, rotas,object_ideITenantContext. O EF converte a propriedade (UseSilNumericIdentity(…, SilIdeapiStorage.Varchar)); a view_apiprojeta<prefixo>_ideapi::uuid AS <prefixo>_ide::uuidnas referências e na organização varchar. - Toda resolução GUID → id compara a coluna com o texto canônico (
ideapi = @guidcom o valor em texto), nuncaideapi::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>_ideapie<prefixo>_orgid_ideapi, via.TextIdeapi(…)da listagem). Filtros por camporeferencecomparam o::uuidda 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 obtrim). - A auditoria continua com
record_ideorganization_idemuuid, gravados a partir doGuidde domínio, não do texto da coluna. - Uma entidade
key: uuidnão referencia um destino varchar: a FK seriauuid→varchar(BLD5019). Converta os consumidores parabigint-identityantes, mantendo o destino emuuid, e só depois troque o destino paravarchar.
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.
Organização externa (somente leitura)
Seção intitulada “Organização externa (somente leitura)”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 }externalexigekey: bigint-identity,tenant: none,audit: none,softDelete: falseecolumnem todo campo;api,rules,acceptance,uniqueereferencesão recusados (BLD5012).- As entidades com
organization: Organizationrecebem a FKREFERENCES base.organizacao (ger_org_id), e a view_apifaz oJOINporger_org_id/ger_org_ideapi. Um camporeferencepara a entidade externa continua recusado (BLD5019). ideapiStorage: varchardescreve oger_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 comouuide 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 (emvarchar, o texto canônico). Odb verifynã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));dotnet builder db verify --connection "$SIL_BUILDER_TEST_POSTGRES" --baseline db/baseline.sqlSem esse baseline, a migration da primeira entidade com tenant falha em BLD6003, porque a FK aponta para uma tabela que não existe.