Found while fixing stacksjs/stacks#3014.
On a timestamped model (useTimestamps / timestampable), both ModelQueryBuilder.update(data) and an instance's save() append updated_at = ? with the time of the write, without checking whether the caller had already set updated_at. An update that names it therefore sends the column twice:
UPDATE posts SET title = ?, updated_at = ?, updated_at = ? WHERE ...- SQLite keeps the rightmost assignment and MySQL applies them left to right, so on both the managed time wins quietly.
- PostgreSQL refuses the statement:
multiple assignments to same column "updated_at".
So in a Stacks app, Model.update(id, { updated_at }) works on SQLite and MySQL and fails on PostgreSQL. The same happens with where(...).update({ updatedAt }), which the builder snake-cases, and with forceFill({ updated_at }); save().
Fix. The write owns updated_at, as it already does on insert (orm.explicit-created-at.test.ts pins "still stamps updated_at with the time of the write"). So a caller's updated_at, in either spelling, is left out of the SET list and the managed one is the only assignment. An update that names only updated_at still writes it, as the time of the write. PostgreSQL now behaves as SQLite and MySQL already did.
Test. test/orm.managed-updated-at.pg.test.ts runs against live Postgres and covers four cases: an update with updated_at, one with updatedAt, an update of updated_at alone, and save() with a changed updated_at. All four throw before the fix and pass after it.