Skip to content

driver-sql: the string family ignores maxLength too — a declared 1024 becomes varchar(255) and refuses legitimate writes #11431

Description

@os-zhuang

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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions