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.
@@ -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 viewDataToSql = (viewData, viewName) => {
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 i = await promptCreateOrRename("schema", schema, dropSchemas, verifying);
157
- if (i) {
158
- const from = dropSchemas[i - 1];
159
- dropSchemas.splice(i - 1, 1);
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, verifying) => {
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, verifying);
236
- await changeColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, dbColumns, columnsToChange, compareSql, changeTableData, typeCastsCache, verifying);
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, { dbTableData, schema, changeTableAst: { name: tableName, shape } }, columnsToAdd, columnsToDrop, columnsToChange, verifying) => {
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 i = await promptCreateOrRename("column", codeName, columnsToDrop.map((x) => x.key), verifying);
281
- if (i) {
282
- const drop = columnsToDrop[i - 1];
283
- columnsToDrop.splice(i - 1, 1);
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, verifying) => {
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, verifying, key, dbName, dbColumn, codeColumn);
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, verifying, key, dbName, dbColumn, codeColumn) => {
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 (verifying) throw new AbortSignal();
352
- const tableName = (0, rake_db.concatSchemaAndName)(changeTableData.changeTableAst);
353
- if (await (0, rake_db.promptSelect)({
354
- message: `Cannot cast type of ${tableName}'s column ${key} from ${dbType} to ${codeType}`,
355
- options: [`${pqb_internal.colors.yellowBold(`-/+`)} recreate the column, existing data will be ${pqb_internal.colors.red("lost")}`, `write migration manually`]
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, verifying, internal: { generatorIgnore } }, pendingDbTypes) => {
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, verifying);
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, verifying);
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 i = await promptCreateOrRename("enum", codeEnum.name, dropEnums.map((x) => x.name), verifying);
657
- if (i) {
658
- const dbEnum = dropEnums[i - 1];
659
- dropEnums.splice(i - 1, 1);
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, verifying);
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, verifying) => {
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 promptEnumValueChanges(name, dbValues, codeValues, addValues, dropValues, verifying);
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 promptEnumValueChanges = async (enumName, dbValues, codeValues, addValues, dropValues, verifying) => {
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
- if (verifying) throw new AbortSignal();
766
- const i = await (0, rake_db.promptSelect)({
767
- message: `Add or rename ${pqb_internal.colors.blueBold(value)} enum value in ${pqb_internal.colors.blueBold(enumName)}?`,
768
- options: [`${pqb_internal.colors.greenBold("+")} ${value} ${pqb_internal.colors.pale("add enum value")}`, ...remainingDropValues.map((dropValue) => `${pqb_internal.colors.yellowBold("~")} ${dropValue} ${pqb_internal.colors.yellowBold("=>")} ${value} ${pqb_internal.colors.pale("rename enum value")}`)]
793
+ const dropValue = await decider.addOrRenameEnumValue({
794
+ ...enumQuestion,
795
+ value,
796
+ candidates: remainingDropValues
769
797
  });
770
- if (i) {
771
- const dropValue = remainingDropValues[i - 1];
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 }, verifying }, pendingDbTypes) => {
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, verifying);
1756
- await applyChangeTables(adapter, changeTables, structureToAstCtx, dbStructure, domainsMap, ast, currentSchema, config, compareSql, expressions, verifying, pendingDbTypes);
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, verifying, pendingDbTypes) => {
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, verifying);
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, verifying) => {
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 i = await promptCreateOrRename("table", codeTable.table, dropTables.map((x) => x.name), verifying);
1888
- if (i) {
1889
- const dbTable = dropTables[i - 1];
1890
- dropTables.splice(i - 1, 1);
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, verifying) => {
1961
- await processColumns(adapter, config, structureToAstCtx, dbStructure, domainsMap, changeTableData, ast, currentSchema, compareSql, typeCastsCache, verifying);
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, { verifying, internal: { roles } }) => {
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 i = await promptCreateOrRename("table", codeRole.name, dropRoles.map((x) => x.name), verifying);
2012
- if (i) {
2013
- const dbRole = dropRoles[i - 1];
2014
- dropRoles.splice(i - 1, 1);
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
- await processRoles(ast, dbStructure, params);
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, generateMigrationParams);
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
- let migrationName = args[0] ?? "generated";
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 = args[1] === "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
  };