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)

Open
#5,780 1 comment 1 reaction 0 assignees View on GitHub
bug
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.