"""PostAttachment — a non-art file preserved from a post. Art images become ImageRecords; everything else a post contained (archives, .exe, .pdf, ...) is captured here so nothing is lost. post_id is nullable (set only when an adjacent sidecar yields a Post); artist_id mirrors the canonical attribution model (FC-2d-vii-c). Both FKs are SET NULL so deleting a Post/Artist never deletes the preserved binary row. """ from datetime import datetime from sqlalchemy import ( BigInteger, DateTime, ForeignKey, Index, Integer, String, Text, func, text, ) from sqlalchemy.orm import Mapped, mapped_column from .base import Base class PostAttachment(Base): __tablename__ = "post_attachment" # Dedup is PER-POST, not global (2026-06-08): the same non-art file attached # to many posts gets one row per post over a single sha-addressed blob, so no # post is left a bare shell. Partial uniques: (post_id, sha256) for real posts; # (sha256) alone for the NULL-post filesystem case (one row per file there). __table_args__ = ( Index( "uq_post_attachment_post_sha", "post_id", "sha256", unique=True, postgresql_where=text("post_id IS NOT NULL"), ), Index( "uq_post_attachment_null_post_sha", "sha256", unique=True, postgresql_where=text("post_id IS NULL"), ), ) id: Mapped[int] = mapped_column(Integer, primary_key=True) post_id: Mapped[int | None] = mapped_column( ForeignKey("post.id", ondelete="SET NULL"), nullable=True, index=True ) artist_id: Mapped[int | None] = mapped_column( ForeignKey("artist.id", ondelete="SET NULL"), nullable=True, index=True ) sha256: Mapped[str] = mapped_column( String(64), nullable=False, index=True ) path: Mapped[str] = mapped_column(Text, nullable=False) original_filename: Mapped[str] = mapped_column(Text, nullable=False) ext: Mapped[str] = mapped_column(String(32), nullable=False) mime: Mapped[str | None] = mapped_column(String(128), nullable=True) size_bytes: Mapped[int] = mapped_column(BigInteger, nullable=False) captured_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() )