ReviewOS

also looking at this

stacks/bun-query-builder

fix: an unbounded string column is text on Postgres, not varchar(255)

#1191
Merged glennmichael123 wants to merge fix/postgres-unbounded-string-is-text into main
5 files +89 -15
8 days ago

What happens

A string column with no declared .max(n) is generated as varchar(255) on every dialect. On Postgres a column the model never bounded then rejects its own writes:

PostgresError: value too long for type character varying(255)

Seen live on a query log table in a Stacks app, which dropped exactly the long, slow queries it exists to record. Full write-up: stacksjs/stacks#2965.

Why 255 was wrong here

The 255 default is a MySQL idiom: there, TEXT cannot be indexed without a prefix length, so a bounded varchar is worth defaulting to. Postgres stores text and varchar(n) identically and indexes both, so the default bought nothing and cost correctness. "No bound declared" means the author did not bound the column, which is text.

What changes

Three places had to agree, or the fix would have been worse than the bug:

WhereBeforeAfter
PostgresDriver.getColumnTypevarchar(${maxLength ?? 255})text when unbounded, varchar(n) for a declared .max(n)
canonicalStorageTypevarchar(255) for the model sidetext, matching what a live text column introspects back as
isNarrowingChangetreated unbounded as 255 on every dialecttakes the dialect; unbounded is unlimited on Postgres

Without the second, every reconciliation would diff the model's varchar(255) against the live text and re-ALTER an unchanged column forever. Without the third, dropping a bound on Postgres would read as a narrowing to 255 and be flagged destructive, when it is a widening to text. Adding a bound to an unbounded column is still destructive, since existing text may not fit.

MySQL keeps varchar(255). SQLite was already text.

Tests

  • varchar-width.test.ts: Postgres DDL asserts "path" text for the unbounded column alongside varchar(32), varchar(2), varchar(320) for bounded ones; MySQL unchanged.
  • migrations.postgres-varchar-length.test.ts: canonical type is text and agrees with the driver; dropping a bound widens non-destructively; adding one is destructive; an unchanged unbounded column produces no operation; MySQL default left alone.
  • migrations.attributes.test.ts: the PostgreSQL types test pinned varchar for an unbounded string, which was the defect. It now asserts text for that column and varchar(40) for a bounded one added beside it.

Full suite: 2505 pass. The one failure, CLI > version prints package version, fails identically on clean main and is unrelated.

Blast radius

This changes generated DDL for every unbounded string column on Postgres. Existing databases are not altered by this PR; a reconciliation run against them will see the widening as a safe, non-destructive ALTER COLUMN ... TYPE text, which is the point of the isNarrowingChange half.

🤖 Generated with Claude Code

5 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.