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:
| Where | Before | After |
|---|---|---|
PostgresDriver.getColumnType | varchar(${maxLength ?? 255}) | text when unbounded, varchar(n) for a declared .max(n) |
canonicalStorageType | varchar(255) for the model side | text, matching what a live text column introspects back as |
isNarrowingChange | treated unbounded as 255 on every dialect | takes 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" textfor the unbounded column alongsidevarchar(32),varchar(2),varchar(320)for bounded ones; MySQL unchanged.migrations.postgres-varchar-length.test.ts: canonical type istextand 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 pinnedvarcharfor an unbounded string, which was the defect. It now assertstextfor that column andvarchar(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