Skip to content

Show captured SQL beside query and mutation examples (needs #700) #725

Closed

Description

@0x054

Parent

#698

What to build

Writing-for-humans rule 13 asks for the SQL beside each query. Ferro has no public way to get the SQL a query or mutation runs; #700 designs one. Once it ships, the Queries, Aggregations, Relationships and Mutations guides show the SQL captured from that API beside each example, produced by the same test run that executes the example, never typed by hand.

The API shipped (#700, PR #771, on main and merged into feat/docs-rewrite): every Query terminal has a .sql() that returns a RenderedSql (sql, params, dialect; str() is the SQL alone) without sending anything, routed like the call, with secrets shown as REDACTED. Mutations on an instance (save, delete, bulk_create) have no .sql(); their statements are read from the statement log (ferro.sql records; the statement_records fixture in tests/conftest.py).

How the SQL reaches the page, and stays true. Follow the migrations pages' precedent (tests/test_docs_migrations.py, STEP_FILE_SOURCES): a page never carries SQL a person typed. Each sql fence on the four pages opens with a marker line naming its source, -- <example module>:<function or snippet> (<dialect>), for example -- predicates.py:first_query (sqlite), and a new test, tests/test_docs_sql.py, regenerates every marked block from the named example (calling the query's terminal .sql(), or reading the statement_records for an instance mutation) on that dialect and compares byte for byte, the way the migration step files are compared. A block for Postgres runs under the postgres_only marker. Where the SQL is the same on both dialects bar placeholders, show one block and say so in its lead-in; where it differs, show both. The convention in docs/solutions/patterns/docs-page-conventions.md ("Generated SQL beside each query") becomes the recipe, with the marker form written down.

Pages: guide/queries.md, guide/aggregations.md, guide/relationships.md, guide/mutations.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).
  • Every query example on the four pages shows the SQL Ferro sends, as a marked sql fence regenerated by tests/test_docs_sql.py from the example's own .sql() (or the statement log for an instance mutation), for each dialect it differs on; editing one character of a marked block makes that test fail naming the page and line.
  • A marked block's lead-in says which call it is the SQL of and, where one block stands for both dialects, that only the placeholders differ.

Files owned

  • Create: tests/test_docs_sql.py
  • Modify: docs/solutions/patterns/docs-page-conventions.md (the "Generated SQL beside each query" section becomes the marker recipe), docs/examples/_preludes/queries.py (if a prelude needs a Postgres twin for the Postgres blocks), docs/pages/guide/queries.md, docs/pages/guide/aggregations.md, docs/pages/guide/relationships.md, docs/pages/guide/mutations.md, docs/examples/mutations.py, docs/examples/relationships.py, docs/examples/relationships_annotated.py, docs/examples/predicates.py, docs/examples/existence_tests.py, docs/examples/partial_selects.py, docs/examples/populated_relations.py, docs/examples/traversal.py, docs/examples/aggregations.py, docs/examples/predicates_annotated.py, docs/examples/existence_tests_annotated.py, docs/examples/partial_selects_annotated.py, docs/examples/populated_relations_annotated.py, docs/examples/traversal_annotated.py, docs/examples/aggregations_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.
  • Happy path: tests/test_docs_sql.py regenerates every marked sql block on the four pages on SQLite, and the Postgres-marked ones on Postgres, and they compare equal.
  • Error path: changing one character inside a marked sql block makes tests/test_docs_sql.py fail naming the page, the line and the example it was regenerated from; a marker naming an example or name that does not exist fails the same way.
  • Edge case: a page sql fence with no marker line is refused by the test (every SQL block on the four pages is regenerated or it does not exist).

Verify

uv run --no-sync zensical build --clean --strict
uv run --no-sync pytest tests/test_docs_examples.py tests/test_docs_sql.py tests/test_docs_site.py tests/test_docs_lint.py -q --no-cov -p no:cacheprovider -n 8
uv run --no-sync pytest tests/test_docs_sql.py -q --no-cov -p no:cacheprovider --db-backends=sqlite,postgres
uv run --no-sync python -m tests._docs_checks docs/pages/guide/queries.md docs/pages/guide/aggregations.md docs/pages/guide/relationships.md docs/pages/guide/mutations.md docs/examples

Blocked by

Audit findings

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

Cross-cutting patterns

  1. I-8 lambda parameter naming is broken in most query examples. About 70 single-letter lambdas: existence_tests.py 11, populated_relations.py 16, partial_selects.py 17, aggregations.py 24, inline queries.md 12. queries.md:62 explicitly teaches "t by convention". Only predicates.py and traversal.py comply.
  2. AGENTS.md I-8 is out of date with the code. col() and operator-style predicates were removed in 0.14, so the "lambda > col() > operator / slated for deprecation" rule describes nothing that exists. One page link (queries.md:874, "three predicate styles") still carries the old story. Fix AGENTS.md as well as the page.
  3. Results hidden from the reader (rule 8): 27 snippets in queries.md alone. Snippet end: markers sit just above the asserts that hold the answer. The pages whose snippets include their asserts (partial_selects, aggregations) read noticeably better. Moving the markers fixes most of it.
  4. No running example (rule 3) on 2 of 3 pages. models-and-fields.md uses 14 unrelated models. queries.md has 4 incompatible Transaction models plus inline references to models no schema on the page defines (Post, user.posts, user.deleted_at).
  5. Schema facts drift from the DDL lowering. About 12 wrong type cells or claims in models-and-fields.md (str→TEXT, datetime→TIMESTAMP, enum→TEXT, UUID width, int→BIGINT), and singular/plural table names in queries.md (2). Generating the type table from resolve_column_storage in a test would stop this class of drift.
  6. Headings mix Title Case, sentence case and slogans (rule 5): about 35 headings across the 3 pages. H2 Title Case with H3 sentence case on the same page. Sentence and slogan headings ("One verb, loud dead ends", "Zero rows, zero groups") name nothing to search for.
  7. No links to the API reference (rule 4/22) on all 3 pages. api/fields.md, api/model.md and api/queries.md exist but are never linked. The pages retype option tables and limit lists instead.
  8. Generated SQL is missing or simplified (rule 13). Aggregations shows no GROUP BY, models-and-fields shows DDL once out of about 10 schema features, and 4 queries.md SQL blocks are hand-simplified (unquoted reserved words, invented aliases) while the lead-ins claim "ships to the database as".

Also seen: 3 try/except-without-else example blocks that cannot fail (rule 9). Tab labels are inconsistent (1), and content.tabs.link is not enabled in zensical.toml; with it on and the labels matched, a reader's Assignment/Annotated choice would follow them across every tab set.

Rule 13 (equivalent SQL)

No example shows the SQL its query generates. Pages carry 23 sql fences in total, and the query-heavy pages carry few: guide/queries.md has 5 SQL blocks for 86 Python blocks, guide/aggregations.md has 0 for 15. Aggregations, traversal joins, existence tests (EXISTS/NOT EXISTS) and include() are exactly where a reader needs the SQL beside the call. Comments in the examples already describe the SQL ("~ renders NOT EXISTS", "COUNT(column) counts non-NULL values", "One SQL statement"), so the SQL is known and only needs showing. If Ferro can render a query's SQL (for example a debug or to_sql hook), using it in the examples would keep that SQL tested.


Activity

  1. added
    documentationImprovements or additions to documentation
    needs-infoAmbiguous; ask before building
    size:MNeeds a plan; one or two local workers
    on Oct 19, 2026
  2. added theissue type on Oct 19, 2026
  3. 0x054 commented on Oct 21, 2026

    @0x054
    ContributorAuthor

    Unblocked from #700: the statement log (ferro.sql records) and Query.<terminal>.sql() shipped to main in PR #771 (f3e46f8). #700 itself stays open only for #767, which is blocked by this epic, so the edge was a cycle. The Interfaces line is filled in from the shipped API below.

  4. added
    ready-for-agentUnblocked and fully specified; an agent may claim it
    and removed
    needs-infoAmbiguous; ask before building
    on Oct 21, 2026
  5. self-assigned this
    on Oct 21, 2026
  6. 0x054 commented on Oct 21, 2026

    @0x054
    ContributorAuthor

    Status: verify-passed
    Worker: worker-725
    Branch: sdd/725-captured-sql
    PR: #775
    Verify: passed 45e4f82

    Re-review (reviewer-725) after fix round 1: Spec PASS, Quality PASS. #775 (review)
    Fixed: the .sql() error that was swallowed now has to match the call's own error, or the capture fails naming the example line.
    Verify passed on SQLite and Postgres; trial merge not run because the base has not moved. Ready for merge.

  7. 0x054 commented on Oct 21, 2026

    @0x054
    ContributorAuthor

    Merged in #775 into feat/docs-rewrite (00dd767).

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