feat(modelling): drop the plan_recommendations m2m

Stop writing the m2m (remove create_plan_recommendations + its call, the bulk
link insert and the now-dead plan_ids_by_index, and the plan_recommendations
delete in delete_property_batch) and remove the PlanRecommendationRow model +
its shim alias and the test_export fixture inserts. Measures now link to their
Plan solely via recommendation.plan_id (writers set it, readers join on it).

The live drop of the plan_recommendations table is the FE-owned Drizzle
migration documented in docs/migrations/recommendation-plan-id.md, sequenced
after the read-cut + backfill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 21:13:00 +00:00
parent af5dbe325d
commit b97d06882f
5 changed files with 10 additions and 99 deletions

View file

@ -17,7 +17,6 @@ from backend.app.db.models.recommendations import (
PlanModel,
Recommendation,
RecommendationMaterials,
PlanRecommendations,
ScenarioModel,
)
from backend.app.db.models.portfolio import PropertyModel
@ -236,23 +235,6 @@ def create_recommendation_material(
return new_recommendation_material.id
def create_plan_recommendations(session: Session, plan_id, recommendation_ids):
"""
This function will create records for the plan_recommendation in the database.
:param session: The database session
:param plan_id: ID of the plan
:param recommendation_ids: list of recommendation IDs
"""
# Prepare a list of dictionaries for bulk insert
data = [
{"plan_id": plan_id, "recommendation_id": rid} for rid in recommendation_ids
]
# Bulk insert using SQLAlchemy's core API
session.execute(insert(PlanRecommendations).values(data))
def upload_recommendations(
session: Session, recommendations_to_upload, property_id, new_plan_id
):
@ -320,10 +302,6 @@ def upload_recommendations(
# flush the changes to get the newly created IDs
session.flush()
create_plan_recommendations(
session, plan_id=new_plan_id, recommendation_ids=uploaded_recommendation_ids
)
# Commit the transaction
session.commit()
@ -348,7 +326,6 @@ def bulk_upload_recommendations_and_materials(
# ---------------------------------------------------------
recommendation_rows = []
parts_by_index = []
plan_ids_by_index = []
for rec in recommendation_payload:
recommendation_rows.append(
@ -375,7 +352,6 @@ def bulk_upload_recommendations_and_materials(
)
parts_by_index.append(rec["parts"])
plan_ids_by_index.append(rec["plan_id"])
# ---------------------------------------------------------
# 2. Insert recommendations and get IDs
@ -407,18 +383,8 @@ def bulk_upload_recommendations_and_materials(
if materials_rows:
session.execute(insert(RecommendationMaterials).values(materials_rows))
# ---------------------------------------------------------
# 4. Insert plan ↔ recommendation links
# ---------------------------------------------------------
plan_recommendation_rows = [
{
"plan_id": plan_id,
"recommendation_id": recommendation_id,
}
for plan_id, recommendation_id in zip(plan_ids_by_index, recommendation_ids)
]
session.execute(insert(PlanRecommendations).values(plan_recommendation_rows))
# Recommendations carry their plan via recommendation.plan_id (set above) —
# the plan_recommendations m2m is retired (ADR-0017 amendment).
def chunked(iterable, size=100):
@ -457,21 +423,6 @@ def delete_property_batch(session: Session, property_ids: list[int]):
params,
)
# --------------------------------------------------
# plan_recommendations (via plan)
# --------------------------------------------------
session.execute(
text(
"""
DELETE FROM plan_recommendations pr
USING plan p
WHERE pr.plan_id = p.id
AND p.property_id = ANY(:property_ids)
"""
),
params,
)
# --------------------------------------------------
# funding_package_measures
# --------------------------------------------------

View file

@ -30,17 +30,17 @@ from backend.app.db.models.portfolio import Portfolio, PortfolioGoal, PropertyMo
from infrastructure.postgres.modelling import (
PlanRow,
PlanType,
PlanRecommendationRow,
RecommendationMaterialRow,
RecommendationRow,
)
# Legacy names → the single SQLModel definitions now in
# `infrastructure/postgres/modelling/`.
# `infrastructure/postgres/modelling/`. The `plan_recommendations` m2m is
# retired (ADR-0017 amendment) — measures link to their Plan via
# `recommendation.plan_id`.
Recommendation = RecommendationRow
RecommendationMaterials = RecommendationMaterialRow
PlanModel = PlanRow
PlanRecommendations = PlanRecommendationRow
PlanTypeEnum = PlanType

View file

@ -16,7 +16,6 @@ from backend.app.db.models.portfolio import (
from backend.app.db.models.recommendations import (
PlanModel,
Recommendation,
PlanRecommendations,
RecommendationMaterials,
)
from backend.app.db.models.materials import Material
@ -189,18 +188,9 @@ def test_default_export_integration(db_session):
db_session.bulk_save_objects(recs)
db_session.flush()
# ----------------------------------------
# 6) Insert PlanRecommendations
# ----------------------------------------
links = [
PlanRecommendations(
plan_id=row.plan_id,
recommendation_id=row.recommendation_id,
)
for row in plan_recs_df.itertuples(index=False)
]
db_session.bulk_save_objects(links)
# Recommendations are linked to their plan by recommendation.plan_id (set
# above from plan_recs_df) — the plan_recommendations m2m is retired
# (ADR-0017 amendment).
db_session.commit()
logger.info("Inserted all data in %.2f seconds", time.perf_counter() - db_load_t0)
@ -628,17 +618,8 @@ def test_solar_with_battery_example(db_session):
db_session.add(rec)
db_session.flush()
# -------------------------------------------------
# Link Plan -> Recommendation
# -------------------------------------------------
for row in plan_recs_df.itertuples(index=False):
db_session.add(
PlanRecommendations(
plan_id=row.plan_id,
recommendation_id=row.recommendation_id,
)
)
db_session.flush()
# Plan ↔ Recommendation link is recommendation.plan_id (set above) — the
# plan_recommendations m2m is retired (ADR-0017 amendment).
# -------------------------------------------------
# Insert Material (includes_battery=True)

View file

@ -10,7 +10,6 @@ One canonical SQLModel per physical table — `plan`, `recommendation`,
from infrastructure.postgres.modelling.plan_table import PlanRow, PlanType
from infrastructure.postgres.modelling.recommendation_table import (
PlanRecommendationRow,
RecommendationMaterialRow,
RecommendationRow,
)
@ -20,5 +19,4 @@ __all__ = [
"PlanType",
"RecommendationRow",
"RecommendationMaterialRow",
"PlanRecommendationRow",
]

View file

@ -118,22 +118,3 @@ class RecommendationMaterialRow(SQLModel, table=True):
),
)
estimated_cost: Optional[float] = Field(default=None)
class PlanRecommendationRow(SQLModel, table=True):
"""The legacy ``plan_recommendations`` m2m — **being retired** (ADR-0017
amendment). Kept as an intra-cluster SQLModel row only for the transition
window while readers/writers move onto ``recommendation.plan_id``; dropped
once no caller remains. Both FKs are intra-cluster."""
__tablename__: ClassVar[str] = "plan_recommendations" # pyright: ignore[reportIncompatibleVariableOverride]
id: Optional[int] = Field(default=None, primary_key=True)
plan_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("plan.id"), nullable=False)
)
recommendation_id: int = Field(
sa_column=Column(
BigInteger, ForeignKey("recommendation.id"), nullable=False
)
)