feat: compound indexes (#11512)

### What?
This PR adds ability to define indexes on several fields for collections
(compound indexes).

Example:
```ts
{
  indexes: [{ unique: true, fields: ['title', 'group.name'] }]
}
```

### Why?
This can be used to either speed up querying/sorting by 2 or more fields
at the same time or to ensure uniqueness between several fields.

### How?
Implements this logic in database adapters. Additionally, adds a utility
`getFieldByPath`.
This commit is contained in:
Sasha
2025-03-05 03:09:24 +02:00
committed by GitHub
parent f01cfbcc57
commit bacc0f002a
18 changed files with 423 additions and 21 deletions

View File

@@ -4,10 +4,9 @@ const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import type { TextField } from 'payload'
import { v4 as uuid } from 'uuid'
import { randomUUID } from 'crypto'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { seed } from './seed.js'
import {
customIDsSlug,
@@ -501,7 +500,7 @@ export default buildConfigWithDefaults({
beforeChange: [
({ value, operation }) => {
if (operation === 'create') {
return uuid()
return randomUUID()
}
return value
},
@@ -564,6 +563,43 @@ export default buildConfigWithDefaults({
],
versions: true,
},
{
slug: 'compound-indexes',
fields: [
{
name: 'one',
type: 'text',
},
{
name: 'two',
type: 'text',
},
{
name: 'three',
type: 'text',
},
{
name: 'group',
type: 'group',
fields: [
{
name: 'four',
type: 'text',
},
],
},
],
indexes: [
{
fields: ['one', 'two'],
unique: true,
},
{
fields: ['three', 'group.four'],
unique: true,
},
],
},
],
globals: [
{

View File

@@ -7,6 +7,7 @@ import {
migrateRelationshipsV2_V3,
migrateVersionsV1_V2,
} from '@payloadcms/db-mongodb/migration-utils'
import { randomUUID } from 'crypto'
import { type Table } from 'drizzle-orm'
import * as drizzlePg from 'drizzle-orm/pg-core'
import * as drizzleSqlite from 'drizzle-orm/sqlite-core'
@@ -305,6 +306,68 @@ describe('database', () => {
})
})
describe('Compound Indexes', () => {
beforeEach(async () => {
await payload.delete({ collection: 'compound-indexes', where: {} })
})
it('top level: should throw a unique error', async () => {
await payload.create({
collection: 'compound-indexes',
data: { three: randomUUID(), one: '1', two: '2' },
})
// does not fail
await payload.create({
collection: 'compound-indexes',
data: { three: randomUUID(), one: '1', two: '3' },
})
// does not fail
await payload.create({
collection: 'compound-indexes',
data: { three: randomUUID(), one: '-1', two: '2' },
})
// fails
await expect(
payload.create({
collection: 'compound-indexes',
data: { three: randomUUID(), one: '1', two: '2' },
}),
).rejects.toBeTruthy()
})
it('combine group and top level: should throw a unique error', async () => {
await payload.create({
collection: 'compound-indexes',
data: {
one: randomUUID(),
three: '3',
group: { four: '4' },
},
})
// does not fail
await payload.create({
collection: 'compound-indexes',
data: { one: randomUUID(), three: '3', group: { four: '5' } },
})
// does not fail
await payload.create({
collection: 'compound-indexes',
data: { one: randomUUID(), three: '4', group: { four: '4' } },
})
// fails
await expect(
payload.create({
collection: 'compound-indexes',
data: { one: randomUUID(), three: '3', group: { four: '4' } },
}),
).rejects.toBeTruthy()
})
})
describe('migrations', () => {
let ranFreshTest = false

View File

@@ -78,6 +78,7 @@ export interface Config {
'custom-ids': CustomId;
'fake-custom-ids': FakeCustomId;
'relationships-migration': RelationshipsMigration;
'compound-indexes': CompoundIndex;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -97,6 +98,7 @@ export interface Config {
'custom-ids': CustomIdsSelect<false> | CustomIdsSelect<true>;
'fake-custom-ids': FakeCustomIdsSelect<false> | FakeCustomIdsSelect<true>;
'relationships-migration': RelationshipsMigrationSelect<false> | RelationshipsMigrationSelect<true>;
'compound-indexes': CompoundIndexesSelect<false> | CompoundIndexesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -400,6 +402,21 @@ export interface RelationshipsMigration {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "compound-indexes".
*/
export interface CompoundIndex {
id: string;
one?: string | null;
two?: string | null;
three?: string | null;
group?: {
four?: string | null;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -472,6 +489,10 @@ export interface PayloadLockedDocument {
relationTo: 'relationships-migration';
value: string | RelationshipsMigration;
} | null)
| ({
relationTo: 'compound-indexes';
value: string | CompoundIndex;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -755,6 +776,22 @@ export interface RelationshipsMigrationSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "compound-indexes_select".
*/
export interface CompoundIndexesSelect<T extends boolean = true> {
one?: T;
two?: T;
three?: T;
group?:
| T
| {
four?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".