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
+49 -25
View File
@@ -57,17 +57,19 @@ Browser ──HTTP/JSON──▶ FastAPI ──SQLAlchemy──▶ TiDB (k3s)
## Domain Model
Six tables, and one rule that the last three follow from.
Six tables. Every one is prefixed with the module it belongs to, and a model
class is named after its table, so the schema can be read as three groups:
```
section_field the reusable heading library
template_field_library the 字段库: reusable headings
id, name, level, font_size, font_color
paper_template a named outline
template a named outline (模板)
id, name, abstract
template_field the join, and the only home of display order
id, template_id → paper_template, field_id → section_field, sort
template_field a field placed in a template — and the only
id, template_id → template, home of display order
field_id → template_field_library, sort
paper the document
id, title, template_id, abstract, author, status, keywords,
@@ -80,9 +82,15 @@ paper_sentence_reference the citations of one sentence
id, sentence_id → paper_sentence, reference_id, quote, sort
```
`template_field` and `template_field_library` are one word apart and mean
opposite things. The first is a *placement* — this template puts this field
here, `sort` included. The second is the *catalogue* the field was picked from,
shared by every template and owned by none. The library is managed from the
模板 menu, because it is the raw material a template is built out of.
### The field library is flat, not a tree
`section_field` has no `parent_id`. Hierarchy is expressed only by `level`
`template_field_library` has no `parent_id`. Hierarchy is expressed only by `level`
(1 for `1.`, 2 for `1.1`), which is a *rendering hint*: it drives indentation
and numbering semantics in the UI and nothing else.
@@ -123,7 +131,7 @@ work.
`paper_sentence.paper_template_filed_sort` holds the `sort` of the template
placement the sentence belongs to — **not** `template_field.id`, and not
`section_field.id`. Together with `sort`, the sentence's own position inside
`template_field_library.id`. Together with `sort`, the sentence's own position inside
that paragraph, it is everything needed to place a line of text.
The indirection is the feature. Two templates have no rows in common, but they
@@ -161,22 +169,34 @@ rejected with `422` rather than stored as a half-record. `reference_id` is a
plain nullable integer with **no** foreign key, because the reference library
does not exist yet; a citation may therefore be written now and linked later.
### TiDB does not enforce foreign keys
### Foreign keys are declared, and this cluster enforces them
TiDB parses `FOREIGN KEY` for compatibility, and from v6.6 honours it when
`tidb_enable_foreign_key` is on. On the cluster this project runs against it is
on, so the constraints are real integrity rather than documentation: a dangling
insert is rejected (`1452`) and `ON DELETE CASCADE` / `SET NULL` actually fire.
Two consequences are worth knowing before touching a migration:
TiDB parses `FOREIGN KEY` for compatibility and then ignores it. The constraints
are declared to document the relationships, and the integrity they would provide
is enforced in the application layer instead:
- **Tables must be created in dependency order**, and a rename must be checked
rather than assumed. `RENAME TABLE` does carry a referencing constraint along
with the renamed table here (the rename in revision `f27a1c6d9e04` was
rehearsed against the cluster before it was written), but that is a property
of the deployment, not of SQL.
- **The API still checks first.** A database violation surfaces as a generic
driver error, while the application refuses with a message that names the row
and the count:
- deleting a library field still placed in a template → `409`, naming the
field and how many templates use it;
- deleting a template a paper is written against → `409`, naming the template
and how many papers use it — the template *is* that paper's structure, so
removing it would empty the paper rather than tidy up;
- creating a template that references a missing field → `400`, and creating
or patching a paper that references a missing template likewise.
- deleting a field still placed in a template is refused with `409`, naming the
field and how many templates use it;
- deleting a template that a paper is written against is refused with `409`,
naming the template and how many papers use it — the template is that paper's
structure, so removing it would empty the paper rather than tidy up;
- creating a template that references a missing field is refused with `400`, and
creating or patching a paper that references a missing template likewise;
- `PaperTemplate.items`, `Paper.sentences` and `PaperSentence.citations` all use
`cascade="all, delete-orphan"`, so deleting a row removes its dependents.
The ORM cascades stay as well. `Template.items`, `Paper.sentences` and
`PaperSentence.citations` use `cascade="all, delete-orphan"`, so deleting a row
removes its dependents whether or not the database would have done it too —
which keeps behaviour identical on a cluster with foreign keys switched off.
## API
@@ -185,10 +205,10 @@ All routes are mounted under `/api`. Interactive docs at `/docs`.
| Method | Path | Notes |
|---|---|---|
| `GET` | `/health` | liveness plus a TiDB probe |
| `GET` | `/section-fields` | `keyword`, `level`, `page`, `page_size` |
| `POST` | `/section-fields` | create |
| `GET` `PATCH` `DELETE` | `/section-fields/{id}` | read / partial update / delete |
| `POST` | `/section-fields/batch-delete` | body `{ "ids": [...] }` |
| `GET` | `/template-field-library` | `keyword`, `level`, `page`, `page_size` |
| `POST` | `/template-field-library` | create a library entry |
| `GET` `PATCH` `DELETE` | `/template-field-library/{id}` | read / partial update / delete |
| `POST` | `/template-field-library/batch-delete` | body `{ "ids": [...] }` |
| `GET` | `/templates` | `keyword` matches name **or** abstract |
| `POST` | `/templates` | name + abstract + ordered `fields` |
| `GET` `PATCH` `DELETE` | `/templates/{id}` | `PATCH` with `fields` replaces the selection |
@@ -211,6 +231,9 @@ Conventions worth knowing:
it before using it in a CSS rule.
- List endpoints return `{ items, total, page, page_size, pages }`.
- A template read returns `fields` already ordered by `sort`; clients never sort.
- Route paths mirror table names one for one: `/templates` for `template`,
`/template-field-library` for `template_field_library`, `/papers` for
`paper`.
- A paper document returns its `paragraphs` in ascending position order,
already carrying their sentences and citation numbering. The client sorts
nothing and merges nothing: two implementations of the same ordering rule
@@ -311,7 +334,8 @@ silent re-shape of the document is exactly what the preview exists to prevent.
paper-doc/
├── backend/ # FastAPI application
│ ├── app/
│ │ ├── api/routes/ # route handlers (health, papers, section_fields, templates)
│ │ ├── api/routes/ # route handlers (health, papers, templates,
│ │ │ # template_field_library)
│ │ ├── core/ # settings and configuration
│ │ ├── crud/ # data-access helpers
│ │ ├── db/ # engine, session, declarative base