fix(driver-sql): canonical ISO-8601-Z audit timestamps on SQLite (ADR-0074)#2342
Merged
Conversation
…-0074)
The SQLite write paths disagreed on the created_at/updated_at format: INSERT
fell back to the CURRENT_TIMESTAMP column default ('YYYY-MM-DD HH:MM:SS') while
UPDATE stamped 'YYYY-MM-DD HH:MM:SS.mmm' — both timezone-naive, space-separated
strings that Date.parse reads as LOCAL time. On a non-UTC runtime a stored UTC
wall-clock silently shifted by the host offset (the objectos kernel
freshness-probe miss: a shifted updated_at landed before builtAtMs, so the
per-env kernel never evicted).
create/bulkCreate/upsert/update now stamp one canonical ISO-8601 instant with an
explicit Z (new Date().toISOString()), matching the caller-stamped paths
(sys_metadata, service outboxes) and Postgres/MySQL native now(). Applied
app-side (not via the column default) so EXISTING tenant DBs are fixed
immediately. formatOutput repairs legacy/raw zone-naive audit timestamps to the
same format on read (idempotent) — no data migration. upsert treats created_at
as insert-only (never clobbered on merge). Postgres/MySQL unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On SQLite the driver's two write paths disagreed on how they stored the builtin
created_at/updated_ataudit columns:defaultTo(knex.fn.now())→CURRENT_TIMESTAMP→'2026-06-26 10:23:40'(space-separated, no millis, no zone).toISOString().replace('T',' ').replace('Z','')→'2026-06-26 10:23:40.246'(space-separated, with millis, no zone).Both are timezone-naive. A zone-less, space-separated string is not ISO-8601, so
Date.parse(V8) reads it as local time. On a non-UTC runtime a stored UTC wall-clock silently shifts by the host offset when read back — corrupting everynew Date(updated_at)comparison. This surfaced as the objectos kernel freshness probe never evicting on a UTC+8 self-hosted EE: a shiftedupdated_atlanded before the absolutebuiltAtMs, so the per-env kernel reported "fresh" and publishes/installs/config toggles didn't take effect until the LRU TTL expired. (The cloud consumer was already hardened defensively; this is the deeper storage-side fix.)Note: the "INSERT stores
…Z" framing only held for caller-stamped tables (e.g.sys_metadatawrites ISO-Z viatoISOString()) — andupdate()then clobbered that back to the naive format, which is the actual mechanism. A SQL-levelORDER BY updated_at(objectql metadata load) also means fixing onlyupdate()would mix formats on disk (space0x20<T0x54) and mis-order rows, so insert and update had to change together.Fix (
packages/plugins/driver-sql/src/sql-driver.ts)update()→ SQLite stampsnew Date().toISOString()(canonical ISO-8601 withZ).create()/bulkCreate()/upsert()→ newstampInsertTimestamps()fills missingcreated_at/updated_atwith the same canonical instant. Applied app-side (not via a DDL default) so existing tenant databases are fixed immediately — a column-default change only affects newly created tables.upsert()→created_atis now insert-only (a conflictingmerge()never overwrites it;updated_atstill advances).formatOutput()→ idempotent tolerant reader repairs any legacy/raw zone-naive audit timestamp to ISO-8601-Zon read (interpreting the stored wall-clock as UTC). No data migration needed; mirrors the existingField.date/numeric read-repair.Field.datetime-typed audit columns stored as epoch-ms INTEGER pass through untouched.Postgres/MySQL are unchanged (native
now()stores a real zone-awareTIMESTAMP).Verification
sql-driver-timestamp-format.test.ts(10 cases incl. the freshness-probe regression, idempotent read-repair, no on-disk mixing, upsertcreated_atimmutability).updated_atformat and pass unchanged.updated_at+ integration), service-analytics 164 (created_atbucketing), dogfood gate 173, service-messaging 129, service-job 36, service-settings 128, service-storage 48.Docs / release
docs/adr/0074-canonical-audit-timestamp-storage-on-sqlite.md), building on ADR-0053.@objectstack/driver-sqlpatch.Follow-up (scoped out, tracked separately): user-declared
datetimefields with aNOW()default still take the naiveCURRENT_TIMESTAMPdefault on SQLite.🤖 Generated with Claude Code