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:
@@ -1,15 +1,67 @@
|
|||||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
import type { LibSQLDatabase } from 'drizzle-orm/libsql'
|
||||||
import type { UpdateOne } from 'payload'
|
import type { FlattenedField, UpdateOne } from 'payload'
|
||||||
|
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
import toSnakeCase from 'to-snake-case'
|
import toSnakeCase from 'to-snake-case'
|
||||||
|
|
||||||
import type { DrizzleAdapter } from './types.js'
|
import type { DrizzleAdapter } from './types.js'
|
||||||
|
|
||||||
|
import { buildFindManyArgs } from './find/buildFindManyArgs.js'
|
||||||
import { buildQuery } from './queries/buildQuery.js'
|
import { buildQuery } from './queries/buildQuery.js'
|
||||||
import { selectDistinct } from './queries/selectDistinct.js'
|
import { selectDistinct } from './queries/selectDistinct.js'
|
||||||
|
import { transform } from './transform/read/index.js'
|
||||||
|
import { transformForWrite } from './transform/write/index.js'
|
||||||
import { upsertRow } from './upsertRow/index.js'
|
import { upsertRow } from './upsertRow/index.js'
|
||||||
import { getTransaction } from './utilities/getTransaction.js'
|
import { getTransaction } from './utilities/getTransaction.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether we should use the upsertRow function for the passed data and otherwise use a simple SQL SET call.
|
||||||
|
* We need to use upsertRow only when the data has arrays, blocks, hasMany select/text/number, localized fields, complex relationships.
|
||||||
|
*/
|
||||||
|
const shouldUseUpsertRow = ({
|
||||||
|
data,
|
||||||
|
fields,
|
||||||
|
}: {
|
||||||
|
data: Record<string, unknown>
|
||||||
|
fields: FlattenedField[]
|
||||||
|
}) => {
|
||||||
|
for (const key in data) {
|
||||||
|
const value = data[key]
|
||||||
|
const field = fields.find((each) => each.name === key)
|
||||||
|
|
||||||
|
if (!field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
field.type === 'array' ||
|
||||||
|
field.type === 'blocks' ||
|
||||||
|
((field.type === 'text' ||
|
||||||
|
field.type === 'relationship' ||
|
||||||
|
field.type === 'upload' ||
|
||||||
|
field.type === 'select' ||
|
||||||
|
field.type === 'number') &&
|
||||||
|
field.hasMany) ||
|
||||||
|
((field.type === 'relationship' || field.type === 'upload') &&
|
||||||
|
Array.isArray(field.relationTo)) ||
|
||||||
|
field.localized
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(field.type === 'group' || field.type === 'tab') &&
|
||||||
|
value &&
|
||||||
|
typeof value === 'object' &&
|
||||||
|
shouldUseUpsertRow({ data: value as Record<string, unknown>, fields: field.flattenedFields })
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export const updateOne: UpdateOne = async function updateOne(
|
export const updateOne: UpdateOne = async function updateOne(
|
||||||
this: DrizzleAdapter,
|
this: DrizzleAdapter,
|
||||||
{
|
{
|
||||||
@@ -74,6 +126,7 @@ export const updateOne: UpdateOne = async function updateOne(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!idToUpdate || shouldUseUpsertRow({ data, fields: collection.flattenedFields })) {
|
||||||
const result = await upsertRow({
|
const result = await upsertRow({
|
||||||
id: idToUpdate,
|
id: idToUpdate,
|
||||||
adapter: this,
|
adapter: this,
|
||||||
@@ -94,3 +147,50 @@ export const updateOne: UpdateOne = async function updateOne(
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { row } = transformForWrite({
|
||||||
|
adapter: this,
|
||||||
|
data,
|
||||||
|
fields: collection.flattenedFields,
|
||||||
|
tableName,
|
||||||
|
})
|
||||||
|
|
||||||
|
const drizzle = db as LibSQLDatabase
|
||||||
|
await drizzle
|
||||||
|
.update(this.tables[tableName])
|
||||||
|
.set(row)
|
||||||
|
// TODO: we can skip fetching idToUpdate here with using the incoming where
|
||||||
|
.where(eq(this.tables[tableName].id, idToUpdate))
|
||||||
|
|
||||||
|
if (returning === false) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const findManyArgs = buildFindManyArgs({
|
||||||
|
adapter: this,
|
||||||
|
depth: 0,
|
||||||
|
fields: collection.flattenedFields,
|
||||||
|
joinQuery: false,
|
||||||
|
select,
|
||||||
|
tableName,
|
||||||
|
})
|
||||||
|
|
||||||
|
findManyArgs.where = eq(this.tables[tableName].id, idToUpdate)
|
||||||
|
|
||||||
|
const doc = await db.query[tableName].findFirst(findManyArgs)
|
||||||
|
|
||||||
|
// //////////////////////////////////
|
||||||
|
// TRANSFORM DATA
|
||||||
|
// //////////////////////////////////
|
||||||
|
|
||||||
|
const result = transform({
|
||||||
|
adapter: this,
|
||||||
|
config: this.payload.config,
|
||||||
|
data: doc,
|
||||||
|
fields: collection.flattenedFields,
|
||||||
|
joinQuery: false,
|
||||||
|
tableName,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -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: {
|
hooks: {
|
||||||
beforeOperation: [
|
beforeOperation: [
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import {
|
|||||||
migrateRelationshipsV2_V3,
|
migrateRelationshipsV2_V3,
|
||||||
migrateVersionsV1_V2,
|
migrateVersionsV1_V2,
|
||||||
} from '@payloadcms/db-mongodb/migration-utils'
|
} from '@payloadcms/db-mongodb/migration-utils'
|
||||||
import { objectToFrontmatter } from '@payloadcms/richtext-lexical'
|
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import { type Table } from 'drizzle-orm'
|
|
||||||
import * as drizzlePg from 'drizzle-orm/pg-core'
|
import * as drizzlePg from 'drizzle-orm/pg-core'
|
||||||
import * as drizzleSqlite from 'drizzle-orm/sqlite-core'
|
import * as drizzleSqlite from 'drizzle-orm/sqlite-core'
|
||||||
import fs from 'fs'
|
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 () => {
|
it('should support x3 nesting blocks', async () => {
|
||||||
const res = await payload.create({
|
const res = await payload.create({
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
|
|||||||
@@ -232,6 +232,12 @@ export interface Post {
|
|||||||
blockType: 'block-first';
|
blockType: 'block-first';
|
||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
|
group?: {
|
||||||
|
text?: string | null;
|
||||||
|
};
|
||||||
|
tab?: {
|
||||||
|
text?: string | null;
|
||||||
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@@ -804,6 +810,16 @@ export interface PostsSelect<T extends boolean = true> {
|
|||||||
blockName?: T;
|
blockName?: T;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
group?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
text?: T;
|
||||||
|
};
|
||||||
|
tab?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
text?: T;
|
||||||
|
};
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,93 @@
|
|||||||
{
|
{
|
||||||
"id": "a3dd8ca0-5e09-407b-9178-e0ff7f15da59",
|
"id": "bf183b76-944c-4e83-bd58-4aa993885106",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"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": {
|
"public.users": {
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-postgres'
|
import type { MigrateDownArgs, MigrateUpArgs} from '@payloadcms/db-postgres';
|
||||||
|
|
||||||
import { sql } from '@payloadcms/db-postgres'
|
import { sql } from '@payloadcms/db-postgres'
|
||||||
|
|
||||||
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
|
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" (
|
CREATE TABLE "users" (
|
||||||
"id" serial PRIMARY KEY NOT NULL,
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
"updated_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
|
"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_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_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_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;
|
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_updated_at_idx" ON "users" USING btree ("updated_at");
|
||||||
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
|
CREATE INDEX "users_created_at_idx" ON "users" USING btree ("created_at");
|
||||||
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
|
CREATE UNIQUE INDEX "users_email_idx" ON "users" USING btree ("email");
|
||||||
@@ -83,6 +94,7 @@ export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
|
|||||||
|
|
||||||
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
|
DROP TABLE "users_sessions" CASCADE;
|
||||||
DROP TABLE "users" CASCADE;
|
DROP TABLE "users" CASCADE;
|
||||||
DROP TABLE "payload_locked_documents" CASCADE;
|
DROP TABLE "payload_locked_documents" CASCADE;
|
||||||
DROP TABLE "payload_locked_documents_rels" CASCADE;
|
DROP TABLE "payload_locked_documents_rels" CASCADE;
|
||||||
@@ -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 = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
up: migration_20250624_214621.up,
|
up: migration_20250707_123508.up,
|
||||||
down: migration_20250624_214621.down,
|
down: migration_20250707_123508.down,
|
||||||
name: '20250624_214621',
|
name: '20250707_123508',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user