@sqb/migrator
Version:
Database migrator for SQB
230 lines (229 loc) • 8.29 kB
JavaScript
import { PgAdapter } from '@sqb/postgres';
import path from 'path';
import { stringifyValueForSQL } from 'postgrejs';
import { MigrationAdapter } from '../migration-adapter.js';
import { isCustomMigrationTask, isInsertDataMigrationTask, isSqlScriptMigrationTask, } from '../migration-package.js';
import { MigrationStatus } from '../types.js';
const pgAdapter = new PgAdapter();
export class PgMigrationAdapter extends MigrationAdapter {
_infoSchema = 'public';
_version = 0;
_status = MigrationStatus.idle;
defaultVariables = {
tablespace: 'pg_default',
schema: 'public',
owner: 'postgres',
};
summaryTable = 'migration_summary';
eventTable = 'migration_events';
get packageName() {
return this._migrationPackage.name;
}
get version() {
return this._version;
}
get status() {
return this._status;
}
get infoSchema() {
return this._infoSchema;
}
get summaryTableFull() {
return this.infoSchema + '.' + this.summaryTable;
}
get eventTableFull() {
return this.infoSchema + '.' + this.eventTable;
}
static async create(options) {
// Create connection
const connection = (await pgAdapter.connect(options.connection))
.intlcon;
try {
const adapter = new PgMigrationAdapter();
adapter._connection = connection;
adapter._migrationPackage = options.migrationPackage;
adapter._infoSchema = options.infoSchema || '__migration';
adapter.defaultVariables.schema = options.connection.schema || '';
if (!adapter.defaultVariables.schema) {
const r = await connection.query('SELECT CURRENT_SCHEMA ', {
objectRows: true,
});
adapter.defaultVariables.schema =
r.rows?.[0]?.current_schema || 'public';
}
// Check if migration schema
await connection.query(`CREATE SCHEMA IF NOT EXISTS ${adapter.infoSchema} AUTHORIZATION postgres;`);
// Create summary table if not exists
await connection.execute(`
CREATE TABLE IF NOT EXISTS ${adapter.summaryTableFull}
(
package_name varchar not null,
status varchar(16) not null,
current_version integer not null default 0,
created_at timestamp without time zone not null default current_timestamp,
updated_at timestamp without time zone,
CONSTRAINT pk_${adapter.summaryTable} PRIMARY KEY (package_name)
)`);
// Create events table if not exists
await connection.execute(`
CREATE TABLE IF NOT EXISTS ${adapter.eventTableFull}
(
id serial not null,
package_name varchar not null,
version integer not null default 0,
event varchar(16) not null,
event_time timestamp without time zone not null,
title text,
message text not null,
filename text,
details text,
CONSTRAINT pk_${adapter.eventTable} PRIMARY KEY (id)
)`);
// Insert summary record if not exists
const r = await connection.query(`SELECT status FROM ${adapter.summaryTableFull} WHERE package_name = $1`, {
params: [adapter.packageName],
objectRows: true,
});
if (!(r && r.rows?.length)) {
await connection.query(`insert into ${adapter.summaryTableFull} (package_name, status) values ($1, $2)`, {
params: [adapter.packageName, MigrationStatus.idle],
});
}
await adapter.refresh();
return adapter;
}
catch (e) {
await connection.close(0);
throw e;
}
}
async close() {
await this._connection.close();
}
async refresh() {
const r = await this._connection.query(`SELECT * FROM ${this.summaryTableFull} WHERE package_name = $1`, {
params: [this.packageName],
objectRows: true,
});
const row = r.rows && r.rows[0];
if (!row)
throw new Error('Summary record did not created');
this._version = row.current_version;
this._status = row.status;
}
async update(info) {
let sql = '';
const params = [];
if (info.status && info.status !== this.status) {
params.push(info.status);
sql += ',\n status = $' + params.length;
}
if (info.version && info.version !== this.version) {
params.push(info.version);
sql += ',\n current_version = $' + params.length;
}
if (sql) {
params.push(this.packageName);
sql =
`update ${this.summaryTableFull} set updated_at = current_timestamp` +
sql +
`\n where package_name =$` +
params.length;
await this._connection.query(sql, { params });
if (info.status)
this._status = info.status;
if (info.version)
this._version = info.version;
}
}
async writeEvent(event) {
const sql = `insert into ${this.eventTableFull} ` +
'(package_name, version, event, event_time, title, message, filename, details) ' +
'values ($1, $2, $3, CURRENT_TIMESTAMP, $4, $5, $6, $7)';
await this._connection.query(sql, {
params: [
this.packageName,
event.version,
event.event,
event.title,
event.message,
event.filename,
event.details,
],
});
}
async executeTask(migrationPackage, migration, task, variables) {
variables = {
...this.defaultVariables,
...variables,
};
if (isSqlScriptMigrationTask(task)) {
try {
let script;
if (typeof task.script === 'function') {
script = await task.script({
migrationPackage,
migration,
task,
variables,
});
}
else
script = task.script;
if (typeof script !== 'string')
return;
script = this.replaceVariables(script, variables);
await this._connection.execute(script);
}
catch (e) {
let msg = `Error in task "${task.title}"`;
if (task.filename)
msg +=
'\n at ' + path.relative(migrationPackage.baseDir, task.filename);
if (e.lineNr) {
if (!task.filename)
e.message += '\n at';
msg += ` (${e.lineNr},${e.colNr}):\n` + e.line;
if (e.colNr)
msg += '\n' + ' '.repeat(e.colNr - 1) + '^';
}
e.message = msg + '\n\n' + e.message;
throw e;
}
return;
}
if (isCustomMigrationTask(task)) {
await task.fn(this._connection, this);
return;
}
if (isInsertDataMigrationTask(task)) {
const tableName = this.replaceVariables(task.tableName, variables);
const script = task.rows
.map(row => this.rowToSql(tableName, row))
.join('\n');
await this._connection.execute(script);
}
}
backupDatabase() {
return Promise.resolve(undefined);
}
lockSchema() {
return Promise.resolve(undefined);
}
restoreDatabase() {
return Promise.resolve(undefined);
}
unlockSchema() {
return Promise.resolve(undefined);
}
rowToSql(tableName, row) {
let sql = '';
const keys = Object.keys(row);
sql += `insert into ${tableName} (${keys}) values (`;
for (let i = 0; i < keys.length; i++) {
sql += (i ? ', ' : '') + stringifyValueForSQL(row[keys[i]]);
}
sql += ');\n';
return sql;
}
}