"""Durable product-resource schema: projects, simulations, and reports. These replace the legacy filesystem-backed ProjectManager / SimulationManager / ReportManager payloads with tenant- and owner-scoped SQL rows, so product state survives restarts and multiple workers without cross-tenant leakage. """ from __future__ import annotations from datetime import datetime, timezone from enum import Enum from uuid import uuid4 from sqlalchemy import ( DateTime, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint, func, text, ) from sqlalchemy.orm import Mapped, mapped_column from ..db import Base def _id(prefix: str) -> str: return f"{prefix}_{uuid4().hex}" def _utc_now() -> datetime: return datetime.now(timezone.utc) class ProjectStatus(str, Enum): CREATED = "created" ONTOLOGY_GENERATED = "ontology_generated" GRAPH_BUILDING = "graph_building" GRAPH_COMPLETED = "graph_completed" FAILED = "failed" class SimulationStatus(str, Enum): CREATED = "created" PREPARING = "preparing" READY = "ready" RUNNING = "running" COMPLETED = "completed" FAILED = "failed" CANCELLED = "cancelled" class ReportStatus(str, Enum): DRAFT = "draft" PLANNING = "planning" GENERATING = "generating" COMPLETED = "completed" FAILED = "failed" class ProductProject(Base): """Durable, tenant-owned project record (metadata + ontology).""" __tablename__ = "projects" __table_args__ = ( Index("ix_projects_org_owner", "organization_id", "owner_user_id"), Index("ix_projects_org_created", "organization_id", "created_at"), ) id: Mapped[str] = mapped_column( String(128), primary_key=True, default=lambda: _id("project") ) organization_id: Mapped[str] = mapped_column( ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True ) owner_user_id: Mapped[str | None] = mapped_column( ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True ) name: Mapped[str] = mapped_column(String(255), nullable=False, server_default=text("''")) status: Mapped[str] = mapped_column( String(32), nullable=False, default=ProjectStatus.CREATED.value, server_default=text("'created'") ) language: Mapped[str] = mapped_column( String(16), nullable=False, default="en", server_default=text("'en'") ) total_text_length: Mapped[int] = mapped_column( Integer, nullable=False, default=0, server_default=text("0") ) source_metadata: Mapped[dict | list | None] = mapped_column(JSON, nullable=True) ontology: Mapped[dict | list | None] = mapped_column(JSON, nullable=True) analysis_summary: Mapped[str | None] = mapped_column(Text, nullable=True) simulation_requirement: Mapped[str | None] = mapped_column(Text, nullable=True) graph_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True) graph_build_task_id: Mapped[str | None] = mapped_column(String(128), nullable=True) error: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=_utc_now, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=_utc_now, server_default=func.now(), onupdate=_utc_now ) class ProductSimulation(Base): """Durable, tenant-scoped simulation with a config snapshot.""" __tablename__ = "simulations" __table_args__ = ( Index("ix_simulations_org_project", "organization_id", "project_id"), Index("ix_simulations_org_created", "organization_id", "created_at"), ) id: Mapped[str] = mapped_column( String(128), primary_key=True, default=lambda: _id("sim") ) organization_id: Mapped[str] = mapped_column( ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True ) project_id: Mapped[str] = mapped_column( ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True ) created_by_user_id: Mapped[str | None] = mapped_column( ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) status: Mapped[str] = mapped_column( String(32), nullable=False, default=SimulationStatus.CREATED.value, server_default=text("'created'") ) platform: Mapped[str] = mapped_column( String(32), nullable=False, default="parallel", server_default=text("'parallel'") ) config: Mapped[dict | list | None] = mapped_column(JSON, nullable=True) current_round: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default=text("0")) error: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=_utc_now, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=_utc_now, server_default=func.now(), onupdate=_utc_now ) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) class DurableReport(Base): """Durable, tenant-scoped report with outline/status/content metadata.""" __tablename__ = "reports" __table_args__ = ( UniqueConstraint("organization_id", "id", name="uq_reports_org_id"), Index("ix_reports_org_project", "organization_id", "project_id"), Index("ix_reports_org_simulation", "organization_id", "simulation_id"), Index("ix_reports_org_created", "organization_id", "created_at"), ) id: Mapped[str] = mapped_column( String(128), primary_key=True, default=lambda: _id("report") ) organization_id: Mapped[str] = mapped_column( ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False, index=True ) project_id: Mapped[str] = mapped_column( ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True ) simulation_id: Mapped[str | None] = mapped_column( ForeignKey("simulations.id", ondelete="SET NULL"), nullable=True, index=True ) created_by_user_id: Mapped[str | None] = mapped_column( ForeignKey("users.id", ondelete="SET NULL"), nullable=True ) status: Mapped[str] = mapped_column( String(32), nullable=False, default=ReportStatus.DRAFT.value, server_default=text("'draft'") ) title: Mapped[str] = mapped_column(String(255), nullable=False, server_default=text("''")) outline: Mapped[dict | list | None] = mapped_column(JSON, nullable=True) markdown_content: Mapped[str | None] = mapped_column(Text, nullable=True) error: Mapped[str | None] = mapped_column(Text, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=_utc_now, server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=_utc_now, server_default=func.now(), onupdate=_utc_now ) finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)