Blocked-by: #11566
Found while implementing #11374 (keyed text columns on MySQL). Filed unassigned. Not fixed there: #11374 is about the text family becoming an unkeyable TEXT, and this is the mirror defect one type family over, with a different symptom and a wider blast radius.
What
createColumn maps the string family — string / email / url / phone / password, plus lookup / user / auto_number — with a bare table.string(name). Knex's default length is 255, and the field's declared maxLength is never read. So a field that declares a wider bound silently gets a narrower column, and on a dialect that enforces varchar length the write is refused.
Evidence
Measured on live MySQL 8.0.46 (utf8mb4, InnoDB, STRICT_TRANS_TABLES) through driver-sql's own initObjects:
wide_string Field.string({ maxLength: 1024 }) -> physical varchar(255)
wide_email Field.email({ maxLength: 400 }) -> physical varchar(255)
narrow_string Field.string({ maxLength: 20 }) -> physical varchar(255)
INSERT 300 chars into the maxLength:1024 column
-> ER_DATA_TOO_LONG: Data too long for column 'wide_string' at row 1
The declared contract says 1024 characters are allowed. The database refuses at 256. Nothing in between reports a problem at write time — the author gets a driver-level error naming a limit they never declared.
Note the third line too: a field declaring maxLength: 20 gets varchar(255), so the declared bound is not enforced in that direction either. maxLength is currently inert for DDL across the whole string family — it binds in neither direction.
Why it matters (declared ≠ enforced)
schema-drift.ts already treats varchar(field.maxLength) as the expected physical shape — it emits a widen_varchar op with the message "metadata allows 1024 chars but the column caps at 255 — widen via os migrate". So the differ and the emitter disagree about the same field, and every such column reports permanent drift against a column the driver itself just created. #11374 fixed exactly this disagreement for the text family; the string family still has it.
This is also the failure mode that is hardest for an AI-authored metadata app to notice: the declaration reads as satisfied, the table creates cleanly, and the refusal only appears later, on a long value, in production data.
Direction (for triage — not prescribing a fix)
The obvious shape is the one #11374 landed for text: emit varchar(field.maxLength) when the field declares one. It needs its own decision because the blast radius is different — the string family is used far more widely, varchar(255) has been the de-facto physical contract for every such column since the beginning, and narrowing an existing column (the maxLength: 20 case) is a destructive migration, not an additive one. A defensible split is to honour the bound only when it widens, and to route narrowing through the existing narrow_varchar drift op rather than emitting it silently at create time.
Repro
node -e "…" // build a SqlDriver on live MySQL, initObjects with
// { wide_string: { type: 'string', maxLength: 1024 } },
// then read information_schema.columns.column_type
Equivalently: create any object with a Field.string({ maxLength: n > 255 }), sync it on MySQL or Postgres, and write n characters to it.
Generated by Claude Code
Generated by Claude Code
Blocked-by: #11566
Found while implementing #11374 (keyed text columns on MySQL). Filed unassigned. Not fixed there: #11374 is about the text family becoming an unkeyable
TEXT, and this is the mirror defect one type family over, with a different symptom and a wider blast radius.What
createColumnmaps the string family —string/email/url/phone/password, pluslookup/user/auto_number— with a baretable.string(name). Knex's default length is 255, and the field's declaredmaxLengthis never read. So a field that declares a wider bound silently gets a narrower column, and on a dialect that enforcesvarcharlength the write is refused.Evidence
Measured on live MySQL 8.0.46 (utf8mb4, InnoDB,
STRICT_TRANS_TABLES) throughdriver-sql's owninitObjects:The declared contract says 1024 characters are allowed. The database refuses at 256. Nothing in between reports a problem at write time — the author gets a driver-level error naming a limit they never declared.
Note the third line too: a field declaring
maxLength: 20getsvarchar(255), so the declared bound is not enforced in that direction either.maxLengthis currently inert for DDL across the whole string family — it binds in neither direction.Why it matters (declared ≠ enforced)
schema-drift.tsalready treatsvarchar(field.maxLength)as the expected physical shape — it emits awiden_varcharop with the message "metadata allows 1024 chars but the column caps at 255 — widen viaos migrate". So the differ and the emitter disagree about the same field, and every such column reports permanent drift against a column the driver itself just created. #11374 fixed exactly this disagreement for the text family; the string family still has it.This is also the failure mode that is hardest for an AI-authored metadata app to notice: the declaration reads as satisfied, the table creates cleanly, and the refusal only appears later, on a long value, in production data.
Direction (for triage — not prescribing a fix)
The obvious shape is the one #11374 landed for text: emit
varchar(field.maxLength)when the field declares one. It needs its own decision because the blast radius is different — the string family is used far more widely,varchar(255)has been the de-facto physical contract for every such column since the beginning, and narrowing an existing column (themaxLength: 20case) is a destructive migration, not an additive one. A defensible split is to honour the bound only when it widens, and to route narrowing through the existingnarrow_varchardrift op rather than emitting it silently at create time.Repro
Equivalently: create any object with a
Field.string({ maxLength: n > 255 }), sync it on MySQL or Postgres, and writencharacters to it.Generated by Claude Code
Generated by Claude Code