"""LibraryAuditRun — async transparency / single_color audit lifecycle. State machine: running → ready → applied / cancelled / error. matched_ids JSONB is appended-to by scan_library_for_rule; apply_audit_run reads it and routes through cleanup_service.delete_images. """ from datetime import datetime from typing import Any from sqlalchemy import DateTime, Integer, String, Text, func from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import Mapped, mapped_column from .base import Base class LibraryAuditRun(Base): __tablename__ = "library_audit_run" id: Mapped[int] = mapped_column(Integer, primary_key=True) rule: Mapped[str] = mapped_column(String(32), nullable=False, index=True) params: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False) status: Mapped[str] = mapped_column( String(16), nullable=False, default="running", index=True, ) # running | ready | applied | cancelled | error started_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), ) finished_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, ) scanned_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) matched_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) matched_ids: Mapped[list[int]] = mapped_column(JSONB, nullable=False, default=list) error: Mapped[str | None] = mapped_column(Text, nullable=True) # Chunked-scan state (alembic 0039): keyset cursor the next chunk resumes # from, and the last time a chunk made progress (so the recovery sweep can # tell a progressing multi-chunk audit from a stuck one). resume_after_id: Mapped[int] = mapped_column(Integer, nullable=False, default=0) last_progress_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True, )