diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 2e714b8f2c..ba2453120c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -85,7 +85,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- database: [ mongoose, postgres, postgres-uuid, supabase ]
+ database: [mongoose, postgres, postgres-custom-schema, postgres-uuid, supabase]
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
@@ -123,7 +123,7 @@ jobs:
postgresql db: ${{ env.POSTGRES_DB }}
postgresql user: ${{ env.POSTGRES_USER }}
postgresql password: ${{ env.POSTGRES_PASSWORD }}
- if: matrix.database == 'postgres' || matrix.database == 'postgres-uuid'
+ if: startsWith(matrix.database, 'postgres')
- name: Install Supabase CLI
uses: supabase/setup-cli@v1
@@ -139,14 +139,19 @@ jobs:
- name: Wait for PostgreSQL
run: sleep 30
- if: matrix.database == 'postgres' || matrix.database == 'postgres-uuid'
+ if: startsWith(matrix.database, 'postgres')
- name: Configure PostgreSQL
run: |
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE ROLE runner SUPERUSER LOGIN;"
psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "SELECT version();"
echo "POSTGRES_URL=postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" >> $GITHUB_ENV
- if: matrix.database == 'postgres' || matrix.database == 'postgres-uuid'
+ if: startsWith(matrix.database, 'postgres')
+
+ - name: Configure PostgreSQL with custom schema
+ run: |
+ psql "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@localhost:5432/$POSTGRES_DB" -c "CREATE SCHEMA custom;"
+ if: matrix.database == 'postgres-custom-schema'
- name: Configure Supabase
run: |
diff --git a/docs/database/postgres.mdx b/docs/database/postgres.mdx
index dda06bcc35..b641fdd83a 100644
--- a/docs/database/postgres.mdx
+++ b/docs/database/postgres.mdx
@@ -2,7 +2,7 @@
title: Postgres
label: Postgres
order: 50
-desc: Payload supports Postgres through an officially supported Drizzle database adapter.
+desc: Payload supports Postgres through an officially supported Drizzle database adapter.
keywords: Postgres, documentation, typescript, Content Management System, cms, headless, javascript, node, react, express
---
@@ -37,11 +37,12 @@ export default buildConfig({
### Options
-| Option | Description |
-| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `pool` | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
-| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
-| `migrationDir` | Customize the directory that migrations are stored. |
+| Option | Description |
+|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `pool` | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres`. |
+| `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. |
+| `migrationDir` | Customize the directory that migrations are stored. |
+| `schemaName` | A string for the postgres schema to use, defaults to 'public'. |
### Access to Drizzle
@@ -65,7 +66,7 @@ In addition to exposing Drizzle directly, all of the tables, Drizzle relations,
Drizzle exposes two ways to work locally in development mode.
-The first is [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push), which automatically pushes changes you make to your Payload config (and therefore, Drizzle schema) to your database so you don't have to manually migrate every time you change your Payload config. This only works in development mode, and should not be mixed with manually running [`migrate`](/docs/database/migrations) commands.
+The first is [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push), which automatically pushes changes you make to your Payload config (and therefore, Drizzle schema) to your database so you don't have to manually migrate every time you change your Payload config. This only works in development mode, and should not be mixed with manually running [`migrate`](/docs/database/migrations) commands.
You will be warned if any changes that you make will entail data loss while in development mode. Push is enabled by default, but you can opt out if you'd like.
@@ -77,11 +78,11 @@ Migrations are extremely powerful thanks to the seamless way that Payload and Dr
1. You are building your Payload config locally, with a local database used for testing.
1. You have left the default setting of `push` enabled, so every time you change your Payload config (add or remove fields, collections, etc.), Drizzle will automatically push changes to your local DB.
-1. Once you're done with your changes, or have completed a feature, you can run `npm run payload migrate:create`.
+1. Once you're done with your changes, or have completed a feature, you can run `npm run payload migrate:create`.
1. Payload and Drizzle will look for any existing migrations, and automatically generate all SQL changes necessary to convert your schema from its prior state into the state of your current Payload config, and store the resulting DDL in a newly created migration.
1. Once you're ready to go to production, you will be able to run `npm run payload migrate` against your production database, which will apply any new migrations that have not yet run.
1. Now your production database is in sync with your Payload config!
Warning: do not mix "push" and migrations with your local development database. If you use "push" locally, and then try to migrate, Payload will throw a warning, telling you that these two methods are not meant to be used interchangeably.
-
\ No newline at end of file
+
diff --git a/packages/db-postgres/src/connect.ts b/packages/db-postgres/src/connect.ts
index d1eeb80b96..19dded99e6 100644
--- a/packages/db-postgres/src/connect.ts
+++ b/packages/db-postgres/src/connect.ts
@@ -3,7 +3,7 @@ import type { Connect } from 'payload/database'
import { eq, sql } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/node-postgres'
-import { numeric, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core'
+import { numeric, timestamp, varchar } from 'drizzle-orm/pg-core'
import { Pool } from 'pg'
import prompts from 'prompts'
@@ -61,9 +61,13 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
this.drizzle = drizzle(this.pool, { logger, schema: this.schema })
if (process.env.PAYLOAD_DROP_DATABASE === 'true') {
- this.payload.logger.info('---- DROPPING TABLES ----')
- await this.drizzle.execute(sql`drop schema public cascade;
- create schema public;`)
+ this.payload.logger.info(`---- DROPPING TABLES SCHEMA(${this.schemaName || 'public'}) ----`)
+ await this.drizzle.execute(
+ sql.raw(`
+ drop schema if exists ${this.schemaName || 'public'} cascade;
+ create schema ${this.schemaName || 'public'};
+ `),
+ )
this.payload.logger.info('---- DROPPED TABLES ----')
}
} catch (err) {
@@ -120,7 +124,7 @@ export const connect: Connect = async function connect(this: PostgresAdapter, pa
await apply()
// Migration table def in order to use query using drizzle
- const migrationsSchema = pgTable('payload_migrations', {
+ const migrationsSchema = this.pgSchema.table('payload_migrations', {
name: varchar('name'),
batch: numeric('batch'),
created_at: timestamp('created_at'),
diff --git a/packages/db-postgres/src/index.ts b/packages/db-postgres/src/index.ts
index aebe1e34cb..c605495426 100644
--- a/packages/db-postgres/src/index.ts
+++ b/packages/db-postgres/src/index.ts
@@ -52,11 +52,13 @@ export function postgresAdapter(args: Args): PostgresAdapterResult {
fieldConstraints: {},
idType,
logger: args.logger,
+ pgSchema: undefined,
pool: undefined,
poolOptions: args.pool,
push: args.push,
relations: {},
schema: {},
+ schemaName: args.schemaName,
sessions: {},
tables: {},
diff --git a/packages/db-postgres/src/init.ts b/packages/db-postgres/src/init.ts
index 777dae82bf..8d82fee089 100644
--- a/packages/db-postgres/src/init.ts
+++ b/packages/db-postgres/src/init.ts
@@ -2,7 +2,7 @@
import type { Init } from 'payload/database'
import type { SanitizedCollectionConfig } from 'payload/types'
-import { pgEnum } from 'drizzle-orm/pg-core'
+import { pgEnum, pgSchema, pgTable } from 'drizzle-orm/pg-core'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload/versions'
import toSnakeCase from 'to-snake-case'
@@ -11,6 +11,12 @@ import type { PostgresAdapter } from './types'
import { buildTable } from './schema/build'
export const init: Init = async function init(this: PostgresAdapter) {
+ if (this.schemaName) {
+ this.pgSchema = pgSchema(this.schemaName)
+ } else {
+ this.pgSchema = { table: pgTable }
+ }
+
if (this.payload.config.localization) {
this.enums.enum__locales = pgEnum(
'_locales',
diff --git a/packages/db-postgres/src/migrate.ts b/packages/db-postgres/src/migrate.ts
index aca6c6c1c5..f054bd5cc9 100644
--- a/packages/db-postgres/src/migrate.ts
+++ b/packages/db-postgres/src/migrate.ts
@@ -39,7 +39,7 @@ export async function migrate(this: PostgresAdapter): Promise {
latestBatch = Number(migrationsInDB[0]?.batch)
}
} else {
- await createMigrationTable(this.drizzle)
+ await createMigrationTable(this)
}
if (migrationsInDB.find((m) => m.batch === -1)) {
diff --git a/packages/db-postgres/src/migrateFresh.ts b/packages/db-postgres/src/migrateFresh.ts
index aa76ba0a88..ba9cc9e0b5 100644
--- a/packages/db-postgres/src/migrateFresh.ts
+++ b/packages/db-postgres/src/migrateFresh.ts
@@ -44,8 +44,10 @@ export async function migrateFresh(
msg: `Dropping database.`,
})
- await this.drizzle.execute(sql`drop schema public cascade;
- create schema public;`)
+ await this.drizzle.execute(
+ sql.raw(`drop schema ${this.schemaName || 'public'} cascade;
+ create schema ${this.schemaName || 'public'};`),
+ )
const migrationFiles = await readMigrationFiles({ payload })
payload.logger.debug({
diff --git a/packages/db-postgres/src/queries/buildQuery.ts b/packages/db-postgres/src/queries/buildQuery.ts
index be77658701..1d483335d8 100644
--- a/packages/db-postgres/src/queries/buildQuery.ts
+++ b/packages/db-postgres/src/queries/buildQuery.ts
@@ -1,4 +1,5 @@
import type { SQL } from 'drizzle-orm'
+import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
import type { Field, Where } from 'payload/types'
import { asc, desc } from 'drizzle-orm'
@@ -12,7 +13,7 @@ export type BuildQueryJoins = Record
export type BuildQueryJoinAliases = {
condition: SQL
- table: GenericTable
+ table: GenericTable | PgTableWithColumns
}[]
type BuildQueryArgs = {
diff --git a/packages/db-postgres/src/queries/getTableColumnFromPath.ts b/packages/db-postgres/src/queries/getTableColumnFromPath.ts
index 1b42366685..eb86ac2069 100644
--- a/packages/db-postgres/src/queries/getTableColumnFromPath.ts
+++ b/packages/db-postgres/src/queries/getTableColumnFromPath.ts
@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */
import type { SQL } from 'drizzle-orm'
-import type { Field, FieldAffectingData, NumberField, TabAsField, TextField } from 'payload/types'
+import type { PgTableWithColumns } from 'drizzle-orm/pg-core'
+import type { Field, FieldAffectingData, NumberField, TabAsField, TextField } from 'payload/types'
import { and, eq, like, sql } from 'drizzle-orm'
import { alias } from 'drizzle-orm/pg-core'
@@ -15,7 +16,7 @@ import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery'
type Constraint = {
columnName: string
- table: GenericTable
+ table: GenericTable | PgTableWithColumns
value: unknown
}
@@ -26,12 +27,12 @@ type TableColumn = {
getNotNullColumnByValue?: (val: unknown) => string
pathSegments?: string[]
rawColumn?: SQL
- table: GenericTable
+ table: GenericTable | PgTableWithColumns
}
type Args = {
adapter: PostgresAdapter
- aliasTable?: GenericTable
+ aliasTable?: GenericTable | PgTableWithColumns
collectionPath: string
columnPrefix?: string
constraintPath?: string
diff --git a/packages/db-postgres/src/schema/build.ts b/packages/db-postgres/src/schema/build.ts
index 2905d8684b..218f65d319 100644
--- a/packages/db-postgres/src/schema/build.ts
+++ b/packages/db-postgres/src/schema/build.ts
@@ -1,19 +1,15 @@
/* eslint-disable no-param-reassign */
import type { Relation } from 'drizzle-orm'
-import type { IndexBuilder, PgColumnBuilder, UniqueConstraintBuilder } from 'drizzle-orm/pg-core'
+import type {
+ IndexBuilder,
+ PgColumnBuilder,
+ PgTableWithColumns,
+ UniqueConstraintBuilder,
+} from 'drizzle-orm/pg-core'
import type { Field } from 'payload/types'
import { relations } from 'drizzle-orm'
-import {
- index,
- integer,
- numeric,
- pgTable,
- serial,
- timestamp,
- unique,
- varchar,
-} from 'drizzle-orm/pg-core'
+import { index, integer, numeric, serial, timestamp, unique, varchar } from 'drizzle-orm/pg-core'
import { fieldAffectsData } from 'payload/types'
import toSnakeCase from 'to-snake-case'
@@ -77,14 +73,14 @@ export const buildTable = ({
const localesColumns: Record = {}
const localesIndexes: Record IndexBuilder> = {}
- let localesTable: GenericTable
- let textsTable: GenericTable
- let numbersTable: GenericTable
+ let localesTable: GenericTable | PgTableWithColumns
+ let textsTable: GenericTable | PgTableWithColumns
+ let numbersTable: GenericTable | PgTableWithColumns
// Relationships to the base collection
const relationships: Set = rootRelationships || new Set()
- let relationshipsTable: GenericTable
+ let relationshipsTable: GenericTable | PgTableWithColumns
// Drizzle relations
const relationsToBuild: Map = new Map()
@@ -136,7 +132,7 @@ export const buildTable = ({
.notNull()
}
- const table = pgTable(tableName, columns, (cols) => {
+ const table = adapter.pgSchema.table(tableName, columns, (cols) => {
const extraConfig = Object.entries(baseExtraConfig).reduce((config, [key, func]) => {
config[key] = func(cols)
return config
@@ -158,7 +154,7 @@ export const buildTable = ({
.references(() => table.id, { onDelete: 'cascade' })
.notNull()
- localesTable = pgTable(localeTableName, localesColumns, (cols) => {
+ localesTable = adapter.pgSchema.table(localeTableName, localesColumns, (cols) => {
return Object.entries(localesIndexes).reduce(
(acc, [colName, func]) => {
acc[colName] = func(cols)
@@ -201,7 +197,7 @@ export const buildTable = ({
columns.locale = adapter.enums.enum__locales('locale')
}
- textsTable = pgTable(textsTableName, columns, (cols) => {
+ textsTable = adapter.pgSchema.table(textsTableName, columns, (cols) => {
const indexes: Record = {
orderParentIdx: index(`${textsTableName}_order_parent_idx`).on(cols.order, cols.parent),
}
@@ -245,7 +241,7 @@ export const buildTable = ({
columns.locale = adapter.enums.enum__locales('locale')
}
- numbersTable = pgTable(numbersTableName, columns, (cols) => {
+ numbersTable = adapter.pgSchema.table(numbersTableName, columns, (cols) => {
const indexes: Record = {
orderParentIdx: index(`${numbersTableName}_order_parent_idx`).on(cols.order, cols.parent),
}
@@ -307,19 +303,23 @@ export const buildTable = ({
const relationshipsTableName = `${tableName}_rels`
- relationshipsTable = pgTable(relationshipsTableName, relationshipColumns, (cols) => {
- const result: Record = {
- order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
- parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
- pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
- }
+ relationshipsTable = adapter.pgSchema.table(
+ relationshipsTableName,
+ relationshipColumns,
+ (cols) => {
+ const result: Record = {
+ order: index(`${relationshipsTableName}_order_idx`).on(cols.order),
+ parentIdx: index(`${relationshipsTableName}_parent_idx`).on(cols.parent),
+ pathIdx: index(`${relationshipsTableName}_path_idx`).on(cols.path),
+ }
- if (hasLocalizedRelationshipField) {
- result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale)
- }
+ if (hasLocalizedRelationshipField) {
+ result.localeIdx = index(`${relationshipsTableName}_locale_idx`).on(cols.locale)
+ }
- return result
- })
+ return result
+ },
+ )
adapter.tables[relationshipsTableName] = relationshipsTable
diff --git a/packages/db-postgres/src/types.ts b/packages/db-postgres/src/types.ts
index e23c6e7345..12dad00497 100644
--- a/packages/db-postgres/src/types.ts
+++ b/packages/db-postgres/src/types.ts
@@ -7,7 +7,14 @@ import type {
Relations,
} from 'drizzle-orm'
import type { NodePgDatabase, NodePgQueryResultHKT } from 'drizzle-orm/node-postgres'
-import type { PgColumn, PgEnum, PgTableWithColumns, PgTransaction } from 'drizzle-orm/pg-core'
+import type {
+ PgColumn,
+ PgEnum,
+ PgSchema,
+ PgTableWithColumns,
+ PgTransaction,
+} from 'drizzle-orm/pg-core'
+import type { PgTableFn } from 'drizzle-orm/pg-core/table'
import type { Payload } from 'payload'
import type { BaseDatabaseAdapter } from 'payload/database'
import type { PayloadRequest } from 'payload/types'
@@ -21,6 +28,7 @@ export type Args = {
migrationDir?: string
pool: PoolConfig
push?: boolean
+ schemaName?: string
}
export type GenericColumn = PgColumn<
@@ -59,11 +67,13 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
fieldConstraints: Record>
idType: Args['idType']
logger: DrizzleConfig['logger']
+ pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
push: boolean
relations: Record
schema: Record
+ schemaName?: Args['schemaName']
sessions: {
[id: string]: {
db: DrizzleTransaction
@@ -71,7 +81,7 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
resolve: () => Promise
}
}
- tables: Record
+ tables: Record>
}
export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
diff --git a/packages/db-postgres/src/utilities/createMigrationTable.ts b/packages/db-postgres/src/utilities/createMigrationTable.ts
index a283f02e86..5b056aa8d5 100644
--- a/packages/db-postgres/src/utilities/createMigrationTable.ts
+++ b/packages/db-postgres/src/utilities/createMigrationTable.ts
@@ -1,13 +1,17 @@
import { sql } from 'drizzle-orm'
-import type { DrizzleDB } from '../types'
+import type { PostgresAdapter } from '../types'
-export const createMigrationTable = async (db: DrizzleDB): Promise => {
- await db.execute(sql`CREATE TABLE IF NOT EXISTS "payload_migrations" (
+export const createMigrationTable = async (adapter: PostgresAdapter): Promise => {
+ const prependSchema = adapter.schemaName ? `"${adapter.schemaName}".` : ''
+
+ await adapter.drizzle.execute(
+ sql.raw(`CREATE TABLE IF NOT EXISTS ${prependSchema}"payload_migrations" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar,
"batch" numeric,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
-);`)
+);`),
+ )
}
diff --git a/test/buildConfigWithDefaults.ts b/test/buildConfigWithDefaults.ts
index 02ef05feb2..3a95a689c4 100644
--- a/test/buildConfigWithDefaults.ts
+++ b/test/buildConfigWithDefaults.ts
@@ -35,6 +35,13 @@ const databaseAdapters = {
connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests',
},
}),
+ 'postgres-custom-schema': postgresAdapter({
+ migrationDir,
+ pool: {
+ connectionString: process.env.POSTGRES_URL || 'postgres://127.0.0.1:5432/payloadtests',
+ },
+ schemaName: 'custom',
+ }),
'postgres-uuid': postgresAdapter({
idType: 'uuid',
migrationDir,
diff --git a/test/database/int.spec.ts b/test/database/int.spec.ts
index 1fd6e2fd38..d0bf40435c 100644
--- a/test/database/int.spec.ts
+++ b/test/database/int.spec.ts
@@ -3,7 +3,7 @@ import fs from 'fs'
import { GraphQLClient } from 'graphql-request'
import path from 'path'
-import type { DrizzleDB } from '../../packages/db-postgres/src/types'
+import type { PostgresAdapter } from '../../packages/db-postgres/src/types'
import type { TypeWithID } from '../../packages/payload/src/collections/config/types'
import type { PayloadRequest } from '../../packages/payload/src/express/types'
@@ -44,10 +44,14 @@ describe('database', () => {
describe('migrations', () => {
beforeAll(async () => {
if (process.env.PAYLOAD_DROP_DATABASE === 'true' && 'drizzle' in payload.db) {
- const drizzle = payload.db.drizzle as DrizzleDB
- // @ts-expect-error drizzle raw sql typing
- await drizzle.execute(sql`drop schema public cascade;
- create schema public;`)
+ const db = payload.db as unknown as PostgresAdapter
+ const drizzle = db.drizzle
+ const schemaName = db.schemaName || 'public'
+
+ await drizzle.execute(
+ sql.raw(`drop schema ${schemaName} cascade;
+ create schema ${schemaName};`),
+ )
}
})
diff --git a/test/helpers/reset.ts b/test/helpers/reset.ts
index e0cbf6886f..1cf2864bba 100644
--- a/test/helpers/reset.ts
+++ b/test/helpers/reset.ts
@@ -20,7 +20,7 @@ export async function resetDB(_payload: Payload, collectionSlugs: string[]) {
return
}
const queries = Object.values(schema).map((table: any) => {
- return sql.raw(`DELETE FROM ${table.dbName}`)
+ return sql.raw(`DELETE FROM ${db.schemaName ? db.schemaName + '.' : ''}${table.dbName}`)
})
await db.drizzle.transaction(async (trx) => {