orchid-orm 1.78.12 → 1.78.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/migrations/index.js +527 -186
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/index.mjs +528 -187
- package/dist/migrations/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1,140 +1,26 @@
|
|
|
1
|
-
import { astToMigration, concatSchemaAndName, createMigrationInterface, dbColumnToAst, encodeColumnDefault, getConstraintName, getDbStructureTableData, getDbTableColumnsChecks, getDbVersion, getExcludeName, getIndexName, getMigrationsSchemaAndTable, getSchemaAndTableFromName, getTableFactoryConfig, instantiateDbColumn, introspectDbSchema, makeDomainsMap, makeFileVersion, makeStructureToAstCtx, migrate, migrateAndClose, promptSelect, rakeDbCommands, saveMigratedVersion, structureToAst, tableToAst, writeMigrationFile } from "rake-db";
|
|
1
|
+
import { RakeDbError, astToMigration, concatSchemaAndName, createMigrationInterface, dbColumnToAst, encodeColumnDefault, getConstraintName, getDbStructureTableData, getDbTableColumnsChecks, getDbVersion, getExcludeName, getIndexName, getMigrationsSchemaAndTable, getSchemaAndTableFromName, getTableFactoryConfig, instantiateDbColumn, introspectDbSchema, makeDomainsMap, makeFileVersion, makeStructureToAstCtx, migrate, migrateAndClose, promptSelect, rakeDbCommands, saveMigratedVersion, structureToAst, tableToAst, writeMigrationFile } from "rake-db";
|
|
2
2
|
import { ArrayColumn, DomainColumn, EnumColumn, RawSql, UnknownColumn, VirtualColumn, addCode, codeToString, colors, columnsShapeToCode, constraintToCode, deepCompare, emptyArray, emptyObject, excludeToCode, exhaustive, getColumnBaseType, getDriverErrorCode, getForeignKeyTableInstance, getFreeSetAlias, getImportPath, getQuerySchema, getSupportedDefaultPrivileges, indexToCode, pathToLog, pluralize, primaryKeyInnerToCode, queryToSql, quoteObjectKey, raw, rawSqlToSql, singleQuote, sqlToRawSql, toArray, toCamelCase, toPascalCase, toSnakeCase } from "pqb/internal";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
export * from "rake-db";
|
|
7
|
-
const
|
|
8
|
-
return sqlToRawSql(viewDataToQuerySql(viewData, viewName));
|
|
9
|
-
};
|
|
10
|
-
const viewDataToQuerySql = (viewData, viewName) => {
|
|
11
|
-
if (viewData.query) return queryToSql(viewData.query);
|
|
12
|
-
if (viewData.sql !== void 0) return rawSqlToSql(viewData.sql);
|
|
13
|
-
throw new Error(`Either sql or query is required for view ${viewName}`);
|
|
14
|
-
};
|
|
15
|
-
const compareSqlExpressions = async (expressions, adapter) => {
|
|
16
|
-
if (!expressions.length) return;
|
|
17
|
-
let id = 1;
|
|
18
|
-
for (const { source, compare, handle } of expressions) {
|
|
19
|
-
const viewName = `orchidTmpView${id++}`;
|
|
20
|
-
const values = [];
|
|
21
|
-
const combinedQueries = [
|
|
22
|
-
`CREATE TEMPORARY VIEW ${viewName} AS (SELECT ${compare.map(({ inDb, inCode }, i) => `${inDb} AS "*inDb-${i}*", ${inCode.map((s, j) => `(${typeof s === "string" ? s : s.toSQL({ values })}) "*inCode-${i}-${j}*"`).join(", ")}`).join(", ")}${source ? ` FROM ${source}` : ""})`,
|
|
23
|
-
`SELECT pg_get_viewdef('${viewName}') v`,
|
|
24
|
-
`DROP VIEW ${viewName}`
|
|
25
|
-
].join("; ");
|
|
26
|
-
const query = () => adapter.query(combinedQueries, values);
|
|
27
|
-
const result = await (adapter.isInTransaction() ? adapter.savepoint(viewName, void 0, query) : query()).then((res) => res[1], async (err) => {
|
|
28
|
-
if (typeof err === "object" && getDriverErrorCode(err) !== "42704") throw err;
|
|
29
|
-
});
|
|
30
|
-
if (!result) {
|
|
31
|
-
handle();
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
handle(compareSqlExpressionResult(result.rows[0].v, compare[0].inCode));
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
const compareViewsExpressions = async (adapter, compare) => {
|
|
38
|
-
if (!compare.length) return;
|
|
39
|
-
const queries = [];
|
|
40
|
-
let id = 1;
|
|
41
|
-
compare.forEach(({ inCode, ast }, i) => {
|
|
42
|
-
const viewName = `orchidTmpView${id++}ForViews`;
|
|
43
|
-
queries.push({ batch: [
|
|
44
|
-
`SAVEPOINT "${viewName}S"`,
|
|
45
|
-
`CREATE TEMPORARY${"recursive" in ast.options && ast.options.recursive ? " RECURSIVE" : ""} VIEW "${viewName}" (${ast.options.columns?.map((column) => `"${column}"`).join(", ")}) AS (${inCode})`,
|
|
46
|
-
`SELECT ${i} i, '${viewName}' v, pg_get_viewdef('"${viewName}"') sql`,
|
|
47
|
-
`DROP VIEW "${viewName}"`,
|
|
48
|
-
`RELEASE SAVEPOINT "${viewName}S"`
|
|
49
|
-
] });
|
|
50
|
-
});
|
|
51
|
-
let results;
|
|
52
|
-
try {
|
|
53
|
-
const sql = queries.flatMap((q) => q.batch).join(";");
|
|
54
|
-
const query = () => adapter.query(sql, []);
|
|
55
|
-
results = await (adapter.isInTransaction() ? adapter.savepoint("orchidOrmGeneratorViews", void 0, query) : query());
|
|
56
|
-
} catch {
|
|
57
|
-
results = (await Promise.all(queries.map(async ({ batch: queries }, i) => {
|
|
58
|
-
const sql = queries.join(";");
|
|
59
|
-
const query = () => adapter.query(sql, []);
|
|
60
|
-
return await (adapter.isInTransaction() ? adapter.savepoint(`orchidOrmGeneratorViews${i}`, void 0, query) : query()).catch((err) => {
|
|
61
|
-
if (typeof err === "object") {
|
|
62
|
-
const code = getDriverErrorCode(err);
|
|
63
|
-
if (code === "42703" || code === "42704" || code === "42P01") return [];
|
|
64
|
-
}
|
|
65
|
-
throw err;
|
|
66
|
-
});
|
|
67
|
-
}))).flat();
|
|
68
|
-
}
|
|
69
|
-
const handled = /* @__PURE__ */ new Set();
|
|
70
|
-
for (const result of results) for (const row of result.rows) if ("sql" in row) {
|
|
71
|
-
const { i, v } = row;
|
|
72
|
-
const cmp = compare[i];
|
|
73
|
-
const hasQuote = !!cmp.inDb.match(/\w*WITH RECURSIVE "/);
|
|
74
|
-
if (row.sql.replaceAll(hasQuote ? v : `"${v}"`, cmp.ast.name) !== cmp.inDb) cmp.onNotEqual();
|
|
75
|
-
handled.add(i);
|
|
76
|
-
}
|
|
77
|
-
compare.forEach((cmp, i) => {
|
|
78
|
-
if (!handled.has(i)) cmp.onNotEqual();
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
const compareSqlExpressionResult = (resultSql, inCode) => {
|
|
82
|
-
let pos = 7;
|
|
83
|
-
const rgx = /\s+AS\s+"\*(inDb-\d+|inCode-\d+-\d+)\*",?/g;
|
|
84
|
-
let match;
|
|
85
|
-
let inDb = "";
|
|
86
|
-
let codeI = 0;
|
|
87
|
-
const matches = inCode.map(() => true);
|
|
88
|
-
while (match = rgx.exec(resultSql)) {
|
|
89
|
-
const sql = resultSql.slice(pos, rgx.lastIndex - match[0].length).trim();
|
|
90
|
-
if (match[1].split("-").length === 2) {
|
|
91
|
-
inDb = sql;
|
|
92
|
-
codeI = 0;
|
|
93
|
-
} else {
|
|
94
|
-
if (inDb !== sql && !(inDb.startsWith("(") && inDb.endsWith(")") && inDb.slice(1, -1) === sql) && !(sql.startsWith("(") && sql.endsWith(")") && sql.slice(1, -1) === inDb)) matches[codeI] = false;
|
|
95
|
-
codeI++;
|
|
96
|
-
}
|
|
97
|
-
pos = rgx.lastIndex;
|
|
98
|
-
}
|
|
99
|
-
const firstMatching = matches.indexOf(true);
|
|
100
|
-
return firstMatching === -1 ? void 0 : firstMatching;
|
|
101
|
-
};
|
|
102
|
-
const promptCreateOrRename = (kind, name, drop, verifying) => {
|
|
103
|
-
if (verifying) throw new AbortSignal();
|
|
104
|
-
let hintPos = name.length + 4;
|
|
105
|
-
for (const from of drop) {
|
|
106
|
-
const value = from.length + 8 + name.length;
|
|
107
|
-
if (value > hintPos) hintPos = value;
|
|
108
|
-
}
|
|
109
|
-
let max = 0;
|
|
110
|
-
const add = name.length + 3;
|
|
111
|
-
for (const name of drop) if (name.length + add > max) max = name.length + add;
|
|
112
|
-
const renameMessage = `rename ${kind}`;
|
|
113
|
-
return promptSelect({
|
|
114
|
-
message: `Create or rename ${colors.blueBold(name)} ${kind} from another ${kind}?`,
|
|
115
|
-
options: [`${colors.greenBold("+")} ${name} ${colors.pale(`create ${kind}`.padStart(hintPos + renameMessage.length - name.length - 4, " "))}`, ...drop.map((d) => `${colors.yellowBold("~")} ${d} ${colors.yellowBold("=>")} ${name} ${colors.pale(renameMessage.padStart(hintPos + renameMessage.length - d.length - name.length - 8, " "))}`)]
|
|
116
|
-
});
|
|
117
|
-
};
|
|
118
|
-
const checkForColumnAddOrDrop = (shape, key) => {
|
|
119
|
-
const item = shape[key];
|
|
120
|
-
if (item) return item && (Array.isArray(item) || item.type === "add" || item.type === "drop");
|
|
121
|
-
for (const k in shape) {
|
|
122
|
-
const item = shape[k];
|
|
123
|
-
if (Array.isArray(item) ? item.some((item) => (item.type === "add" || item.type === "drop") && item.item.data.name === key) : (item.type === "add" || item.type === "drop") && item.item.data.name === key) return true;
|
|
124
|
-
}
|
|
125
|
-
return false;
|
|
126
|
-
};
|
|
127
|
-
const processSchemas = async (ast, dbStructure, { codeItems: { schemas }, verifying, internal: { generatorIgnore }, currentSchema }) => {
|
|
7
|
+
const processSchemas = async (ast, dbStructure, { codeItems: { schemas }, internal: { generatorIgnore }, currentSchema }, { decider }) => {
|
|
128
8
|
const createSchemas = [];
|
|
129
9
|
const dropSchemas = [];
|
|
130
10
|
for (const schema of schemas) if (!dbStructure.schemas.includes(schema)) createSchemas.push(schema);
|
|
131
11
|
for (const schema of dbStructure.schemas) if (!schemas.has(schema) && schema !== "public" && schema !== currentSchema && !generatorIgnore?.schemas?.includes(schema)) dropSchemas.push(schema);
|
|
132
12
|
for (const schema of createSchemas) {
|
|
133
13
|
if (dropSchemas.length) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
dropSchemas
|
|
14
|
+
const from = await decider.createOrRename({
|
|
15
|
+
kind: "schema",
|
|
16
|
+
target: { name: schema },
|
|
17
|
+
candidates: dropSchemas,
|
|
18
|
+
candidateSource: (x) => ({ name: x }),
|
|
19
|
+
name: schema,
|
|
20
|
+
candidateName: (x) => x
|
|
21
|
+
});
|
|
22
|
+
if (from) {
|
|
23
|
+
dropSchemas.splice(dropSchemas.indexOf(from), 1);
|
|
138
24
|
const views = dbStructure.views || [];
|
|
139
25
|
renameSchemaInStructures(dbStructure.tables, from, schema);
|
|
140
26
|
renameSchemaInStructures(views, from, schema);
|
|
@@ -206,12 +92,12 @@ const processExtensions = (ast, dbStructure, { currentSchema, internal: { extens
|
|
|
206
92
|
...ext
|
|
207
93
|
})));
|
|
208
94
|
};
|
|
209
|
-
const processColumns = async (adapter, config, structureToAstCtx, dbStructure, domainsMap, changeTableData, ast, currentSchema, compareSql, typeCastsCache,
|
|
95
|
+
const processColumns = async (adapter, config, structureToAstCtx, dbStructure, domainsMap, changeTableData, ast, currentSchema, compareSql, typeCastsCache, decisionCtx) => {
|
|
210
96
|
const { dbTable } = changeTableData;
|
|
211
97
|
const dbColumns = Object.fromEntries(dbTable.columns.map((column) => [column.name, column]));
|
|
212
98
|
const { columnsToAdd, columnsToDrop, columnsToChange } = groupColumns(structureToAstCtx, dbStructure, domainsMap, dbColumns, changeTableData);
|
|
213
|
-
await addOrRenameColumns(config, dbStructure, changeTableData, columnsToAdd, columnsToDrop, columnsToChange,
|
|
214
|
-
await changeColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, dbColumns, columnsToChange, compareSql, changeTableData, typeCastsCache,
|
|
99
|
+
await addOrRenameColumns(config, dbStructure, changeTableData, columnsToAdd, columnsToDrop, columnsToChange, decisionCtx);
|
|
100
|
+
await changeColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, dbColumns, columnsToChange, compareSql, changeTableData, typeCastsCache, decisionCtx);
|
|
215
101
|
dropColumns(changeTableData, columnsToDrop);
|
|
216
102
|
};
|
|
217
103
|
const groupColumns = (structureToAstCtx, dbStructure, domainsMap, dbColumns, changeTableData) => {
|
|
@@ -251,14 +137,21 @@ const groupColumns = (structureToAstCtx, dbStructure, domainsMap, dbColumns, cha
|
|
|
251
137
|
columnsToChange
|
|
252
138
|
};
|
|
253
139
|
};
|
|
254
|
-
const addOrRenameColumns = async (config, dbStructure,
|
|
140
|
+
const addOrRenameColumns = async (config, dbStructure, changeTableData, columnsToAdd, columnsToDrop, columnsToChange, decisionCtx) => {
|
|
141
|
+
const { dbTableData, schema, changeTableAst: { name: tableName, shape } } = changeTableData;
|
|
255
142
|
for (const { key, column } of columnsToAdd) {
|
|
256
143
|
if (columnsToDrop.length) {
|
|
257
144
|
const codeName = column.data.name ?? key;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
columnsToDrop
|
|
145
|
+
const drop = await decisionCtx.decider.createOrRename({
|
|
146
|
+
kind: "column",
|
|
147
|
+
target: codeColumnTarget(changeTableData, codeName),
|
|
148
|
+
candidates: columnsToDrop,
|
|
149
|
+
candidateSource: (x) => dbColumnSource(decisionCtx, changeTableData, x.column.data.name ?? x.key),
|
|
150
|
+
name: codeName,
|
|
151
|
+
candidateName: (x) => x.key
|
|
152
|
+
});
|
|
153
|
+
if (drop) {
|
|
154
|
+
columnsToDrop.splice(columnsToDrop.indexOf(drop), 1);
|
|
262
155
|
const from = drop.column.data.name ?? drop.key;
|
|
263
156
|
columnsToChange.set(from, {
|
|
264
157
|
key,
|
|
@@ -289,11 +182,11 @@ const dropColumns = ({ changeTableAst: { shape } }, columnsToDrop) => {
|
|
|
289
182
|
item: column
|
|
290
183
|
};
|
|
291
184
|
};
|
|
292
|
-
const changeColumns = async (adapter, config, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, dbColumns, columnsToChange, compareSql, changeTableData, typeCastsCache,
|
|
185
|
+
const changeColumns = async (adapter, config, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, dbColumns, columnsToChange, compareSql, changeTableData, typeCastsCache, decisionCtx) => {
|
|
293
186
|
for (const [key, { key: codeKey, dbName, column: codeColumn }] of columnsToChange) {
|
|
294
187
|
const dbColumnStructure = dbColumns[dbName];
|
|
295
188
|
const dbColumn = instantiateDbColumn(structureToAstCtx, dbStructure, domainsMap, dbColumnStructure);
|
|
296
|
-
const action = await compareColumns(adapter, domainsMap, ast, currentSchema, compareSql, changeTableData, typeCastsCache,
|
|
189
|
+
const action = await compareColumns(adapter, domainsMap, ast, currentSchema, compareSql, changeTableData, typeCastsCache, decisionCtx, key, dbName, codeColumn.data.name ?? codeKey, dbColumn, codeColumn);
|
|
297
190
|
if (action === "change") changeColumn(changeTableData, key, dbName, dbColumn, codeColumn);
|
|
298
191
|
else if (action === "recreate") changeTableData.changeTableAst.shape[key] = [{
|
|
299
192
|
type: "drop",
|
|
@@ -311,7 +204,7 @@ const changeColumns = async (adapter, config, structureToAstCtx, dbStructure, do
|
|
|
311
204
|
}
|
|
312
205
|
}
|
|
313
206
|
};
|
|
314
|
-
const compareColumns = async (adapter, domainsMap, ast, currentSchema, compareSql, changeTableData, typeCastsCache,
|
|
207
|
+
const compareColumns = async (adapter, domainsMap, ast, currentSchema, compareSql, changeTableData, typeCastsCache, decisionCtx, key, dbName, codeName, dbColumn, codeColumn) => {
|
|
315
208
|
const maybeArrayDbColumn = dbColumn;
|
|
316
209
|
const maybeArrayCodeColumn = codeColumn;
|
|
317
210
|
if (dbColumn instanceof ArrayColumn && codeColumn instanceof ArrayColumn) {
|
|
@@ -326,11 +219,13 @@ const compareColumns = async (adapter, domainsMap, ast, currentSchema, compareSq
|
|
|
326
219
|
const codeBaseType = getColumnBaseType(codeColumn, domainsMap, codeType);
|
|
327
220
|
if (!typeCasts.get(dbBaseType)?.has(codeBaseType)) {
|
|
328
221
|
if (!(dbColumn instanceof EnumColumn) || !(codeColumn instanceof EnumColumn) || !deepCompare(dbColumn.options, codeColumn.options)) {
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
222
|
+
if (!await decisionCtx.decider.recreateColumn({
|
|
223
|
+
source: dbColumnSource(decisionCtx, changeTableData, dbName),
|
|
224
|
+
target: codeColumnTarget(changeTableData, codeName),
|
|
225
|
+
tableName: concatSchemaAndName(changeTableData.changeTableAst),
|
|
226
|
+
columnName: key,
|
|
227
|
+
fromType: dbType,
|
|
228
|
+
toType: codeType
|
|
334
229
|
})) throw new AbortSignal();
|
|
335
230
|
dbColumn.data.name = codeColumn.data.name;
|
|
336
231
|
return "recreate";
|
|
@@ -445,6 +340,19 @@ const changeColumn = (changeTableData, key, dbName, dbColumn, codeColumn) => {
|
|
|
445
340
|
to: { column: simpleCodeColumn }
|
|
446
341
|
};
|
|
447
342
|
};
|
|
343
|
+
const dbColumnSource = (decisionCtx, { dbTable }, name) => {
|
|
344
|
+
const { schema, name: table } = decisionCtx.dbSource(dbTable);
|
|
345
|
+
return {
|
|
346
|
+
schema,
|
|
347
|
+
table,
|
|
348
|
+
name
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
const codeColumnTarget = ({ schema, changeTableAst }, name) => ({
|
|
352
|
+
schema,
|
|
353
|
+
table: changeTableAst.name,
|
|
354
|
+
name
|
|
355
|
+
});
|
|
448
356
|
const getColumnDbType = (column, currentSchema) => {
|
|
449
357
|
if (column instanceof EnumColumn) {
|
|
450
358
|
const [schema = currentSchema, name] = getSchemaAndTableFromName(currentSchema, column.enumName);
|
|
@@ -464,6 +372,110 @@ const getColumnDbType = (column, currentSchema) => {
|
|
|
464
372
|
const renameColumn = (columns, from, to) => {
|
|
465
373
|
for (let i = 0; i < columns.length; i++) if (columns[i] === from) columns[i] = to;
|
|
466
374
|
};
|
|
375
|
+
const viewDataToSql = (viewData, viewName) => {
|
|
376
|
+
return sqlToRawSql(viewDataToQuerySql(viewData, viewName));
|
|
377
|
+
};
|
|
378
|
+
const viewDataToQuerySql = (viewData, viewName) => {
|
|
379
|
+
if (viewData.query) return queryToSql(viewData.query);
|
|
380
|
+
if (viewData.sql !== void 0) return rawSqlToSql(viewData.sql);
|
|
381
|
+
throw new Error(`Either sql or query is required for view ${viewName}`);
|
|
382
|
+
};
|
|
383
|
+
const compareSqlExpressions = async (expressions, adapter) => {
|
|
384
|
+
if (!expressions.length) return;
|
|
385
|
+
let id = 1;
|
|
386
|
+
for (const { source, compare, handle } of expressions) {
|
|
387
|
+
const viewName = `orchidTmpView${id++}`;
|
|
388
|
+
const values = [];
|
|
389
|
+
const combinedQueries = [
|
|
390
|
+
`CREATE TEMPORARY VIEW ${viewName} AS (SELECT ${compare.map(({ inDb, inCode }, i) => `${inDb} AS "*inDb-${i}*", ${inCode.map((s, j) => `(${typeof s === "string" ? s : s.toSQL({ values })}) "*inCode-${i}-${j}*"`).join(", ")}`).join(", ")}${source ? ` FROM ${source}` : ""})`,
|
|
391
|
+
`SELECT pg_get_viewdef('${viewName}') v`,
|
|
392
|
+
`DROP VIEW ${viewName}`
|
|
393
|
+
].join("; ");
|
|
394
|
+
const query = () => adapter.query(combinedQueries, values);
|
|
395
|
+
const result = await (adapter.isInTransaction() ? adapter.savepoint(viewName, void 0, query) : query()).then((res) => res[1], async (err) => {
|
|
396
|
+
if (typeof err === "object" && getDriverErrorCode(err) !== "42704") throw err;
|
|
397
|
+
});
|
|
398
|
+
if (!result) {
|
|
399
|
+
handle();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
handle(compareSqlExpressionResult(result.rows[0].v, compare[0].inCode));
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const compareViewsExpressions = async (adapter, compare) => {
|
|
406
|
+
if (!compare.length) return;
|
|
407
|
+
const queries = [];
|
|
408
|
+
let id = 1;
|
|
409
|
+
compare.forEach(({ inCode, ast }, i) => {
|
|
410
|
+
const viewName = `orchidTmpView${id++}ForViews`;
|
|
411
|
+
queries.push({ batch: [
|
|
412
|
+
`SAVEPOINT "${viewName}S"`,
|
|
413
|
+
`CREATE TEMPORARY${"recursive" in ast.options && ast.options.recursive ? " RECURSIVE" : ""} VIEW "${viewName}" (${ast.options.columns?.map((column) => `"${column}"`).join(", ")}) AS (${inCode})`,
|
|
414
|
+
`SELECT ${i} i, '${viewName}' v, pg_get_viewdef('"${viewName}"') sql`,
|
|
415
|
+
`DROP VIEW "${viewName}"`,
|
|
416
|
+
`RELEASE SAVEPOINT "${viewName}S"`
|
|
417
|
+
] });
|
|
418
|
+
});
|
|
419
|
+
let results;
|
|
420
|
+
try {
|
|
421
|
+
const sql = queries.flatMap((q) => q.batch).join(";");
|
|
422
|
+
const query = () => adapter.query(sql, []);
|
|
423
|
+
results = await (adapter.isInTransaction() ? adapter.savepoint("orchidOrmGeneratorViews", void 0, query) : query());
|
|
424
|
+
} catch {
|
|
425
|
+
results = (await Promise.all(queries.map(async ({ batch: queries }, i) => {
|
|
426
|
+
const sql = queries.join(";");
|
|
427
|
+
const query = () => adapter.query(sql, []);
|
|
428
|
+
return await (adapter.isInTransaction() ? adapter.savepoint(`orchidOrmGeneratorViews${i}`, void 0, query) : query()).catch((err) => {
|
|
429
|
+
if (typeof err === "object") {
|
|
430
|
+
const code = getDriverErrorCode(err);
|
|
431
|
+
if (code === "42703" || code === "42704" || code === "42P01") return [];
|
|
432
|
+
}
|
|
433
|
+
throw err;
|
|
434
|
+
});
|
|
435
|
+
}))).flat();
|
|
436
|
+
}
|
|
437
|
+
const handled = /* @__PURE__ */ new Set();
|
|
438
|
+
for (const result of results) for (const row of result.rows) if ("sql" in row) {
|
|
439
|
+
const { i, v } = row;
|
|
440
|
+
const cmp = compare[i];
|
|
441
|
+
const hasQuote = !!cmp.inDb.match(/\w*WITH RECURSIVE "/);
|
|
442
|
+
if (row.sql.replaceAll(hasQuote ? v : `"${v}"`, cmp.ast.name) !== cmp.inDb) cmp.onNotEqual();
|
|
443
|
+
handled.add(i);
|
|
444
|
+
}
|
|
445
|
+
compare.forEach((cmp, i) => {
|
|
446
|
+
if (!handled.has(i)) cmp.onNotEqual();
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
const compareSqlExpressionResult = (resultSql, inCode) => {
|
|
450
|
+
let pos = 7;
|
|
451
|
+
const rgx = /\s+AS\s+"\*(inDb-\d+|inCode-\d+-\d+)\*",?/g;
|
|
452
|
+
let match;
|
|
453
|
+
let inDb = "";
|
|
454
|
+
let codeI = 0;
|
|
455
|
+
const matches = inCode.map(() => true);
|
|
456
|
+
while (match = rgx.exec(resultSql)) {
|
|
457
|
+
const sql = resultSql.slice(pos, rgx.lastIndex - match[0].length).trim();
|
|
458
|
+
if (match[1].split("-").length === 2) {
|
|
459
|
+
inDb = sql;
|
|
460
|
+
codeI = 0;
|
|
461
|
+
} else {
|
|
462
|
+
if (inDb !== sql && !(inDb.startsWith("(") && inDb.endsWith(")") && inDb.slice(1, -1) === sql) && !(sql.startsWith("(") && sql.endsWith(")") && sql.slice(1, -1) === inDb)) matches[codeI] = false;
|
|
463
|
+
codeI++;
|
|
464
|
+
}
|
|
465
|
+
pos = rgx.lastIndex;
|
|
466
|
+
}
|
|
467
|
+
const firstMatching = matches.indexOf(true);
|
|
468
|
+
return firstMatching === -1 ? void 0 : firstMatching;
|
|
469
|
+
};
|
|
470
|
+
const checkForColumnAddOrDrop = (shape, key) => {
|
|
471
|
+
const item = shape[key];
|
|
472
|
+
if (item) return item && (Array.isArray(item) || item.type === "add" || item.type === "drop");
|
|
473
|
+
for (const k in shape) {
|
|
474
|
+
const item = shape[k];
|
|
475
|
+
if (Array.isArray(item) ? item.some((item) => (item.type === "add" || item.type === "drop") && item.item.data.name === key) : (item.type === "add" || item.type === "drop") && item.item.data.name === key) return true;
|
|
476
|
+
}
|
|
477
|
+
return false;
|
|
478
|
+
};
|
|
467
479
|
const processDomains = async (ast, adapter, domainsMap, dbStructure, { codeItems: { domains }, structureToAstCtx, currentSchema, internal: { generatorIgnore } }, pendingDbTypes) => {
|
|
468
480
|
const codeDomains = [];
|
|
469
481
|
if (domains) for (const { schemaName, name, column } of domains) codeDomains.push(makeComparableDomain(currentSchema, schemaName, name, column));
|
|
@@ -594,7 +606,7 @@ const createAst = (codeDomain) => ({
|
|
|
594
606
|
name: codeDomain.name,
|
|
595
607
|
baseType: codeDomain.column
|
|
596
608
|
});
|
|
597
|
-
const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSchema,
|
|
609
|
+
const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSchema, internal: { generatorIgnore } }, decisionCtx, pendingDbTypes) => {
|
|
598
610
|
const createEnums = [];
|
|
599
611
|
const dropEnums = [];
|
|
600
612
|
for (const [, codeEnum] of enums) {
|
|
@@ -605,7 +617,7 @@ const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSch
|
|
|
605
617
|
if (generatorIgnore?.schemas?.includes(dbEnum.schemaName) || generatorIgnore?.enums?.includes(dbEnum.name)) continue;
|
|
606
618
|
const codeEnum = enums.get(`${dbEnum.schemaName}.${dbEnum.name}`);
|
|
607
619
|
if (codeEnum) {
|
|
608
|
-
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes,
|
|
620
|
+
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes, decisionCtx, currentSchema);
|
|
609
621
|
continue;
|
|
610
622
|
}
|
|
611
623
|
const i = createEnums.findIndex((x) => x.name === dbEnum.name);
|
|
@@ -624,17 +636,26 @@ const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSch
|
|
|
624
636
|
to: dbEnum.name
|
|
625
637
|
});
|
|
626
638
|
pendingDbTypes.add(toSchema, dbEnum.name);
|
|
627
|
-
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes,
|
|
639
|
+
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes, decisionCtx, currentSchema);
|
|
628
640
|
continue;
|
|
629
641
|
}
|
|
630
642
|
dropEnums.push(dbEnum);
|
|
631
643
|
}
|
|
632
644
|
for (const codeEnum of createEnums) {
|
|
633
645
|
if (dropEnums.length) {
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
646
|
+
const dbEnum = await decisionCtx.decider.createOrRename({
|
|
647
|
+
kind: "enum",
|
|
648
|
+
target: {
|
|
649
|
+
schema: codeEnum.schema ?? currentSchema,
|
|
650
|
+
name: codeEnum.name
|
|
651
|
+
},
|
|
652
|
+
candidates: dropEnums,
|
|
653
|
+
candidateSource: (x) => decisionCtx.dbSource(x),
|
|
654
|
+
name: codeEnum.name,
|
|
655
|
+
candidateName: (x) => x.name
|
|
656
|
+
});
|
|
657
|
+
if (dbEnum) {
|
|
658
|
+
dropEnums.splice(dropEnums.indexOf(dbEnum), 1);
|
|
638
659
|
const fromSchema = dbEnum.schemaName;
|
|
639
660
|
const from = dbEnum.name;
|
|
640
661
|
const toSchema = codeEnum.schema ?? currentSchema;
|
|
@@ -650,7 +671,7 @@ const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSch
|
|
|
650
671
|
to
|
|
651
672
|
});
|
|
652
673
|
pendingDbTypes.add(toSchema, to);
|
|
653
|
-
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes,
|
|
674
|
+
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes, decisionCtx, currentSchema);
|
|
654
675
|
continue;
|
|
655
676
|
}
|
|
656
677
|
}
|
|
@@ -669,7 +690,7 @@ const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSch
|
|
|
669
690
|
values: dbEnum.values
|
|
670
691
|
});
|
|
671
692
|
};
|
|
672
|
-
const changeEnum = async (ast, dbEnum, codeEnum, pendingDbTypes,
|
|
693
|
+
const changeEnum = async (ast, dbEnum, codeEnum, pendingDbTypes, decisionCtx, currentSchema) => {
|
|
673
694
|
const { values: dbValues } = dbEnum;
|
|
674
695
|
const { values: codeValues, schema, name } = codeEnum;
|
|
675
696
|
const addValues = codeValues.filter((value) => !dbValues.includes(value));
|
|
@@ -699,7 +720,14 @@ const changeEnum = async (ast, dbEnum, codeEnum, pendingDbTypes, verifying) => {
|
|
|
699
720
|
return;
|
|
700
721
|
}
|
|
701
722
|
} else if (!dropValues.length) return;
|
|
702
|
-
const enumValueChanges = await
|
|
723
|
+
const enumValueChanges = await resolveEnumValueChanges({
|
|
724
|
+
source: decisionCtx.dbSource(dbEnum),
|
|
725
|
+
target: {
|
|
726
|
+
schema: schema ?? currentSchema,
|
|
727
|
+
name
|
|
728
|
+
},
|
|
729
|
+
enumName: name
|
|
730
|
+
}, dbValues, codeValues, addValues, dropValues, decisionCtx.decider);
|
|
703
731
|
if (enumValueChanges) {
|
|
704
732
|
let changed = false;
|
|
705
733
|
if (Object.keys(enumValueChanges.renamedValues).length) {
|
|
@@ -735,19 +763,18 @@ const changeEnum = async (ast, dbEnum, codeEnum, pendingDbTypes, verifying) => {
|
|
|
735
763
|
});
|
|
736
764
|
pendingDbTypes.add(schema, name);
|
|
737
765
|
};
|
|
738
|
-
const
|
|
766
|
+
const resolveEnumValueChanges = async (enumQuestion, dbValues, codeValues, addValues, dropValues, decider) => {
|
|
739
767
|
if (!addValues.length || !dropValues.length) return;
|
|
740
768
|
const renamedValues = {};
|
|
741
769
|
const remainingDropValues = [...dropValues];
|
|
742
770
|
for (const value of addValues) if (remainingDropValues.length) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
771
|
+
const dropValue = await decider.addOrRenameEnumValue({
|
|
772
|
+
...enumQuestion,
|
|
773
|
+
value,
|
|
774
|
+
candidates: remainingDropValues
|
|
747
775
|
});
|
|
748
|
-
if (
|
|
749
|
-
|
|
750
|
-
remainingDropValues.splice(i - 1, 1);
|
|
776
|
+
if (dropValue !== void 0) {
|
|
777
|
+
remainingDropValues.splice(remainingDropValues.indexOf(dropValue), 1);
|
|
751
778
|
renamedValues[dropValue] = value;
|
|
752
779
|
}
|
|
753
780
|
}
|
|
@@ -1721,7 +1748,7 @@ const isSameStringArray = (a, b) => {
|
|
|
1721
1748
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
1722
1749
|
return true;
|
|
1723
1750
|
};
|
|
1724
|
-
const processTables = async (ast, domainsMap, adapter, dbStructure, config, { structureToAstCtx, codeItems: { tables }, currentSchema, internal: { generatorIgnore }
|
|
1751
|
+
const processTables = async (ast, domainsMap, adapter, dbStructure, config, { structureToAstCtx, codeItems: { tables }, currentSchema, internal: { generatorIgnore } }, decisionCtx, pendingDbTypes) => {
|
|
1725
1752
|
const createTables = collectCreateTables(tables, dbStructure, currentSchema);
|
|
1726
1753
|
const compareSql = {
|
|
1727
1754
|
values: [],
|
|
@@ -1730,8 +1757,8 @@ const processTables = async (ast, domainsMap, adapter, dbStructure, config, { st
|
|
|
1730
1757
|
const expressions = [];
|
|
1731
1758
|
const { changeTables, changeTableSchemas, dropTables, tableShapes } = collectChangeAndDropTables(adapter, config, tables, dbStructure, currentSchema, createTables, generatorIgnore);
|
|
1732
1759
|
applyChangeTableSchemas(changeTableSchemas, currentSchema, ast);
|
|
1733
|
-
await applyCreateOrRenameTables(dbStructure, createTables, dropTables, changeTables, tableShapes, currentSchema, ast,
|
|
1734
|
-
await applyChangeTables(adapter, changeTables, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, compareSql, expressions,
|
|
1760
|
+
await applyCreateOrRenameTables(dbStructure, createTables, dropTables, changeTables, tableShapes, currentSchema, ast, decisionCtx);
|
|
1761
|
+
await applyChangeTables(adapter, changeTables, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, compareSql, expressions, decisionCtx, pendingDbTypes);
|
|
1735
1762
|
processForeignKeys(config, ast, changeTables, currentSchema, tableShapes);
|
|
1736
1763
|
await Promise.all([applyCompareSql(compareSql, adapter), compareSqlExpressions(expressions, adapter)]);
|
|
1737
1764
|
await processTableRls(adapter, ast, dbStructure, tables, currentSchema, generatorIgnore);
|
|
@@ -1807,12 +1834,12 @@ const applyChangeTableSchemas = (changeTableSchemas, currentSchema, ast) => {
|
|
|
1807
1834
|
});
|
|
1808
1835
|
}
|
|
1809
1836
|
};
|
|
1810
|
-
const applyChangeTables = async (adapter, changeTables, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, compareSql, expressions,
|
|
1837
|
+
const applyChangeTables = async (adapter, changeTables, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, compareSql, expressions, decisionCtx, pendingDbTypes) => {
|
|
1811
1838
|
const compareExpressions = [];
|
|
1812
1839
|
const typeCastsCache = {};
|
|
1813
1840
|
for (const changeTableData of changeTables) {
|
|
1814
1841
|
compareExpressions.length = 0;
|
|
1815
|
-
await processTableChange(adapter, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, changeTableData, compareSql, compareExpressions, typeCastsCache,
|
|
1842
|
+
await processTableChange(adapter, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, changeTableData, compareSql, compareExpressions, typeCastsCache, decisionCtx);
|
|
1816
1843
|
if (compareExpressions.length) {
|
|
1817
1844
|
const { codeTable } = changeTableData;
|
|
1818
1845
|
const names = [];
|
|
@@ -1859,13 +1886,22 @@ const applyCompareSql = async (compareSql, adapter) => {
|
|
|
1859
1886
|
for (let i = 0; i < results.length; i++) if (!results[i]) compareSql.expressions[i].change();
|
|
1860
1887
|
}
|
|
1861
1888
|
};
|
|
1862
|
-
const applyCreateOrRenameTables = async (dbStructure, createTables, dropTables, changeTables, tableShapes, currentSchema, ast,
|
|
1889
|
+
const applyCreateOrRenameTables = async (dbStructure, createTables, dropTables, changeTables, tableShapes, currentSchema, ast, decisionCtx) => {
|
|
1863
1890
|
for (const codeTable of createTables) {
|
|
1864
1891
|
if (dropTables.length) {
|
|
1865
|
-
const
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1892
|
+
const dbTable = await decisionCtx.decider.createOrRename({
|
|
1893
|
+
kind: "table",
|
|
1894
|
+
target: {
|
|
1895
|
+
schema: codeTable.q.schema ?? currentSchema,
|
|
1896
|
+
name: codeTable.table
|
|
1897
|
+
},
|
|
1898
|
+
candidates: dropTables,
|
|
1899
|
+
candidateSource: (x) => decisionCtx.dbSource(x),
|
|
1900
|
+
name: codeTable.table,
|
|
1901
|
+
candidateName: (x) => x.name
|
|
1902
|
+
});
|
|
1903
|
+
if (dbTable) {
|
|
1904
|
+
dropTables.splice(dropTables.indexOf(dbTable), 1);
|
|
1869
1905
|
ast.push({
|
|
1870
1906
|
type: "renameType",
|
|
1871
1907
|
kind: "TABLE",
|
|
@@ -1935,8 +1971,8 @@ const makeTableShape = (table) => {
|
|
|
1935
1971
|
}
|
|
1936
1972
|
return shape;
|
|
1937
1973
|
};
|
|
1938
|
-
const processTableChange = async (adapter, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, changeTableData, compareSql, compareExpressions, typeCastsCache,
|
|
1939
|
-
await processColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, changeTableData, ast, currentSchema, compareSql, typeCastsCache,
|
|
1974
|
+
const processTableChange = async (adapter, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, changeTableData, compareSql, compareExpressions, typeCastsCache, decisionCtx) => {
|
|
1975
|
+
await processColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, changeTableData, ast, currentSchema, compareSql, typeCastsCache, decisionCtx);
|
|
1940
1976
|
processPrimaryKey(config, changeTableData);
|
|
1941
1977
|
processIndexesAndExcludes(config, changeTableData, ast, compareExpressions);
|
|
1942
1978
|
processChecks(ast, changeTableData, compareExpressions);
|
|
@@ -1957,7 +1993,7 @@ const defaults = {
|
|
|
1957
1993
|
connLimit: -1,
|
|
1958
1994
|
bypassRls: false
|
|
1959
1995
|
};
|
|
1960
|
-
const processRoles = async (ast, dbStructure, {
|
|
1996
|
+
const processRoles = async (ast, dbStructure, { internal: { roles } }, { decider }) => {
|
|
1961
1997
|
if (!dbStructure.roles || !roles) return;
|
|
1962
1998
|
const codeRoles = roles.map((role) => {
|
|
1963
1999
|
const { defaultPrivileges: _, ...roleWithoutPrivileges } = role;
|
|
@@ -1986,10 +2022,16 @@ const processRoles = async (ast, dbStructure, { verifying, internal: { roles } }
|
|
|
1986
2022
|
for (const codeRole of codeRoles) {
|
|
1987
2023
|
if (found.has(codeRole.name)) continue;
|
|
1988
2024
|
if (dropRoles.length) {
|
|
1989
|
-
const
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
dropRoles
|
|
2025
|
+
const dbRole = await decider.createOrRename({
|
|
2026
|
+
kind: "role",
|
|
2027
|
+
target: { name: codeRole.name },
|
|
2028
|
+
candidates: dropRoles,
|
|
2029
|
+
candidateSource: (x) => ({ name: x.name }),
|
|
2030
|
+
name: codeRole.name,
|
|
2031
|
+
candidateName: (x) => x.name
|
|
2032
|
+
});
|
|
2033
|
+
if (dbRole) {
|
|
2034
|
+
dropRoles.splice(dropRoles.indexOf(dbRole), 1);
|
|
1993
2035
|
ast.push(makeRenameOrChangeAst(dbRole, codeRole));
|
|
1994
2036
|
continue;
|
|
1995
2037
|
}
|
|
@@ -2785,6 +2827,74 @@ const normalizedViewName = (view, currentSchema) => {
|
|
|
2785
2827
|
return view.schemaName === currentSchema ? view.name : `${view.schemaName}.${view.name}`;
|
|
2786
2828
|
};
|
|
2787
2829
|
/**
|
|
2830
|
+
* Creates `MigrationDecisionCtx` for the `decider`, remembering the identities
|
|
2831
|
+
* of tables and enums in `dbStructure`, so it must be called for every
|
|
2832
|
+
* introspection before the generators change it.
|
|
2833
|
+
*/
|
|
2834
|
+
const makeMigrationDecisionCtx = (decider, dbStructure) => {
|
|
2835
|
+
const sources = /* @__PURE__ */ new WeakMap();
|
|
2836
|
+
for (const item of [...dbStructure.tables, ...dbStructure.enums]) sources.set(item, {
|
|
2837
|
+
schema: item.schemaName,
|
|
2838
|
+
name: item.name
|
|
2839
|
+
});
|
|
2840
|
+
return {
|
|
2841
|
+
decider,
|
|
2842
|
+
dbSource(item) {
|
|
2843
|
+
const source = sources.get(item);
|
|
2844
|
+
if (!source) throw new Error(`${item.schemaName}.${item.name} is missing in the introspected structure`);
|
|
2845
|
+
return source;
|
|
2846
|
+
}
|
|
2847
|
+
};
|
|
2848
|
+
};
|
|
2849
|
+
/**
|
|
2850
|
+
* Decider that asks the user in the terminal.
|
|
2851
|
+
*/
|
|
2852
|
+
const interactiveDecider = {
|
|
2853
|
+
async createOrRename({ kind, name, candidates, candidateName }) {
|
|
2854
|
+
const names = candidates.map(candidateName);
|
|
2855
|
+
let hintPos = name.length + 4;
|
|
2856
|
+
for (const from of names) {
|
|
2857
|
+
const value = from.length + 8 + name.length;
|
|
2858
|
+
if (value > hintPos) hintPos = value;
|
|
2859
|
+
}
|
|
2860
|
+
const renameMessage = `rename ${kind}`;
|
|
2861
|
+
const i = await promptSelect({
|
|
2862
|
+
message: `Create or rename ${colors.blueBold(name)} ${kind} from another ${kind}?`,
|
|
2863
|
+
options: [`${colors.greenBold("+")} ${name} ${colors.pale(`create ${kind}`.padStart(hintPos + renameMessage.length - name.length - 4, " "))}`, ...names.map((d) => `${colors.yellowBold("~")} ${d} ${colors.yellowBold("=>")} ${name} ${colors.pale(renameMessage.padStart(hintPos + renameMessage.length - d.length - name.length - 8, " "))}`)]
|
|
2864
|
+
});
|
|
2865
|
+
return i ? candidates[i - 1] : void 0;
|
|
2866
|
+
},
|
|
2867
|
+
async addOrRenameEnumValue({ enumName, value, candidates }) {
|
|
2868
|
+
const i = await promptSelect({
|
|
2869
|
+
message: `Add or rename ${colors.blueBold(value)} enum value in ${colors.blueBold(enumName)}?`,
|
|
2870
|
+
options: [`${colors.greenBold("+")} ${value} ${colors.pale("add enum value")}`, ...candidates.map((dropValue) => `${colors.yellowBold("~")} ${dropValue} ${colors.yellowBold("=>")} ${value} ${colors.pale("rename enum value")}`)]
|
|
2871
|
+
});
|
|
2872
|
+
return i ? candidates[i - 1] : void 0;
|
|
2873
|
+
},
|
|
2874
|
+
async recreateColumn({ tableName, columnName, fromType, toType }) {
|
|
2875
|
+
return !await promptSelect({
|
|
2876
|
+
message: `Cannot cast type of ${tableName}'s column ${columnName} from ${fromType} to ${toType}`,
|
|
2877
|
+
options: [`${colors.yellowBold(`-/+`)} recreate the column, existing data will be ${colors.red("lost")}`, `write migration manually`]
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
2881
|
+
/**
|
|
2882
|
+
* Decider for re-generating a migration after applying it: any remaining
|
|
2883
|
+
* question means the migration did not bring the database to the code state,
|
|
2884
|
+
* so it aborts instead of asking.
|
|
2885
|
+
*/
|
|
2886
|
+
const verifyingDecider = {
|
|
2887
|
+
createOrRename() {
|
|
2888
|
+
throw new AbortSignal();
|
|
2889
|
+
},
|
|
2890
|
+
addOrRenameEnumValue() {
|
|
2891
|
+
throw new AbortSignal();
|
|
2892
|
+
},
|
|
2893
|
+
recreateColumn() {
|
|
2894
|
+
throw new AbortSignal();
|
|
2895
|
+
}
|
|
2896
|
+
};
|
|
2897
|
+
/**
|
|
2788
2898
|
* This is needed to compare SQLs of table expressions.
|
|
2789
2899
|
* Need to exclude table columns of pending types, such as enums or domains,
|
|
2790
2900
|
* that aren't created yet from the SQL comparison.
|
|
@@ -2800,16 +2910,17 @@ var PendingDbTypes = class {
|
|
|
2800
2910
|
};
|
|
2801
2911
|
const composeMigration = async (adapter, config, ast, dbStructure, params) => {
|
|
2802
2912
|
const { structureToAstCtx, currentSchema } = params;
|
|
2803
|
-
|
|
2913
|
+
const decisionCtx = makeMigrationDecisionCtx(params.decider, dbStructure);
|
|
2914
|
+
await processRoles(ast, dbStructure, params, decisionCtx);
|
|
2804
2915
|
processDefaultPrivileges(ast, dbStructure, params);
|
|
2805
2916
|
processGrants(ast, dbStructure, params);
|
|
2806
2917
|
const domainsMap = makeDomainsMap(structureToAstCtx, dbStructure);
|
|
2807
|
-
await processSchemas(ast, dbStructure, params);
|
|
2918
|
+
await processSchemas(ast, dbStructure, params, decisionCtx);
|
|
2808
2919
|
processExtensions(ast, dbStructure, params);
|
|
2809
2920
|
const pendingDbTypes = new PendingDbTypes();
|
|
2810
2921
|
await processDomains(ast, adapter, domainsMap, dbStructure, params, pendingDbTypes);
|
|
2811
|
-
await processEnums(ast, dbStructure, params, pendingDbTypes);
|
|
2812
|
-
await processTables(ast, domainsMap, adapter, dbStructure, config, params, pendingDbTypes);
|
|
2922
|
+
await processEnums(ast, dbStructure, params, decisionCtx, pendingDbTypes);
|
|
2923
|
+
await processTables(ast, domainsMap, adapter, dbStructure, config, params, decisionCtx, pendingDbTypes);
|
|
2813
2924
|
await processViews(ast, adapter, dbStructure, params);
|
|
2814
2925
|
await processMaterializedViews(ast, adapter, dbStructure, params);
|
|
2815
2926
|
return astToMigration(currentSchema, config, ast);
|
|
@@ -2834,9 +2945,11 @@ const verifyMigration = async (adapter, config, migrationCode, generateMigration
|
|
|
2834
2945
|
roles,
|
|
2835
2946
|
...structureParams
|
|
2836
2947
|
});
|
|
2837
|
-
generateMigrationParams.verifying = true;
|
|
2838
2948
|
try {
|
|
2839
|
-
code = await composeMigration(trx, config, [], dbStructure,
|
|
2949
|
+
code = await composeMigration(trx, config, [], dbStructure, {
|
|
2950
|
+
...generateMigrationParams,
|
|
2951
|
+
decider: verifyingDecider
|
|
2952
|
+
});
|
|
2840
2953
|
} catch (err) {
|
|
2841
2954
|
if (err instanceof AbortSignal) {
|
|
2842
2955
|
code = false;
|
|
@@ -2851,6 +2964,222 @@ const verifyMigration = async (adapter, config, migrationCode, generateMigration
|
|
|
2851
2964
|
}
|
|
2852
2965
|
return code;
|
|
2853
2966
|
};
|
|
2967
|
+
const pathLengths = {
|
|
2968
|
+
schema: 1,
|
|
2969
|
+
role: 1,
|
|
2970
|
+
enum: 2,
|
|
2971
|
+
table: 2,
|
|
2972
|
+
column: 3,
|
|
2973
|
+
"enum-value": 3
|
|
2974
|
+
};
|
|
2975
|
+
/**
|
|
2976
|
+
* Parses arguments of the generate command, separating the flags that answer
|
|
2977
|
+
* the generator questions from the positional arguments.
|
|
2978
|
+
*/
|
|
2979
|
+
const parseGenerateArgs = (args) => {
|
|
2980
|
+
const positional = [];
|
|
2981
|
+
let nonInteractive = false;
|
|
2982
|
+
const answers = [];
|
|
2983
|
+
for (let i = 0; i < args.length; i++) {
|
|
2984
|
+
const arg = args[i];
|
|
2985
|
+
if (arg === "--non-interactive") {
|
|
2986
|
+
nonInteractive = true;
|
|
2987
|
+
continue;
|
|
2988
|
+
}
|
|
2989
|
+
if (arg === "--transaction") {
|
|
2990
|
+
i++;
|
|
2991
|
+
continue;
|
|
2992
|
+
}
|
|
2993
|
+
if (arg.startsWith("--transaction=")) continue;
|
|
2994
|
+
if (!arg.startsWith("--")) {
|
|
2995
|
+
positional.push(arg);
|
|
2996
|
+
continue;
|
|
2997
|
+
}
|
|
2998
|
+
const match = arg.match(/^--(rename|create|recreate)(?:=(.*))?$/s);
|
|
2999
|
+
if (!match) throw new RakeDbError(`Unknown flag ${arg}`);
|
|
3000
|
+
const action = match[1];
|
|
3001
|
+
const value = match[2] ?? args[++i];
|
|
3002
|
+
if (value === void 0) throw new RakeDbError(`--${action} requires a value`);
|
|
3003
|
+
answers.push(parseAnswer(action, value));
|
|
3004
|
+
}
|
|
3005
|
+
checkConflictingAnswers(answers);
|
|
3006
|
+
return {
|
|
3007
|
+
positional,
|
|
3008
|
+
nonInteractive,
|
|
3009
|
+
answers
|
|
3010
|
+
};
|
|
3011
|
+
};
|
|
3012
|
+
const parseAnswer = (action, value) => {
|
|
3013
|
+
const flag = `--${action} ${value}`;
|
|
3014
|
+
const invalid = (reason) => new RakeDbError(`Invalid ${flag}: ${reason}`);
|
|
3015
|
+
const colon = value.indexOf(":");
|
|
3016
|
+
const kind = value.slice(0, colon);
|
|
3017
|
+
if (colon === -1 || !Object.hasOwn(pathLengths, kind)) throw invalid(`expected one of ${Object.keys(pathLengths).join(", ")} followed by a colon`);
|
|
3018
|
+
if (action === "recreate" && kind !== "column") throw invalid("only a column can be recreated");
|
|
3019
|
+
const length = pathLengths[kind];
|
|
3020
|
+
const readPath = (start) => {
|
|
3021
|
+
const { path, end } = parsePath(value, start, invalid);
|
|
3022
|
+
if (path.length !== length) throw invalid(`${kind} must be given by ${length} dot-separated name${length === 1 ? "" : "s"}`);
|
|
3023
|
+
return {
|
|
3024
|
+
path,
|
|
3025
|
+
end
|
|
3026
|
+
};
|
|
3027
|
+
};
|
|
3028
|
+
const first = readPath(colon + 1);
|
|
3029
|
+
if (action === "create") {
|
|
3030
|
+
if (first.end !== value.length) throw invalid("unexpected text after the name");
|
|
3031
|
+
return {
|
|
3032
|
+
flag,
|
|
3033
|
+
action,
|
|
3034
|
+
kind,
|
|
3035
|
+
target: first.path
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
if (value[first.end] !== "=") throw invalid("expected the old name and the new name separated by \"=\"");
|
|
3039
|
+
const second = readPath(first.end + 1);
|
|
3040
|
+
if (second.end !== value.length) throw invalid("unexpected text after the name");
|
|
3041
|
+
return {
|
|
3042
|
+
flag,
|
|
3043
|
+
action,
|
|
3044
|
+
kind,
|
|
3045
|
+
source: first.path,
|
|
3046
|
+
target: second.path
|
|
3047
|
+
};
|
|
3048
|
+
};
|
|
3049
|
+
const parsePath = (value, start, invalid) => {
|
|
3050
|
+
const path = [];
|
|
3051
|
+
let i = start;
|
|
3052
|
+
for (;;) {
|
|
3053
|
+
let segment = "";
|
|
3054
|
+
if (value[i] === "\"") {
|
|
3055
|
+
for (i++;; i++) {
|
|
3056
|
+
if (i === value.length) throw invalid("unterminated quoted name");
|
|
3057
|
+
if (value[i] === "\"") {
|
|
3058
|
+
if (value[i + 1] !== "\"") break;
|
|
3059
|
+
i++;
|
|
3060
|
+
}
|
|
3061
|
+
segment += value[i];
|
|
3062
|
+
}
|
|
3063
|
+
i++;
|
|
3064
|
+
} else {
|
|
3065
|
+
while (i < value.length && !".=\"".includes(value[i])) segment += value[i++];
|
|
3066
|
+
if (!segment) throw invalid("expected a name");
|
|
3067
|
+
}
|
|
3068
|
+
path.push(segment);
|
|
3069
|
+
if (value[i] !== ".") return {
|
|
3070
|
+
path,
|
|
3071
|
+
end: i
|
|
3072
|
+
};
|
|
3073
|
+
i++;
|
|
3074
|
+
}
|
|
3075
|
+
};
|
|
3076
|
+
const answerGroup = (answer) => answer.action === "recreate" ? "recreate" : "createOrRename";
|
|
3077
|
+
const answerKey = (group, kind, path) => `${group} ${kind}:${formatPath(path)}`;
|
|
3078
|
+
const checkConflictingAnswers = (answers) => {
|
|
3079
|
+
const targets = /* @__PURE__ */ new Map();
|
|
3080
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3081
|
+
for (const answer of answers) {
|
|
3082
|
+
const group = answerGroup(answer);
|
|
3083
|
+
const conflict = (other) => new RakeDbError(`${answer.flag} conflicts with ${other.flag}`);
|
|
3084
|
+
const targetKey = answerKey(group, answer.kind, answer.target);
|
|
3085
|
+
const sameTarget = targets.get(targetKey);
|
|
3086
|
+
if (sameTarget) throw conflict(sameTarget);
|
|
3087
|
+
targets.set(targetKey, answer);
|
|
3088
|
+
if (answer.action !== "create") {
|
|
3089
|
+
const sourceKey = answerKey(group, answer.kind, answer.source);
|
|
3090
|
+
const sameSource = sources.get(sourceKey);
|
|
3091
|
+
if (sameSource) throw conflict(sameSource);
|
|
3092
|
+
sources.set(sourceKey, answer);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
};
|
|
3096
|
+
/**
|
|
3097
|
+
* Formats database names as they are given in the answer flags:
|
|
3098
|
+
* separated by dots, quoting the names that cannot be read otherwise.
|
|
3099
|
+
*/
|
|
3100
|
+
const formatPath = (path) => path.map((name) => /^[^\s."=]+$/.test(name) ? name : `"${name.replaceAll("\"", "\"\"")}"`).join(".");
|
|
3101
|
+
const itemPath = ({ schema, table, name }) => [
|
|
3102
|
+
...schema === void 0 ? [] : [schema],
|
|
3103
|
+
...table === void 0 ? [] : [table],
|
|
3104
|
+
name
|
|
3105
|
+
];
|
|
3106
|
+
const samePath = (a, b) => a.length === b.length && a.every((name, i) => name === b[i]);
|
|
3107
|
+
/**
|
|
3108
|
+
* Creates an `AnswersDecider` for the `answers`.
|
|
3109
|
+
* Without the `ask` decider, a question without an answer is collected,
|
|
3110
|
+
* and the generation goes on as if the item was created or recreated,
|
|
3111
|
+
* so that the following questions are collected as well.
|
|
3112
|
+
*/
|
|
3113
|
+
const makeAnswersDecider = (answers, ask) => {
|
|
3114
|
+
const answerMap = /* @__PURE__ */ new Map();
|
|
3115
|
+
for (const answer of answers) answerMap.set(answerKey(answerGroup(answer), answer.kind, answer.target), answer);
|
|
3116
|
+
const used = /* @__PURE__ */ new Set();
|
|
3117
|
+
const takeAnswer = (group, kind, target) => {
|
|
3118
|
+
const answer = answerMap.get(answerKey(group, kind, target));
|
|
3119
|
+
if (answer) used.add(answer);
|
|
3120
|
+
return answer;
|
|
3121
|
+
};
|
|
3122
|
+
const errors = [];
|
|
3123
|
+
const questions = [];
|
|
3124
|
+
const addQuestion = (question, options) => {
|
|
3125
|
+
questions.push([question, ...options.map((option) => ` ${option}`)].join("\n"));
|
|
3126
|
+
};
|
|
3127
|
+
const findCandidate = (answer, kind, target, candidates, candidatePath) => {
|
|
3128
|
+
const candidate = candidates.find((candidate) => samePath(candidatePath(candidate), answer.source));
|
|
3129
|
+
if (candidate === void 0) errors.push(`${answer.flag}: ${formatPath(answer.source)} is not among the ${kind}s that can be renamed to ${formatPath(target)}: ${candidates.map((candidate) => formatPath(candidatePath(candidate))).join(", ")}`);
|
|
3130
|
+
return candidate;
|
|
3131
|
+
};
|
|
3132
|
+
return {
|
|
3133
|
+
async createOrRename(question) {
|
|
3134
|
+
const { kind, candidates, candidateSource } = question;
|
|
3135
|
+
const target = itemPath(question.target);
|
|
3136
|
+
const candidatePath = (candidate) => itemPath(candidateSource(candidate));
|
|
3137
|
+
const answer = takeAnswer("createOrRename", kind, target);
|
|
3138
|
+
if (answer) return answer.action === "rename" ? findCandidate(answer, kind, target, candidates, candidatePath) : void 0;
|
|
3139
|
+
if (ask) return ask.createOrRename(question);
|
|
3140
|
+
addQuestion(`Create or rename ${kind} ${formatPath(target)}?`, [formatFlag("create", kind, target), ...candidates.map((candidate) => formatFlag("rename", kind, candidatePath(candidate), target))]);
|
|
3141
|
+
},
|
|
3142
|
+
async addOrRenameEnumValue(question) {
|
|
3143
|
+
const { value, candidates } = question;
|
|
3144
|
+
const kind = "enum-value";
|
|
3145
|
+
const target = [...itemPath(question.target), value];
|
|
3146
|
+
const source = itemPath(question.source);
|
|
3147
|
+
const candidatePath = (candidate) => [...source, candidate];
|
|
3148
|
+
const answer = takeAnswer("createOrRename", kind, target);
|
|
3149
|
+
if (answer) return answer.action === "rename" ? findCandidate(answer, kind, target, candidates, candidatePath) : void 0;
|
|
3150
|
+
if (ask) return ask.addOrRenameEnumValue(question);
|
|
3151
|
+
addQuestion(`Add or rename enum value ${formatPath(target)}?`, [formatFlag("create", kind, target), ...candidates.map((candidate) => formatFlag("rename", kind, candidatePath(candidate), target))]);
|
|
3152
|
+
},
|
|
3153
|
+
async recreateColumn(question) {
|
|
3154
|
+
const kind = "column";
|
|
3155
|
+
const source = itemPath(question.source);
|
|
3156
|
+
const target = itemPath(question.target);
|
|
3157
|
+
const answer = takeAnswer("recreate", kind, target);
|
|
3158
|
+
if (answer && answer.action === "recreate") {
|
|
3159
|
+
if (!samePath(source, answer.source)) errors.push(`${answer.flag}: the column to recreate is ${formatPath(source)}`);
|
|
3160
|
+
return true;
|
|
3161
|
+
}
|
|
3162
|
+
if (ask) return ask.recreateColumn(question);
|
|
3163
|
+
addQuestion(`Cannot cast column ${formatPath(source)} from ${question.fromType} to ${question.toType}. Recreate it (existing data will be lost), or write the migration manually?`, [formatFlag("recreate", kind, source, target)]);
|
|
3164
|
+
return true;
|
|
3165
|
+
},
|
|
3166
|
+
problems() {
|
|
3167
|
+
const sections = [];
|
|
3168
|
+
if (errors.length) sections.push(errors.join("\n"));
|
|
3169
|
+
if (questions.length) sections.push("Run the command again with one of the suggested flags for each question, keeping the answer flags given before:", ...questions, "More questions may come up after these are answered.");
|
|
3170
|
+
else {
|
|
3171
|
+
const unused = answers.filter((answer) => answer.action === "rename" && !used.has(answer));
|
|
3172
|
+
if (unused.length) sections.push(`These renames do not match any question:\n${unused.map((answer) => answer.flag).join("\n")}`);
|
|
3173
|
+
}
|
|
3174
|
+
return sections.length ? `Cannot generate the migration.\n\n${sections.join("\n\n")}` : void 0;
|
|
3175
|
+
}
|
|
3176
|
+
};
|
|
3177
|
+
};
|
|
3178
|
+
const formatFlag = (action, kind, path, target) => {
|
|
3179
|
+
const value = `${kind}:${formatPath(path)}${target ? `=${formatPath(target)}` : ""}`;
|
|
3180
|
+
return `--${action} ${shellQuote(value)}`;
|
|
3181
|
+
};
|
|
3182
|
+
const shellQuote = (value) => /^[\w.:=@%+,/-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
2854
3183
|
const report = (ast, config, currentSchema) => {
|
|
2855
3184
|
if (!config.logger) return;
|
|
2856
3185
|
const code = [];
|
|
@@ -3140,12 +3469,14 @@ const generate = async (adapters, config, args, afterPull) => {
|
|
|
3140
3469
|
if (!dbPath || !getTableFactoryConfig(config)) throw invalidConfig(config);
|
|
3141
3470
|
if (!adapters.length) throw new Error(`Database options must not be empty`);
|
|
3142
3471
|
if (!dbPath.endsWith(".ts")) dbPath += ".ts";
|
|
3143
|
-
|
|
3472
|
+
const { positional, nonInteractive, answers } = parseGenerateArgs(args);
|
|
3473
|
+
let migrationName = positional[0] ?? "generated";
|
|
3144
3474
|
let up;
|
|
3145
3475
|
if (migrationName === "up") {
|
|
3146
3476
|
up = true;
|
|
3147
3477
|
migrationName = "generated";
|
|
3148
|
-
} else up =
|
|
3478
|
+
} else up = positional[1] === "up";
|
|
3479
|
+
const decider = makeAnswersDecider(answers, nonInteractive || !process.stdin.isTTY ? void 0 : interactiveDecider);
|
|
3149
3480
|
if (afterPull) adapters = [afterPull.adapter];
|
|
3150
3481
|
const db = await getDbFromConfig(config, dbPath);
|
|
3151
3482
|
const { columnTypes, internal } = db.$qb;
|
|
@@ -3170,7 +3501,8 @@ const generate = async (adapters, config, args, afterPull) => {
|
|
|
3170
3501
|
...internal,
|
|
3171
3502
|
generatorIgnore,
|
|
3172
3503
|
grants: effectiveGrants
|
|
3173
|
-
}
|
|
3504
|
+
},
|
|
3505
|
+
decider
|
|
3174
3506
|
};
|
|
3175
3507
|
const ast = [];
|
|
3176
3508
|
let migrationCode;
|
|
@@ -3183,6 +3515,11 @@ const generate = async (adapters, config, args, afterPull) => {
|
|
|
3183
3515
|
}
|
|
3184
3516
|
throw err;
|
|
3185
3517
|
}
|
|
3518
|
+
const problems = decider.problems();
|
|
3519
|
+
if (problems) {
|
|
3520
|
+
if (!afterPull) await closeAdapters(adapters);
|
|
3521
|
+
throw new RakeDbError(problems);
|
|
3522
|
+
}
|
|
3186
3523
|
if (migrationCode && !afterPull) {
|
|
3187
3524
|
const result = await verifyMigration(adapter, config, migrationCode, generateMigrationParams, rolesDbStructureParam, structureParams);
|
|
3188
3525
|
if (result !== void 0) throw new Error(`Failed to verify generated migration: some of database changes were not applied properly. This is a bug, please open an issue, attach the following migration code:\n${migrationCode}${result === false ? "" : `\nAfter applying:\n${result}`}`);
|
|
@@ -3908,7 +4245,11 @@ const patchRakeDb$1 = () => {
|
|
|
3908
4245
|
"no arguments": "\"generated\" is a default file name",
|
|
3909
4246
|
"migration-name": "set migration file name",
|
|
3910
4247
|
up: "auto-apply migration",
|
|
3911
|
-
"migration-name up": "with a custom name and apply it"
|
|
4248
|
+
"migration-name up": "with a custom name and apply it",
|
|
4249
|
+
"--non-interactive": "report unanswered questions instead of asking",
|
|
4250
|
+
"--rename kind:old=new": "answer to rename an item",
|
|
4251
|
+
"--create kind:name": "answer to create an item",
|
|
4252
|
+
"--recreate column:old=new": "answer to recreate a column, losing data"
|
|
3912
4253
|
},
|
|
3913
4254
|
helpAfter: "reset"
|
|
3914
4255
|
};
|