pgpm 5.19.0 → 5.20.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.
Files changed (46) hide show
  1. package/commands/cache.js +1 -1
  2. package/commands/diff.d.ts +3 -0
  3. package/commands/diff.js +325 -0
  4. package/commands/dump.js +3 -3
  5. package/commands/export.js +24 -5
  6. package/commands/import.d.ts +3 -0
  7. package/commands/import.js +198 -0
  8. package/commands/init/boilerplate.js +1 -1
  9. package/commands/init/index.js +3 -3
  10. package/commands/package.js +0 -1
  11. package/commands/slice.js +1 -1
  12. package/commands/test-packages.js +1 -1
  13. package/commands/transform.d.ts +13 -0
  14. package/commands/transform.js +275 -0
  15. package/commands/update.js +1 -1
  16. package/commands.js +7 -0
  17. package/esm/commands/cache.js +1 -1
  18. package/esm/commands/diff.js +290 -0
  19. package/esm/commands/dump.js +3 -3
  20. package/esm/commands/export.js +22 -3
  21. package/esm/commands/import.js +163 -0
  22. package/esm/commands/init/boilerplate.js +1 -1
  23. package/esm/commands/init/index.js +3 -3
  24. package/esm/commands/package.js +0 -1
  25. package/esm/commands/slice.js +1 -1
  26. package/esm/commands/test-packages.js +1 -1
  27. package/esm/commands/transform.js +236 -0
  28. package/esm/commands/update.js +1 -1
  29. package/esm/commands.js +7 -0
  30. package/esm/utils/display.js +3 -0
  31. package/esm/utils/driver.js +1 -1
  32. package/esm/utils/emit-package.js +1 -0
  33. package/esm/utils/engine.js +1 -1
  34. package/esm/utils/module-projections.js +47 -0
  35. package/esm/utils/scratch-db.js +61 -0
  36. package/package.json +14 -12
  37. package/utils/display.d.ts +1 -1
  38. package/utils/display.js +3 -0
  39. package/utils/driver.js +1 -1
  40. package/utils/emit-package.d.ts +9 -0
  41. package/utils/emit-package.js +7 -0
  42. package/utils/engine.js +1 -1
  43. package/utils/module-projections.d.ts +15 -0
  44. package/utils/module-projections.js +85 -0
  45. package/utils/scratch-db.d.ts +31 -0
  46. package/utils/scratch-db.js +66 -0
package/commands/slice.js CHANGED
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@pgpmjs/core");
4
4
  const slice_1 = require("@pgpmjs/slice");
5
5
  const types_1 = require("@pgpmjs/types");
6
- const path_1 = require("path");
7
6
  const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
8
  const sliceUsageText = `
9
9
  Slice Command:
10
10
 
@@ -7,8 +7,8 @@ const core_1 = require("@pgpmjs/core");
7
7
  const env_1 = require("@pgpmjs/env");
8
8
  const logger_1 = require("@pgpmjs/logger");
9
9
  const path_1 = __importDefault(require("path"));
10
- const pg_env_1 = require("pg-env");
11
10
  const pg_cache_1 = require("pg-cache");
11
+ const pg_env_1 = require("pg-env");
12
12
  const log = new logger_1.Logger('test-packages');
13
13
  // ANSI color codes
14
14
  const RED = '\x1b[0;31m';
@@ -0,0 +1,13 @@
1
+ import { PartitionedPackageRows } from '@pgpmjs/transform';
2
+ import { CLIOptions, Inquirerer, ParsedArgs } from 'inquirerer';
3
+ export { checkOverwrite } from '../utils/emit-package';
4
+ /**
5
+ * Resolve the base directory package dir(s) are created in. Without --out,
6
+ * packages are written as siblings of the source module — a plain transform
7
+ * of `my-mod` at granularity `object` lands in `../my-mod-object`.
8
+ */
9
+ export declare const resolveOutBase: (modulePath: string, out?: string) => string;
10
+ /** Order partition packages so prerequisites come before dependents. */
11
+ export declare const orderPackages: (packages: PartitionedPackageRows[]) => PartitionedPackageRows[];
12
+ declare const _default: (argv: Partial<ParsedArgs>, prompter: Inquirerer, _options: CLIOptions) => Promise<Partial<ParsedArgs>>;
13
+ export default _default;
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.orderPackages = exports.resolveOutBase = exports.checkOverwrite = void 0;
37
+ const core_1 = require("@pgpmjs/core");
38
+ const logger_1 = require("@pgpmjs/logger");
39
+ const transform_1 = require("@pgpmjs/transform");
40
+ const fs = __importStar(require("fs"));
41
+ const inquirerer_1 = require("inquirerer");
42
+ const path = __importStar(require("path"));
43
+ const pg_env_1 = require("pg-env");
44
+ const emit_package_1 = require("../utils/emit-package");
45
+ const scratch_db_1 = require("../utils/scratch-db");
46
+ var emit_package_2 = require("../utils/emit-package");
47
+ Object.defineProperty(exports, "checkOverwrite", { enumerable: true, get: function () { return emit_package_2.checkOverwrite; } });
48
+ const log = new logger_1.Logger('transform');
49
+ const transformUsageText = `
50
+ Transform Command:
51
+
52
+ pgpm transform --granularity <atomic|object|consolidated> [OPTIONS]
53
+
54
+ Re-dial an existing pgpm module (or every module in a workspace) through the
55
+ dials pipeline: flatten the deploy scripts in plan order, re-project them at
56
+ the requested granularity with spec-derived change paths, graph-derived
57
+ requires, and generated revert/verify scripts.
58
+
59
+ Options:
60
+ --help, -h Show this help message
61
+ --granularity <level> Target granularity: atomic | object | consolidated (required)
62
+ --partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
63
+ splitting the module into multiple pgpm packages with
64
+ derived cross-package requires.
65
+ --naming <style> Change path naming style: directory | flat (default: directory)
66
+ --cwd <directory> Module or workspace directory (default: current directory)
67
+ --out <dir> Output directory (default: sibling <module>-<granularity>)
68
+ --write Allow writing over an existing/module directory
69
+ --check Deploy original and transformed output into scratch
70
+ databases and assert the catalogs are equivalent
71
+ --dry-run Print the resulting plan/paths without writing
72
+
73
+ Examples:
74
+ pgpm transform --granularity object
75
+ pgpm transform --granularity atomic --naming flat --out ./out
76
+ pgpm transform --granularity object --partition partition.json --check
77
+ `;
78
+ const NAMING_STYLES = ['directory', 'flat'];
79
+ /**
80
+ * Resolve the base directory package dir(s) are created in. Without --out,
81
+ * packages are written as siblings of the source module — a plain transform
82
+ * of `my-mod` at granularity `object` lands in `../my-mod-object`.
83
+ */
84
+ const resolveOutBase = (modulePath, out) => {
85
+ if (out)
86
+ return path.resolve(out);
87
+ return path.dirname(modulePath);
88
+ };
89
+ exports.resolveOutBase = resolveOutBase;
90
+ /** Order partition packages so prerequisites come before dependents. */
91
+ const orderPackages = (packages) => {
92
+ const byName = new Map(packages.map(pkg => [pkg.name, pkg]));
93
+ const ordered = [];
94
+ const seen = new Set();
95
+ const visit = (pkg) => {
96
+ if (seen.has(pkg.name))
97
+ return;
98
+ seen.add(pkg.name);
99
+ for (const req of pkg.requires) {
100
+ const dep = byName.get(req);
101
+ if (dep)
102
+ visit(dep);
103
+ }
104
+ ordered.push(pkg);
105
+ };
106
+ for (const pkg of packages)
107
+ visit(pkg);
108
+ return ordered;
109
+ };
110
+ exports.orderPackages = orderPackages;
111
+ const readControlRequires = (modulePath, name) => {
112
+ const controlPath = path.join(modulePath, `${name}.control`);
113
+ if (!fs.existsSync(controlPath))
114
+ return [];
115
+ const match = fs.readFileSync(controlPath, 'utf-8').match(/^requires\s*=\s*'([^']*)'\s*$/m);
116
+ if (!match)
117
+ return [];
118
+ return match[1].split(',').map(s => s.trim()).filter(Boolean);
119
+ };
120
+ const deployModules = async (config, modulePaths) => {
121
+ const client = new core_1.PgpmMigrate(config);
122
+ for (const modulePath of modulePaths) {
123
+ const result = await client.deploy({ modulePath });
124
+ if (result.failed) {
125
+ throw new Error(`deploy failed at change ${result.failed} (module ${modulePath})`);
126
+ }
127
+ }
128
+ };
129
+ /**
130
+ * Prove the transform is structurally lossless: deploy the original module and
131
+ * the transformed package(s) into two scratch databases and compare catalogs.
132
+ */
133
+ const runCheck = async (transformed) => {
134
+ const config = (0, pg_env_1.getPgEnvOptions)();
135
+ const stamp = Date.now();
136
+ const names = [`pgpm_transform_check_a_${stamp}`, `pgpm_transform_check_b_${stamp}`];
137
+ return (0, scratch_db_1.withScratchDatabases)(config, names, async ([cfgOriginal, cfgTransformed]) => {
138
+ await deployModules(cfgOriginal, [transformed.modulePath]);
139
+ const orderedDirs = (0, exports.orderPackages)(transformed.packages).map(pkg => path.join(transformed.outBase, pkg.name));
140
+ await deployModules(cfgTransformed, orderedDirs);
141
+ return (0, scratch_db_1.catalogDifferences)(cfgOriginal, cfgTransformed);
142
+ });
143
+ };
144
+ const transformModule = async (modulePath, granularity, naming, partition, out) => {
145
+ const source = (0, transform_1.loadModuleSource)(modulePath);
146
+ const warnings = [...source.warnings];
147
+ const rows = source.changes.map(change => ({
148
+ name: change.name,
149
+ deploy: change.name,
150
+ deps: change.dependencies,
151
+ content: change.deploy
152
+ }));
153
+ const restructured = await (0, transform_1.restructureExportRows)(rows, granularity, { naming });
154
+ warnings.push(...restructured.warnings.map(w => `restructure (${granularity}): ${w}`));
155
+ const outBase = (0, exports.resolveOutBase)(modulePath, out);
156
+ let packages;
157
+ if (partition) {
158
+ const result = await (0, transform_1.partitionExportRows)(restructured.rows, partition);
159
+ warnings.push(...result.warnings.map(w => `partition: ${w}`));
160
+ packages = result.packages;
161
+ }
162
+ else {
163
+ // Distinct package name so the output can live beside the source module
164
+ // in the same workspace without colliding.
165
+ packages = [{ name: `${source.name}-${granularity}`, requires: [], rows: restructured.rows }];
166
+ }
167
+ return { modulePath, name: source.name, outBase, packages, warnings };
168
+ };
169
+ const printDryRun = (transformed) => {
170
+ console.log(`module ${transformed.name} (${transformed.modulePath})`);
171
+ for (const pkg of transformed.packages) {
172
+ console.log(` package ${pkg.name} -> ${path.join(transformed.outBase, pkg.name)}`);
173
+ for (const row of pkg.rows) {
174
+ const deps = row.deps?.length ? ` [${row.deps.join(' ')}]` : '';
175
+ console.log(` ${row.deploy}${deps}`);
176
+ }
177
+ }
178
+ };
179
+ exports.default = async (argv, prompter, _options) => {
180
+ if (argv.help || argv.h) {
181
+ console.log(transformUsageText);
182
+ process.exit(0);
183
+ }
184
+ const granularityRaw = argv.granularity;
185
+ if (granularityRaw === undefined) {
186
+ await (0, inquirerer_1.cliExitWithError)(`--granularity is required. Expected one of: ${transform_1.EXPORT_GRANULARITIES.join(', ')}.`);
187
+ }
188
+ if (!(0, transform_1.isExportGranularity)(granularityRaw)) {
189
+ await (0, inquirerer_1.cliExitWithError)(`Invalid --granularity "${granularityRaw}". Expected one of: ${transform_1.EXPORT_GRANULARITIES.join(', ')}.`);
190
+ }
191
+ const granularity = granularityRaw;
192
+ const namingRaw = argv.naming ?? 'directory';
193
+ if (!NAMING_STYLES.includes(namingRaw)) {
194
+ await (0, inquirerer_1.cliExitWithError)(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
195
+ }
196
+ const naming = namingRaw;
197
+ const cwd = argv.cwd || process.cwd();
198
+ let partition;
199
+ if (typeof argv.partition === 'string' && argv.partition) {
200
+ try {
201
+ partition = (0, transform_1.parsePartitionConfig)(path.resolve(cwd, argv.partition));
202
+ }
203
+ catch (err) {
204
+ await (0, inquirerer_1.cliExitWithError)(err instanceof Error ? err.message : String(err));
205
+ }
206
+ }
207
+ const out = typeof argv.out === 'string' && argv.out ? path.resolve(cwd, argv.out) : undefined;
208
+ const write = Boolean(argv.write);
209
+ const check = Boolean(argv.check);
210
+ const dryRun = Boolean(argv['dry-run'] ?? argv.dryRun);
211
+ const pkg = new core_1.PgpmPackage(path.resolve(cwd));
212
+ let modulePaths;
213
+ if (pkg.isInModule()) {
214
+ modulePaths = [pkg.modulePath];
215
+ }
216
+ else if (pkg.workspacePath) {
217
+ const moduleMap = pkg.getModuleMap();
218
+ modulePaths = Object.values(moduleMap).map(mod => path.resolve(pkg.workspacePath, mod.path));
219
+ if (modulePaths.length === 0) {
220
+ await (0, inquirerer_1.cliExitWithError)('No modules found in workspace.');
221
+ }
222
+ if (out && modulePaths.length > 1) {
223
+ await (0, inquirerer_1.cliExitWithError)('--out is not supported when transforming a multi-module workspace.');
224
+ }
225
+ }
226
+ else {
227
+ await (0, inquirerer_1.cliExitWithError)('Not inside a pgpm module or workspace (pass --cwd <dir>).');
228
+ return;
229
+ }
230
+ for (const modulePath of modulePaths) {
231
+ let transformed;
232
+ try {
233
+ transformed = await transformModule(modulePath, granularity, naming, partition, out);
234
+ }
235
+ catch (err) {
236
+ if (err instanceof transform_1.PartitionCycleError) {
237
+ await (0, inquirerer_1.cliExitWithError)(`Partition failed: ${err.message}`);
238
+ return;
239
+ }
240
+ throw err;
241
+ }
242
+ for (const warning of transformed.warnings) {
243
+ console.warn(`transform: ${warning}`);
244
+ }
245
+ if (dryRun) {
246
+ printDryRun(transformed);
247
+ continue;
248
+ }
249
+ for (const pkgRows of transformed.packages) {
250
+ const targetDir = path.join(transformed.outBase, pkgRows.name);
251
+ const guard = (0, emit_package_1.checkOverwrite)(targetDir, modulePath, write);
252
+ if (guard) {
253
+ await (0, inquirerer_1.cliExitWithError)(guard);
254
+ }
255
+ }
256
+ const sourceRequires = readControlRequires(modulePath, transformed.name);
257
+ for (const pkgRows of transformed.packages) {
258
+ const dir = (0, emit_package_1.writePackage)(transformed.outBase, pkgRows, sourceRequires);
259
+ log.success(`wrote ${pkgRows.rows.length} changes to ${dir}`);
260
+ }
261
+ if (check) {
262
+ log.info('running --check: deploying original and transformed output to scratch databases...');
263
+ const diffs = await runCheck(transformed);
264
+ if (diffs.length) {
265
+ console.error(`--check failed: catalogs differ (${diffs.length} differences):`);
266
+ for (const diff of diffs)
267
+ console.error(` ${diff}`);
268
+ await (0, inquirerer_1.cliExitWithError)('Transform is not structurally lossless.');
269
+ }
270
+ log.success('--check passed: catalogs are structurally equivalent.');
271
+ }
272
+ }
273
+ prompter.close();
274
+ return argv;
275
+ };
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const utils_1 = require("@inquirerer/utils");
4
4
  const logger_1 = require("@pgpmjs/logger");
5
- const inquirerer_1 = require("inquirerer");
6
5
  const child_process_1 = require("child_process");
6
+ const inquirerer_1 = require("inquirerer");
7
7
  const npm_version_1 = require("../utils/npm-version");
8
8
  const log = new logger_1.Logger('update');
9
9
  const updateUsageText = `
package/commands.js CHANGED
@@ -13,12 +13,14 @@ const analyze_1 = __importDefault(require("./commands/analyze"));
13
13
  const cache_1 = __importDefault(require("./commands/cache"));
14
14
  const clear_1 = __importDefault(require("./commands/clear"));
15
15
  const deploy_1 = __importDefault(require("./commands/deploy"));
16
+ const diff_1 = __importDefault(require("./commands/diff"));
16
17
  const docker_1 = __importDefault(require("./commands/docker"));
17
18
  const doctor_1 = __importDefault(require("./commands/doctor"));
18
19
  const dump_1 = __importDefault(require("./commands/dump"));
19
20
  const env_1 = __importDefault(require("./commands/env"));
20
21
  const export_1 = __importDefault(require("./commands/export"));
21
22
  const extension_1 = __importDefault(require("./commands/extension"));
23
+ const import_1 = __importDefault(require("./commands/import"));
22
24
  const init_1 = __importDefault(require("./commands/init"));
23
25
  const install_1 = __importDefault(require("./commands/install"));
24
26
  const kill_1 = __importDefault(require("./commands/kill"));
@@ -34,6 +36,7 @@ const slice_1 = __importDefault(require("./commands/slice"));
34
36
  const sync_versions_1 = __importDefault(require("./commands/sync-versions"));
35
37
  const tag_1 = __importDefault(require("./commands/tag"));
36
38
  const test_packages_1 = __importDefault(require("./commands/test-packages"));
39
+ const transform_1 = __importDefault(require("./commands/transform"));
37
40
  const tune_1 = __importDefault(require("./commands/tune"));
38
41
  const update_1 = __importDefault(require("./commands/update"));
39
42
  const upgrade_1 = __importDefault(require("./commands/upgrade"));
@@ -53,6 +56,7 @@ const ENGINE_EXEMPT_COMMANDS = new Set([
53
56
  'doctor',
54
57
  'env',
55
58
  'extension',
59
+ 'import',
56
60
  'init',
57
61
  'install',
58
62
  'package',
@@ -85,6 +89,7 @@ const createPgpmCommandMap = (skipPgTeardown = false) => {
85
89
  'admin-users': pgt(admin_users_1.default),
86
90
  clear: pgt(clear_1.default),
87
91
  deploy: pgt(deploy_1.default),
92
+ diff: pgt(diff_1.default),
88
93
  docker: docker_1.default,
89
94
  doctor: doctor_1.default,
90
95
  dump: pgt(dump_1.default),
@@ -97,6 +102,7 @@ const createPgpmCommandMap = (skipPgTeardown = false) => {
97
102
  plan: pgt(plan_1.default),
98
103
  regen: regen_1.default,
99
104
  export: pgt(export_1.default),
105
+ import: pgt(import_1.default),
100
106
  package: pgt(package_1.default),
101
107
  tag: pgt(tag_1.default),
102
108
  kill: pgt(kill_1.default),
@@ -108,6 +114,7 @@ const createPgpmCommandMap = (skipPgTeardown = false) => {
108
114
  slice: slice_1.default,
109
115
  'sync-versions': sync_versions_1.default,
110
116
  'test-packages': pgt(test_packages_1.default),
117
+ transform: pgt(transform_1.default),
111
118
  tune: pgt(tune_1.default),
112
119
  upgrade: pgt(upgrade_1.default),
113
120
  up: pgt(upgrade_1.default),
@@ -1,5 +1,5 @@
1
- import { cliExitWithError } from 'inquirerer';
2
1
  import { CacheManager } from 'genomic';
2
+ import { cliExitWithError } from 'inquirerer';
3
3
  const cacheUsageText = `
4
4
  Cache Command:
5
5
 
@@ -0,0 +1,290 @@
1
+ import { PgpmMigrate } from '@pgpmjs/core';
2
+ import { deltaChangesToRows, loadDiffSideFromDisk, sqlToDiffChanges } from '@pgpmjs/diff';
3
+ import { Logger } from '@pgpmjs/logger';
4
+ import { diffChangeSets, EXPORT_GRANULARITIES, isExportGranularity, loadModule, withoutColumnOrder, writeModule } from '@pgpmjs/transform';
5
+ import { spawn } from 'child_process';
6
+ import * as fs from 'fs';
7
+ import { cliExitWithError, extractFirst } from 'inquirerer';
8
+ import * as os from 'os';
9
+ import * as path from 'path';
10
+ import { getPgPool } from 'pg-cache';
11
+ import { getPgEnvOptions, getSpawnEnvWithPg } from 'pg-env';
12
+ import { emitModuleBundle, emitModuleSql, STDOUT_TARGET } from '../utils/module-projections';
13
+ import { catalogDifferences, withScratchDatabases } from '../utils/scratch-db';
14
+ const log = new Logger('diff');
15
+ const diffUsageText = `
16
+ Diff Command:
17
+
18
+ pgpm diff <A> <B> [OPTIONS]
19
+
20
+ Identity-keyed semantic diff between two schema sources. Each side is
21
+ normalized to an object set keyed by identity (kind/schema/name), so the
22
+ same schema authored at different granularity, naming, or partitioning
23
+ diffs as empty. Tables are compared column-by-column and
24
+ constraint-by-constraint, so table changes emit ALTER TABLE, not a rebuild.
25
+
26
+ Sides:
27
+ <A> / <B> may each be:
28
+ - a pgpm module directory (flattened in plan order)
29
+ - a raw .sql file
30
+ - a live database: a postgres:// connection string, or db:<name>
31
+ (uses PG* env for host/port/user; schema is read via pg_dump)
32
+
33
+ The delta is one model; every --emit-* flag is a projection of it, so they
34
+ compose (a single run can emit a module, a linear SQL file, and a bundle).
35
+
36
+ Options:
37
+ --help, -h Show this help message
38
+ --emit-migration <dir> Write the delta as a pgpm module (deploy/revert/
39
+ verify per change, spec-derived paths, graph-derived
40
+ requires) into <dir>/<pkg>
41
+ --emit-module <dir> Alias of --emit-migration
42
+ --emit-sql <file|-> Also project the delta to a single consolidated SQL
43
+ file (deparsed in plan order); - writes to stdout
44
+ --emit-bundle <file> Also project the delta to a content-addressed
45
+ .bundle.tar.gz archive
46
+ --pkg <name> Emitted migration package name (default: diff-migration)
47
+ --granularity <level> Granularity for emitted changes: atomic | object |
48
+ consolidated (default: object)
49
+ --naming <style> Change path naming style: directory | flat (default: directory)
50
+ --json Machine-readable output
51
+ --verify Oracle mode: deploy A plus the emitted migration into
52
+ a scratch database and assert catalog equivalence
53
+ with B deployed fresh
54
+ --cwd <directory> Working directory (default: current directory)
55
+
56
+ Exit status: 0 when the sides are identical or the diff succeeds; non-zero on
57
+ error or when --verify finds the migrated catalog differs from B.
58
+
59
+ Examples:
60
+ pgpm diff ./v1-module ./v2-module
61
+ pgpm diff ./v1-module ./schema-v2.sql --json
62
+ pgpm diff db:app_v1 db:app_v2 --emit-migration ./out --verify
63
+ `;
64
+ const NAMING_STYLES = ['directory', 'flat'];
65
+ /** pg_dump a side's schema: `db:<name>` uses PG* env; DSNs pass through. */
66
+ const dumpDatabase = async (spec) => {
67
+ const args = ['--schema-only', '--no-owner'];
68
+ let env = process.env;
69
+ if (spec.startsWith('db:')) {
70
+ const config = getPgEnvOptions({ database: spec.slice(3) });
71
+ env = getSpawnEnvWithPg(config);
72
+ }
73
+ else {
74
+ args.push('--dbname', spec);
75
+ }
76
+ return new Promise((resolve, reject) => {
77
+ const child = spawn('pg_dump', args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
78
+ let out = '';
79
+ let err = '';
80
+ child.stdout.on('data', chunk => { out += chunk; });
81
+ child.stderr.on('data', chunk => { err += chunk; });
82
+ child.on('error', (e) => {
83
+ if (e.code === 'ENOENT') {
84
+ reject(new Error('pg_dump not found; ensure PostgreSQL client tools are installed and in PATH'));
85
+ return;
86
+ }
87
+ reject(e);
88
+ });
89
+ child.on('close', code => {
90
+ if (code === 0)
91
+ resolve(out);
92
+ else
93
+ reject(new Error(`pg_dump exited with code ${code}: ${err.trim()}`));
94
+ });
95
+ });
96
+ };
97
+ /** Resolve one side spec into diff input changes. */
98
+ const loadSide = async (spec, cwd) => {
99
+ if (resolveDiffSideKindSafe(spec) === 'database') {
100
+ const label = spec.startsWith('db:') ? spec.slice(3) : spec;
101
+ return {
102
+ kind: 'database',
103
+ label,
104
+ changes: sqlToDiffChanges(await dumpDatabase(spec), label),
105
+ warnings: []
106
+ };
107
+ }
108
+ return loadDiffSideFromDisk(path.resolve(cwd, spec));
109
+ };
110
+ /** `resolveDiffSideKind` without throwing on relative on-disk paths. */
111
+ const resolveDiffSideKindSafe = (spec) => /^postgres(ql)?:\/\//.test(spec) || spec.startsWith('db:') ? 'database' : 'disk';
112
+ /** Apply one diff side into a scratch database (module deploy or raw SQL). */
113
+ const applySide = async (config, side, spec, cwd) => {
114
+ if (side.kind === 'module') {
115
+ const client = new PgpmMigrate(config);
116
+ const result = await client.deploy({ modulePath: path.resolve(cwd, spec) });
117
+ if (result.failed) {
118
+ throw new Error(`deploy failed at change ${result.failed} (module ${spec})`);
119
+ }
120
+ return;
121
+ }
122
+ const pool = getPgPool(config);
123
+ const sql = side.changes.map(c => c.deploy).filter(Boolean).join('\n\n');
124
+ if (sql.trim())
125
+ await pool.query(sql);
126
+ };
127
+ /**
128
+ * Oracle mode: deploy A into a scratch database, deploy the emitted migration
129
+ * module on top (when there is one), and assert catalog equivalence with B
130
+ * deployed fresh into a second scratch database. Column order is physical — a
131
+ * drop+add migration cannot reproduce a fresh deploy's ordinals — so
132
+ * equivalence is checked order-insensitively via `withoutColumnOrder`.
133
+ */
134
+ const runVerify = async (sideA, sideB, specA, specB, migrationDir, cwd) => {
135
+ const config = getPgEnvOptions();
136
+ const stamp = Date.now();
137
+ const names = [`pgpm_diff_verify_a_${stamp}`, `pgpm_diff_verify_b_${stamp}`];
138
+ return withScratchDatabases(config, names, async ([cfgMigrated, cfgTarget]) => {
139
+ await applySide(cfgMigrated, sideA, specA, cwd);
140
+ if (migrationDir) {
141
+ const result = await new PgpmMigrate(cfgMigrated).deploy({ modulePath: migrationDir });
142
+ if (result.failed) {
143
+ throw new Error(`migration deploy failed at change ${result.failed}`);
144
+ }
145
+ }
146
+ await applySide(cfgTarget, sideB, specB, cwd);
147
+ return catalogDifferences(cfgMigrated, cfgTarget, withoutColumnOrder);
148
+ });
149
+ };
150
+ const printSummary = (result, labelA, labelB) => {
151
+ if (result.identical) {
152
+ console.log(`No differences: ${labelA} and ${labelB} describe the same objects.`);
153
+ return;
154
+ }
155
+ const byDelta = (delta) => result.objects.filter(o => o.delta === delta);
156
+ const section = (title, objects) => {
157
+ if (objects.length === 0)
158
+ return;
159
+ console.log(`${title} (${objects.length}):`);
160
+ for (const obj of objects) {
161
+ const detail = [];
162
+ if (obj.columnsAdded?.length)
163
+ detail.push(`+cols: ${obj.columnsAdded.join(', ')}`);
164
+ if (obj.columnsRemoved?.length)
165
+ detail.push(`-cols: ${obj.columnsRemoved.join(', ')}`);
166
+ if (obj.columnsModified?.length)
167
+ detail.push(`~cols: ${obj.columnsModified.join(', ')}`);
168
+ console.log(` ${obj.identity.kind} ${obj.path}${detail.length ? ` (${detail.join('; ')})` : ''}`);
169
+ }
170
+ };
171
+ section('Added', byDelta('added'));
172
+ section('Removed', byDelta('removed'));
173
+ section('Changed', byDelta('modified'));
174
+ console.log(`${result.changes.length} migration change(s) derivable.`);
175
+ };
176
+ export default async (argv, prompter, _options) => {
177
+ if (argv.help || argv.h) {
178
+ console.log(diffUsageText);
179
+ process.exit(0);
180
+ }
181
+ const { first: specA, newArgv } = extractFirst(argv);
182
+ const { first: specB, newArgv: restArgv } = extractFirst(newArgv);
183
+ argv = restArgv;
184
+ if (!specA || !specB) {
185
+ await cliExitWithError('pgpm diff requires two sides: pgpm diff <A> <B> (module dir, .sql file, db:<name>, or connection string).');
186
+ }
187
+ const granularityRaw = argv.granularity ?? 'object';
188
+ if (!isExportGranularity(granularityRaw)) {
189
+ await cliExitWithError(`Invalid --granularity "${granularityRaw}". Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
190
+ }
191
+ const granularity = granularityRaw;
192
+ const namingRaw = argv.naming ?? 'directory';
193
+ if (!NAMING_STYLES.includes(namingRaw)) {
194
+ await cliExitWithError(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
195
+ }
196
+ const naming = namingRaw;
197
+ const cwd = argv.cwd || process.cwd();
198
+ const json = Boolean(argv.json);
199
+ const verify = Boolean(argv.verify);
200
+ const emitModuleRaw = argv['emit-migration'] ?? argv.emitMigration ?? argv['emit-module'] ?? argv.emitModule;
201
+ const emitModuleDir = typeof emitModuleRaw === 'string' && emitModuleRaw
202
+ ? path.resolve(cwd, emitModuleRaw)
203
+ : undefined;
204
+ const emitSqlRaw = argv['emit-sql'] ?? argv.emitSql;
205
+ const emitSql = typeof emitSqlRaw === 'string' && emitSqlRaw
206
+ ? (emitSqlRaw === STDOUT_TARGET ? STDOUT_TARGET : path.resolve(cwd, emitSqlRaw))
207
+ : undefined;
208
+ const emitBundleRaw = argv['emit-bundle'] ?? argv.emitBundle;
209
+ const emitBundle = typeof emitBundleRaw === 'string' && emitBundleRaw
210
+ ? path.resolve(cwd, emitBundleRaw)
211
+ : undefined;
212
+ const sqlToStdout = emitSql === STDOUT_TARGET;
213
+ const pkgName = argv.pkg || 'diff-migration';
214
+ await loadModule();
215
+ let sideA;
216
+ let sideB;
217
+ try {
218
+ sideA = await loadSide(specA, cwd);
219
+ sideB = await loadSide(specB, cwd);
220
+ }
221
+ catch (err) {
222
+ await cliExitWithError(err instanceof Error ? err.message : String(err));
223
+ return;
224
+ }
225
+ const result = diffChangeSets(sideA.changes, sideB.changes, { granularity, style: naming });
226
+ const warnings = [
227
+ ...sideA.warnings.map(w => `${sideA.label}: ${w}`),
228
+ ...sideB.warnings.map(w => `${sideB.label}: ${w}`),
229
+ ...result.warnings
230
+ ];
231
+ if (json) {
232
+ console.log(JSON.stringify({
233
+ identical: result.identical,
234
+ objects: result.objects,
235
+ changes: result.changes.map(c => ({
236
+ name: c.name,
237
+ dependencies: c.dependencies,
238
+ deploy: c.deploy,
239
+ revert: c.revert,
240
+ verify: c.verify
241
+ })),
242
+ warnings
243
+ }, null, 2));
244
+ }
245
+ else {
246
+ for (const warning of warnings)
247
+ console.warn(`diff: ${warning}`);
248
+ // Keep stdout clean when the SQL projection is piped there.
249
+ if (!sqlToStdout)
250
+ printSummary(result, sideA.label, sideB.label);
251
+ }
252
+ let migrationDir;
253
+ const needModule = Boolean(emitModuleDir || emitSql || emitBundle) || (verify && !result.identical);
254
+ if (needModule) {
255
+ const outBase = emitModuleDir ?? fs.mkdtempSync(path.join(os.tmpdir(), 'pgpm-diff-'));
256
+ const rows = deltaChangesToRows(result.changes);
257
+ if (rows.length === 0) {
258
+ if (!sqlToStdout)
259
+ log.info('no migration changes to emit (sides are identical).');
260
+ }
261
+ else {
262
+ migrationDir = writeModule(outBase, { name: pkgName, requires: [], rows });
263
+ if (emitModuleDir && !sqlToStdout)
264
+ log.success(`wrote ${rows.length} migration change(s) to ${migrationDir}`);
265
+ if (emitSql) {
266
+ await emitModuleSql(migrationDir, emitSql);
267
+ if (!sqlToStdout)
268
+ log.success(`wrote linear SQL to ${emitSql}`);
269
+ }
270
+ if (emitBundle) {
271
+ await emitModuleBundle(migrationDir, emitBundle);
272
+ if (!sqlToStdout)
273
+ log.success(`wrote bundle to ${emitBundle}`);
274
+ }
275
+ }
276
+ }
277
+ if (verify) {
278
+ log.info('running --verify: deploying A plus the migration and B into scratch databases...');
279
+ const diffs = await runVerify(sideA, sideB, specA, specB, migrationDir, cwd);
280
+ if (diffs.length) {
281
+ console.error(`--verify failed: catalogs differ (${diffs.length} differences):`);
282
+ for (const diff of diffs)
283
+ console.error(` ${diff}`);
284
+ await cliExitWithError('Migrated catalog does not match the target.');
285
+ }
286
+ log.success('--verify passed: migrated catalog is structurally equivalent to the target.');
287
+ }
288
+ prompter.close();
289
+ return argv;
290
+ };