Found while implementing #16294 cause 3 (PR #17230), which repaired the column DEFAULT divergence between os generate migration and driver-sql. This is the same producer pair and the same "the generator should reproduce the platform" question, one property over — nullability on a multi-value column — and it is deliberately out of that PR's diff because repairing it contradicts a landed pin (see below).
⛔ Not graded or prioritised here. No labels applied, no assignee — this is for triage.
Driven, not read
Measured on a private PostgreSQL 16.13 cluster with the same harness #16294 used: all three producers run from ONE object and their columns read back out of information_schema.columns — driver-sql through its own initObjects, os generate migration --format sql through db.raw of the emitted DDL, and --format ts by importing the emitted module and calling up(db).
{ d_multi_notnull: { type: 'lookup', referenceTo: 'sys_user', multiple: true, storage: { notNull: true } } }
field driver sqlgen tsgen verdict
d_multi_notnull null=YES default=- null=NO default=- null=NO default=- DIVERGED
This was the ONE row still diverging in a 23-column defaultValue / nullability probe after #17230 — before it as well as after, i.e. #17230 neither caused nor repaired it.
Why
SqlDriver.createColumn short-circuits on the flag and returns before it reaches either the nullability line or the column DEFAULT:
if (field.multiple) {
this.jsonColumn(table, name);
return;
}
So the driver's multi-value column is nullable, always, whatever storage.notNull says. Both generators instead apply declaredNotNull(fieldDef) to their JSON column: the sql format emits "tags_nn" JSONB NOT NULL and the ts format table.jsonb('tags_nn').notNullable().
Consequence, in the same direction #16294's cause 2 named one property over: a table created from a generated migration constrains a column the platform's own table leaves open. An INSERT that omits the field is accepted by the platform's table and refused by both generated ones.
⚠️ Why it was NOT repaired under #16294
The current generator behaviour is pinned as intended — packages/cli/src/commands/generate-multiple-json-column.pin.test.ts:
it('nullability comes from `storage.notNull`, not from the flag and not from `required`', () => {
const constrained = { type: 'lookup', multiple: true, storage: { notNull: true } };
...
expect(out).toContain('"tags_nn" JSONB NOT NULL');
expect(ts).toContain("table.jsonb('tags_nn').notNullable();");
});
That pin's stated subject (#14829) is "multiple does not decide nullability", and it was rewritten under #16318 when the vehicle moved from required to storage.notNull — but neither card measured the assertion against the DRIVER, which returns before the question is asked. So the pin encodes a rule the platform does not follow. Moving it is therefore a deliberate change to a landed pin, not a rider on an unrelated diff, and the direction is not free to pick by an executor: it is the same "which side moves" question #16318 answered for column TYPE and #17218 still carries for required.
The two sides
⛔ Not answered here. Note the two sides land in different repos' worth of surface — A is domain:cli, B is domain:engine plus possibly a spec refusal — so the routing depends on the ruling.
Reproducing
One object with the field above, driven through all three producers into a live PostgreSQL 16.13 and read back from information_schema.columns. A postgresql-16 server package is present in the standard dev container, so a private cluster is initdb plus pg_ctl with no external service. The same divergence is visible without a server: PRAGMA table_info on three in-memory better-sqlite3 databases, the harness packages/cli/src/commands/generate-declared-column-default.pin.test.ts already uses.
Generated by Claude Code
Found while implementing #16294 cause 3 (PR #17230), which repaired the column DEFAULT divergence between
os generate migrationanddriver-sql. This is the same producer pair and the same "the generator should reproduce the platform" question, one property over — nullability on a multi-value column — and it is deliberately out of that PR's diff because repairing it contradicts a landed pin (see below).⛔ Not graded or prioritised here. No labels applied, no assignee — this is for triage.
Driven, not read
Measured on a private PostgreSQL 16.13 cluster with the same harness #16294 used: all three producers run from ONE object and their columns read back out of
information_schema.columns—driver-sqlthrough its owninitObjects,os generate migration --format sqlthroughdb.rawof the emitted DDL, and--format tsby importing the emitted module and callingup(db).This was the ONE row still diverging in a 23-column
defaultValue/ nullability probe after #17230 — before it as well as after, i.e. #17230 neither caused nor repaired it.Why
SqlDriver.createColumnshort-circuits on the flag and returns before it reaches either the nullability line or the column DEFAULT:So the driver's multi-value column is nullable, always, whatever
storage.notNullsays. Both generators instead applydeclaredNotNull(fieldDef)to their JSON column: the sql format emits"tags_nn" JSONB NOT NULLand the ts formattable.jsonb('tags_nn').notNullable().Consequence, in the same direction #16294's cause 2 named one property over: a table created from a generated migration constrains a column the platform's own table leaves open. An INSERT that omits the field is accepted by the platform's table and refused by both generated ones.
The current generator behaviour is pinned as intended —
packages/cli/src/commands/generate-multiple-json-column.pin.test.ts:That pin's stated subject (#14829) is "
multipledoes not decide nullability", and it was rewritten under #16318 when the vehicle moved fromrequiredtostorage.notNull— but neither card measured the assertion against the DRIVER, which returns before the question is asked. So the pin encodes a rule the platform does not follow. Moving it is therefore a deliberate change to a landed pin, not a rider on an unrelated diff, and the direction is not free to pick by an executor: it is the same "which side moves" question #16318 answered for column TYPE and #17218 still carries forrequired.The two sides
notNullable()/NOT NULLfrom the multi-value arm, and rewrite the pin to assert that a multi-value column is nullable on all three producers. Consistent withos generate migration's audit-stamp columns diverge from driver-sql — the generators emitNOT NULLwhere the driver emits nullable, and the SQL format emitsTIMESTAMPwhere both knex paths yieldtimestamptz#15521's ruling ("the generator follows the driver") as applied by [finding] Both migration generators captextfields at VARCHAR(255) while driver-sql creates an unbounded text column — a 300-char value the platform accepts is refused by every generated migration #16091, [finding] The NUMERIC column family diverges from driver-sql in both migration formats — driverreal, generatorsnumeric, andratingisrealagainstinteger#16318 and fix(cli): a generated migration carries the field-level unique index driver-sql creates #17208 to this same producer pair.createColumn's early return meansstorage: { notNull: true }on a multi-value field is silently inert on the platform — a declared-but-unenforced key, which is the ADR-0049 enforce-or-remove shape. On this reading the repair belongs indriver-sql(honour the constraint on the JSON column) or inpackages/spec(refuse the combination at authoring time), not in the generators.⛔ Not answered here. Note the two sides land in different repos' worth of surface — A is
domain:cli, B isdomain:engineplus possibly a spec refusal — so the routing depends on the ruling.Reproducing
One object with the field above, driven through all three producers into a live PostgreSQL 16.13 and read back from
information_schema.columns. Apostgresql-16server package is present in the standard dev container, so a private cluster isinitdbpluspg_ctlwith no external service. The same divergence is visible without a server:PRAGMA table_infoon three in-memory better-sqlite3 databases, the harnesspackages/cli/src/commands/generate-declared-column-default.pin.test.tsalready uses.Generated by Claude Code