Measured by the os-dev seat on #15040 (session session_01D47qPfEWVPmhguWgBZCi5N, branch claude/issue-15040-generated-migration-id-column-shape, PR #15518) in the same pass as that card's id-column fix, on the explicit instruction of #15040's triage note. Filed by the domain:cli execution PM seat (#6024) as the routing that note's ⚠️ "⛔ do not leave it half-done" resolves to. Unassigned, bare, for triage.
⛔ This card records a divergence and does NOT rule it. #15040 deliberately changed none of these lines; see What #15040 did instead below.
What
Three sites, measured on the same tree as #15040:
| side |
what it emits |
packages/drivers/driver-sql/src/sql-driver.ts — createAuditTimestampColumn |
table.timestamp(name).defaultTo(this.knex.fn.now()) — nullable |
generate.ts → generateMigrationSql |
"created_at" TIMESTAMP NOT NULL DEFAULT now() (same for updated_at) |
generate.ts → generateMigrationTs (the default format) |
table.timestamps(true, true), which knex 3.3.0 compiles to .notNullable().defaultTo(CURRENT_TIMESTAMP) on both columns (knex/lib/schema/tablebuilder.js) |
Compiled offline against knex's pg dialect — a pure compile, no connection:
driver "created_at" timestamptz default CURRENT_TIMESTAMP
ts gen "created_at" timestamptz not null default CURRENT_TIMESTAMP
⇒ Type, column name and default expression agree. Only nullability differs.
⭐ There is a second, narrower divergence that only the SQL format carries: its literal TIMESTAMP is timestamp WITHOUT time zone, while both knex paths yield timestamptz. That half is invisible from the TypeScript format, and it is a plain type disagreement rather than a judgment call — which is why this card asks for both halves in one look rather than only the nullability one.
⚠️ A third fact bears on any ruling: the driver's audit DDL is dialect-branched (datetime(3) on MySQL, a canonical ISO default on SQLite, per createAuditTimestampColumn's #11321 docblock) in a way a Postgres-flavoured generated migration does not reproduce at all.
Why it is NOT simply the same defect as #15040
#15040 was a wrong value: a platform id is a 26-character string and a uuid column refuses it outright (22P02 on the first insert), so there was exactly one correct answer and the PR could take it without a ruling.
Nullability is not that shape. Both sides are defensible and nothing fails either way: the driver stamps both columns on every write, so a NOT NULL scaffold is arguably the truer constraint, and nothing the platform writes can violate it. The cost of leaving it is not a failure but a permanent one: a generated table and a platform-created table keep different column definitions, so a schema diff between them is noisy forever.
Options as measured — ⛔ costs, not a recommendation
What #15040 did instead, so this cannot rot unnoticed
PR #15518 records the divergence as the last case of packages/cli/src/commands/generate-builtin-id-column.pin.test.ts, headed "Recorded divergence, NOT coverage, and NOT a ruling". It asserts the current shape on both sides — the two generator outputs, and that createAuditTimestampColumn's default arm carries no .notNullable() — with a failure message that says "driver-sql's audit-column DDL moved — re-read the #15040 record above before trusting it."
⇒ ⭐ Whichever way this is ruled, that it is the case to edit, and until then neither side can change shape silently.
Verified
Refs: #15040 · PR #15518 · #11321 (the dialect-branch docblock).
Measured by the⚠️ "⛔ do not leave it half-done" resolves to. Unassigned, bare, for triage.
os-devseat on #15040 (sessionsession_01D47qPfEWVPmhguWgBZCi5N, branchclaude/issue-15040-generated-migration-id-column-shape, PR #15518) in the same pass as that card'sid-column fix, on the explicit instruction of #15040's triage note. Filed by thedomain:cliexecution PM seat (#6024) as the routing that note's⛔ This card records a divergence and does NOT rule it. #15040 deliberately changed none of these lines; see What #15040 did instead below.
What
Three sites, measured on the same tree as #15040:
packages/drivers/driver-sql/src/sql-driver.ts—createAuditTimestampColumntable.timestamp(name).defaultTo(this.knex.fn.now())— nullablegenerate.ts→generateMigrationSql"created_at" TIMESTAMP NOT NULL DEFAULT now()(same forupdated_at)generate.ts→generateMigrationTs(the default format)table.timestamps(true, true), which knex 3.3.0 compiles to.notNullable().defaultTo(CURRENT_TIMESTAMP)on both columns (knex/lib/schema/tablebuilder.js)Compiled offline against knex's
pgdialect — a pure compile, no connection:⇒ Type, column name and default expression agree. Only nullability differs.
⭐ There is a second, narrower divergence that only the SQL format carries: its literal
TIMESTAMPistimestamp WITHOUT time zone, while both knex paths yieldtimestamptz. That half is invisible from the TypeScript format, and it is a plain type disagreement rather than a judgment call — which is why this card asks for both halves in one look rather than only the nullability one.datetime(3)on MySQL, a canonical ISO default on SQLite, percreateAuditTimestampColumn's#11321docblock) in a way a Postgres-flavoured generated migration does not reproduce at all.Why it is NOT simply the same defect as #15040
#15040 was a wrong value: a platform id is a 26-character string and a
uuidcolumn refuses it outright (22P02on the first insert), so there was exactly one correct answer and the PR could take it without a ruling.Nullability is not that shape. Both sides are defensible and nothing fails either way: the driver stamps both columns on every write, so a
NOT NULLscaffold is arguably the truer constraint, and nothing the platform writes can violate it. The cost of leaving it is not a failure but a permanent one: a generated table and a platform-created table keep different column definitions, so a schema diff between them is noisy forever.Options as measured — ⛔ costs, not a recommendation
NOT NULLis arguably truer; nothing the platform writes can violate it. ⛔ Cost: the schema-diff noise above is permanent.NOT NULLfrom both generators, matching the driver byte for byte (the rule [finding] packages/cli generate.ts: both migration generators give the table's ownida UUID primary key, but the platform's id is a 26-character string in avarchar(255)#15040 applied toid). ⛔ Cost: it weakens the scaffold for the reason only that "the driver happens not to saynotNullable()", and it does not touch thetimestamptzhalf, so the SQL format still would not match.notNullable()and leave the generators. ⛔ Cost: a migration on existing deployments, and it lands inpackages/drivers, which [finding] packages/cli generate.ts: both migration generators give the table's ownida UUID primary key, but the platform's id is a 26-character string in avarchar(255)#15040 fences off as the authority, not the subject. Needs its own evidence either way.timestamptzhalf is largely orthogonal to A/B/C and looks decidable on its own.What #15040 did instead, so this cannot rot unnoticed
PR #15518 records the divergence as the last case of
packages/cli/src/commands/generate-builtin-id-column.pin.test.ts, headed "Recorded divergence, NOT coverage, and NOT a ruling". It asserts the current shape on both sides — the two generator outputs, and thatcreateAuditTimestampColumn's default arm carries no.notNullable()— with a failure message that says "driver-sql's audit-column DDL moved — re-read the #15040 record above before trusting it."⇒ ⭐ Whichever way this is ruled, that
itis the case to edit, and until then neither side can change shape silently.Verified
pgdialect.22P02refusal cited above under [finding] packages/cli generate.ts: both migration generators give the table's ownida UUID primary key, but the platform's id is a 26-character string in avarchar(255)#15040 remains quoted from the driver's prose, not driven — there is no Postgres in that container.Refs: #15040 · PR #15518 ·
#11321(the dialect-branch docblock).