Skip to content

Commit a0d8131

Browse files
authored
fix(db-postgres): joins to self collection (#10182)
### What? With Postgres, before join to self like: ```ts import type { CollectionConfig } from 'payload' export const SelfJoins: CollectionConfig = { slug: 'self-joins', fields: [ { name: 'rel', type: 'relationship', relationTo: 'self-joins', }, { name: 'joins', type: 'join', on: 'rel', collection: 'self-joins', }, ], } ``` wasn't possible, even though it's a valid usage. ### How? Now, to differentiate parent `self_joins` and children `self_joins` we do additional alias for the nested select - `"4d3cf2b6_1adf_46a8_b6d2_3e1c3809d737"`: ```sql select "id", "rel_id", "updated_at", "created_at", ( select coalesce( json_agg( json_build_object('id', "joins_alias".id) ), '[]' :: json ) from ( select "created_at", "rel_id", "id" from "self_joins" "4d3cf2b6_1adf_46a8_b6d2_3e1c3809d737" where "4d3cf2b6_1adf_46a8_b6d2_3e1c3809d737"."rel_id" = "self_joins"."id" order by "4d3cf2b6_1adf_46a8_b6d2_3e1c3809d737"."created_at" desc limit $1 ) "joins_alias" ) as "joins_alias" from "self_joins" where "self_joins"."id" = $2 order by "self_joins"."created_at" desc limit $3 ``` Fixes #10144 -->
1 parent 6b45b2d commit a0d8131

File tree

9 files changed

+119
-16
lines changed

9 files changed

+119
-16
lines changed

packages/drizzle/src/find/traverseFields.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type { BuildQueryJoinAliases, ChainedMethods, DrizzleAdapter } from '../t
99
import type { Result } from './buildFindManyArgs.js'
1010

1111
import buildQuery from '../queries/buildQuery.js'
12+
import { getTableAlias } from '../queries/getTableAlias.js'
13+
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
1214
import { jsonAggBuildObject } from '../utilities/json.js'
1315
import { rawConstraint } from '../utilities/rawConstraint.js'
1416
import { chainMethods } from './chainMethods.js'
@@ -385,12 +387,22 @@ export const traverseFields = ({
385387
}
386388
}
387389

390+
const columnName = `${path.replaceAll('.', '_')}${field.name}`
391+
392+
const subQueryAlias = `${columnName}_alias`
393+
394+
const { newAliasTable } = getTableAlias({
395+
adapter,
396+
tableName: joinCollectionTableName,
397+
})
398+
388399
const {
389400
orderBy,
390401
selectFields,
391402
where: subQueryWhere,
392403
} = buildQuery({
393404
adapter,
405+
aliasTable: newAliasTable,
394406
fields,
395407
joins,
396408
locale,
@@ -418,15 +430,21 @@ export const traverseFields = ({
418430

419431
const db = adapter.drizzle as LibSQLDatabase
420432

421-
const columnName = `${path.replaceAll('.', '_')}${field.name}`
433+
for (let key in selectFields) {
434+
const val = selectFields[key]
422435

423-
const subQueryAlias = `${columnName}_alias`
436+
if (val.table && getNameFromDrizzleTable(val.table) === joinCollectionTableName) {
437+
delete selectFields[key]
438+
key = key.split('.').pop()
439+
selectFields[key] = newAliasTable[key]
440+
}
441+
}
424442

425443
const subQuery = chainMethods({
426444
methods: chainedMethods,
427445
query: db
428446
.select(selectFields as any)
429-
.from(adapter.tables[joinCollectionTableName])
447+
.from(newAliasTable)
430448
.where(subQueryWhere)
431449
.orderBy(() => orderBy.map(({ column, order }) => order(column))),
432450
}).as(subQueryAlias)
@@ -440,7 +458,7 @@ export const traverseFields = ({
440458
}),
441459
}),
442460
})
443-
.from(sql`${subQuery}`)}`.as(columnName)
461+
.from(sql`${subQuery}`)}`.as(subQueryAlias)
444462

445463
break
446464
}

packages/drizzle/src/queries/buildAndOrConditions.ts

Lines changed: 4 additions & 1 deletion
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 { FlattenedField, Where } from 'payload'
33

44
import type { DrizzleAdapter, GenericColumn } from '../types.js'
@@ -8,6 +8,7 @@ import { parseParams } from './parseParams.js'
88

99
export function buildAndOrConditions({
1010
adapter,
11+
aliasTable,
1112
fields,
1213
joins,
1314
locale,
@@ -17,6 +18,7 @@ export function buildAndOrConditions({
1718
where,
1819
}: {
1920
adapter: DrizzleAdapter
21+
aliasTable?: Table
2022
collectionSlug?: string
2123
fields: FlattenedField[]
2224
globalSlug?: string
@@ -36,6 +38,7 @@ export function buildAndOrConditions({
3638
if (typeof condition === 'object') {
3739
const result = parseParams({
3840
adapter,
41+
aliasTable,
3942
fields,
4043
joins,
4144
locale,

packages/drizzle/src/queries/buildOrderBy.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
import type { Table } from 'drizzle-orm'
12
import type { FlattenedField, Sort } from 'payload'
23

34
import { asc, desc } from 'drizzle-orm'
45

56
import type { DrizzleAdapter, GenericColumn } from '../types.js'
67
import type { BuildQueryJoinAliases, BuildQueryResult } from './buildQuery.js'
78

9+
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
810
import { getTableColumnFromPath } from './getTableColumnFromPath.js'
911

1012
type Args = {
1113
adapter: DrizzleAdapter
14+
aliasTable?: Table
1215
fields: FlattenedField[]
1316
joins: BuildQueryJoinAliases
1417
locale?: string
@@ -22,6 +25,7 @@ type Args = {
2225
*/
2326
export const buildOrderBy = ({
2427
adapter,
28+
aliasTable,
2529
fields,
2630
joins,
2731
locale,
@@ -68,7 +72,10 @@ export const buildOrderBy = ({
6872
})
6973
if (sortTable?.[sortTableColumnName]) {
7074
orderBy.push({
71-
column: sortTable[sortTableColumnName],
75+
column:
76+
aliasTable && tableName === getNameFromDrizzleTable(sortTable)
77+
? aliasTable[sortTableColumnName]
78+
: sortTable[sortTableColumnName],
7279
order: sortDirection === 'asc' ? asc : desc,
7380
})
7481

packages/drizzle/src/queries/buildQuery.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { asc, desc, SQL } from 'drizzle-orm'
1+
import type { asc, desc, SQL, Table } from 'drizzle-orm'
22
import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
33
import type { FlattenedField, Sort, Where } from 'payload'
44

@@ -15,6 +15,7 @@ export type BuildQueryJoinAliases = {
1515

1616
type BuildQueryArgs = {
1717
adapter: DrizzleAdapter
18+
aliasTable?: Table
1819
fields: FlattenedField[]
1920
joins?: BuildQueryJoinAliases
2021
locale?: string
@@ -35,6 +36,7 @@ export type BuildQueryResult = {
3536
}
3637
const buildQuery = function buildQuery({
3738
adapter,
39+
aliasTable,
3840
fields,
3941
joins = [],
4042
locale,
@@ -49,6 +51,7 @@ const buildQuery = function buildQuery({
4951

5052
const orderBy = buildOrderBy({
5153
adapter,
54+
aliasTable,
5255
fields,
5356
joins,
5457
locale,
@@ -62,6 +65,7 @@ const buildQuery = function buildQuery({
6265
if (incomingWhere && Object.keys(incomingWhere).length > 0) {
6366
where = parseParams({
6467
adapter,
68+
aliasTable,
6569
fields,
6670
joins,
6771
locale,

packages/drizzle/src/queries/parseParams.ts

Lines changed: 17 additions & 8 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 { FlattenedField, Operator, Where } from 'payload'
33

44
import { and, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm'
@@ -9,12 +9,14 @@ import { validOperators } from 'payload/shared'
99
import type { DrizzleAdapter, GenericColumn } from '../types.js'
1010
import type { BuildQueryJoinAliases } from './buildQuery.js'
1111

12+
import { getNameFromDrizzleTable } from '../utilities/getNameFromDrizzleTable.js'
1213
import { buildAndOrConditions } from './buildAndOrConditions.js'
1314
import { getTableColumnFromPath } from './getTableColumnFromPath.js'
1415
import { sanitizeQueryValue } from './sanitizeQueryValue.js'
1516

1617
type Args = {
1718
adapter: DrizzleAdapter
19+
aliasTable?: Table
1820
fields: FlattenedField[]
1921
joins: BuildQueryJoinAliases
2022
locale: string
@@ -26,6 +28,7 @@ type Args = {
2628

2729
export function parseParams({
2830
adapter,
31+
aliasTable,
2932
fields,
3033
joins,
3134
locale,
@@ -51,6 +54,7 @@ export function parseParams({
5154
if (Array.isArray(condition)) {
5255
const builtConditions = buildAndOrConditions({
5356
adapter,
57+
aliasTable,
5458
fields,
5559
joins,
5660
locale,
@@ -83,6 +87,7 @@ export function parseParams({
8387
table,
8488
} = getTableColumnFromPath({
8589
adapter,
90+
aliasTable,
8691
collectionPath: relationOrPath,
8792
fields,
8893
joins,
@@ -261,12 +266,18 @@ export function parseParams({
261266
break
262267
}
263268

269+
const resolvedColumn =
270+
rawColumn ||
271+
(aliasTable && tableName === getNameFromDrizzleTable(table)
272+
? aliasTable[columnName]
273+
: table[columnName])
274+
264275
if (queryOperator === 'not_equals' && queryValue !== null) {
265276
constraints.push(
266277
or(
267-
isNull(rawColumn || table[columnName]),
278+
isNull(resolvedColumn),
268279
/* eslint-disable @typescript-eslint/no-explicit-any */
269-
ne<any>(rawColumn || table[columnName], queryValue),
280+
ne<any>(resolvedColumn, queryValue),
270281
),
271282
)
272283
break
@@ -288,12 +299,12 @@ export function parseParams({
288299
}
289300

290301
if (operator === 'equals' && queryValue === null) {
291-
constraints.push(isNull(rawColumn || table[columnName]))
302+
constraints.push(isNull(resolvedColumn))
292303
break
293304
}
294305

295306
if (operator === 'not_equals' && queryValue === null) {
296-
constraints.push(isNotNull(rawColumn || table[columnName]))
307+
constraints.push(isNotNull(resolvedColumn))
297308
break
298309
}
299310

@@ -330,9 +341,7 @@ export function parseParams({
330341
break
331342
}
332343

333-
constraints.push(
334-
adapter.operators[queryOperator](rawColumn || table[columnName], queryValue),
335-
)
344+
constraints.push(adapter.operators[queryOperator](resolvedColumn, queryValue))
336345
}
337346
}
338347
}

test/joins/collections/SelfJoins.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
export const SelfJoins: CollectionConfig = {
4+
slug: 'self-joins',
5+
fields: [
6+
{
7+
name: 'rel',
8+
type: 'relationship',
9+
relationTo: 'self-joins',
10+
},
11+
{
12+
name: 'joins',
13+
type: 'join',
14+
on: 'rel',
15+
collection: 'self-joins',
16+
},
17+
],
18+
}

test/joins/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Categories } from './collections/Categories.js'
66
import { CategoriesVersions } from './collections/CategoriesVersions.js'
77
import { HiddenPosts } from './collections/HiddenPosts.js'
88
import { Posts } from './collections/Posts.js'
9+
import { SelfJoins } from './collections/SelfJoins.js'
910
import { Singular } from './collections/Singular.js'
1011
import { Uploads } from './collections/Uploads.js'
1112
import { Versions } from './collections/Versions.js'
@@ -37,6 +38,7 @@ export default buildConfigWithDefaults({
3738
Versions,
3839
CategoriesVersions,
3940
Singular,
41+
SelfJoins,
4042
{
4143
slug: localizedPostsSlug,
4244
admin: {

test/joins/int.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Payload } from 'payload'
1+
import type { Payload, TypeWithID } from 'payload'
22

33
import path from 'path'
44
import { getFileByPath } from 'payload'
@@ -975,6 +975,15 @@ describe('Joins Field', () => {
975975

976976
await payload.delete({ collection: categoriesSlug, where: { name: { equals: 'totalDocs' } } })
977977
})
978+
979+
it('should self join', async () => {
980+
const doc_1 = await payload.create({ collection: 'self-joins', data: {} })
981+
const doc_2 = await payload.create({ collection: 'self-joins', data: { rel: doc_1 }, depth: 0 })
982+
983+
const data = await payload.findByID({ collection: 'self-joins', id: doc_1.id, depth: 1 })
984+
985+
expect((data.joins.docs[0] as TypeWithID).id).toBe(doc_2.id)
986+
})
978987
})
979988

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

test/joins/payload-types.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface Config {
1818
versions: Version;
1919
'categories-versions': CategoriesVersion;
2020
singular: Singular;
21+
'self-joins': SelfJoin;
2122
'localized-posts': LocalizedPost;
2223
'localized-categories': LocalizedCategory;
2324
'restricted-categories': RestrictedCategory;
@@ -53,6 +54,9 @@ export interface Config {
5354
relatedVersions: 'versions';
5455
relatedVersionsMany: 'versions';
5556
};
57+
'self-joins': {
58+
joins: 'self-joins';
59+
};
5660
'localized-categories': {
5761
relatedPosts: 'localized-posts';
5862
};
@@ -71,6 +75,7 @@ export interface Config {
7175
versions: VersionsSelect<false> | VersionsSelect<true>;
7276
'categories-versions': CategoriesVersionsSelect<false> | CategoriesVersionsSelect<true>;
7377
singular: SingularSelect<false> | SingularSelect<true>;
78+
'self-joins': SelfJoinsSelect<false> | SelfJoinsSelect<true>;
7479
'localized-posts': LocalizedPostsSelect<false> | LocalizedPostsSelect<true>;
7580
'localized-categories': LocalizedCategoriesSelect<false> | LocalizedCategoriesSelect<true>;
7681
'restricted-categories': RestrictedCategoriesSelect<false> | RestrictedCategoriesSelect<true>;
@@ -355,6 +360,20 @@ export interface CategoriesVersion {
355360
createdAt: string;
356361
_status?: ('draft' | 'published') | null;
357362
}
363+
/**
364+
* This interface was referenced by `Config`'s JSON-Schema
365+
* via the `definition` "self-joins".
366+
*/
367+
export interface SelfJoin {
368+
id: string;
369+
rel?: (string | null) | SelfJoin;
370+
joins?: {
371+
docs?: (string | SelfJoin)[] | null;
372+
hasNextPage?: boolean | null;
373+
} | null;
374+
updatedAt: string;
375+
createdAt: string;
376+
}
358377
/**
359378
* This interface was referenced by `Config`'s JSON-Schema
360379
* via the `definition` "localized-posts".
@@ -467,6 +486,10 @@ export interface PayloadLockedDocument {
467486
relationTo: 'singular';
468487
value: string | Singular;
469488
} | null)
489+
| ({
490+
relationTo: 'self-joins';
491+
value: string | SelfJoin;
492+
} | null)
470493
| ({
471494
relationTo: 'localized-posts';
472495
value: string | LocalizedPost;
@@ -666,6 +689,16 @@ export interface SingularSelect<T extends boolean = true> {
666689
updatedAt?: T;
667690
createdAt?: T;
668691
}
692+
/**
693+
* This interface was referenced by `Config`'s JSON-Schema
694+
* via the `definition` "self-joins_select".
695+
*/
696+
export interface SelfJoinsSelect<T extends boolean = true> {
697+
rel?: T;
698+
joins?: T;
699+
updatedAt?: T;
700+
createdAt?: T;
701+
}
669702
/**
670703
* This interface was referenced by `Config`'s JSON-Schema
671704
* via the `definition` "localized-posts_select".

0 commit comments

Comments
 (0)