Split the migrations guide: tutorial loop, how-tos, a squash page, and reference tables in Reference #714
Copy link
Copy link
Closed
Labels
documentationImprovements or additions to documentationImprovements or additions to documentationready-for-agentUnblocked and fully specified; an agent may claim itUnblocked and fully specified; an agent may claim itsize:LCross-cutting or multi-worker; local waves onlyCross-cutting or multi-worker; local waves only
Description
Parent
What to build
The 664-line migrations guide wraps five how-tos and several reference tables around the tutorial loop, and three overlapping "which door handles which change" tables already disagree with each other.
- The guide keeps the tutorial loop (
new→ review →up). - Renames, retirements, label retirement and squash become how-to pages; squash gets its own page, in the CONTEXT.md vocabulary (Range, Inheritance).
- The auto-migrate matrix, the
PassReportkinds, the migration header table, the exit codes, the door comparison and the expand/contract table each get one home in a new Reference page and are linked from the guides; the copies on other schema pages are removed. - The doc-output pins in
tests/test_docs_migrations.pyfollow the blocks to their new pages. New pages are not added to the nav; the nav ticket (Navigation: six tabs, sentence-case labels that match page titles, moved pages redirected #723) wires them.
Pages: guide/schema/migrations.md, guide/schema/squash.md, guide/schema/renames-and-retirements.md, reference/migrations.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).
- Each of the six reference tables appears exactly once in the site source.
- Every old anchor of the migrations guide that moved resolves (the strict build checks it) or is listed for the nav ticket (Navigation: six tabs, sentence-case labels that match page titles, moved pages redirected #723)'s redirects in the PR body.
Files owned
- Create:
docs/pages/guide/schema/squash.md,docs/pages/guide/schema/renames-and-retirements.md,docs/pages/reference/migrations.md - Modify:
docs/pages/guide/schema/migrations.md,docs/pages/guide/schema/overview.md,docs/pages/guide/schema/auto-migrate.md,docs/pages/guide/schema/deploying.md,docs/pages/guide/schema/data-steps.md,docs/pages/reference/cli.md,docs/examples/migrations_quickstart.py,docs/examples/migrations_rename.py,docs/examples/migrations_retire.py,docs/examples/migrations_retire_label.py,docs/examples/migrations_squash.py,docs/examples/migrations_quickstart_annotated.py,docs/examples/migrations_rename_annotated.py,docs/examples/migrations_retire_annotated.py,docs/examples/migrations_retire_label_annotated.py,docs/examples/migrations_squash_annotated.py - Test:
tests/test_docs_migrations.py
Interfaces
- Consumes: Step-file pinning (schema pages ticket (Schema management pages: fix stale facts and pin every quoted generated step file #713))
- Produces: New pages
guide/schema/squash.md,guide/schema/renames-and-retirements.md,reference/migrations.md, wired into the nav by the nav ticket (Navigation: six tabs, sentence-case labels that match page titles, moved pages redirected #723).
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.
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_migrations.py -q --no-cov -p no:cacheprovider -n 8
uv run --no-sync python -m tests._docs_checks docs/pages/guide/schema docs/pages/reference/migrations.mdBlocked by
Audit findings
Rows from the docs audit this ticket covers (file, line, rule, fix)
docs/pages/guide/schema/migrations.md
- Kind (rule 1): mixed. It holds a tutorial-ish loop (init → new → up), reference (header table,
check/status/driftexit codes,--replacerules, squash rules), and several how-tos (renames, retire column, retire label, hand-written steps, two branches, squash, change a PK). At 664 lines it is the longest page in the section. - Running example (rule 3):
Author+nicknamethrough "Reading a migration". It then drifts toWriter/handle/Status(renames),Author.bio(retire),Order/Status(retire label),0002_add_x/add_ywithbio/nick(branches),Team/tmp_code(squash),Author.handle/bio(squash rename), andWriter/Book(PK). - Verdict: The content is strong and the CLI output is pinned by tests. However, two SQL blocks are stale against the generator, one sentence contradicts the overview and auto-migrate pages on table renames, and the page holds five how-tos that each deserve their own page. C
- Findings:
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| H | 214 | fact | "Auto-migrate honours the column and label hints but not a table's: its create pass builds the new table name as a new, empty table." This is stale. overview.md:64, auto-migrate.md:55 and CONTEXT.md (Create pass: "a table a live rename hint says already exists under its old name is not missing, so the pass holds it back") all say the pass renames under migrate_updates and otherwise warns and creates nothing. |
"Auto-migrate honours every hint under migrate_updates. Without it, a table hint makes the create pass hold the table back with a warning rather than create an empty one (what each door covers)." |
| H | 112–121 | 9, fact | The 0001_create_author SQL is stale. The running example's 0001 model has email: str = Field(unique=True) (migrations_quickstart.py, tests/test_docs_migrations.py:AUTHOR). The generator actually writes CREATE TABLE IF NOT EXISTS "author" ( "email" varchar NOT NULL, "id" serial PRIMARY KEY NOT NULL, "name" varchar NOT NULL ); plus CREATE UNIQUE INDEX IF NOT EXISTS "uq_author_email" ON "author" ("email"); (SQLite likewise). The page shows id, name only, in a different column order and with no unique index. The block is not pinned by any test. |
Paste the real files. Then add a test, as the <!-- output: --> blocks have, that regenerates every -- NNNN_…/…sql block the guide quotes and compares it byte for byte. |
| H | 327–339 | 9, fact | The SQLite rebuild block (0002_author_slug/04_contract.up.sqlite.sql) is not what the generator writes. The real file also has DELETE FROM sqlite_sequence … / INSERT INTO sqlite_sequence … (which keep the AUTOINCREMENT counter) and recreates CREATE INDEX IF NOT EXISTS "idx_author_name" …, because the backfill example's name has index=True. The page says the rebuild "recreates the table and its indexes", yet the block shows no index. The same stale block is duplicated in data-steps.md:69–81. |
Paste the real file. Show the rebuild once (on data-steps or here) and link to it from the other page. |
| M | 598 | 1 | "Two pins in tests/test_cross_emitter_parity.py hold a squash to this page … (h) … (i)", with links into AGENTS.md. This is contributor-internal material in a user guide. |
Delete it. If a guarantee belongs here, state it as product behaviour: "A squash writes exactly the files new would write across the range." |
| M | 458–598 | 1, 5 | The squash section (about 140 lines, four subsections) is a full how-to inside a page that is already a how-to collection. The heading "Squashing an epic's migrations" uses the project's internal SDD word "epic". The subheadings "A squash is pre-release" and "Every database that applied the originals inherits the squash" are sentences, not names. | Move it to its own page, guide/schema/squash.md ("Squashing migrations"). Use "a feature branch's migrations". Headings: "Squash timing", "Inheritance on databases that ran the originals", "Renames inside the range", "squash vs --replace". |
| M | 1–664 | 1 | The page splits by topic, not by kind. Renames, retiring a column or label, two branches, squash and changing a PK are each how-tos. Headers, check/status/drift exit codes and --replace rules are reference. |
Keep migrations.md as the loop: init, new, read, up, check, down. Split out a "Renames and retirement" page (renames, retire column, retire label: all rolling-deploy shapes), "Branches and squashing" (--replace, squash), and move "Changing a primary key" to howto/. Move the header table and exit codes to reference/cli.md, linking there instead. |
| M | 13, 296, 307, 413, 469, 604 | 7, 22 | Commands are written as a backticked paragraph (ferro migrate init --models …) rather than a fenced bash block. On line 13 the bare command line is the lead-in itself, which is not a sentence ending in a colon. |
"Pass every answer as a flag and init asks nothing:", then a ````bashblock with the command, then thetext` output block. |
| M | 7–9, 92 | 7 | The install block and the directory-tree block have no lead-in sentence. | Line 7: "Install the CLI extra:". Line 92: "new writes one directory per migration:". |
| M | 400–431 | 10, 3 | The two-branches example uses placeholder names add_x / add_y / nick that carry no meaning, while the page's own example is Author.nickname. |
Use 0002_author_bio and 0002_author_nick (or _email), so the regenerated directory reads 0003_author_bio. |
| M | 247–249 | I-7 | The retirement table shows the declaration in assignment style only (bio: str = Field(index=True)). |
Acceptable inside a table cell, but add "(or Annotated[...], as above)". Alternatively, write the cell in prose: "bio required, indexed" → "bio nullable, retired=True". |
| L | 134 | 1, 20 | The header table carries version history: "A ferro older than this header refuses the file…". | Move it to a version note under the table, or to the changelog. |
| L | 489 | 22 | "A squash is a one-way step for older ferro releases": "step" is a domain term (Step), so this overloads it. | "Older ferro releases cannot read a squash; read …". |
| L | 159, 288, 375, 394, 398, 586 | 5 | Several headings name no feature: "Changes existing rows need a value for", "Going back", "Editing a migration", "Two branches, one number", "Squash or --replace". |
"Backfills (changes existing rows need a value for)", "Reverting with down", "Edited migrations", "Duplicate migration numbers and --replace", "squash vs --replace". |
| L | 88, 216, 257, 286, 543 | 22 | Example scripts are linked by GitHub main URL. On a released docs version these point at a moving branch. |
Link to the tagged version, or embed the relevant region with --8<--. |
| L | (examples) migrations_retire.py:16 vs _annotated.py:19 |
I-7, 12 | The two tabs are not equivalent. Assignment has bio: str | None = Field(index=True, retired=True) with no default; Annotated has = None. |
Give the assignment form default=None, matching every other nullable field in the examples. |
| L | (examples) migrations_retire_label_annotated.py:26 |
I-7 | status: Annotated[Status, FerroField()] = Status.PAID uses an empty FerroField(), while the assignment tab has none. |
Drop the empty FerroField(). The line is then identical in both tabs, as I-7 allows. |
Does the section split cleanly by kind? (rule 1)
The seven pages split by topic, and only roughly by kind. The overlaps:
- Three coverage matrices for one question. "Which change does which door handle?" is answered by
overview.md"What each door covers" (14 rows),auto-migrate.mdlines 29–57 (29 rows, per backend), andalembic.md"What autogenerate refuses". They already disagree once (the Alembic/SQLite required-column row). Recommendation: one reference page holds the full per-backend matrix, and the guides keep a short summary and link to it. check/status/drift/ exit codes appear inmigrations.md"Checking",deploying.md"In CI" and "Where a database stands", andreference/cli.md. Let the CLI reference own the codes.- The SQLite table rebuild block appears twice, verbatim and stale in both places:
migrations.md:327anddata-steps.md:69. - Rolling deploys / two releases are spread across
migrations.md(rename online, retire column, retire label, PK recipe's last paragraph),data-steps.md(late row) anddeploying.md(two-release contract, step classes). Recommendation: retiring a column or label is the how-to half of the two-release contract, so "Renames and retirement" plus the two-release contract could be one "Changing schema under a rolling deploy" page. migrations.mdis five how-tos plus reference wrapped around a tutorial loop. Split it as described in its table above.squashespecially warrants its own page.- Release-specific notes (
deploying.md"Upgrading to the release withsquash", the version history in the header table, "Added in 0.11.0") belong in upgrade notes or the changelog, not in guides.
data-steps.md and testing.md are cleanly scoped. overview.md is a proper landing page once the "old page" map goes.
Cross-cutting patterns
- Generated-file blocks are unpinned and three have rotted (3 blocks, 2 files). The
<!-- output: -->CLI blocks are tested and correct. The-- NNNN_…/….sqlfile blocks are not tested, and three are stale against the generator:0001CREATE TABLE, and the SQLite rebuild twice. Fix: extendtests/test_docs_migrations.pyto regenerate and compare every quoted step file, the way it already does for output. - Title-case headings (12): every "See Also" (7) plus the page titles "Schema Management", "Data Steps and Backfills", "Deploying Migrations", "Testing Migrations", and the nav labels. Rule 5 asks for sentence case.
- Headings that are clauses, slogans or metaphors, not names (≈18): "The ladder", "Choosing", "Going back", "Two processes at once", "SQLite: warn and skip", "One decider", "A squash is pre-release", "Every database that applied the originals inherits the squash", "When a late row arrives", "Squash or
--replace", and others. - Code blocks missing a lead-in sentence (≈14): auto-migrate 9, 226; migrations 7, 92 plus 6 commands written as bare inline-code paragraphs; data-steps 145, 156, 202; alembic 9, 17, 84; testing 62. There are also 3 places in data-steps with doubled lead-ins.
- Internal references in user prose (12): 9 ADR numbers across 6 pages, the AGENTS.md "pins" paragraph, the SDD word "epic", and "One decider".
- Running example drifts within a page (4 pages): migrations.md uses 7 different models, auto-migrate.md 4, and alembic.md has none (
card). data-steps, testing and deploying holdAuthor.slugwell. - Reference material embedded in guides (5 large blocks): the auto-migrate matrix, the
PassReportkinds, the migration header table, the exit codes (twice), and the expand/contract table. Each should live once inreference/orapi/and be linked from the guides. - Time-bound or version-history text in guides (6): "Written 2026-10-18…", "first release after 0.24", "Added in 0.11.0" ×2, "Added in 0.18.0", "0.18.0+", the 0.22 record-class paragraph, and the header-table note about older ferro.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
documentationImprovements or additions to documentationImprovements or additions to documentationready-for-agentUnblocked and fully specified; an agent may claim itUnblocked and fully specified; an agent may claim itsize:LCross-cutting or multi-worker; local waves onlyCross-cutting or multi-worker; local waves only
Activity
docs: schema pages fixed against the generator, quoted step files pin…
0x054 commentedon Oct 19, 2026
Status: verify-passed
Worker: worker-714
Branch:
sdd/714-migrations-splitPR: #744
Verify: passed cf83b7a; trial merge into
feat/docs-rewriteat 7bf0e39 passedScoped re-review by reviewer-714: Spec PASS, Quality PASS.
migrations.md:55.test_docs_migrations.pypasses in full; the one skip needs Postgres. That fixes the base's RLS failures.squash.md:3.docs(guide): sessions-first connections, mutations, transactions and …
docs(guide): split the migrations guide into the loop, renames and re…
docs(guide): name the migrations guide's sections after their features (
test(docs): pin the row-level security guide's quoted step files, and…
fix(docs): repoint the upgrade guide's links into the split migration…
docs: split the migrations guide into the loop, renames and retiremen…
0x054 commentedon Oct 19, 2026
Merged in #744 into
feat/docs-rewrite