Model/docs/MIGRATION_NOTE_predicted_epc_source.md
Khalim Conn-Kowlessar fd43cf2d23 feat(epc-prediction): slice-5c predicted-EPC persistence slot
Add a `source` discriminator (lodged | predicted) to the EPC store so a Property
holds a lodged EPC and a predicted one (EPC Prediction gap-fill) at once
(ADR-0031). EpcRepository.save gains source="lodged"; idempotent delete is now
per-source (a predicted save no longer wipes lodged, and vice versa);
get_for_property/get_for_properties filter lodged; new get_predicted_for_property
/ get_predicted_for_properties read predicted. PropertyPostgresRepository.get +
get_many hydrate Property.predicted_epc, so the predicted picture reaches the
modelling read (both load via get_many). FakeEpcRepo mirrors the dual slot.

EpcPropertyModel gains `source` (default "lodged"); the test DB builds from the
SQLModel mirror so this is exercised without the prod migration. The matching
Drizzle change (column + per-(property_id,source) uniqueness) is the team's to
action before merge — docs/MIGRATION_NOTE_predicted_epc_source.md.

3 store tests (coexist, idempotent predicted re-save leaves lodged, lodged-only
has no predicted) + property-repo wiring; 85 pass across affected suites; new
code pyright-clean (2 pre-existing wwhrs errors in epc_property_table untouched).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 03:50:19 +00:00

51 lines
2.4 KiB
Markdown

# Migration note — `epc_property.source` (predicted-EPC slot)
**For the team to action before merging the EPC Prediction production-wiring
branch.** The model-side code is done and tested against the SQLModel-built test
DB; the **production Drizzle schema needs a matching column** that this repo does
not own.
## What changed in code (this branch)
Per **ADR-0031**, a Property can now hold a **lodged** EPC and a **predicted** EPC
(EPC Prediction gap-fill) at the same time. The two are distinguished by a new
`source` discriminator on the `epc_property` row:
- `infrastructure/postgres/epc_property_table.py``EpcPropertyModel` gains
`source: str = Field(default="lodged")`.
- `repositories/epc/epc_postgres_repository.py``save(..., source="lodged")`
writes it; `_delete_for_property` is now per-source (idempotency no longer wipes
the other slot); `get_for_property` / `get_for_properties` filter `source =
'lodged'`; new `get_predicted_for_property` / `get_predicted_for_properties`
read `source = 'predicted'`.
The test database is built from the SQLModel mirrors via `create_all`, so tests
already exercise the column. **Production is not** — hence this note.
## Required Drizzle migration
On the `epc_property` table:
1. **Add column** `source``text` (or your enum), **NOT NULL**, **default
`'lodged'`**. The default backfills every existing row as a real EPC, which is
correct (all current rows are lodged).
2. **Relax any single-row-per-property uniqueness.** If a unique constraint /
index exists on `epc_property(property_id)`, it must become
**`(property_id, source)`** — a property may now have one `lodged` row and one
`predicted` row. (Verify whether such a constraint exists; the SQLModel mirror
has none, but the production schema may.)
3. **Recommended index** `(property_id, source)` — every predicted/lodged read
filters on both columns.
## Allowed values
`source ∈ {'lodged', 'predicted'}` (see `EpcSource` in
`repositories/epc/epc_repository.py`). No other values are written.
## Why
ADR-0031: predicted EPCs are stored in their own slot rather than overwriting the
lodged `epc`, so (a) provenance is structural — the Validation Cohort excludes
predicted-sourced Properties and the UI flags them — and (b) lodged + predicted
coexist, which the planned **EPC Anomaly Flags** feature needs (compare a
Property's lodged EPC against its predicted one).