"""vision results and reference images cascade with their task Both `vision_results.task_id` and `vision_reference_images.task_id` were created (012, 013) with a plain, unnamed foreign key - no ON DELETE clause, so deleting a task with any camera history or reference image attached fails on a strict FK check (MySQL) instead of taking the vision rows with it, the way `production_events.run_id` already does for a closed production run. The original constraint has no name to drop by (SQLAlchemy leaves inline `ForeignKey(...)` unnamed unless told otherwise, and MySQL then assigns its own `_ibfk_N`, which is not portable to reflect on). `sa.inspect` looks it up at migration time instead of guessing it - the same way on MySQL and on SQLite (batch mode), where an anonymous FK reflects with `name=None` and there is nothing to drop before adding the new one. Reference images on disk are not touched here - deleting a task's row does not, on its own, know to also delete the file next to it; that gap is declared open in issue #38 point 6's write-up (`vision_service` has no single point today that intercepts a task deletion). Revision ID: 014_vision_fk_ondelete Revises: 013_reference_images Create Date: 2026-09-21 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = '014_vision_fk_ondelete' down_revision: Union[str, None] = '013_reference_images' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None _NEW_FK_NAMES = { 'vision_results': 'fk_vision_results_task_id', 'vision_reference_images': 'fk_vision_reference_images_task_id', } def _existing_task_id_fk_name(bind, table_name: str) -> Union[str, None]: """The name reflection gives the pre-existing `task_id` FK, if any. None on SQLite for a constraint that was never named - batch mode then has nothing to drop, and just adds the new named one alongside it. """ inspector = sa.inspect(bind) for fk in inspector.get_foreign_keys(table_name): if fk.get("constrained_columns") == ["task_id"]: return fk.get("name") return None def upgrade() -> None: bind = op.get_bind() for table_name, new_name in _NEW_FK_NAMES.items(): existing_name = _existing_task_id_fk_name(bind, table_name) with op.batch_alter_table(table_name) as batch: if existing_name: batch.drop_constraint(existing_name, type_='foreignkey') batch.create_foreign_key( new_name, 'recipe_tasks', ['task_id'], ['id'], ondelete='CASCADE', ) def downgrade() -> None: # Reverse order: reference_images before results, mirroring upgrade(). for table_name, name in reversed(list(_NEW_FK_NAMES.items())): with op.batch_alter_table(table_name) as batch: batch.drop_constraint(name, type_='foreignkey') batch.create_foreign_key( name, 'recipe_tasks', ['task_id'], ['id'], )