1/**
2 * Runs database migrations for PostgreSQL projects that use Effect SQL.
3 *
4 * This module reuses the shared SQL migrator and connects it to PostgreSQL. It
5 * exposes the common migration helpers and adds `run` and `layer` functions
6 * that apply pending migration files with the current SQL client. When schema
7 * dumps are requested, it uses `pg_dump` and the usual process and filesystem
8 * services.
9 *
10 * @since 4.0.0
11 */
12import * as Effect from "effect/Effect"
13import * as FileSystem from "effect/FileSystem"
14import * as Layer from "effect/Layer"
15import * as Path from "effect/Path"
16import * as ChildProcess from "effect/process/ChildProcess"
17import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"
18import * as Migrator from "effect/sql/Migrator"
19import type { SqlClient } from "effect/sql/SqlClient"
20import type { SqlError } from "effect/sql/SqlError"
21import * as Password from "./internal/password.ts"
22import { PgClient } from "./PgClient.ts"
23
24/**
25 * @since 4.0.0
26 */
27export * from "effect/sql/Migrator"
28
29/**
30 * Runs PostgreSQL SQL migrations using the configured clients. Schema dumps use `pg_dump` and require child process, filesystem, and path services.
31 *
32 * @category running
33 * @since 4.0.0
34 */
35export const run: <R2 = never>(
36 options: Migrator.MigratorOptions<R2>
37) => Effect.Effect<
38 ReadonlyArray<readonly [id: number, name: string]>,
39 Migrator.MigrationError | SqlError,
40 | SqlClient
41 | PgClient
42 | ChildProcessSpawner.ChildProcessSpawner
43 | FileSystem.FileSystem
44 | Path.Path
45 | R2
46> = Migrator.make({
47 dumpSchema(path, table) {
48 const pgDump = (args: Array<string>, password: string | undefined) =>
49 Effect.gen(function*() {
50 const sql = yield* PgClient
51 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
52 const dump = yield* ChildProcess.make("pg_dump", [...args, "--no-owner", "--no-privileges"], {
53 env: {
54 PATH: (globalThis as any).process?.env.PATH,
55 PGHOST: sql.config.host,
56 PGPORT: sql.config.port?.toString(),
57 PGUSER: sql.config.username,
58 PGPASSWORD: password,
59 PGDATABASE: sql.config.database,
60 PGSSLMODE: sql.config.ssl ? "require" : "prefer"
61 }
62 }).pipe(spawner.string)
63
64 return dump.replace(/^--.*$/gm, "")
65 .replace(/^SET .*$/gm, "")
66 .replace(/^SELECT pg_catalog\..*$/gm, "")
67 .replace(/\n{2,}/gm, "\n\n")
68 .trim()
69 }).pipe(
70 Effect.mapError((error) =>
71 new Migrator.MigrationError({ kind: "Failed", message: error.message, cause: error })
72 )
73 )
74
75 const pgDumpAll = Effect.gen(function*() {
76 const sql = yield* PgClient
77 const password = yield* Password.resolve(sql.config.password)
78 const [schema, migrations] = yield* Effect.all([
79 pgDump(["--schema-only"], password),
80 pgDump(["--column-inserts", "--data-only", `--table=${table}`], password)
81 ], { concurrency: 2 })
82 return schema + "\n\n" + migrations
83 })
84
85 const pgDumpFile = (path: string) =>
86 Effect.gen(function*() {
87 const fs = yield* FileSystem.FileSystem
88 const path_ = yield* Path.Path
89 const dump = yield* pgDumpAll
90 yield* fs.makeDirectory(path_.dirname(path), { recursive: true })
91 yield* fs.writeFileString(path, dump)
92 }).pipe(
93 Effect.mapError((error) =>
94 new Migrator.MigrationError({ kind: "Failed", message: error.message, cause: error })
95 )
96 )
97
98 return pgDumpFile(path)
99 }
100})
101
102/**
103 * Creates a layer that runs PostgreSQL migrations during layer construction, including `pg_dump`-based schema dump support when requested.
104 *
105 * @category layers
106 * @since 4.0.0
107 */
108export const layer = <R>(
109 options: Migrator.MigratorOptions<R>
110): Layer.Layer<
111 never,
112 Migrator.MigrationError | SqlError,
113 | SqlClient
114 | PgClient
115 | ChildProcessSpawner.ChildProcessSpawner
116 | FileSystem.FileSystem
117 | Path.Path
118 | R
119> => Layer.effectDiscard(run(options))