pgpm 5.20.0 → 5.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/commands/diff.js CHANGED
@@ -74,6 +74,10 @@ Options:
74
74
  verify per change, spec-derived paths, graph-derived
75
75
  requires) into <dir>/<pkg>
76
76
  --emit-module <dir> Alias of --emit-migration
77
+ --append-module <dir> Append the delta into an EXISTING pgpm module at
78
+ <dir> (new changes only; existing changes, scripts,
79
+ and .control are left untouched). Standalone: not
80
+ combinable with --emit-*/--verify.
77
81
  --emit-sql <file|-> Also project the delta to a single consolidated SQL
78
82
  file (deparsed in plan order); - writes to stdout
79
83
  --emit-bundle <file> Also project the delta to a content-addressed
@@ -81,6 +85,9 @@ Options:
81
85
  --pkg <name> Emitted migration package name (default: diff-migration)
82
86
  --granularity <level> Granularity for emitted changes: atomic | object |
83
87
  consolidated (default: object)
88
+ --change-granularity <level>
89
+ Change-level distribution for emitted changes:
90
+ alteration | object | single (default: object)
84
91
  --naming <style> Change path naming style: directory | flat (default: directory)
85
92
  --json Machine-readable output
86
93
  --verify Oracle mode: deploy A plus the emitted migration into
@@ -224,6 +231,11 @@ exports.default = async (argv, prompter, _options) => {
224
231
  await (0, inquirerer_1.cliExitWithError)(`Invalid --granularity "${granularityRaw}". Expected one of: ${transform_1.EXPORT_GRANULARITIES.join(', ')}.`);
225
232
  }
226
233
  const granularity = granularityRaw;
234
+ const changeGranularityRaw = argv['change-granularity'] ?? argv.changeGranularity ?? 'object';
235
+ if (!(0, transform_1.isChangeGranularity)(changeGranularityRaw)) {
236
+ await (0, inquirerer_1.cliExitWithError)(`Invalid --change-granularity "${changeGranularityRaw}". Expected one of: ${transform_1.CHANGE_GRANULARITIES.join(', ')}.`);
237
+ }
238
+ const changeGranularity = changeGranularityRaw;
227
239
  const namingRaw = argv.naming ?? 'directory';
228
240
  if (!NAMING_STYLES.includes(namingRaw)) {
229
241
  await (0, inquirerer_1.cliExitWithError)(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
@@ -236,6 +248,10 @@ exports.default = async (argv, prompter, _options) => {
236
248
  const emitModuleDir = typeof emitModuleRaw === 'string' && emitModuleRaw
237
249
  ? path.resolve(cwd, emitModuleRaw)
238
250
  : undefined;
251
+ const appendModuleRaw = argv['append-module'] ?? argv.appendModule;
252
+ const appendModuleDir = typeof appendModuleRaw === 'string' && appendModuleRaw
253
+ ? path.resolve(cwd, appendModuleRaw)
254
+ : undefined;
239
255
  const emitSqlRaw = argv['emit-sql'] ?? argv.emitSql;
240
256
  const emitSql = typeof emitSqlRaw === 'string' && emitSqlRaw
241
257
  ? (emitSqlRaw === module_projections_1.STDOUT_TARGET ? module_projections_1.STDOUT_TARGET : path.resolve(cwd, emitSqlRaw))
@@ -246,6 +262,9 @@ exports.default = async (argv, prompter, _options) => {
246
262
  : undefined;
247
263
  const sqlToStdout = emitSql === module_projections_1.STDOUT_TARGET;
248
264
  const pkgName = argv.pkg || 'diff-migration';
265
+ if (appendModuleDir && (emitModuleDir || emitSql || emitBundle || verify)) {
266
+ await (0, inquirerer_1.cliExitWithError)('--append-module is standalone; it cannot be combined with --emit-migration/--emit-module/--emit-sql/--emit-bundle/--verify.');
267
+ }
249
268
  await (0, transform_1.loadModule)();
250
269
  let sideA;
251
270
  let sideB;
@@ -257,7 +276,7 @@ exports.default = async (argv, prompter, _options) => {
257
276
  await (0, inquirerer_1.cliExitWithError)(err instanceof Error ? err.message : String(err));
258
277
  return;
259
278
  }
260
- const result = (0, transform_1.diffChangeSets)(sideA.changes, sideB.changes, { granularity, style: naming });
279
+ const result = (0, transform_1.diffChangeSets)(sideA.changes, sideB.changes, { granularity, changeGranularity, style: naming });
261
280
  const warnings = [
262
281
  ...sideA.warnings.map(w => `${sideA.label}: ${w}`),
263
282
  ...sideB.warnings.map(w => `${sideB.label}: ${w}`),
@@ -284,6 +303,21 @@ exports.default = async (argv, prompter, _options) => {
284
303
  if (!sqlToStdout)
285
304
  printSummary(result, sideA.label, sideB.label);
286
305
  }
306
+ if (appendModuleDir) {
307
+ const rows = (0, diff_1.deltaChangesToRows)(result.changes);
308
+ if (rows.length === 0) {
309
+ log.info('no migration changes to append (sides are identical).');
310
+ }
311
+ else {
312
+ const appended = (0, transform_1.appendModule)(appendModuleDir, rows);
313
+ for (const w of appended.warnings)
314
+ console.warn(`diff: ${w}`);
315
+ log.success(`appended ${appended.added.length} change(s) to ${appended.dir}` +
316
+ (appended.skipped.length ? ` (${appended.skipped.length} skipped)` : ''));
317
+ }
318
+ prompter.close();
319
+ return argv;
320
+ }
287
321
  let migrationDir;
288
322
  const needModule = Boolean(emitModuleDir || emitSql || emitBundle) || (verify && !result.identical);
289
323
  if (needModule) {
@@ -39,6 +39,7 @@ const transform_1 = require("@pgpmjs/transform");
39
39
  const inquirerer_1 = require("inquirerer");
40
40
  const path = __importStar(require("path"));
41
41
  const emit_package_1 = require("../utils/emit-package");
42
+ const module_projections_1 = require("../utils/module-projections");
42
43
  const log = new logger_1.Logger('import');
43
44
  const importUsageText = `
44
45
  Import Command:
@@ -70,6 +71,9 @@ Options:
70
71
  --pkg <name> Module name for the generated package (required)
71
72
  --granularity <level> Granularity dial: atomic | object | consolidated
72
73
  (default: object)
74
+ --change-granularity <level>
75
+ Change-level distribution: alteration | object | single
76
+ (default: object)
73
77
  --naming <style> Change path naming style: directory | flat (default: directory)
74
78
  --out <dir> Output base directory (default: current directory);
75
79
  the module is written to <out>/<pkg>
@@ -78,14 +82,24 @@ Options:
78
82
  splitting the import into multiple pgpm packages with
79
83
  derived cross-package requires
80
84
  --write Allow overwriting an existing module directory
85
+ --emit-sql <file|-> Also project the imported module into a single linear
86
+ SQL script (- for stdout). Requires a single output
87
+ package (not supported with --partition).
88
+ --emit-bundle <file> Also project the imported module into a
89
+ content-addressed .bundle.tar.gz. Same single-package
90
+ requirement as --emit-sql.
81
91
  --cwd <directory> Working directory (default: current directory)
82
92
  --dry-run Print the resulting plan/paths without writing
83
93
 
94
+ The imported module is the canonical artifact; --emit-sql and --emit-bundle are
95
+ pure projections of it (the same machinery pgpm package and pgpm diff use).
96
+
84
97
  Examples:
85
98
  pgpm import dump.sql --pkg my-app
86
99
  pgpm import dump.sql --pkg my-app --granularity consolidated --naming flat
87
100
  pgpm import ./sql-files --pkg my-app --out ./packages --with-data
88
101
  pgpm import dump.sql --pkg my-app --partition partition.json
102
+ pgpm import dump.sql --pkg my-app --emit-sql my-app.sql
89
103
  `;
90
104
  const NAMING_STYLES = ['directory', 'flat'];
91
105
  exports.default = async (argv, prompter, _options) => {
@@ -107,6 +121,11 @@ exports.default = async (argv, prompter, _options) => {
107
121
  await (0, inquirerer_1.cliExitWithError)(`Invalid --granularity "${granularityRaw}". Expected one of: ${transform_1.EXPORT_GRANULARITIES.join(', ')}.`);
108
122
  }
109
123
  const granularity = granularityRaw;
124
+ const changeGranularityRaw = argv['change-granularity'] ?? argv.changeGranularity ?? 'object';
125
+ if (!(0, transform_1.isChangeGranularity)(changeGranularityRaw)) {
126
+ await (0, inquirerer_1.cliExitWithError)(`Invalid --change-granularity "${changeGranularityRaw}". Expected one of: ${transform_1.CHANGE_GRANULARITIES.join(', ')}.`);
127
+ }
128
+ const changeGranularity = changeGranularityRaw;
110
129
  const namingRaw = argv.naming ?? 'directory';
111
130
  if (!NAMING_STYLES.includes(namingRaw)) {
112
131
  await (0, inquirerer_1.cliExitWithError)(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
@@ -125,6 +144,12 @@ exports.default = async (argv, prompter, _options) => {
125
144
  const withData = Boolean(argv['with-data'] ?? argv.withData);
126
145
  const write = Boolean(argv.write);
127
146
  const dryRun = Boolean(argv['dry-run'] ?? argv.dryRun);
147
+ const emit = (0, module_projections_1.parseEmitProjectionTargets)(argv, cwd);
148
+ const emitRequested = (0, module_projections_1.hasEmitProjection)(emit);
149
+ const sqlToStdout = emit.emitSql === module_projections_1.STDOUT_TARGET;
150
+ if (emitRequested && dryRun) {
151
+ await (0, inquirerer_1.cliExitWithError)('--emit-sql/--emit-bundle cannot be combined with --dry-run.');
152
+ }
128
153
  let source;
129
154
  try {
130
155
  source = (0, import_1.loadDumpSource)(path.resolve(cwd, input));
@@ -133,13 +158,13 @@ exports.default = async (argv, prompter, _options) => {
133
158
  await (0, inquirerer_1.cliExitWithError)(err instanceof Error ? err.message : String(err));
134
159
  return;
135
160
  }
136
- if (source.files.length > 1) {
161
+ if (source.files.length > 1 && !sqlToStdout) {
137
162
  log.info(`concatenated ${source.files.length} .sql files in sorted order`);
138
163
  }
139
164
  for (const warning of (0, import_1.dumpCompatibilityWarnings)(source)) {
140
165
  console.warn(`\nWARNING: ${warning}\n`);
141
166
  }
142
- const result = await (0, import_1.importDumpRows)(source, { granularity, naming, withData });
167
+ const result = await (0, import_1.importDumpRows)(source, { granularity, changeGranularity, naming, withData });
143
168
  let packages;
144
169
  if (partition) {
145
170
  try {
@@ -178,21 +203,33 @@ exports.default = async (argv, prompter, _options) => {
178
203
  }
179
204
  }
180
205
  else {
206
+ if (emitRequested && packages.length !== 1) {
207
+ await (0, inquirerer_1.cliExitWithError)('--emit-sql/--emit-bundle require a single output package; a --partition import emits multiple packages.');
208
+ }
181
209
  for (const pkg of packages) {
182
210
  const guard = (0, emit_package_1.checkOverwrite)(path.join(outBase, pkg.name), source.files[0], write);
183
211
  if (guard) {
184
212
  await (0, inquirerer_1.cliExitWithError)(guard);
185
213
  }
186
214
  }
215
+ let firstDir;
187
216
  for (const pkg of packages) {
188
217
  const dir = (0, emit_package_1.writePackage)(outBase, pkg);
189
- log.success(`wrote ${pkg.rows.length} changes to ${dir}`);
218
+ firstDir ??= dir;
219
+ if (!sqlToStdout) {
220
+ log.success(`wrote ${pkg.rows.length} changes to ${dir}`);
221
+ }
222
+ }
223
+ if (emitRequested && firstDir) {
224
+ await (0, module_projections_1.projectModule)(firstDir, emit, sqlToStdout ? undefined : msg => log.success(msg));
190
225
  }
191
226
  }
192
227
  const { summary } = result;
193
- log.success(`import: ${summary.statements} statement(s) -> ${summary.changes} change(s), ` +
194
- `${summary.skippedPreamble} preamble skipped, ${summary.skippedData} data skipped, ` +
195
- `${summary.misc} in misc, ${result.warnings.length} warning(s)`);
228
+ if (!sqlToStdout) {
229
+ log.success(`import: ${summary.statements} statement(s) -> ${summary.changes} change(s), ` +
230
+ `${summary.skippedPreamble} preamble skipped, ${summary.skippedData} data skipped, ` +
231
+ `${summary.misc} in misc, ${result.warnings.length} warning(s)`);
232
+ }
196
233
  prompter.close();
197
234
  return argv;
198
235
  };
@@ -42,6 +42,7 @@ const inquirerer_1 = require("inquirerer");
42
42
  const path = __importStar(require("path"));
43
43
  const pg_env_1 = require("pg-env");
44
44
  const emit_package_1 = require("../utils/emit-package");
45
+ const module_projections_1 = require("../utils/module-projections");
45
46
  const scratch_db_1 = require("../utils/scratch-db");
46
47
  var emit_package_2 = require("../utils/emit-package");
47
48
  Object.defineProperty(exports, "checkOverwrite", { enumerable: true, get: function () { return emit_package_2.checkOverwrite; } });
@@ -59,6 +60,12 @@ Transform Command:
59
60
  Options:
60
61
  --help, -h Show this help message
61
62
  --granularity <level> Target granularity: atomic | object | consolidated (required)
63
+ --change-granularity <level>
64
+ Change-level distribution: alteration | object | single
65
+ (default: object). With alteration, every ADD COLUMN /
66
+ ADD CONSTRAINT becomes its own change with its own
67
+ deploy/revert/verify and requires. With single, the
68
+ whole module becomes one change.
62
69
  --partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
63
70
  splitting the module into multiple pgpm packages with
64
71
  derived cross-package requires.
@@ -66,14 +73,25 @@ Options:
66
73
  --cwd <directory> Module or workspace directory (default: current directory)
67
74
  --out <dir> Output directory (default: sibling <module>-<granularity>)
68
75
  --write Allow writing over an existing/module directory
76
+ --emit-sql <file|-> Also project the transformed module into a single
77
+ linear SQL script (- for stdout). Requires a single
78
+ output package (no workspace/partition fan-out).
79
+ --emit-bundle <file> Also project the transformed module into a
80
+ content-addressed .bundle.tar.gz. Same single-package
81
+ requirement as --emit-sql.
69
82
  --check Deploy original and transformed output into scratch
70
83
  databases and assert the catalogs are equivalent
71
84
  --dry-run Print the resulting plan/paths without writing
72
85
 
86
+ The transformed module is the canonical artifact; --emit-sql and --emit-bundle
87
+ are pure projections of it (the same machinery pgpm package and pgpm diff use).
88
+
73
89
  Examples:
74
90
  pgpm transform --granularity object
75
91
  pgpm transform --granularity atomic --naming flat --out ./out
76
92
  pgpm transform --granularity object --partition partition.json --check
93
+ pgpm transform --granularity consolidated --emit-sql migration.sql
94
+ pgpm transform --granularity atomic --change-granularity alteration
77
95
  `;
78
96
  const NAMING_STYLES = ['directory', 'flat'];
79
97
  /**
@@ -141,7 +159,7 @@ const runCheck = async (transformed) => {
141
159
  return (0, scratch_db_1.catalogDifferences)(cfgOriginal, cfgTransformed);
142
160
  });
143
161
  };
144
- const transformModule = async (modulePath, granularity, naming, partition, out) => {
162
+ const transformModule = async (modulePath, granularity, changeGranularity, naming, partition, out) => {
145
163
  const source = (0, transform_1.loadModuleSource)(modulePath);
146
164
  const warnings = [...source.warnings];
147
165
  const rows = source.changes.map(change => ({
@@ -150,7 +168,7 @@ const transformModule = async (modulePath, granularity, naming, partition, out)
150
168
  deps: change.dependencies,
151
169
  content: change.deploy
152
170
  }));
153
- const restructured = await (0, transform_1.restructureExportRows)(rows, granularity, { naming });
171
+ const restructured = await (0, transform_1.restructureExportRows)(rows, granularity, { naming, changeGranularity });
154
172
  warnings.push(...restructured.warnings.map(w => `restructure (${granularity}): ${w}`));
155
173
  const outBase = (0, exports.resolveOutBase)(modulePath, out);
156
174
  let packages;
@@ -189,6 +207,11 @@ exports.default = async (argv, prompter, _options) => {
189
207
  await (0, inquirerer_1.cliExitWithError)(`Invalid --granularity "${granularityRaw}". Expected one of: ${transform_1.EXPORT_GRANULARITIES.join(', ')}.`);
190
208
  }
191
209
  const granularity = granularityRaw;
210
+ const changeGranularityRaw = argv['change-granularity'] ?? argv.changeGranularity ?? 'object';
211
+ if (!(0, transform_1.isChangeGranularity)(changeGranularityRaw)) {
212
+ await (0, inquirerer_1.cliExitWithError)(`Invalid --change-granularity "${changeGranularityRaw}". Expected one of: ${transform_1.CHANGE_GRANULARITIES.join(', ')}.`);
213
+ }
214
+ const changeGranularity = changeGranularityRaw;
192
215
  const namingRaw = argv.naming ?? 'directory';
193
216
  if (!NAMING_STYLES.includes(namingRaw)) {
194
217
  await (0, inquirerer_1.cliExitWithError)(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
@@ -208,6 +231,9 @@ exports.default = async (argv, prompter, _options) => {
208
231
  const write = Boolean(argv.write);
209
232
  const check = Boolean(argv.check);
210
233
  const dryRun = Boolean(argv['dry-run'] ?? argv.dryRun);
234
+ const emit = (0, module_projections_1.parseEmitProjectionTargets)(argv, cwd);
235
+ const emitRequested = (0, module_projections_1.hasEmitProjection)(emit);
236
+ const sqlToStdout = emit.emitSql === module_projections_1.STDOUT_TARGET;
211
237
  const pkg = new core_1.PgpmPackage(path.resolve(cwd));
212
238
  let modulePaths;
213
239
  if (pkg.isInModule()) {
@@ -227,10 +253,16 @@ exports.default = async (argv, prompter, _options) => {
227
253
  await (0, inquirerer_1.cliExitWithError)('Not inside a pgpm module or workspace (pass --cwd <dir>).');
228
254
  return;
229
255
  }
256
+ if (emitRequested && modulePaths.length > 1) {
257
+ await (0, inquirerer_1.cliExitWithError)('--emit-sql/--emit-bundle target a single module; they are not supported when transforming a multi-module workspace.');
258
+ }
259
+ if (emitRequested && dryRun) {
260
+ await (0, inquirerer_1.cliExitWithError)('--emit-sql/--emit-bundle cannot be combined with --dry-run.');
261
+ }
230
262
  for (const modulePath of modulePaths) {
231
263
  let transformed;
232
264
  try {
233
- transformed = await transformModule(modulePath, granularity, naming, partition, out);
265
+ transformed = await transformModule(modulePath, granularity, changeGranularity, naming, partition, out);
234
266
  }
235
267
  catch (err) {
236
268
  if (err instanceof transform_1.PartitionCycleError) {
@@ -253,10 +285,20 @@ exports.default = async (argv, prompter, _options) => {
253
285
  await (0, inquirerer_1.cliExitWithError)(guard);
254
286
  }
255
287
  }
288
+ if (emitRequested && transformed.packages.length !== 1) {
289
+ await (0, inquirerer_1.cliExitWithError)('--emit-sql/--emit-bundle require a single output package; a --partition transform emits multiple packages.');
290
+ }
256
291
  const sourceRequires = readControlRequires(modulePath, transformed.name);
292
+ let firstDir;
257
293
  for (const pkgRows of transformed.packages) {
258
294
  const dir = (0, emit_package_1.writePackage)(transformed.outBase, pkgRows, sourceRequires);
259
- log.success(`wrote ${pkgRows.rows.length} changes to ${dir}`);
295
+ firstDir ??= dir;
296
+ if (!sqlToStdout) {
297
+ log.success(`wrote ${pkgRows.rows.length} changes to ${dir}`);
298
+ }
299
+ }
300
+ if (emitRequested && firstDir) {
301
+ await (0, module_projections_1.projectModule)(firstDir, emit, sqlToStdout ? undefined : msg => log.success(msg));
260
302
  }
261
303
  if (check) {
262
304
  log.info('running --check: deploying original and transformed output to scratch databases...');
@@ -1,7 +1,7 @@
1
1
  import { PgpmMigrate } from '@pgpmjs/core';
2
2
  import { deltaChangesToRows, loadDiffSideFromDisk, sqlToDiffChanges } from '@pgpmjs/diff';
3
3
  import { Logger } from '@pgpmjs/logger';
4
- import { diffChangeSets, EXPORT_GRANULARITIES, isExportGranularity, loadModule, withoutColumnOrder, writeModule } from '@pgpmjs/transform';
4
+ import { appendModule, CHANGE_GRANULARITIES, diffChangeSets, EXPORT_GRANULARITIES, isChangeGranularity, isExportGranularity, loadModule, withoutColumnOrder, writeModule } from '@pgpmjs/transform';
5
5
  import { spawn } from 'child_process';
6
6
  import * as fs from 'fs';
7
7
  import { cliExitWithError, extractFirst } from 'inquirerer';
@@ -39,6 +39,10 @@ Options:
39
39
  verify per change, spec-derived paths, graph-derived
40
40
  requires) into <dir>/<pkg>
41
41
  --emit-module <dir> Alias of --emit-migration
42
+ --append-module <dir> Append the delta into an EXISTING pgpm module at
43
+ <dir> (new changes only; existing changes, scripts,
44
+ and .control are left untouched). Standalone: not
45
+ combinable with --emit-*/--verify.
42
46
  --emit-sql <file|-> Also project the delta to a single consolidated SQL
43
47
  file (deparsed in plan order); - writes to stdout
44
48
  --emit-bundle <file> Also project the delta to a content-addressed
@@ -46,6 +50,9 @@ Options:
46
50
  --pkg <name> Emitted migration package name (default: diff-migration)
47
51
  --granularity <level> Granularity for emitted changes: atomic | object |
48
52
  consolidated (default: object)
53
+ --change-granularity <level>
54
+ Change-level distribution for emitted changes:
55
+ alteration | object | single (default: object)
49
56
  --naming <style> Change path naming style: directory | flat (default: directory)
50
57
  --json Machine-readable output
51
58
  --verify Oracle mode: deploy A plus the emitted migration into
@@ -189,6 +196,11 @@ export default async (argv, prompter, _options) => {
189
196
  await cliExitWithError(`Invalid --granularity "${granularityRaw}". Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
190
197
  }
191
198
  const granularity = granularityRaw;
199
+ const changeGranularityRaw = argv['change-granularity'] ?? argv.changeGranularity ?? 'object';
200
+ if (!isChangeGranularity(changeGranularityRaw)) {
201
+ await cliExitWithError(`Invalid --change-granularity "${changeGranularityRaw}". Expected one of: ${CHANGE_GRANULARITIES.join(', ')}.`);
202
+ }
203
+ const changeGranularity = changeGranularityRaw;
192
204
  const namingRaw = argv.naming ?? 'directory';
193
205
  if (!NAMING_STYLES.includes(namingRaw)) {
194
206
  await cliExitWithError(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
@@ -201,6 +213,10 @@ export default async (argv, prompter, _options) => {
201
213
  const emitModuleDir = typeof emitModuleRaw === 'string' && emitModuleRaw
202
214
  ? path.resolve(cwd, emitModuleRaw)
203
215
  : undefined;
216
+ const appendModuleRaw = argv['append-module'] ?? argv.appendModule;
217
+ const appendModuleDir = typeof appendModuleRaw === 'string' && appendModuleRaw
218
+ ? path.resolve(cwd, appendModuleRaw)
219
+ : undefined;
204
220
  const emitSqlRaw = argv['emit-sql'] ?? argv.emitSql;
205
221
  const emitSql = typeof emitSqlRaw === 'string' && emitSqlRaw
206
222
  ? (emitSqlRaw === STDOUT_TARGET ? STDOUT_TARGET : path.resolve(cwd, emitSqlRaw))
@@ -211,6 +227,9 @@ export default async (argv, prompter, _options) => {
211
227
  : undefined;
212
228
  const sqlToStdout = emitSql === STDOUT_TARGET;
213
229
  const pkgName = argv.pkg || 'diff-migration';
230
+ if (appendModuleDir && (emitModuleDir || emitSql || emitBundle || verify)) {
231
+ await cliExitWithError('--append-module is standalone; it cannot be combined with --emit-migration/--emit-module/--emit-sql/--emit-bundle/--verify.');
232
+ }
214
233
  await loadModule();
215
234
  let sideA;
216
235
  let sideB;
@@ -222,7 +241,7 @@ export default async (argv, prompter, _options) => {
222
241
  await cliExitWithError(err instanceof Error ? err.message : String(err));
223
242
  return;
224
243
  }
225
- const result = diffChangeSets(sideA.changes, sideB.changes, { granularity, style: naming });
244
+ const result = diffChangeSets(sideA.changes, sideB.changes, { granularity, changeGranularity, style: naming });
226
245
  const warnings = [
227
246
  ...sideA.warnings.map(w => `${sideA.label}: ${w}`),
228
247
  ...sideB.warnings.map(w => `${sideB.label}: ${w}`),
@@ -249,6 +268,21 @@ export default async (argv, prompter, _options) => {
249
268
  if (!sqlToStdout)
250
269
  printSummary(result, sideA.label, sideB.label);
251
270
  }
271
+ if (appendModuleDir) {
272
+ const rows = deltaChangesToRows(result.changes);
273
+ if (rows.length === 0) {
274
+ log.info('no migration changes to append (sides are identical).');
275
+ }
276
+ else {
277
+ const appended = appendModule(appendModuleDir, rows);
278
+ for (const w of appended.warnings)
279
+ console.warn(`diff: ${w}`);
280
+ log.success(`appended ${appended.added.length} change(s) to ${appended.dir}` +
281
+ (appended.skipped.length ? ` (${appended.skipped.length} skipped)` : ''));
282
+ }
283
+ prompter.close();
284
+ return argv;
285
+ }
252
286
  let migrationDir;
253
287
  const needModule = Boolean(emitModuleDir || emitSql || emitBundle) || (verify && !result.identical);
254
288
  if (needModule) {
@@ -1,9 +1,10 @@
1
1
  import { dumpCompatibilityWarnings, importDumpRows, linkTextualDeps, loadDumpSource } from '@pgpmjs/import';
2
2
  import { Logger } from '@pgpmjs/logger';
3
- import { EXPORT_GRANULARITIES, isExportGranularity, parsePartitionConfig, PartitionCycleError, partitionExportRows } from '@pgpmjs/transform';
3
+ import { CHANGE_GRANULARITIES, EXPORT_GRANULARITIES, isChangeGranularity, isExportGranularity, parsePartitionConfig, PartitionCycleError, partitionExportRows } from '@pgpmjs/transform';
4
4
  import { cliExitWithError } from 'inquirerer';
5
5
  import * as path from 'path';
6
6
  import { checkOverwrite, writePackage } from '../utils/emit-package';
7
+ import { hasEmitProjection, parseEmitProjectionTargets, projectModule, STDOUT_TARGET } from '../utils/module-projections';
7
8
  const log = new Logger('import');
8
9
  const importUsageText = `
9
10
  Import Command:
@@ -35,6 +36,9 @@ Options:
35
36
  --pkg <name> Module name for the generated package (required)
36
37
  --granularity <level> Granularity dial: atomic | object | consolidated
37
38
  (default: object)
39
+ --change-granularity <level>
40
+ Change-level distribution: alteration | object | single
41
+ (default: object)
38
42
  --naming <style> Change path naming style: directory | flat (default: directory)
39
43
  --out <dir> Output base directory (default: current directory);
40
44
  the module is written to <out>/<pkg>
@@ -43,14 +47,24 @@ Options:
43
47
  splitting the import into multiple pgpm packages with
44
48
  derived cross-package requires
45
49
  --write Allow overwriting an existing module directory
50
+ --emit-sql <file|-> Also project the imported module into a single linear
51
+ SQL script (- for stdout). Requires a single output
52
+ package (not supported with --partition).
53
+ --emit-bundle <file> Also project the imported module into a
54
+ content-addressed .bundle.tar.gz. Same single-package
55
+ requirement as --emit-sql.
46
56
  --cwd <directory> Working directory (default: current directory)
47
57
  --dry-run Print the resulting plan/paths without writing
48
58
 
59
+ The imported module is the canonical artifact; --emit-sql and --emit-bundle are
60
+ pure projections of it (the same machinery pgpm package and pgpm diff use).
61
+
49
62
  Examples:
50
63
  pgpm import dump.sql --pkg my-app
51
64
  pgpm import dump.sql --pkg my-app --granularity consolidated --naming flat
52
65
  pgpm import ./sql-files --pkg my-app --out ./packages --with-data
53
66
  pgpm import dump.sql --pkg my-app --partition partition.json
67
+ pgpm import dump.sql --pkg my-app --emit-sql my-app.sql
54
68
  `;
55
69
  const NAMING_STYLES = ['directory', 'flat'];
56
70
  export default async (argv, prompter, _options) => {
@@ -72,6 +86,11 @@ export default async (argv, prompter, _options) => {
72
86
  await cliExitWithError(`Invalid --granularity "${granularityRaw}". Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
73
87
  }
74
88
  const granularity = granularityRaw;
89
+ const changeGranularityRaw = argv['change-granularity'] ?? argv.changeGranularity ?? 'object';
90
+ if (!isChangeGranularity(changeGranularityRaw)) {
91
+ await cliExitWithError(`Invalid --change-granularity "${changeGranularityRaw}". Expected one of: ${CHANGE_GRANULARITIES.join(', ')}.`);
92
+ }
93
+ const changeGranularity = changeGranularityRaw;
75
94
  const namingRaw = argv.naming ?? 'directory';
76
95
  if (!NAMING_STYLES.includes(namingRaw)) {
77
96
  await cliExitWithError(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
@@ -90,6 +109,12 @@ export default async (argv, prompter, _options) => {
90
109
  const withData = Boolean(argv['with-data'] ?? argv.withData);
91
110
  const write = Boolean(argv.write);
92
111
  const dryRun = Boolean(argv['dry-run'] ?? argv.dryRun);
112
+ const emit = parseEmitProjectionTargets(argv, cwd);
113
+ const emitRequested = hasEmitProjection(emit);
114
+ const sqlToStdout = emit.emitSql === STDOUT_TARGET;
115
+ if (emitRequested && dryRun) {
116
+ await cliExitWithError('--emit-sql/--emit-bundle cannot be combined with --dry-run.');
117
+ }
93
118
  let source;
94
119
  try {
95
120
  source = loadDumpSource(path.resolve(cwd, input));
@@ -98,13 +123,13 @@ export default async (argv, prompter, _options) => {
98
123
  await cliExitWithError(err instanceof Error ? err.message : String(err));
99
124
  return;
100
125
  }
101
- if (source.files.length > 1) {
126
+ if (source.files.length > 1 && !sqlToStdout) {
102
127
  log.info(`concatenated ${source.files.length} .sql files in sorted order`);
103
128
  }
104
129
  for (const warning of dumpCompatibilityWarnings(source)) {
105
130
  console.warn(`\nWARNING: ${warning}\n`);
106
131
  }
107
- const result = await importDumpRows(source, { granularity, naming, withData });
132
+ const result = await importDumpRows(source, { granularity, changeGranularity, naming, withData });
108
133
  let packages;
109
134
  if (partition) {
110
135
  try {
@@ -143,21 +168,33 @@ export default async (argv, prompter, _options) => {
143
168
  }
144
169
  }
145
170
  else {
171
+ if (emitRequested && packages.length !== 1) {
172
+ await cliExitWithError('--emit-sql/--emit-bundle require a single output package; a --partition import emits multiple packages.');
173
+ }
146
174
  for (const pkg of packages) {
147
175
  const guard = checkOverwrite(path.join(outBase, pkg.name), source.files[0], write);
148
176
  if (guard) {
149
177
  await cliExitWithError(guard);
150
178
  }
151
179
  }
180
+ let firstDir;
152
181
  for (const pkg of packages) {
153
182
  const dir = writePackage(outBase, pkg);
154
- log.success(`wrote ${pkg.rows.length} changes to ${dir}`);
183
+ firstDir ??= dir;
184
+ if (!sqlToStdout) {
185
+ log.success(`wrote ${pkg.rows.length} changes to ${dir}`);
186
+ }
187
+ }
188
+ if (emitRequested && firstDir) {
189
+ await projectModule(firstDir, emit, sqlToStdout ? undefined : msg => log.success(msg));
155
190
  }
156
191
  }
157
192
  const { summary } = result;
158
- log.success(`import: ${summary.statements} statement(s) -> ${summary.changes} change(s), ` +
159
- `${summary.skippedPreamble} preamble skipped, ${summary.skippedData} data skipped, ` +
160
- `${summary.misc} in misc, ${result.warnings.length} warning(s)`);
193
+ if (!sqlToStdout) {
194
+ log.success(`import: ${summary.statements} statement(s) -> ${summary.changes} change(s), ` +
195
+ `${summary.skippedPreamble} preamble skipped, ${summary.skippedData} data skipped, ` +
196
+ `${summary.misc} in misc, ${result.warnings.length} warning(s)`);
197
+ }
161
198
  prompter.close();
162
199
  return argv;
163
200
  };
@@ -1,11 +1,12 @@
1
1
  import { PgpmMigrate, PgpmPackage } from '@pgpmjs/core';
2
2
  import { Logger } from '@pgpmjs/logger';
3
- import { EXPORT_GRANULARITIES, isExportGranularity, loadModuleSource, parsePartitionConfig, PartitionCycleError, partitionExportRows, restructureExportRows } from '@pgpmjs/transform';
3
+ import { CHANGE_GRANULARITIES, EXPORT_GRANULARITIES, isChangeGranularity, isExportGranularity, loadModuleSource, parsePartitionConfig, PartitionCycleError, partitionExportRows, restructureExportRows } from '@pgpmjs/transform';
4
4
  import * as fs from 'fs';
5
5
  import { cliExitWithError } from 'inquirerer';
6
6
  import * as path from 'path';
7
7
  import { getPgEnvOptions } from 'pg-env';
8
8
  import { checkOverwrite, writePackage } from '../utils/emit-package';
9
+ import { hasEmitProjection, parseEmitProjectionTargets, projectModule, STDOUT_TARGET } from '../utils/module-projections';
9
10
  import { catalogDifferences, withScratchDatabases } from '../utils/scratch-db';
10
11
  export { checkOverwrite } from '../utils/emit-package';
11
12
  const log = new Logger('transform');
@@ -22,6 +23,12 @@ Transform Command:
22
23
  Options:
23
24
  --help, -h Show this help message
24
25
  --granularity <level> Target granularity: atomic | object | consolidated (required)
26
+ --change-granularity <level>
27
+ Change-level distribution: alteration | object | single
28
+ (default: object). With alteration, every ADD COLUMN /
29
+ ADD CONSTRAINT becomes its own change with its own
30
+ deploy/revert/verify and requires. With single, the
31
+ whole module becomes one change.
25
32
  --partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
26
33
  splitting the module into multiple pgpm packages with
27
34
  derived cross-package requires.
@@ -29,14 +36,25 @@ Options:
29
36
  --cwd <directory> Module or workspace directory (default: current directory)
30
37
  --out <dir> Output directory (default: sibling <module>-<granularity>)
31
38
  --write Allow writing over an existing/module directory
39
+ --emit-sql <file|-> Also project the transformed module into a single
40
+ linear SQL script (- for stdout). Requires a single
41
+ output package (no workspace/partition fan-out).
42
+ --emit-bundle <file> Also project the transformed module into a
43
+ content-addressed .bundle.tar.gz. Same single-package
44
+ requirement as --emit-sql.
32
45
  --check Deploy original and transformed output into scratch
33
46
  databases and assert the catalogs are equivalent
34
47
  --dry-run Print the resulting plan/paths without writing
35
48
 
49
+ The transformed module is the canonical artifact; --emit-sql and --emit-bundle
50
+ are pure projections of it (the same machinery pgpm package and pgpm diff use).
51
+
36
52
  Examples:
37
53
  pgpm transform --granularity object
38
54
  pgpm transform --granularity atomic --naming flat --out ./out
39
55
  pgpm transform --granularity object --partition partition.json --check
56
+ pgpm transform --granularity consolidated --emit-sql migration.sql
57
+ pgpm transform --granularity atomic --change-granularity alteration
40
58
  `;
41
59
  const NAMING_STYLES = ['directory', 'flat'];
42
60
  /**
@@ -102,7 +120,7 @@ const runCheck = async (transformed) => {
102
120
  return catalogDifferences(cfgOriginal, cfgTransformed);
103
121
  });
104
122
  };
105
- const transformModule = async (modulePath, granularity, naming, partition, out) => {
123
+ const transformModule = async (modulePath, granularity, changeGranularity, naming, partition, out) => {
106
124
  const source = loadModuleSource(modulePath);
107
125
  const warnings = [...source.warnings];
108
126
  const rows = source.changes.map(change => ({
@@ -111,7 +129,7 @@ const transformModule = async (modulePath, granularity, naming, partition, out)
111
129
  deps: change.dependencies,
112
130
  content: change.deploy
113
131
  }));
114
- const restructured = await restructureExportRows(rows, granularity, { naming });
132
+ const restructured = await restructureExportRows(rows, granularity, { naming, changeGranularity });
115
133
  warnings.push(...restructured.warnings.map(w => `restructure (${granularity}): ${w}`));
116
134
  const outBase = resolveOutBase(modulePath, out);
117
135
  let packages;
@@ -150,6 +168,11 @@ export default async (argv, prompter, _options) => {
150
168
  await cliExitWithError(`Invalid --granularity "${granularityRaw}". Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
151
169
  }
152
170
  const granularity = granularityRaw;
171
+ const changeGranularityRaw = argv['change-granularity'] ?? argv.changeGranularity ?? 'object';
172
+ if (!isChangeGranularity(changeGranularityRaw)) {
173
+ await cliExitWithError(`Invalid --change-granularity "${changeGranularityRaw}". Expected one of: ${CHANGE_GRANULARITIES.join(', ')}.`);
174
+ }
175
+ const changeGranularity = changeGranularityRaw;
153
176
  const namingRaw = argv.naming ?? 'directory';
154
177
  if (!NAMING_STYLES.includes(namingRaw)) {
155
178
  await cliExitWithError(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
@@ -169,6 +192,9 @@ export default async (argv, prompter, _options) => {
169
192
  const write = Boolean(argv.write);
170
193
  const check = Boolean(argv.check);
171
194
  const dryRun = Boolean(argv['dry-run'] ?? argv.dryRun);
195
+ const emit = parseEmitProjectionTargets(argv, cwd);
196
+ const emitRequested = hasEmitProjection(emit);
197
+ const sqlToStdout = emit.emitSql === STDOUT_TARGET;
172
198
  const pkg = new PgpmPackage(path.resolve(cwd));
173
199
  let modulePaths;
174
200
  if (pkg.isInModule()) {
@@ -188,10 +214,16 @@ export default async (argv, prompter, _options) => {
188
214
  await cliExitWithError('Not inside a pgpm module or workspace (pass --cwd <dir>).');
189
215
  return;
190
216
  }
217
+ if (emitRequested && modulePaths.length > 1) {
218
+ await cliExitWithError('--emit-sql/--emit-bundle target a single module; they are not supported when transforming a multi-module workspace.');
219
+ }
220
+ if (emitRequested && dryRun) {
221
+ await cliExitWithError('--emit-sql/--emit-bundle cannot be combined with --dry-run.');
222
+ }
191
223
  for (const modulePath of modulePaths) {
192
224
  let transformed;
193
225
  try {
194
- transformed = await transformModule(modulePath, granularity, naming, partition, out);
226
+ transformed = await transformModule(modulePath, granularity, changeGranularity, naming, partition, out);
195
227
  }
196
228
  catch (err) {
197
229
  if (err instanceof PartitionCycleError) {
@@ -214,10 +246,20 @@ export default async (argv, prompter, _options) => {
214
246
  await cliExitWithError(guard);
215
247
  }
216
248
  }
249
+ if (emitRequested && transformed.packages.length !== 1) {
250
+ await cliExitWithError('--emit-sql/--emit-bundle require a single output package; a --partition transform emits multiple packages.');
251
+ }
217
252
  const sourceRequires = readControlRequires(modulePath, transformed.name);
253
+ let firstDir;
218
254
  for (const pkgRows of transformed.packages) {
219
255
  const dir = writePackage(transformed.outBase, pkgRows, sourceRequires);
220
- log.success(`wrote ${pkgRows.rows.length} changes to ${dir}`);
256
+ firstDir ??= dir;
257
+ if (!sqlToStdout) {
258
+ log.success(`wrote ${pkgRows.rows.length} changes to ${dir}`);
259
+ }
260
+ }
261
+ if (emitRequested && firstDir) {
262
+ await projectModule(firstDir, emit, sqlToStdout ? undefined : msg => log.success(msg));
221
263
  }
222
264
  if (check) {
223
265
  log.info('running --check: deploying original and transformed output to scratch databases...');
@@ -45,3 +45,38 @@ export const emitModuleBundle = async (moduleDir, target) => {
45
45
  fs.mkdirSync(path.dirname(resolved), { recursive: true });
46
46
  writeBundleArchiveFile(bundle, resolved);
47
47
  };
48
+ /**
49
+ * Parse the shared `--emit-sql` / `--emit-bundle` projection flags off a parsed
50
+ * argv, resolving file targets against `cwd` (the `-` stdout sentinel for SQL is
51
+ * preserved). Keeps every command's projection flags identical.
52
+ */
53
+ export const parseEmitProjectionTargets = (argv, cwd) => {
54
+ const sqlRaw = argv['emit-sql'] ?? argv.emitSql;
55
+ const emitSql = typeof sqlRaw === 'string' && sqlRaw
56
+ ? sqlRaw === STDOUT_TARGET
57
+ ? STDOUT_TARGET
58
+ : path.resolve(cwd, sqlRaw)
59
+ : undefined;
60
+ const bundleRaw = argv['emit-bundle'] ?? argv.emitBundle;
61
+ const emitBundle = typeof bundleRaw === 'string' && bundleRaw ? path.resolve(cwd, bundleRaw) : undefined;
62
+ return { emitSql, emitBundle };
63
+ };
64
+ /** Whether any projection target was requested. */
65
+ export const hasEmitProjection = (targets) => Boolean(targets.emitSql || targets.emitBundle);
66
+ /**
67
+ * Run the requested SQL/bundle projections against a written module directory.
68
+ * `onSuccess` (when provided) is invoked with a human-readable line per emitted
69
+ * artifact; it is skipped for stdout SQL so the stream stays valid SQL.
70
+ */
71
+ export const projectModule = async (moduleDir, targets, onSuccess) => {
72
+ if (targets.emitSql) {
73
+ await emitModuleSql(moduleDir, targets.emitSql);
74
+ if (targets.emitSql !== STDOUT_TARGET) {
75
+ onSuccess?.(`wrote linear SQL to ${targets.emitSql}`);
76
+ }
77
+ }
78
+ if (targets.emitBundle) {
79
+ await emitModuleBundle(moduleDir, targets.emitBundle);
80
+ onSuccess?.(`wrote bundle to ${targets.emitBundle}`);
81
+ }
82
+ };
@@ -13,8 +13,8 @@
13
13
  * `PgpmMigrate` from `@pgpmjs/core`, and `core` already depends on `transform`
14
14
  * — hosting the oracle in `transform` would close that cycle.
15
15
  */
16
- import { diffCatalogSnapshots, snapshotCatalog } from '@pgpmjs/transform';
17
16
  import { Logger } from '@pgpmjs/logger';
17
+ import { diffCatalogSnapshots, snapshotCatalog } from '@pgpmjs/transform';
18
18
  import { getPgPool } from 'pg-cache';
19
19
  const log = new Logger('scratch-db');
20
20
  const createScratchDb = async (config, dbName) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgpm",
3
- "version": "5.20.0",
3
+ "version": "5.21.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "PostgreSQL Package Manager - Database migration and package management CLI",
6
6
  "main": "index.js",
@@ -46,14 +46,14 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@inquirerer/utils": "^3.3.9",
49
- "@pgpmjs/core": "^7.21.3",
50
- "@pgpmjs/diff": "^0.1.1",
49
+ "@pgpmjs/core": "^7.21.4",
50
+ "@pgpmjs/diff": "^0.1.2",
51
51
  "@pgpmjs/env": "^2.40.3",
52
- "@pgpmjs/export": "^1.15.0",
53
- "@pgpmjs/import": "^0.1.1",
52
+ "@pgpmjs/export": "^1.15.1",
53
+ "@pgpmjs/import": "^0.2.0",
54
54
  "@pgpmjs/logger": "^2.23.3",
55
- "@pgpmjs/slice": "^0.13.8",
56
- "@pgpmjs/transform": "^0.19.0",
55
+ "@pgpmjs/slice": "^0.13.9",
56
+ "@pgpmjs/transform": "^0.20.0",
57
57
  "@pgpmjs/types": "^2.49.3",
58
58
  "@pgsql/quotes": "^18.2.0",
59
59
  "appstash": "^0.7.0",
@@ -80,5 +80,5 @@
80
80
  "pg",
81
81
  "pgsql"
82
82
  ],
83
- "gitHead": "c20b6be96b39bf71c0e2df0f4b51e05deacd006b"
83
+ "gitHead": "e6a7a57f88c9deb2a4acecaba2451320d1be8203"
84
84
  }
@@ -13,3 +13,24 @@ export declare const emitModuleSql: (moduleDir: string, target: string) => Promi
13
13
  * stores beside the packaged SQL, but written to an arbitrary path.
14
14
  */
15
15
  export declare const emitModuleBundle: (moduleDir: string, target: string) => Promise<void>;
16
+ /** Resolved `--emit-sql` / `--emit-bundle` projection targets for a command. */
17
+ export interface EmitProjectionTargets {
18
+ /** Linear SQL target (absolute path, or `-` for stdout). */
19
+ emitSql?: string;
20
+ /** Bundle archive target (absolute path). */
21
+ emitBundle?: string;
22
+ }
23
+ /**
24
+ * Parse the shared `--emit-sql` / `--emit-bundle` projection flags off a parsed
25
+ * argv, resolving file targets against `cwd` (the `-` stdout sentinel for SQL is
26
+ * preserved). Keeps every command's projection flags identical.
27
+ */
28
+ export declare const parseEmitProjectionTargets: (argv: Record<string, unknown>, cwd: string) => EmitProjectionTargets;
29
+ /** Whether any projection target was requested. */
30
+ export declare const hasEmitProjection: (targets: EmitProjectionTargets) => boolean;
31
+ /**
32
+ * Run the requested SQL/bundle projections against a written module directory.
33
+ * `onSuccess` (when provided) is invoked with a human-readable line per emitted
34
+ * artifact; it is skipped for stdout SQL so the stream stays valid SQL.
35
+ */
36
+ export declare const projectModule: (moduleDir: string, targets: EmitProjectionTargets, onSuccess?: (message: string) => void) => Promise<void>;
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.emitModuleBundle = exports.emitModuleSql = exports.STDOUT_TARGET = void 0;
36
+ exports.projectModule = exports.hasEmitProjection = exports.parseEmitProjectionTargets = exports.emitModuleBundle = exports.emitModuleSql = exports.STDOUT_TARGET = void 0;
37
37
  /**
38
38
  * Projections of an emitted pgpm module directory.
39
39
  *
@@ -83,3 +83,41 @@ const emitModuleBundle = async (moduleDir, target) => {
83
83
  (0, core_1.writeBundleArchiveFile)(bundle, resolved);
84
84
  };
85
85
  exports.emitModuleBundle = emitModuleBundle;
86
+ /**
87
+ * Parse the shared `--emit-sql` / `--emit-bundle` projection flags off a parsed
88
+ * argv, resolving file targets against `cwd` (the `-` stdout sentinel for SQL is
89
+ * preserved). Keeps every command's projection flags identical.
90
+ */
91
+ const parseEmitProjectionTargets = (argv, cwd) => {
92
+ const sqlRaw = argv['emit-sql'] ?? argv.emitSql;
93
+ const emitSql = typeof sqlRaw === 'string' && sqlRaw
94
+ ? sqlRaw === exports.STDOUT_TARGET
95
+ ? exports.STDOUT_TARGET
96
+ : path.resolve(cwd, sqlRaw)
97
+ : undefined;
98
+ const bundleRaw = argv['emit-bundle'] ?? argv.emitBundle;
99
+ const emitBundle = typeof bundleRaw === 'string' && bundleRaw ? path.resolve(cwd, bundleRaw) : undefined;
100
+ return { emitSql, emitBundle };
101
+ };
102
+ exports.parseEmitProjectionTargets = parseEmitProjectionTargets;
103
+ /** Whether any projection target was requested. */
104
+ const hasEmitProjection = (targets) => Boolean(targets.emitSql || targets.emitBundle);
105
+ exports.hasEmitProjection = hasEmitProjection;
106
+ /**
107
+ * Run the requested SQL/bundle projections against a written module directory.
108
+ * `onSuccess` (when provided) is invoked with a human-readable line per emitted
109
+ * artifact; it is skipped for stdout SQL so the stream stays valid SQL.
110
+ */
111
+ const projectModule = async (moduleDir, targets, onSuccess) => {
112
+ if (targets.emitSql) {
113
+ await (0, exports.emitModuleSql)(moduleDir, targets.emitSql);
114
+ if (targets.emitSql !== exports.STDOUT_TARGET) {
115
+ onSuccess?.(`wrote linear SQL to ${targets.emitSql}`);
116
+ }
117
+ }
118
+ if (targets.emitBundle) {
119
+ await (0, exports.emitModuleBundle)(moduleDir, targets.emitBundle);
120
+ onSuccess?.(`wrote bundle to ${targets.emitBundle}`);
121
+ }
122
+ };
123
+ exports.projectModule = projectModule;
@@ -1,18 +1,3 @@
1
- /**
2
- * Scratch-database catalog oracle shared by the dials commands that prove
3
- * structural equivalence (`pgpm transform --check`, `pgpm diff --verify`).
4
- *
5
- * Both commands do the same dance: create throwaway databases, deploy a
6
- * schema into each, snapshot the catalogs, and diff them — always dropping the
7
- * scratch databases afterwards even on failure. This centralizes the lifecycle
8
- * (`withScratchDatabases`) and the snapshot/compare step (`catalogDifferences`)
9
- * so each command only supplies what is genuinely command-specific: how it
10
- * deploys a side.
11
- *
12
- * It lives in the CLI (not `@pgpmjs/transform`) because deploying touches
13
- * `PgpmMigrate` from `@pgpmjs/core`, and `core` already depends on `transform`
14
- * — hosting the oracle in `transform` would close that cycle.
15
- */
16
1
  import { CatalogSnapshot } from '@pgpmjs/transform';
17
2
  import type { PgConfig } from 'pg-env';
18
3
  /**
@@ -16,8 +16,8 @@ exports.catalogDifferences = exports.withScratchDatabases = void 0;
16
16
  * `PgpmMigrate` from `@pgpmjs/core`, and `core` already depends on `transform`
17
17
  * — hosting the oracle in `transform` would close that cycle.
18
18
  */
19
- const transform_1 = require("@pgpmjs/transform");
20
19
  const logger_1 = require("@pgpmjs/logger");
20
+ const transform_1 = require("@pgpmjs/transform");
21
21
  const pg_cache_1 = require("pg-cache");
22
22
  const log = new logger_1.Logger('scratch-db');
23
23
  const createScratchDb = async (config, dbName) => {