Repository navigation
Description
Found while choosing the stamp expression for #11176's upsert path. Not fixed there: that card is about whether updated_at advances, this one is about the fidelity of the value it advances to, and fixing it changes the SQL every update() on MySQL emits.
The mismatch
packages/drivers/driver-sql/src/sql-driver.ts:
createAuditTimestampColumnbuilds the audit columns on MySQL asDATETIME(3)defaulted withthis.knex.fn.now(3), and its docblock says why in as many words: "CURRENT_TIMESTAMPhas to carry matching precision for aDATETIME(3)default, hencenow(3)" (MySQL: Field.datetime maps to TIMESTAMP (range ends 2038-01-19) and binds a Date in the process-local timezone #3942).updatedAtStamp()— the value every UPDATE door writes into that same column — returns a barethis.knex.fn.now(), which compiles to an unqualifiedCURRENT_TIMESTAMP. MySQL truncates that to whole seconds.
So the column is created with millisecond precision on purpose and then written at second precision.
Measured, live MySQL 8.0.46
Against the exact schema createAuditTimestampColumn produces:
AFTER INSERT (both columns from the DEFAULT now(3))
created_at 2026-08-23 11:59:33.357 updated_at 2026-08-23 11:59:33.357
AFTER an update() stamp — CURRENT_TIMESTAMP, what updatedAtStamp() emits
created_at 2026-08-23 11:59:33.357 updated_at 2026-08-23 11:59:33.000
-> updated_at is EARLIER than created_at, by 357 ms
AFTER the precision-matched form CURRENT_TIMESTAMP(3)
created_at 2026-08-23 11:59:33.357 updated_at 2026-08-23 11:59:33.361 ok
Consequences
- "Last modified" can precede "created". Any consumer comparing the two — an audit answer, a "modified since creation?" badge, a data-quality check — reads a row that was modified as if it were not.
- Delta / incremental sync can SKIP a row. A cursor held at millisecond precision (
updated_at > cursor) misses every row whose stamp was truncated back below the cursor. That is the same silent-wrong-answer family as driver-sql:updated_atis never refreshed on a deployment that skips boot schema sync —tablesWithTimestampsis also only filled by DDL #11067 and driver-sql:updateMany()never stampsupdated_at, andupsert()'s merge branch does not advance it on Postgres/MySQL — on every deployment, DDL or not #11176, reached by a different mechanism. - Two updates in the same second are indistinguishable, so an ordering by
updated_atis unstable exactly where it matters most.
Postgres is unaffected: CURRENT_TIMESTAMP there is transaction_timestamp() at microsecond precision, and the column is timestamptz. SQLite is unaffected: updatedAtStamp() returns a full ISO-8601 string with millis there.
This is already half-known in the repo — sql-driver-timestamps-without-ddl.test.ts documents the truncation in its BACKDATED_MS docblock as a reason to backdate rather than sleep, but records it as a testing hazard rather than as a defect in the stored value.
Suggested shape (not decided here)
Give updatedAtStamp() the same precision the column default carries — this.knex.fn.now(3) on MySQL, unchanged elsewhere. #11176's upsertUpdatedAtStamp() already does exactly this for the upsert door and carries the reasoning at the site; the two would then collapse into one helper. Deliberately left as two by that PR so the UPDATE door's emitted SQL was not changed by a card that had not measured it.
Note the ordering constraint if both land: whoever converges them owns re-running the live MySQL cells of the driver-sql matrix, since the emitted SQL for every update() on MySQL changes.
Generated by Claude Code
Activity
os-zhuang commentedon Aug 23, 2026
Triage:
pm:queue,domain:engine, Bug — measured on live MySQL 8.0.46:updated_atwritten at second precision into aDATETIME(3)column whose DEFAULT isnow(3); "modified before created" rows and ms-cursor delta-sync skips are user-reachable consequences. Fix shape inherited from #11176'supsertUpdatedAtStamp()(precision-matchednow(3)on MySQL, unchanged elsewhere); converging the two helpers is in scope. Constraints: ① serial/fold with thesql-driver.tsfamily — #11223 and any in-flight #11176 work touch the same file; the engine seat answers fold-or-serial across the family with the five gates at claim time; ② whoever lands it owns re-running the live MySQL cells of the driver-sql matrix (emitted SQL for everyupdate()on MySQL changes), per the card's own ordering note.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
Claim
Seat
domain:engine· sessionsession_01RfyXxZ2WPjcjhuXpiQQc3y· branchclaude/issue-11224-mysql-update-stamp-precisionFiled by this seat's dev while choosing the stamp expression for #11176; promoted by triage. Duplicate check clean, both queries: repo-scoped PR search returns nothing, and
git ls-remote origin 'refs/heads/*issue-11224*'returns 0 branches — with a positive control (issue-9167returns 2), so the zero is a reading.⭐ This card is NOT a fresh design — the correct form already exists and is already measured
#11176 landed
upsertUpdatedAtStamp()for the upsert door precisely because the bare form was measured wrong there, and its dev wrote the follow-up plan down: "#11176'supsertUpdatedAtStamp()already carries the correct form for the upsert door; the two collapse into one helper when #11224 lands."⇒ The work is generalising an already-verified expression to the UPDATE door and collapsing the pair into one helper, not deciding what the right stamp is. ⛔ Do not re-derive it.
The defect, as measured on live MySQL 8.0.46
updatedAtStamp()is a bareknex.fn.now(), which compiles to an unqualifiedCURRENT_TIMESTAMP— truncated to whole seconds — against aDATETIME(3)audit column whose own DEFAULT isnow(3)(createAuditTimestampColumn's docblock states the precision requirement in as many words):Three consequences, all silent: "last modified" can precede "created"; a millisecond-precision delta cursor SKIPS rows whose stamp was truncated back below it; two updates within the same second are indistinguishable.
⛔ Serialization: the file is occupied by ANOTHER SEAT, and the reasoning matters
packages/drivers/driver-sql/src/sql-driver.tsis currently also being changed by PR #11270 (spec seat, card #11122,claude/issue-11122-introspection-contract, draft since 07:24Z, not enqueued). It touches the file's introspection region — withdrawing theindexespromise, wideningdefaultValue/maxLength— plusschema-drift.tsandpackages/spec.This seat's same-file rule is not being applied against it, deliberately:
check-single-claim-paths.mjs: 68 concurrent pairs / 300 PRs; the at-most-one-writer list has exactly one entry, and it is not this file).indexespromise; widendefaultValue/maxLengthto the measured emitted types #11270 is in the introspection region, this card is in the write-door stamp. No semantic overlap, and they sit far apart in the file.introspectForeignKeysis the same region #11270 is rewriting. That one waits for #11270, and that is a technical judgement about overlap, not a protocol requirement.Boundaries
indexespromise; widendefaultValue/maxLengthto the measured emitted types #11270 will land changes elsewhere in this file. Branch from currentmain, and if it merges first, rebase rather than fight it. ⛔ Do not touch the introspection region.pm:on-holdneighbours live in this file: drivers(sql):upsert's empty-merge-set fallback is merge-ALL, which re-admits every insert-only column — currently unreachable, but it is the wrong shape for "nothing to merge" #8740 (restart =insertOnlyUpsertColumnsgains a member / a fourth dialect / a conflict-target column gains a non-null DEFAULT) and drivers(sql): 共享 canonical 修复表达式把「裸数字文本」当儒略日读,backfill 会把这个误读写进盘里(观察项) #6009 (restart = touchingsqliteCanonicalDatetimeSqlorbackfillCanonicalDatetimes). ⛔ Touch either symbol ⇒ stop and report; do not repair them in passing.update()on MySQL emits — the card itself flags that as a verification surface driver-sql:updateMany()never stampsupdated_at, andupsert()'s merge branch does not advance it on Postgres/MySQL — on every deployment, DDL or not #11176 did not measure. Live MySQL evidence is required, not a unit-test-only argument.packages/specoff limits (and spec: withdraw the never-honored introspectionindexespromise; widendefaultValue/maxLengthto the measured emitted types #11270 is already there — ⛔ do not collide with it). ⛔ Never editcontent/docs/releases/. Breaking changes shipminor, nevermajor.sql-driver.tscarries an ADR-0120 anchor roster thatdispatch-gates.mjsdoes not select — runcheck:adr-anchorsby hand.pnpm --filter <pkg> <script>exits 0; read the tool's printed lines and confirm the script name was echoed. A gate failing on an unbuilt worktree is NOT MEASURED, not a pass.Generated by Claude Code
fix(driver-sql): stamp updated_at at the audit column's own precision…
os-zhuang commentedon Aug 23, 2026
{ "issue": 11224, "status": "done", "branch": "claude/issue-11224-mysql-update-stamp-precision", "pr": "https://github.com/objectstack-ai/objectstack/pull/11320", "premise_still_valid": true, "summary": "Verified the card's premise against origin/main before writing code: `updatedAtStamp()` was a bare `knex.fn.now()` on non-SQLite, `createAuditTimestampColumn` really does build MySQL audit columns as DATETIME(3) DEFAULT now(3), and #11176's `upsertUpdatedAtStamp()` was still a separate helper carrying the precision-matched form. Generalised that already-measured expression to the UPDATE door (now(3) on MySQL, unchanged on PG/SQLite) and COLLAPSED THE PAIR: `upsertUpdatedAtStamp()` is deleted and `stampUpsertUpdatedAt` reads `updatedAtStamp()`, so all four stamping doors (update, updateMany, rotatedUpdateById, upsert merge) share one definition. Because the removed helper is a `protected` member that shipped in 17.2.0, the changeset declares BREAKING at `minor` (never major) and carries an ADR-0087 `not-required (no-migration-prescription)` disposition; `content/docs/releases/` untouched. Neither pm:on-hold neighbour is touched. Two live DB services (MySQL 8.0.46, Postgres 16) were started in this shared container for the measurements and were deliberately LEFT RUNNING rather than stopped: they are system daemons, not my child processes, and a sibling agent's live-dialect-matrix run could be using them — stopping them is PM's call, not a silent teardown.", "tests": "LIVE BEFORE/AFTER, MySQL 8.0.46, raw SQL against the exact DDL createAuditTimestampColumn emits (`datetime(3) default current_timestamp(3)`): before/CURRENT_TIMESTAMP -> created_at 2026-08-23 10:22:36.799, updated_at 2026-08-23 10:22:36.000, delta -799.0000 ms, '*** updated_at BEFORE created_at ***'; after/CURRENT_TIMESTAMP(3) -> created_at 10:22:36.799, updated_at 10:22:36.802, delta +3.0000 ms, 'ok'. FAILING BASELINE DEMONSTRATED FIRST, through the real driver: the new suite was written and run against the UNMODIFIED driver -> 'Tests 6 failed | 15 passed (21)', e.g. 's0: updated_at 2026-08-23T10:19:37.000Z is EARLIER than created_at 2026-08-23T10:19:37.911Z' (911 ms), 'rows skipped by their own delta cursor after update(): c0, c1, c2, c3, c4, c5', 'every update in this second stamped the SAME instant: expected 1 to be greater than 1'. All 6 reds were on the live-mysql cell; SQLite 7/7 and live Postgres 7/7 green in that same baseline run — that IS the other-dialects measurement, repeated green after the fix. Baseline was run BEFORE any edit, so no revert/restore step was needed and no measurement is post-hoc. AFTER: `pnpm --filter @objectstack/driver-sql test` with TZ=America/New_York, OS_TEST_MYSQL_URL + OS_TEST_POSTGRES_URL set and OS_EXPECT_LIVE_DIALECT_MATRIX=1, servers skewed as CI skews them (MySQL @@global.time_zone='+08:00', PG timezone='Asia/Shanghai') -> 'Test Files 121 passed (121)' / 'Tests 2488 passed (2488)', VERDICT command-exit 0. DOWNSTREAM direction, prefix form `...@objectstack/driver-sql` = the package plus its 47 DEPENDENTS: `turbo run typecheck` 76/78 tasks green on the first pass; the only failure was @objectstack/plugin-auth reporting TS7016 'Could not find a declaration file for module @objectstack/plugin-auth' in its OWN examples/basic-usage.ts — a cold-worktree race between its build's DTS step and its own typecheck, i.e. NOT MEASURED rather than a fail; built alone and re-run it is exit 0 ('tsc --noEmit && tsc --noEmit -p tsconfig.examples.json'). REPO-WIDE `pnpm lint` (eslint . --no-inline-config) run in full, exit 0 in 2m03s — NO NARROWING DECLARED anywhere in this task. GATES: `node scripts/pm/dispatch-gates.mjs` run with NO paths passed (it derived the change set itself: 5 paths vs merge base f24c90df3); every derived family plus every convention-triggered one plus `check:adr-anchors` by hand (the ADR-0120 roster the derivation does not select) plus `check:nul-bytes` — all exit 0, each captured before any pipe via `cmd > file 2>&1; EXIT=$?`: adr-anchors 0, nul-bytes 0, changeset-gate-self-tests 0, driver-conformance 0, objectui-changeset 0, published-files 0, slot-lookup 0, test-source-alias 0, type-source-resolution 0, check-adr-0087-registration 0, check-changeset-no-major 0, check-ci-filter-parity 0, check-empty-changeset 0, check-plugin-teardown-shape 0, docs-audit/check-affected-docs 0, query-options-erasure 0, type-check-coverage 0, type-check-debt 0, engine-double-contract 0, cross-package-test-inputs 0, where-matcher 0, lint 0. The three changeset gates were re-run AFTER committing (they diff HEAD, so a staged-only tree reads as '0 changesets seen'); post-commit check-adr-0087-registration reports '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'. The ratchet family was re-run on the FINAL commit c46bb3405 and reports against it: 'slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new', 'query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new', 'where-matcher conformance holds: 283 matcher(s) discovered, 283 answer the combinator battery correctly or refuse it loudly'. check:type-check-debt ran on a fully built workspace (turbo build --filter=./packages/* --filter=./packages/*/* exit 0 first), so its verdict is a measurement: 'OK — 33 ledger entr(ies) re-measured in 325.1s, 1897 raw tsc error(s) total, none above its recorded number.' No ratchet ceiling raised, no test skipped/disabled/quarantined. ON-HOLD SYMBOL COUNTS over the diff's changed lines, with controls: insertOnlyUpsertColumns 0, sqliteCanonicalDatetimeSql 0, backfillCanonicalDatetimes 0; POSITIVE CONTROL, same literals over sql-driver.ts as it stands: 2 / 10 / 7 occurrences respectively, and a second control feeding the diff filter a synthetic '+ this.insertOnlyUpsertColumns(object);' line returns 1 — so the three zeros are readings, not an unarmed grep. Also verified: NO in-repo subclass overrides either helper (SqliteWasmDriver and TursoDriver override neither and are SQLite-family, so they take the unchanged branch). Zero-match pnpm filter trap avoided: every run's output was checked for the echoed script name rather than a bare $?. NO ABLATION was performed in this task, so no rebuild/mutation-on-disk claim is made — the baseline red was obtained by running the new suite against unmodified source, which needs no mutation.", "open_questions": [], "out_of_scope_findings": [ "filed as #11321: on SQLite `createAuditTimestampColumn` gives the builtin audit columns `default CURRENT_TIMESTAMP` (zone-naive, space-separated, second-precision — measured: an out-of-band insert stores \"2026-08-23 11:03:23\") while a declared Field.datetime NOW() default in the SAME table gets the canonical `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))` and stores \"2026-08-23T11:03:23.611Z\". Unassigned, no labels; searched first (repo-scoped issue search + local scan, no existing card). Not repaired in passing: different door, different dialect, and it changes DDL — a new verification surface, so it fails the bounded-exemption test. Flagged in the card that a fix reaching for backfillCanonicalDatetimes / sqliteCanonicalDatetimeSql collides with pm:on-hold #6009." ] }Two bounded in-place repairs are declared in the PR body rather than left silent: removing
upsertUpdatedAtStamp()left three docblocks (twoBACKDATED_MSrationales and one §4 head note, acrosssql-driver-11176-bulk-and-merge-updated-at.test.tsandsql-driver-timestamps-without-ddl.test.ts) asserting driver behaviour that is false as of this commit — one of them recording the very defect this card fixes as if it were a property of MySQL. All three edits are comment-only; no assertion, fixture or executed line changed, and both files pass unchanged.Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
os-dev-report
Marker note (read this first). The previous comment (#issuecomment-5385669065) carried the same report but opened with the HTML-comment form of the marker. Read back after posting, GitHub's sanitizer had eaten it — the stored body starts at the fenced block, so that comment is invisible to a marker scan. No comment-edit tool is exposed to this seat, so this comment supersedes it with the marker as literal text. This is the authoritative report; the earlier one is a duplicate, not a second run.
{ "issue": 11224, "status": "done", "branch": "claude/issue-11224-mysql-update-stamp-precision", "pr": "https://github.com/objectstack-ai/objectstack/pull/11320", "premise_still_valid": true, "summary": "Verified the card's premise against origin/main before writing code: `updatedAtStamp()` was a bare `knex.fn.now()` on non-SQLite, `createAuditTimestampColumn` really does build MySQL audit columns as DATETIME(3) DEFAULT now(3), and #11176's `upsertUpdatedAtStamp()` was still a separate helper carrying the precision-matched form. Generalised that already-measured expression to the UPDATE door (now(3) on MySQL, unchanged on PG/SQLite) and COLLAPSED THE PAIR: `upsertUpdatedAtStamp()` is deleted and `stampUpsertUpdatedAt` reads `updatedAtStamp()`, so all four stamping doors (update, updateMany, rotatedUpdateById, upsert merge) share one definition. Because the removed helper is a `protected` member that shipped in 17.2.0, the changeset declares BREAKING at `minor` (never major) and carries an ADR-0087 `not-required (no-migration-prescription)` disposition; `content/docs/releases/` untouched. Neither pm:on-hold neighbour is touched. Two live DB services (MySQL 8.0.46, Postgres 16) were started in this shared container for the measurements and were deliberately LEFT RUNNING rather than stopped: they are system daemons, not my child processes, and a sibling agent's live-dialect-matrix run could be using them — stopping them is PM's call, not a silent teardown. Worktree torn down without --force after the PR opened; the sibling worktree objectstack-issue-11201 was left untouched.", "tests": "LIVE BEFORE/AFTER, MySQL 8.0.46, raw SQL against the exact DDL createAuditTimestampColumn emits (`datetime(3) default current_timestamp(3)`): before/CURRENT_TIMESTAMP -> created_at 2026-08-23 10:22:36.799, updated_at 2026-08-23 10:22:36.000, delta -799.0000 ms, '*** updated_at BEFORE created_at ***'; after/CURRENT_TIMESTAMP(3) -> created_at 10:22:36.799, updated_at 10:22:36.802, delta +3.0000 ms, 'ok'. FAILING BASELINE DEMONSTRATED FIRST, through the real driver: the new suite was written and run against the UNMODIFIED driver -> 'Tests 6 failed | 15 passed (21)', e.g. 's0: updated_at 2026-08-23T10:19:37.000Z is EARLIER than created_at 2026-08-23T10:19:37.911Z' (911 ms), 'rows skipped by their own delta cursor after update(): c0, c1, c2, c3, c4, c5', 'every update in this second stamped the SAME instant: expected 1 to be greater than 1'. All 6 reds were on the live-mysql cell; SQLite 7/7 and live Postgres 7/7 green in that same baseline run — that IS the other-dialects measurement, repeated green after the fix. Baseline was run BEFORE any edit, so no revert/restore step was needed and no measurement is post-hoc. AFTER: `pnpm --filter @objectstack/driver-sql test` with TZ=America/New_York, OS_TEST_MYSQL_URL + OS_TEST_POSTGRES_URL set and OS_EXPECT_LIVE_DIALECT_MATRIX=1, servers skewed as CI skews them (MySQL @@global.time_zone='+08:00', PG timezone='Asia/Shanghai') -> 'Test Files 121 passed (121)' / 'Tests 2488 passed (2488)', VERDICT command-exit 0. DOWNSTREAM direction, prefix form `...@objectstack/driver-sql` = the package plus its 47 DEPENDENTS: `turbo run typecheck` 76/78 tasks green on the first pass; the only failure was @objectstack/plugin-auth reporting TS7016 'Could not find a declaration file for module @objectstack/plugin-auth' in its OWN examples/basic-usage.ts — a cold-worktree race between its build's DTS step and its own typecheck, i.e. NOT MEASURED rather than a fail; built alone and re-run it is exit 0 ('tsc --noEmit && tsc --noEmit -p tsconfig.examples.json'). REPO-WIDE `pnpm lint` (eslint . --no-inline-config) run in full, exit 0 in 2m03s — NO NARROWING DECLARED anywhere in this task. GATES: `node scripts/pm/dispatch-gates.mjs` run with NO paths passed (it derived the change set itself: 5 paths vs merge base f24c90df3); every derived family plus every convention-triggered one plus `check:adr-anchors` by hand (the ADR-0120 roster the derivation does not select) plus `check:nul-bytes` — all exit 0, each captured before any pipe via `cmd > file 2>&1; EXIT=$?`: adr-anchors 0, nul-bytes 0, changeset-gate-self-tests 0, driver-conformance 0, objectui-changeset 0, published-files 0, slot-lookup 0, test-source-alias 0, type-source-resolution 0, check-adr-0087-registration 0, check-changeset-no-major 0, check-ci-filter-parity 0, check-empty-changeset 0, check-plugin-teardown-shape 0, docs-audit/check-affected-docs 0, query-options-erasure 0, type-check-coverage 0, type-check-debt 0, engine-double-contract 0, cross-package-test-inputs 0, where-matcher 0, lint 0. The three changeset gates were re-run AFTER committing (they diff HEAD, so a staged-only tree reads as '0 changesets seen'); post-commit check-adr-0087-registration reports '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'. The ratchet family was re-run on the FINAL commit c46bb3405 and reports against it: 'slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new', 'query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new', 'where-matcher conformance holds: 283 matcher(s) discovered, 283 answer the combinator battery correctly or refuse it loudly'. check:type-check-debt ran on a fully built workspace (turbo build --filter=./packages/* --filter=./packages/*/* exit 0 first), so its verdict is a measurement: 'OK — 33 ledger entr(ies) re-measured in 325.1s, 1897 raw tsc error(s) total, none above its recorded number.' No ratchet ceiling raised, no test skipped/disabled/quarantined. ON-HOLD SYMBOL COUNTS over the diff's changed lines, with controls: insertOnlyUpsertColumns 0, sqliteCanonicalDatetimeSql 0, backfillCanonicalDatetimes 0; POSITIVE CONTROL, same literals over sql-driver.ts as it stands: 2 / 10 / 7 occurrences respectively, and a second control feeding the diff filter a synthetic '+ this.insertOnlyUpsertColumns(object);' line returns 1 — so the three zeros are readings, not an unarmed grep. Also verified: NO in-repo subclass overrides either helper (SqliteWasmDriver and TursoDriver override neither and are SQLite-family, so they take the unchanged branch). Zero-match pnpm filter trap avoided: every run's output was checked for the echoed script name rather than a bare $?. NO ABLATION was performed in this task, so no rebuild/mutation-on-disk claim is made — the baseline red was obtained by running the new suite against unmodified source, which needs no mutation.", "open_questions": [], "out_of_scope_findings": [ "filed as #11321: on SQLite `createAuditTimestampColumn` gives the builtin audit columns `default CURRENT_TIMESTAMP` (zone-naive, space-separated, second-precision — measured: an out-of-band insert stores '2026-08-23 11:03:23') while a declared Field.datetime NOW() default in the SAME table gets the canonical `(strftime('%Y-%m-%dT%H:%M:%fZ','now'))` and stores '2026-08-23T11:03:23.611Z'. Unassigned, no labels; searched first (repo-scoped issue search + local scan, no existing card). Not repaired in passing: different door, different dialect, and it changes DDL — a new verification surface, so it fails the bounded-exemption test. Flagged in the card that a fix reaching for backfillCanonicalDatetimes / sqliteCanonicalDatetimeSql collides with pm:on-hold #6009." ] }Two bounded in-place repairs are declared in the PR body rather than left silent: removing
upsertUpdatedAtStamp()left three docblocks (twoBACKDATED_MSrationales and one §4 head note, acrosssql-driver-11176-bulk-and-merge-updated-at.test.tsandsql-driver-timestamps-without-ddl.test.ts) asserting driver behaviour that is false as of this commit — one of them recording the very defect this card fixes as if it were a property of MySQL. All three edits are comment-only; no assertion, fixture or executed line changed, and both files pass unchanged.Generated by Claude Code
fix(driver-sql): stamp updated_at at the audit column's own precision…
fix(driver-sql): give the SQLite builtin audit columns the canonical …
test(driver-sql): derive §3's span ceiling from the conclusion it gua…
test(driver-sql): derive §3's span ceiling from the conclusion it gua…