Repository navigation
Models and fields page with a generated field type table #708
Description
Parent
What to build
The field type table is wrong in about 9 cells (str is varchar, not TEXT; datetime is timestamptz on Postgres; enums are varchar(n) on SQLite; UUID width; int is never BIGINT without db_type) and the page's own DDL contradicts it.
The table is generated from the storage resolution the emitters share (_core._resolve_storage_type, I-1 item 3) for every supported annotation on both dialects, written to docs/includes/field-types.md and embedded by the page; a test regenerates it and fails on any difference. The page gets one running example, DDL shown beside each schema feature (PassReport statements or the create pass rendering), and one explanation of what happens when a Model base class declares fields (the base gets a table of its own; verify before writing), which the timestamps and soft-delete how-tos will link to.
Pages: guide/models-and-fields.md
Acceptance criteria
- Every fact on the page matches the code at the time of the PR; each High finding in the audit rows below is fixed, and each Medium/Low finding is fixed or answered in the PR body with the reason it was left.
- Each page is one kind (tutorial, how-to, reference or explanation) and opens with one sentence saying what it covers, then an example on the first screen (writing-for-humans rules 1–2).
- Each page uses one running example, declared once near the top and reused; each section shows only what changed (rule 3).
- Every code block has a lead-in sentence ending in a colon and a result sentence after it, and shows its result: printed output, or the asserted value inside the embedded snippet range (rules 7–8). Snippet
end:markers sit below the assert or print that holds the result. - Every copyable block either shows the
async with engines.session():wrapper or sits under a sentence that says it runs inside one and links to Connections › Sessions; the page's first block shows the wrapper. - Every inline Python block that does real work runs under the harness (
{test=run prelude=<name>}) or moves into an example script; a block left compile-only says why in its fence marker. - Every lambda passed to a query method names its parameter after the model in lowercase singular (AGENTS.md I-8);
python -m tests._docs_checksreports no violation for the owned files. - Every error check in an owned example fails when the expected error is not raised (
try/except/else: raise AssertionError, or the harness helper). - Field-declaring examples show both declaration styles as tabs, each backed by a runnable script (AGENTS.md I-7), and the twins use the one Annotated idiom recorded in the docs conventions.
- Headings are short sentence-case noun phrases naming the feature (rule 5); how-to titles name the goal (rule 24); no minimising words (rule 17); admonitions follow rule 20.
- No ADR numbers, issue numbers, codenames, dated or release-bound notes, or contributor-only references in page text; version markers are self-contained blocks at the end of a section.
- Reference material that has a home in Reference is linked, not repeated; each topic ends with a pointer to its reference entry (rule 4).
Files owned
- Create:
docs/includes/field-types.md,tests/test_docs_field_type_table.py,docs/examples/models_and_fields.py,docs/examples/models_and_fields_annotated.py - Modify:
docs/pages/guide/models-and-fields.md,docs/examples/table_checks.py,docs/examples/table_checks_annotated.py
Interfaces
- Consumes: I-7, I-14, docs conventions (AGENTS.md ticket (AGENTS.md: rewrite I-8 for the lambda-only query API, add a docstring invariant, record the docs conventions #703))
- Consumes: harness (harness ticket (Docs harness: run inline blocks with a prelude, run docstring examples, and check lambda names and error checks #704))
- Consumes: quickstart example as rewritten by the get-started ticket (Get started pages: home page and README, Why Ferro, installation and quickstart #705)
- Produces: Section anchor
#fields-on-a-model-base-classon the models-and-fields page: the one explanation of base-class fields, linked by the short how-tos ticket (How-tos: testing, pagination, timestamps and soft deletes #716). - Produces:
python -m tests.test_docs_field_type_tableregeneratesdocs/includes/field-types.md.
Test scenarios
- Happy path: the strict site build succeeds and every snippet range on the owned pages renders without leading indentation.
- Happy path: every owned example script runs to completion under
tests/test_docs_examples.py, and every inline block marked{test=run ...}runs. - Error path: deleting the
raisean owned example's error check expects makes that example's test fail. - Edge case:
python -m tests._docs_checksover the owned files exits 0; renaming one lambda parameter totmakes it exit 1 naming the file and line. - Integration: the type table test fails when one cell of
docs/includes/field-types.mdis edited by hand.
Verify
uv run --no-sync zensical build --clean --strict
uv run --no-sync pytest tests/test_docs_examples.py -q --no-cov -p no:cacheprovider -n 8
uv run --no-sync pytest tests/test_docs_field_type_table.py -q --no-cov -p no:cacheprovider
uv run --no-sync python -m tests._docs_checks docs/pages/guide/models-and-fields.md docs/examples/models_and_fields.py docs/examples/table_checks.pyBlocked by
- Docs site config: Zensical features, Markdown extensions, API reference options and a strict build #701, AGENTS.md: rewrite I-8 for the lambda-only query API, add a docstring invariant, record the docs conventions #703, Docs harness: run inline blocks with a prelude, run docstring examples, and check lambda names and error checks #704, Get started pages: home page and README, Why Ferro, installation and quickstart #705
Audit findings
Rows from the docs audit this ticket covers (file, line, rule, fix)
docs/pages/guide/models-and-fields.md
- Kind (rule 1): mixed. It is mostly a guide/how-to, with reference mixed in (the "Field Types" and "Field Options" tables, which repeat
api/fields.mddocstrings) and some explanation (JSONB round-trip contract, NULL semantics). - Running example (rule 3): none. There are 14 unrelated models: Author/Post, Product, Document, Transaction/Payment, User, Country, LegacyRecord, Order, OrgMembership, Comment, Pair, User (again,
app_users), Account, Note/Task. - Verdict: I-7 is honoured throughout: every
Field()declaration is tabbed, and the snippet-backed tabs have runnable_annotatedcompanions. The core reference table, "Field Types", is wrong in about 9 cells against the real DDL, and the page has no running example. Grade C.
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| H | 71-84 | facts | The type table contradicts the emitted DDL. Real SQLite CREATE TABLE (verified) gives: str → varchar (not TEXT); bool → integer (not BOOLEAN (0/1)); datetime → DATETIME; date → DATE; time → TIME (not TEXT (ISO 8601)); UUID → CHAR(32) (the value is stored as 36-char text); Enum → varchar(<longest label>) (not TEXT); dict/list → JSON. Postgres (canonical_from_parts + pg_alter_type_target): int → integer only (no derived BIGINT); str → varchar (not TEXT); datetime → timestamptz always (naive timestamp only through db_type="timestamp"). |
Regenerate the table from resolve_column_storage output, or better, generate it in a test so it cannot drift again. Put the declared type and the stored value in separate columns where they differ (UUID). |
| H | 74, 88 | facts | "str … TEXT … Override with db_type=varchar(n)" and "varchar(255) instead of unbounded TEXT". The default is already unbounded varchar, which line 478 of this same page shows ("email" varchar NOT NULL). |
Rewrite as: "When you want a length limit, for example varchar(255) instead of unbounded varchar, pass a db_type token:". |
| H | 119 | facts | "enum.Enum fields are stored as text on SQLite". The column is varchar(n), sized to the longest label. |
"stored as varchar(n) on SQLite, sized to the longest label". |
| M | 304 | facts | "on SQLite it is stored as a 36-character string". The value is, but the declared type is CHAR(32), which surprises anyone reading the schema. |
Name both: "declared CHAR(32); the value is the 36-character hyphenated string". |
| M | 545-556 | facts / rule 18 | The warning says inheriting fields "silently produces broken subclass defaults". I could not reproduce that. class Article(TimestampedModel) hydrates and saves correctly (default_factory works). The visible effect is that the base is a concrete model, so auto-migrate creates a stray timestampedmodel table. |
Verify the real hazard with the maintainer, then state it with its reason: "the base class is itself a model, so it gets its own table (timestampedmodel)…". If "broken defaults" is real on some path, show that path. |
| M | whole page | rule 3 | There is no running example: 14 unrelated models, two of them named User. |
Pick one domain (for example the quickstart's Author/Post, or a Product catalog) and grow it section by section. Each section then shows only the new field or ClassVar. |
| M | 90-114, 308-401 | rule 13 | db_type, composite uniques and composite indexes show no generated DDL. The page shows it only once (line 476). The names are I-1 contracts readers will see in their database. |
After each, show the DDL. For example, CREATE UNIQUE INDEX "uq_orgmembership_user_id_org_id" … and CREATE INDEX "idx_comment_user_id_created_at" …. |
| M | 233, 541 | rule 7/8 | Claims with no code: "the generated ID is written back onto the instance", and the validator/model_config sentence. Line 532's block prints a count but the page never shows the output (it is 3 validation errors — nothing was written, verified). |
Add the output line under the block (3 validation errors — nothing was written). Add a two-line save() → print(user.id) example for line 233. |
| M | 1-636 | rule 4 / 22 | No link to the reference: api/fields.md (Field, FerroField, DbType) and api/model.md are never linked. "Field Options" retypes the docstrings. |
Link Field() / FerroField / db_type on first use. End "Field Options" with "Full signature: Field". |
| M | 27, 44 | rule 12 / I-7 | The tab labels "Assignment with Field()" / "Annotated metadata" differ from the "Assignment" / "Annotated" labels used everywhere else. Linked tabs cannot sync on mismatched labels. | Use "Assignment" / "Annotated" exactly. |
| L | 1, 5, 23, 67, 183, 203, 306, 500, 543, 630 | rule 5 | Title Case headings ("Defining a Model", "Field Types", "Reusing Fields Across Models"…) sit beside sentence-case ones ("Custom table names", "Table checks"). | Use sentence case throughout: "Models and fields", "Defining a model", "Field types"… |
| L | 171, 173, 175 | rule 22 | Bold run-in pseudo-headings: "SQLite lowering.", "Round-trip contract is value equality, not byte fidelity.", "Migrating between json and jsonb is a declaration edit." The last two are whole bold sentences. |
Make them #### headings ("SQLite storage", "Round-trip contract", "Switching between json and jsonb") or plain sentences. |
| L | 181 | rule 19/16 | "Today JSONB gives you honest storage and round-tripping" is a slogan. | "Today you can store and read a JSONB column as a whole value." |
| L | 409 | rule 22 | "ferro derives" in lowercase; elsewhere the page says "Ferro". | "Ferro derives". |
| L | 433-435 | rule 8 | The violation snippet shows test scaffolding (except …: pass / else: raise AssertionError) and no visible error. |
Move the snippet markers to show except CheckViolationError as exc: print(exc.constraint), with the printed name under the block. |
| L | 142-144, 164-166 | pymdownx | Trailing # JSONB (the default) comments are good candidates for code annotations (content.code.annotate is already on). |
Use # (1)! callouts. |
docs/examples/table_checks.py + table_checks_annotated.py
- Embedded by:
guide/models-and-fields.md(models,violation+ annotated) - Verdict: B. It is the only example whose error check can fail (
else: raise AssertionError). The model is abstract, though.
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| M | 10–20 | 10 | Pair with left/right and values "only-left"/"both"/"set" is a placeholder domain. |
A real one, e.g. Payment with card_token/bank_account, "at most one payment method". |
| L | 34–39 | 11 / 8 | The page shows test scaffolding (else: raise AssertionError) instead of the error the reader would see. |
Show the CheckViolationError message as output, and keep the else out of the section. |
Activity
0x054 commentedon Oct 19, 2026
Status: verify-passed
Worker: worker-708 (reviewed by reviewer-708)
Branch:
sdd/708-models-and-fieldsPR: #738
Verify: passed 0810900 (trial merge into
feat/docs-rewriteat f8aa4b2 also passed)The review found Spec PASS and Quality PASS, with two Minor findings and nothing Critical or Important. The type table cells come from the create pass, which consumes
resolve_column_storage, so they are the I-1 storage decision. Leaving nested-model round-trips out of scope is accepted (#737). Ready for merge intofeat/docs-rewrite.docs(guide): Models and fields — one running example, generated field…
0x054 commentedon Oct 19, 2026
Merged in #738 into
feat/docs-rewritedocs(concepts): concept pages checked against the code, one running e…