From 3b63285bd15bafc9c2e1f6380970e332ddca6733 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 21 Feb 2026 17:50:54 +0100 Subject: [PATCH] Fix FK violations when pruning stale routes Bulk delete bypasses ORM-level cascades, so manually delete route_encounters, nullify boss_battle.after_route_id, and skip routes referenced by user encounters before deleting stale routes. Co-Authored-By: Claude Opus 4.6 --- backend/src/app/seeds/loader.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/backend/src/app/seeds/loader.py b/backend/src/app/seeds/loader.py index efa1730..4b47db9 100644 --- a/backend/src/app/seeds/loader.py +++ b/backend/src/app/seeds/loader.py @@ -1,11 +1,12 @@ """Database upsert helpers for seed data.""" -from sqlalchemy import delete, select +from sqlalchemy import delete, select, update from sqlalchemy.dialects.postgresql import insert from sqlalchemy.ext.asyncio import AsyncSession from app.models.boss_battle import BossBattle from app.models.boss_pokemon import BossPokemon +from app.models.encounter import Encounter from app.models.evolution import Evolution from app.models.game import Game from app.models.pokemon import Pokemon @@ -195,17 +196,33 @@ async def upsert_routes( for child in route.get("children", []): seed_names.add(child["name"]) - pruned = await session.execute( - delete(Route) - .where( + # Find stale route IDs, excluding routes with user encounters + in_use_subq = select(Encounter.route_id).distinct().subquery() + stale_route_ids_result = await session.execute( + select(Route.id).where( Route.version_group_id == version_group_id, Route.name.not_in(seed_names), + Route.id.not_in(select(in_use_subq)), ) - .returning(Route.id) ) - pruned_count = len(pruned.all()) - if pruned_count: - print(f" Pruned {pruned_count} stale route(s)") + stale_route_ids = [row.id for row in stale_route_ids_result] + + if stale_route_ids: + # Delete encounters referencing stale routes (no DB-level cascade) + await session.execute( + delete(RouteEncounter).where( + RouteEncounter.route_id.in_(stale_route_ids) + ) + ) + # Nullify boss battle references to stale routes + await session.execute( + update(BossBattle) + .where(BossBattle.after_route_id.in_(stale_route_ids)) + .values(after_route_id=None) + ) + # Now safe to delete the routes + await session.execute(delete(Route).where(Route.id.in_(stale_route_ids))) + print(f" Pruned {len(stale_route_ids)} stale route(s)") await session.flush()