Skip to content

SecretBytes saved into a 0.25 varchar column on Postgres is stored as escaped text with no error #785

Open

Description

@0x054

Problem

A SecretBytes column created by Ferro 0.25 is varchar on Postgres (0.25 wrote every secret as the mask **********, so the column was text). Ferro 0.26 binds the real bytes. On Postgres, save() of b"\xff\x00k" into that old varchar column succeeds and stores the text \xff006b; a fetch returns SecretBytes of those seven characters, not the three bytes written. No error on either door.

class Token(Model):
    id: int = Field(primary_key=True)
    secret: SecretBytes          # 0.25 created this as varchar on Postgres

await Token(id=1, secret=SecretBytes(b"\xff\x00k")).save()   # 0.26: stores '\xff006b', no error
token = await Token.get(1)
token.secret.get_secret_value()                                  # b'\\xff006b', not b'\xff\x00k'

connect(auto_migrate=True) does not report the column: the live type (varchar) and the declared type (bytea) differ, and the pass's type-change rung is migrate_updates/migrate_destructive, so a default connect says nothing.

Reproduced by the worker and independently by the reviewer on PR #784 (#784 (review)). The 0.26 upgrade notes state the fact; this issue is the code-side fix.

What a fix needs

AGENTS.md I-6: fail loudly. A binary declared column backed by a text live column is a type mismatch the pass already knows about (it plans the ALTER COLUMN ... TYPE for migrate_updates). The question is whether a save into a column whose live type cannot hold the declared type should refuse, and whether connect() should warn when it sees the mismatch and is not asked to fix it. Candidates, to grill:

  1. connect(auto_migrate=True) emits a warning per column whose live type and declared type disagree in storage class (text vs binary), naming the rung that fixes it (ferro migrate up or migrate_updates).
  2. The runtime refuses to bind a bytes value for a column the live schema says is text on Postgres. Costs a live-schema read on the bind path; probably the wrong door.
  3. Nothing in src/, documented only (rejected: that is the current state, and the corruption is silent).

Out of scope here: the SQLite side (text affinity stores the blob as written, so the round trip holds; verify before closing).

Related

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageNeeds evaluation by a maintainer

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions