drizzle-team / drizzle-team/drizzle-orm
[BUG]: `$onUpdate` callback invoked eagerly in `buildUpdateSet` even when column is explicitly provided in set (regression in 0.45.x)
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
### Report hasn't been filed before.
- [x] I have verified that the bug I'm about to report hasn't been filed before.
### What version of `drizzle-orm` are you using?
0.45.2
### What version of `drizzle-kit` are you using?
0.0.0
### Other packages
_No response_
### Describe the Bug
In 0.45.x, the `$onUpdate` callback is called unconditionally for every column that has one, even when that column's value is explicitly provided in the `set` object. In 0.44.6 the callback was lazy — it only fired when the column was absent from `set`.
This is a regression. Any `$onUpdate` callback with side effects (throwing, reading external state, logging) will now trigger on every `UPDATE` query, even those that explicitly supply the column value and would never use the hook's return value.
**To Reproduce**
```ts
const table = pgTable('example', {
id: uuid('id').primaryKey(),
name: text('name').notNull(),
updatedById: uuid('updated_by_id')
.$onUpdate(() => {
throw new Error('should not be called when column is explicitly set');
})
.notNull(),
});
// This should NOT invoke the $onUpdate callback — updatedById is explicitly provided.
await db.update(table).set({ name: 'foo', updatedById: 'some-uuid' }).where(...);
```
With `0.44.6` this completes without error. With `0.45.x` it throws.
**Expected behavior**
The `$onUpdate` callback should only be invoked when the column is **absent** from `set`. Calling a callback whose return value will be unconditionally discarded is surprising and constitutes a breaking change for any callback with side effects — throwing, reading external state, logging, etc. The 0.44.6 behavior (lazy evaluation via `??` short-circuit) was consistent with this expectation.
**Actual behavior**
The callback is invoked for every column that has `$onUpdate`, regardless of whether that column appears in `set`. The return value is still correctly discarded via `??`, but the side effect of calling the callback (e.g. a `throw`, reading external state, logging) always runs.
**Root cause**
In `pg-core/dialect.ts`, `buildUpdateSet` changed between versions:
```js
// 0.44.6 — lazy: onUpdateFn only called when set[colName] is absent
const value = set[colName] ?? sql.param(col.onUpdateFn(), col);
// 0.45.x — eager: onUpdateFn always called, result conditionally used
const onUpdateFnResult = col.onUpdateFn?.();
const value = set[colName] ?? (is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col));
```
The variable was extracted (likely to support the new `is(onUpdateFnResult, SQL)` check) but the extraction moved the call outside the short-circuit.
**Suggested fix**
Keep the `SQL`-type check from 0.45.x but restore lazy evaluation:
```js
// only invoke the hook when the column is absent from set
const onUpdateFnResult = set[colName] !== void 0 ? undefined : col.onUpdateFn?.();
const value = set[colName] ?? (is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col));
```
**Environment**
- `drizzle-orm`: 0.45.x (regression from 0.44.6)
- Driver: `pg` / `postgres` (PostgreSQL)
- Node: 24.x
Contributor guide
Assessment
This issue has not been assessed yet.