UNPKG

@adonisjs/lucid

Version:

SQL ORM built on top of Active Record pattern

172 lines (171 loc) 6.48 kB
/* * @adonisjs/lucid * * (c) AdonisJS * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ import { readFile, writeFile } from 'node:fs/promises'; import { BaseSchemaState } from '../base_schema_state.js'; /** * PostgreSQL schema dumps are delegated to `pg_dump`. */ export class PgSchemaState extends BaseSchemaState { constructor(client, connectionConfig, schemaTableName, schemaVersionsTableName) { super(client, connectionConfig, schemaTableName, schemaVersionsTableName); } /** * Convert a Lucid PostgreSQL connection config into the shape expected by * the PostgreSQL CLI tools. */ #normalizeCliConnection() { const connection = this.connectionConfig.connection; if (!connection) { throw new Error('Incomplete PostgreSQL connection config. Cannot create schema dump'); } if (typeof connection === 'string') return this.#parseConnectionString(connection); if (connection.connectionString) return this.#parseConnectionString(connection.connectionString); if (!connection.database || !connection.user) { throw new Error('Incomplete PostgreSQL connection config. Cannot create schema dump'); } return { host: connection.host, port: String(connection.port), user: connection.user, password: connection.password ?? '', database: connection.database, }; } /** * Parse a PostgreSQL connection string into the normalized structure used * by the CLI integration helpers. */ #parseConnectionString(connectionString) { const url = new URL(connectionString); return { host: url.hostname, port: url.port, user: decodeURIComponent(url.username), password: decodeURIComponent(url.password), database: url.pathname.replace(/^\//, ''), }; } /** * Dump the structural schema using `pg_dump`. */ async dumpSchema(path) { const connection = this.#normalizeCliConnection(); const args = [ '--schema-only', /** * Fresh database wipes (db:wipe) do not necessarily remove custom schemas, enums, * sequences, or other non-table objects. Emitting `DROP ... IF EXISTS` * before the structural SQL keeps the dump replayable on that state. */ '--clean', '--if-exists', '--no-owner', '--no-acl', `--host=${connection.host}`, `--port=${connection.port}`, `--username=${connection.user}`, `--dbname=${connection.database}`, ]; await this.spawnToFile('pg_dump', args, path, { env: { ...process.env, /** * `pg_dump` reads passwords from `PGPASSWORD`. This keeps credentials * out of process arguments. */ PGPASSWORD: connection.password, }, }); await this.normalizeDump(path); } /** * Dump Lucid's migration bookkeeping tables with `pg_dump --data-only`. * * PostgreSQL cannot rely on the generic metadata dumper from the base class. * That path appends plain `INSERT` statements, but it does not update the * `serial` sequence backing `adonis_schema.id`. Dumping these tables through * `pg_dump` keeps the restore native to Postgres and preserves the sequence * state needed by future migration inserts. */ async dumpMigrationMetadata() { const connection = this.#normalizeCliConnection(); const statements = []; for (let tableName of [this.schemaTableName, this.schemaVersionsTableName]) { if (!(await this.client.schema.hasTable(tableName))) { continue; } const output = await this.spawnCommand('pg_dump', [ '--data-only', '--no-owner', '--no-acl', /** * Lucid keeps its migration bookkeeping tables in PostgreSQL's * default `public` schema. Qualifying the table name keeps the * metadata dump explicit after the structural dump resets * `search_path`. */ `--table=public.${tableName}`, `--host=${connection.host}`, `--port=${connection.port}`, `--username=${connection.user}`, `--dbname=${connection.database}`, ], { env: { ...process.env, PGPASSWORD: connection.password, }, }); const normalizedOutput = this.removeUnsupportedSessionSettings(output).trim(); if (normalizedOutput) { statements.push(normalizedOutput); } } return statements.join('\n\n'); } /** * Load the schema dump using `psql`. */ async load(path) { const connection = this.#normalizeCliConnection(); /** * PostgreSQL schema dumps contain psql-specific commands emitted by * `pg_dump`. Executing the file directly keeps the restore flow aligned * with how the tool expects to process that output. */ await this.spawnCommand('psql', [ `--file=${path}`, '--set=ON_ERROR_STOP=on', `--host=${connection.host}`, `--port=${connection.port}`, `--username=${connection.user}`, `--dbname=${connection.database}`, ], { env: { ...process.env, PGPASSWORD: connection.password, }, }); } /** * Remove session-level settings that are not understood by older PostgreSQL * server versions. */ removeUnsupportedSessionSettings(output) { return output.replace(/^SET transaction_timeout = 0;\r?\n/gimu, ''); } /** * Normalize the final SQL dump before it is consumed by `psql`. */ async normalizeDump(path) { const output = await readFile(path, 'utf-8'); await writeFile(path, this.removeUnsupportedSessionSettings(output).trim() + '\n', 'utf-8'); } }