Files
TieMeasureFlow/src/backend/models/orm/vision.py
T
Adriano 54bc384650 fix(vision): task_id di vision_results/vision_reference_images in CASCADE (#38.6)
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>
2026-09-21 22:44:55 +02:00

72 lines
3.2 KiB
Python

"""One row per vision execution - not per quote."""
from datetime import datetime
from typing import Optional
from sqlalchemy import DateTime, Enum, ForeignKey, Integer, JSON, String, func
from sqlalchemy.orm import Mapped, mapped_column
from src.backend.database import Base
class VisionResult(Base):
__tablename__ = "vision_results"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
task_id: Mapped[int] = mapped_column(
Integer, ForeignKey("recipe_tasks.id", ondelete="CASCADE"),
nullable=False, index=True,
)
image_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
overlay_path: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
engine_version: Mapped[str] = mapped_column(String(64), nullable=False)
executed_on: Mapped[str] = mapped_column(
Enum("server", "station", name="vision_executed_on_enum"), nullable=False
)
station_id: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("stations.id"), nullable=True
)
device_code: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
# Copied, not referenced: a calibration redone six months from now must not
# rewrite what a measurement already taken means. Same for the graph.
calibration_snapshot: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
graph_snapshot: Mapped[dict] = mapped_column(JSON, nullable=False)
duration_ms: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
executed_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.now()
)
__table_args__ = ({"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"},)
def __repr__(self) -> str:
return f"<VisionResult task={self.task_id} engine={self.engine_version}>"
class VisionReferenceImage(Base):
"""An image kept so a graph can be composed, tried and re-tried without a
camera - which is exactly what VisionSuite's source abstraction is for."""
__tablename__ = "vision_reference_images"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
task_id: Mapped[int] = mapped_column(
Integer, ForeignKey("recipe_tasks.id", ondelete="CASCADE"),
nullable=False, index=True,
)
path: Mapped[str] = mapped_column(String(500), nullable=False)
station_id: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("stations.id"), nullable=True
)
device_code: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
calibration_snapshot: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
engine_version: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
note: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
expected_json: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
acquired_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, server_default=func.now()
)
__table_args__ = ({"mysql_engine": "InnoDB", "mysql_charset": "utf8mb4"},)
def __repr__(self) -> str:
return f"<VisionReferenceImage task={self.task_id} path={self.path}>"