Skip to content

[BUG]: $onUpdate callback invoked eagerly in buildUpdateSet even when column is explicitly provided in set (regression in 0.45.x) #5780

@hognevevle

Description

@hognevevle

Report hasn't been filed before.

  • 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

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:

// 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:

  // 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions