"""The template <-> field join table (关联表), which owns the display order. This table is the heart of the design. It carries exactly one piece of information that belongs to neither side alone: ``sort`` — where this field sits in *this* template. Consequences worth stating explicitly, because they are the requirements: * A field can appear in many templates, at a different position in each. * A field can legitimately appear **more than once** in the same template (e.g. a level-2 "Background" under both "1. Introduction" and "2. Related Work"), so there is deliberately **no** unique constraint on ``(template_id, field_id)``. The UI warns about repeats; it does not forbid them. * The user picks fields in any order they like. Nothing about the selection order is stored — readers order strictly by ``sort``, then by ``id`` as a stable tie-breaker. """ from typing import TYPE_CHECKING from sqlalchemy import ForeignKey, Index, Integer from sqlalchemy.orm import Mapped, mapped_column, relationship from app.db.base import Base if TYPE_CHECKING: # pragma: no cover - typing only from app.models.paper_template import PaperTemplate from app.models.section_field import SectionField class TemplateField(Base): """One placement of one library field inside one template.""" __tablename__ = "template_field" __table_args__ = ( # Every read is "the fields of template X, in order", so the index # covers both the filter and the sort. Index("ix_template_field_template_sort", "template_id", "sort"), Index("ix_template_field_field_id", "field_id"), ) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) template_id: Mapped[int] = mapped_column( ForeignKey("paper_template.id", ondelete="CASCADE"), nullable=False, ) #: ``RESTRICT``: a field still placed in a template must not vanish from #: under it. The API enforces this in Python as well, since TiDB does not #: enforce foreign keys itself. field_id: Mapped[int] = mapped_column( ForeignKey("section_field.id", ondelete="RESTRICT"), nullable=False, ) #: Display position within the template. Plain ascending integer — lower #: sorts render first regardless of the field's ``level``. sort: Mapped[int] = mapped_column(Integer, nullable=False, default=0) template: Mapped["PaperTemplate"] = relationship(back_populates="items") #: Eager-loaded because every template read needs the field's name and #: typography; lazy loading would emit one query per row. field: Mapped["SectionField"] = relationship(lazy="joined") def __repr__(self) -> str: # pragma: no cover - debugging aid return ( f"" )