""" SQLAlchemy ORM models matching the Supabase PostgreSQL database schema. This file serves as the single source of truth for the database schema, used by SQLAlchemy for query ORM mappings and Alembic for migration version control. """ import uuid from datetime import datetime, timezone from typing import Any, List, Optional from sqlalchemy import ( ARRAY, Boolean, Column, DateTime, Float, ForeignKey, Integer, String, Text, func, text, ) from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship try: from pgvector.sqlalchemy import Vector except ImportError: # Fallback type if pgvector package is not available from sqlalchemy.types import UserDefinedType class Vector(UserDefinedType): # type: ignore def __init__(self, dim: Optional[int] = None, *args, **kwargs): self.dim = dim def get_col_spec(self, **kw): return f"VECTOR({self.dim})" if self.dim else "VECTOR" class Base(DeclarativeBase): """Base class for all SQLAlchemy ORM models.""" pass class Profile(Base): __tablename__ = "profiles" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, server_default=text("gen_random_uuid()") ) email: Mapped[Optional[str]] = mapped_column(String, nullable=True) display_name: Mapped[Optional[str]] = mapped_column(String, nullable=True) avatar_url: Mapped[Optional[str]] = mapped_column(String, nullable=True) daily_requests: Mapped[int] = mapped_column(Integer, default=0, server_default=text("0")) last_request_date: Mapped[Optional[str]] = mapped_column(String, nullable=True) theme: Mapped[str] = mapped_column(String, default="system", server_default=text("'system'")) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) # Relationships materials: Mapped[List["Material"]] = relationship("Material", back_populates="user", cascade="all, delete-orphan") quizzes: Mapped[List["Quiz"]] = relationship("Quiz", back_populates="user", cascade="all, delete-orphan") quiz_attempts: Mapped[List["QuizAttempt"]] = relationship("QuizAttempt", back_populates="user", cascade="all, delete-orphan") summaries: Mapped[List["Summary"]] = relationship("Summary", back_populates="user", cascade="all, delete-orphan") chat_sessions: Mapped[List["ChatSession"]] = relationship("ChatSession", back_populates="user", cascade="all, delete-orphan") class Material(Base): __tablename__ = "materials" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, server_default=text("gen_random_uuid()") ) user_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("profiles.id", ondelete="CASCADE"), nullable=False ) source_type: Mapped[str] = mapped_column(String, nullable=False) title: Mapped[str] = mapped_column(String, nullable=False) file_path: Mapped[Optional[str]] = mapped_column(String, nullable=True) url: Mapped[Optional[str]] = mapped_column(String, nullable=True) status: Mapped[str] = mapped_column(String, default="pending", server_default=text("'pending'")) error_message: Mapped[Optional[str]] = mapped_column(String, nullable=True) vector_store_path: Mapped[Optional[str]] = mapped_column(String, nullable=True) title_normalized: Mapped[Optional[str]] = mapped_column(String, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) # Relationships user: Mapped["Profile"] = relationship("Profile", back_populates="materials") chunks: Mapped[List["MaterialChunk"]] = relationship("MaterialChunk", back_populates="material", cascade="all, delete-orphan") embeddings: Mapped[List["MaterialEmbedding"]] = relationship("MaterialEmbedding", back_populates="material", cascade="all, delete-orphan") summaries: Mapped[List["Summary"]] = relationship("Summary", back_populates="material", cascade="all, delete-orphan") quizzes: Mapped[List["Quiz"]] = relationship("Quiz", back_populates="material", cascade="all, delete-orphan") chat_sessions: Mapped[List["ChatSession"]] = relationship("ChatSession", back_populates="material", cascade="all, delete-orphan") class MaterialChunk(Base): __tablename__ = "material_chunks" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, server_default=text("gen_random_uuid()") ) material_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("materials.id", ondelete="CASCADE"), nullable=False ) chunk_index: Mapped[int] = mapped_column(Integer, nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False) token_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) # Relationships material: Mapped["Material"] = relationship("Material", back_populates="chunks") embeddings: Mapped[List["MaterialEmbedding"]] = relationship("MaterialEmbedding", back_populates="chunk", cascade="all, delete-orphan") class MaterialEmbedding(Base): __tablename__ = "material_embeddings" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, server_default=text("gen_random_uuid()") ) material_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("materials.id", ondelete="CASCADE"), nullable=False ) chunk_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("material_chunks.id", ondelete="CASCADE"), nullable=False ) embedding: Mapped[Any] = mapped_column(Vector(384), nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) # Relationships material: Mapped["Material"] = relationship("Material", back_populates="embeddings") chunk: Mapped["MaterialChunk"] = relationship("MaterialChunk", back_populates="embeddings") class Summary(Base): __tablename__ = "summaries" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, server_default=text("gen_random_uuid()") ) material_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("materials.id", ondelete="CASCADE"), nullable=False ) user_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("profiles.id", ondelete="CASCADE"), nullable=False ) summary: Mapped[str] = mapped_column(Text, nullable=False) status: Mapped[str] = mapped_column(String, default="completed", server_default=text("'completed'")) model_name: Mapped[Optional[str]] = mapped_column(String, nullable=True) error_message: Mapped[Optional[str]] = mapped_column(String, nullable=True) time_taken: Mapped[Optional[float]] = mapped_column(Float, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) # Relationships material: Mapped["Material"] = relationship("Material", back_populates="summaries") user: Mapped["Profile"] = relationship("Profile", back_populates="summaries") class Quiz(Base): __tablename__ = "quizzes" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, server_default=text("gen_random_uuid()") ) user_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("profiles.id", ondelete="CASCADE"), nullable=False ) material_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("materials.id", ondelete="CASCADE"), nullable=True ) source_type: Mapped[Optional[str]] = mapped_column(String, nullable=True) difficulty: Mapped[Optional[str]] = mapped_column(String, nullable=True) mcq_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) tf_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) quiz_data: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) status: Mapped[str] = mapped_column(String, default="completed", server_default=text("'completed'")) model_name: Mapped[Optional[str]] = mapped_column(String, nullable=True) error_message: Mapped[Optional[str]] = mapped_column(String, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) # Relationships user: Mapped["Profile"] = relationship("Profile", back_populates="quizzes") material: Mapped[Optional["Material"]] = relationship("Material", back_populates="quizzes") attempts: Mapped[List["QuizAttempt"]] = relationship("QuizAttempt", back_populates="quiz", cascade="all, delete-orphan") class QuizAttempt(Base): __tablename__ = "quiz_attempts" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, server_default=text("gen_random_uuid()") ) quiz_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("quizzes.id", ondelete="CASCADE"), nullable=False ) user_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("profiles.id", ondelete="CASCADE"), nullable=False ) score: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) total: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) results: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) # Relationships quiz: Mapped["Quiz"] = relationship("Quiz", back_populates="attempts") user: Mapped["Profile"] = relationship("Profile", back_populates="quiz_attempts") class ChatSession(Base): __tablename__ = "chat_sessions" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, server_default=text("gen_random_uuid()") ) user_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("profiles.id", ondelete="CASCADE"), nullable=False ) material_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("materials.id", ondelete="CASCADE"), nullable=False ) title: Mapped[Optional[str]] = mapped_column(String, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) # Relationships user: Mapped["Profile"] = relationship("Profile", back_populates="chat_sessions") material: Mapped["Material"] = relationship("Material", back_populates="chat_sessions") messages: Mapped[List["ChatMessage"]] = relationship("ChatMessage", back_populates="session", cascade="all, delete-orphan") class ChatMessage(Base): __tablename__ = "chat_messages" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, server_default=text("gen_random_uuid()") ) session_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("chat_sessions.id", ondelete="CASCADE"), nullable=False ) role: Mapped[Optional[str]] = mapped_column(String, nullable=True) content: Mapped[Optional[str]] = mapped_column(Text, nullable=True) message_metadata: Mapped[Optional[dict]] = mapped_column("metadata", JSONB, nullable=True) retrieved_chunk_ids: Mapped[Optional[list]] = mapped_column(JSONB, nullable=True) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), server_default=func.now() ) # Relationships session: Mapped["ChatSession"] = relationship("ChatSession", back_populates="messages") class ChatMessageChunk(Base): __tablename__ = "chat_message_chunks" message_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("chat_messages.id", ondelete="CASCADE"), primary_key=True ) chunk_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("material_chunks.id", ondelete="CASCADE"), primary_key=True ) relevance_score: Mapped[Optional[float]] = mapped_column(Float, nullable=True)