Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion drizzle-orm/src/gel-core/dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@ export class GelDialect {
return sql.join(columnNames.flatMap((colName, i) => {
const col = tableColumns[colName]!;

const onUpdateFnResult = col.onUpdateFn?.();
// Only invoke the `$onUpdate` callback when the column is absent from
// `set` — see drizzle-team/drizzle-orm#5780.
const onUpdateFnResult = set[colName] !== undefined ? undefined : col.onUpdateFn?.();
const value = set[colName] ?? (is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col));
const res = sql`${sql.identifier(this.casing.getColumnCasing(col))} = ${value}`;

Expand Down
4 changes: 3 additions & 1 deletion drizzle-orm/src/mysql-core/dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ export class MySqlDialect {
columnNames.flatMap((colName, i) => {
const col = tableColumns[colName]!;

const onUpdateFnResult = col.onUpdateFn?.();
// Only invoke the `$onUpdate` callback when the column is absent
// from `set` — see drizzle-team/drizzle-orm#5780.
const onUpdateFnResult = set[colName] !== undefined ? undefined : col.onUpdateFn?.();
const value = set[colName]
?? (is(onUpdateFnResult, SQL)
? onUpdateFnResult
Expand Down
7 changes: 6 additions & 1 deletion drizzle-orm/src/pg-core/dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,12 @@ export class PgDialect {
return sql.join(columnNames.flatMap((colName, i) => {
const col = tableColumns[colName]!;

const onUpdateFnResult = col.onUpdateFn?.();
// Only invoke the `$onUpdate` callback when the column is absent from
// `set` — eagerly calling it for every column would re-introduce the
// regression in #5780 where side-effecting callbacks (logging,
// throwing, reading external state) fire on every UPDATE even when
// the caller explicitly provided the column value.
const onUpdateFnResult = set[colName] !== undefined ? undefined : col.onUpdateFn?.();
const value = set[colName] ?? (is(onUpdateFnResult, SQL) ? onUpdateFnResult : sql.param(onUpdateFnResult, col));
const res = sql`${sql.identifier(this.casing.getColumnCasing(col))} = ${value}`;

Expand Down
4 changes: 3 additions & 1 deletion drizzle-orm/src/singlestore-core/dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ export class SingleStoreDialect {
columnNames.flatMap((colName, i) => {
const col = tableColumns[colName]!;

const onUpdateFnResult = col.onUpdateFn?.();
// Only invoke the `$onUpdate` callback when the column is absent
// from `set` — see drizzle-team/drizzle-orm#5780.
const onUpdateFnResult = set[colName] !== undefined ? undefined : col.onUpdateFn?.();
const value = set[colName]
?? (is(onUpdateFnResult, SQL)
? onUpdateFnResult
Expand Down
4 changes: 3 additions & 1 deletion drizzle-orm/src/sqlite-core/dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ export abstract class SQLiteDialect {
columnNames.flatMap((colName, i) => {
const col = tableColumns[colName]!;

const onUpdateFnResult = col.onUpdateFn?.();
// Only invoke the `$onUpdate` callback when the column is absent
// from `set` — see drizzle-team/drizzle-orm#5780.
const onUpdateFnResult = set[colName] !== undefined ? undefined : col.onUpdateFn?.();
const value = set[colName]
?? (is(onUpdateFnResult, SQL)
? onUpdateFnResult
Expand Down
70 changes: 70 additions & 0 deletions drizzle-orm/tests/on-update.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import postgres from 'postgres';
import { describe, expect, it } from 'vitest';
import { pgTable, text, uuid } from '~/pg-core';
import { drizzle } from '~/postgres-js';
import { eq } from '~/sql';

// Regression for drizzle-team/drizzle-orm#5780. In 0.45.x `buildUpdateSet`
// invoked every column's `$onUpdate` callback eagerly, even when the column
// value was explicitly supplied in `set`. Callbacks with side effects
// (throwing, logging, reading external state) then ran on every UPDATE.
//
// These tests assert the 0.44.6 contract: `$onUpdate` is only invoked when
// the column is absent from `set`. The pg-core dialect is exercised here;
// the identical pattern lives in mysql-core, sqlite-core, singlestore-core,
// and gel-core, and is fixed alongside pg-core in the same patch.

const db = drizzle(postgres(''));

describe('$onUpdate evaluation', () => {
it('does not invoke the callback when the column is explicitly provided in set', () => {
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(),
});

const query = db
.update(table)
.set({ name: 'foo', updatedById: '00000000-0000-0000-0000-000000000000' })
.where(eq(table.id, '00000000-0000-0000-0000-000000000000'));

expect(() => query.toSQL()).not.toThrow();
expect(query.toSQL()).toEqual({
sql:
'update "example" set "name" = $1, "updated_by_id" = $2 where "example"."id" = $3',
params: [
'foo',
'00000000-0000-0000-0000-000000000000',
'00000000-0000-0000-0000-000000000000',
],
});
});

it('invokes the callback exactly once when the column is absent from set', () => {
let invocations = 0;
const table = pgTable('example', {
id: uuid('id').primaryKey(),
name: text('name').notNull(),
updatedById: uuid('updated_by_id')
.$onUpdate(() => {
invocations++;
return '11111111-1111-1111-1111-111111111111';
})
.notNull(),
});

const query = db
.update(table)
.set({ name: 'foo' })
.where(eq(table.id, '00000000-0000-0000-0000-000000000000'));

const compiled = query.toSQL();
expect(invocations).toBe(1);
expect(compiled.params).toContain('11111111-1111-1111-1111-111111111111');
});
});