feat(db-postgres): allow to store blocks in a JSON column (#12750)
Continuation of https://github.com/payloadcms/payload/pull/6245. This PR allows you to pass `blocksAsJSON: true` to SQL adapters and the adapter instead of aligning with the SQL preferred relation approach for blocks will just use a simple JSON column, which can improve performance with a large amount of blocks. To try these changes you can install `3.43.0-internal.c5bbc84`.
This commit is contained in:
@@ -81,6 +81,7 @@ export default buildConfig({
|
|||||||
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
|
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
|
||||||
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
|
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
|
||||||
| `readReplicas` | An array of DB read replicas connection strings, can be used to offload read-heavy traffic. |
|
| `readReplicas` | An array of DB read replicas connection strings, can be used to offload read-heavy traffic. |
|
||||||
|
| `blocksAsJSON` | Store blocks as a JSON column instead of using the relational structure which can improve performance with a large amount of blocks |
|
||||||
|
|
||||||
## Access to Drizzle
|
## Access to Drizzle
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export default buildConfig({
|
|||||||
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
|
| `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` |
|
||||||
| `autoIncrement` | Pass `true` to enable SQLite [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for primary keys to ensure the same ID cannot be reused from deleted rows |
|
| `autoIncrement` | Pass `true` to enable SQLite [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for primary keys to ensure the same ID cannot be reused from deleted rows |
|
||||||
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
|
| `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. |
|
||||||
|
| `blocksAsJSON` | Store blocks as a JSON column instead of using the relational structure which can improve performance with a large amount of blocks |
|
||||||
|
|
||||||
## Access to Drizzle
|
## Access to Drizzle
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export function postgresAdapter(args: Args): DatabaseAdapterObj<PostgresAdapter>
|
|||||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||||
allowIDOnCreate,
|
allowIDOnCreate,
|
||||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||||
|
blocksAsJSON: args.blocksAsJSON ?? false,
|
||||||
createDatabase,
|
createDatabase,
|
||||||
createExtensions,
|
createExtensions,
|
||||||
createMigration: buildCreateMigration({
|
createMigration: buildCreateMigration({
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export type Args = {
|
|||||||
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||||
*/
|
*/
|
||||||
beforeSchemaInit?: PostgresSchemaHook[]
|
beforeSchemaInit?: PostgresSchemaHook[]
|
||||||
|
/**
|
||||||
|
* Store blocks as JSON column instead of storing them in relational structure.
|
||||||
|
*/
|
||||||
|
blocksAsJSON?: boolean
|
||||||
/**
|
/**
|
||||||
* Pass `true` to disale auto database creation if it doesn't exist.
|
* Pass `true` to disale auto database creation if it doesn't exist.
|
||||||
* @default false
|
* @default false
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export function sqliteAdapter(args: Args): DatabaseAdapterObj<SQLiteAdapter> {
|
|||||||
allowIDOnCreate,
|
allowIDOnCreate,
|
||||||
autoIncrement: args.autoIncrement ?? false,
|
autoIncrement: args.autoIncrement ?? false,
|
||||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||||
|
blocksAsJSON: args.blocksAsJSON ?? false,
|
||||||
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
|
||||||
client: undefined,
|
client: undefined,
|
||||||
clientConfig: args.client,
|
clientConfig: args.client,
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ export type Args = {
|
|||||||
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||||
*/
|
*/
|
||||||
beforeSchemaInit?: SQLiteSchemaHook[]
|
beforeSchemaInit?: SQLiteSchemaHook[]
|
||||||
|
/**
|
||||||
|
* Store blocks as JSON column instead of storing them in relational structure.
|
||||||
|
*/
|
||||||
|
blocksAsJSON?: boolean
|
||||||
client: Config
|
client: Config
|
||||||
/** Generated schema from payload generate:db-schema file path */
|
/** Generated schema from payload generate:db-schema file path */
|
||||||
generateSchemaOutputFile?: string
|
generateSchemaOutputFile?: string
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export function vercelPostgresAdapter(args: Args = {}): DatabaseAdapterObj<Verce
|
|||||||
afterSchemaInit: args.afterSchemaInit ?? [],
|
afterSchemaInit: args.afterSchemaInit ?? [],
|
||||||
allowIDOnCreate,
|
allowIDOnCreate,
|
||||||
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
beforeSchemaInit: args.beforeSchemaInit ?? [],
|
||||||
|
blocksAsJSON: args.blocksAsJSON ?? false,
|
||||||
createDatabase,
|
createDatabase,
|
||||||
createExtensions,
|
createExtensions,
|
||||||
defaultDrizzleSnapshot,
|
defaultDrizzleSnapshot,
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ export type Args = {
|
|||||||
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
* To generate Drizzle schema from the database, see [Drizzle Kit introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull)
|
||||||
*/
|
*/
|
||||||
beforeSchemaInit?: PostgresSchemaHook[]
|
beforeSchemaInit?: PostgresSchemaHook[]
|
||||||
|
/**
|
||||||
|
* Store blocks as JSON column instead of storing them in relational structure.
|
||||||
|
*/
|
||||||
|
blocksAsJSON?: boolean
|
||||||
connectionString?: string
|
connectionString?: string
|
||||||
/**
|
/**
|
||||||
* Pass `true` to disale auto database creation if it doesn't exist.
|
* Pass `true` to disale auto database creation if it doesn't exist.
|
||||||
|
|||||||
@@ -252,6 +252,20 @@ export const traverseFields = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (adapter.blocksAsJSON) {
|
||||||
|
if (select || selectAllOnCurrentLevel) {
|
||||||
|
const fieldPath = `${path}${field.name}`
|
||||||
|
|
||||||
|
if ((isFieldLocalized || parentIsLocalized) && _locales) {
|
||||||
|
_locales.columns[fieldPath] = true
|
||||||
|
} else if (adapter.tables[currentTableName]?.[fieldPath]) {
|
||||||
|
currentArgs.columns[fieldPath] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||||
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
const block = typeof _block === 'string' ? adapter.payload.blocks[_block] : _block
|
||||||
const blockKey = `_blocks_${block.slug}${!block[InternalBlockTableNameIndex] ? '' : `_${block[InternalBlockTableNameIndex]}`}`
|
const blockKey = `_blocks_${block.slug}${!block[InternalBlockTableNameIndex] ? '' : `_${block[InternalBlockTableNameIndex]}`}`
|
||||||
|
|||||||
@@ -180,6 +180,9 @@ export const getTableColumnFromPath = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
case 'blocks': {
|
case 'blocks': {
|
||||||
|
if (adapter.blocksAsJSON) {
|
||||||
|
break
|
||||||
|
}
|
||||||
let blockTableColumn: TableColumn
|
let blockTableColumn: TableColumn
|
||||||
let newTableName: string
|
let newTableName: string
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ export function parseParams({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
['json', 'richText'].includes(field.type) &&
|
(['json', 'richText'].includes(field.type) ||
|
||||||
|
(field.type === 'blocks' && adapter.blocksAsJSON)) &&
|
||||||
Array.isArray(pathSegments) &&
|
Array.isArray(pathSegments) &&
|
||||||
pathSegments.length > 1
|
pathSegments.length > 1
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export const traverseFields = ({
|
|||||||
adapter.payload.config.localization &&
|
adapter.payload.config.localization &&
|
||||||
(isFieldLocalized || forceLocalized) &&
|
(isFieldLocalized || forceLocalized) &&
|
||||||
field.type !== 'array' &&
|
field.type !== 'array' &&
|
||||||
field.type !== 'blocks' &&
|
(field.type !== 'blocks' || adapter.blocksAsJSON) &&
|
||||||
(('hasMany' in field && field.hasMany !== true) || !('hasMany' in field))
|
(('hasMany' in field && field.hasMany !== true) || !('hasMany' in field))
|
||||||
) {
|
) {
|
||||||
hasLocalizedField = true
|
hasLocalizedField = true
|
||||||
@@ -370,6 +370,17 @@ export const traverseFields = ({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'blocks': {
|
case 'blocks': {
|
||||||
|
if (adapter.blocksAsJSON) {
|
||||||
|
targetTable[fieldName] = withDefault(
|
||||||
|
{
|
||||||
|
name: columnName,
|
||||||
|
type: 'jsonb',
|
||||||
|
},
|
||||||
|
field,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
const disableNotNullFromHere = Boolean(field.admin?.condition) || disableNotNull
|
||||||
|
|
||||||
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
;(field.blockReferences ?? field.blocks).forEach((_block) => {
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'blocks') {
|
if (field.type === 'blocks' && !adapter.blocksAsJSON) {
|
||||||
const blockFieldPath = `${sanitizedPath}${field.name}`
|
const blockFieldPath = `${sanitizedPath}${field.name}`
|
||||||
const blocksByPath = blocks[blockFieldPath]
|
const blocksByPath = blocks[blockFieldPath]
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ export const traverseFields = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'blocks') {
|
if (field.type === 'blocks' && !adapter.blocksAsJSON) {
|
||||||
;(field.blockReferences ?? field.blocks).forEach((block) => {
|
;(field.blockReferences ?? field.blocks).forEach((block) => {
|
||||||
const matchedBlock =
|
const matchedBlock =
|
||||||
typeof block === 'string'
|
typeof block === 'string'
|
||||||
|
|||||||
@@ -315,6 +315,7 @@ export type BuildDrizzleTable<T extends DrizzleAdapter = DrizzleAdapter> = (args
|
|||||||
}) => void
|
}) => void
|
||||||
|
|
||||||
export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
||||||
|
blocksAsJSON?: boolean
|
||||||
convertPathToJSONTraversal?: (incomingSegments: string[]) => string
|
convertPathToJSONTraversal?: (incomingSegments: string[]) => string
|
||||||
countDistinct: CountDistinct
|
countDistinct: CountDistinct
|
||||||
createJSONQuery: (args: CreateJSONQueryArgs) => string
|
createJSONQuery: (args: CreateJSONQueryArgs) => string
|
||||||
@@ -323,8 +324,8 @@ export interface DrizzleAdapter extends BaseDatabaseAdapter {
|
|||||||
drizzle: LibSQLDatabase | PostgresDB
|
drizzle: LibSQLDatabase | PostgresDB
|
||||||
dropDatabase: DropDatabase
|
dropDatabase: DropDatabase
|
||||||
enums?: never | Record<string, unknown>
|
enums?: never | Record<string, unknown>
|
||||||
execute: Execute<unknown>
|
|
||||||
|
|
||||||
|
execute: Execute<unknown>
|
||||||
features: {
|
features: {
|
||||||
json?: boolean
|
json?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2618,6 +2618,54 @@ describe('database', () => {
|
|||||||
expect(res.testBlocksLocalized[0]?.text).toBe('text-localized')
|
expect(res.testBlocksLocalized[0]?.text).toBe('text-localized')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should CRUD with blocks as JSON in SQL adapters', async () => {
|
||||||
|
// eslint-disable-next-line jest/no-conditional-in-test
|
||||||
|
if (!('drizzle' in payload.db)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
process.env.PAYLOAD_FORCE_DRIZZLE_PUSH = 'true'
|
||||||
|
payload.db.blocksAsJSON = true
|
||||||
|
delete payload.db.pool
|
||||||
|
await payload.db.init()
|
||||||
|
await payload.db.connect()
|
||||||
|
expect(payload.db.tables.blocks_docs.testBlocks).toBeDefined()
|
||||||
|
expect(payload.db.tables.blocks_docs_locales.testBlocksLocalized).toBeDefined()
|
||||||
|
const res = await payload.create({
|
||||||
|
collection: 'blocks-docs',
|
||||||
|
data: {
|
||||||
|
testBlocks: [{ blockType: 'cta', text: 'text' }],
|
||||||
|
testBlocksLocalized: [{ blockType: 'cta', text: 'text-localized' }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(res.testBlocks[0]?.text).toBe('text')
|
||||||
|
expect(res.testBlocksLocalized[0]?.text).toBe('text-localized')
|
||||||
|
const res_es = await payload.update({
|
||||||
|
collection: 'blocks-docs',
|
||||||
|
id: res.id,
|
||||||
|
locale: 'es',
|
||||||
|
data: {
|
||||||
|
testBlocksLocalized: [{ blockType: 'cta', text: 'text-localized-es' }],
|
||||||
|
testBlocks: [{ blockType: 'cta', text: 'text_updated' }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(res_es.testBlocks[0]?.text).toBe('text_updated')
|
||||||
|
expect(res_es.testBlocksLocalized[0]?.text).toBe('text-localized-es')
|
||||||
|
const res_all = await payload.findByID({
|
||||||
|
collection: 'blocks-docs',
|
||||||
|
id: res.id,
|
||||||
|
locale: 'all',
|
||||||
|
})
|
||||||
|
expect(res_all.testBlocks[0]?.text).toBe('text_updated')
|
||||||
|
expect(res_all.testBlocksLocalized.es[0]?.text).toBe('text-localized-es')
|
||||||
|
expect(res_all.testBlocksLocalized.en[0]?.text).toBe('text-localized')
|
||||||
|
payload.db.blocksAsJSON = false
|
||||||
|
process.env.PAYLOAD_FORCE_DRIZZLE_PUSH = 'false'
|
||||||
|
delete payload.db.pool
|
||||||
|
await payload.db.init()
|
||||||
|
await payload.db.connect()
|
||||||
|
})
|
||||||
|
|
||||||
it('should support in with null', async () => {
|
it('should support in with null', async () => {
|
||||||
await payload.delete({ collection: 'posts', where: {} })
|
await payload.delete({ collection: 'posts', where: {} })
|
||||||
const post_1 = await payload.create({
|
const post_1 = await payload.create({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "353cac31-1e1a-4190-8584-025abe855faa",
|
"id": "3c35a6b5-e20d-4a43-af15-a6b3a0844000",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { MigrateDownArgs, MigrateUpArgs } from '@payloadcms/db-postgres'
|
import type { MigrateDownArgs, MigrateUpArgs} from '@payloadcms/db-postgres';
|
||||||
|
|
||||||
import { sql } from '@payloadcms/db-postgres'
|
import { sql } from '@payloadcms/db-postgres'
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as migration_20250611_163948 from './20250611_163948.js'
|
import * as migration_20250616_190121 from './20250616_190121.js'
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
up: migration_20250611_163948.up,
|
up: migration_20250616_190121.up,
|
||||||
down: migration_20250611_163948.down,
|
down: migration_20250616_190121.down,
|
||||||
name: '20250611_163948',
|
name: '20250616_190121',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user