Found by the driver-materialisation consumer census on #13973 (class (c) — genuinely wrong on one side). That sweep is not addressed by this card and does not close it.
The site
packages/metadata/src/migrations/migrate-sys-notification-to-event.ts:103 and :131:
constcreatedAt=row.created_at!=null ? String(row.created_at) : now();…awaitdata.insert(INBOX_OBJECT,{ …,created_at: createdAt});awaitdata.insert(RECEIPT_OBJECT,{ …,at: isRead&&row.read_at!=null ? String(row.read_at) : createdAt,created_at: createdAt});row comes from selectLegacyRows(driver) — a driver read of the legacy sys_notification table.
Why it is wrong on the production default driver
created_at is a builtin audit column: not in datetimeFields, and SqlDriver#formatOutput repairs it only inside if (this.isSqlite). Pinned in packages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts — the live dialects hand it out as a JS Date, SQLite as canonical ISO-Z text.
So on Postgres and MySQL this migration reads a Date and writes
"Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)"
back into created_at and at on the new inbox and receipt rows. Unlike the read-side sites in this census, this one persists the wrong spelling — the migration is one-way, so whatever lands is what the platform carries afterwards.
Two failure shapes are possible depending on the target column's type and the write path's coercion, and which one occurs should be measured against a live server rather than reasoned:
- the value is accepted and stored with milliseconds dropped and the migrating host's timezone baked in (
Date.prototype.toString renders whole seconds in the process zone), so the migrated history is silently skewed and de-precisioned; or - the value is rejected — the trailing
(China Standard Time) is not part of any dialect's timestamp grammar — and the migration throws or degrades mid-run.
Neither is visible today: this migration's tests drive SQLite/memory, where String(row.created_at) is the identity on already-canonical text.
Why this is not a ?? fallback
Per #13973's standing prohibition, the question is which side owes the canonical spelling:
- A — canonicalise at the migration, which is also the only shape that repairs already-migrated deployments if any exist. The repo's existing correct form is one file family over,
packages/metadata-protocol/src/protocol.ts:7710-7715. - B — normalise at the producer (one presented shape per dialect at the read door) — fixes every sibling site, but reverses a deliberate driver decision (
withPostgresCalendarDayAsText) and is a maintainer call.
⚠️ Worth deciding first: whether any deployment has already run this migration against Postgres or MySQL. If so, the repair has a data half as well as a code half, and that changes its priority.
Re-run
rg -n 'String\(row\.(created_at|read_at)\)' packages/metadata/src/migrations/
Backlink: #13973 (census), #13382 (the OCC seam, the same class). Neither is addressed here.
Found by the driver-materialisation consumer census on #13973 (class (c) — genuinely wrong on one side). That sweep is not addressed by this card and does not close it.
The site
packages/metadata/src/migrations/migrate-sys-notification-to-event.ts:103and:131:rowcomes fromselectLegacyRows(driver)— a driver read of the legacysys_notificationtable.Why it is wrong on the production default driver
created_atis a builtin audit column: not indatetimeFields, andSqlDriver#formatOutputrepairs it only insideif (this.isSqlite). Pinned inpackages/drivers/driver-sql/src/sql-driver-13567-audit-stamp-materialisation.test.ts— the live dialects hand it out as a JSDate, SQLite as canonical ISO-Z text.So on Postgres and MySQL this migration reads a
Dateand writesback into
created_atandaton the new inbox and receipt rows. Unlike the read-side sites in this census, this one persists the wrong spelling — the migration is one-way, so whatever lands is what the platform carries afterwards.Two failure shapes are possible depending on the target column's type and the write path's coercion, and which one occurs should be measured against a live server rather than reasoned:
Date.prototype.toStringrenders whole seconds in the process zone), so the migrated history is silently skewed and de-precisioned; or(China Standard Time)is not part of any dialect's timestamp grammar — and the migration throws or degrades mid-run.Neither is visible today: this migration's tests drive SQLite/memory, where
String(row.created_at)is the identity on already-canonical text.Why this is not a
??fallbackPer #13973's standing prohibition, the question is which side owes the canonical spelling:
packages/metadata-protocol/src/protocol.ts:7710-7715.withPostgresCalendarDayAsText) and is a maintainer call.Re-run
Backlink: #13973 (census), #13382 (the OCC seam, the same class). Neither is addressed here.