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 < T 0x54) 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

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 26, 2026 10:57am

Request Review

@github-actions github-actions Bot 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 main Jun 26, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/musing-swanson-bda53a branch June 26, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant