Found while landing #11224 (the MySQL precision half of the same helper). Not fixed there: that card is about the UPDATE door's stamp on MySQL, this one is about the SQLite column DEFAULT, and fixing it changes DDL — a different verification surface.
The mismatch
packages/drivers/driver-sql/src/sql-driver.ts:
createAuditTimestampColumn's non-MySQL branch is table.timestamp(name).defaultTo(this.knex.fn.now()). On SQLite knex.fn.now() compiles to an unqualified CURRENT_TIMESTAMP, which SQLite renders as a zone-naive, space-separated, second-precision string.defaultDatetimeSql's SQLite branch — the one a declaredField.datetime with defaultValue: 'NOW()' gets — is (strftime('%Y-%m-%dT%H:%M:%fZ','now')), the canonical zone-explicit ISO-8601 form with millis.
So one table carries two different default shapes for the same conceptual value.
Measured (better-sqlite3, in-memory, via the driver's own initObjects)
Object declared as { id: text, title: string, when: datetime defaultValue 'NOW()' }. Emitted DDL:
CREATETABLE `probe_t` (
`id`varchar(255),
`created_at` datetime default CURRENT_TIMESTAMP, -- builtin audit`updated_at` datetime default CURRENT_TIMESTAMP, -- builtin audit`title`varchar(255),
`when` datetime default (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), -- declared fieldprimary key (`id`)
)An insert that lets the DEFAULTs fire (insert into probe_t (id, title) values ('x','y')) stores:
created_at "2026-08-23 11:03:23" <- zone-naive, space-separated, no millis
updated_at "2026-08-23 11:03:23" <- same
when "2026-08-23T11:03:23.611Z" <- canonical
Why this is the shape the driver elsewhere calls a defect
updatedAtStamp()'s own docblock says exactly what is wrong with that shape, about a form it was already fixed away from:
The previous …replace('T',' ').replace('Z','') wrote a zone-NAIVE, space-separated string that Date.parse reads as LOCAL time, silently shifting the instant by the host offset on a non-UTC runtime (the objectos freshness-probe miss).
A row whose created_at came from this DEFAULT carries that exact string. It is also the pre-canonical storage form that backfillCanonicalDatetimes and repairCanonicalDatetimes exist to converge — reachable here through a path that is writing it today, not through legacy data.
Reachability — narrow, but real, and stated honestly
On the driver's own insert paths stampInsertTimestamps fills both audit columns explicitly with new Date().toISOString(), so the DEFAULT does not fire and the stored value is canonical. The DEFAULT is what a write outside the driver hits:
- raw SQL /
knex(...) inserts (including test fixtures and seed scripts), - a hand-written migration or another process writing the same file,
- any future driver path that inserts without going through
stampInsertTimestamps.
Whether that population is worth DDL churn is a triage call, not something this card decides.
Corroborating signal already in the repo
Two live-matrix suites build "a hand-written migration that got the schema right" and spell the SQLite audit default as strftime('%Y-%m-%dT%H:%M:%fZ','now'), each with a docblock claiming those columns "take the SAME physical types createAuditTimestampColumn produces":
packages/drivers/driver-sql/src/sql-driver-11176-bulk-and-merge-updated-at.test.tspackages/drivers/driver-sql/src/sql-driver-timestamps-without-ddl.test.ts
On MySQL that claim holds (datetime(3) default current_timestamp(3) both sides). On SQLite it does not: the fixtures model a more correct table than the driver itself creates. So the fixtures and the driver disagree, and only the fixtures are asserted — which is why nothing has failed.
Suggested shape (not decided here)
Give createAuditTimestampColumn's SQLite branch the same canonical expression defaultDatetimeSql already produces, so one table cannot carry two default shapes. Whoever takes it owns the DDL-change surface: existing SQLite databases keep their old DEFAULT until the column is rebuilt, so the card has to say what happens to them (leave them, or converge them the way backfillCanonicalDatetimes converges stored values) rather than only changing what new tables get.
⚠️ Note for triage: backfillCanonicalDatetimes and sqliteCanonicalDatetimeSql are the restart conditions of the pm:on-hold card #6009, so a fix that reaches for them collides with that hold and should be sequenced against it.
Generated by Claude Code
Found while landing #11224 (the MySQL precision half of the same helper). Not fixed there: that card is about the UPDATE door's stamp on MySQL, this one is about the SQLite column DEFAULT, and fixing it changes DDL — a different verification surface.
The mismatch
packages/drivers/driver-sql/src/sql-driver.ts:createAuditTimestampColumn's non-MySQL branch istable.timestamp(name).defaultTo(this.knex.fn.now()). On SQLiteknex.fn.now()compiles to an unqualifiedCURRENT_TIMESTAMP, which SQLite renders as a zone-naive, space-separated, second-precision string.defaultDatetimeSql's SQLite branch — the one a declaredField.datetimewithdefaultValue: 'NOW()'gets — is(strftime('%Y-%m-%dT%H:%M:%fZ','now')), the canonical zone-explicit ISO-8601 form with millis.So one table carries two different default shapes for the same conceptual value.
Measured (better-sqlite3, in-memory, via the driver's own
initObjects)Object declared as
{ id: text, title: string, when: datetime defaultValue 'NOW()' }. Emitted DDL:An insert that lets the DEFAULTs fire (
insert into probe_t (id, title) values ('x','y')) stores:Why this is the shape the driver elsewhere calls a defect
updatedAtStamp()'s own docblock says exactly what is wrong with that shape, about a form it was already fixed away from:A row whose
created_atcame from this DEFAULT carries that exact string. It is also the pre-canonical storage form thatbackfillCanonicalDatetimesandrepairCanonicalDatetimesexist to converge — reachable here through a path that is writing it today, not through legacy data.Reachability — narrow, but real, and stated honestly
On the driver's own insert paths
stampInsertTimestampsfills both audit columns explicitly withnew Date().toISOString(), so the DEFAULT does not fire and the stored value is canonical. The DEFAULT is what a write outside the driver hits:knex(...)inserts (including test fixtures and seed scripts),stampInsertTimestamps.Whether that population is worth DDL churn is a triage call, not something this card decides.
Corroborating signal already in the repo
Two live-matrix suites build "a hand-written migration that got the schema right" and spell the SQLite audit default as
strftime('%Y-%m-%dT%H:%M:%fZ','now'), each with a docblock claiming those columns "take the SAME physical typescreateAuditTimestampColumnproduces":packages/drivers/driver-sql/src/sql-driver-11176-bulk-and-merge-updated-at.test.tspackages/drivers/driver-sql/src/sql-driver-timestamps-without-ddl.test.tsOn MySQL that claim holds (
datetime(3) default current_timestamp(3)both sides). On SQLite it does not: the fixtures model a more correct table than the driver itself creates. So the fixtures and the driver disagree, and only the fixtures are asserted — which is why nothing has failed.Suggested shape (not decided here)
Give
createAuditTimestampColumn's SQLite branch the same canonical expressiondefaultDatetimeSqlalready produces, so one table cannot carry two default shapes. Whoever takes it owns the DDL-change surface: existing SQLite databases keep their old DEFAULT until the column is rebuilt, so the card has to say what happens to them (leave them, or converge them the waybackfillCanonicalDatetimesconverges stored values) rather than only changing what new tables get.backfillCanonicalDatetimesandsqliteCanonicalDatetimeSqlare the restart conditions of thepm:on-holdcard #6009, so a fix that reaches for them collides with that hold and should be sequenced against it.Generated by Claude Code