Skip to content

Commit 1f166ba

Browse files
authored
fix(db-*): querying joins with $exists on mongodb and improve performance when querying multiple times on postgres (#14315)
1. Fixes handling of `$exists` when querying a field nested to a join field. 2. Avoids adding new SQL joins when querying a join field multiple times before ```sql SELECT DISTINCT "categories"."id", "categories"."created_at", "categories"."created_at" FROM "categories" LEFT JOIN "posts_rels" AS "c40660ed_b4bf_4644_bbd1_b7225045bb92" ON ( "categories"."id" = "c40660ed_b4bf_4644_bbd1_b7225045bb92"."categories_id" AND "c40660ed_b4bf_4644_bbd1_b7225045bb92"."path" LIKE ? ) LEFT JOIN "posts" AS "6702692c_e77e_41b3_ab0c_038d8ca584f6" ON "c40660ed_b4bf_4644_bbd1_b7225045bb92"."parent_id" = "6702692c_e77e_41b3_ab0c_038d8ca584f6"."id" LEFT JOIN "posts" AS "b8d97c7b_51c7_448e_96dd_567ac0a24ccf" ON "c40660ed_b4bf_4644_bbd1_b7225045bb92"."parent_id" = "b8d97c7b_51c7_448e_96dd_567ac0a24ccf"."id" LEFT JOIN "posts" AS "89586995_3641_4226_b047_dc3c96b33d9f" ON "c40660ed_b4bf_4644_bbd1_b7225045bb92"."parent_id" = "89586995_3641_4226_b047_dc3c96b33d9f"."id" WHERE "6702692c_e77e_41b3_ab0c_038d8ca584f6"."title" = ? AND "b8d97c7b_51c7_448e_96dd_567ac0a24ccf"."title" IS NOT NULL AND "89586995_3641_4226_b047_dc3c96b33d9f"."is_filtered" = ? ORDER BY "categories"."created_at" DESC LIMIT ? -- params: ["categories", "my-title", 1, 10] ``` now ```sql SELECT DISTINCT "categories"."id", "categories"."created_at", "categories"."created_at" FROM "categories" LEFT JOIN "posts_rels" AS "b6ba04a3_a557_4321_abfb_7b58250baa2c" ON ( "categories"."id" = "b6ba04a3_a557_4321_abfb_7b58250baa2c"."categories_id" AND "b6ba04a3_a557_4321_abfb_7b58250baa2c"."path" LIKE ? ) LEFT JOIN "posts" AS "86c9090f_c50b_485d_a9bb_1ca8b5ad079d" ON "b6ba04a3_a557_4321_abfb_7b58250baa2c"."parent_id" = "86c9090f_c50b_485d_a9bb_1ca8b5ad079d"."id" WHERE "86c9090f_c50b_485d_a9bb_1ca8b5ad079d"."title" = ? AND "86c9090f_c50b_485d_a9bb_1ca8b5ad079d"."title" IS NOT NULL AND "86c9090f_c50b_485d_a9bb_1ca8b5ad079d"."is_filtered" = ? ORDER BY "categories"."created_at" DESC LIMIT ? -- params: ["categories", "my-title", 1, 10] ```
1 parent c29e1f0 commit 1f166ba

File tree

3 files changed

+116
-37
lines changed

3 files changed

+116
-37
lines changed

packages/db-mongodb/src/queries/buildSearchParams.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export async function buildSearchParam({
114114

115115
const { operator: formattedOperator, rawQuery, val: formattedValue } = sanitizedQueryValue
116116

117-
if (rawQuery) {
117+
if (rawQuery && paths.length === 1) {
118118
return { value: rawQuery }
119119
}
120120

packages/drizzle/src/queries/getTableColumnFromPath.ts

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SQL } from 'drizzle-orm'
1+
import type { SQL, Table } from 'drizzle-orm'
22
import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'
33
import type {
44
FlattenedBlock,
@@ -8,7 +8,7 @@ import type {
88
TextField,
99
} from 'payload'
1010

11-
import { and, eq, getTableName, like, or, sql, Table } from 'drizzle-orm'
11+
import { and, eq, getTableName, like, or, sql } from 'drizzle-orm'
1212
import { type PgTableWithColumns } from 'drizzle-orm/pg-core'
1313
import { APIError, getFieldByPath } from 'payload'
1414
import { fieldShouldBeLocalized, tabHasName } from 'payload/shared'
@@ -368,10 +368,16 @@ export const getTableColumnFromPath = ({
368368

369369
if (field.hasMany) {
370370
const relationTableName = `${adapter.tableNameMap.get(toSnakeCase(field.collection))}${adapter.relationshipsSuffix}`
371-
const { newAliasTable: aliasRelationshipTable } = getTableAlias({
372-
adapter,
373-
tableName: relationTableName,
374-
})
371+
372+
const existingTable = joins.find(
373+
(e) => e.queryPath === `${constraintPath}${field.name}._rels`,
374+
)
375+
376+
const aliasRelationshipTable = (existingTable?.table ??
377+
getTableAlias({
378+
adapter,
379+
tableName: relationTableName,
380+
}).newAliasTable) as PgTableWithColumns<any>
375381

376382
const relationshipField = getFieldByPath({
377383
fields: adapter.payload.collections[field.collection].config.flattenedFields,
@@ -381,20 +387,22 @@ export const getTableColumnFromPath = ({
381387
throw new APIError('Relationship was not found')
382388
}
383389

384-
addJoinTable({
385-
condition: and(
386-
eq(
387-
adapter.tables[rootTableName].id,
388-
aliasRelationshipTable[
389-
`${(relationshipField.field as RelationshipField).relationTo as string}ID`
390-
],
390+
if (!existingTable) {
391+
addJoinTable({
392+
condition: and(
393+
eq(
394+
adapter.tables[rootTableName].id,
395+
aliasRelationshipTable[
396+
`${(relationshipField.field as RelationshipField).relationTo as string}ID`
397+
],
398+
),
399+
like(aliasRelationshipTable.path, field.on),
391400
),
392-
like(aliasRelationshipTable.path, field.on),
393-
),
394-
joins,
395-
queryPath: field.on,
396-
table: aliasRelationshipTable,
397-
})
401+
joins,
402+
queryPath: `${constraintPath}${field.name}._rels`,
403+
table: aliasRelationshipTable,
404+
})
405+
}
398406

399407
if (newCollectionPath === 'id') {
400408
return {
@@ -416,15 +424,23 @@ export const getTableColumnFromPath = ({
416424
// parent to relationship join table
417425
const relationshipFields = relationshipConfig.flattenedFields
418426

419-
const { newAliasTable: relationshipTable } = getTableAlias({
420-
adapter,
421-
tableName: relationshipTableName,
422-
})
427+
const existingMainTable = joins.find(
428+
(e) => e.queryPath === `${constraintPath}${field.name}`,
429+
)
423430

424-
joins.push({
425-
condition: eq(aliasRelationshipTable.parent, relationshipTable.id),
426-
table: relationshipTable,
427-
})
431+
const relationshipTable = (existingMainTable?.table ??
432+
getTableAlias({
433+
adapter,
434+
tableName: relationshipTableName,
435+
}).newAliasTable) as PgTableWithColumns<any>
436+
437+
if (!existingMainTable) {
438+
joins.push({
439+
condition: eq(aliasRelationshipTable.parent, relationshipTable.id),
440+
queryPath: `${constraintPath}${field.name}`,
441+
table: relationshipTable,
442+
})
443+
}
428444

429445
return getTableColumnFromPath({
430446
adapter,
@@ -448,15 +464,23 @@ export const getTableColumnFromPath = ({
448464
const newTableName = adapter.tableNameMap.get(
449465
toSnakeCase(adapter.payload.collections[field.collection].config.slug),
450466
)
451-
const { newAliasTable } = getTableAlias({ adapter, tableName: newTableName })
452-
453-
joins.push({
454-
condition: eq(
455-
newAliasTable[field.on.replaceAll('.', '_')],
456-
aliasTable ? aliasTable.id : adapter.tables[tableName].id,
457-
),
458-
table: newAliasTable,
459-
})
467+
468+
const existingTable = joins.find(
469+
(e) => e.queryPath === `${constraintPath}${field.name}`,
470+
)?.table
471+
const newAliasTable =
472+
existingTable || getTableAlias({ adapter, tableName: newTableName }).newAliasTable
473+
474+
if (!existingTable) {
475+
joins.push({
476+
condition: eq(
477+
newAliasTable[field.on.replaceAll('.', '_')],
478+
aliasTable ? aliasTable.id : adapter.tables[tableName].id,
479+
),
480+
queryPath: `${constraintPath}${field.name}`,
481+
table: newAliasTable,
482+
})
483+
}
460484

461485
if (newCollectionPath === 'id') {
462486
return {

test/joins/int.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,6 +1837,61 @@ describe('Joins Field', () => {
18371837
expect(found.docs).toHaveLength(1)
18381838
expect(found.docs[0].id).toBe(category.id)
18391839
})
1840+
1841+
it('should support where querying by a join field multiple times', async () => {
1842+
const category = await payload.create({ collection: 'categories', data: {} })
1843+
await payload.create({
1844+
collection: 'posts',
1845+
data: { group: { category: category.id }, isFiltered: true, title: 'my-category-title' },
1846+
})
1847+
1848+
const found = await payload.find({
1849+
collection: 'categories',
1850+
where: {
1851+
and: [
1852+
{
1853+
'group.relatedPosts.title': { equals: 'my-category-title' },
1854+
},
1855+
{
1856+
'group.relatedPosts.title': { exists: true },
1857+
},
1858+
{
1859+
'group.relatedPosts.isFiltered': { equals: true },
1860+
},
1861+
],
1862+
},
1863+
})
1864+
1865+
expect(found.docs).toHaveLength(1)
1866+
expect(found.docs[0].id).toBe(category.id)
1867+
})
1868+
1869+
it('should support where querying by a join field with hasMany relationship multiple times', async () => {
1870+
const category = await payload.create({ collection: 'categories', data: {} })
1871+
await payload.create({
1872+
collection: 'posts',
1873+
data: { categories: [category.id], title: 'my-title', isFiltered: true },
1874+
})
1875+
1876+
const found = await payload.find({
1877+
collection: 'categories',
1878+
where: {
1879+
and: [
1880+
{
1881+
'hasManyPosts.title': { equals: 'my-title' },
1882+
},
1883+
{
1884+
'hasManyPosts.title': { exists: true },
1885+
},
1886+
{
1887+
'hasManyPosts.isFiltered': { equals: true },
1888+
},
1889+
],
1890+
},
1891+
})
1892+
expect(found.docs).toHaveLength(1)
1893+
expect(found.docs[0].id).toBe(category.id)
1894+
})
18401895
})
18411896

18421897
async function createPost(overrides?: Partial<Post>, locale?: Config['locale']) {

0 commit comments

Comments
 (0)