Repository navigation
docs: 0.26 notes state the secret-column change; RunRefused exported and documented (panel Round 3 F42, F44) - #784
Merged
Conversation
0.25 wrote a SecretStr/SecretBytes column as the mask `**********`, in field order, loaded it back as the str mask, and created SecretBytes columns as varchar. The 0.26 notes said a secret column used to come last and that rows were unchanged. Drop "or secret" from the order bullet and add a fifth bullet: the real value is stored and loads as the secret type, 0.25 rows hold an unrecoverable mask, and a SecretBytes column needs a migration to BYTEA/BLOB before 0.26 saves into it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
The tracked-database example prints `RunRefused`, a class no page documented and ferro.migrations did not export. Export it, document it in the migrations API page and the exceptions tree, and say on the Overview page what to catch. The printed-output check now fails a block that names a Ferro exception class no public path exports. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
0x054
commented
Oct 21, 2026
0x054
left a comment
Contributor
Author
There was a problem hiding this comment.
Spec: FAIL
- S1 · Critical · docs/pages/guide/schema/overview.md:50 at 640726c · Misunderstood · "A
RunRefusedfrom a migration run names its refusal inkind" is false. Several refusals raised during a run carrykind=None. Examples:runner.py:828(_order_keys_for, "Nothing was applied/reverted"),runner.py:949(HistoricalModelError→ "Nothing more was run."),runner.py:1453(down) andrunner.py:459. The class's own attribute docstring sayskindisNonefor "a refusal that is not the planner's". A reader who matcheskindon every run refusal getsNone. F44 asks for an accurate sentence telling the reader what to catch. The catch half is right. Fix: "A refusal the run planner makes names itself inkind; this one is not the planner's, and itskindisNone." The same overclaim appears at docs/pages/api/exceptions.md:25 (".kindnames why"). - F42: verified. Every claim in the new bullet holds at 640726c and at v0.25.0. 0.25 wrote the mask on
save()/bulk_create()(model_dumpoverSecretBytes) and onupdate()(to_jsoninupdate_bind_payload).bytesalone came last (overlay after the dump). A probe over a hand-madevarcharSecretBytescolumn gave these results:- Postgres:
save(SecretBytes(b"\xff\x00k"))succeeds, the raw value is'\\xff006b', andgetreturnsSecretBytes(b'\\xff006b'). No error, as the page says. - SQLite: the value round-trips (stored as a blob).
- The pass plans
ALTER TABLE "vault" ALTER COLUMN "blob" TYPE bytea USING "blob"::byteaon Postgres. On SQLite it points toferro migrate new. The IRformatchanges frompasswordtobinary, so a snapshot diff sees it. - The count is now "Five changes". The section is pinned by
test_the_upgrade_guide_states_the_secret_column_change.
- Postgres:
- F44 otherwise met:
__all__export, API page entry, exceptions tree (MRO matches), public-only recogniser plus the internal-name test.kindisNonefor the connect refusal (run.rs:69raisesRunRefused(text)). The strict build rendersferro.migrations.RunRefused.kindonce. - PR body carries both required lines. Every file is in the allowed list.
Cannot verify: none
Quality: PASS
- Minor · docs/pages/api/exceptions.md:25 at 640726c · "a migration run refused before it ran anything" now also covers
connect(auto_migrate=True), which is not a run, and.kindis oftenNone(see S1). Fix: "a migration run, or auto-migrate on a tracked database, refused before anything ran".
Verify: PR head 640726c passed (strict build clean; pytest 1429 passed on sqlite+postgres with FERRO_POSTGRES_URL set; _docs_checks exit 0; ruff check and format clean)
Trial merge: not run (base unchanged)
RunRefused.kind is None on many refusals, the tracked-database refusal of connect(auto_migrate=True) among them. The Overview and the exceptions tree said kind always names the refusal. State that only some refusals carry a kind and that the others are matched on the message or the MigrationRefused base; name connect(auto_migrate=True) as a door that raises RunRefused. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP
0x054
commented
Oct 21, 2026
0x054
left a comment
Contributor
Author
There was a problem hiding this comment.
Re-review: 640726c → 2775816
- S1 · ADDRESSED · docs/pages/guide/schema/overview.md:50, src/ferro/migrations/report.py:55-57 and :84-89 at 2775816. No sentence on either page or in the
RunRefuseddocstrings now claims every refusal carrieskind.- Overview: the block's refusal really has
kind=None.src/run.rs:69refused()callsRunRefused(text), andsrc/migrate.rs:643supplies the text. - The
kinddocstring names three refusals withkind=None, and each raise site confirms it:- the tracked-database refusal:
refused()as above; - a lost run lock:
src/run.rs:215,217,LockCheckFailed::into_py→refused(...); - a data step that does not load:
src/ferro/migrations/runner.py:855,RunRefused(f"{refused}. Nothing was {nothing}.")with nokind.
- the tracked-database refusal:
- Overview: the block's refusal really has
- Q1 · ADDRESSED · docs/pages/api/exceptions.md:25 at 2775816. The tree now names both doors: "a migration run, or connect(auto_migrate=True) on a tracked database, refused".
New findings in the fix diff: none at Critical or Important.
Out-of-scope observations: none.
Verify: PR head 2775816 passed. Strict build clean. pytest: 1430 passed on sqlite+postgres, with FERRO_POSTGRES_URL set. _docs_checks exit 0. ruff check and format clean.
Trial merge: not run (base unchanged, f20bcca)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two findings from the panel's Round 3 report on #777, fixed before merge.
F42 · the 0.26 notes misstated the secret-column change
docs/pages/upgrading.md, "Upgrading to 0.26":SecretStr/SecretBytescolumn was dumped in field order, as the mask. "The rows written are the same" is now "The order changes no value written".update()'s clause names onlybytes.SecretStr/SecretBytes, where 0.25 wrote**********(onsave(),bulk_create()andupdate()) and loaded thestr**********. A row 0.25 saved holds the mask and cannot be recovered. ASecretBytescolumn is nowBYTEA/BLOB, where 0.25 createdvarchar, and it needs a migration before 0.26 saves into it.I checked one fact by hand, and it differs from #762's body. On this branch a
SecretBytesfield over a 0.25varcharcolumn loads without error on SQLite and on Postgres; I saw noFailed to hydrate. The real hazard is the write. On Postgres,save()ofSecretBytes(b"\xff\x00k")into thevarcharcolumn stores the text\xff006b, and that text loads back as the bytesb"\\xff006b". The bullet says exactly that. On SQLite the value round-trips.Covering test:
tests/test_save_column_order.py::test_the_upgrade_guide_states_the_secret_column_change. It checks that the order bullet has no "secret column came last" and that the section opens with "Five changes". It also checks that the secret bullet namesSecretBytes,**********, "cannot be recovered",varchar,BYTEA,BLOBand "migration".F44 · the Overview printed
RunRefused, a class nothing exportedferro.migrations.RunRefusedis exported (__all__). It appears in the API migrations page as::: ferro.migrations.RunRefusedand in the exceptions tree, betweenMigrationRefusedand its two subclassesAlreadyTrackedErrorandAppliedAboveBaselineError.report.py, docstring only) now says thatconnect(auto_migrate=True)also raises it and thatexcept MigrationRefusedcatches it. It gains anExamples:block that matches onkind.kindkeeps its attribute docstring. I did not add anAttributes:section: every class in the package documents fields that way, and a section would renderkindtwice.overview.mdnow says, after the block, that the error is aRunRefused, a subclass ofMigrationRefused, soexcept ferro.migrations.MigrationRefusedcatches it. It also says thatkindnames a run's refusal and isNonefor this one. I checked: this refusal'skindisNone, so "whosekindnames the refusal" would have been wrong here.tests/test_docs_output.py:_ferro_exception_namesnow lists only public classes. A public class is one inferro.__all__orferro.migrations.__all__, or a public class offerro.exceptions. A new check,test_a_printed_exception_name_is_one_a_reader_can_import, runs over every text block and fails a line that opens with an internal Ferro exception class.test_a_block_naming_an_internal_exception_class_failspins the check withStepRefused, bare and dotted, andtest_the_recogniser_names_only_public_exception_classespins the list.Interfaces changed: ferro.migrations.RunRefused is exported (new public name; the class existed and was raised before)
Verify (on 640726c)
uv run --no-sync zensical build --clean --strictpytest tests/test_doc*.py tests/test_docstring_examples.py tests/test_routing_errors.py tests/test_save_column_order.py tests/test_step_class.py -n 8 --db-backends=sqlite,postgrespython -m tests._docs_checks docs/pages docs/examples src/ferroruff check src/ferro tests docs/examples && ruff format --check .pytest tests --db-backends=sqlite,postgres -n 8Verify block corrected: no
For the reviewer
On Postgres, a
SecretBytesvalue saved into a 0.25varcharcolumn is stored as hex text and loads back wrong, with no error. The upgrade note warns about it. The code may deserve its own issue; I leftsrc/alone here.src/ferro/migrations/errors.py's module docstring still saysRunRefusedlives "beside it inferro.migrations.report". That file is outside this slice, and the sentence still holds.Issue status: no issue closes here; this is part of Docs rewrite: every snippet runs, every fact matches the code, and the API reference renders cleanly #698.
Part of #698 (panel Round 3 on PR #777)
🤖 Generated with Claude Code
https://claude.ai/code/session_01Hb9U9A7Zu9ypdHTsDvdScP