feat(quality): schema drift guard ORM↔DB + fix 16 latent drifts (FDD-OPS-001 L5)#7
Merged
nascimentolimaandre-cloud merged 1 commit intomainfrom Apr 29, 2026
Conversation
…OPS-001 L5)
5ª linha de defesa do FDD-OPS-001 contra a classe de bug que causou
INC-023 (sprint 4-layer cheese): coluna existia no DB mas SQLAlchemy
não tinha `Mapped[]` correspondente. Path que omitia o campo
funcionava silently empty; path que tentava popular crashava com
"Unconsumed column names". Bug ficou meses oculto.
THE GUARD
`tests/integration/test_orm_schema_drift_guard.py` roda Alembic
autogenerate diff:
1. Conecta a um DB com migrations aplicadas (live dev DB ou CI fixture)
2. Compara ORM `Base.metadata` vs DB schema
3. Filtra cosmetic noise (indices nomeados, comments, nullability,
server defaults) — mantém apenas operações que causam BUG REAL
4. Filtra Postgres GENERATED columns mapped via `column_property`
(lead_time_hours/cycle_time_hours são equivalentes)
5. Filtra tabelas managed por outras layers (TypeORM/raw SQL)
Failure prints actionable diagnostics: qual coluna, qual table, e como fix.
DRIFT REAIS CORRIGIDOS NESTA PR
O guard (na primeira execução) achou **16 drifts reais** em modelos
existentes — todos ignorados por anos. Categoria 1: colunas no DB sem
ORM (INC-023#4 class):
- eng_pull_requests.url, .closed_at
- eng_issues.url, .priority, .linked_pr_ids
- eng_deployments.url, .trigger_type, .trigger_ref
Categoria 2: type mismatches que poderiam quebrar INSERT em valores
boundary-length (ORM declarava maior que DB → INSERT failure):
- eng_pull_requests.author (256→255), .external_id (512→500)
- eng_sprints.external_id (512→500), .name (256→255)
Categoria 3: type mismatches cosmeticos (ORM stricter que DB) alinhados:
- eng_issues.issue_type (64→100), .status (128→100), .normalized_status (32→50)
- eng_pull_requests.state (32→50), .source (32→50), .repo (512→255)
- eng_sprints.source (32→50), .board_id (128→500)
- eng_deployments.source (32→50), .environment (64→100), etc.
- eng_issues.project_key (128→100)
- metrics_snapshots.metric_type (64→50), .metric_name (128→100)
- eng_deployments.repo nullable=False→True (matches actual DB)
Todos fixes são **ORM-only annotations** — nenhum DB change/migration
necessária. Os campos existem como esperado no DB; ORM agora bate.
PADRÕES PEDAGÓGICOS
Adiciona à coleção do `ingestion-spec.md §7.D.6` (anti-patterns):
- "Schema drift entre migration e ORM" — coluna existe no DB mas
SQLAlchemy `Mapped[]` ausente → paths que omitem passam, paths que
incluem crashern → bug assimétrico difícil de diagnosticar
CI INTEGRATION
Test usa `PULSE_DRIFT_TEST_DATABASE_URL` env var (CI) ou
`settings.database_url` (dev). Read-only — não polui DB. Adicionar ao
existing CI quality gate é trivial (pytest tests/integration/test_orm_*).
TESTS
- 167/167 verde (24 progress_tracker + outros + 1 drift guard novo)
- O drift guard hoje: PASS quando ORM ≡ DB (estado atual)
- Quando alguém adicionar coluna no DB sem update ORM (ou vice-versa),
CI quebra com mensagem precisa indicando qual campo + como fix
DEFERIDO
- Documentar guard no docs/onboarding.md (próxima sessão)
- Atualizar ops-backlog.md FDD-OPS-001 com Linha 5 SHIPPED label
(sessão de docs de fechamento)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
5ª linha de defesa do FDD-OPS-001 contra a classe de bug que causou INC-023 (sprint 4-layer cheese): coluna existia no DB mas SQLAlchemy não tinha
Mapped[]correspondente. Path que omitia → silently empty. Path que tentava popular → crash. Bug ficou MESES oculto.The guard
tests/integration/test_orm_schema_drift_guard.pyusa Alembic autogenerate diff para comparar ORMBase.metadatavs DB schema. Filtra ruído cosmético (indices nomeados, comments, nullability, server defaults) e Postgres GENERATED columns. Falha apenas em drifts reais que causam silent bugs:add_column/remove_columnadd_table/remove_tablemodify_type(VARCHAR(50) vs VARCHAR(100), etc.)Mensagem de falha cita arquivo, coluna e action item ("add
Mapped[...]to model" ou "create migration").🔴 Drifts reais que o guard achou (e esta PR corrige)
Na primeira execução do guard, 16 drifts foram detectados em modelos existentes — todos invisíveis por anos.
Categoria 1: colunas no DB sem
Mapped(INC-023#4 class)Estes são bugs latentes — código que tentasse usá-los crasharia, código que ignorasse passaria silenciosamente:
eng_pull_requestsurleng_pull_requestsclosed_ateng_issuesurleng_issuespriorityeng_issueslinked_pr_idseng_deploymentsurleng_deploymentstrigger_typeeng_deploymentstrigger_refCategoria 2: type mismatches com risco de INSERT failure
ORM declarava size MAIOR que DB → INSERT de valor boundary-length falharia:
eng_pull_requests.authoreng_pull_requests.external_ideng_sprints.external_ideng_sprints.nameCategoria 3: type mismatches cosméticos (ORM stricter)
ORM declarava size MENOR que DB — não causa erro mas sinaliza desalinhamento:
eng_issues.issue_typeeng_issues.statuseng_issues.normalized_statuseng_issues.project_keyeng_pull_requests.state/source/repoeng_sprints.source/board_ideng_deployments.source/environment/sha/authormetrics_snapshots.metric_type/metric_nameTodos fixes são ORM-only annotations — nenhum DB change/migration necessária.
Como o guard distingue ruído de drift real
Filtrados (ruído):
add_index/remove_index: ORM raramente nomeia indices igual a migrations; runtime queries não dependem dissomodify_comment: cosméticos, ORM não carrega COMMENTmodify_nullable: drifts cosméticos entre TenantModel base e migration columnsmodify_default: complicações comcompare_server_defaultPostgres GENERATED columns (
lead_time_hours,cycle_time_hours): physical em DB +column_propertyem ORM. Fórmulas equivalentes; allowlisted explicitamente.Tabelas owned por outras layers (pulse-api TypeORM ou raw SQL):
users,memberships,tenants,iam_*,teams,organizationsconnections,integration_connectionstenant_jira_config,jira_project_catalog,jira_discovery_auditCI integration
Teste lê
PULSE_DRIFT_TEST_DATABASE_URLenv var (CI) ousettings.database_url(dev). Read-only — não polui DB. Adicionar ao existing CI quality gate é 1 linha.Padrões pedagógicos registrados
Adiciona ao
ingestion-spec.md §7.D.6anti-patterns:Stats
Test plan
cd packages/pulse-data && pytest tests/integration/test_orm_schema_drift_guard.py→ PASSDependencies
🤖 Generated with Claude Code