From 8219c046de2f8c02abffd0306c07295ff054079e Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Mon, 12 May 2025 22:34:15 +0300 Subject: [PATCH] fix(db-postgres): selectDistinct might remove expected rows when querying with nested fields or relations (#12365) Fixes https://github.com/payloadcms/payload/issues/12263 This was caused by passing not needed columns to the `SELECT DISTINCT` query, which we execute in case if we have a filter / sort by a nested field / relationship. Since the only columns that we need to pass to the `SELECT DISTINCT` query are: ID and field(s) specified in `sort`, we now filter the `selectFields` variable. --- packages/drizzle/src/find/findMany.ts | 21 +++++++++++ test/fields/collections/Date/index.ts | 10 ++++++ test/fields/int.spec.ts | 50 +++++++++++++++++++++++++++ test/fields/payload-types.ts | 26 +++++++++++--- 4 files changed, 103 insertions(+), 4 deletions(-) diff --git a/packages/drizzle/src/find/findMany.ts b/packages/drizzle/src/find/findMany.ts index 6b429f38ca..d1374f42a6 100644 --- a/packages/drizzle/src/find/findMany.ts +++ b/packages/drizzle/src/find/findMany.ts @@ -7,6 +7,7 @@ import type { DrizzleAdapter } from '../types.js' import buildQuery from '../queries/buildQuery.js' import { selectDistinct } from '../queries/selectDistinct.js' import { transform } from '../transform/read/index.js' +import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js' import { getTransaction } from '../utilities/getTransaction.js' import { buildFindManyArgs } from './buildFindManyArgs.js' @@ -75,6 +76,26 @@ export const findMany = async function find({ tableName, versions, }) + + if (orderBy) { + for (const key in selectFields) { + const column = selectFields[key] + if (column.primary) { + continue + } + + if ( + !orderBy.some( + (col) => + col.column.name === column.name && + getNameFromDrizzleTable(col.column.table) === getNameFromDrizzleTable(column.table), + ) + ) { + delete selectFields[key] + } + } + } + const selectDistinctResult = await selectDistinct({ adapter, db, diff --git a/test/fields/collections/Date/index.ts b/test/fields/collections/Date/index.ts index b6755fb3b6..4e4b21a546 100644 --- a/test/fields/collections/Date/index.ts +++ b/test/fields/collections/Date/index.ts @@ -115,6 +115,16 @@ const DateFields: CollectionConfig = { }, ], }, + { + type: 'array', + name: 'array', + fields: [ + { + name: 'date', + type: 'date', + }, + ], + }, ], } diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 6796bbb745..5ab7350323 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -600,6 +600,56 @@ describe('Fields', () => { expect(result.docs[0].id).toEqual(doc.id) }) + + // Function to generate random date between start and end dates + function getRandomDate(start: Date, end: Date): string { + const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())) + return date.toISOString() + } + + // Generate sample data + const dataSample = Array.from({ length: 100 }, (_, index) => { + const startDate = new Date('2024-01-01') + const endDate = new Date('2025-12-31') + + return { + array: Array.from({ length: 5 }, (_, listIndex) => { + return { + date: getRandomDate(startDate, endDate), + } + }), + ...dateDoc, + } + }) + + it('should query a date field inside an array field', async () => { + await payload.delete({ collection: 'date-fields', where: {} }) + for (const doc of dataSample) { + await payload.create({ + collection: 'date-fields', + data: doc, + }) + } + + const res = await payload.find({ + collection: 'date-fields', + where: { 'array.date': { greater_than: new Date('2025-06-01').toISOString() } }, + }) + + const filter = (doc: any) => + doc.array.some((item) => new Date(item.date).getTime() > new Date('2025-06-01').getTime()) + + expect(res.docs.every(filter)).toBe(true) + expect(dataSample.filter(filter)).toHaveLength(res.totalDocs) + // eslint-disable-next-line jest/no-conditional-in-test + if (res.totalDocs > 10) { + // This is where postgres might fail! selectDistinct actually removed some rows here, because it distincts by: + // not only ID, but also created_at, updated_at, items_date + expect(res.docs).toHaveLength(10) + } else { + expect(res.docs.length).toBeLessThanOrEqual(res.totalDocs) + } + }) }) describe('select', () => { diff --git a/test/fields/payload-types.ts b/test/fields/payload-types.ts index 8b1ce2ad53..c303ea6b4c 100644 --- a/test/fields/payload-types.ts +++ b/test/fields/payload-types.ts @@ -929,6 +929,12 @@ export interface DateField { id?: string | null; }[] | null; + array?: + | { + date?: string | null; + id?: string | null; + }[] + | null; updatedAt: string; createdAt: string; } @@ -1326,10 +1332,16 @@ export interface RelationshipField { } | null); relationshipDrawerHasMany?: (string | TextField)[] | null; relationshipDrawerHasManyPolymorphic?: - | { - relationTo: 'text-fields'; - value: string | TextField; - }[] + | ( + | { + relationTo: 'text-fields'; + value: string | TextField; + } + | { + relationTo: 'array-fields'; + value: string | ArrayField; + } + )[] | null; relationshipDrawerWithAllowCreateFalse?: (string | null) | TextField; relationshipDrawerWithFilterOptions?: { @@ -2492,6 +2504,12 @@ export interface DateFieldsSelect { dayAndTime_tz?: T; id?: T; }; + array?: + | T + | { + date?: T; + id?: T; + }; updatedAt?: T; createdAt?: T; }