"""Stable asset identity and path history. A file path is mutable metadata, never identity: ``assets.id`` is a generated UUID that never changes across moves, renames, or archival. ``asset_paths`` records every path an asset has occupied. ``state_version`` supports optimistic concurrency; ``created_at``/``updated_at`` are the audit timestamps. Later-stage columns (canonical link, safety score, archive location) are added by the migrations of the stories that own them, not speculatively here. """ from __future__ import annotations from datetime import datetime from sqlalchemy import DateTime, ForeignKey, Integer, String, func from sqlalchemy.orm import Mapped, mapped_column from photo_pipeline.db import Base class Asset(Base): __tablename__ = "assets" id: Mapped[str] = mapped_column(String, primary_key=True) original_path: Mapped[str] = mapped_column(String, nullable=False) current_path: Mapped[str | None] = mapped_column(String, unique=True) current_sha256: Mapped[str | None] = mapped_column(String) pixel_sha256: Mapped[str | None] = mapped_column(String) phash: Mapped[str | None] = mapped_column(String) hash_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) phash_version: Mapped[int | None] = mapped_column(Integer) byte_size: Mapped[int | None] = mapped_column(Integer) discovered_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) missing_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) availability_state: Mapped[str] = mapped_column(String, nullable=False, default="active") # Duplicate canonical link: NULL when the asset is itself canonical or undecided. canonical_asset_id: Mapped[str | None] = mapped_column(ForeignKey("assets.id")) state_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() ) class AssetPath(Base): __tablename__ = "asset_paths" asset_id: Mapped[str] = mapped_column( ForeignKey("assets.id"), primary_key=True ) path: Mapped[str] = mapped_column(String, primary_key=True) valid_from: Mapped[datetime] = mapped_column( DateTime(timezone=True), primary_key=True ) valid_until: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) reason: Mapped[str | None] = mapped_column(String)