Skip to content

fix: panel round 1 on #777 (src) — restore status flags, PoolConfig.validate and the no-route message; save(only=) in field order; docstring fences marked - #779

Merged
0x054 merged 8 commits into
feat/docs-rewritefrom
sdd/698-fix-src-round-1
Oct 21, 2026
Merged

0x054 merged 8 commits into
feat/docs-rewritefrom
sdd/698-fix-src-round-1

Conversation

@0x054

@0x054 0x054 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Panel round 1 on #777, the src/ findings. Each section says what changed, the test that pins it, and the command that ran it.

F1 · ferro migrate status --no-steps / --no-json restored

negative="" came off status's steps and json_. No other verb's parameters gained it in the epic: every other negative="" in src/ferro/cli/migrate.py predates the branch (checked against f3e46f8).

$ ferro migrate status --no-steps     # before: UnknownOptionError: Unknown option: --no-steps. Did you mean --steps?

Pinned by tests/test_cli_help.py::test_status_accepts_both_spellings_of_its_flags (all four spellings parse to the right bool) and ::test_status_help_lists_the_negative_flags. RED before the fix: 3 failed (--no-steps, --no-json, help).

docs/pages/reference/cli.md:245 lists only --steps; that page belongs to the docs worker this round.

F3 · PoolConfig.validate restored

pool = PoolConfig(max_connections=3, min_connections=1)
pool.validate()   # before: TypeError: BaseModel.validate() missing 1 required positional argument: 'value'

The validator is validate again, with a short reference docstring (Returns / Raises). Pinned by tests/test_connection.py::test_pool_config_validate_returns_the_instance. RED before the fix, GREEN after.

F4 · select() names AttributeError

Author.select("zzz") and Author.select(lambda author: author.zzz) raise AttributeError: Author has no queryable column 'zzz'…. Model.select's Raises: now lists it, and its ValueError entry names the cases that actually raise one (empty selection, duplicate output name, a string naming a related column). Query.select did not claim ValueError for a missing column, but it left out the AttributeError, so that entry is added there too. I checked by running each call.

F5 · Session.query typed: kept, noted

docs/pages/upgrading.md gains an Upgrading to 0.26 section: Session.query now takes type[M] and returns Query[M], so a type checker may flag a call that passes a non-Model class. The "Which sections apply" table and the intro range now run to 0.26. The next release is assumed to be 0.26, since main is at 0.25.0 plus feat commits; rename the section if release tooling picks another number.

F6 · The no-route RuntimeError text now names the release that removed routing

The rewrite reworded _NO_ROUTE_MESSAGE under a docs commit, and the panel caught it. Restoring the shipped text was not an option either, because that text is wrong: implicit default-connection routing was removed in v0.14.0, not v0.13. The removal is a328cfc (feat(ff-d)!: … single-handle routing (#201), 2026-07-03). CHANGELOG lists it under v0.14.0, git tag --contains a328cfc starts at v0.14.0, and the upgrade guide files "Session routing" under Upgrading to 0.14. The "sessions migration guide" the old text pointed to no longer exists either.

The message now reads …removed in v0.14 (deprecated since v0.12); see the upgrade guide. This PR declares the change openly (see Interfaces changed). docs/pages/guide/connections.md:119 quotes it exactly, and the page's !!! note "Changed in 0.13" (line 187) now says 0.14. Pinned by tests/test_routing_errors.py::test_no_route_message_is_the_shipped_text, which checks the full string.

F12 / #776 · Writes list columns in field declaration order

class Customer(Model):
    id: int | None = Field(default=None, primary_key=True)
    email: str = Field(unique=True)
    name: str = ""
    plan: str = "free"
    views: int = 0
    updated_at: datetime | None = None

await customer.save(only={"updated_at", "name"})
# before (varied per process): UPDATE "customer" SET "updated_at" = ?, "name" = ? WHERE "id" = ?
# after (every run):           UPDATE "customer" SET "name" = ?, "updated_at" = ? WHERE "id" = ?

There were three causes, and all three are fixed in src/ferro/_bind_payload.py:

  1. only= / exclude= filtered the payload through a set, so columns came out in hash order. The filter now walks the payload in order.
  2. A fetched instance dumped in its __dict__ order. Hydration fills __dict__ in the SELECT's alphabetical order, so even a full save() of a fetched row was alphabetical, not declared.
  3. Bytes and secret columns are bound raw, and they were appended last.

save_bind_payload now returns its map in field declaration order. That covers save(), save(only=), save(exclude=) and bulk_create. Query.update(**fields) keeps the call's keyword order. No other write takes a set.

Pinned by tests/test_save_column_order.py. Its models declare their fields out of alphabetical order, with a bytes and a secret column in the middle, and it runs on both dialects: save(only={"a", "b"}) → SET "b", "a" in field order; only= with 7 columns; exclude=; a full save() and INSERT; a fetched instance with no raw columns; bulk_create; and update() keyword order. Before the fix, 10 of 12 failed. The fetched-instance case was added once the docs gate showed alphabetical output, and it failed 2/2 until the third cause was fixed.

Then:

  • tests/test_docs_sql.py: the PYTHONHASHSEED=0 env line and its docstring sentence are removed. The now-unused import os went with them, because ruff F401 required it; nothing else in the file changed.
  • docs/pages/guide/mutations.md: the four save-only / save-exclude SQL blocks are regenerated (SQLite and Postgres), and the sentence saying the order is not fixed is deleted. Exactly these four blocks differed on the gate.
  • Capturing mutations.py under PYTHONHASHSEED=1,2,3 gives byte-identical output (same md5).

F16 · ferro.migrations._chunk_loop: kept, noted

The rename stays. The 0.26 upgrade section says the never-documented ferro.migrations.chunked module is now the private ferro.migrations._chunk_loop, and that from ferro.migrations import chunked still imports the decorator, unchanged.

Coherence · Every docstring fence in src/ferro is marked

grep -rn '```python$' src/ferro found 19 fences. None can run against the reference prelude: they need a migrations project, a data-step context, Alembic or Postgres row security, or they redeclare the prelude's own models. Each is now {test=compile reason="…"} with the reason. Two fences listed bare values (report.head_applied.name # "0006_add_teams", db.directory # …) and are rewritten as code: assert report.head_applied.name == "0006_add_teams", and directory = db.directory. grep -rn '```py' src/ferro | grep -v 'test=' is now empty.

Verify

$ FERRO_POSTGRES_URL=postgresql://postgres:password@localhost:5432/postgres uv run --no-sync pytest tests/test_save_column_order.py tests/test_routing_errors.py tests/test_connection.py tests/test_cli_help.py tests/test_docs_sql.py tests/test_docstring_examples.py tests/test_statement_log.py tests/test_rendered_statement.py -q --no-cov -p no:cacheprovider --db-backends=sqlite,postgres
581 passed in 35.16s
$ uv run --no-sync ruff check src/ferro tests && uv run --no-sync ruff format --check .
All checks passed!
343 files already formatted
$ uv run --no-sync ty check src/ferro/query tests/test_query_typing.py tests/test_static_contracts.py tests/test_alembic_typing.py
All checks passed!
$ uv run --no-sync zensical build --clean --strict
No issues found
$ uv run --no-sync pytest tests -q --no-cov -p no:cacheprovider -n 8
3735 passed, 449 skipped, 2 xfailed, 1 warning in 174.51s
$ FERRO_POSTGRES_URL=… uv run --no-sync pytest tests -q --no-cov -p no:cacheprovider -n 8 --db-backends=sqlite,postgres -k "save or bulk or secret or bytes or blob or update"
460 passed, 1 skipped
$ uv run --no-sync python -m tests._docs_checks src/ferro docs/pages/upgrading.md docs/pages/guide/mutations.md docs/pages/guide/connections.md
(exit 0)

Postgres was accepting connections on localhost:5432; the Postgres halves above ran.

Interfaces changed:

  • The RuntimeError text for an operation with no route now reads: "No database route for this operation. Open a session (async with ferro.engines.session(...)) or pass using=/session= explicitly. Implicit default-connection routing was removed in v0.14 (deprecated since v0.12); see the upgrade guide." The old text named v0.13 (routing was removed in v0.14.0, a328cfc) and a page that no longer exists. Code matching on "No database route" is unaffected.
  • PoolConfig.validate, migrate status --no-steps / --no-json and the no-route message are restored to their shipped form. save() / save(only=) / save(exclude=) / bulk_create now list columns in field declaration order (the statement text changes; the rows written do not).
    Verify block corrected: no

Exit steps

Fix report: F6 correction (88ae5e5)

The coordinator corrected the F6 decision: the goal was no silent behaviour change under a docs commit, not keeping the old wording. This commit sets the no-route message to the accurate v0.14 / upgrade-guide wording, updates the exact-text pin, quotes it at connections.md:119, fixes Changed in 0.13 → 0.14 at connections.md:187, and declares the change under Interfaces changed.

$ uv run --no-sync pytest tests/test_routing_errors.py tests/test_docs_examples.py tests/test_docs_sql.py -q --no-cov -p no:cacheprovider -n 8
324 passed, 107 skipped in 21.81s
$ uv run --no-sync ruff check src/ferro tests && uv run --no-sync ruff format --check .
All checks passed!
343 files already formatted
$ uv run --no-sync zensical build --clean --strict
No issues found

Closes #776
Part of #698 (panel round 1 on PR #777)

🤖 Generated with Claude Code
https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP

0x054 and others added 7 commits October 21, 2026 14:22
A docstrings commit set negative="" on status's two bool flags, which
removed the --no-steps / --no-json spellings that shipped. Drop it, and pin
both spellings and their place in --help.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
The bounds validator was renamed _check_bounds, so PoolConfig(...).validate()
fell through to pydantic's deprecated classmethod and raised TypeError.
Restore the public name, give it a reference docstring, and pin that it
returns the instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
The message was reworded in a docs commit; callers may match on it.
Restore it, pin it exactly, and quote it as sent on the Connections page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
Model.select and Query.select raise AttributeError for a column the model
does not have; Model.select's entry said ValueError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
save(only=) and save(exclude=) built their write set as a Python set, so the
UPDATE listed its columns in hash order, which changes per process. A fetched
instance also dumped in the SELECT's (alphabetical) order, and bytes and
secret columns were appended last. save_bind_payload now returns its map in
field declaration order and the write-set filter keeps that order, so
save(), save(only=), save(exclude=) and bulk_create emit one statement per
call on every run.

The docs SQL gate no longer pins PYTHONHASHSEED=0, the Mutations page's
save-only and save-exclude blocks are regenerated, and its sentence saying
the order is not fixed is gone.

Closes #776

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
…oop module

Add the 0.26 section: Session.query now takes type[M] and returns Query[M],
and ferro.migrations.chunked (never documented) is now the private
ferro.migrations._chunk_loop; the chunked decorator is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
Nineteen Python fences in module and class docstrings carried no {test=...}
marker. Each is now {test=compile reason="..."} with why it cannot run
against the reference prelude, and the two that listed bare values
(StatusReport's fields, FerroSettings' database) are written as code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
Implicit default-connection routing was removed in v0.14.0 (a328cfc), and the
sessions migration guide no longer exists, so the shipped text named the wrong
release and a missing page. Use the accurate wording, pin it exactly, quote it
on the Connections page, and correct that page's Changed note to 0.14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP

@0x054 0x054 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spec: PASS

Spec = the panel findings this slice owns on #777 (F1, F3, F4, F5, F6, F12/#776, F16, Coherence).

  • F1 ADDRESSED · src/ferro/cli/migrate.py:557-565 at 88ae5e5 · negative="" gone; tests/test_cli_help.py::test_status_accepts_both_spellings_of_its_flags / ::test_status_help_lists_the_negative_flags fail 3/5 on base src, pass on head.
  • F3 ADDRESSED · src/ferro/init.py:307 at 88ae5e5 · validate restored with a Google-style docstring; tests/test_connection.py::test_pool_config_validate_returns_the_instance fails on base src (TypeError), passes on head.
  • F4 ADDRESSED · src/ferro/models.py:794-798, src/ferro/query/builder.py:787 at 88ae5e5 · ran Author.select("zzz"), select(lambda author: author.zzz), Query.select("zzz") → AttributeError; empty lambda tuple, duplicate output name, "author.name" → ValueError, each as the new entries say.
  • F5 ADDRESSED · docs/pages/upgrading.md:72-77 at 88ae5e5 · typed Session.query noted in the 0.26 section.
  • F6 ADDRESSED · src/ferro/state.py:54-59 (unchanged from base, already the v0.14 wording), docs/pages/guide/connections.md:119 quotes it byte for byte, :187 now "Changed in 0.14"; CHANGELOG v0.14.0 carries the routing removal; declared under Interfaces changed:; pinned exactly by tests/test_routing_errors.py::test_no_route_message_is_the_shipped_text (passes on base too, since the text did not change in this PR).
  • F12 / #776 ADDRESSED · src/ferro/_bind_payload.py:150-172, :270-271 at 88ae5e5 · save_bind_payload returns field order with raw (bytes/secret) columns in place; _keep_write_set walks the payload. tests/test_save_column_order.py fails 10/14 on base src (sqlite and postgres: save(only=), save(exclude=), full save UPDATE + INSERT, fetched instance, bulk_create), passes on head. PYTHONHASHSEED pin removed (tests/test_docs_sql.py:417-428); mutations.md blocks regenerated and the "no fixed order" sentence gone (:328). Query.update keeps keyword order (pinned). FK shadow column checked by hand: author_id lands after declared fields, identically for new and fetched instances.
  • F16 ADDRESSED · docs/pages/upgrading.md:76 at 88ae5e5.
  • Coherence ADDRESSED · 19 fences in src/ferro now {test=compile reason="…"}; grep -rn '```python$' src/ferro and grep -rn '```py' src/ferro | grep -v test= both empty; tests/test_docstring_examples.py passes.
  • Upgrade guide: pyproject.toml version 0.25.0, CHANGELOG top v0.25.0, major_on_zero = false → next feat release is 0.26; the section name and table are consistent.
    Cannot verify: none.

Quality: PASS

  • Minor · PR body Interfaces changed: second bullet at 88ae5e5 · says "the no-route message [is] restored to its shipped form", contradicting the first bullet (the message is the new v0.14 text, not 0.25.0's v0.13 text). · The merge reader gets two answers. · Drop "and the no-route message" from the second bullet.
  • Minor · docs/pages/upgrading.md:74 at 88ae5e5 · "0.26 asks nothing of code that runs today" omits the no-route RuntimeError text change relative to 0.25.0 (v0.13 + "sessions migration guide" → v0.14 + "upgrade guide"), which the PR itself declares as an interface change; code matching the full string breaks. · One bullet in the 0.26 section.
  • Minor · commit c05df7a at 88ae5e5 · "restore the shipped no-route RuntimeError text" is reverted by 88ae5e5; net zero in state.py. Squash or leave; history only.
    Depth check: no module added and no interface changed (save_bind_payload's signature is unchanged; PoolConfig.validate is a restoration), so nothing to run it over.

Verify: PR head 88ae5e5 passed (named set incl. test_connection with --db-backends=sqlite,postgres 581 passed, Postgres accepting; PYTHONHASHSEED=3 … test_docs_sql.py -k mutations sqlite+postgres 35 passed, seed 7 sqlite passed; SQLite capture of mutations.py byte-identical under seeds 0/3/11; ruff check/format clean; ty clean; zensical build --clean --strict no issues; _docs_checks exit 0; full suite 3735 passed, 449 skipped, 2 xfailed; Postgres write subset 460 passed, 1 skipped)
Trial merge: not run (base unchanged, feat/docs-rewrite at 2a0205b = merge base)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant