Model/docs/migrations/recommendation-plan-id.md
Khalim Conn-Kowlessar 772cdd4f5a docs(modelling): #1157 Plan-persistence design review
Outcome of the /grill-with-docs session scoping #1157.

- CONTEXT.md: add **Plan Measure** (the persisted selected Option +
  role-3 attribution + cost); Recommendation stays the candidate.
  Remove Scenario Phase / Plan Phase / Rolled-over Options — multi-phase
  is deferred. Reshape Scenario + Plan to single-phase; fix relationships,
  dialogue, and the "phase" ambiguity note.
- ADR-0005: rewritten to Deferred (multi-phase was speculative
  prospective-client work; single-phase now; future plan_phase back-fill
  path preserved). Stray phase refs cleaned in ADR-0016 / ADR-0009.
- ADR-0017 (new): Plan persistence — reuse the live plan/recommendation
  tables via SQLModel mirrors + a PlanRepository on the UoW; add
  recommendation.plan_id, retire the plan_recommendations m2m; flat
  post-retrofit on plan; idempotent replace; CO2 in tonnes. Unselected
  alternatives + bills noted as deferred directions.
- docs/migrations/recommendation-plan-id.md: the FE-owned Drizzle change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:12:54 +00:00

2 KiB

recommendation.plan_id — FE-owned migration

Context: #1157 of the Modelling-stage rebuild. The ModellingOrchestrator persists a Plan and its selected Plan Measures (rows of the live recommendation table). To link a measure to its Plan it adds recommendation.plan_id, replacing the plan_recommendations many-to-many join for new writes (the m2m's cascade delete is pathologically slow — see ADR-0017).

The SQLModel mirror is defined in infrastructure/postgres/ so the ephemeral-Postgres tests build it via SQLModel.metadata.create_all. The production migration is FE-owned (Drizzle ORM).

Change

Add one column to the existing recommendation table:

Column Type Notes
plan_id bigint, FK → plan.id, ON DELETE CASCADE, indexed the Plan this measure belongs to. Nullable during transition (legacy rows predate it); new writes always set it.
  • Index plan_id — the orchestrator's idempotent replace deletes a Plan and relies on the cascade to remove its measures; reads fetch a Plan's measures by plan_id.
  • ON DELETE CASCADE is what makes "delete the Plan → its measures go too" a single statement, replacing the m2m cleanup.

Transition / sequencing

  1. Add plan_id (nullable) — this migration. New ModellingOrchestrator writes populate it; legacy writers and existing rows are unaffected.
  2. Cut legacy readers off plan_recommendations onto plan_id (separate work, not in #1157).
  3. Drop plan_recommendations once no reader remains (separate migration).

Existing live recommendation rows keep plan_id = NULL until/unless re-modelled; they remain reachable via the legacy plan_recommendations join during the transition.

Not changed here

No new columns for contingency (per-measure contingency stays summed into plan.contingency_cost, matching legacy), no phase column (multi-phase deferred, ADR-0005), and the energy/bill columns are populated by a later Bill Derivation slice (ADR-0017).