Skip to content

fix(driver-sql): canonical ISO-8601-Z audit timestamps on SQLite (ADR-0074) - #2342

Merged
os-zhuang merged 1 commit into
mainfrom
claude/musing-swanson-bda53a
Jun 26, 2026
Merged

fix(driver-sql): canonical ISO-8601-Z audit timestamps on SQLite (ADR-0074)#2342
os-zhuang merged 1 commit into
mainfrom
claude/musing-swanson-bda53a

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Problem

On SQLite the driver's two write paths disagreed on how they stored the builtin created_at/updated_at audit columns:

  • INSERT relied on the column default defaultTo(knex.fn.now())CURRENT_TIMESTAMP'2026-06-26 10:23:40' (space-separated, no millis, no zone).
  • UPDATE stamped 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 every new Date(updated_at) comparison. This surfaced as the objectos kernel freshness probe never evicting on a UTC+8 self-hosted EE: a shifted updated_at landed before the absolute builtAtMs, 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_metadata writes ISO-Z via toISOString()) — and update() then clobbered that back to the naive format, which is the actual mechanism. A SQL-level ORDER BY updated_at (objectql metadata load) also means fixing only update() would mix formats on disk (space 0x20 < T0x54) and mis-order rows, so insert and update had to change together.

Fix (packages/plugins/driver-sql/src/sql-driver.ts)

  • update() → SQLite stamps new Date().toISOString() (canonical ISO-8601 with Z).
  • create() / bulkCreate() / upsert() → new stampInsertTimestamps() fills missing created_at/updated_at with 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_at is now insert-only (a conflicting merge() never overwrites it; updated_at still advances).
  • formatOutput() → idempotent tolerant reader repairs any legacy/raw zone-naive audit timestamp to ISO-8601-Z on read (interpreting the stored wall-clock as UTC). No data migration needed; mirrors the existing Field.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-aware TIMESTAMP).

Verification

  • New regression suite sql-driver-timestamp-format.test.ts (10 cases incl. the freshness-probe regression, idempotent read-repair, no on-disk mixing, upsert created_at immutability).
  • driver-sql full suite: 217 pass. The 3 date/time tests flagged as at-risk don't actually pin the updated_at format and pass unchanged.
  • Downstream green: objectql 64 (optimistic locking via updated_at + integration), service-analytics 164 (created_at bucketing), dogfood gate 173, service-messaging 129, service-job 36, service-settings 128, service-storage 48.
  • eslint clean; tsup DTS (type) build passes.

Docs / release

  • ADR-0074 (docs/adr/0074-canonical-audit-timestamp-storage-on-sqlite.md), building on ADR-0053.
  • Changeset: @objectstack/driver-sql patch.

Follow-up (scoped out, tracked separately): user-declared datetime fields with a NOW() default still take the naive CURRENT_TIMESTAMP default on SQLite.

🤖 Generated with Claude Code

…-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>
@vercel

vercelBot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJun 26, 2026 10:57am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m labels Jun 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/driver-sql.

8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/core/plugins.mdx(via @objectstack/driver-sql)
  • content/docs/concepts/implementation-status.mdx(via @objectstack/driver-sql)
  • content/docs/concepts/terminology.mdx(via @objectstack/driver-sql)
  • content/docs/getting-started/glossary.mdx(via @objectstack/driver-sql)
  • content/docs/guides/driver-configuration.mdx(via @objectstack/driver-sql)
  • content/docs/guides/packages.mdx(via @objectstack/driver-sql)
  • content/docs/protocol/objectos/index.mdx(via @objectstack/driver-sql)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/driver-sql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit 98a1535 into mainJun 26, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/musing-swanson-bda53a branch June 26, 2026 11:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang