refactor: prefix every table with its module

Two tables were named after the concept they came from rather than the module
they belong to, so the schema read as if the template tables were part of the
paper module. Renamed (data preserved, `RENAME TABLE` moves rows in place):

    paper_template  -> template                 the 模板 module
    section_field   -> template_field_library   the 字段库 the 模板 module owns

The paper tables and `template_field` already followed the rule. The rename
carries through everything that named a module:

  models   Template, TemplateField, TemplateFieldLibrary
  schemas  Template*, TemplateFieldLibrary*
  crud     app/crud/template.py, app/crud/template_field_library.py
  API      /template-field-library (was /section-fields); handlers are now
           named after library entries, which removes the ambiguity with
           TemplateField — a placement, a different thing entirely
  client   src/api/templateFieldLibrary.ts

`paper_template_filed_sort` is deliberately untouched: it is a column of the
paper module, spelled as the feature was specified.

TiDB v8.5 with tidb_enable_foreign_key on — as this cluster runs — enforces
foreign keys rather than ignoring them, so the docs' "TiDB does not enforce
foreign keys" was wrong. Corrected, with what actually follows from it: the
rename was rehearsed (RENAME TABLE carries a referencing constraint along), the
API keeps checking first so a violation names the row instead of surfacing a
driver error, and the ORM cascades stay so behaviour does not depend on a
cluster setting.

Revision f27a1c6d9e04 verified both ways; 40 smoke checks, type-check and build
all pass.
This commit is contained in:
2026-09-18 17:48:11 +08:00
parent 2d113f9f6b
commit a5f884f440
25 changed files with 371 additions and 260 deletions
+2 -2
View File
@@ -5,6 +5,6 @@ models. They own the commit: a route calls one function and gets back either a
persisted object or ``None``.
"""
from app.crud import paper, paper_template, section_field
from app.crud import paper, template, template_field_library
__all__ = ["paper", "paper_template", "section_field"]
__all__ = ["paper", "template", "template_field_library"]
+1 -1
View File
@@ -36,7 +36,7 @@ from app.models import (
Paper,
PaperSentence,
PaperSentenceReference,
PaperTemplate,
Template,
TemplateField,
)
from app.schemas.paper import (
@@ -1,4 +1,4 @@
"""Data access for paper templates and their ordered field selections."""
"""Data access for templates and their ordered field selections."""
from collections.abc import Sequence
@@ -6,8 +6,8 @@ from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session
from app.crud.filters import LIKE_ESCAPE, like_pattern
from app.models import PaperTemplate, SectionField, TemplateField
from app.schemas.paper_template import PaperTemplateListItem, TemplateFieldInput
from app.models import Template, TemplateFieldLibrary, TemplateField
from app.schemas.template import TemplateListItem, TemplateFieldInput
def _conditions(keyword: str | None) -> list:
@@ -17,8 +17,8 @@ def _conditions(keyword: str | None) -> list:
pattern = like_pattern(keyword)
return [
or_(
PaperTemplate.name.like(pattern, escape=LIKE_ESCAPE),
PaperTemplate.abstract.like(pattern, escape=LIKE_ESCAPE),
Template.name.like(pattern, escape=LIKE_ESCAPE),
Template.abstract.like(pattern, escape=LIKE_ESCAPE),
)
]
@@ -27,8 +27,8 @@ def _field_count_column():
"""A correlated ``COUNT`` of the template's placement rows."""
return (
select(func.count(TemplateField.id))
.where(TemplateField.template_id == PaperTemplate.id)
.correlate(PaperTemplate)
.where(TemplateField.template_id == Template.id)
.correlate(Template)
.scalar_subquery()
)
@@ -39,7 +39,7 @@ def list_templates(
keyword: str | None = None,
page: int = 1,
page_size: int = 20,
) -> tuple[list[PaperTemplateListItem], int]:
) -> tuple[list[TemplateListItem], int]:
"""Return one page of templates with their field counts, plus the total.
The outline itself is left out: a table row needs the count, not the rows.
@@ -48,19 +48,19 @@ def list_templates(
conditions = _conditions(keyword)
total = db.scalar(
select(func.count(PaperTemplate.id)).where(*conditions)
select(func.count(Template.id)).where(*conditions)
) or 0
stmt = (
select(PaperTemplate, _field_count_column().label("field_count"))
select(Template, _field_count_column().label("field_count"))
.where(*conditions)
.order_by(PaperTemplate.updated_at.desc(), PaperTemplate.id.desc())
.order_by(Template.updated_at.desc(), Template.id.desc())
.offset((page - 1) * page_size)
.limit(page_size)
)
items = [
PaperTemplateListItem(
TemplateListItem(
id=template.id,
name=template.name,
abstract=template.abstract,
@@ -73,21 +73,21 @@ def list_templates(
return items, total
def get(db: Session, template_id: int) -> PaperTemplate | None:
def get(db: Session, template_id: int) -> Template | None:
"""Return one template with its outline already loaded, or ``None``.
``items`` (ordered by ``sort``) and each item's ``field`` are both
configured for eager loading on the relationships, so this is a fixed
number of queries rather than one per field.
"""
return db.get(PaperTemplate, template_id)
return db.get(Template, template_id)
def name_taken(db: Session, name: str, *, exclude_id: int | None = None) -> bool:
"""Whether another template already uses ``name``."""
stmt = select(func.count(PaperTemplate.id)).where(PaperTemplate.name == name)
stmt = select(func.count(Template.id)).where(Template.name == name)
if exclude_id is not None:
stmt = stmt.where(PaperTemplate.id != exclude_id)
stmt = stmt.where(Template.id != exclude_id)
return bool(db.scalar(stmt))
@@ -101,7 +101,7 @@ def missing_field_ids(db: Session, field_ids: Sequence[int]) -> list[int]:
if not wanted:
return []
found = set(
db.scalars(select(SectionField.id).where(SectionField.id.in_(wanted))).all()
db.scalars(select(TemplateFieldLibrary.id).where(TemplateFieldLibrary.id.in_(wanted))).all()
)
return sorted(wanted - found)
@@ -117,9 +117,9 @@ def create(
name: str,
abstract: str | None,
fields: Sequence[TemplateFieldInput],
) -> PaperTemplate:
) -> Template:
"""Insert a template together with its ordered selection."""
template = PaperTemplate(name=name, abstract=abstract)
template = Template(name=name, abstract=abstract)
template.items = _build_items(fields)
db.add(template)
db.commit()
@@ -129,13 +129,13 @@ def create(
def update(
db: Session,
template: PaperTemplate,
template: Template,
*,
name: str | None = None,
abstract: str | None = None,
abstract_provided: bool = False,
fields: Sequence[TemplateFieldInput] | None = None,
) -> PaperTemplate:
) -> Template:
"""Apply a partial update. ``fields=None`` leaves the selection untouched.
``abstract_provided`` distinguishes "clear the abstract" from "leave it"
@@ -159,7 +159,7 @@ def update(
return template
def delete(db: Session, template: PaperTemplate) -> None:
def delete(db: Session, template: Template) -> None:
"""Delete a template and its placement rows."""
db.delete(template)
db.commit()
@@ -171,7 +171,7 @@ def delete_many(db: Session, template_ids: Sequence[int]) -> int:
return 0
templates = list(
db.scalars(
select(PaperTemplate).where(PaperTemplate.id.in_(list(template_ids)))
select(Template).where(Template.id.in_(list(template_ids)))
).all()
)
for template in templates:
@@ -1,4 +1,4 @@
"""Data access for the reusable section-field library (字段管理)."""
"""Data access for the reusable template-field library (字段管理)."""
from collections.abc import Sequence
@@ -6,8 +6,11 @@ from sqlalchemy import Select, func, select
from sqlalchemy.orm import Session
from app.crud.filters import LIKE_ESCAPE, like_pattern
from app.models import SectionField, TemplateField
from app.schemas.section_field import SectionFieldCreate, SectionFieldUpdate
from app.models import TemplateField, TemplateFieldLibrary
from app.schemas.template_field_library import (
TemplateFieldLibraryCreate,
TemplateFieldLibraryUpdate,
)
def _conditions(keyword: str | None, level: int | None) -> list:
@@ -15,10 +18,10 @@ def _conditions(keyword: str | None, level: int | None) -> list:
conditions = []
if keyword:
conditions.append(
SectionField.name.like(like_pattern(keyword), escape=LIKE_ESCAPE)
TemplateFieldLibrary.name.like(like_pattern(keyword), escape=LIKE_ESCAPE)
)
if level is not None:
conditions.append(SectionField.level == level)
conditions.append(TemplateFieldLibrary.level == level)
return conditions
@@ -30,7 +33,7 @@ def _ordered(stmt: Select) -> Select:
ordered by ``name``: names carry hand-written numbering ("1.", "10.",
"2."), and string ordering would scramble it.
"""
return stmt.order_by(SectionField.level.asc(), SectionField.id.asc())
return stmt.order_by(TemplateFieldLibrary.level.asc(), TemplateFieldLibrary.id.asc())
def list_fields(
@@ -40,16 +43,16 @@ def list_fields(
level: int | None = None,
page: int = 1,
page_size: int = 20,
) -> tuple[list[SectionField], int]:
) -> tuple[list[TemplateFieldLibrary], int]:
"""Return one page of the field library, plus the unpaged total."""
conditions = _conditions(keyword, level)
total = db.scalar(
select(func.count(SectionField.id)).where(*conditions)
select(func.count(TemplateFieldLibrary.id)).where(*conditions)
) or 0
stmt = _ordered(
select(SectionField)
select(TemplateFieldLibrary)
.where(*conditions)
.offset((page - 1) * page_size)
.limit(page_size)
@@ -57,16 +60,16 @@ def list_fields(
return list(db.scalars(stmt).all()), total
def get(db: Session, field_id: int) -> SectionField | None:
def get(db: Session, field_id: int) -> TemplateFieldLibrary | None:
"""Return one field, or ``None``."""
return db.get(SectionField, field_id)
return db.get(TemplateFieldLibrary, field_id)
def get_many(db: Session, field_ids: Sequence[int]) -> list[SectionField]:
def get_many(db: Session, field_ids: Sequence[int]) -> list[TemplateFieldLibrary]:
"""Return every field whose id is in ``field_ids`` (missing ids ignored)."""
if not field_ids:
return []
stmt = select(SectionField).where(SectionField.id.in_(list(field_ids)))
stmt = select(TemplateFieldLibrary).where(TemplateFieldLibrary.id.in_(list(field_ids)))
return list(db.scalars(stmt).all())
@@ -89,16 +92,20 @@ def usage_counts(db: Session, field_ids: Sequence[int]) -> dict[int, int]:
return {field_id: count for field_id, count in db.execute(stmt).all()}
def create(db: Session, data: SectionFieldCreate) -> SectionField:
def create(db: Session, data: TemplateFieldLibraryCreate) -> TemplateFieldLibrary:
"""Insert a field."""
field = SectionField(**data.model_dump())
field = TemplateFieldLibrary(**data.model_dump())
db.add(field)
db.commit()
db.refresh(field)
return field
def update(db: Session, field: SectionField, data: SectionFieldUpdate) -> SectionField:
def update(
db: Session,
field: TemplateFieldLibrary,
data: TemplateFieldLibraryUpdate,
) -> TemplateFieldLibrary:
"""Apply a partial update to a field.
``exclude_unset`` is what makes PATCH semantics work: a key the client did
@@ -112,7 +119,7 @@ def update(db: Session, field: SectionField, data: SectionFieldUpdate) -> Sectio
return field
def delete(db: Session, field: SectionField) -> None:
def delete(db: Session, field: TemplateFieldLibrary) -> None:
"""Delete a field. Callers must check :func:`usage_counts` first."""
db.delete(field)
db.commit()