54bc384650
Migrazione 014 (batch_alter_table, nome del vincolo esistente letto con sa.inspect invece di indovinato) e stessi ondelete nei modelli ORM. Cancellare un task con esecuzioni o immagini di riferimento non fallisce più su un vincolo FK MySQL. MANCA: i file delle immagini di riferimento su disco non vengono rimossi alla cancellazione del task - tasks.py::delete_task (NON TOCCARE da questo lotto) cancella con `db.delete(task)` diretto, senza un punto unico in vision_service da agganciare. `_copy_tasks_to_version` (recipe_service.py) non copia le immagini di riferimento nel copy-on-write: nemmeno questo si tocca qui. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
77 lines
3.0 KiB
Python
77 lines
3.0 KiB
Python
"""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 `<table>_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'],
|
|
)
|