Files
payloadcms/test/select/postgreslogs.int.spec.ts
Alessio Gravili 94f5e790f6 perf(drizzle): single-roundtrip db updates for simple collections (#13186)
Currently, an optimized DB update (simple data => no
delete-and-create-row) does the following:
1. sql UPDATE
2. sql SELECT

This PR reduces this further to one single DB call for simple
collections:
1. sql UPDATE with RETURNING()

This only works for simple collections that do not have any fields that
need to be fetched from other tables. If a collection has fields like
relationship or blocks, we'll need that separate SELECT call to join in
the other tables.

In 4.0, we can remove all "complex" fields from the jobs collection and
replace them with a JSON field to make use of this optimization

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1210803039809814
2025-07-23 01:45:55 -07:00

180 lines
4.5 KiB
TypeScript

/* eslint-disable jest/require-top-level-describe */
import type { Payload } from 'payload'
import path from 'path'
import { assert } from 'ts-essentials'
import { fileURLToPath } from 'url'
import type { Point, Post } from './payload-types.js'
import { initPayloadInt } from '../helpers/initPayloadInt.js'
let payload: Payload
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const describePostgres = process.env.PAYLOAD_DATABASE === 'postgres' ? describe : describe.skip
describePostgres('Select - with postgres logs', () => {
// --__--__--__--__--__--__--__--__--__
// Boilerplate test setup/teardown
// --__--__--__--__--__--__--__--__--__
beforeAll(async () => {
const initialized = await initPayloadInt(
dirname,
undefined,
undefined,
'config.postgreslogs.ts',
)
assert(initialized.payload)
assert(initialized.restClient)
;({ payload } = initialized)
})
afterAll(async () => {
await payload.destroy()
})
describe('Local API - Base', () => {
let post: Post
let postId: number | string
let point: Point
let pointId: number | string
beforeEach(async () => {
post = await createPost()
postId = post.id
point = await createPoint()
pointId = point.id
})
// Clean up to safely mutate in each test
afterEach(async () => {
await payload.delete({ id: postId, collection: 'posts' })
await payload.delete({ id: pointId, collection: 'points' })
})
describe('Local API - operations', () => {
it('ensure optimized db update is still used when using select', async () => {
const post = await createPost()
// Count every console log
const consoleCount = jest.spyOn(console, 'log').mockImplementation(() => {})
const res = removeEmptyAndUndefined(
(await payload.db.updateOne({
collection: 'posts',
id: post.id,
data: {
text: 'new text',
},
select: { text: true, number: true },
})) as any,
)
expect(consoleCount).toHaveBeenCalledTimes(1) // Should be 1 single sql call if the optimization is used. If not, this would be 2 calls
consoleCount.mockRestore()
expect(res.number).toEqual(1)
expect(res.text).toEqual('new text')
expect(res.id).toEqual(post.id)
expect(Object.keys(res)).toHaveLength(3)
})
})
})
})
function removeEmptyAndUndefined(obj: any): any {
if (Array.isArray(obj)) {
const cleanedArray = obj
.map(removeEmptyAndUndefined)
.filter(
(item) =>
item !== undefined && !(typeof item === 'object' && Object.keys(item).length === 0),
)
return cleanedArray.length > 0 ? cleanedArray : undefined
}
if (obj !== null && typeof obj === 'object') {
const cleanedEntries = Object.entries(obj)
.map(([key, value]) => [key, removeEmptyAndUndefined(value)])
.filter(
([, value]) =>
value !== undefined &&
!(
typeof value === 'object' &&
(Array.isArray(value) ? value.length === 0 : Object.keys(value).length === 0)
),
)
return cleanedEntries.length > 0 ? Object.fromEntries(cleanedEntries) : undefined
}
return obj
}
async function createPost() {
const upload = await payload.create({
collection: 'upload',
data: {},
filePath: path.resolve(dirname, 'image.jpg'),
})
const relation = await payload.create({
depth: 0,
collection: 'rels',
data: {},
})
return payload.create({
collection: 'posts',
depth: 0,
data: {
number: 1,
text: 'text',
select: 'a',
selectMany: ['a'],
group: {
number: 1,
text: 'text',
},
hasMany: [relation],
hasManyUpload: [upload],
hasOne: relation,
hasManyPoly: [{ relationTo: 'rels', value: relation }],
hasOnePoly: { relationTo: 'rels', value: relation },
blocks: [
{
blockType: 'cta',
ctaText: 'cta-text',
text: 'text',
},
{
blockType: 'intro',
introText: 'intro-text',
text: 'text',
},
],
array: [
{
text: 'text',
number: 1,
},
],
tab: {
text: 'text',
number: 1,
},
unnamedTabNumber: 2,
unnamedTabText: 'text2',
},
})
}
function createPoint() {
return payload.create({ collection: 'points', data: { text: 'some', point: [10, 20] } })
}