Repository navigation
Migrating from SQLAlchemy: show sessions, make every block run #717
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:SOne context window, <= 3 files; cloud-eligibleOne context window, <= 3 files; cloud-eligible
Description
Parent
What to build
The page says Ferro has "no session to manage", and every Ferro snippet on it raises RuntimeError: No database route on 0.25.0, including transaction() blocks. Its comparison table says migrations are Alembic. The page explains Ferro's session against SQLAlchemy's, every Ferro block runs (moved into an example pair or marked to run), each SQLAlchemy/Ferro pair shows what the query returns, and the table names ferro migrate with the Alembic bridge as an option.
Pages: howto/migrate-from-sqlalchemy.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/examples/from_sqlalchemy.py,docs/examples/from_sqlalchemy_annotated.py - Modify:
docs/pages/howto/migrate-from-sqlalchemy.md
Interfaces
- Consumes: the Connections guide's existing sessions section (link target)
- Consumes: harness (harness ticket (Docs harness: run inline blocks with a prelude, run docstring examples, and check lambda names and error checks #704))
- Produces: nothing other tickets rely on
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 python -m tests._docs_checks docs/pages/howto/migrate-from-sqlalchemy.md docs/examples/from_sqlalchemy.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
Audit findings
Rows from the docs audit this ticket covers (file, line, rule, fix)
docs/pages/howto/migrate-from-sqlalchemy.md
- Kind (rule 1): how-to / comparison map (fits the kind).
- Running example (rule 3):
User(thenUser/Postfor relationships), reasonably held. - Verdict: The page says Ferro has "no session to manage", and every Ferro snippet on it raises
RuntimeError: No database route for this operationwhen run as shown. I reproduced this with both
User.create(...)andasync with transaction():against a connected SQLite database. The migrations
row is also stale, and it useslambda t:. Grade: C.
| Sev | Line | Rule | Finding | Suggested fix |
|---|---|---|---|---|
| H | 3, 13, 238 | fact (stale API) | "there is no session to manage", "Sessions: None — connections are managed for you", "Same shape, no session". Since 0.13/0.14 an operation with no ambient session and no using= raises RuntimeError (reproduced on 0.25.0, including inside async with transaction():). A SQLAlchemy user who copies these blocks hits that error on the first call. |
Say what is true: Ferro has a session, but it is a routing and identity-map scope, not a unit of work. There is no add(), flush() or commit(). Table row: "async with ferro.engines.session(): once per request; no add/commit". Show it once in Queries, and use it in Creating Records and Transactions. |
| H | 89, 103, 118–123, 140, 213–215, 233–235 | 9 | Every Ferro block runs outside a session and would raise. Inline blocks are only compile-checked, so the tests cannot catch it. | Wrap them (or state once "inside a session, as above"). Better: move the Ferro side into docs/examples/migrate_from_sqlalchemy.py with --8<-- sections so it runs. |
| H | 103, 215, 267 | I-8 | lambda t: t.age, order_by(lambda t: t.id, "desc"), t.amount.sum(). |
lambda user: user.age >= 18, lambda post: post.id, lambda order: order.amount.sum(). |
| M | 17 | fact (stale) | The table row says "Migrations: Alembic (via ferro.migrations.get_metadata)". Ferro now has first-party migrations (ferro migrate), as the page itself says at line 259. |
"ferro migrate (own migrations), or Alembic via get_metadata() + ferro_options()". |
| M | 71 | humanizer/17 | "you get validation, serialization, and FastAPI integration for free" is sales language. | "Validation and serialization come from Pydantic, so the model doubles as your API schema." |
| M | 76–141 | 7, 13 | The SQLAlchemy/Ferro pairs are good, but they are two separate blocks with a # SQLAlchemy / # Ferro comment instead of tabs, and they have no result sentence. |
Use content tabs "SQLAlchemy" / "Ferro" for each pair (rule 12). Add one sentence after each on what comes back (list[User], an instance with id set). |
| L | 5, 128, 261, 272 | 5 | Title-case headings ("Quick Comparison", "Creating Records", "What Has No Ferro Equivalent Yet", "See Also"). | Sentence case. "Gaps" or "Features without a Ferro equivalent" for the third. |
| L | 268 | clarity | "Postgres .merge() is shipped on the recipe door; .concat() is still coming." "Recipe door" is internal jargon. |
"update() recipes support Postgres .merge(); .concat() is planned." |
| L | 172–208 | I-7 | Both tabs are present but inline, and therefore only compile-checked, not run. | Back them with docs/examples/ files, as I-7 asks for snippet-embedded models. |
I-7 otherwise satisfied (Models and Relationships both tabbed).
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:SOne context window, <= 3 files; cloud-eligibleOne context window, <= 3 files; cloud-eligible
Activity
0x054 commentedon Oct 19, 2026
Status: verify-passed
Worker: sdd worker (PR #743), reviewed by reviewer-717
Branch:
sdd/717-from-sqlalchemyPR: #743
Verify: passed c24afa5
Review posted on #743: Spec PASS, Quality PASS (2 Minor).
Verify passed on the PR head and on the trial merge into
feat/docs-rewriteat acd1127.docs(guide): rewrite the SQLAlchemy migration how-to (#717) (#743)
0x054 commentedon Oct 19, 2026
Merged in #743 into
feat/docs-rewrite