Skip to content

Commit

Permalink
feat(db-postgres): configurable custom schema to use (#5047)
Browse files Browse the repository at this point in the history
* feat(db-postgres): configurable custom schema to use

* test(db-postgres): use public schema

* chore(db-postgres): simplify drop schema

* chore: add postgres-custom-schema test to ci

* chore: add custom schema to postgres ci

* chore(db-postgres): custom schema in migrate

* chore: ci postgres wait condition
  • Loading branch information
DanRibbens authored Feb 23, 2024
1 parent ceca5c4 commit e8f2ca4
Show file tree
Hide file tree
Showing 15 changed files with 118 additions and 71 deletions.
13 changes: 9 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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: |
Expand Down
19 changes: 10 additions & 9 deletions docs/database/postgres.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand Down Expand Up @@ -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

Expand All @@ -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.

Expand All @@ -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!

<Banner type="warning">
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.
</Banner>
</Banner>
14 changes: 9 additions & 5 deletions packages/db-postgres/src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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'),
Expand Down
2 changes: 2 additions & 0 deletions packages/db-postgres/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},

Expand Down
8 changes: 7 additions & 1 deletion packages/db-postgres/src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages/db-postgres/src/migrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export async function migrate(this: PostgresAdapter): Promise<void> {
latestBatch = Number(migrationsInDB[0]?.batch)
}
} else {
await createMigrationTable(this.drizzle)
await createMigrationTable(this)
}

if (migrationsInDB.find((m) => m.batch === -1)) {
Expand Down
6 changes: 4 additions & 2 deletions packages/db-postgres/src/migrateFresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
3 changes: 2 additions & 1 deletion packages/db-postgres/src/queries/buildQuery.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -12,7 +13,7 @@ export type BuildQueryJoins = Record<string, SQL>

export type BuildQueryJoinAliases = {
condition: SQL
table: GenericTable
table: GenericTable | PgTableWithColumns<any>
}[]

type BuildQueryArgs = {
Expand Down
9 changes: 5 additions & 4 deletions packages/db-postgres/src/queries/getTableColumnFromPath.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -15,7 +16,7 @@ import type { BuildQueryJoinAliases, BuildQueryJoins } from './buildQuery'

type Constraint = {
columnName: string
table: GenericTable
table: GenericTable | PgTableWithColumns<any>
value: unknown
}

Expand All @@ -26,12 +27,12 @@ type TableColumn = {
getNotNullColumnByValue?: (val: unknown) => string
pathSegments?: string[]
rawColumn?: SQL
table: GenericTable
table: GenericTable | PgTableWithColumns<any>
}

type Args = {
adapter: PostgresAdapter
aliasTable?: GenericTable
aliasTable?: GenericTable | PgTableWithColumns<any>
collectionPath: string
columnPrefix?: string
constraintPath?: string
Expand Down
64 changes: 32 additions & 32 deletions packages/db-postgres/src/schema/build.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -77,14 +73,14 @@ export const buildTable = ({

const localesColumns: Record<string, PgColumnBuilder> = {}
const localesIndexes: Record<string, (cols: GenericColumns) => IndexBuilder> = {}
let localesTable: GenericTable
let textsTable: GenericTable
let numbersTable: GenericTable
let localesTable: GenericTable | PgTableWithColumns<any>
let textsTable: GenericTable | PgTableWithColumns<any>
let numbersTable: GenericTable | PgTableWithColumns<any>

// Relationships to the base collection
const relationships: Set<string> = rootRelationships || new Set()

let relationshipsTable: GenericTable
let relationshipsTable: GenericTable | PgTableWithColumns<any>

// Drizzle relations
const relationsToBuild: Map<string, string> = new Map()
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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<string, IndexBuilder> = {
orderParentIdx: index(`${textsTableName}_order_parent_idx`).on(cols.order, cols.parent),
}
Expand Down Expand Up @@ -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<string, IndexBuilder> = {
orderParentIdx: index(`${numbersTableName}_order_parent_idx`).on(cols.order, cols.parent),
}
Expand Down Expand Up @@ -307,19 +303,23 @@ export const buildTable = ({

const relationshipsTableName = `${tableName}_rels`

relationshipsTable = pgTable(relationshipsTableName, relationshipColumns, (cols) => {
const result: Record<string, unknown> = {
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)
}

return result
})
relationshipsTable = adapter.pgSchema.table(
relationshipsTableName,
relationshipColumns,
(cols) => {
const result: Record<string, unknown> = {
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)
}

return result
},
)

adapter.tables[relationshipsTableName] = relationshipsTable

Expand Down
14 changes: 12 additions & 2 deletions packages/db-postgres/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -21,6 +28,7 @@ export type Args = {
migrationDir?: string
pool: PoolConfig
push?: boolean
schemaName?: string
}

export type GenericColumn = PgColumn<
Expand Down Expand Up @@ -59,19 +67,21 @@ export type PostgresAdapter = BaseDatabaseAdapter & {
fieldConstraints: Record<string, Record<string, string>>
idType: Args['idType']
logger: DrizzleConfig['logger']
pgSchema?: { table: PgTableFn } | PgSchema
pool: Pool
poolOptions: Args['pool']
push: boolean
relations: Record<string, GenericRelation>
schema: Record<string, GenericEnum | GenericRelation | GenericTable>
schemaName?: Args['schemaName']
sessions: {
[id: string]: {
db: DrizzleTransaction
reject: () => Promise<void>
resolve: () => Promise<void>
}
}
tables: Record<string, GenericTable>
tables: Record<string, GenericTable | PgTableWithColumns<any>>
}

export type IDType = 'integer' | 'numeric' | 'uuid' | 'varchar'
Expand Down
Loading

0 comments on commit e8f2ca4

Please sign in to comment.