orchid-orm 1.78.11 → 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 +2 -2
package/dist/migrations/index.js
CHANGED
|
@@ -26,137 +26,23 @@ node_path = __toESM(node_path);
|
|
|
26
26
|
let node_url = require("node:url");
|
|
27
27
|
let node_fs_promises = require("node:fs/promises");
|
|
28
28
|
node_fs_promises = __toESM(node_fs_promises);
|
|
29
|
-
const
|
|
30
|
-
return (0, pqb_internal.sqlToRawSql)(viewDataToQuerySql(viewData, viewName));
|
|
31
|
-
};
|
|
32
|
-
const viewDataToQuerySql = (viewData, viewName) => {
|
|
33
|
-
if (viewData.query) return (0, pqb_internal.queryToSql)(viewData.query);
|
|
34
|
-
if (viewData.sql !== void 0) return (0, pqb_internal.rawSqlToSql)(viewData.sql);
|
|
35
|
-
throw new Error(`Either sql or query is required for view ${viewName}`);
|
|
36
|
-
};
|
|
37
|
-
const compareSqlExpressions = async (expressions, adapter) => {
|
|
38
|
-
if (!expressions.length) return;
|
|
39
|
-
let id = 1;
|
|
40
|
-
for (const { source, compare, handle } of expressions) {
|
|
41
|
-
const viewName = `orchidTmpView${id++}`;
|
|
42
|
-
const values = [];
|
|
43
|
-
const combinedQueries = [
|
|
44
|
-
`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}` : ""})`,
|
|
45
|
-
`SELECT pg_get_viewdef('${viewName}') v`,
|
|
46
|
-
`DROP VIEW ${viewName}`
|
|
47
|
-
].join("; ");
|
|
48
|
-
const query = () => adapter.query(combinedQueries, values);
|
|
49
|
-
const result = await (adapter.isInTransaction() ? adapter.savepoint(viewName, void 0, query) : query()).then((res) => res[1], async (err) => {
|
|
50
|
-
if (typeof err === "object" && (0, pqb_internal.getDriverErrorCode)(err) !== "42704") throw err;
|
|
51
|
-
});
|
|
52
|
-
if (!result) {
|
|
53
|
-
handle();
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
handle(compareSqlExpressionResult(result.rows[0].v, compare[0].inCode));
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
const compareViewsExpressions = async (adapter, compare) => {
|
|
60
|
-
if (!compare.length) return;
|
|
61
|
-
const queries = [];
|
|
62
|
-
let id = 1;
|
|
63
|
-
compare.forEach(({ inCode, ast }, i) => {
|
|
64
|
-
const viewName = `orchidTmpView${id++}ForViews`;
|
|
65
|
-
queries.push({ batch: [
|
|
66
|
-
`SAVEPOINT "${viewName}S"`,
|
|
67
|
-
`CREATE TEMPORARY${"recursive" in ast.options && ast.options.recursive ? " RECURSIVE" : ""} VIEW "${viewName}" (${ast.options.columns?.map((column) => `"${column}"`).join(", ")}) AS (${inCode})`,
|
|
68
|
-
`SELECT ${i} i, '${viewName}' v, pg_get_viewdef('"${viewName}"') sql`,
|
|
69
|
-
`DROP VIEW "${viewName}"`,
|
|
70
|
-
`RELEASE SAVEPOINT "${viewName}S"`
|
|
71
|
-
] });
|
|
72
|
-
});
|
|
73
|
-
let results;
|
|
74
|
-
try {
|
|
75
|
-
const sql = queries.flatMap((q) => q.batch).join(";");
|
|
76
|
-
const query = () => adapter.query(sql, []);
|
|
77
|
-
results = await (adapter.isInTransaction() ? adapter.savepoint("orchidOrmGeneratorViews", void 0, query) : query());
|
|
78
|
-
} catch {
|
|
79
|
-
results = (await Promise.all(queries.map(async ({ batch: queries }, i) => {
|
|
80
|
-
const sql = queries.join(";");
|
|
81
|
-
const query = () => adapter.query(sql, []);
|
|
82
|
-
return await (adapter.isInTransaction() ? adapter.savepoint(`orchidOrmGeneratorViews${i}`, void 0, query) : query()).catch((err) => {
|
|
83
|
-
if (typeof err === "object") {
|
|
84
|
-
const code = (0, pqb_internal.getDriverErrorCode)(err);
|
|
85
|
-
if (code === "42703" || code === "42704" || code === "42P01") return [];
|
|
86
|
-
}
|
|
87
|
-
throw err;
|
|
88
|
-
});
|
|
89
|
-
}))).flat();
|
|
90
|
-
}
|
|
91
|
-
const handled = /* @__PURE__ */ new Set();
|
|
92
|
-
for (const result of results) for (const row of result.rows) if ("sql" in row) {
|
|
93
|
-
const { i, v } = row;
|
|
94
|
-
const cmp = compare[i];
|
|
95
|
-
const hasQuote = !!cmp.inDb.match(/\w*WITH RECURSIVE "/);
|
|
96
|
-
if (row.sql.replaceAll(hasQuote ? v : `"${v}"`, cmp.ast.name) !== cmp.inDb) cmp.onNotEqual();
|
|
97
|
-
handled.add(i);
|
|
98
|
-
}
|
|
99
|
-
compare.forEach((cmp, i) => {
|
|
100
|
-
if (!handled.has(i)) cmp.onNotEqual();
|
|
101
|
-
});
|
|
102
|
-
};
|
|
103
|
-
const compareSqlExpressionResult = (resultSql, inCode) => {
|
|
104
|
-
let pos = 7;
|
|
105
|
-
const rgx = /\s+AS\s+"\*(inDb-\d+|inCode-\d+-\d+)\*",?/g;
|
|
106
|
-
let match;
|
|
107
|
-
let inDb = "";
|
|
108
|
-
let codeI = 0;
|
|
109
|
-
const matches = inCode.map(() => true);
|
|
110
|
-
while (match = rgx.exec(resultSql)) {
|
|
111
|
-
const sql = resultSql.slice(pos, rgx.lastIndex - match[0].length).trim();
|
|
112
|
-
if (match[1].split("-").length === 2) {
|
|
113
|
-
inDb = sql;
|
|
114
|
-
codeI = 0;
|
|
115
|
-
} else {
|
|
116
|
-
if (inDb !== sql && !(inDb.startsWith("(") && inDb.endsWith(")") && inDb.slice(1, -1) === sql) && !(sql.startsWith("(") && sql.endsWith(")") && sql.slice(1, -1) === inDb)) matches[codeI] = false;
|
|
117
|
-
codeI++;
|
|
118
|
-
}
|
|
119
|
-
pos = rgx.lastIndex;
|
|
120
|
-
}
|
|
121
|
-
const firstMatching = matches.indexOf(true);
|
|
122
|
-
return firstMatching === -1 ? void 0 : firstMatching;
|
|
123
|
-
};
|
|
124
|
-
const promptCreateOrRename = (kind, name, drop, verifying) => {
|
|
125
|
-
if (verifying) throw new AbortSignal();
|
|
126
|
-
let hintPos = name.length + 4;
|
|
127
|
-
for (const from of drop) {
|
|
128
|
-
const value = from.length + 8 + name.length;
|
|
129
|
-
if (value > hintPos) hintPos = value;
|
|
130
|
-
}
|
|
131
|
-
let max = 0;
|
|
132
|
-
const add = name.length + 3;
|
|
133
|
-
for (const name of drop) if (name.length + add > max) max = name.length + add;
|
|
134
|
-
const renameMessage = `rename ${kind}`;
|
|
135
|
-
return (0, rake_db.promptSelect)({
|
|
136
|
-
message: `Create or rename ${pqb_internal.colors.blueBold(name)} ${kind} from another ${kind}?`,
|
|
137
|
-
options: [`${pqb_internal.colors.greenBold("+")} ${name} ${pqb_internal.colors.pale(`create ${kind}`.padStart(hintPos + renameMessage.length - name.length - 4, " "))}`, ...drop.map((d) => `${pqb_internal.colors.yellowBold("~")} ${d} ${pqb_internal.colors.yellowBold("=>")} ${name} ${pqb_internal.colors.pale(renameMessage.padStart(hintPos + renameMessage.length - d.length - name.length - 8, " "))}`)]
|
|
138
|
-
});
|
|
139
|
-
};
|
|
140
|
-
const checkForColumnAddOrDrop = (shape, key) => {
|
|
141
|
-
const item = shape[key];
|
|
142
|
-
if (item) return item && (Array.isArray(item) || item.type === "add" || item.type === "drop");
|
|
143
|
-
for (const k in shape) {
|
|
144
|
-
const item = shape[k];
|
|
145
|
-
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;
|
|
146
|
-
}
|
|
147
|
-
return false;
|
|
148
|
-
};
|
|
149
|
-
const processSchemas = async (ast, dbStructure, { codeItems: { schemas }, verifying, internal: { generatorIgnore }, currentSchema }) => {
|
|
29
|
+
const processSchemas = async (ast, dbStructure, { codeItems: { schemas }, internal: { generatorIgnore }, currentSchema }, { decider }) => {
|
|
150
30
|
const createSchemas = [];
|
|
151
31
|
const dropSchemas = [];
|
|
152
32
|
for (const schema of schemas) if (!dbStructure.schemas.includes(schema)) createSchemas.push(schema);
|
|
153
33
|
for (const schema of dbStructure.schemas) if (!schemas.has(schema) && schema !== "public" && schema !== currentSchema && !generatorIgnore?.schemas?.includes(schema)) dropSchemas.push(schema);
|
|
154
34
|
for (const schema of createSchemas) {
|
|
155
35
|
if (dropSchemas.length) {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
dropSchemas
|
|
36
|
+
const from = await decider.createOrRename({
|
|
37
|
+
kind: "schema",
|
|
38
|
+
target: { name: schema },
|
|
39
|
+
candidates: dropSchemas,
|
|
40
|
+
candidateSource: (x) => ({ name: x }),
|
|
41
|
+
name: schema,
|
|
42
|
+
candidateName: (x) => x
|
|
43
|
+
});
|
|
44
|
+
if (from) {
|
|
45
|
+
dropSchemas.splice(dropSchemas.indexOf(from), 1);
|
|
160
46
|
const views = dbStructure.views || [];
|
|
161
47
|
renameSchemaInStructures(dbStructure.tables, from, schema);
|
|
162
48
|
renameSchemaInStructures(views, from, schema);
|
|
@@ -228,12 +114,12 @@ const processExtensions = (ast, dbStructure, { currentSchema, internal: { extens
|
|
|
228
114
|
...ext
|
|
229
115
|
})));
|
|
230
116
|
};
|
|
231
|
-
const processColumns = async (adapter, config, structureToAstCtx, dbStructure, domainsMap, changeTableData, ast, currentSchema, compareSql, typeCastsCache,
|
|
117
|
+
const processColumns = async (adapter, config, structureToAstCtx, dbStructure, domainsMap, changeTableData, ast, currentSchema, compareSql, typeCastsCache, decisionCtx) => {
|
|
232
118
|
const { dbTable } = changeTableData;
|
|
233
119
|
const dbColumns = Object.fromEntries(dbTable.columns.map((column) => [column.name, column]));
|
|
234
120
|
const { columnsToAdd, columnsToDrop, columnsToChange } = groupColumns(structureToAstCtx, dbStructure, domainsMap, dbColumns, changeTableData);
|
|
235
|
-
await addOrRenameColumns(config, dbStructure, changeTableData, columnsToAdd, columnsToDrop, columnsToChange,
|
|
236
|
-
await changeColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, dbColumns, columnsToChange, compareSql, changeTableData, typeCastsCache,
|
|
121
|
+
await addOrRenameColumns(config, dbStructure, changeTableData, columnsToAdd, columnsToDrop, columnsToChange, decisionCtx);
|
|
122
|
+
await changeColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, dbColumns, columnsToChange, compareSql, changeTableData, typeCastsCache, decisionCtx);
|
|
237
123
|
dropColumns(changeTableData, columnsToDrop);
|
|
238
124
|
};
|
|
239
125
|
const groupColumns = (structureToAstCtx, dbStructure, domainsMap, dbColumns, changeTableData) => {
|
|
@@ -273,14 +159,21 @@ const groupColumns = (structureToAstCtx, dbStructure, domainsMap, dbColumns, cha
|
|
|
273
159
|
columnsToChange
|
|
274
160
|
};
|
|
275
161
|
};
|
|
276
|
-
const addOrRenameColumns = async (config, dbStructure,
|
|
162
|
+
const addOrRenameColumns = async (config, dbStructure, changeTableData, columnsToAdd, columnsToDrop, columnsToChange, decisionCtx) => {
|
|
163
|
+
const { dbTableData, schema, changeTableAst: { name: tableName, shape } } = changeTableData;
|
|
277
164
|
for (const { key, column } of columnsToAdd) {
|
|
278
165
|
if (columnsToDrop.length) {
|
|
279
166
|
const codeName = column.data.name ?? key;
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
columnsToDrop
|
|
167
|
+
const drop = await decisionCtx.decider.createOrRename({
|
|
168
|
+
kind: "column",
|
|
169
|
+
target: codeColumnTarget(changeTableData, codeName),
|
|
170
|
+
candidates: columnsToDrop,
|
|
171
|
+
candidateSource: (x) => dbColumnSource(decisionCtx, changeTableData, x.column.data.name ?? x.key),
|
|
172
|
+
name: codeName,
|
|
173
|
+
candidateName: (x) => x.key
|
|
174
|
+
});
|
|
175
|
+
if (drop) {
|
|
176
|
+
columnsToDrop.splice(columnsToDrop.indexOf(drop), 1);
|
|
284
177
|
const from = drop.column.data.name ?? drop.key;
|
|
285
178
|
columnsToChange.set(from, {
|
|
286
179
|
key,
|
|
@@ -311,11 +204,11 @@ const dropColumns = ({ changeTableAst: { shape } }, columnsToDrop) => {
|
|
|
311
204
|
item: column
|
|
312
205
|
};
|
|
313
206
|
};
|
|
314
|
-
const changeColumns = async (adapter, config, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, dbColumns, columnsToChange, compareSql, changeTableData, typeCastsCache,
|
|
207
|
+
const changeColumns = async (adapter, config, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, dbColumns, columnsToChange, compareSql, changeTableData, typeCastsCache, decisionCtx) => {
|
|
315
208
|
for (const [key, { key: codeKey, dbName, column: codeColumn }] of columnsToChange) {
|
|
316
209
|
const dbColumnStructure = dbColumns[dbName];
|
|
317
210
|
const dbColumn = (0, rake_db.instantiateDbColumn)(structureToAstCtx, dbStructure, domainsMap, dbColumnStructure);
|
|
318
|
-
const action = await compareColumns(adapter, domainsMap, ast, currentSchema, compareSql, changeTableData, typeCastsCache,
|
|
211
|
+
const action = await compareColumns(adapter, domainsMap, ast, currentSchema, compareSql, changeTableData, typeCastsCache, decisionCtx, key, dbName, codeColumn.data.name ?? codeKey, dbColumn, codeColumn);
|
|
319
212
|
if (action === "change") changeColumn(changeTableData, key, dbName, dbColumn, codeColumn);
|
|
320
213
|
else if (action === "recreate") changeTableData.changeTableAst.shape[key] = [{
|
|
321
214
|
type: "drop",
|
|
@@ -333,7 +226,7 @@ const changeColumns = async (adapter, config, structureToAstCtx, dbStructure, do
|
|
|
333
226
|
}
|
|
334
227
|
}
|
|
335
228
|
};
|
|
336
|
-
const compareColumns = async (adapter, domainsMap, ast, currentSchema, compareSql, changeTableData, typeCastsCache,
|
|
229
|
+
const compareColumns = async (adapter, domainsMap, ast, currentSchema, compareSql, changeTableData, typeCastsCache, decisionCtx, key, dbName, codeName, dbColumn, codeColumn) => {
|
|
337
230
|
const maybeArrayDbColumn = dbColumn;
|
|
338
231
|
const maybeArrayCodeColumn = codeColumn;
|
|
339
232
|
if (dbColumn instanceof pqb_internal.ArrayColumn && codeColumn instanceof pqb_internal.ArrayColumn) {
|
|
@@ -348,11 +241,13 @@ const compareColumns = async (adapter, domainsMap, ast, currentSchema, compareSq
|
|
|
348
241
|
const codeBaseType = (0, pqb_internal.getColumnBaseType)(codeColumn, domainsMap, codeType);
|
|
349
242
|
if (!typeCasts.get(dbBaseType)?.has(codeBaseType)) {
|
|
350
243
|
if (!(dbColumn instanceof pqb_internal.EnumColumn) || !(codeColumn instanceof pqb_internal.EnumColumn) || !(0, pqb_internal.deepCompare)(dbColumn.options, codeColumn.options)) {
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
244
|
+
if (!await decisionCtx.decider.recreateColumn({
|
|
245
|
+
source: dbColumnSource(decisionCtx, changeTableData, dbName),
|
|
246
|
+
target: codeColumnTarget(changeTableData, codeName),
|
|
247
|
+
tableName: (0, rake_db.concatSchemaAndName)(changeTableData.changeTableAst),
|
|
248
|
+
columnName: key,
|
|
249
|
+
fromType: dbType,
|
|
250
|
+
toType: codeType
|
|
356
251
|
})) throw new AbortSignal();
|
|
357
252
|
dbColumn.data.name = codeColumn.data.name;
|
|
358
253
|
return "recreate";
|
|
@@ -467,6 +362,19 @@ const changeColumn = (changeTableData, key, dbName, dbColumn, codeColumn) => {
|
|
|
467
362
|
to: { column: simpleCodeColumn }
|
|
468
363
|
};
|
|
469
364
|
};
|
|
365
|
+
const dbColumnSource = (decisionCtx, { dbTable }, name) => {
|
|
366
|
+
const { schema, name: table } = decisionCtx.dbSource(dbTable);
|
|
367
|
+
return {
|
|
368
|
+
schema,
|
|
369
|
+
table,
|
|
370
|
+
name
|
|
371
|
+
};
|
|
372
|
+
};
|
|
373
|
+
const codeColumnTarget = ({ schema, changeTableAst }, name) => ({
|
|
374
|
+
schema,
|
|
375
|
+
table: changeTableAst.name,
|
|
376
|
+
name
|
|
377
|
+
});
|
|
470
378
|
const getColumnDbType = (column, currentSchema) => {
|
|
471
379
|
if (column instanceof pqb_internal.EnumColumn) {
|
|
472
380
|
const [schema = currentSchema, name] = (0, rake_db.getSchemaAndTableFromName)(currentSchema, column.enumName);
|
|
@@ -486,6 +394,110 @@ const getColumnDbType = (column, currentSchema) => {
|
|
|
486
394
|
const renameColumn = (columns, from, to) => {
|
|
487
395
|
for (let i = 0; i < columns.length; i++) if (columns[i] === from) columns[i] = to;
|
|
488
396
|
};
|
|
397
|
+
const viewDataToSql = (viewData, viewName) => {
|
|
398
|
+
return (0, pqb_internal.sqlToRawSql)(viewDataToQuerySql(viewData, viewName));
|
|
399
|
+
};
|
|
400
|
+
const viewDataToQuerySql = (viewData, viewName) => {
|
|
401
|
+
if (viewData.query) return (0, pqb_internal.queryToSql)(viewData.query);
|
|
402
|
+
if (viewData.sql !== void 0) return (0, pqb_internal.rawSqlToSql)(viewData.sql);
|
|
403
|
+
throw new Error(`Either sql or query is required for view ${viewName}`);
|
|
404
|
+
};
|
|
405
|
+
const compareSqlExpressions = async (expressions, adapter) => {
|
|
406
|
+
if (!expressions.length) return;
|
|
407
|
+
let id = 1;
|
|
408
|
+
for (const { source, compare, handle } of expressions) {
|
|
409
|
+
const viewName = `orchidTmpView${id++}`;
|
|
410
|
+
const values = [];
|
|
411
|
+
const combinedQueries = [
|
|
412
|
+
`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}` : ""})`,
|
|
413
|
+
`SELECT pg_get_viewdef('${viewName}') v`,
|
|
414
|
+
`DROP VIEW ${viewName}`
|
|
415
|
+
].join("; ");
|
|
416
|
+
const query = () => adapter.query(combinedQueries, values);
|
|
417
|
+
const result = await (adapter.isInTransaction() ? adapter.savepoint(viewName, void 0, query) : query()).then((res) => res[1], async (err) => {
|
|
418
|
+
if (typeof err === "object" && (0, pqb_internal.getDriverErrorCode)(err) !== "42704") throw err;
|
|
419
|
+
});
|
|
420
|
+
if (!result) {
|
|
421
|
+
handle();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
handle(compareSqlExpressionResult(result.rows[0].v, compare[0].inCode));
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
const compareViewsExpressions = async (adapter, compare) => {
|
|
428
|
+
if (!compare.length) return;
|
|
429
|
+
const queries = [];
|
|
430
|
+
let id = 1;
|
|
431
|
+
compare.forEach(({ inCode, ast }, i) => {
|
|
432
|
+
const viewName = `orchidTmpView${id++}ForViews`;
|
|
433
|
+
queries.push({ batch: [
|
|
434
|
+
`SAVEPOINT "${viewName}S"`,
|
|
435
|
+
`CREATE TEMPORARY${"recursive" in ast.options && ast.options.recursive ? " RECURSIVE" : ""} VIEW "${viewName}" (${ast.options.columns?.map((column) => `"${column}"`).join(", ")}) AS (${inCode})`,
|
|
436
|
+
`SELECT ${i} i, '${viewName}' v, pg_get_viewdef('"${viewName}"') sql`,
|
|
437
|
+
`DROP VIEW "${viewName}"`,
|
|
438
|
+
`RELEASE SAVEPOINT "${viewName}S"`
|
|
439
|
+
] });
|
|
440
|
+
});
|
|
441
|
+
let results;
|
|
442
|
+
try {
|
|
443
|
+
const sql = queries.flatMap((q) => q.batch).join(";");
|
|
444
|
+
const query = () => adapter.query(sql, []);
|
|
445
|
+
results = await (adapter.isInTransaction() ? adapter.savepoint("orchidOrmGeneratorViews", void 0, query) : query());
|
|
446
|
+
} catch {
|
|
447
|
+
results = (await Promise.all(queries.map(async ({ batch: queries }, i) => {
|
|
448
|
+
const sql = queries.join(";");
|
|
449
|
+
const query = () => adapter.query(sql, []);
|
|
450
|
+
return await (adapter.isInTransaction() ? adapter.savepoint(`orchidOrmGeneratorViews${i}`, void 0, query) : query()).catch((err) => {
|
|
451
|
+
if (typeof err === "object") {
|
|
452
|
+
const code = (0, pqb_internal.getDriverErrorCode)(err);
|
|
453
|
+
if (code === "42703" || code === "42704" || code === "42P01") return [];
|
|
454
|
+
}
|
|
455
|
+
throw err;
|
|
456
|
+
});
|
|
457
|
+
}))).flat();
|
|
458
|
+
}
|
|
459
|
+
const handled = /* @__PURE__ */ new Set();
|
|
460
|
+
for (const result of results) for (const row of result.rows) if ("sql" in row) {
|
|
461
|
+
const { i, v } = row;
|
|
462
|
+
const cmp = compare[i];
|
|
463
|
+
const hasQuote = !!cmp.inDb.match(/\w*WITH RECURSIVE "/);
|
|
464
|
+
if (row.sql.replaceAll(hasQuote ? v : `"${v}"`, cmp.ast.name) !== cmp.inDb) cmp.onNotEqual();
|
|
465
|
+
handled.add(i);
|
|
466
|
+
}
|
|
467
|
+
compare.forEach((cmp, i) => {
|
|
468
|
+
if (!handled.has(i)) cmp.onNotEqual();
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
const compareSqlExpressionResult = (resultSql, inCode) => {
|
|
472
|
+
let pos = 7;
|
|
473
|
+
const rgx = /\s+AS\s+"\*(inDb-\d+|inCode-\d+-\d+)\*",?/g;
|
|
474
|
+
let match;
|
|
475
|
+
let inDb = "";
|
|
476
|
+
let codeI = 0;
|
|
477
|
+
const matches = inCode.map(() => true);
|
|
478
|
+
while (match = rgx.exec(resultSql)) {
|
|
479
|
+
const sql = resultSql.slice(pos, rgx.lastIndex - match[0].length).trim();
|
|
480
|
+
if (match[1].split("-").length === 2) {
|
|
481
|
+
inDb = sql;
|
|
482
|
+
codeI = 0;
|
|
483
|
+
} else {
|
|
484
|
+
if (inDb !== sql && !(inDb.startsWith("(") && inDb.endsWith(")") && inDb.slice(1, -1) === sql) && !(sql.startsWith("(") && sql.endsWith(")") && sql.slice(1, -1) === inDb)) matches[codeI] = false;
|
|
485
|
+
codeI++;
|
|
486
|
+
}
|
|
487
|
+
pos = rgx.lastIndex;
|
|
488
|
+
}
|
|
489
|
+
const firstMatching = matches.indexOf(true);
|
|
490
|
+
return firstMatching === -1 ? void 0 : firstMatching;
|
|
491
|
+
};
|
|
492
|
+
const checkForColumnAddOrDrop = (shape, key) => {
|
|
493
|
+
const item = shape[key];
|
|
494
|
+
if (item) return item && (Array.isArray(item) || item.type === "add" || item.type === "drop");
|
|
495
|
+
for (const k in shape) {
|
|
496
|
+
const item = shape[k];
|
|
497
|
+
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;
|
|
498
|
+
}
|
|
499
|
+
return false;
|
|
500
|
+
};
|
|
489
501
|
const processDomains = async (ast, adapter, domainsMap, dbStructure, { codeItems: { domains }, structureToAstCtx, currentSchema, internal: { generatorIgnore } }, pendingDbTypes) => {
|
|
490
502
|
const codeDomains = [];
|
|
491
503
|
if (domains) for (const { schemaName, name, column } of domains) codeDomains.push(makeComparableDomain(currentSchema, schemaName, name, column));
|
|
@@ -616,7 +628,7 @@ const createAst = (codeDomain) => ({
|
|
|
616
628
|
name: codeDomain.name,
|
|
617
629
|
baseType: codeDomain.column
|
|
618
630
|
});
|
|
619
|
-
const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSchema,
|
|
631
|
+
const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSchema, internal: { generatorIgnore } }, decisionCtx, pendingDbTypes) => {
|
|
620
632
|
const createEnums = [];
|
|
621
633
|
const dropEnums = [];
|
|
622
634
|
for (const [, codeEnum] of enums) {
|
|
@@ -627,7 +639,7 @@ const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSch
|
|
|
627
639
|
if (generatorIgnore?.schemas?.includes(dbEnum.schemaName) || generatorIgnore?.enums?.includes(dbEnum.name)) continue;
|
|
628
640
|
const codeEnum = enums.get(`${dbEnum.schemaName}.${dbEnum.name}`);
|
|
629
641
|
if (codeEnum) {
|
|
630
|
-
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes,
|
|
642
|
+
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes, decisionCtx, currentSchema);
|
|
631
643
|
continue;
|
|
632
644
|
}
|
|
633
645
|
const i = createEnums.findIndex((x) => x.name === dbEnum.name);
|
|
@@ -646,17 +658,26 @@ const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSch
|
|
|
646
658
|
to: dbEnum.name
|
|
647
659
|
});
|
|
648
660
|
pendingDbTypes.add(toSchema, dbEnum.name);
|
|
649
|
-
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes,
|
|
661
|
+
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes, decisionCtx, currentSchema);
|
|
650
662
|
continue;
|
|
651
663
|
}
|
|
652
664
|
dropEnums.push(dbEnum);
|
|
653
665
|
}
|
|
654
666
|
for (const codeEnum of createEnums) {
|
|
655
667
|
if (dropEnums.length) {
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
668
|
+
const dbEnum = await decisionCtx.decider.createOrRename({
|
|
669
|
+
kind: "enum",
|
|
670
|
+
target: {
|
|
671
|
+
schema: codeEnum.schema ?? currentSchema,
|
|
672
|
+
name: codeEnum.name
|
|
673
|
+
},
|
|
674
|
+
candidates: dropEnums,
|
|
675
|
+
candidateSource: (x) => decisionCtx.dbSource(x),
|
|
676
|
+
name: codeEnum.name,
|
|
677
|
+
candidateName: (x) => x.name
|
|
678
|
+
});
|
|
679
|
+
if (dbEnum) {
|
|
680
|
+
dropEnums.splice(dropEnums.indexOf(dbEnum), 1);
|
|
660
681
|
const fromSchema = dbEnum.schemaName;
|
|
661
682
|
const from = dbEnum.name;
|
|
662
683
|
const toSchema = codeEnum.schema ?? currentSchema;
|
|
@@ -672,7 +693,7 @@ const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSch
|
|
|
672
693
|
to
|
|
673
694
|
});
|
|
674
695
|
pendingDbTypes.add(toSchema, to);
|
|
675
|
-
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes,
|
|
696
|
+
await changeEnum(ast, dbEnum, codeEnum, pendingDbTypes, decisionCtx, currentSchema);
|
|
676
697
|
continue;
|
|
677
698
|
}
|
|
678
699
|
}
|
|
@@ -691,7 +712,7 @@ const processEnums = async (ast, dbStructure, { codeItems: { enums }, currentSch
|
|
|
691
712
|
values: dbEnum.values
|
|
692
713
|
});
|
|
693
714
|
};
|
|
694
|
-
const changeEnum = async (ast, dbEnum, codeEnum, pendingDbTypes,
|
|
715
|
+
const changeEnum = async (ast, dbEnum, codeEnum, pendingDbTypes, decisionCtx, currentSchema) => {
|
|
695
716
|
const { values: dbValues } = dbEnum;
|
|
696
717
|
const { values: codeValues, schema, name } = codeEnum;
|
|
697
718
|
const addValues = codeValues.filter((value) => !dbValues.includes(value));
|
|
@@ -721,7 +742,14 @@ const changeEnum = async (ast, dbEnum, codeEnum, pendingDbTypes, verifying) => {
|
|
|
721
742
|
return;
|
|
722
743
|
}
|
|
723
744
|
} else if (!dropValues.length) return;
|
|
724
|
-
const enumValueChanges = await
|
|
745
|
+
const enumValueChanges = await resolveEnumValueChanges({
|
|
746
|
+
source: decisionCtx.dbSource(dbEnum),
|
|
747
|
+
target: {
|
|
748
|
+
schema: schema ?? currentSchema,
|
|
749
|
+
name
|
|
750
|
+
},
|
|
751
|
+
enumName: name
|
|
752
|
+
}, dbValues, codeValues, addValues, dropValues, decisionCtx.decider);
|
|
725
753
|
if (enumValueChanges) {
|
|
726
754
|
let changed = false;
|
|
727
755
|
if (Object.keys(enumValueChanges.renamedValues).length) {
|
|
@@ -757,19 +785,18 @@ const changeEnum = async (ast, dbEnum, codeEnum, pendingDbTypes, verifying) => {
|
|
|
757
785
|
});
|
|
758
786
|
pendingDbTypes.add(schema, name);
|
|
759
787
|
};
|
|
760
|
-
const
|
|
788
|
+
const resolveEnumValueChanges = async (enumQuestion, dbValues, codeValues, addValues, dropValues, decider) => {
|
|
761
789
|
if (!addValues.length || !dropValues.length) return;
|
|
762
790
|
const renamedValues = {};
|
|
763
791
|
const remainingDropValues = [...dropValues];
|
|
764
792
|
for (const value of addValues) if (remainingDropValues.length) {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
793
|
+
const dropValue = await decider.addOrRenameEnumValue({
|
|
794
|
+
...enumQuestion,
|
|
795
|
+
value,
|
|
796
|
+
candidates: remainingDropValues
|
|
769
797
|
});
|
|
770
|
-
if (
|
|
771
|
-
|
|
772
|
-
remainingDropValues.splice(i - 1, 1);
|
|
798
|
+
if (dropValue !== void 0) {
|
|
799
|
+
remainingDropValues.splice(remainingDropValues.indexOf(dropValue), 1);
|
|
773
800
|
renamedValues[dropValue] = value;
|
|
774
801
|
}
|
|
775
802
|
}
|
|
@@ -1743,7 +1770,7 @@ const isSameStringArray = (a, b) => {
|
|
|
1743
1770
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
1744
1771
|
return true;
|
|
1745
1772
|
};
|
|
1746
|
-
const processTables = async (ast, domainsMap, adapter, dbStructure, config, { structureToAstCtx, codeItems: { tables }, currentSchema, internal: { generatorIgnore }
|
|
1773
|
+
const processTables = async (ast, domainsMap, adapter, dbStructure, config, { structureToAstCtx, codeItems: { tables }, currentSchema, internal: { generatorIgnore } }, decisionCtx, pendingDbTypes) => {
|
|
1747
1774
|
const createTables = collectCreateTables(tables, dbStructure, currentSchema);
|
|
1748
1775
|
const compareSql = {
|
|
1749
1776
|
values: [],
|
|
@@ -1752,8 +1779,8 @@ const processTables = async (ast, domainsMap, adapter, dbStructure, config, { st
|
|
|
1752
1779
|
const expressions = [];
|
|
1753
1780
|
const { changeTables, changeTableSchemas, dropTables, tableShapes } = collectChangeAndDropTables(adapter, config, tables, dbStructure, currentSchema, createTables, generatorIgnore);
|
|
1754
1781
|
applyChangeTableSchemas(changeTableSchemas, currentSchema, ast);
|
|
1755
|
-
await applyCreateOrRenameTables(dbStructure, createTables, dropTables, changeTables, tableShapes, currentSchema, ast,
|
|
1756
|
-
await applyChangeTables(adapter, changeTables, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, compareSql, expressions,
|
|
1782
|
+
await applyCreateOrRenameTables(dbStructure, createTables, dropTables, changeTables, tableShapes, currentSchema, ast, decisionCtx);
|
|
1783
|
+
await applyChangeTables(adapter, changeTables, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, compareSql, expressions, decisionCtx, pendingDbTypes);
|
|
1757
1784
|
processForeignKeys(config, ast, changeTables, currentSchema, tableShapes);
|
|
1758
1785
|
await Promise.all([applyCompareSql(compareSql, adapter), compareSqlExpressions(expressions, adapter)]);
|
|
1759
1786
|
await processTableRls(adapter, ast, dbStructure, tables, currentSchema, generatorIgnore);
|
|
@@ -1829,12 +1856,12 @@ const applyChangeTableSchemas = (changeTableSchemas, currentSchema, ast) => {
|
|
|
1829
1856
|
});
|
|
1830
1857
|
}
|
|
1831
1858
|
};
|
|
1832
|
-
const applyChangeTables = async (adapter, changeTables, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, compareSql, expressions,
|
|
1859
|
+
const applyChangeTables = async (adapter, changeTables, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, compareSql, expressions, decisionCtx, pendingDbTypes) => {
|
|
1833
1860
|
const compareExpressions = [];
|
|
1834
1861
|
const typeCastsCache = {};
|
|
1835
1862
|
for (const changeTableData of changeTables) {
|
|
1836
1863
|
compareExpressions.length = 0;
|
|
1837
|
-
await processTableChange(adapter, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, changeTableData, compareSql, compareExpressions, typeCastsCache,
|
|
1864
|
+
await processTableChange(adapter, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, changeTableData, compareSql, compareExpressions, typeCastsCache, decisionCtx);
|
|
1838
1865
|
if (compareExpressions.length) {
|
|
1839
1866
|
const { codeTable } = changeTableData;
|
|
1840
1867
|
const names = [];
|
|
@@ -1881,13 +1908,22 @@ const applyCompareSql = async (compareSql, adapter) => {
|
|
|
1881
1908
|
for (let i = 0; i < results.length; i++) if (!results[i]) compareSql.expressions[i].change();
|
|
1882
1909
|
}
|
|
1883
1910
|
};
|
|
1884
|
-
const applyCreateOrRenameTables = async (dbStructure, createTables, dropTables, changeTables, tableShapes, currentSchema, ast,
|
|
1911
|
+
const applyCreateOrRenameTables = async (dbStructure, createTables, dropTables, changeTables, tableShapes, currentSchema, ast, decisionCtx) => {
|
|
1885
1912
|
for (const codeTable of createTables) {
|
|
1886
1913
|
if (dropTables.length) {
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1914
|
+
const dbTable = await decisionCtx.decider.createOrRename({
|
|
1915
|
+
kind: "table",
|
|
1916
|
+
target: {
|
|
1917
|
+
schema: codeTable.q.schema ?? currentSchema,
|
|
1918
|
+
name: codeTable.table
|
|
1919
|
+
},
|
|
1920
|
+
candidates: dropTables,
|
|
1921
|
+
candidateSource: (x) => decisionCtx.dbSource(x),
|
|
1922
|
+
name: codeTable.table,
|
|
1923
|
+
candidateName: (x) => x.name
|
|
1924
|
+
});
|
|
1925
|
+
if (dbTable) {
|
|
1926
|
+
dropTables.splice(dropTables.indexOf(dbTable), 1);
|
|
1891
1927
|
ast.push({
|
|
1892
1928
|
type: "renameType",
|
|
1893
1929
|
kind: "TABLE",
|
|
@@ -1957,8 +1993,8 @@ const makeTableShape = (table) => {
|
|
|
1957
1993
|
}
|
|
1958
1994
|
return shape;
|
|
1959
1995
|
};
|
|
1960
|
-
const processTableChange = async (adapter, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, changeTableData, compareSql, compareExpressions, typeCastsCache,
|
|
1961
|
-
await processColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, changeTableData, ast, currentSchema, compareSql, typeCastsCache,
|
|
1996
|
+
const processTableChange = async (adapter, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, changeTableData, compareSql, compareExpressions, typeCastsCache, decisionCtx) => {
|
|
1997
|
+
await processColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, changeTableData, ast, currentSchema, compareSql, typeCastsCache, decisionCtx);
|
|
1962
1998
|
processPrimaryKey(config, changeTableData);
|
|
1963
1999
|
processIndexesAndExcludes(config, changeTableData, ast, compareExpressions);
|
|
1964
2000
|
processChecks(ast, changeTableData, compareExpressions);
|
|
@@ -1979,7 +2015,7 @@ const defaults = {
|
|
|
1979
2015
|
connLimit: -1,
|
|
1980
2016
|
bypassRls: false
|
|
1981
2017
|
};
|
|
1982
|
-
const processRoles = async (ast, dbStructure, {
|
|
2018
|
+
const processRoles = async (ast, dbStructure, { internal: { roles } }, { decider }) => {
|
|
1983
2019
|
if (!dbStructure.roles || !roles) return;
|
|
1984
2020
|
const codeRoles = roles.map((role) => {
|
|
1985
2021
|
const { defaultPrivileges: _, ...roleWithoutPrivileges } = role;
|
|
@@ -2008,10 +2044,16 @@ const processRoles = async (ast, dbStructure, { verifying, internal: { roles } }
|
|
|
2008
2044
|
for (const codeRole of codeRoles) {
|
|
2009
2045
|
if (found.has(codeRole.name)) continue;
|
|
2010
2046
|
if (dropRoles.length) {
|
|
2011
|
-
const
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
dropRoles
|
|
2047
|
+
const dbRole = await decider.createOrRename({
|
|
2048
|
+
kind: "role",
|
|
2049
|
+
target: { name: codeRole.name },
|
|
2050
|
+
candidates: dropRoles,
|
|
2051
|
+
candidateSource: (x) => ({ name: x.name }),
|
|
2052
|
+
name: codeRole.name,
|
|
2053
|
+
candidateName: (x) => x.name
|
|
2054
|
+
});
|
|
2055
|
+
if (dbRole) {
|
|
2056
|
+
dropRoles.splice(dropRoles.indexOf(dbRole), 1);
|
|
2015
2057
|
ast.push(makeRenameOrChangeAst(dbRole, codeRole));
|
|
2016
2058
|
continue;
|
|
2017
2059
|
}
|
|
@@ -2807,6 +2849,74 @@ const normalizedViewName = (view, currentSchema) => {
|
|
|
2807
2849
|
return view.schemaName === currentSchema ? view.name : `${view.schemaName}.${view.name}`;
|
|
2808
2850
|
};
|
|
2809
2851
|
/**
|
|
2852
|
+
* Creates `MigrationDecisionCtx` for the `decider`, remembering the identities
|
|
2853
|
+
* of tables and enums in `dbStructure`, so it must be called for every
|
|
2854
|
+
* introspection before the generators change it.
|
|
2855
|
+
*/
|
|
2856
|
+
const makeMigrationDecisionCtx = (decider, dbStructure) => {
|
|
2857
|
+
const sources = /* @__PURE__ */ new WeakMap();
|
|
2858
|
+
for (const item of [...dbStructure.tables, ...dbStructure.enums]) sources.set(item, {
|
|
2859
|
+
schema: item.schemaName,
|
|
2860
|
+
name: item.name
|
|
2861
|
+
});
|
|
2862
|
+
return {
|
|
2863
|
+
decider,
|
|
2864
|
+
dbSource(item) {
|
|
2865
|
+
const source = sources.get(item);
|
|
2866
|
+
if (!source) throw new Error(`${item.schemaName}.${item.name} is missing in the introspected structure`);
|
|
2867
|
+
return source;
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
};
|
|
2871
|
+
/**
|
|
2872
|
+
* Decider that asks the user in the terminal.
|
|
2873
|
+
*/
|
|
2874
|
+
const interactiveDecider = {
|
|
2875
|
+
async createOrRename({ kind, name, candidates, candidateName }) {
|
|
2876
|
+
const names = candidates.map(candidateName);
|
|
2877
|
+
let hintPos = name.length + 4;
|
|
2878
|
+
for (const from of names) {
|
|
2879
|
+
const value = from.length + 8 + name.length;
|
|
2880
|
+
if (value > hintPos) hintPos = value;
|
|
2881
|
+
}
|
|
2882
|
+
const renameMessage = `rename ${kind}`;
|
|
2883
|
+
const i = await (0, rake_db.promptSelect)({
|
|
2884
|
+
message: `Create or rename ${pqb_internal.colors.blueBold(name)} ${kind} from another ${kind}?`,
|
|
2885
|
+
options: [`${pqb_internal.colors.greenBold("+")} ${name} ${pqb_internal.colors.pale(`create ${kind}`.padStart(hintPos + renameMessage.length - name.length - 4, " "))}`, ...names.map((d) => `${pqb_internal.colors.yellowBold("~")} ${d} ${pqb_internal.colors.yellowBold("=>")} ${name} ${pqb_internal.colors.pale(renameMessage.padStart(hintPos + renameMessage.length - d.length - name.length - 8, " "))}`)]
|
|
2886
|
+
});
|
|
2887
|
+
return i ? candidates[i - 1] : void 0;
|
|
2888
|
+
},
|
|
2889
|
+
async addOrRenameEnumValue({ enumName, value, candidates }) {
|
|
2890
|
+
const i = await (0, rake_db.promptSelect)({
|
|
2891
|
+
message: `Add or rename ${pqb_internal.colors.blueBold(value)} enum value in ${pqb_internal.colors.blueBold(enumName)}?`,
|
|
2892
|
+
options: [`${pqb_internal.colors.greenBold("+")} ${value} ${pqb_internal.colors.pale("add enum value")}`, ...candidates.map((dropValue) => `${pqb_internal.colors.yellowBold("~")} ${dropValue} ${pqb_internal.colors.yellowBold("=>")} ${value} ${pqb_internal.colors.pale("rename enum value")}`)]
|
|
2893
|
+
});
|
|
2894
|
+
return i ? candidates[i - 1] : void 0;
|
|
2895
|
+
},
|
|
2896
|
+
async recreateColumn({ tableName, columnName, fromType, toType }) {
|
|
2897
|
+
return !await (0, rake_db.promptSelect)({
|
|
2898
|
+
message: `Cannot cast type of ${tableName}'s column ${columnName} from ${fromType} to ${toType}`,
|
|
2899
|
+
options: [`${pqb_internal.colors.yellowBold(`-/+`)} recreate the column, existing data will be ${pqb_internal.colors.red("lost")}`, `write migration manually`]
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
};
|
|
2903
|
+
/**
|
|
2904
|
+
* Decider for re-generating a migration after applying it: any remaining
|
|
2905
|
+
* question means the migration did not bring the database to the code state,
|
|
2906
|
+
* so it aborts instead of asking.
|
|
2907
|
+
*/
|
|
2908
|
+
const verifyingDecider = {
|
|
2909
|
+
createOrRename() {
|
|
2910
|
+
throw new AbortSignal();
|
|
2911
|
+
},
|
|
2912
|
+
addOrRenameEnumValue() {
|
|
2913
|
+
throw new AbortSignal();
|
|
2914
|
+
},
|
|
2915
|
+
recreateColumn() {
|
|
2916
|
+
throw new AbortSignal();
|
|
2917
|
+
}
|
|
2918
|
+
};
|
|
2919
|
+
/**
|
|
2810
2920
|
* This is needed to compare SQLs of table expressions.
|
|
2811
2921
|
* Need to exclude table columns of pending types, such as enums or domains,
|
|
2812
2922
|
* that aren't created yet from the SQL comparison.
|
|
@@ -2822,16 +2932,17 @@ var PendingDbTypes = class {
|
|
|
2822
2932
|
};
|
|
2823
2933
|
const composeMigration = async (adapter, config, ast, dbStructure, params) => {
|
|
2824
2934
|
const { structureToAstCtx, currentSchema } = params;
|
|
2825
|
-
|
|
2935
|
+
const decisionCtx = makeMigrationDecisionCtx(params.decider, dbStructure);
|
|
2936
|
+
await processRoles(ast, dbStructure, params, decisionCtx);
|
|
2826
2937
|
processDefaultPrivileges(ast, dbStructure, params);
|
|
2827
2938
|
processGrants(ast, dbStructure, params);
|
|
2828
2939
|
const domainsMap = (0, rake_db.makeDomainsMap)(structureToAstCtx, dbStructure);
|
|
2829
|
-
await processSchemas(ast, dbStructure, params);
|
|
2940
|
+
await processSchemas(ast, dbStructure, params, decisionCtx);
|
|
2830
2941
|
processExtensions(ast, dbStructure, params);
|
|
2831
2942
|
const pendingDbTypes = new PendingDbTypes();
|
|
2832
2943
|
await processDomains(ast, adapter, domainsMap, dbStructure, params, pendingDbTypes);
|
|
2833
|
-
await processEnums(ast, dbStructure, params, pendingDbTypes);
|
|
2834
|
-
await processTables(ast, domainsMap, adapter, dbStructure, config, params, pendingDbTypes);
|
|
2944
|
+
await processEnums(ast, dbStructure, params, decisionCtx, pendingDbTypes);
|
|
2945
|
+
await processTables(ast, domainsMap, adapter, dbStructure, config, params, decisionCtx, pendingDbTypes);
|
|
2835
2946
|
await processViews(ast, adapter, dbStructure, params);
|
|
2836
2947
|
await processMaterializedViews(ast, adapter, dbStructure, params);
|
|
2837
2948
|
return (0, rake_db.astToMigration)(currentSchema, config, ast);
|
|
@@ -2856,9 +2967,11 @@ const verifyMigration = async (adapter, config, migrationCode, generateMigration
|
|
|
2856
2967
|
roles,
|
|
2857
2968
|
...structureParams
|
|
2858
2969
|
});
|
|
2859
|
-
generateMigrationParams.verifying = true;
|
|
2860
2970
|
try {
|
|
2861
|
-
code = await composeMigration(trx, config, [], dbStructure,
|
|
2971
|
+
code = await composeMigration(trx, config, [], dbStructure, {
|
|
2972
|
+
...generateMigrationParams,
|
|
2973
|
+
decider: verifyingDecider
|
|
2974
|
+
});
|
|
2862
2975
|
} catch (err) {
|
|
2863
2976
|
if (err instanceof AbortSignal) {
|
|
2864
2977
|
code = false;
|
|
@@ -2873,6 +2986,222 @@ const verifyMigration = async (adapter, config, migrationCode, generateMigration
|
|
|
2873
2986
|
}
|
|
2874
2987
|
return code;
|
|
2875
2988
|
};
|
|
2989
|
+
const pathLengths = {
|
|
2990
|
+
schema: 1,
|
|
2991
|
+
role: 1,
|
|
2992
|
+
enum: 2,
|
|
2993
|
+
table: 2,
|
|
2994
|
+
column: 3,
|
|
2995
|
+
"enum-value": 3
|
|
2996
|
+
};
|
|
2997
|
+
/**
|
|
2998
|
+
* Parses arguments of the generate command, separating the flags that answer
|
|
2999
|
+
* the generator questions from the positional arguments.
|
|
3000
|
+
*/
|
|
3001
|
+
const parseGenerateArgs = (args) => {
|
|
3002
|
+
const positional = [];
|
|
3003
|
+
let nonInteractive = false;
|
|
3004
|
+
const answers = [];
|
|
3005
|
+
for (let i = 0; i < args.length; i++) {
|
|
3006
|
+
const arg = args[i];
|
|
3007
|
+
if (arg === "--non-interactive") {
|
|
3008
|
+
nonInteractive = true;
|
|
3009
|
+
continue;
|
|
3010
|
+
}
|
|
3011
|
+
if (arg === "--transaction") {
|
|
3012
|
+
i++;
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
if (arg.startsWith("--transaction=")) continue;
|
|
3016
|
+
if (!arg.startsWith("--")) {
|
|
3017
|
+
positional.push(arg);
|
|
3018
|
+
continue;
|
|
3019
|
+
}
|
|
3020
|
+
const match = arg.match(/^--(rename|create|recreate)(?:=(.*))?$/s);
|
|
3021
|
+
if (!match) throw new rake_db.RakeDbError(`Unknown flag ${arg}`);
|
|
3022
|
+
const action = match[1];
|
|
3023
|
+
const value = match[2] ?? args[++i];
|
|
3024
|
+
if (value === void 0) throw new rake_db.RakeDbError(`--${action} requires a value`);
|
|
3025
|
+
answers.push(parseAnswer(action, value));
|
|
3026
|
+
}
|
|
3027
|
+
checkConflictingAnswers(answers);
|
|
3028
|
+
return {
|
|
3029
|
+
positional,
|
|
3030
|
+
nonInteractive,
|
|
3031
|
+
answers
|
|
3032
|
+
};
|
|
3033
|
+
};
|
|
3034
|
+
const parseAnswer = (action, value) => {
|
|
3035
|
+
const flag = `--${action} ${value}`;
|
|
3036
|
+
const invalid = (reason) => new rake_db.RakeDbError(`Invalid ${flag}: ${reason}`);
|
|
3037
|
+
const colon = value.indexOf(":");
|
|
3038
|
+
const kind = value.slice(0, colon);
|
|
3039
|
+
if (colon === -1 || !Object.hasOwn(pathLengths, kind)) throw invalid(`expected one of ${Object.keys(pathLengths).join(", ")} followed by a colon`);
|
|
3040
|
+
if (action === "recreate" && kind !== "column") throw invalid("only a column can be recreated");
|
|
3041
|
+
const length = pathLengths[kind];
|
|
3042
|
+
const readPath = (start) => {
|
|
3043
|
+
const { path, end } = parsePath(value, start, invalid);
|
|
3044
|
+
if (path.length !== length) throw invalid(`${kind} must be given by ${length} dot-separated name${length === 1 ? "" : "s"}`);
|
|
3045
|
+
return {
|
|
3046
|
+
path,
|
|
3047
|
+
end
|
|
3048
|
+
};
|
|
3049
|
+
};
|
|
3050
|
+
const first = readPath(colon + 1);
|
|
3051
|
+
if (action === "create") {
|
|
3052
|
+
if (first.end !== value.length) throw invalid("unexpected text after the name");
|
|
3053
|
+
return {
|
|
3054
|
+
flag,
|
|
3055
|
+
action,
|
|
3056
|
+
kind,
|
|
3057
|
+
target: first.path
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
if (value[first.end] !== "=") throw invalid("expected the old name and the new name separated by \"=\"");
|
|
3061
|
+
const second = readPath(first.end + 1);
|
|
3062
|
+
if (second.end !== value.length) throw invalid("unexpected text after the name");
|
|
3063
|
+
return {
|
|
3064
|
+
flag,
|
|
3065
|
+
action,
|
|
3066
|
+
kind,
|
|
3067
|
+
source: first.path,
|
|
3068
|
+
target: second.path
|
|
3069
|
+
};
|
|
3070
|
+
};
|
|
3071
|
+
const parsePath = (value, start, invalid) => {
|
|
3072
|
+
const path = [];
|
|
3073
|
+
let i = start;
|
|
3074
|
+
for (;;) {
|
|
3075
|
+
let segment = "";
|
|
3076
|
+
if (value[i] === "\"") {
|
|
3077
|
+
for (i++;; i++) {
|
|
3078
|
+
if (i === value.length) throw invalid("unterminated quoted name");
|
|
3079
|
+
if (value[i] === "\"") {
|
|
3080
|
+
if (value[i + 1] !== "\"") break;
|
|
3081
|
+
i++;
|
|
3082
|
+
}
|
|
3083
|
+
segment += value[i];
|
|
3084
|
+
}
|
|
3085
|
+
i++;
|
|
3086
|
+
} else {
|
|
3087
|
+
while (i < value.length && !".=\"".includes(value[i])) segment += value[i++];
|
|
3088
|
+
if (!segment) throw invalid("expected a name");
|
|
3089
|
+
}
|
|
3090
|
+
path.push(segment);
|
|
3091
|
+
if (value[i] !== ".") return {
|
|
3092
|
+
path,
|
|
3093
|
+
end: i
|
|
3094
|
+
};
|
|
3095
|
+
i++;
|
|
3096
|
+
}
|
|
3097
|
+
};
|
|
3098
|
+
const answerGroup = (answer) => answer.action === "recreate" ? "recreate" : "createOrRename";
|
|
3099
|
+
const answerKey = (group, kind, path) => `${group} ${kind}:${formatPath(path)}`;
|
|
3100
|
+
const checkConflictingAnswers = (answers) => {
|
|
3101
|
+
const targets = /* @__PURE__ */ new Map();
|
|
3102
|
+
const sources = /* @__PURE__ */ new Map();
|
|
3103
|
+
for (const answer of answers) {
|
|
3104
|
+
const group = answerGroup(answer);
|
|
3105
|
+
const conflict = (other) => new rake_db.RakeDbError(`${answer.flag} conflicts with ${other.flag}`);
|
|
3106
|
+
const targetKey = answerKey(group, answer.kind, answer.target);
|
|
3107
|
+
const sameTarget = targets.get(targetKey);
|
|
3108
|
+
if (sameTarget) throw conflict(sameTarget);
|
|
3109
|
+
targets.set(targetKey, answer);
|
|
3110
|
+
if (answer.action !== "create") {
|
|
3111
|
+
const sourceKey = answerKey(group, answer.kind, answer.source);
|
|
3112
|
+
const sameSource = sources.get(sourceKey);
|
|
3113
|
+
if (sameSource) throw conflict(sameSource);
|
|
3114
|
+
sources.set(sourceKey, answer);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
};
|
|
3118
|
+
/**
|
|
3119
|
+
* Formats database names as they are given in the answer flags:
|
|
3120
|
+
* separated by dots, quoting the names that cannot be read otherwise.
|
|
3121
|
+
*/
|
|
3122
|
+
const formatPath = (path) => path.map((name) => /^[^\s."=]+$/.test(name) ? name : `"${name.replaceAll("\"", "\"\"")}"`).join(".");
|
|
3123
|
+
const itemPath = ({ schema, table, name }) => [
|
|
3124
|
+
...schema === void 0 ? [] : [schema],
|
|
3125
|
+
...table === void 0 ? [] : [table],
|
|
3126
|
+
name
|
|
3127
|
+
];
|
|
3128
|
+
const samePath = (a, b) => a.length === b.length && a.every((name, i) => name === b[i]);
|
|
3129
|
+
/**
|
|
3130
|
+
* Creates an `AnswersDecider` for the `answers`.
|
|
3131
|
+
* Without the `ask` decider, a question without an answer is collected,
|
|
3132
|
+
* and the generation goes on as if the item was created or recreated,
|
|
3133
|
+
* so that the following questions are collected as well.
|
|
3134
|
+
*/
|
|
3135
|
+
const makeAnswersDecider = (answers, ask) => {
|
|
3136
|
+
const answerMap = /* @__PURE__ */ new Map();
|
|
3137
|
+
for (const answer of answers) answerMap.set(answerKey(answerGroup(answer), answer.kind, answer.target), answer);
|
|
3138
|
+
const used = /* @__PURE__ */ new Set();
|
|
3139
|
+
const takeAnswer = (group, kind, target) => {
|
|
3140
|
+
const answer = answerMap.get(answerKey(group, kind, target));
|
|
3141
|
+
if (answer) used.add(answer);
|
|
3142
|
+
return answer;
|
|
3143
|
+
};
|
|
3144
|
+
const errors = [];
|
|
3145
|
+
const questions = [];
|
|
3146
|
+
const addQuestion = (question, options) => {
|
|
3147
|
+
questions.push([question, ...options.map((option) => ` ${option}`)].join("\n"));
|
|
3148
|
+
};
|
|
3149
|
+
const findCandidate = (answer, kind, target, candidates, candidatePath) => {
|
|
3150
|
+
const candidate = candidates.find((candidate) => samePath(candidatePath(candidate), answer.source));
|
|
3151
|
+
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(", ")}`);
|
|
3152
|
+
return candidate;
|
|
3153
|
+
};
|
|
3154
|
+
return {
|
|
3155
|
+
async createOrRename(question) {
|
|
3156
|
+
const { kind, candidates, candidateSource } = question;
|
|
3157
|
+
const target = itemPath(question.target);
|
|
3158
|
+
const candidatePath = (candidate) => itemPath(candidateSource(candidate));
|
|
3159
|
+
const answer = takeAnswer("createOrRename", kind, target);
|
|
3160
|
+
if (answer) return answer.action === "rename" ? findCandidate(answer, kind, target, candidates, candidatePath) : void 0;
|
|
3161
|
+
if (ask) return ask.createOrRename(question);
|
|
3162
|
+
addQuestion(`Create or rename ${kind} ${formatPath(target)}?`, [formatFlag("create", kind, target), ...candidates.map((candidate) => formatFlag("rename", kind, candidatePath(candidate), target))]);
|
|
3163
|
+
},
|
|
3164
|
+
async addOrRenameEnumValue(question) {
|
|
3165
|
+
const { value, candidates } = question;
|
|
3166
|
+
const kind = "enum-value";
|
|
3167
|
+
const target = [...itemPath(question.target), value];
|
|
3168
|
+
const source = itemPath(question.source);
|
|
3169
|
+
const candidatePath = (candidate) => [...source, candidate];
|
|
3170
|
+
const answer = takeAnswer("createOrRename", kind, target);
|
|
3171
|
+
if (answer) return answer.action === "rename" ? findCandidate(answer, kind, target, candidates, candidatePath) : void 0;
|
|
3172
|
+
if (ask) return ask.addOrRenameEnumValue(question);
|
|
3173
|
+
addQuestion(`Add or rename enum value ${formatPath(target)}?`, [formatFlag("create", kind, target), ...candidates.map((candidate) => formatFlag("rename", kind, candidatePath(candidate), target))]);
|
|
3174
|
+
},
|
|
3175
|
+
async recreateColumn(question) {
|
|
3176
|
+
const kind = "column";
|
|
3177
|
+
const source = itemPath(question.source);
|
|
3178
|
+
const target = itemPath(question.target);
|
|
3179
|
+
const answer = takeAnswer("recreate", kind, target);
|
|
3180
|
+
if (answer && answer.action === "recreate") {
|
|
3181
|
+
if (!samePath(source, answer.source)) errors.push(`${answer.flag}: the column to recreate is ${formatPath(source)}`);
|
|
3182
|
+
return true;
|
|
3183
|
+
}
|
|
3184
|
+
if (ask) return ask.recreateColumn(question);
|
|
3185
|
+
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)]);
|
|
3186
|
+
return true;
|
|
3187
|
+
},
|
|
3188
|
+
problems() {
|
|
3189
|
+
const sections = [];
|
|
3190
|
+
if (errors.length) sections.push(errors.join("\n"));
|
|
3191
|
+
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.");
|
|
3192
|
+
else {
|
|
3193
|
+
const unused = answers.filter((answer) => answer.action === "rename" && !used.has(answer));
|
|
3194
|
+
if (unused.length) sections.push(`These renames do not match any question:\n${unused.map((answer) => answer.flag).join("\n")}`);
|
|
3195
|
+
}
|
|
3196
|
+
return sections.length ? `Cannot generate the migration.\n\n${sections.join("\n\n")}` : void 0;
|
|
3197
|
+
}
|
|
3198
|
+
};
|
|
3199
|
+
};
|
|
3200
|
+
const formatFlag = (action, kind, path, target) => {
|
|
3201
|
+
const value = `${kind}:${formatPath(path)}${target ? `=${formatPath(target)}` : ""}`;
|
|
3202
|
+
return `--${action} ${shellQuote(value)}`;
|
|
3203
|
+
};
|
|
3204
|
+
const shellQuote = (value) => /^[\w.:=@%+,/-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
2876
3205
|
const report = (ast, config, currentSchema) => {
|
|
2877
3206
|
if (!config.logger) return;
|
|
2878
3207
|
const code = [];
|
|
@@ -3162,12 +3491,14 @@ const generate = async (adapters, config, args, afterPull) => {
|
|
|
3162
3491
|
if (!dbPath || !(0, rake_db.getTableFactoryConfig)(config)) throw invalidConfig(config);
|
|
3163
3492
|
if (!adapters.length) throw new Error(`Database options must not be empty`);
|
|
3164
3493
|
if (!dbPath.endsWith(".ts")) dbPath += ".ts";
|
|
3165
|
-
|
|
3494
|
+
const { positional, nonInteractive, answers } = parseGenerateArgs(args);
|
|
3495
|
+
let migrationName = positional[0] ?? "generated";
|
|
3166
3496
|
let up;
|
|
3167
3497
|
if (migrationName === "up") {
|
|
3168
3498
|
up = true;
|
|
3169
3499
|
migrationName = "generated";
|
|
3170
|
-
} else up =
|
|
3500
|
+
} else up = positional[1] === "up";
|
|
3501
|
+
const decider = makeAnswersDecider(answers, nonInteractive || !process.stdin.isTTY ? void 0 : interactiveDecider);
|
|
3171
3502
|
if (afterPull) adapters = [afterPull.adapter];
|
|
3172
3503
|
const db = await getDbFromConfig(config, dbPath);
|
|
3173
3504
|
const { columnTypes, internal } = db.$qb;
|
|
@@ -3192,7 +3523,8 @@ const generate = async (adapters, config, args, afterPull) => {
|
|
|
3192
3523
|
...internal,
|
|
3193
3524
|
generatorIgnore,
|
|
3194
3525
|
grants: effectiveGrants
|
|
3195
|
-
}
|
|
3526
|
+
},
|
|
3527
|
+
decider
|
|
3196
3528
|
};
|
|
3197
3529
|
const ast = [];
|
|
3198
3530
|
let migrationCode;
|
|
@@ -3205,6 +3537,11 @@ const generate = async (adapters, config, args, afterPull) => {
|
|
|
3205
3537
|
}
|
|
3206
3538
|
throw err;
|
|
3207
3539
|
}
|
|
3540
|
+
const problems = decider.problems();
|
|
3541
|
+
if (problems) {
|
|
3542
|
+
if (!afterPull) await closeAdapters(adapters);
|
|
3543
|
+
throw new rake_db.RakeDbError(problems);
|
|
3544
|
+
}
|
|
3208
3545
|
if (migrationCode && !afterPull) {
|
|
3209
3546
|
const result = await verifyMigration(adapter, config, migrationCode, generateMigrationParams, rolesDbStructureParam, structureParams);
|
|
3210
3547
|
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}`}`);
|
|
@@ -3930,7 +4267,11 @@ const patchRakeDb$1 = () => {
|
|
|
3930
4267
|
"no arguments": "\"generated\" is a default file name",
|
|
3931
4268
|
"migration-name": "set migration file name",
|
|
3932
4269
|
up: "auto-apply migration",
|
|
3933
|
-
"migration-name up": "with a custom name and apply it"
|
|
4270
|
+
"migration-name up": "with a custom name and apply it",
|
|
4271
|
+
"--non-interactive": "report unanswered questions instead of asking",
|
|
4272
|
+
"--rename kind:old=new": "answer to rename an item",
|
|
4273
|
+
"--create kind:name": "answer to create an item",
|
|
4274
|
+
"--recreate column:old=new": "answer to recreate a column, losing data"
|
|
3934
4275
|
},
|
|
3935
4276
|
helpAfter: "reset"
|
|
3936
4277
|
};
|