rake-db
Version:
Migrations tool for Postgresql DB
1,600 lines (1,590 loc) • 188 kB
JavaScript
'use strict';
var orchidCore = require('orchid-core');
var pqb = require('pqb');
var path = require('path');
var node_url = require('node:url');
var fs = require('fs/promises');
require('url');
require('node:path');
const getFirstWordAndRest = (input) => {
const i = input.search(/(?=[A-Z])|[-_ ]/);
if (i !== -1) {
const restStart = input[i] === "-" || input[i] === "_" || input[i] === " " ? i + 1 : i;
const rest = input.slice(restStart);
return [input.slice(0, i), rest[0].toLowerCase() + rest.slice(1)];
} else {
return [input];
}
};
const getTextAfterRegExp = (input, regex, length) => {
let i = input.search(regex);
if (i === -1) return;
if (input[i] === "-" || input[i] === "_" || input[i] === " ") i++;
i += length;
const start = input[i] == "-" || input[i] === "_" || input[i] === " " ? i + 1 : i;
const text = input.slice(start);
return text[0].toLowerCase() + text.slice(1);
};
const getTextAfterTo = (input) => {
return getTextAfterRegExp(input, /(To|-to|_to| to)[A-Z-_ ]/, 2);
};
const getTextAfterFrom = (input) => {
return getTextAfterRegExp(input, /(From|-from|_from| from)[A-Z-_ ]/, 4);
};
const joinColumns = (columns) => {
return columns.map((column) => `"${column}"`).join(", ");
};
const quoteWithSchema = ({
schema,
name
}) => quoteTable(schema, name);
const quoteTable = (schema, table) => schema ? `"${schema}"."${table}"` : `"${table}"`;
const getSchemaAndTableFromName = (name) => {
const i = name.indexOf(".");
return i !== -1 ? [name.slice(0, i), name.slice(i + 1)] : [void 0, name];
};
const quoteNameFromString = (string) => {
return quoteTable(...getSchemaAndTableFromName(string));
};
const quoteCustomType = (s) => {
const [schema, type] = getSchemaAndTableFromName(s);
return schema ? '"' + schema + '".' + type : type;
};
const quoteSchemaTable = (arg) => {
return orchidCore.singleQuote(concatSchemaAndName(arg));
};
const concatSchemaAndName = ({
schema,
name
}) => {
return schema ? `${schema}.${name}` : name;
};
const makePopulateEnumQuery = (item) => {
const [schema, name] = getSchemaAndTableFromName(item.enumName);
return {
text: `SELECT unnest(enum_range(NULL::${quoteTable(schema, name)}))::text`,
then(result) {
item.options.push(...result.rows.map(([value]) => value));
}
};
};
const begin = {
text: "BEGIN"
};
const transaction = (adapter, fn) => {
return adapter.transaction(begin, fn);
};
const queryLock = (trx) => trx.query(`SELECT pg_advisory_xact_lock('${RAKE_DB_LOCK_KEY}')`);
let currentChanges = [];
const clearChanges = () => {
currentChanges = [];
};
const getCurrentChanges = () => currentChanges;
const pushChange = (change) => currentChanges.push(change);
const versionToString = (config, version) => config.migrationId === "timestamp" ? `${version}` : `${version}`.padStart(config.migrationId.serial, "0");
const columnTypeToSql = (item) => {
return item.data.isOfCustomType ? item instanceof pqb.DomainColumn ? quoteNameFromString(item.dataType) : quoteCustomType(item.toSQL()) : item.toSQL();
};
const getColumnName = (item, key, snakeCase) => {
return item.data.name || (snakeCase ? orchidCore.toSnakeCase(key) : key);
};
const columnToSql = (name, item, values, hasMultiplePrimaryKeys, snakeCase) => {
const line = [`"${name}" ${columnTypeToSql(item)}`];
if (item.data.compression) {
line.push(`COMPRESSION ${item.data.compression}`);
}
if (item.data.collate) {
line.push(`COLLATE ${quoteNameFromString(item.data.collate)}`);
}
if (item.data.identity) {
line.push(identityToSql(item.data.identity));
} else if (item.data.generated) {
line.push(
`GENERATED ALWAYS AS (${item.data.generated.toSQL({
values,
snakeCase
})}) STORED`
);
}
if (item.data.primaryKey && !hasMultiplePrimaryKeys) {
if (item.data.primaryKey !== true) {
line.push(`CONSTRAINT "${item.data.primaryKey}"`);
}
line.push("PRIMARY KEY");
} else if (!item.data.isNullable) {
line.push("NOT NULL");
}
if (item.data.checks) {
line.push(
item.data.checks.map(
(check) => (check.name ? `CONSTRAINT "${check.name}" ` : "") + checkToSql(check.sql, values)
).join(", ")
);
}
const def = encodeColumnDefault(item.data.default, values, item);
if (def !== null) line.push(`DEFAULT ${def}`);
const { foreignKeys } = item.data;
if (foreignKeys) {
for (const foreignKey of foreignKeys) {
if (foreignKey.options?.name) {
line.push(`CONSTRAINT "${foreignKey.options?.name}"`);
}
line.push(
referencesToSql(
{
columns: [name],
...foreignKey
},
snakeCase
)
);
}
}
return line.join(" ");
};
const encodeColumnDefault = (def, values, column) => {
if (def !== void 0 && def !== null && typeof def !== "function") {
if (orchidCore.isRawSQL(def)) {
return def.toSQL({ values });
} else {
return pqb.escapeForMigration(
column instanceof pqb.ArrayColumn && Array.isArray(def) ? "{" + (column.data.item.data.encode ? def.map((x) => column.data.item.data.encode(x)) : def).join(",") + "}" : column?.data.encode ? column.data.encode(def) : def
);
}
}
return null;
};
const identityToSql = (identity) => {
const options = sequenceOptionsToSql(identity);
return `GENERATED ${identity.always ? "ALWAYS" : "BY DEFAULT"} AS IDENTITY${options ? ` (${options})` : ""}`;
};
const sequenceOptionsToSql = (item) => {
const line = [];
if (item.dataType) line.push(`AS ${item.dataType}`);
if (item.increment !== void 0) line.push(`INCREMENT BY ${item.increment}`);
if (item.min !== void 0) line.push(`MINVALUE ${item.min}`);
if (item.max !== void 0) line.push(`MAXVALUE ${item.max}`);
if (item.start !== void 0) line.push(`START WITH ${item.start}`);
if (item.cache !== void 0) line.push(`CACHE ${item.cache}`);
if (item.cycle) line.push(`CYCLE`);
if (item.ownedBy) {
const [schema, table] = getSchemaAndTableFromName(item.ownedBy);
line.push(`OWNED BY ${quoteTable(schema, table)}`);
}
return line.join(" ");
};
const addColumnIndex = (indexes, name, item) => {
if (item.data.indexes) {
indexes.push(
...item.data.indexes.map((index) => ({
columns: [{ ...index.options, column: name }],
...index
}))
);
}
};
const addColumnExclude = (excludes, name, item) => {
if (item.data.excludes) {
excludes.push(
...item.data.excludes.map(({ with: w, ...exclude }) => ({
columns: [{ ...exclude.options, column: name, with: w }],
...exclude
}))
);
}
};
const addColumnComment = (comments, name, item) => {
if (item.data.comment) {
comments.push({ column: name, comment: item.data.comment });
}
};
const getForeignKeyTable = (fnOrTable) => {
if (typeof fnOrTable === "string") {
return getSchemaAndTableFromName(fnOrTable);
}
const item = new (fnOrTable())();
return [item.schema, item.table];
};
const getConstraintName = (table, constraint, snakeCase) => {
if (constraint.references) {
let { columns } = constraint.references;
if (snakeCase) {
columns = columns.map(orchidCore.toSnakeCase);
}
return makeConstraintName(table, columns, "fkey");
}
if (constraint.check) return `${table}_check`;
if (constraint.identity) return `${table}_identity`;
return `${table}_constraint`;
};
const constraintToSql = ({ name }, up, constraint, values, snakeCase) => {
const constraintName = constraint.name || getConstraintName(name, constraint, snakeCase);
if (!up) {
const { dropMode } = constraint;
return `CONSTRAINT "${constraintName}"${dropMode ? ` ${dropMode}` : ""}`;
}
const sql = [`CONSTRAINT "${constraintName}"`];
if (constraint.references) {
sql.push(foreignKeyToSql(constraint.references, snakeCase));
}
if (constraint.check) {
sql.push(checkToSql(constraint.check, values));
}
return sql.join(" ");
};
const checkToSql = (check, values) => {
return `CHECK (${check.toSQL({ values })})`;
};
const foreignKeyToSql = (item, snakeCase) => {
return `FOREIGN KEY (${joinColumns(
snakeCase ? item.columns.map(orchidCore.toSnakeCase) : item.columns
)}) ${referencesToSql(item, snakeCase)}`;
};
const referencesToSql = (references, snakeCase) => {
const [schema, table] = getForeignKeyTable(references.fnOrTable);
const sql = [
`REFERENCES ${quoteTable(schema, table)}(${joinColumns(
snakeCase ? references.foreignColumns.map(orchidCore.toSnakeCase) : references.foreignColumns
)})`
];
const { options } = references;
if (options?.match) {
sql.push(`MATCH ${options?.match.toUpperCase()}`);
}
if (options?.onDelete) {
sql.push(`ON DELETE ${options?.onDelete.toUpperCase()}`);
}
if (options?.onUpdate) {
sql.push(`ON UPDATE ${options?.onUpdate.toUpperCase()}`);
}
return sql.join(" ");
};
const MAX_CONSTRAINT_NAME_LEN = 63;
const makeConstraintName = (table, columns, suffix) => {
const long = `${table}_${columns.join("_")}_${suffix}`;
if (long.length <= MAX_CONSTRAINT_NAME_LEN) return long;
for (let partLen = 3; partLen > 0; partLen--) {
const shorter = `${orchidCore.toCamelCase(
orchidCore.toSnakeCase(table).split("_").map((p) => p.slice(0, partLen)).join("_")
)}_${columns.map(
(c) => orchidCore.toCamelCase(
c.split("_").map((p) => p.slice(0, partLen)).join("_")
)
).join("_")}_${suffix}`;
if (shorter.length <= MAX_CONSTRAINT_NAME_LEN) return shorter;
}
const short = `${table}_${columns.length}columns_${suffix}`;
if (short.length <= MAX_CONSTRAINT_NAME_LEN) return short;
for (let partLen = 3; partLen > 0; partLen--) {
const short2 = `${orchidCore.toCamelCase(
orchidCore.toSnakeCase(table).split("_").map((p) => p.slice(0, partLen)).join("_")
)}_${columns.length}columns_${suffix}`;
if (short2.length <= MAX_CONSTRAINT_NAME_LEN) return short2;
}
return `long_ass_table_${suffix}`;
};
const getIndexOrExcludeName = (table, columns, suffix) => makeConstraintName(
table,
columns.map(
(it) => "column" in it ? it.column : "expression"
),
suffix
);
const getIndexName = (table, columns) => getIndexOrExcludeName(table, columns, "idx");
const getExcludeName = (table, columns) => getIndexOrExcludeName(table, columns, "exclude");
const indexesToQuery = (up, { schema, name: tableName }, indexes, snakeCase, language) => {
return indexes.map((index) => {
const { options } = index;
const { columns, include, name } = getIndexOrExcludeMainOptions(
tableName,
index,
getIndexName,
snakeCase
);
if (!up) {
return {
text: `DROP INDEX "${name}"${options.dropMode ? ` ${options.dropMode}` : ""}`
};
}
const values = [];
const sql = ["CREATE"];
if (options.unique) {
sql.push("UNIQUE");
}
sql.push(`INDEX "${name}" ON ${quoteTable(schema, tableName)}`);
const u = options.using || options.tsVector && "GIN";
if (u) {
sql.push(`USING ${u}`);
}
const lang = options.tsVector && options.languageColumn ? `"${options.languageColumn}"` : options.language ? `'${options.language}'` : `'${language || "english"}'`;
let hasWeight = options.tsVector && columns.some((column) => !!column.weight);
const columnsSql = columns.map((column) => {
let sql2 = [
"expression" in column ? `(${column.expression})` : `"${column.column}"`,
column.collate && `COLLATE ${quoteNameFromString(column.collate)}`,
column.opclass,
column.order
].filter((x) => !!x).join(" ");
if (hasWeight) {
sql2 = `to_tsvector(${lang}, coalesce(${sql2}, ''))`;
if (column.weight) {
hasWeight = true;
sql2 = `setweight(${sql2}, '${column.weight}')`;
}
}
return sql2;
});
let columnList;
if (hasWeight) {
columnList = `(${columnsSql.join(" || ")})`;
} else if (options.tsVector) {
columnList = `to_tsvector(${lang}, ${columnsSql.join(" || ' ' || ")})`;
} else {
columnList = columnsSql.join(", ");
}
sql.push(`(${columnList})`);
if (include && include.length) {
sql.push(
`INCLUDE (${include.map((column) => `"${column}"`).join(", ")})`
);
}
if (options.nullsNotDistinct) {
sql.push(`NULLS NOT DISTINCT`);
}
if (options.with) {
sql.push(`WITH (${options.with})`);
}
if (options.tablespace) {
sql.push(`TABLESPACE ${options.tablespace}`);
}
if (options.where) {
sql.push(
`WHERE ${orchidCore.isRawSQL(options.where) ? options.where.toSQL({ values }) : options.where}`
);
}
return { text: sql.join(" "), values };
});
};
const excludesToQuery = (up, { schema, name: tableName }, excludes, snakeCase) => {
return excludes.map((exclude) => {
const { options } = exclude;
const { columns, include, name } = getIndexOrExcludeMainOptions(
tableName,
exclude,
getExcludeName,
snakeCase
);
if (!up) {
return {
text: `ALTER TABLE ${quoteTable(
schema,
tableName
)} DROP CONSTRAINT "${name}"${options.dropMode ? ` ${options.dropMode}` : ""}`
};
}
const columnList = columns.map(
(column) => [
"expression" in column ? `(${column.expression})` : `"${column.column}"`,
column.collate && `COLLATE ${quoteNameFromString(column.collate)}`,
column.opclass,
column.order,
`WITH ${column.with}`
].filter((x) => !!x).join(" ")
).join(", ");
const values = [];
const text = [
`ALTER TABLE ${quoteTable(
schema,
tableName
)} ADD CONSTRAINT "${name}" EXCLUDE`,
options.using && `USING ${options.using}`,
`(${columnList})`,
include?.length && `INCLUDE (${include.map((column) => `"${column}"`).join(", ")})`,
options.with && `WITH (${options.with})`,
options.tablespace && `USING INDEX TABLESPACE ${options.tablespace}`,
options.where && `WHERE ${orchidCore.isRawSQL(options.where) ? options.where.toSQL({ values }) : options.where}`
].filter((x) => !!x).join(" ");
return { text, values };
});
};
const getIndexOrExcludeMainOptions = (tableName, item, getName, snakeCase) => {
let include = item.options.include ? orchidCore.toArray(item.options.include) : void 0;
let { columns } = item;
if (snakeCase) {
columns = columns.map(
(c) => "column" in c ? { ...c, column: orchidCore.toSnakeCase(c.column) } : c
);
if (include) include = include.map(orchidCore.toSnakeCase);
}
return {
columns,
include,
name: item.name || getName(tableName, columns)
};
};
const commentsToQuery = (schemaTable, comments) => {
return comments.map(({ column, comment }) => ({
text: `COMMENT ON COLUMN ${quoteWithSchema(
schemaTable
)}."${column}" IS ${pqb.escapeForMigration(comment)}`,
values: []
}));
};
const primaryKeyToSql = (primaryKey) => {
return `${primaryKey.name ? `CONSTRAINT "${primaryKey.name}" ` : ""}PRIMARY KEY (${joinColumns(primaryKey.columns)})`;
};
const interpolateSqlValues = ({ text, values }) => {
return values?.length ? text.replace(/\$(\d+)/g, (_, n) => {
const i = +n - 1;
return pqb.escapeForMigration(values[i]);
}) : text;
};
const nameColumnChecks = (table, column, checks) => checks.map((check, i) => ({
...check,
name: check.name || `${table}_${column}_check${i === 0 ? "" : i}`
}));
const cmpRawSql = (a, b) => {
const values = [];
const aSql = a.makeSQL({ values });
const aValues = JSON.stringify(values);
values.length = 0;
const bSql = b.makeSQL({ values });
const bValues = JSON.stringify(values);
return aSql === bSql && aValues === bValues;
};
const tableMethods = {
enum(name) {
return new pqb.EnumColumn(
pqb.defaultSchemaConfig,
name,
[],
void 0
);
}
};
class RakeDbError extends Error {
}
class NoPrimaryKey extends RakeDbError {
}
const createTable = async (migration, up, tableName, first, second, third) => {
let options;
let fn;
let dataFn;
if (typeof first === "object") {
options = first;
fn = second;
dataFn = third;
} else {
options = orchidCore.emptyObject;
fn = first;
dataFn = second;
}
const snakeCase = "snakeCase" in options ? options.snakeCase : migration.options.snakeCase;
const language = "language" in options ? options.language : migration.options.language;
const types = Object.assign(
Object.create(migration.columnTypes),
tableMethods
);
types[orchidCore.snakeCaseKey] = snakeCase;
let shape;
let tableData;
if (fn) {
shape = pqb.getColumnTypes(
types,
fn,
migration.options.baseTable?.nowSQL,
language
);
tableData = pqb.parseTableData(dataFn);
tableData.constraints?.forEach((x, i) => {
if (x.name || !x.check) return;
x.name = `${tableName}_check${i === 0 ? "" : i}`;
});
} else {
shape = tableData = orchidCore.emptyObject;
}
const ast = makeAst$2(
up,
tableName,
shape,
tableData,
options,
migration.options.noPrimaryKey
);
fn && validatePrimaryKey(ast);
const queries = astToQueries$1(ast, snakeCase, language);
for (const { then, ...query } of queries) {
const result = await migration.adapter.arrays(interpolateSqlValues(query));
then?.(result);
}
let table;
return {
get table() {
return table ?? (table = migration(
tableName,
shape,
void 0,
{
noPrimaryKey: options.noPrimaryKey ? "ignore" : void 0,
snakeCase
}
));
}
};
};
const makeAst$2 = (up, tableName, shape, tableData, options, noPrimaryKey) => {
const shapePKeys = [];
for (const key in shape) {
const column = shape[key];
if (column.data.primaryKey) {
shapePKeys.push(key);
}
}
const { primaryKey } = tableData;
const [schema, table] = getSchemaAndTableFromName(tableName);
return {
type: "table",
action: up ? "create" : "drop",
schema,
name: table,
shape,
...tableData,
primaryKey: shapePKeys.length <= 1 ? primaryKey : primaryKey ? {
...primaryKey,
columns: [.../* @__PURE__ */ new Set([...shapePKeys, ...primaryKey.columns])]
} : { columns: shapePKeys },
...options,
noPrimaryKey: options.noPrimaryKey ? "ignore" : noPrimaryKey || "error"
};
};
const validatePrimaryKey = (ast) => {
if (ast.noPrimaryKey !== "ignore") {
let hasPrimaryKey = !!ast.primaryKey?.columns?.length;
if (!hasPrimaryKey) {
for (const key in ast.shape) {
if (ast.shape[key].data.primaryKey) {
hasPrimaryKey = true;
break;
}
}
}
if (!hasPrimaryKey) {
const error = new NoPrimaryKey(
`Table ${ast.name} has no primary key.
You can suppress this error by setting { noPrimaryKey: true } after a table name.`
);
if (ast.noPrimaryKey === "error") {
throw error;
} else {
console.warn(error.message);
}
}
}
};
const astToQueries$1 = (ast, snakeCase, language) => {
const queries = [];
const { shape } = ast;
for (const key in shape) {
const item = shape[key];
if (!(item instanceof pqb.EnumColumn)) continue;
queries.push(makePopulateEnumQuery(item));
}
if (ast.action === "drop") {
queries.push({
text: `DROP TABLE${ast.dropIfExists ? " IF EXISTS" : ""} ${quoteWithSchema(ast)}${ast.dropMode ? ` ${ast.dropMode}` : ""}`
});
return queries;
}
const lines = [];
const values = [];
const indexes = [];
const excludes = [];
const comments = [];
for (const key in shape) {
const item = shape[key];
const name = getColumnName(item, key, snakeCase);
addColumnIndex(indexes, name, item);
addColumnExclude(excludes, name, item);
addColumnComment(comments, name, item);
lines.push(
`
${columnToSql(name, item, values, !!ast.primaryKey, snakeCase)}`
);
}
if (ast.primaryKey) {
lines.push(
`
${primaryKeyToSql({
name: ast.primaryKey.name,
columns: ast.primaryKey.columns.map(
(key) => getColumnName(shape[key], key, snakeCase)
)
})}`
);
}
ast.constraints?.forEach((item) => {
lines.push(
`
${constraintToSql(
ast,
true,
{
...item,
references: item.references ? {
...item.references,
columns: item.references.columns.map(
(column) => getColumnName(shape[column], column, snakeCase)
)
} : void 0
},
values,
snakeCase
)}`
);
});
pushIndexesOrExcludesFromAst(indexes, ast.indexes, shape, snakeCase);
pushIndexesOrExcludesFromAst(excludes, ast.excludes, shape, snakeCase);
queries.push(
{
text: `CREATE TABLE${ast.createIfNotExists ? " IF NOT EXISTS" : ""} ${quoteWithSchema(ast)} (${lines.join(",")}
)`,
values
},
...indexesToQuery(true, ast, indexes, snakeCase, language),
...excludesToQuery(true, ast, excludes, snakeCase),
...commentsToQuery(ast, comments)
);
if (ast.comment) {
queries.push({
text: `COMMENT ON TABLE ${quoteWithSchema(ast)} IS ${pqb.escapeString(
ast.comment
)}`
});
}
return queries;
};
const pushIndexesOrExcludesFromAst = (arr, inAst, shape, snakeCase) => {
arr.push(
...inAst?.map((x) => ({
...x,
columns: x.columns.map((item) => ({
...item,
..."column" in item ? {
column: getColumnName(shape[item.column], item.column, snakeCase)
} : {}
}))
})) || []
);
};
const newChangeTableData = () => ({
add: {},
drop: {}
});
let changeTableData = newChangeTableData();
const resetChangeTableData = () => {
changeTableData = newChangeTableData();
};
const addOrDropChanges = [];
function add(item, options) {
orchidCore.consumeColumnName();
setName(this, item);
if (item instanceof pqb.ColumnType) {
const result = addOrDrop("add", item, options);
if (result.type === "change") return result;
addOrDropChanges.push(result);
return addOrDropChanges.length - 1;
}
for (const key in item) {
if (item[key] instanceof orchidCore.ColumnTypeBase) {
const result = {};
for (const key2 in item) {
result[key2] = {
type: "add",
item: item[key2],
dropMode: options?.dropMode
};
}
return result;
}
pqb.parseTableDataInput(changeTableData.add, item);
break;
}
return void 0;
}
const drop = function(item, options) {
orchidCore.consumeColumnName();
setName(this, item);
if (item instanceof pqb.ColumnType) {
const result = addOrDrop("drop", item, options);
if (result.type === "change") return result;
addOrDropChanges.push(result);
return addOrDropChanges.length - 1;
}
for (const key in item) {
if (item[key] instanceof orchidCore.ColumnTypeBase) {
const result = {};
for (const key2 in item) {
result[key2] = {
type: "drop",
item: item[key2],
dropMode: options?.dropMode
};
}
return result;
}
pqb.parseTableDataInput(changeTableData.drop, item);
break;
}
return void 0;
};
const addOrDrop = (type, item, options) => {
if (item instanceof pqb.UnknownColumn) {
const empty = columnTypeToColumnChange({
type: "change",
to: {}
});
const add2 = columnTypeToColumnChange({
type: "change",
to: {
checks: item.data.checks
}
});
return {
type: "change",
from: type === "add" ? empty : add2,
to: type === "add" ? add2 : empty,
...options
};
}
return {
type,
item,
dropMode: options?.dropMode
};
};
const columnTypeToColumnChange = (item, name) => {
if (item instanceof pqb.ColumnType) {
let column = item;
const foreignKeys = column.data.foreignKeys;
if (foreignKeys?.some((it) => "fn" in it)) {
throw new Error("Callback in foreignKey is not allowed in migration");
}
return {
column,
type: column.toSQL(),
nullable: column.data.isNullable,
...column.data,
primaryKey: column.data.primaryKey === void 0 ? void 0 : true,
foreignKeys
};
}
return item.to;
};
const nameKey = Symbol("name");
const setName = (self, item) => {
var _a, _b;
const name = self[nameKey];
if (!name) return;
if ("column" in item && item.column instanceof pqb.ColumnType) {
(_a = item.column.data).name ?? (_a.name = name);
} else if (item instanceof pqb.ColumnType) {
(_b = item.data).name ?? (_b.name = name);
} else {
item.name ?? (item.name = name);
}
};
const tableChangeMethods = {
...tableMethods,
...pqb.tableDataMethods,
name(name) {
orchidCore.setCurrentColumnName(name);
const types = Object.create(this);
types[nameKey] = name;
return types;
},
add,
drop,
change(from, to, using) {
orchidCore.consumeColumnName();
const f = columnTypeToColumnChange(from);
const t = columnTypeToColumnChange(to);
setName(this, f);
setName(this, t);
return {
type: "change",
// eslint-disable-next-line @typescript-eslint/no-explicit-any
name: this[nameKey],
from: f,
to: t,
using
};
},
default(value) {
return { type: "change", to: { default: value } };
},
nullable() {
return {
type: "change",
to: { nullable: true }
};
},
nonNullable() {
return {
type: "change",
to: { nullable: false }
};
},
comment(comment) {
return { type: "change", to: { comment } };
},
/**
* Rename a column:
*
* ```ts
* import { change } from '../dbScript';
*
* change(async (db) => {
* await db.changeTable('table', (t) => ({
* oldColumnName: t.rename('newColumnName'),
* }));
* });
* ```
*
* Note that the renaming `ALTER TABLE` is executed before the rest of alterations,
* so if you're also adding a new constraint on this column inside the same `changeTable`,
* refer to it with a new name.
*
* @param name
*/
rename(name) {
return { type: "rename", name };
}
};
const changeTable = async (migration, up, tableName, options, fn) => {
const snakeCase = "snakeCase" in options ? options.snakeCase : migration.options.snakeCase;
const language = "language" in options ? options.language : migration.options.language;
orchidCore.setDefaultLanguage(language);
resetChangeTableData();
const tableChanger = Object.create(
migration.columnTypes
);
Object.assign(tableChanger, tableChangeMethods);
tableChanger[orchidCore.snakeCaseKey] = snakeCase;
addOrDropChanges.length = 0;
const changeData = fn?.(tableChanger) || {};
const ast = makeAst$1(up, tableName, changeData, changeTableData, options);
const queries = astToQueries(ast, snakeCase, language);
for (const query of queries) {
const result = await migration.adapter.arrays(interpolateSqlValues(query));
query.then?.(result);
}
};
const makeAst$1 = (up, name, changeData, changeTableData2, options) => {
const { comment } = options;
const shape = {};
const consumedChanges = {};
for (const key in changeData) {
let item = changeData[key];
if (typeof item === "number") {
consumedChanges[item] = true;
item = addOrDropChanges[item];
} else if (item instanceof pqb.ColumnType) {
item = addOrDrop("add", item);
}
if ("type" in item) {
if (up) {
shape[key] = item;
} else {
if (item.type === "rename") {
shape[item.name] = { ...item, name: key };
} else {
shape[key] = item.type === "add" ? { ...item, type: "drop" } : item.type === "drop" ? { ...item, type: "add" } : item.type === "change" ? {
...item,
from: item.to,
to: item.from,
using: item.using && {
usingUp: item.using.usingDown,
usingDown: item.using.usingUp
}
} : item;
}
}
}
}
for (let i = 0; i < addOrDropChanges.length; i++) {
if (consumedChanges[i]) continue;
const change = addOrDropChanges[i];
const name2 = change.item.data.name;
if (!name2) {
throw new Error(`Column in ...t.${change.type}() must have a name`);
}
const arr = shape[name2] ? orchidCore.toArray(shape[name2]) : [];
arr[up ? "push" : "unshift"](
up ? change : { ...change, type: change.type === "add" ? "drop" : "add" }
);
shape[name2] = arr;
}
const [schema, table] = getSchemaAndTableFromName(name);
return {
type: "changeTable",
schema,
name: table,
comment: comment ? up ? Array.isArray(comment) ? comment[1] : comment : Array.isArray(comment) ? comment[0] : null : void 0,
shape,
...up ? changeTableData2 : { add: changeTableData2.drop, drop: changeTableData2.add }
};
};
const astToQueries = (ast, snakeCase, language) => {
const queries = [];
if (ast.comment !== void 0) {
queries.push({
text: `COMMENT ON TABLE ${quoteWithSchema(ast)} IS ${ast.comment === null ? "NULL" : pqb.escapeString(
typeof ast.comment === "string" ? ast.comment : ast.comment[1]
)}`
});
}
const addPrimaryKeys = {
columns: []
};
const dropPrimaryKeys = {
columns: []
};
for (const key in ast.shape) {
const item = ast.shape[key];
if (Array.isArray(item)) {
for (const it of item) {
handlePrerequisitesForTableItem(
key,
it,
queries,
addPrimaryKeys,
dropPrimaryKeys,
snakeCase
);
}
} else {
handlePrerequisitesForTableItem(
key,
item,
queries,
addPrimaryKeys,
dropPrimaryKeys,
snakeCase
);
}
}
if (ast.add.primaryKey) {
addPrimaryKeys.name = ast.add.primaryKey.name;
const { columns } = ast.add.primaryKey;
addPrimaryKeys.columns.push(
...snakeCase ? columns.map(orchidCore.toSnakeCase) : columns
);
}
if (ast.drop.primaryKey) {
dropPrimaryKeys.name = ast.drop.primaryKey.name;
const { columns } = ast.drop.primaryKey;
dropPrimaryKeys.columns.push(
...snakeCase ? columns.map(orchidCore.toSnakeCase) : columns
);
}
const alterTable = [];
const renameItems = [];
const values = [];
const addIndexes = ast.add.indexes ?? [];
const dropIndexes = ast.drop.indexes ?? [];
const addExcludes = ast.add.excludes ?? [];
const dropExcludes = ast.drop.excludes ?? [];
const addConstraints = ast.add.constraints ?? [];
const dropConstraints = ast.drop.constraints ?? [];
const comments = [];
for (const key in ast.shape) {
const item = ast.shape[key];
if (Array.isArray(item)) {
for (const it of item) {
handleTableItemChange(
key,
it,
ast,
alterTable,
renameItems,
values,
addPrimaryKeys,
addIndexes,
dropIndexes,
addExcludes,
dropExcludes,
addConstraints,
dropConstraints,
comments,
snakeCase
);
}
} else {
handleTableItemChange(
key,
item,
ast,
alterTable,
renameItems,
values,
addPrimaryKeys,
addIndexes,
dropIndexes,
addExcludes,
dropExcludes,
addConstraints,
dropConstraints,
comments,
snakeCase
);
}
}
const prependAlterTable = [];
if (ast.drop.primaryKey || dropPrimaryKeys.change || dropPrimaryKeys.columns.length > 1) {
const name = dropPrimaryKeys.name || `${ast.name}_pkey`;
prependAlterTable.push(`DROP CONSTRAINT "${name}"`);
}
prependAlterTable.push(
...dropConstraints.map(
(foreignKey) => `
DROP ${constraintToSql(ast, false, foreignKey, values, snakeCase)}`
)
);
alterTable.unshift(...prependAlterTable);
if (ast.add.primaryKey || addPrimaryKeys.change || addPrimaryKeys.columns.length > 1) {
addPrimaryKeys.columns = [...new Set(addPrimaryKeys.columns)];
alterTable.push(
`ADD ${primaryKeyToSql(
snakeCase ? {
name: addPrimaryKeys.name,
columns: addPrimaryKeys.columns.map(orchidCore.toSnakeCase)
} : addPrimaryKeys
)}`
);
}
alterTable.push(
...addConstraints.map(
(foreignKey) => `
ADD ${constraintToSql(ast, true, foreignKey, values, snakeCase)}`
)
);
const tableName = quoteWithSchema(ast);
if (renameItems.length) {
queries.push(
...renameItems.map((sql) => ({
text: `ALTER TABLE ${tableName}
${sql}`,
values
}))
);
}
if (alterTable.length) {
queries.push(alterTableSql(tableName, alterTable, values));
}
queries.push(...indexesToQuery(false, ast, dropIndexes, snakeCase, language));
queries.push(...indexesToQuery(true, ast, addIndexes, snakeCase, language));
queries.push(...excludesToQuery(false, ast, dropExcludes, snakeCase));
queries.push(...excludesToQuery(true, ast, addExcludes, snakeCase));
queries.push(...commentsToQuery(ast, comments));
return queries;
};
const alterTableSql = (tableName, lines, values) => ({
text: `ALTER TABLE ${tableName}
${lines.join(",\n ")}`,
values
});
const handlePrerequisitesForTableItem = (key, item, queries, addPrimaryKeys, dropPrimaryKeys, snakeCase) => {
if ("item" in item) {
const { item: column } = item;
if (column instanceof pqb.EnumColumn) {
queries.push(makePopulateEnumQuery(column));
}
}
if (item.type === "add") {
if (item.item.data.primaryKey) {
addPrimaryKeys.columns.push(getColumnName(item.item, key, snakeCase));
}
} else if (item.type === "drop") {
if (item.item.data.primaryKey) {
dropPrimaryKeys.columns.push(getColumnName(item.item, key, snakeCase));
}
} else if (item.type === "change") {
if (item.from.column instanceof pqb.EnumColumn) {
queries.push(makePopulateEnumQuery(item.from.column));
}
if (item.to.column instanceof pqb.EnumColumn) {
queries.push(makePopulateEnumQuery(item.to.column));
}
if (item.from.primaryKey) {
dropPrimaryKeys.columns.push(
item.from.column ? getColumnName(item.from.column, key, snakeCase) : snakeCase ? orchidCore.toSnakeCase(key) : key
);
dropPrimaryKeys.change = true;
}
if (item.to.primaryKey) {
addPrimaryKeys.columns.push(
item.to.column ? getColumnName(item.to.column, key, snakeCase) : snakeCase ? orchidCore.toSnakeCase(key) : key
);
addPrimaryKeys.change = true;
}
}
};
const handleTableItemChange = (key, item, ast, alterTable, renameItems, values, addPrimaryKeys, addIndexes, dropIndexes, addExcludes, dropExcludes, addConstraints, dropConstraints, comments, snakeCase) => {
if (item.type === "add") {
const column = item.item;
const name = getColumnName(column, key, snakeCase);
addColumnIndex(addIndexes, name, column);
addColumnExclude(addExcludes, name, column);
addColumnComment(comments, name, column);
alterTable.push(
`ADD COLUMN ${columnToSql(
name,
column,
values,
addPrimaryKeys.columns.length > 1,
snakeCase
)}`
);
} else if (item.type === "drop") {
const name = getColumnName(item.item, key, snakeCase);
alterTable.push(
`DROP COLUMN "${name}"${item.dropMode ? ` ${item.dropMode}` : ""}`
);
} else if (item.type === "change") {
const { from, to } = item;
const name = getChangeColumnName("to", item, key, snakeCase);
const fromName = getChangeColumnName("from", item, key, snakeCase);
if (fromName !== name) {
renameItems.push(renameColumnSql(fromName, name));
}
let changeType = false;
if (to.type && (from.type !== to.type || from.collate !== to.collate)) {
changeType = true;
const type = !to.column || to.column.data.isOfCustomType ? to.column && to.column instanceof pqb.DomainColumn ? quoteNameFromString(to.type) : quoteCustomType(to.type) : to.type;
const using = item.using?.usingUp ? ` USING ${item.using.usingUp.toSQL({ values })}` : to.column instanceof pqb.EnumColumn ? ` USING "${name}"::text::${type}` : to.column instanceof pqb.ArrayColumn ? ` USING "${name}"::text[]::${type}` : "";
alterTable.push(
`ALTER COLUMN "${name}" TYPE ${type}${to.collate ? ` COLLATE ${quoteNameFromString(to.collate)}` : ""}${using}`
);
}
if (typeof from.identity !== typeof to.identity || !orchidCore.deepCompare(from.identity, to.identity)) {
if (from.identity) {
alterTable.push(`ALTER COLUMN "${name}" DROP IDENTITY`);
}
if (to.identity) {
alterTable.push(
`ALTER COLUMN "${name}" ADD ${identityToSql(to.identity)}`
);
}
}
if (from.default !== to.default) {
const value = encodeColumnDefault(to.default, values, to.column);
if (changeType && value !== null) {
alterTable.push(`ALTER COLUMN "${name}" DROP DEFAULT`);
}
const expr = value === null ? "DROP DEFAULT" : `SET DEFAULT ${value}`;
alterTable.push(`ALTER COLUMN "${name}" ${expr}`);
}
if (from.nullable !== to.nullable) {
alterTable.push(
`ALTER COLUMN "${name}" ${to.nullable ? "DROP" : "SET"} NOT NULL`
);
}
if (from.compression !== to.compression) {
alterTable.push(
`ALTER COLUMN "${name}" SET COMPRESSION ${to.compression || "DEFAULT"}`
);
}
const fromChecks = from.checks && nameColumnChecks(ast.name, fromName, from.checks);
const toChecks = to.checks && nameColumnChecks(ast.name, name, to.checks);
fromChecks?.forEach((fromCheck) => {
if (!toChecks?.some((toCheck) => cmpRawSql(fromCheck.sql, toCheck.sql))) {
alterTable.push(`DROP CONSTRAINT "${fromCheck.name}"`);
}
});
toChecks?.forEach((toCheck) => {
if (!fromChecks?.some((fromCheck) => cmpRawSql(fromCheck.sql, toCheck.sql))) {
alterTable.push(
`ADD CONSTRAINT "${toCheck.name}"
CHECK (${toCheck.sql.toSQL({
values
})})`
);
}
});
const foreignKeysLen = Math.max(
from.foreignKeys?.length || 0,
to.foreignKeys?.length || 0
);
for (let i = 0; i < foreignKeysLen; i++) {
const fromFkey = from.foreignKeys?.[i];
const toFkey = to.foreignKeys?.[i];
if ((fromFkey || toFkey) && (!fromFkey || !toFkey || fromFkey.options?.name !== toFkey.options?.name || fromFkey.options?.match !== toFkey.options?.match || fromFkey.options?.onUpdate !== toFkey.options?.onUpdate || fromFkey.options?.onDelete !== toFkey.options?.onDelete || fromFkey.options?.dropMode !== toFkey.options?.dropMode || fromFkey.fnOrTable !== toFkey.fnOrTable)) {
if (fromFkey) {
dropConstraints.push({
name: fromFkey.options?.name,
dropMode: fromFkey.options?.dropMode,
references: {
columns: [name],
...fromFkey,
foreignColumns: snakeCase ? fromFkey.foreignColumns.map(orchidCore.toSnakeCase) : fromFkey.foreignColumns
}
});
}
if (toFkey) {
addConstraints.push({
name: toFkey.options?.name,
dropMode: toFkey.options?.dropMode,
references: {
columns: [name],
...toFkey,
foreignColumns: snakeCase ? toFkey.foreignColumns.map(orchidCore.toSnakeCase) : toFkey.foreignColumns
}
});
}
}
}
pushIndexesOrExcludes("indexes", from, to, name, addIndexes, dropIndexes);
pushIndexesOrExcludes(
"excludes",
from,
to,
name,
addExcludes,
dropExcludes
);
if (from.comment !== to.comment) {
comments.push({ column: name, comment: to.comment || null });
}
} else if (item.type === "rename") {
renameItems.push(
snakeCase ? renameColumnSql(orchidCore.toSnakeCase(key), orchidCore.toSnakeCase(item.name)) : renameColumnSql(key, item.name)
);
}
};
const pushIndexesOrExcludes = (key, from, to, name, add2, drop2) => {
const len = Math.max(from[key]?.length || 0, to[key]?.length || 0);
for (let i = 0; i < len; i++) {
const fromItem = from[key]?.[i];
const toItem = to[key]?.[i];
if ((fromItem || toItem) && (!fromItem || !toItem || !orchidCore.deepCompare(fromItem, toItem))) {
if (fromItem) {
drop2.push({
...fromItem,
columns: [
{
column: name,
...fromItem.options,
with: fromItem.with
}
]
});
}
if (toItem) {
add2.push({
...toItem,
columns: [
{
column: name,
...toItem.options,
with: toItem.with
}
]
});
}
}
}
};
const getChangeColumnName = (what, change, key, snakeCase) => {
return change.name || (change[what].column ? (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getColumnName(change[what].column, key, snakeCase)
) : snakeCase ? orchidCore.toSnakeCase(key) : key);
};
const renameColumnSql = (from, to) => {
return `RENAME COLUMN "${from}" TO "${to}"`;
};
const createView = async (migration, up, name, options, sql) => {
const ast = makeAst(up, name, options, sql);
const query = astToQuery(ast);
await migration.adapter.arrays(interpolateSqlValues(query));
};
const makeAst = (up, name, options, sql) => {
if (typeof sql === "string") {
sql = pqb.raw({ raw: sql });
}
return {
type: "view",
action: up ? "create" : "drop",
name,
shape: {},
sql,
options,
deps: []
};
};
const astToQuery = (ast) => {
const values = [];
const sql = [];
const { options } = ast;
if (ast.action === "create") {
sql.push("CREATE");
if (options?.createOrReplace) sql.push("OR REPLACE");
if (options?.temporary) sql.push("TEMPORARY");
if (options?.recursive) sql.push("RECURSIVE");
sql.push(`VIEW "${ast.name}"`);
if (options?.columns) {
sql.push(
`(${options.columns.map((column) => `"${column}"`).join(", ")})`
);
}
if (options?.with) {
const list = [];
if (options.with.checkOption)
list.push(`check_option = ${orchidCore.singleQuote(options.with.checkOption)}`);
if (options.with.securityBarrier) list.push(`security_barrier = true`);
if (options.with.securityInvoker) list.push(`security_invoker = true`);
sql.push(`WITH ( ${list.join(", ")} )`);
}
sql.push(`AS (${ast.sql.toSQL({ values })})`);
} else {
sql.push("DROP VIEW");
if (options?.dropIfExists) sql.push(`IF EXISTS`);
sql.push(`"${ast.name}"`);
if (options?.dropMode) sql.push(options.dropMode);
}
return {
text: sql.join(" "),
values
};
};
const createMigrationInterface = (tx, up, config) => {
const adapter = new pqb.TransactionAdapter(
tx,
tx.client,
tx.types
);
adapter.schema = adapter.adapter.schema ?? "public";
const { query, arrays } = adapter;
const log = pqb.logParamToLogObject(config.logger || console, config.log);
adapter.query = (q, types) => {
return wrapWithLog(log, q, () => query.call(adapter, q, types));
};
adapter.arrays = (q, types) => {
return wrapWithLog(log, q, () => arrays.call(adapter, q, types));
};
Object.assign(adapter, { silentQuery: query, silentArrays: arrays });
const db = pqb.createDb({
adapter,
columnTypes: config.columnTypes
});
const { prototype: proto } = Migration;
for (const key of Object.getOwnPropertyNames(proto)) {
db[key] = proto[key];
}
return Object.assign(db, {
adapter,
log,
up,
options: config
});
};
class Migration {
createTable(tableName, first, second, third) {
return createTable(this, this.up, tableName, first, second, third);
}
dropTable(tableName, first, second, third) {
return createTable(this, !this.up, tableName, first, second, third);
}
changeTable(tableName, cbOrOptions, cb) {
const [fn, options] = typeof cbOrOptions === "function" ? [cbOrOptions, {}] : [cb, cbOrOptions];
return changeTable(this, this.up, tableName, options, fn);
}
/**
* Rename a table:
*
* ```ts
* import { change } from '../dbScript';
*
* change(async (db) => {
* await db.renameTable('oldTableName', 'newTableName');
* });
* ```
*
* Prefix table name with a schema to set a different schema:
*
* ```ts
* import { change } from '../dbScript';
*
* change(async (db) => {
* await db.renameTable('fromSchema.oldTable', 'toSchema.newTable');
* });
* ```
*
* @param from - rename the table from
* @param to - rename the table to
*/
renameTable(from, to) {
return renameType(this, from, to, "TABLE");
}
/**
* Set a different schema to the table:
*
* ```ts
* import { change } from '../dbScript';
*
* change(async (db) => {
* await db.changeTableSchema('tableName', 'fromSchema', 'toSchema');
* });
* ```
*
* @param table - table name
* @param from - current table schema
* @param to - desired table schema
*/
changeTableSchema(table, from, to) {
return this.renameTable(`${from}.${table}`, `${to}.${table}`);
}
/**
* Add a column to the table on migrating, and remove it on rollback.
*
* `dropColumn` takes the same arguments, removes a column on migrate, and adds it on rollback.
*
* ```ts
* import { change } from '../dbScript';
*
* change(async (db) => {
* await db.addColumn('tableName', 'columnName', (t) =>
* t.integer().index().nullable(),
* );
* });
* ```
*
* @param tableName - name of the table to add the column to
* @param columnName - name of the column to add
* @param fn - function returning a type of the column
*/
addColumn(tableName, columnName, fn) {
return addColumn(this, this.up, tableName, columnName, fn);
}
/**
* Drop the schema, create it on rollback. See {@link addColumn}.
*
* @param tableName - name of the table to add the column to
* @param columnName - name of the column to add
* @param fn - function returning a type of the column
*/
dropColumn(tableName, columnName, fn) {
return addColumn(this, !this.up, tableName, columnName, fn);
}
/**
* Add an index to the table on migrating, and remove it on rollback.
*
* `dropIndex` takes the same arguments, removes the index on migrate, and adds it on rollback.
*
* The first argument is the table name, other arguments are the same as in [composite index](#composite-index).
*
* ```ts
* import { change } from '../dbScript';
*
* change(async (db) => {
* await db.addIndex(
* 'tableName',
* ['column1', { column: 'column2', order: 'DESC' }],
* {
* name: 'indexName',
* },
* );
* });
* ```
*
* @param tableName - name of the table to add the index for
* @param columns - indexed columns
* @param args - index options, or an index name and then options
*/
addIndex(tableName, columns, ...args) {
return addIndex(this, this.up, tableName, columns, args);
}
/**
* Drop the schema, create it on rollback. See {@link addIndex}.
*
* @param tableName - name of the table to add the index for
* @param columns - indexed columns
* @param args - index options, or an index name and then options
*/
dropIndex(tableName, columns, ...args) {
return addIndex(this, !this.up, tableName, columns, args);
}
/**
* Rename index:
*
* ```ts
* import { change } from '../dbScript';
*
* change(async (db) => {
* // tableName can be prefixed with a schema
* await db.renameIndex('tableName', 'oldIndexName', 'newIndexName');
* });
* ```
*
* @param tableName - table which this index belongs to
* @param from - rename the index from
* @param to - rename the index to
*/
renameIndex(tableName, from, to) {
return renameTableItem(this, tableName, from, to, "INDEX");
}
/**
* Add a foreign key to a table on migrating, and remove it on rollback.
*
* `dropForeignKey` takes the same arguments, removes the foreign key on migrate, and adds it on rollback.
*
* Arguments:
*
* - table name
* - column names in the table
* - other table name
* - column names in the other table
* - options:
* - `name`: constraint name
* - `match`: 'FULL', 'PARTIAL', or 'SIMPLE'
* - `onUpdate` and `onDelete`: 'NO ACTION', 'RESTRICT', 'CASCADE', 'SET NULL', or 'SET DEFAULT'
*
* The first argument is the table name, other arguments are the same as in [composite foreign key](#composite-foreign-key).
*
* ```ts
* import { change } from '../dbScript';
*
* change(async (db) => {
* await db.addForeignKey(
* 'tableName',
* ['id', 'name'],
* 'otherTable',
* ['foreignId', 'foreignName'],
* {
* name: 'constraintName',
* match: 'FULL',
* onUpdate: 'RESTRICT',
* onDelete: 'CASCADE',
* },
* );
* });
* ```
*
* @param tableName - table name
* @param columns - column names in the table
* @param foreignTable - other table name
* @param foreignColumns - column names in the other table
* @param options - foreign key options
*/
addForeignKey(tableName, columns, foreignTable, foreignColumns, options) {
return addForeignKey(
this,
this.up,
tableName,
columns,
foreignTable,
foreignColumns,
options
);
}
/*