Skip to content

Models and fields page with a generated field type table #708

Closed

Description

@0x054

Parent

#698

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_checks reports 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

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 raise an owned example's error check expects makes that example's test fail.
  • Edge case: python -m tests._docs_checks over the owned files exits 0; renaming one lambda parameter to t makes it exit 1 naming the file and line.
  • Integration: the type table test fails when one cell of docs/includes/field-types.md is 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.py

Blocked by

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.md docstrings) 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 _annotated companions. 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

  1. added
    documentationImprovements or additions to documentation
    ready-for-agentUnblocked and fully specified; an agent may claim it
    size:MNeeds a plan; one or two local workers
    on Oct 19, 2026
  2. added theissue type on Oct 19, 2026
  3. self-assigned this
    on Oct 19, 2026
  4. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Status: verify-passed
    Worker: worker-708 (reviewed by reviewer-708)
    Branch: sdd/708-models-and-fields
    PR: #738
    Verify: passed 0810900 (trial merge into feat/docs-rewrite at 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 into feat/docs-rewrite.

  5. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Merged in #738 into feat/docs-rewrite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationready-for-agentUnblocked and fully specified; an agent may claim itsize:MNeeds a plan; one or two local workers

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions