# SQLModel Table Models These models implement the canonical [Version 2 database schema](../schema_v2.md). Each schema entity is represented by exactly one `SQLModel` table class. Because `SQLModel` is built on Pydantic and SQLAlchemy, these classes provide application validation and PostgreSQL mappings without parallel row and create models. Database-generated UUIDs and timestamps are `None` until PostgreSQL supplies their values during insert. The database columns remain non-nullable. `Person.metadata_` maps to the `metadata` column because `metadata` is reserved by SQLAlchemy's declarative API. ```python from datetime import date from datetime import datetime from enum import StrEnum from uuid import UUID from pydantic import JsonValue from sqlalchemy import Column from sqlalchemy import Date from sqlalchemy import DateTime from sqlalchemy import ForeignKey from sqlalchemy import Index from sqlalchemy import Integer from sqlalchemy import String from sqlalchemy import Text from sqlalchemy import UniqueConstraint from sqlalchemy import text from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import UUID as PostgreSQLUUID from sqlmodel import Field from sqlmodel import Relationship from sqlmodel import SQLModel class PersonRole(StrEnum): AUTHOR = "author" RECIPIENT = "recipient" class JobStatus(StrEnum): QUEUED = "queued" PROCESSING = "processing" COMPLETED = "completed" PARTIAL_SUCCESS = "partial_success" FAILED = "failed" class JobSourceStatus(StrEnum): PENDING = "pending" TRANSCRIBED = "transcribed" FAILED = "failed" class Person(SQLModel, table=True): __tablename__ = "person" __table_args__ = (Index("idx_person_full_name", "full_name"),) id: UUID | None = Field( default=None, sa_column=Column( PostgreSQLUUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), ), ) full_name: str = Field(sa_column=Column(Text, nullable=False)) display_name: str | None = Field(default=None, sa_column=Column(Text)) maiden_name: str | None = Field(default=None, sa_column=Column(Text)) birth_date: date | None = Field(default=None, sa_column=Column(Date)) birth_date_raw: str | None = Field(default=None, sa_column=Column(Text)) birth_place: str | None = Field(default=None, sa_column=Column(Text)) death_date: date | None = Field(default=None, sa_column=Column(Date)) death_date_raw: str | None = Field(default=None, sa_column=Column(Text)) death_place: str | None = Field(default=None, sa_column=Column(Text)) biography: str | None = Field(default=None, sa_column=Column(Text)) portrait_path: str | None = Field(default=None, sa_column=Column(Text)) metadata_: JsonValue | None = Field( default_factory=dict, sa_column=Column( "metadata", JSONB, server_default=text("'{}'::jsonb"), ), ) created_at: datetime | None = Field( default=None, sa_column=Column( DateTime(timezone=True), nullable=False, server_default=text("now()"), ), ) updated_at: datetime | None = Field( default=None, sa_column=Column( DateTime(timezone=True), nullable=False, server_default=text("now()"), ), ) document_people: list["DocumentPerson"] = Relationship( back_populates="person", sa_relationship_kwargs={"lazy": "raise", "passive_deletes": True}, ) class Document(SQLModel, table=True): __tablename__ = "document" __table_args__ = (Index("idx_document_date", "document_date"),) id: UUID | None = Field( default=None, sa_column=Column( PostgreSQLUUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), ), ) name: str = Field(sa_column=Column(Text, nullable=False)) document_type: str | None = Field(default=None, sa_column=Column(Text)) document_date: date | None = Field(default=None, sa_column=Column(Date)) document_date_raw: str | None = Field(default=None, sa_column=Column(Text)) location_created: str | None = Field(default=None, sa_column=Column(Text)) notes: str | None = Field(default=None, sa_column=Column(Text)) archive_identifier: str | None = Field(default=None, sa_column=Column(Text)) created_at: datetime | None = Field( default=None, sa_column=Column( DateTime(timezone=True), nullable=False, server_default=text("now()"), ), ) updated_at: datetime | None = Field( default=None, sa_column=Column( DateTime(timezone=True), nullable=False, server_default=text("now()"), ), ) document_people: list["DocumentPerson"] = Relationship( back_populates="document", sa_relationship_kwargs={"lazy": "raise", "passive_deletes": True}, ) jobs: list["Job"] = Relationship( back_populates="document", sa_relationship_kwargs={"lazy": "raise", "passive_deletes": True}, ) sources: list["Source"] = Relationship( back_populates="document", sa_relationship_kwargs={"lazy": "raise", "passive_deletes": True}, ) class DocumentPerson(SQLModel, table=True): __tablename__ = "document_person" __table_args__ = ( UniqueConstraint( "document_id", "person_id", "role", name="unique_document_person_role", ), Index("idx_document_person_doc", "document_id"), Index("idx_document_person_per", "person_id"), ) id: UUID | None = Field( default=None, sa_column=Column( PostgreSQLUUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), ), ) document_id: UUID = Field( sa_column=Column( PostgreSQLUUID(as_uuid=True), ForeignKey("document.id", ondelete="CASCADE"), nullable=False, ), ) person_id: UUID = Field( sa_column=Column( PostgreSQLUUID(as_uuid=True), ForeignKey("person.id", ondelete="CASCADE"), nullable=False, ), ) role: PersonRole = Field(sa_column=Column(String(20), nullable=False)) created_at: datetime | None = Field( default=None, sa_column=Column( DateTime(timezone=True), nullable=False, server_default=text("now()"), ), ) document: Document | None = Relationship( back_populates="document_people", sa_relationship_kwargs={"lazy": "raise"}, ) person: Person | None = Relationship( back_populates="document_people", sa_relationship_kwargs={"lazy": "raise"}, ) class Job(SQLModel, table=True): __tablename__ = "job" __table_args__ = (Index("idx_job_document", "document_id"),) id: UUID | None = Field( default=None, sa_column=Column( PostgreSQLUUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), ), ) document_id: UUID = Field( sa_column=Column( PostgreSQLUUID(as_uuid=True), ForeignKey("document.id", ondelete="CASCADE"), nullable=False, ), ) status: JobStatus = Field( default=JobStatus.QUEUED, sa_column=Column( String(50), nullable=False, server_default=text("'queued'"), ), ) retry_count: int = Field( default=0, sa_column=Column( Integer, nullable=False, server_default=text("0"), ), ) provider: str = Field(sa_column=Column(Text, nullable=False)) model: str = Field(sa_column=Column(Text, nullable=False)) prompt_name: str | None = Field(default=None, sa_column=Column(Text)) date_created: datetime | None = Field( default=None, sa_column=Column( DateTime(timezone=True), nullable=False, server_default=text("now()"), ), ) date_updated: datetime | None = Field( default=None, sa_column=Column( DateTime(timezone=True), nullable=False, server_default=text("now()"), ), ) document: Document | None = Relationship( back_populates="jobs", sa_relationship_kwargs={"lazy": "raise"}, ) job_sources: list["JobSource"] = Relationship( back_populates="job", sa_relationship_kwargs={"lazy": "raise", "passive_deletes": True}, ) class Source(SQLModel, table=True): __tablename__ = "source" __table_args__ = ( Index("idx_source_document", "document_id"), Index("idx_source_page_order", "document_id", "page_number"), ) id: UUID | None = Field( default=None, sa_column=Column( PostgreSQLUUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), ), ) document_id: UUID = Field( sa_column=Column( PostgreSQLUUID(as_uuid=True), ForeignKey("document.id", ondelete="CASCADE"), nullable=False, ), ) page_number: int = Field( default=1, sa_column=Column( Integer, nullable=False, server_default=text("1"), ), ) upload_name: str = Field(sa_column=Column(Text, nullable=False)) filename: str = Field(sa_column=Column(Text, nullable=False)) file_path: str = Field(sa_column=Column(Text, nullable=False)) raw_transcription: str | None = Field(default=None, sa_column=Column(Text)) revised_text: str | None = Field(default=None, sa_column=Column(Text)) date_uploaded: datetime | None = Field( default=None, sa_column=Column( DateTime(timezone=True), nullable=False, server_default=text("now()"), ), ) date_revised: datetime | None = Field( default=None, sa_column=Column(DateTime(timezone=True)), ) document: Document | None = Relationship( back_populates="sources", sa_relationship_kwargs={"lazy": "raise"}, ) job_sources: list["JobSource"] = Relationship( back_populates="source", sa_relationship_kwargs={"lazy": "raise", "passive_deletes": True}, ) class JobSource(SQLModel, table=True): __tablename__ = "job_source" __table_args__ = ( UniqueConstraint("job_id", "source_id", name="unique_job_source"), Index("idx_job_source_job", "job_id"), Index("idx_job_source_source", "source_id"), Index( "idx_job_source_ai_metadata", "ai_metadata", postgresql_using="gin", ), ) id: UUID | None = Field( default=None, sa_column=Column( PostgreSQLUUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), ), ) job_id: UUID = Field( sa_column=Column( PostgreSQLUUID(as_uuid=True), ForeignKey("job.id", ondelete="CASCADE"), nullable=False, ), ) source_id: UUID = Field( sa_column=Column( PostgreSQLUUID(as_uuid=True), ForeignKey("source.id", ondelete="CASCADE"), nullable=False, ), ) status: JobSourceStatus = Field( default=JobSourceStatus.PENDING, sa_column=Column( String(50), nullable=False, server_default=text("'pending'"), ), ) raw_transcription: str | None = Field(default=None, sa_column=Column(Text)) ai_metadata: JsonValue | None = Field( default=None, sa_column=Column(JSONB), ) raw_api_response: JsonValue | None = Field( default=None, sa_column=Column(JSONB), ) error_detail: str | None = Field(default=None, sa_column=Column(Text)) executed_at: datetime | None = Field( default=None, sa_column=Column( DateTime(timezone=True), nullable=False, server_default=text("now()"), ), ) job: Job | None = Relationship( back_populates="job_sources", sa_relationship_kwargs={"lazy": "raise"}, ) source: Source | None = Relationship( back_populates="job_sources", sa_relationship_kwargs={"lazy": "raise"}, ) ``` The enum annotations validate application values while the mapped columns retain the `VARCHAR` types specified by the DDL. PostgreSQL owns generated UUIDs and timestamps through `server_default`; call `session.refresh(instance)` after a flush or commit when those generated values are needed immediately. `ai_metadata`, `raw_api_response`, and `metadata_` accept any JSON value supported by `JSONB`. Validate provider-specific payload structure before assigning it to these fields, while preserving the complete raw response in `raw_api_response`. Relationships use `lazy="raise"` to prevent implicit database I/O in async code. Queries must explicitly load relationships they need, for example with `selectinload()`. The schema's behavioral invariants are enforced outside the table shape where appropriate: - `PersonRole`, `JobStatus`, and `JobSourceStatus` define the exact values listed by the schema. - `unique_document_person_role` enforces role uniqueness for `(document_id, person_id, role)`. - Services order document sources by `Source.document_id` and `Source.page_number`. - Services derive aggregate `Job.status` from related `JobSource.status` values. - Services preserve `JobSource.raw_transcription` and `JobSource.raw_api_response` as point-in-time outputs while updating the active text on `Source`.