Files
photoanalyzer/photo_pipeline/models/renames.py

85 lines
3.8 KiB
Python

"""Rename plan and per-operation journal persistence (US04-01, US04-02).
A plan is the complete, reviewable preview of a set of folder renames: every source,
destination, affected asset, expected hash, and validation issue. Nothing here
touches the filesystem — applying a plan is US04-03.
The journal columns on ``RenameOperation`` belong to US04-02's crash-safe state
machine and are created with the table so the journal never has to re-migrate the
rows the planner writes. They stay at their defaults until apply runs.
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import (
Boolean,
DateTime,
ForeignKey,
Integer,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column
from photo_pipeline.db import Base
class RenamePlan(Base):
__tablename__ = "rename_plans"
id: Mapped[str] = mapped_column(String, primary_key=True)
# planned | validated | invalid | applying | applied | failed | rolled_back
state: Mapped[str] = mapped_column(String, nullable=False, default="planned")
schema_version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
checksum: Mapped[str | None] = mapped_column(String)
blockers: Mapped[str | None] = mapped_column(String) # JSON array
operation_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
validated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
applied_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
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 RenameOperation(Base):
__tablename__ = "rename_operations"
__table_args__ = (
UniqueConstraint("plan_id", "sequence", name="uq_rename_operations_plan_sequence"),
)
id: Mapped[str] = mapped_column(String, primary_key=True)
plan_id: Mapped[str] = mapped_column(
ForeignKey("rename_plans.id", ondelete="CASCADE"), nullable=False, index=True
)
sequence: Mapped[int] = mapped_column(Integer, nullable=False)
operation: Mapped[str] = mapped_column(String, nullable=False)
album: Mapped[str | None] = mapped_column(String)
source_path: Mapped[str] = mapped_column(String, nullable=False)
destination_path: Mapped[str] = mapped_column(String, nullable=False)
asset_ids: Mapped[str | None] = mapped_column(String) # JSON array
expected_sha256: Mapped[str | None] = mapped_column(String) # JSON {asset_id: sha}
asset_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
same_filesystem: Mapped[bool | None] = mapped_column(Boolean)
case_only: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
issues: Mapped[str | None] = mapped_column(String) # JSON array
# ── journal (US04-02) ────────────────────────────────────────────────────
journal_state: Mapped[str] = mapped_column(String, nullable=False, default="planned")
attempt_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
fencing_token: Mapped[int | None] = mapped_column(Integer)
worker_id: Mapped[str | None] = mapped_column(String)
verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
error_code: Mapped[str | None] = mapped_column(String)
error_message: Mapped[str | None] = mapped_column(String)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)