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>
2.4 KiB
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—EpcPropertyModelgainssource: str = Field(default="lodged").repositories/epc/epc_postgres_repository.py—save(..., source="lodged")writes it;_delete_for_propertyis now per-source (idempotency no longer wipes the other slot);get_for_property/get_for_propertiesfiltersource = 'lodged'; newget_predicted_for_property/get_predicted_for_propertiesreadsource = '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:
- 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). - 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 onelodgedrow and onepredictedrow. (Verify whether such a constraint exists; the SQLModel mirror has none, but the production schema may.) - 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).