Coverage for src / quber / db / models.py: 100%
41 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
« prev ^ index » next coverage.py v7.14.0, created at 2026-09-23 22:14 -0400
1"""SQLAlchemy models for Quber RAG database."""
3from datetime import datetime
4from typing import Any, Optional
6from pgvector.sqlalchemy import Vector
7from sqlalchemy import JSON, ForeignKey, Integer, String, Text, UniqueConstraint
8from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
11class Base(DeclarativeBase):
12 """Base class for all database models."""
14 pass
17class Document(Base):
18 """Represents a processed document with extracted tables."""
20 __tablename__ = "documents"
22 id: Mapped[int] = mapped_column(Integer, primary_key=True)
23 filename: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True)
24 extraction_date: Mapped[datetime] = mapped_column(nullable=False)
25 total_pages: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
26 total_tables: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
27 executive_summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
28 model_provider: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
29 model: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
30 created_at: Mapped[datetime] = mapped_column(default=datetime.now)
32 # Relationship to extracted tables
33 tables: Mapped[list["ExtractedTable"]] = relationship(
34 "ExtractedTable", back_populates="document", cascade="all, delete-orphan"
35 )
37 def __repr__(self) -> str:
38 return f"<Document(id={self.id}, filename='{self.filename}', tables={self.total_tables})>"
41class ExtractedTable(Base):
42 """Represents an extracted table with embeddings for semantic search."""
44 __tablename__ = "extracted_tables"
45 __table_args__ = (UniqueConstraint("document_id", "table_id", name="uq_document_table"),)
47 id: Mapped[int] = mapped_column(Integer, primary_key=True)
48 document_id: Mapped[int] = mapped_column(ForeignKey("documents.id", ondelete="CASCADE"), index=True)
49 table_id: Mapped[int] = mapped_column(Integer, nullable=False)
50 page_number: Mapped[int] = mapped_column(Integer, nullable=False)
51 # `extractor` records which engine produced the row — e.g.
52 # "docling-consolidator" for the Parser+HeaderConsolidator path, or
53 # "camelot-llm" for the CamelotLLMTableExtractor path.
54 extractor: Mapped[Optional[str]] = mapped_column(String(50), nullable=True)
55 procedural_title: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
56 llm_title: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
57 llm_subtitle: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
58 llm_description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
59 table_markdown: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
60 headers: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON, nullable=True)
61 footnotes: Mapped[Optional[list[str]]] = mapped_column(JSON, nullable=True)
62 bbox: Mapped[Optional[list[float]]] = mapped_column(JSON, nullable=True)
63 flavor: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
64 table_metadata: Mapped[Optional[dict[str, Any]]] = mapped_column(JSON, nullable=True)
66 # Vector embeddings (1024 dims for bge-large-en-v1.5)
67 title_embedding: Mapped[Optional[Any]] = mapped_column(Vector(1024), nullable=True)
68 description_embedding: Mapped[Optional[Any]] = mapped_column(Vector(1024), nullable=True)
70 created_at: Mapped[datetime] = mapped_column(default=datetime.now)
72 # Relationship to document
73 document: Mapped["Document"] = relationship("Document", back_populates="tables")
75 def __repr__(self) -> str:
76 return (
77 f"<ExtractedTable(id={self.id}, document_id={self.document_id}, "
78 f"table_id={self.table_id}, page={self.page_number})>"
79 )