Skip to content

Relationships and raw SQL guides #712

Closed
Closed

Description

@0x054

Parent

#698

What to build

Raw SQL examples query users and events, but Ferro creates user and event; the NO ACTION row says the constraint is not enforced (it is); the many-to-many join table's name and constraints are documented nowhere. Both pages get one running example shown on the page, real table names, the join-table schema shown as the DDL Ferro creates, and results beside each call.

Pages: guide/relationships.md, guide/raw-sql.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

  • Modify: docs/pages/guide/relationships.md, docs/pages/guide/raw-sql.md, docs/examples/raw_sql.py, docs/examples/relationships.py, docs/examples/relationships_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.

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 python -m tests._docs_checks docs/pages/guide/relationships.md docs/pages/guide/raw-sql.md docs/examples/relationships.py docs/examples/relationships_annotated.py docs/examples/raw_sql.py

Blocked by

Audit findings

Rows from the docs audit this ticket covers (file, line, rule, fix)

docs/pages/guide/relationships.md

  • Kind (rule 1): how-to with reference tables (ManyToMany params, on_delete). The mix is acceptable.
  • Running example (rule 3): none. The page uses nine model sets: Team/Player, User/Profile, Student/Course, Employee, Category/Product, Library/Document, Org/Project, Post/Author in prose, and Transaction/Account in the populated-relations snippet.
  • Verdict: Accurate and well tested. Every Assignment/Annotated pair is backed by relationships_annotated.py. The self-referential claim holds: Annotated["Employee | None", …] fails with Relationship resolution failed. The weak spots are the scattered cast, the lack of SQL anywhere on a page about schema shape, a misleading NO ACTION row, and I-8 lambda names. B
  • Findings:
Sev Line Rule Finding Suggested fix
H 9, 36; snippet populated_relations.py:basic L82 I-8 Three lambdas break the parameter-naming rule: include(lambda p: p.author), Player.where(lambda t: t.team_id == team.id) and Transaction.select().include(lambda t: t.account). lambda post: post.author, lambda player: player.team_id == team.id, lambda transaction: transaction.account. The last one needs the loop variable renamed in the example file.
M 187 fact The "NO ACTION" row says "The constraint is not enforced at delete time". On both backends, NO ACTION still refuses the delete while children exist. The check runs at the end of the statement and can be deferred, unlike RESTRICT's immediate check. "Like RESTRICT, but checked at the end of the statement (deferrable on Postgres)."
M 74–104 13, 24 "Ferro synthesizes the join table automatically", but the page never says the table's name or shape. Verified output for the Student/Course example: table student_courses (course_id, student_id), uq_student_courses_student_id_course_id, idx_student_courses_course_id_student_id, two fk_* constraints. Show the generated CREATE TABLE / indexes under the M2M models. Do the same for one-to-many (player.team_id + fk_player_team_id_team), so the shadow-column section has something to point at.
M whole page 3 Nine casts on one page. The populated-relations snippet brings in a tenth domain, Transaction/Account, and its loop variable transaction shadows the ferro.transaction import that readers see on the next page. Use one domain throughout, for example Team/Player plus Player/Profile for one-to-one, Player/Tournament for M2M, and Player.captain for self-reference. Point the populated snippet at Team/Player too.
M 30, 68, 90, 122 7, 8 The usage blocks carry no lead-in or result sentence, and their results are asserts. Add a lead-in ("Create a team and a player, then read the relation both ways:") and a result sentence ("crab.team_id holds team.id without a query; team.members is a query until you call .all().").
M 50 16 "a separate future mechanism; until it lands, reverse relations stay chainable queries" is roadmap language in a guide. "include() covers forward foreign keys only; load a BackRef or M2M collection with await author.posts.all()."
L 5 5 The heading "Overview" names nothing. Fold the section into the intro, or rename it "Lazy and populated relations".
L 54, 120 — The one-to-one reverse field is annotated profile: "Profile" = BackRef(), but await user.profile is awaited, so a type checker reads the annotation as wrong. The page doesn't say what await user.profile returns when no child exists. Say what a missing child returns (None?). Raise the annotation question with the concepts/type-safety auditor.
L 12 22 The pairing rule says "raises at connect time" but names no exception. Name the exception type.
L headings 5 The headings are Title Case. Change them to sentence case.

docs/pages/guide/raw-sql.md

  • Kind (rule 1): how-to with a reference table (bind types). Good.
  • Running example (rule 3): Event in the snippets, but the inline blocks query the users and events tables.
  • Verdict: The tightest page in the set. The _marshal table matches raw.py exactly. It leaves out autocommit=True, and its inline SQL uses plural table names that Ferro never creates. B
  • Findings:
Sev Line Rule Finding Suggested fix
H 43, 53, 83 correctness, 9 The inline SQL uses FROM users and UPDATE events. Ferro names tables model_name.lower(), which gives user and event, and the page's own snippet uses event. A reader copying the placeholder example against Ferro models gets "no such table". Use event and the Event model's columns everywhere: SELECT * FROM event WHERE kind = ? AND id >= ?.
H whole page correctness Top-level execute/fetch_all/fetch_one outside a session raise RuntimeError (verified). The page says nothing about this. Add one line plus the session wrapper in the first example.
M 14–20 coverage execute(..., autocommit=True) is not mentioned. It is needed for CREATE INDEX CONCURRENTLY / VACUUM in a settings session and is documented only in the docstring and the RLS guide. session= is also unmentioned. Add a short "Statements that refuse a transaction" subsection with the docstring's example, and link the RLS caveat.
M 5 5 "When to Reach for Raw SQL" is an instruction-shaped heading. "Use cases" or fold into the intro.
M 16, 26, 95 7, 8 No block shows its returned value. execute returns 2; fetch_all returns [{"kind": "click", "n": 2}, {"kind": "signup", "n": 1}]. Add result sentences with the literal values, which are already asserted in raw_sql.py.
L 7 16 "which are ORM territory now" refers to history. "…grouped queries, which the ORM handles".
L 9–10 20 The Warning box covers a correctness gotcha (types come back as strings), not data loss. Rule 20 puts surprising behaviour in a Note. !!! note.

docs/examples/relationships.py + relationships_annotated.py

  • Embedded by: guide/relationships.md (9 sections + 5 annotated)
  • Running example: Team/Player, User/Profile, Student/Course, Employee, Library/Document
  • Verdict: B. Real-looking data (Ferris, Grace/Linus, Rust 101), and the asserts are visible inside the sections. on-delete is never exercised.
Sev Line Rule Finding Suggested fix
M 79–94 9 on_delete="CASCADE" is declared but never exercised. The assignment script creates no Library, and the annotated twin only creates one. The page's CASCADE claim is untested. Add an on-delete-usage section: delete the library, then assert await Document.select().count() == 0.
M 135–136 8 / 11 remove(python201) then clear() with no result between them. Assert [course.title for course in await sam.courses.all()] == ["Rust 101"] after remove.
L 103 10 Variable crab for a Player. ferris.
L 70–72 typing Same "Employee"-without-None as traversal. As above.

docs/examples/raw_sql.py

  • Embedded by: guide/raw-sql.md (3 sections)
  • Verdict: A. Small, checkable data (2 clicks, 1 signup) and one idea per block. The only gap: results are asserted outside the sections (affected == 2, rows[0]["n"] == 2), so the page cannot show them. Move the asserts in.

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-712
    Branch: sdd/712-relationships-raw-sql
    PR: #739
    Verify: passed 1511130 (on feat/docs-rewrite at a10707f; no trial merge needed)

    Re-review by reviewer-712: Spec PASS, Quality PASS. The fix round addresses both blocking findings: the three dead anchors point at #sessions and concepts/row-level-security.md#connection-delivery, and the Postgres blocks use {test=compile reason="..."}. One Minor stays open: the DDL blocks on the Relationships page are hand-reflowed, and no test compares them. Ready for the orchestrator to merge.

  5. 0x054 commented on Oct 19, 2026

    @0x054
    ContributorAuthor

    Merged in #739 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