Found while working #12121 (which lands the undeclared half of the same seam in packages/drivers/driver-sql/src/schema-drift.ts). ⛔ Deliberately not repaired there: #12121's scope is the case where the author declared no bound, and both cases below are the opposite half — the author did declare one, and the differ honours it in a way the emitter does not. Filed unassigned rather than ridden along.
Both measured on origin/main at d29e42f8, via direct diffManagedTable calls against a pre-existing varchar(255) column, dialect postgres. Duplicate search run before filing (targeted semantic search over this repo): the six hits are the closed family cards #11431 / #11566 / #11565 / #11794 / #11875 / #12017, none of which covers either case.
The shared root cause
The varchar differ treats varchar(field.maxLength) as the expected physical shape of any bounded field. SqlDriver.createColumn does not — and varcharColumnChars is its own read-only mirror of that switch, so what the emitter would build is knowable exactly. The two disagree in two measured directions.
Case A — an unkeyed, bounded TEXT-family field reports narrow_varchar at destructive
{ type: 'text', maxLength: 50 } over varchar(255) -> narrow_varchar error / destructive expected varchar(50)
{ type: 'richtext', maxLength: 50 } over varchar(255) -> narrow_varchar error / destructive expected varchar(50)
{ type: 'signature', maxLength: 50 } over varchar(255) -> narrow_varchar error / destructive expected varchar(50)
{ type: 'markdown', maxLength: 50 } over varchar(255) -> narrow_varchar error / destructive expected varchar(50)
Two problems, and the second is the serious one:
- The expectation is wrong. Unkeyed,
createColumn emits TEXT for every one of these (keyable = keyed ? keyableTextLength(field) : null). So the differ demands the column be narrowed to a shape the platform itself would never create. - It is
category: 'destructive', which refuses the boot.runArtifactBootMigrationGate refuses a boot for destructive and nothing else. So an already-serving deployment whose author adds maxLength: 50 to a legacy varchar(255) text column stops booting — over a divergence that changes no behaviour at all, because the write seam already enforces 50 for exactly BOUNDED_STRING_FIELD_TYPES (record-validator's max_length branch). No value above 50 can reach the column, so the column's extra width refuses nothing.
This is the same false positive #11431 closed for genuine TEXT columns (isCharacterColumn), arriving through a different door: the column is spelled varchar because an older release created it, not because the current emitter wants one.
Case B — a bound past the dialect ceiling plans DDL MySQL refuses
FieldSchema declares maxLength: z.number().int().min(1) — no upper bound — so this is authorable on any bounded-string type:
{ type: 'email', maxLength: 100000 } over varchar(255) -> widen_varchar warning / safe expected varchar(100000)
{ type: 'url', maxLength: 100000 } over varchar(255) -> widen_varchar warning / safe expected varchar(100000)
{ type: 'password', maxLength: 100000 } over varchar(255) -> widen_varchar warning / safe expected varchar(100000)
{ type: 'email', maxLength: 16384 } over varchar(255) -> widen_varchar warning / safe expected varchar(16384) <- first over the ceiling
{ type: 'email', maxLength: 16383 } over varchar(255) -> widen_varchar warning / safe expected varchar(16383) <- last legal one
declaredVarcharLength returns null above MAX_VARCHAR_CHARS (16383) and the column becomes TEXT, precisely so varchar(100000) is never emitted — MySQL refuses it (ERROR 1074 Column length too big … max = 16383). The differ has no such ceiling, so it plans ALTER … TYPE varchar(100000), categorised safe and therefore eligible for dev auto-reconcile as well as os migrate apply. Postgres accepts it (its own limit is 10485760) and MySQL fails the statement, which is the dialect-divergent enforcement of one declaration this package's conformance matrices exist to close.
Suggested shape of the remedy (not a decision)
One predicate rather than two patches: expect the width createColumn would actually emit. SqlDriver.varcharColumnChars(field, keyed) already answers that and is pinned against columnInfo() for every FieldType by sql-driver-11565-row-byte-budget.test.ts. null means "the emitter would not make this a varchar", which is the honest expectation in both cases above.
⚠️ It needs input the differ does not currently receive: which columns an index keys, since a keyed + bounded text field legitimately takes varchar(maxLength). indexedKeyColumns() already computes that map inside schema-drift.ts; the missing half is threading it from SqlDriver.detectManagedDrift into diffManagedTable, i.e. this touches sql-driver.ts and wants the serial slot on that file. #12121 avoided needing it by gating strictly on the declaration being absent, which is keyedness-independent; this card cannot.
Also worth deciding as part of it: Case A's category. Turning a boot-refusing destructive into a report is a behaviour change for deployments currently blocked by it.
Refs
#12121 (the undeclared half, in flight) · #11431 (the ceiling and the positive-integer predicate) · #11794 / #11875 (the TEXT family) · #12017 (the emitter/spec parity pin) · #11565 (varcharColumnChars and its columnInfo() pin)
Generated by Claude Code
Found while working #12121 (which lands the undeclared half of the same seam in
packages/drivers/driver-sql/src/schema-drift.ts). ⛔ Deliberately not repaired there: #12121's scope is the case where the author declared no bound, and both cases below are the opposite half — the author did declare one, and the differ honours it in a way the emitter does not. Filed unassigned rather than ridden along.Both measured on
origin/mainatd29e42f8, via directdiffManagedTablecalls against a pre-existingvarchar(255)column, dialectpostgres. Duplicate search run before filing (targeted semantic search over this repo): the six hits are the closed family cards #11431 / #11566 / #11565 / #11794 / #11875 / #12017, none of which covers either case.The shared root cause
The varchar differ treats
varchar(field.maxLength)as the expected physical shape of any bounded field.SqlDriver.createColumndoes not — andvarcharColumnCharsis its own read-only mirror of that switch, so what the emitter would build is knowable exactly. The two disagree in two measured directions.Case A — an unkeyed, bounded TEXT-family field reports
narrow_varcharatdestructiveTwo problems, and the second is the serious one:
createColumnemits TEXT for every one of these (keyable = keyed ? keyableTextLength(field) : null). So the differ demands the column be narrowed to a shape the platform itself would never create.category: 'destructive', which refuses the boot.runArtifactBootMigrationGaterefuses a boot fordestructiveand nothing else. So an already-serving deployment whose author addsmaxLength: 50to a legacyvarchar(255)text column stops booting — over a divergence that changes no behaviour at all, because the write seam already enforces 50 for exactlyBOUNDED_STRING_FIELD_TYPES(record-validator'smax_lengthbranch). No value above 50 can reach the column, so the column's extra width refuses nothing.This is the same false positive #11431 closed for genuine TEXT columns (
isCharacterColumn), arriving through a different door: the column is spelledvarcharbecause an older release created it, not because the current emitter wants one.Case B — a bound past the dialect ceiling plans DDL MySQL refuses
FieldSchemadeclaresmaxLength: z.number().int().min(1)— no upper bound — so this is authorable on any bounded-string type:declaredVarcharLengthreturnsnullaboveMAX_VARCHAR_CHARS(16383) and the column becomes TEXT, precisely sovarchar(100000)is never emitted — MySQL refuses it (ERROR 1074 Column length too big … max = 16383). The differ has no such ceiling, so it plansALTER … TYPE varchar(100000), categorisedsafeand therefore eligible for dev auto-reconcile as well asos migrate apply. Postgres accepts it (its own limit is 10485760) and MySQL fails the statement, which is the dialect-divergent enforcement of one declaration this package's conformance matrices exist to close.Suggested shape of the remedy (not a decision)
One predicate rather than two patches: expect the width
createColumnwould actually emit.SqlDriver.varcharColumnChars(field, keyed)already answers that and is pinned againstcolumnInfo()for everyFieldTypebysql-driver-11565-row-byte-budget.test.ts.nullmeans "the emitter would not make this a varchar", which is the honest expectation in both cases above.varchar(maxLength).indexedKeyColumns()already computes that map insideschema-drift.ts; the missing half is threading it fromSqlDriver.detectManagedDriftintodiffManagedTable, i.e. this touchessql-driver.tsand wants the serial slot on that file. #12121 avoided needing it by gating strictly on the declaration being absent, which is keyedness-independent; this card cannot.Also worth deciding as part of it: Case A's category. Turning a boot-refusing
destructiveinto a report is a behaviour change for deployments currently blocked by it.Refs
#12121 (the undeclared half, in flight) · #11431 (the ceiling and the positive-integer predicate) · #11794 / #11875 (the TEXT family) · #12017 (the emitter/spec parity pin) · #11565 (
varcharColumnCharsand itscolumnInfo()pin)Generated by Claude Code