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.
@@ -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 viewDataToSql = (viewData, viewName) => {
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 i = await promptCreateOrRename("schema", schema, dropSchemas, verifying);
135
- if (i) {
136
- const from = dropSchemas[i - 1];
137
- dropSchemas.splice(i - 1, 1);
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, verifying) => {
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, verifying);
214
- await changeColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, dbColumns, columnsToChange, compareSql, changeTableData, typeCastsCache, verifying);
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, { dbTableData, schema, changeTableAst: { name: tableName, shape } }, columnsToAdd, columnsToDrop, columnsToChange, verifying) => {
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 i = await promptCreateOrRename("column", codeName, columnsToDrop.map((x) => x.key), verifying);
259
- if (i) {
260
- const drop = columnsToDrop[i - 1];
261
- columnsToDrop.splice(i - 1, 1);
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, verifying) => {
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, verifying, key, dbName, dbColumn, codeColumn);
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, verifying, key, dbName, dbColumn, codeColumn) => {
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 (verifying) throw new AbortSignal();
330
- const tableName = concatSchemaAndName(changeTableData.changeTableAst);
331
- if (await promptSelect({
332
- message: `Cannot cast type of ${tableName}'s column ${key} from ${dbType} to ${codeType}`,
333
- options: [`${colors.yellowBold(`-/+`)} recreate the column, existing data will be ${colors.red("lost")}`, `write migration manually`]
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, verifying, internal: { generatorIgnore } }, pendingDbTypes) => {
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, verifying);
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, verifying);
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 i = await promptCreateOrRename("enum", codeEnum.name, dropEnums.map((x) => x.name), verifying);
635
- if (i) {
636
- const dbEnum = dropEnums[i - 1];
637
- dropEnums.splice(i - 1, 1);
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, verifying);
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, verifying) => {
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 promptEnumValueChanges(name, dbValues, codeValues, addValues, dropValues, verifying);
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 promptEnumValueChanges = async (enumName, dbValues, codeValues, addValues, dropValues, verifying) => {
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
- if (verifying) throw new AbortSignal();
744
- const i = await promptSelect({
745
- message: `Add or rename ${colors.blueBold(value)} enum value in ${colors.blueBold(enumName)}?`,
746
- options: [`${colors.greenBold("+")} ${value} ${colors.pale("add enum value")}`, ...remainingDropValues.map((dropValue) => `${colors.yellowBold("~")} ${dropValue} ${colors.yellowBold("=>")} ${value} ${colors.pale("rename enum value")}`)]
771
+ const dropValue = await decider.addOrRenameEnumValue({
772
+ ...enumQuestion,
773
+ value,
774
+ candidates: remainingDropValues
747
775
  });
748
- if (i) {
749
- const dropValue = remainingDropValues[i - 1];
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 }, verifying }, pendingDbTypes) => {
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, verifying);
1734
- await applyChangeTables(adapter, changeTables, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, compareSql, expressions, verifying, pendingDbTypes);
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, verifying, pendingDbTypes) => {
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, verifying);
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, verifying) => {
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 i = await promptCreateOrRename("table", codeTable.table, dropTables.map((x) => x.name), verifying);
1866
- if (i) {
1867
- const dbTable = dropTables[i - 1];
1868
- dropTables.splice(i - 1, 1);
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, verifying) => {
1939
- await processColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, changeTableData, ast, currentSchema, compareSql, typeCastsCache, verifying);
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, { verifying, internal: { roles } }) => {
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 i = await promptCreateOrRename("table", codeRole.name, dropRoles.map((x) => x.name), verifying);
1990
- if (i) {
1991
- const dbRole = dropRoles[i - 1];
1992
- dropRoles.splice(i - 1, 1);
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
- await processRoles(ast, dbStructure, params);
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, generateMigrationParams);
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
- let migrationName = args[0] ?? "generated";
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 = args[1] === "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
  };