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
This commit is contained in:
Sasha
2025-07-10 16:49:12 +03:00
committed by GitHub
parent c77b39c3b4
commit 055cc4ef12
7 changed files with 272 additions and 19 deletions

View File

@@ -223,6 +223,20 @@ export default buildConfigWithDefaults({
},
],
},
{
type: 'group',
name: 'group',
fields: [{ name: 'text', type: 'text' }],
},
{
type: 'tabs',
tabs: [
{
name: 'tab',
fields: [{ name: 'text', type: 'text' }],
},
],
},
],
hooks: {
beforeOperation: [

View File

@@ -7,9 +7,7 @@ import {
migrateRelationshipsV2_V3,
migrateVersionsV1_V2,
} from '@payloadcms/db-mongodb/migration-utils'
import { objectToFrontmatter } from '@payloadcms/richtext-lexical'
import { randomUUID } from 'crypto'
import { type Table } from 'drizzle-orm'
import * as drizzlePg from 'drizzle-orm/pg-core'
import * as drizzleSqlite from 'drizzle-orm/sqlite-core'
import fs from 'fs'
@@ -2809,6 +2807,35 @@ describe('database', () => {
}
})
it('should update simple', async () => {
const post = await payload.create({
collection: 'posts',
data: {
text: 'other text (should not be nuked)',
title: 'hello',
group: { text: 'in group' },
tab: { text: 'in tab' },
arrayWithIDs: [{ text: 'some text' }],
},
})
const res = await payload.db.updateOne({
where: { id: { equals: post.id } },
data: {
title: 'hello updated',
group: { text: 'in group updated' },
tab: { text: 'in tab updated' },
},
collection: 'posts',
})
expect(res.title).toBe('hello updated')
expect(res.text).toBe('other text (should not be nuked)')
expect(res.group.text).toBe('in group updated')
expect(res.tab.text).toBe('in tab updated')
expect(res.arrayWithIDs).toHaveLength(1)
expect(res.arrayWithIDs[0].text).toBe('some text')
})
it('should support x3 nesting blocks', async () => {
const res = await payload.create({
collection: 'posts',

View File

@@ -232,6 +232,12 @@ export interface Post {
blockType: 'block-first';
}[]
| null;
group?: {
text?: string | null;
};
tab?: {
text?: string | null;
};
updatedAt: string;
createdAt: string;
}
@@ -804,6 +810,16 @@ export interface PostsSelect<T extends boolean = true> {
blockName?: T;
};
};
group?:
| T
| {
text?: T;
};
tab?:
| T
| {
text?: T;
};
updatedAt?: T;
createdAt?: T;
}

View File

@@ -1,9 +1,93 @@
{
"id": "a3dd8ca0-5e09-407b-9178-e0ff7f15da59",
"id": "bf183b76-944c-4e83-bd58-4aa993885106",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.users_sessions": {
"name": "users_sessions",
"schema": "",
"columns": {
"_order": {
"name": "_order",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"_parent_id": {
"name": "_parent_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"id": {
"name": "id",
"type": "varchar",
"primaryKey": true,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp(3) with time zone",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp(3) with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"users_sessions_order_idx": {
"name": "users_sessions_order_idx",
"columns": [
{
"expression": "_order",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"users_sessions_parent_id_idx": {
"name": "users_sessions_parent_id_idx",
"columns": [
{
"expression": "_parent_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"users_sessions_parent_id_fk": {
"name": "users_sessions_parent_id_fk",
"tableFrom": "users_sessions",
"tableTo": "users",
"columnsFrom": ["_parent_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",

View File

@@ -1,10 +1,18 @@
import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-postgres'
import type { MigrateDownArgs, MigrateUpArgs} from '@payloadcms/db-postgres';
import { sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TABLE "users" (
CREATE TABLE "users_sessions" (
"_order" integer NOT NULL,
"_parent_id" integer NOT NULL,
"id" varchar PRIMARY KEY NOT NULL,
"created_at" timestamp(3) with time zone,
"expires_at" timestamp(3) with time zone NOT NULL
);
CREATE TABLE "users" (
"id" serial PRIMARY KEY NOT NULL,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"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> {
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
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;
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;
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;
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;
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;
CREATE INDEX "users_sessions_order_idx" ON "users_sessions" USING btree ("_order");
CREATE INDEX "users_sessions_parent_id_idx" ON "users_sessions" USING btree ("_parent_id");
CREATE INDEX "users_updated_at_idx" ON "users" USING btree ("updated_at");
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
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> {
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
DROP TABLE "users" CASCADE;
DROP TABLE "users_sessions" CASCADE;
DROP TABLE "users" CASCADE;
DROP TABLE "payload_locked_documents" CASCADE;
DROP TABLE "payload_locked_documents_rels" CASCADE;
DROP TABLE "payload_preferences" CASCADE;

View File

@@ -1,9 +1,9 @@
import * as migration_20250624_214621 from './20250624_214621.js'
import * as migration_20250707_123508 from './20250707_123508.js'
export const migrations = [
{
up: migration_20250624_214621.up,
down: migration_20250624_214621.down,
name: '20250624_214621',
up: migration_20250707_123508.up,
down: migration_20250707_123508.down,
name: '20250707_123508',
},
]