import uuid from datetime import datetime from sqlalchemy import Column, String, Boolean, Integer, DateTime, ForeignKey, UniqueConstraint, LargeBinary from sqlalchemy.dialects.postgresql import UUID, JSONB from sqlalchemy.orm import relationship from pgvector.sqlalchemy import Vector from app.db import Base class User(Base): __tablename__ = "users" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) email = Column(String(320), nullable=False) password_hash = Column(String(255), nullable=False) role = Column(String(20), nullable=False, default="owner") is_active = Column(Boolean, nullable=False, default=True) created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) class Hub(Base): __tablename__ = "hubs" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False) name = Column(String(120), nullable=False) description = Column(String(500), nullable=True) ai_summary = Column(String, nullable=True) suggested_questions = Column(JSONB, nullable=False, default=list) created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) updated_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) # These relationships belong in Hub, not User! documents = relationship("Document", back_populates="hub", cascade="all, delete-orphan") chunks = relationship("Chunk", back_populates="hub", cascade="all, delete-orphan") __table_args__ = ( UniqueConstraint("owner_id", "name", name="uq_hub_name_per_owner"), ) class Document(Base): __tablename__ = "documents" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) hub_id = Column(UUID(as_uuid=True), ForeignKey("hubs.id", ondelete="CASCADE"), nullable=False) owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False) origin = Column(String(20), nullable=False) filename = Column(String(255), nullable=False) mime_type = Column(String(100), nullable=False) size_bytes = Column(Integer, nullable=False) checksum_sha256 = Column(String(64), nullable=False) status = Column(String(20), nullable=False, default="pending") error_message = Column(String, nullable=True) file_bytes = Column(LargeBinary, nullable=True) process_claim = Column(UUID(as_uuid=True), nullable=True) created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) updated_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) hub = relationship("Hub", back_populates="documents") __table_args__ = ( UniqueConstraint("hub_id", "checksum_sha256", name="uq_doc_checksum_per_hub"), ) class Chunk(Base): __tablename__ = "chunks" id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) document_id = Column(UUID(as_uuid=True), ForeignKey("documents.id", ondelete="CASCADE"), nullable=False) hub_id = Column(UUID(as_uuid=True), ForeignKey("hubs.id", ondelete="CASCADE"), nullable=False) owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False) chunk_index = Column(Integer, nullable=False) text = Column(String, nullable=False) token_count = Column(Integer, nullable=False) embedding = Column(Vector(768), nullable=False) created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) hub = relationship("Hub", back_populates="chunks") class IdempotencyKey(Base): __tablename__ = "idempotency_keys" user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), primary_key=True) key = Column(UUID(as_uuid=True), primary_key=True) request_method = Column(String(10), nullable=False) request_path = Column(String(255), nullable=False) request_hash = Column(String(64), nullable=False) status = Column(String(20), nullable=False, default="in_progress") response_status = Column(Integer, nullable=True) response_body = Column(JSONB, nullable=True) created_at = Column(DateTime(timezone=True), nullable=False, default=datetime.utcnow) expires_at = Column(DateTime(timezone=True), nullable=False)