"""SQLAlchemy ORM models for the API persistence layer. Typed with SQLAlchemy 2.0 ``Mapped`` annotations so mypy (via the ``sqlalchemy.ext.mypy.plugin``) can infer attribute types instead of ``Column[...]``. """ from __future__ import annotations import uuid from datetime import datetime, timezone from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column def _utcnow() -> datetime: return datetime.now(timezone.utc) class Base(DeclarativeBase): pass class Review(Base): __tablename__ = "reviews" id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) text: Mapped[str] = mapped_column(Text, nullable=False) language: Mapped[str] = mapped_column(String(10), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) processing_time_ms: Mapped[float] = mapped_column(Float, nullable=False) class AspectResult(Base): __tablename__ = "aspect_results" id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) review_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("reviews.id"), nullable=False) aspect: Mapped[str] = mapped_column(String(255), nullable=False) sentiment: Mapped[str] = mapped_column(String(50), nullable=False) confidence: Mapped[float] = mapped_column(Float, nullable=False) start_pos: Mapped[int] = mapped_column(Integer, nullable=False) end_pos: Mapped[int] = mapped_column(Integer, nullable=False) class BatchJob(Base): __tablename__ = "batch_jobs" id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) status: Mapped[str] = mapped_column(String(50), nullable=False, default="queued") total: Mapped[int] = mapped_column(Integer, nullable=False) processed: Mapped[int] = mapped_column(Integer, nullable=False, default=0) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)