Skip to content

Commit 824a996

Browse files
os-zhuangclaude
andauthored
fix(driver-sql): advance updated_at on updateMany() and on upsert()'s merge branch (#11176) (#11240)
Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3ece770 commit 824a996

3 files changed

Lines changed: 566 additions & 6 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@objectstack/driver-sql": patch
3+
---
4+
5+
`updateMany()` and `upsert()`'s merge branch now advance `updated_at` (#11176).
6+
7+
Two write doors were leaving "last modified" reading the row's previous value —
8+
on **every** deployment, DDL-managed or not (which is what separates these from
9+
#11067, whose defect needed `skipSchemaSync`):
10+
11+
- **`updateMany()` stamped nothing.** No `tablesWithTimestamps` consultation, no
12+
`updated_at`. `update()`, `bulkUpdate()` and `rotatedUpdateById()` all stamp;
13+
this door did not, so a bulk edit left every row it touched reading its
14+
creation time.
15+
- **`upsert()`'s merge branch did not advance it on Postgres and MySQL.** The
16+
merge set is derived from the keys of the formatted payload, and
17+
`stampInsertTimestamps` — the only thing that put `updated_at` there — returns
18+
early on any non-SQLite dialect, because the column DEFAULT already stores a
19+
zone-aware instant on insert. A DEFAULT does not re-fire on the conflict path.
20+
**SQLite was accidentally correct; Postgres and MySQL were not.**
21+
22+
Measured on live PostgreSQL 16.13 and MySQL 8.0.46, through the driver's own
23+
`initObjects`, with `update()` advancing the same column on the same table in
24+
the same run as the contrast.
25+
26+
Nothing errored before this fix, which is why it went unnoticed: list-view
27+
sorts, delta/incremental sync, cache invalidation and audit answers simply read
28+
a stale `updated_at`. A bulk status change and a sync/import that upserts — the
29+
common shape for connector and seed writes — are exactly the operations most
30+
likely to be feeding a downstream delta consumer.
31+
32+
No accept-set change and no new rejection. Both doors keep #3493's opt-in
33+
historical import (`preserveAudit` with an explicit `updated_at`) intact, and a
34+
hand-migrated table that genuinely lacks the column keeps working: `updateMany`
35+
reuses #11067's presumption-and-recovery machinery whole, and the upsert door
36+
stamps only where the column has been OBSERVED, never on a presumption it has no
37+
way to recover from. The SQL emitted for SQLite, and for any object with no
38+
observed `updated_at` column, is unchanged.

0 commit comments

Comments
 (0)