Skip to content

feat(quality): schema drift guard ORM↔DB + fix 16 latent drifts (FDD-OPS-001 L5)#7

Merged
nascimentolimaandre-cloud merged 1 commit intomainfrom
feat/ops-001-l5-orm-schema-guard
Apr 29, 2026
Merged

feat(quality): schema drift guard ORM↔DB + fix 16 latent drifts (FDD-OPS-001 L5)#7
nascimentolimaandre-cloud merged 1 commit intomainfrom
feat/ops-001-l5-orm-schema-guard

Conversation

@nascimentolimaandre-cloud
Copy link
Copy Markdown
Owner

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.py usa Alembic autogenerate diff para comparar ORM Base.metadata vs 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_column
  • add_table / remove_table
  • modify_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:

Tabela Coluna ausente Tipo
eng_pull_requests url TEXT
eng_pull_requests closed_at TIMESTAMPTZ
eng_issues url TEXT
eng_issues priority VARCHAR(50)
eng_issues linked_pr_ids JSONB
eng_deployments url TEXT
eng_deployments trigger_type VARCHAR(100)
eng_deployments trigger_ref VARCHAR(500)

Categoria 2: type mismatches com risco de INSERT failure

ORM declarava size MAIOR que DB → INSERT de valor boundary-length falharia:

Campo ORM (antes) DB ORM (depois)
eng_pull_requests.author 256 255 255
eng_pull_requests.external_id 512 500 500
eng_sprints.external_id 512 500 500
eng_sprints.name 256 255 255

Categoria 3: type mismatches cosméticos (ORM stricter)

ORM declarava size MENOR que DB — não causa erro mas sinaliza desalinhamento:

Campo Antes Depois
eng_issues.issue_type 64 100
eng_issues.status 128 100
eng_issues.normalized_status 32 50
eng_issues.project_key 128 100
eng_pull_requests.state/source/repo 32/32/512 50/50/255
eng_sprints.source/board_id 32/128 50/500
eng_deployments.source/environment/sha/author 32/64/512/256 50/100/512/256
metrics_snapshots.metric_type/metric_name 64/128 50/100

Todos fixes são ORM-only annotations — nenhum DB change/migration necessária.

Como o guard distingue ruído de drift real

REAL_BUG_OPS = {
    "add_column", "remove_column",      # Schema drift INC-023#4 class
    "add_table", "remove_table",        # Orphan tables
    "modify_type",                      # VARCHAR size mismatch
}

Filtrados (ruído):

  • add_index/remove_index: ORM raramente nomeia indices igual a migrations; runtime queries não dependem disso
  • modify_comment: cosméticos, ORM não carrega COMMENT
  • modify_nullable: drifts cosméticos entre TenantModel base e migration columns
  • modify_default: complicações com compare_server_default

Postgres GENERATED columns (lead_time_hours, cycle_time_hours): physical em DB + column_property em ORM. Fórmulas equivalentes; allowlisted explicitamente.

Tabelas owned por outras layers (pulse-api TypeORM ou raw SQL):

  • IAM: users, memberships, tenants, iam_*, teams, organizations
  • Integration: connections, integration_connections
  • Jira discovery: tenant_jira_config, jira_project_catalog, jira_discovery_audit

CI integration

Teste lê 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 é 1 linha.

Padrões pedagógicos registrados

Adiciona ao 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 (não tem sintoma uniforme observável).

Stats

  • 3 arquivos, +410 / -23 linhas
  • 1 teste novo (drift guard)
  • 167/167 regression verde (24 progress_tracker + 142 anteriores + 1 novo)
  • 16 drifts latentes corrigidos

Test plan

  • CI roda 4 gates verdes
  • cd packages/pulse-data && pytest tests/integration/test_orm_schema_drift_guard.py → PASS
  • Adicionar coluna nova ao DB via migration sem updates ao ORM → guard quebra com mensagem precisa
  • Mesmo drift inverso (Mapped novo, sem migration) → guard quebra
  • Mudança de String(50)→String(100) em ORM sem migration correspondente → guard quebra

Dependencies

🤖 Generated with Claude Code

…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>
@nascimentolimaandre-cloud nascimentolimaandre-cloud merged commit f3fa27e into main Apr 29, 2026
4 checks passed
@nascimentolimaandre-cloud nascimentolimaandre-cloud deleted the feat/ops-001-l5-orm-schema-guard branch April 29, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant