From b97d06882ff96d88119fe1455aeb37ad27083d3a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 3 Jun 2026 21:13:00 +0000 Subject: [PATCH] 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 --- .../db/functions/recommendations_functions.py | 53 +------------------ backend/app/db/models/recommendations.py | 6 +-- backend/export/tests/test_export.py | 29 ++-------- infrastructure/postgres/modelling/__init__.py | 2 - .../modelling/recommendation_table.py | 19 ------- 5 files changed, 10 insertions(+), 99 deletions(-) diff --git a/backend/app/db/functions/recommendations_functions.py b/backend/app/db/functions/recommendations_functions.py index 72affd2a..79168d71 100644 --- a/backend/app/db/functions/recommendations_functions.py +++ b/backend/app/db/functions/recommendations_functions.py @@ -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 # -------------------------------------------------- diff --git a/backend/app/db/models/recommendations.py b/backend/app/db/models/recommendations.py index 653e7051..8fad1ff3 100644 --- a/backend/app/db/models/recommendations.py +++ b/backend/app/db/models/recommendations.py @@ -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 diff --git a/backend/export/tests/test_export.py b/backend/export/tests/test_export.py index 973364fd..9d84fa4a 100644 --- a/backend/export/tests/test_export.py +++ b/backend/export/tests/test_export.py @@ -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) diff --git a/infrastructure/postgres/modelling/__init__.py b/infrastructure/postgres/modelling/__init__.py index c1c8cb8c..6b882b25 100644 --- a/infrastructure/postgres/modelling/__init__.py +++ b/infrastructure/postgres/modelling/__init__.py @@ -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", ] diff --git a/infrastructure/postgres/modelling/recommendation_table.py b/infrastructure/postgres/modelling/recommendation_table.py index c50a2947..67b327a3 100644 --- a/infrastructure/postgres/modelling/recommendation_table.py +++ b/infrastructure/postgres/modelling/recommendation_table.py @@ -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 - ) - )