Skip to content

Commit 055cc4e

Browse files
authored
perf(db-postgres): simplify db.updateOne to a single DB call with if the passed data doesn't include nested fields (#13060)
In case, if `payload.db.updateOne` received simple data, meaning no: * Arrays / Blocks * Localized Fields * `hasMany: true` text / select / number / relationship fields * relationship fields with `relationTo` as an array This PR simplifies the logic to a single SQL `set` call. No any extra (useless) steps with rewriting all the arrays / blocks / localized tables even if there were no any changes to them. However, it's good to note that `payload.update` (not `payload.db.updateOne`) as for now passes all the previous data as well, so this change won't have any effect unless you're using `payload.db.updateOne` directly (or for our internal logic that uses it), in the future a separate PR with optimization for `payload.update` as well may be implemented. --- - To see the specific tasks where the Asana app for GitHub is being used, see below: - https://app.asana.com/0/0/1210710489889576
1 parent c77b39c commit 055cc4e

File tree

7 files changed

+272
-19
lines changed

7 files changed

+272
-19
lines changed

packages/drizzle/src/updateOne.ts

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,67 @@
11
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
2-
import type { UpdateOne } from 'payload'
2+
import type { FlattenedField, UpdateOne } from 'payload'
33

4+
import { eq } from 'drizzle-orm'
45
import toSnakeCase from 'to-snake-case'
56

67
import type { DrizzleAdapter } from './types.js'
78

9+
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
810
import { buildQuery } from './queries/buildQuery.js'
911
import { selectDistinct } from './queries/selectDistinct.js'
12+
import { transform } from './transform/read/index.js'
13+
import { transformForWrite } from './transform/write/index.js'
1014
import { upsertRow } from './upsertRow/index.js'
1115
import { getTransaction } from './utilities/getTransaction.js'
1216

17+
/**
18+
* Checks whether we should use the upsertRow function for the passed data and otherwise use a simple SQL SET call.
19+
* We need to use upsertRow only when the data has arrays, blocks, hasMany select/text/number, localized fields, complex relationships.
20+
*/
21+
const shouldUseUpsertRow = ({
22+
data,
23+
fields,
24+
}: {
25+
data: Record<string, unknown>
26+
fields: FlattenedField[]
27+
}) => {
28+
for (const key in data) {
29+
const value = data[key]
30+
const field = fields.find((each) => each.name === key)
31+
32+
if (!field) {
33+
continue
34+
}
35+
36+
if (
37+
field.type === 'array' ||
38+
field.type === 'blocks' ||
39+
((field.type === 'text' ||
40+
field.type === 'relationship' ||
41+
field.type === 'upload' ||
42+
field.type === 'select' ||
43+
field.type === 'number') &&
44+
field.hasMany) ||
45+
((field.type === 'relationship' || field.type === 'upload') &&
46+
Array.isArray(field.relationTo)) ||
47+
field.localized
48+
) {
49+
return true
50+
}
51+
52+
if (
53+
(field.type === 'group' || field.type === 'tab') &&
54+
value &&
55+
typeof value === 'object' &&
56+
shouldUseUpsertRow({ data: value as Record<string, unknown>, fields: field.flattenedFields })
57+
) {
58+
return true
59+
}
60+
}
61+
62+
return false
63+
}
64+
1365
export const updateOne: UpdateOne = async function updateOne(
1466
this: DrizzleAdapter,
1567
{
@@ -74,23 +126,71 @@ export const updateOne: UpdateOne = async function updateOne(
74126
return null
75127
}
76128

77-
const result = await upsertRow({
78-
id: idToUpdate,
129+
if (!idToUpdate || shouldUseUpsertRow({ data, fields: collection.flattenedFields })) {
130+
const result = await upsertRow({
131+
id: idToUpdate,
132+
adapter: this,
133+
data,
134+
db,
135+
fields: collection.flattenedFields,
136+
ignoreResult: returning === false,
137+
joinQuery,
138+
operation: 'update',
139+
req,
140+
select,
141+
tableName,
142+
})
143+
144+
if (returning === false) {
145+
return null
146+
}
147+
148+
return result
149+
}
150+
151+
const { row } = transformForWrite({
79152
adapter: this,
80153
data,
81-
db,
82154
fields: collection.flattenedFields,
83-
ignoreResult: returning === false,
84-
joinQuery,
85-
operation: 'update',
86-
req,
87-
select,
88155
tableName,
89156
})
90157

158+
const drizzle = db as LibSQLDatabase
159+
await drizzle
160+
.update(this.tables[tableName])
161+
.set(row)
162+
// TODO: we can skip fetching idToUpdate here with using the incoming where
163+
.where(eq(this.tables[tableName].id, idToUpdate))
164+
91165
if (returning === false) {
92166
return null
93167
}
94168

169+
const findManyArgs = buildFindManyArgs({
170+
adapter: this,
171+
depth: 0,
172+
fields: collection.flattenedFields,
173+
joinQuery: false,
174+
select,
175+
tableName,
176+
})
177+
178+
findManyArgs.where = eq(this.tables[tableName].id, idToUpdate)
179+
180+
const doc = await db.query[tableName].findFirst(findManyArgs)
181+
182+
// //////////////////////////////////
183+
// TRANSFORM DATA
184+
// //////////////////////////////////
185+
186+
const result = transform({
187+
adapter: this,
188+
config: this.payload.config,
189+
data: doc,
190+
fields: collection.flattenedFields,
191+
joinQuery: false,
192+
tableName,
193+
})
194+
95195
return result
96196
}

test/database/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,20 @@ export default buildConfigWithDefaults({
223223
},
224224
],
225225
},
226+
{
227+
type: 'group',
228+
name: 'group',
229+
fields: [{ name: 'text', type: 'text' }],
230+
},
231+
{
232+
type: 'tabs',
233+
tabs: [
234+
{
235+
name: 'tab',
236+
fields: [{ name: 'text', type: 'text' }],
237+
},
238+
],
239+
},
226240
],
227241
hooks: {
228242
beforeOperation: [

test/database/int.spec.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import {
77
migrateRelationshipsV2_V3,
88
migrateVersionsV1_V2,
99
} from '@payloadcms/db-mongodb/migration-utils'
10-
import { objectToFrontmatter } from '@payloadcms/richtext-lexical'
1110
import { randomUUID } from 'crypto'
12-
import { type Table } from 'drizzle-orm'
1311
import * as drizzlePg from 'drizzle-orm/pg-core'
1412
import * as drizzleSqlite from 'drizzle-orm/sqlite-core'
1513
import fs from 'fs'
@@ -2809,6 +2807,35 @@ describe('database', () => {
28092807
}
28102808
})
28112809

2810+
it('should update simple', async () => {
2811+
const post = await payload.create({
2812+
collection: 'posts',
2813+
data: {
2814+
text: 'other text (should not be nuked)',
2815+
title: 'hello',
2816+
group: { text: 'in group' },
2817+
tab: { text: 'in tab' },
2818+
arrayWithIDs: [{ text: 'some text' }],
2819+
},
2820+
})
2821+
const res = await payload.db.updateOne({
2822+
where: { id: { equals: post.id } },
2823+
data: {
2824+
title: 'hello updated',
2825+
group: { text: 'in group updated' },
2826+
tab: { text: 'in tab updated' },
2827+
},
2828+
collection: 'posts',
2829+
})
2830+
2831+
expect(res.title).toBe('hello updated')
2832+
expect(res.text).toBe('other text (should not be nuked)')
2833+
expect(res.group.text).toBe('in group updated')
2834+
expect(res.tab.text).toBe('in tab updated')
2835+
expect(res.arrayWithIDs).toHaveLength(1)
2836+
expect(res.arrayWithIDs[0].text).toBe('some text')
2837+
})
2838+
28122839
it('should support x3 nesting blocks', async () => {
28132840
const res = await payload.create({
28142841
collection: 'posts',

test/database/payload-types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,12 @@ export interface Post {
232232
blockType: 'block-first';
233233
}[]
234234
| null;
235+
group?: {
236+
text?: string | null;
237+
};
238+
tab?: {
239+
text?: string | null;
240+
};
235241
updatedAt: string;
236242
createdAt: string;
237243
}
@@ -804,6 +810,16 @@ export interface PostsSelect<T extends boolean = true> {
804810
blockName?: T;
805811
};
806812
};
813+
group?:
814+
| T
815+
| {
816+
text?: T;
817+
};
818+
tab?:
819+
| T
820+
| {
821+
text?: T;
822+
};
807823
updatedAt?: T;
808824
createdAt?: T;
809825
}

test/database/up-down-migration/migrations/20250624_214621.json renamed to test/database/up-down-migration/migrations/20250707_123508.json

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,93 @@
11
{
2-
"id": "a3dd8ca0-5e09-407b-9178-e0ff7f15da59",
2+
"id": "bf183b76-944c-4e83-bd58-4aa993885106",
33
"prevId": "00000000-0000-0000-0000-000000000000",
44
"version": "7",
55
"dialect": "postgresql",
66
"tables": {
7+
"public.users_sessions": {
8+
"name": "users_sessions",
9+
"schema": "",
10+
"columns": {
11+
"_order": {
12+
"name": "_order",
13+
"type": "integer",
14+
"primaryKey": false,
15+
"notNull": true
16+
},
17+
"_parent_id": {
18+
"name": "_parent_id",
19+
"type": "integer",
20+
"primaryKey": false,
21+
"notNull": true
22+
},
23+
"id": {
24+
"name": "id",
25+
"type": "varchar",
26+
"primaryKey": true,
27+
"notNull": true
28+
},
29+
"created_at": {
30+
"name": "created_at",
31+
"type": "timestamp(3) with time zone",
32+
"primaryKey": false,
33+
"notNull": false
34+
},
35+
"expires_at": {
36+
"name": "expires_at",
37+
"type": "timestamp(3) with time zone",
38+
"primaryKey": false,
39+
"notNull": true
40+
}
41+
},
42+
"indexes": {
43+
"users_sessions_order_idx": {
44+
"name": "users_sessions_order_idx",
45+
"columns": [
46+
{
47+
"expression": "_order",
48+
"isExpression": false,
49+
"asc": true,
50+
"nulls": "last"
51+
}
52+
],
53+
"isUnique": false,
54+
"concurrently": false,
55+
"method": "btree",
56+
"with": {}
57+
},
58+
"users_sessions_parent_id_idx": {
59+
"name": "users_sessions_parent_id_idx",
60+
"columns": [
61+
{
62+
"expression": "_parent_id",
63+
"isExpression": false,
64+
"asc": true,
65+
"nulls": "last"
66+
}
67+
],
68+
"isUnique": false,
69+
"concurrently": false,
70+
"method": "btree",
71+
"with": {}
72+
}
73+
},
74+
"foreignKeys": {
75+
"users_sessions_parent_id_fk": {
76+
"name": "users_sessions_parent_id_fk",
77+
"tableFrom": "users_sessions",
78+
"tableTo": "users",
79+
"columnsFrom": ["_parent_id"],
80+
"columnsTo": ["id"],
81+
"onDelete": "cascade",
82+
"onUpdate": "no action"
83+
}
84+
},
85+
"compositePrimaryKeys": {},
86+
"uniqueConstraints": {},
87+
"policies": {},
88+
"checkConstraints": {},
89+
"isRLSEnabled": false
90+
},
791
"public.users": {
892
"name": "users",
993
"schema": "",

test/database/up-down-migration/migrations/20250624_214621.ts renamed to test/database/up-down-migration/migrations/20250707_123508.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-postgres'
1+
import type { MigrateDownArgs, MigrateUpArgs} from '@payloadcms/db-postgres';
22

33
import { sql } from '@payloadcms/db-postgres'
44

55
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
66
await db.execute(sql`
7-
CREATE TABLE "users" (
7+
CREATE TABLE "users_sessions" (
8+
"_order" integer NOT NULL,
9+
"_parent_id" integer NOT NULL,
10+
"id" varchar PRIMARY KEY NOT NULL,
11+
"created_at" timestamp(3) with time zone,
12+
"expires_at" timestamp(3) with time zone NOT NULL
13+
);
14+
15+
CREATE TABLE "users" (
816
"id" serial PRIMARY KEY NOT NULL,
917
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
1018
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
@@ -56,10 +64,13 @@ export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
5664
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
5765
);
5866
67+
ALTER TABLE "users_sessions" ADD CONSTRAINT "users_sessions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
5968
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_locked_documents"("id") ON DELETE cascade ON UPDATE no action;
6069
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
6170
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."payload_preferences"("id") ON DELETE cascade ON UPDATE no action;
6271
ALTER TABLE "payload_preferences_rels" ADD CONSTRAINT "payload_preferences_rels_users_fk" FOREIGN KEY ("users_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
72+
CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
73+
CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");
6374
CREATE INDEX "users_updated_at_idx" ON "users" USING btree ("updated_at");
6475
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
6576
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
@@ -83,7 +94,8 @@ export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
8394

8495
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
8596
await db.execute(sql`
86-
DROP TABLE "users" CASCADE;
97+
DROP TABLE "users_sessions" CASCADE;
98+
DROP TABLE "users" CASCADE;
8799
DROP TABLE "payload_locked_documents" CASCADE;
88100
DROP TABLE "payload_locked_documents_rels" CASCADE;
89101
DROP TABLE "payload_preferences" CASCADE;
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import * as migration_20250624_214621 from './20250624_214621.js'
1+
import * as migration_20250707_123508 from './20250707_123508.js'
22

33
export const migrations = [
44
{
5-
up: migration_20250624_214621.up,
6-
down: migration_20250624_214621.down,
7-
name: '20250624_214621',
5+
up: migration_20250707_123508.up,
6+
down: migration_20250707_123508.down,
7+
name: '20250707_123508',
88
},
99
]

0 commit comments

Comments
 (0)