drizzle-team / drizzle-team/drizzle-orm
[BUG]: Drizzle ORM update().set() Omits Dynamic Keys with postgres-js Adapter
- 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.43.1
### What version of `drizzle-kit` are you using?
0.31.0
### Other packages
_No response_
### Describe the Bug
Environment:
drizzle-orm: 0.43.1
Drizzle Adapter: drizzle-orm/postgres-js
Postgres Client Library: postgres (postgres.js) v3.4.5
Node.js Version: [Specify your Node.js version, e.g., v18.18.0]
Operating System: [Specify your OS, e.g., macOS Sonoma, Ubuntu 22.04]
Problem Description:
When using db.update().set(payload).where(...), the generated SQL query incorrectly omits keys from the SET clause if those keys are provided dynamically using computed property names (bracket notation []) derived from a variable within the payload object. Keys provided as hardcoded strings in the payload object are correctly included in the generated SQL.
This occurs specifically when using the postgres-js adapter. The issue persists even when the JavaScript payload object is constructed correctly before being passed to .set().
Steps to Reproduce (Conceptual):
Define Schema: Have a PostgreSQL table schema defined using Drizzle, for example:
// src/db/schema.ts
import { pgTable, pgSchema, text, timestamp } from 'drizzle-orm/pg-core';
export const underwritingSchema = pgSchema("underwriting_schema");
export const someTable = underwritingSchema.table('some_table', {
id: text('id').primaryKey(),
// Multiple timestamp columns
timestamp_a: timestamp('timestamp_a', { mode: 'date', withTimezone: true }),
timestamp_b: timestamp('timestamp_b', { mode: 'date', withTimezone: true }),
// A standard updated_at
updatedAt: timestamp('updated_at', { mode: 'date', withTimezone: true }).defaultNow().notNull(),
});
TypeScript
Initialize Drizzle: Initialize Drizzle using the postgres-js adapter:
// src/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
// ... configuration ...
const queryClient = postgres({ /* connection options */ });
export const db = drizzle(queryClient, { schema, logger: true }); // Enable logger
TypeScript
Service Function: Create a function that dynamically determines which timestamp column to update based on input, and constructs a payload object:
// src/services/someService.ts
import { db } from '../db';
import * as schema from '../db/schema';
import { eq } from 'drizzle-orm';
async function updateDynamicTimestamp(id: string, type: 'a' | 'b'): Promise {
const columnMapping = {
'a': schema.someTable.timestamp_a,
'b': schema.someTable.timestamp_b,
};
const columnToUpdate = columnMapping[type];
if (!columnToUpdate) return false;
const columnName = columnToUpdate.name; // e.g., "timestamp_a" or "timestamp_b"
const timestampValue = new Date();
// Construct payload explicitly
const updatePayload: Record = {};
updatePayload[columnName] = timestampValue; // Dynamic key assignment
updatePayload.updatedAt = timestampValue; // Static key assignment
console.log('DEBUG: Payload sent to .set():', JSON.stringify(updatePayload));
try {
const result = await db.update(schema.someTable)
.set(updatePayload) // <--- PROBLEM AREA
.where(eq(schema.someTable.id, id))
.execute(); // Non-returning update
const rowsAffected = (result as any)?.count ?? 0;
return rowsAffected > 0;
} catch (error) {
console.error('Update Error:', error);
return false;
}
}
TypeScript
Execute: Call the service function, e.g., updateDynamicTimestamp('some-id', 'a').
Observed Behavior:
The DEBUG: Payload sent to .set(): log correctly shows an object with both keys, e.g., { "timestamp_a": "...", "updatedAt": "..." }.
The subsequent Drizzle logger output for the generated SQL query only includes the hardcoded key in the SET clause:
Query: update "underwriting_schema"."some_table" set "updated_at" = $1 where "underwriting_schema"."some_table"."id" = $2 -- params: ["...", "some-id"]
SQL
The dynamic column (timestamp_a in this example) is missing from the SET clause.
The database row is updated, but only the updatedAt column changes. The function might return true because rowCount/count is 1, but the intended update didn't fully happen.
Expected Behavior:
Drizzle should generate an SQL query that includes all keys provided in the object passed to .set(), regardless of whether they are hardcoded strings or derived dynamically from variables (computed property names). The expected query would be:
Query: update "underwriting_schema"."some_table" set "timestamp_a" = $1, "updated_at" = $2 where "underwriting_schema"."some_table"."id" = $3 -- params: ["...", "...", "some-id"]
SQL
Workaround:
The only effective workaround found was to bypass the .update().set() method entirely and use raw SQL via db.execute(sql...), manually constructing the SET clause using sql.raw() for column identifiers and ensuring Date objects are formatted as strings (.toISOString()) for parameters:
// Example Workaround Code Snippet
const dynamicColumnName = columnToUpdate.name;
const timestampString = timestampValue.toISOString();
await db.execute(sql`
UPDATE ${schema.someTable}
SET ${sql.raw(`"${dynamicColumnName}"`)} = ${timestampString},
${sql.raw(`"updatedAt"`)} = ${timestampString}
WHERE ${schema.someTable.id} = ${id}
`);
TypeScript
This raw SQL approach correctly updates both columns in the database.
Conclusion:
There appears to be a bug in the SQL generation logic for db.update().set() within drizzle-orm@0.43.1 when used with the postgres-js adapter, specifically when handling objects containing dynamically assigned keys.
Contributor guide
Assessment
This issue has not been assessed yet.