pgpm 5.21.2 → 5.23.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.
@@ -127,7 +127,8 @@ exports.default = async (argv, prompter, _options) => {
127
127
  if (createdb) {
128
128
  if (capabilities.createdb) {
129
129
  log.info(`Creating database ${database}...`);
130
- (0, child_process_1.execSync)(`createdb ${database}`, {
130
+ const [cmd, ...prefixArgs] = (0, pg_env_1.getPgClientCommand)('createdb');
131
+ (0, child_process_1.execFileSync)(cmd, [...prefixArgs, database], {
131
132
  env: (0, pg_env_1.getSpawnEnvWithPg)(pgEnv)
132
133
  });
133
134
  }
package/commands/diff.js CHANGED
@@ -37,7 +37,6 @@ const core_1 = require("@pgpmjs/core");
37
37
  const diff_1 = require("@pgpmjs/diff");
38
38
  const logger_1 = require("@pgpmjs/logger");
39
39
  const transform_1 = require("@pgpmjs/transform");
40
- const child_process_1 = require("child_process");
41
40
  const fs = __importStar(require("fs"));
42
41
  const inquirerer_1 = require("inquirerer");
43
42
  const os = __importStar(require("os"));
@@ -61,6 +60,8 @@ Diff Command:
61
60
  Sides:
62
61
  <A> / <B> may each be:
63
62
  - a pgpm module directory (flattened in plan order)
63
+ - a pgpm workspace directory (every local module, in dependency order,
64
+ with package-qualified change names)
64
65
  - a raw .sql file
65
66
  - a live database: a postgres:// connection string, or db:<name>
66
67
  (uses PG* env for host/port/user; schema is read via pg_dump)
@@ -82,6 +83,16 @@ Options:
82
83
  file (deparsed in plan order); - writes to stdout
83
84
  --emit-bundle <file> Also project the delta to a content-addressed
84
85
  .bundle.tar.gz archive
86
+ --ledger <db> Read a database's pgpm_migrate ledger (db:<name> or a
87
+ connection string) as the deployment cursor for side
88
+ B, and report which of B's plan entries that database
89
+ already has (identical / drifted / pending / orphaned
90
+ / out-of-order). Combines with --emit-ledger.
91
+ --emit-ledger <file> Write an idempotent pgpm_migrate backfill script that
92
+ records B's already-satisfied changes without
93
+ executing them. Satisfaction is semantic (identity-
94
+ keyed against side A), so regenerated or reordered
95
+ plans still classify as satisfied.
85
96
  --pkg <name> Emitted migration package name (default: diff-migration)
86
97
  --granularity <level> Granularity for emitted changes: atomic | object |
87
98
  consolidated (default: object)
@@ -102,55 +113,22 @@ Examples:
102
113
  pgpm diff ./v1-module ./v2-module
103
114
  pgpm diff ./v1-module ./schema-v2.sql --json
104
115
  pgpm diff db:app_v1 db:app_v2 --emit-migration ./out --verify
116
+ pgpm diff db:prod ./workspace-v2 --ledger db:prod --emit-ledger ./backfill.sql \
117
+ --emit-migration ./out --pkg upgrade
105
118
  `;
106
119
  const NAMING_STYLES = ['directory', 'flat'];
107
- /** pg_dump a side's schema: `db:<name>` uses PG* env; DSNs pass through. */
108
- const dumpDatabase = async (spec) => {
109
- const args = ['--schema-only', '--no-owner'];
110
- let env = process.env;
111
- if (spec.startsWith('db:')) {
112
- const config = (0, pg_env_1.getPgEnvOptions)({ database: spec.slice(3) });
113
- env = (0, pg_env_1.getSpawnEnvWithPg)(config);
120
+ /** A plan-bearing on-disk side (workspace or module directory). */
121
+ const isPlanSide = (spec, cwd) => {
122
+ if ((0, core_1.isDatabaseSpec)(spec))
123
+ return false;
124
+ try {
125
+ const kind = (0, diff_1.resolveDiffSideKind)(path.resolve(cwd, spec));
126
+ return kind === 'workspace' || kind === 'module';
114
127
  }
115
- else {
116
- args.push('--dbname', spec);
128
+ catch {
129
+ return false;
117
130
  }
118
- return new Promise((resolve, reject) => {
119
- const child = (0, child_process_1.spawn)('pg_dump', args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
120
- let out = '';
121
- let err = '';
122
- child.stdout.on('data', chunk => { out += chunk; });
123
- child.stderr.on('data', chunk => { err += chunk; });
124
- child.on('error', (e) => {
125
- if (e.code === 'ENOENT') {
126
- reject(new Error('pg_dump not found; ensure PostgreSQL client tools are installed and in PATH'));
127
- return;
128
- }
129
- reject(e);
130
- });
131
- child.on('close', code => {
132
- if (code === 0)
133
- resolve(out);
134
- else
135
- reject(new Error(`pg_dump exited with code ${code}: ${err.trim()}`));
136
- });
137
- });
138
131
  };
139
- /** Resolve one side spec into diff input changes. */
140
- const loadSide = async (spec, cwd) => {
141
- if (resolveDiffSideKindSafe(spec) === 'database') {
142
- const label = spec.startsWith('db:') ? spec.slice(3) : spec;
143
- return {
144
- kind: 'database',
145
- label,
146
- changes: (0, diff_1.sqlToDiffChanges)(await dumpDatabase(spec), label),
147
- warnings: []
148
- };
149
- }
150
- return (0, diff_1.loadDiffSideFromDisk)(path.resolve(cwd, spec));
151
- };
152
- /** `resolveDiffSideKind` without throwing on relative on-disk paths. */
153
- const resolveDiffSideKindSafe = (spec) => /^postgres(ql)?:\/\//.test(spec) || spec.startsWith('db:') ? 'database' : 'disk';
154
132
  /** Apply one diff side into a scratch database (module deploy or raw SQL). */
155
133
  const applySide = async (config, side, spec, cwd) => {
156
134
  if (side.kind === 'module') {
@@ -189,6 +167,29 @@ const runVerify = async (sideA, sideB, specA, specB, migrationDir, cwd) => {
189
167
  return (0, scratch_db_1.catalogDifferences)(cfgMigrated, cfgTarget, transform_1.withoutColumnOrder);
190
168
  });
191
169
  };
170
+ const printLedgerSummary = (report, ledgerSpec) => {
171
+ const counts = new Map();
172
+ for (const entry of report.classification.entries) {
173
+ counts.set(entry.status, (counts.get(entry.status) ?? 0) + 1);
174
+ }
175
+ const parts = [...counts.entries()].map(([status, n]) => `${n} ${status}`);
176
+ console.log(`Ledger (${ledgerSpec}): ${parts.join(', ') || 'empty plan'}.`);
177
+ if (report.classification.orphaned.length) {
178
+ console.log(` Orphaned ledger entries (${report.classification.orphaned.length}):`);
179
+ for (const row of report.classification.orphaned) {
180
+ console.log(` ${row.package}:${row.changeName}`);
181
+ }
182
+ }
183
+ if (report.classification.outOfOrder.length) {
184
+ console.log(` Deployed out of plan order: ${report.classification.outOfOrder.join(', ')}`);
185
+ }
186
+ if (report.backfilled.length) {
187
+ console.log(` Satisfied but unrecorded (backfillable): ${report.backfilled.length}`);
188
+ }
189
+ if (report.unprovable.length) {
190
+ console.log(` Not provably satisfied (inert/partial; will deploy normally): ${report.unprovable.join(', ')}`);
191
+ }
192
+ };
192
193
  const printSummary = (result, labelA, labelB) => {
193
194
  if (result.identical) {
194
195
  console.log(`No differences: ${labelA} and ${labelB} describe the same objects.`);
@@ -224,7 +225,7 @@ exports.default = async (argv, prompter, _options) => {
224
225
  const { first: specB, newArgv: restArgv } = (0, inquirerer_1.extractFirst)(newArgv);
225
226
  argv = restArgv;
226
227
  if (!specA || !specB) {
227
- await (0, inquirerer_1.cliExitWithError)('pgpm diff requires two sides: pgpm diff <A> <B> (module dir, .sql file, db:<name>, or connection string).');
228
+ await (0, inquirerer_1.cliExitWithError)('pgpm diff requires two sides: pgpm diff <A> <B> (module dir, workspace dir, .sql file, db:<name>, or connection string).');
228
229
  }
229
230
  const granularityRaw = argv.granularity ?? 'object';
230
231
  if (!(0, transform_1.isExportGranularity)(granularityRaw)) {
@@ -262,6 +263,17 @@ exports.default = async (argv, prompter, _options) => {
262
263
  : undefined;
263
264
  const sqlToStdout = emitSql === module_projections_1.STDOUT_TARGET;
264
265
  const pkgName = argv.pkg || 'diff-migration';
266
+ const ledgerSpec = typeof argv.ledger === 'string' && argv.ledger ? argv.ledger : undefined;
267
+ const emitLedgerRaw = argv['emit-ledger'] ?? argv.emitLedger;
268
+ const emitLedgerTarget = typeof emitLedgerRaw === 'string' && emitLedgerRaw
269
+ ? path.resolve(cwd, emitLedgerRaw)
270
+ : undefined;
271
+ if (emitLedgerTarget && !ledgerSpec) {
272
+ await (0, inquirerer_1.cliExitWithError)('--emit-ledger requires --ledger <db> (the database whose pgpm_migrate ledger is backfilled).');
273
+ }
274
+ if (ledgerSpec && !isPlanSide(specB, cwd)) {
275
+ await (0, inquirerer_1.cliExitWithError)('--ledger requires side B to be a pgpm workspace or module directory (it needs a plan to classify).');
276
+ }
265
277
  if (appendModuleDir && (emitModuleDir || emitSql || emitBundle || verify)) {
266
278
  await (0, inquirerer_1.cliExitWithError)('--append-module is standalone; it cannot be combined with --emit-migration/--emit-module/--emit-sql/--emit-bundle/--verify.');
267
279
  }
@@ -269,8 +281,8 @@ exports.default = async (argv, prompter, _options) => {
269
281
  let sideA;
270
282
  let sideB;
271
283
  try {
272
- sideA = await loadSide(specA, cwd);
273
- sideB = await loadSide(specB, cwd);
284
+ sideA = await (0, core_1.loadDiffSide)(specA, cwd);
285
+ sideB = await (0, core_1.loadDiffSide)(specB, cwd);
274
286
  }
275
287
  catch (err) {
276
288
  await (0, inquirerer_1.cliExitWithError)(err instanceof Error ? err.message : String(err));
@@ -282,6 +294,27 @@ exports.default = async (argv, prompter, _options) => {
282
294
  ...sideB.warnings.map(w => `${sideB.label}: ${w}`),
283
295
  ...result.warnings
284
296
  ];
297
+ let ledgerReport;
298
+ if (ledgerSpec) {
299
+ ledgerReport = await (0, core_1.buildLedgerReport)({
300
+ config: (0, core_1.resolveDatabaseSpec)(ledgerSpec),
301
+ spec: specB,
302
+ cwd,
303
+ changes: sideB.changes,
304
+ diff: result
305
+ });
306
+ if (emitLedgerTarget) {
307
+ if (ledgerReport.backfillSql) {
308
+ fs.mkdirSync(path.dirname(emitLedgerTarget), { recursive: true });
309
+ fs.writeFileSync(emitLedgerTarget, ledgerReport.backfillSql);
310
+ if (!sqlToStdout && !json)
311
+ log.success(`wrote ledger backfill (${ledgerReport.backfilled.length} change(s)) to ${emitLedgerTarget}`);
312
+ }
313
+ else if (!sqlToStdout && !json) {
314
+ log.info('no ledger backfill to emit (nothing satisfied-but-unrecorded).');
315
+ }
316
+ }
317
+ }
285
318
  if (json) {
286
319
  console.log(JSON.stringify({
287
320
  identical: result.identical,
@@ -293,15 +326,30 @@ exports.default = async (argv, prompter, _options) => {
293
326
  revert: c.revert,
294
327
  verify: c.verify
295
328
  })),
296
- warnings
329
+ warnings,
330
+ ...(ledgerReport
331
+ ? {
332
+ ledger: {
333
+ entries: ledgerReport.classification.entries,
334
+ orphaned: ledgerReport.classification.orphaned,
335
+ outOfOrder: ledgerReport.classification.outOfOrder,
336
+ coverage: ledgerReport.coverage,
337
+ backfilled: ledgerReport.backfilled,
338
+ unprovable: ledgerReport.unprovable
339
+ }
340
+ }
341
+ : {})
297
342
  }, null, 2));
298
343
  }
299
344
  else {
300
345
  for (const warning of warnings)
301
346
  console.warn(`diff: ${warning}`);
302
347
  // Keep stdout clean when the SQL projection is piped there.
303
- if (!sqlToStdout)
348
+ if (!sqlToStdout) {
304
349
  printSummary(result, sideA.label, sideB.label);
350
+ if (ledgerReport)
351
+ printLedgerSummary(ledgerReport, ledgerSpec);
352
+ }
305
353
  }
306
354
  if (appendModuleDir) {
307
355
  const rows = (0, diff_1.deltaChangesToRows)(result.changes);
package/commands/dump.js CHANGED
@@ -3,9 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const core_1 = require("@pgpmjs/core");
6
7
  const logger_1 = require("@pgpmjs/logger");
7
8
  const quotes_1 = require("@pgsql/quotes");
8
- const child_process_1 = require("child_process");
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
11
  const pg_cache_1 = require("pg-cache");
@@ -37,28 +37,6 @@ function nowStamp() {
37
37
  const pad = (n) => String(n).padStart(2, '0');
38
38
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
39
39
  }
40
- async function runPgDump(args, env) {
41
- await new Promise((resolve, reject) => {
42
- const child = (0, child_process_1.spawn)('pg_dump', args, {
43
- env,
44
- stdio: 'inherit',
45
- shell: false
46
- });
47
- child.on('error', (err) => {
48
- if (err.code === 'ENOENT') {
49
- log.error('pg_dump not found; ensure PostgreSQL client tools are installed and in PATH');
50
- }
51
- reject(err);
52
- });
53
- child.on('close', (code) => {
54
- if (code === 0) {
55
- resolve();
56
- return;
57
- }
58
- reject(new Error(`pg_dump exited with code ${code ?? 1}`));
59
- });
60
- });
61
- }
62
40
  async function resolveDatabaseId(dbname, databaseIdRaw) {
63
41
  const pool = (0, pg_cache_1.getPgPool)((0, pg_env_1.getPgEnvOptions)({ database: dbname }));
64
42
  const res = await pool.query(`select id, name from metaschema_public.database order by name`);
@@ -145,16 +123,13 @@ exports.default = async (argv, prompter, _options) => {
145
123
  log.info(`database id ${databaseIdInfo.id}`);
146
124
  }
147
125
  const pgEnv = (0, pg_env_1.getPgEnvOptions)({ database: dbname });
148
- const spawnEnv = (0, pg_env_1.getSpawnEnvWithPg)(pgEnv);
149
- const args = [
150
- '--format=plain',
151
- '--no-owner',
152
- '--no-privileges',
153
- '--file',
154
- outPath,
155
- dbname
156
- ];
157
- await runPgDump(args, spawnEnv);
126
+ await (0, core_1.pgDump)({
127
+ config: pgEnv,
128
+ format: 'plain',
129
+ noOwner: true,
130
+ noPrivileges: true,
131
+ file: outPath
132
+ });
158
133
  if (databaseIdInfo) {
159
134
  const pruneSql = await buildPruneSql(dbname, databaseIdInfo.id);
160
135
  // Use writeFileSync with 'a' flag for explicit append as requested
@@ -33,9 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ const core_1 = require("@pgpmjs/core");
36
37
  const import_1 = require("@pgpmjs/import");
37
38
  const logger_1 = require("@pgpmjs/logger");
38
39
  const transform_1 = require("@pgpmjs/transform");
40
+ const fs_1 = require("fs");
39
41
  const inquirerer_1 = require("inquirerer");
40
42
  const path = __importStar(require("path"));
41
43
  const emit_package_1 = require("../utils/emit-package");
@@ -88,6 +90,12 @@ Options:
88
90
  --emit-bundle <file> Also project the imported module into a
89
91
  content-addressed .bundle.tar.gz. Same single-package
90
92
  requirement as --emit-sql.
93
+ --baseline After writing each package, also write a
94
+ <pkg>/baseline.sql: an idempotent, log-only
95
+ pgpm_migrate backfill recording every generated
96
+ change as already deployed. Adopts the source
97
+ database — apply the script (after 'pgpm migrate
98
+ init'), then 'pgpm deploy' runs only real changes.
91
99
  --cwd <directory> Working directory (default: current directory)
92
100
  --dry-run Print the resulting plan/paths without writing
93
101
 
@@ -100,6 +108,7 @@ Examples:
100
108
  pgpm import ./sql-files --pkg my-app --out ./packages --with-data
101
109
  pgpm import dump.sql --pkg my-app --partition partition.json
102
110
  pgpm import dump.sql --pkg my-app --emit-sql my-app.sql
111
+ pgpm import dump.sql --pkg my-app --baseline
103
112
  `;
104
113
  const NAMING_STYLES = ['directory', 'flat'];
105
114
  exports.default = async (argv, prompter, _options) => {
@@ -147,9 +156,13 @@ exports.default = async (argv, prompter, _options) => {
147
156
  const emit = (0, module_projections_1.parseEmitProjectionTargets)(argv, cwd);
148
157
  const emitRequested = (0, module_projections_1.hasEmitProjection)(emit);
149
158
  const sqlToStdout = emit.emitSql === module_projections_1.STDOUT_TARGET;
159
+ const baseline = Boolean(argv.baseline);
150
160
  if (emitRequested && dryRun) {
151
161
  await (0, inquirerer_1.cliExitWithError)('--emit-sql/--emit-bundle cannot be combined with --dry-run.');
152
162
  }
163
+ if (baseline && dryRun) {
164
+ await (0, inquirerer_1.cliExitWithError)('--baseline cannot be combined with --dry-run.');
165
+ }
153
166
  let source;
154
167
  try {
155
168
  source = (0, import_1.loadDumpSource)(path.resolve(cwd, input));
@@ -213,9 +226,11 @@ exports.default = async (argv, prompter, _options) => {
213
226
  }
214
227
  }
215
228
  let firstDir;
229
+ const writtenDirs = [];
216
230
  for (const pkg of packages) {
217
231
  const dir = (0, emit_package_1.writePackage)(outBase, pkg);
218
232
  firstDir ??= dir;
233
+ writtenDirs.push(dir);
219
234
  if (!sqlToStdout) {
220
235
  log.success(`wrote ${pkg.rows.length} changes to ${dir}`);
221
236
  }
@@ -223,6 +238,14 @@ exports.default = async (argv, prompter, _options) => {
223
238
  if (emitRequested && firstDir) {
224
239
  await (0, module_projections_1.projectModule)(firstDir, emit, sqlToStdout ? undefined : msg => log.success(msg));
225
240
  }
241
+ if (baseline) {
242
+ for (const dir of writtenDirs) {
243
+ const { entries, backfillSql } = await (0, core_1.buildBaselineBackfill)(dir);
244
+ const baselinePath = path.join(dir, 'baseline.sql');
245
+ (0, fs_1.writeFileSync)(baselinePath, backfillSql);
246
+ log.success(`wrote baseline backfill for ${entries.length} change(s) to ${baselinePath}`);
247
+ }
248
+ }
226
249
  }
227
250
  const { summary } = result;
228
251
  if (!sqlToStdout) {
@@ -0,0 +1,4 @@
1
+ import { CLIOptions, Inquirerer, ParsedArgs } from 'inquirerer';
2
+ export declare const baselineUsageText = "\nMigrate Baseline Command:\n\n pgpm migrate baseline [<workspace|module>] [OPTIONS]\n\n Record a plan's changes as already deployed WITHOUT executing any DDL, so a\n database that already carries the schema (imported from a dump, restored, or\n otherwise adopted from outside pgpm) is brought to the plan's head in the\n pgpm_migrate ledger. A subsequent 'pgpm deploy' then runs only genuinely new\n changes.\n\n Each change is recorded through the same pgpm_migrate.deploy procedure a real\n deploy uses, with log-only mode (empty script, p_log_only => TRUE), in plan\n order.\n\nOptions:\n --help, -h Show this help message\n --emit-ledger <file|-> Write the log-only backfill SQL instead of applying\n it (- for stdout). Offline; no database is touched.\n The target must already have the pgpm_migrate schema\n (run 'pgpm migrate init' first).\n --database <name> Database to apply the baseline to (skips the prompt)\n -y, --yes Skip the confirmation prompt when applying\n --no-tx Apply without wrapping each module in a transaction\n --cwd <directory> Working directory (default: current directory)\n\nExamples:\n pgpm migrate baseline ./my-app --emit-ledger baseline.sql\n pgpm migrate baseline --database mydb\n pgpm migrate baseline ./ws --emit-ledger -\n";
3
+ declare const _default: (argv: Partial<ParsedArgs>, prompter: Inquirerer, _options: CLIOptions) => Promise<Partial<ParsedArgs>>;
4
+ export default _default;
@@ -0,0 +1,141 @@
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.baselineUsageText = void 0;
37
+ const core_1 = require("@pgpmjs/core");
38
+ const diff_1 = require("@pgpmjs/diff");
39
+ const logger_1 = require("@pgpmjs/logger");
40
+ const fs_1 = require("fs");
41
+ const path = __importStar(require("path"));
42
+ const pg_env_1 = require("pg-env");
43
+ const database_1 = require("../../utils/database");
44
+ const log = new logger_1.Logger('migrate-baseline');
45
+ const STDOUT_TARGET = '-';
46
+ exports.baselineUsageText = `
47
+ Migrate Baseline Command:
48
+
49
+ pgpm migrate baseline [<workspace|module>] [OPTIONS]
50
+
51
+ Record a plan's changes as already deployed WITHOUT executing any DDL, so a
52
+ database that already carries the schema (imported from a dump, restored, or
53
+ otherwise adopted from outside pgpm) is brought to the plan's head in the
54
+ pgpm_migrate ledger. A subsequent 'pgpm deploy' then runs only genuinely new
55
+ changes.
56
+
57
+ Each change is recorded through the same pgpm_migrate.deploy procedure a real
58
+ deploy uses, with log-only mode (empty script, p_log_only => TRUE), in plan
59
+ order.
60
+
61
+ Options:
62
+ --help, -h Show this help message
63
+ --emit-ledger <file|-> Write the log-only backfill SQL instead of applying
64
+ it (- for stdout). Offline; no database is touched.
65
+ The target must already have the pgpm_migrate schema
66
+ (run 'pgpm migrate init' first).
67
+ --database <name> Database to apply the baseline to (skips the prompt)
68
+ -y, --yes Skip the confirmation prompt when applying
69
+ --no-tx Apply without wrapping each module in a transaction
70
+ --cwd <directory> Working directory (default: current directory)
71
+
72
+ Examples:
73
+ pgpm migrate baseline ./my-app --emit-ledger baseline.sql
74
+ pgpm migrate baseline --database mydb
75
+ pgpm migrate baseline ./ws --emit-ledger -
76
+ `;
77
+ exports.default = async (argv, prompter, _options) => {
78
+ if (argv.help || argv.h) {
79
+ console.log(exports.baselineUsageText);
80
+ process.exit(0);
81
+ }
82
+ const cwd = argv.cwd || process.cwd();
83
+ const spec = argv._?.filter(Boolean)[0] ?? cwd;
84
+ const modules = await (0, core_1.loadPlanSideModules)(spec, cwd);
85
+ const entries = await (0, core_1.baselineBackfillEntries)(modules);
86
+ if (entries.length === 0) {
87
+ log.warn('No plan changes with deploy scripts found; nothing to baseline.');
88
+ return argv;
89
+ }
90
+ const emitTarget = argv['emit-ledger'] ?? argv.emitLedger;
91
+ // Offline emit: write the log-only backfill SQL, touch no database.
92
+ if (typeof emitTarget === 'string' && emitTarget) {
93
+ const sql = (0, diff_1.emitLedgerBackfill)(entries);
94
+ if (emitTarget === STDOUT_TARGET) {
95
+ process.stdout.write(sql);
96
+ }
97
+ else {
98
+ const outPath = path.resolve(cwd, emitTarget);
99
+ (0, fs_1.writeFileSync)(outPath, sql);
100
+ log.success(`Wrote baseline backfill for ${entries.length} change(s) to ${outPath}`);
101
+ }
102
+ return argv;
103
+ }
104
+ // Apply: record every change log-only against the target database.
105
+ const database = await (0, database_1.getTargetDatabase)(argv, prompter, {
106
+ message: 'Select database to baseline'
107
+ });
108
+ const { yes } = await prompter.prompt(argv, [
109
+ {
110
+ name: 'yes',
111
+ type: 'confirm',
112
+ message: `Record ${entries.length} change(s) as deployed (log-only) in "${database}"?`,
113
+ required: true
114
+ }
115
+ ]);
116
+ if (!yes) {
117
+ log.info('Operation cancelled.');
118
+ return argv;
119
+ }
120
+ const config = (0, pg_env_1.getPgEnvOptions)({ database });
121
+ const useTransaction = argv.tx !== false;
122
+ const client = new core_1.PgpmMigrate(config);
123
+ try {
124
+ let recorded = 0;
125
+ let skipped = 0;
126
+ for (const mod of modules) {
127
+ const result = await client.deploy({
128
+ modulePath: mod.modulePath,
129
+ logOnly: true,
130
+ useTransaction
131
+ });
132
+ recorded += result.deployed.length;
133
+ skipped += result.skipped.length;
134
+ }
135
+ log.success(`Baseline complete in "${database}": ${recorded} change(s) recorded, ${skipped} already present.`);
136
+ }
137
+ finally {
138
+ await client.close();
139
+ }
140
+ return argv;
141
+ };
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const inquirerer_1 = require("inquirerer");
7
+ const baseline_1 = __importDefault(require("./migrate/baseline"));
7
8
  const deps_1 = __importDefault(require("./migrate/deps"));
8
9
  // Migrate subcommands
9
10
  const init_1 = __importDefault(require("./migrate/init"));
@@ -13,7 +14,8 @@ const subcommandMap = {
13
14
  init: init_1.default,
14
15
  status: status_1.default,
15
16
  list: list_1.default,
16
- deps: deps_1.default
17
+ deps: deps_1.default,
18
+ baseline: baseline_1.default
17
19
  };
18
20
  const migrateUsageText = `
19
21
  Migrate Commands:
@@ -22,6 +24,7 @@ Migrate Commands:
22
24
  pgpm migrate status Show current migration status
23
25
  pgpm migrate list List all changes (deployed and pending)
24
26
  pgpm migrate deps Show change dependencies
27
+ pgpm migrate baseline Record a plan as already deployed (log-only), adopting an existing database
25
28
 
26
29
  Options:
27
30
  --help, -h Show this help message
@@ -63,7 +66,8 @@ function getSubcommandDescription(cmd) {
63
66
  init: 'Initialize migration tracking in database',
64
67
  status: 'Show current migration status',
65
68
  list: 'List all changes (deployed and pending)',
66
- deps: 'Show change dependencies'
69
+ deps: 'Show change dependencies',
70
+ baseline: 'Record a plan as already deployed (log-only), adopting an existing database'
67
71
  };
68
72
  return descriptions[cmd] || '';
69
73
  }
@@ -1,8 +1,8 @@
1
1
  import { PgpmPackage } from '@pgpmjs/core';
2
2
  import { getEnvOptions } from '@pgpmjs/env';
3
3
  import { Logger } from '@pgpmjs/logger';
4
- import { execSync } from 'child_process';
5
- import { getPgEnvOptions, getSpawnEnvWithPg, } from 'pg-env';
4
+ import { execFileSync } from 'child_process';
5
+ import { getPgClientCommand, getPgEnvOptions, getSpawnEnvWithPg, } from 'pg-env';
6
6
  import { getActiveEngine, getTargetDatabase, resolvePackageAlias } from '../utils';
7
7
  import { selectPackage } from '../utils/module-utils';
8
8
  const deployUsageText = `
@@ -125,7 +125,8 @@ export default async (argv, prompter, _options) => {
125
125
  if (createdb) {
126
126
  if (capabilities.createdb) {
127
127
  log.info(`Creating database ${database}...`);
128
- execSync(`createdb ${database}`, {
128
+ const [cmd, ...prefixArgs] = getPgClientCommand('createdb');
129
+ execFileSync(cmd, [...prefixArgs, database], {
129
130
  env: getSpawnEnvWithPg(pgEnv)
130
131
  });
131
132
  }
@@ -1,14 +1,13 @@
1
- import { PgpmMigrate } from '@pgpmjs/core';
2
- import { deltaChangesToRows, loadDiffSideFromDisk, sqlToDiffChanges } from '@pgpmjs/diff';
1
+ import { buildLedgerReport, isDatabaseSpec, loadDiffSide, PgpmMigrate, resolveDatabaseSpec } from '@pgpmjs/core';
2
+ import { deltaChangesToRows, resolveDiffSideKind } from '@pgpmjs/diff';
3
3
  import { Logger } from '@pgpmjs/logger';
4
4
  import { appendModule, CHANGE_GRANULARITIES, diffChangeSets, EXPORT_GRANULARITIES, isChangeGranularity, isExportGranularity, loadModule, withoutColumnOrder, writeModule } from '@pgpmjs/transform';
5
- import { spawn } from 'child_process';
6
5
  import * as fs from 'fs';
7
6
  import { cliExitWithError, extractFirst } from 'inquirerer';
8
7
  import * as os from 'os';
9
8
  import * as path from 'path';
10
9
  import { getPgPool } from 'pg-cache';
11
- import { getPgEnvOptions, getSpawnEnvWithPg } from 'pg-env';
10
+ import { getPgEnvOptions } from 'pg-env';
12
11
  import { emitModuleBundle, emitModuleSql, STDOUT_TARGET } from '../utils/module-projections';
13
12
  import { catalogDifferences, withScratchDatabases } from '../utils/scratch-db';
14
13
  const log = new Logger('diff');
@@ -26,6 +25,8 @@ Diff Command:
26
25
  Sides:
27
26
  <A> / <B> may each be:
28
27
  - a pgpm module directory (flattened in plan order)
28
+ - a pgpm workspace directory (every local module, in dependency order,
29
+ with package-qualified change names)
29
30
  - a raw .sql file
30
31
  - a live database: a postgres:// connection string, or db:<name>
31
32
  (uses PG* env for host/port/user; schema is read via pg_dump)
@@ -47,6 +48,16 @@ Options:
47
48
  file (deparsed in plan order); - writes to stdout
48
49
  --emit-bundle <file> Also project the delta to a content-addressed
49
50
  .bundle.tar.gz archive
51
+ --ledger <db> Read a database's pgpm_migrate ledger (db:<name> or a
52
+ connection string) as the deployment cursor for side
53
+ B, and report which of B's plan entries that database
54
+ already has (identical / drifted / pending / orphaned
55
+ / out-of-order). Combines with --emit-ledger.
56
+ --emit-ledger <file> Write an idempotent pgpm_migrate backfill script that
57
+ records B's already-satisfied changes without
58
+ executing them. Satisfaction is semantic (identity-
59
+ keyed against side A), so regenerated or reordered
60
+ plans still classify as satisfied.
50
61
  --pkg <name> Emitted migration package name (default: diff-migration)
51
62
  --granularity <level> Granularity for emitted changes: atomic | object |
52
63
  consolidated (default: object)
@@ -67,55 +78,22 @@ Examples:
67
78
  pgpm diff ./v1-module ./v2-module
68
79
  pgpm diff ./v1-module ./schema-v2.sql --json
69
80
  pgpm diff db:app_v1 db:app_v2 --emit-migration ./out --verify
81
+ pgpm diff db:prod ./workspace-v2 --ledger db:prod --emit-ledger ./backfill.sql \
82
+ --emit-migration ./out --pkg upgrade
70
83
  `;
71
84
  const NAMING_STYLES = ['directory', 'flat'];
72
- /** pg_dump a side's schema: `db:<name>` uses PG* env; DSNs pass through. */
73
- const dumpDatabase = async (spec) => {
74
- const args = ['--schema-only', '--no-owner'];
75
- let env = process.env;
76
- if (spec.startsWith('db:')) {
77
- const config = getPgEnvOptions({ database: spec.slice(3) });
78
- env = getSpawnEnvWithPg(config);
79
- }
80
- else {
81
- args.push('--dbname', spec);
85
+ /** A plan-bearing on-disk side (workspace or module directory). */
86
+ const isPlanSide = (spec, cwd) => {
87
+ if (isDatabaseSpec(spec))
88
+ return false;
89
+ try {
90
+ const kind = resolveDiffSideKind(path.resolve(cwd, spec));
91
+ return kind === 'workspace' || kind === 'module';
82
92
  }
83
- return new Promise((resolve, reject) => {
84
- const child = spawn('pg_dump', args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
85
- let out = '';
86
- let err = '';
87
- child.stdout.on('data', chunk => { out += chunk; });
88
- child.stderr.on('data', chunk => { err += chunk; });
89
- child.on('error', (e) => {
90
- if (e.code === 'ENOENT') {
91
- reject(new Error('pg_dump not found; ensure PostgreSQL client tools are installed and in PATH'));
92
- return;
93
- }
94
- reject(e);
95
- });
96
- child.on('close', code => {
97
- if (code === 0)
98
- resolve(out);
99
- else
100
- reject(new Error(`pg_dump exited with code ${code}: ${err.trim()}`));
101
- });
102
- });
103
- };
104
- /** Resolve one side spec into diff input changes. */
105
- const loadSide = async (spec, cwd) => {
106
- if (resolveDiffSideKindSafe(spec) === 'database') {
107
- const label = spec.startsWith('db:') ? spec.slice(3) : spec;
108
- return {
109
- kind: 'database',
110
- label,
111
- changes: sqlToDiffChanges(await dumpDatabase(spec), label),
112
- warnings: []
113
- };
93
+ catch {
94
+ return false;
114
95
  }
115
- return loadDiffSideFromDisk(path.resolve(cwd, spec));
116
96
  };
117
- /** `resolveDiffSideKind` without throwing on relative on-disk paths. */
118
- const resolveDiffSideKindSafe = (spec) => /^postgres(ql)?:\/\//.test(spec) || spec.startsWith('db:') ? 'database' : 'disk';
119
97
  /** Apply one diff side into a scratch database (module deploy or raw SQL). */
120
98
  const applySide = async (config, side, spec, cwd) => {
121
99
  if (side.kind === 'module') {
@@ -154,6 +132,29 @@ const runVerify = async (sideA, sideB, specA, specB, migrationDir, cwd) => {
154
132
  return catalogDifferences(cfgMigrated, cfgTarget, withoutColumnOrder);
155
133
  });
156
134
  };
135
+ const printLedgerSummary = (report, ledgerSpec) => {
136
+ const counts = new Map();
137
+ for (const entry of report.classification.entries) {
138
+ counts.set(entry.status, (counts.get(entry.status) ?? 0) + 1);
139
+ }
140
+ const parts = [...counts.entries()].map(([status, n]) => `${n} ${status}`);
141
+ console.log(`Ledger (${ledgerSpec}): ${parts.join(', ') || 'empty plan'}.`);
142
+ if (report.classification.orphaned.length) {
143
+ console.log(` Orphaned ledger entries (${report.classification.orphaned.length}):`);
144
+ for (const row of report.classification.orphaned) {
145
+ console.log(` ${row.package}:${row.changeName}`);
146
+ }
147
+ }
148
+ if (report.classification.outOfOrder.length) {
149
+ console.log(` Deployed out of plan order: ${report.classification.outOfOrder.join(', ')}`);
150
+ }
151
+ if (report.backfilled.length) {
152
+ console.log(` Satisfied but unrecorded (backfillable): ${report.backfilled.length}`);
153
+ }
154
+ if (report.unprovable.length) {
155
+ console.log(` Not provably satisfied (inert/partial; will deploy normally): ${report.unprovable.join(', ')}`);
156
+ }
157
+ };
157
158
  const printSummary = (result, labelA, labelB) => {
158
159
  if (result.identical) {
159
160
  console.log(`No differences: ${labelA} and ${labelB} describe the same objects.`);
@@ -189,7 +190,7 @@ export default async (argv, prompter, _options) => {
189
190
  const { first: specB, newArgv: restArgv } = extractFirst(newArgv);
190
191
  argv = restArgv;
191
192
  if (!specA || !specB) {
192
- await cliExitWithError('pgpm diff requires two sides: pgpm diff <A> <B> (module dir, .sql file, db:<name>, or connection string).');
193
+ await cliExitWithError('pgpm diff requires two sides: pgpm diff <A> <B> (module dir, workspace dir, .sql file, db:<name>, or connection string).');
193
194
  }
194
195
  const granularityRaw = argv.granularity ?? 'object';
195
196
  if (!isExportGranularity(granularityRaw)) {
@@ -227,6 +228,17 @@ export default async (argv, prompter, _options) => {
227
228
  : undefined;
228
229
  const sqlToStdout = emitSql === STDOUT_TARGET;
229
230
  const pkgName = argv.pkg || 'diff-migration';
231
+ const ledgerSpec = typeof argv.ledger === 'string' && argv.ledger ? argv.ledger : undefined;
232
+ const emitLedgerRaw = argv['emit-ledger'] ?? argv.emitLedger;
233
+ const emitLedgerTarget = typeof emitLedgerRaw === 'string' && emitLedgerRaw
234
+ ? path.resolve(cwd, emitLedgerRaw)
235
+ : undefined;
236
+ if (emitLedgerTarget && !ledgerSpec) {
237
+ await cliExitWithError('--emit-ledger requires --ledger <db> (the database whose pgpm_migrate ledger is backfilled).');
238
+ }
239
+ if (ledgerSpec && !isPlanSide(specB, cwd)) {
240
+ await cliExitWithError('--ledger requires side B to be a pgpm workspace or module directory (it needs a plan to classify).');
241
+ }
230
242
  if (appendModuleDir && (emitModuleDir || emitSql || emitBundle || verify)) {
231
243
  await cliExitWithError('--append-module is standalone; it cannot be combined with --emit-migration/--emit-module/--emit-sql/--emit-bundle/--verify.');
232
244
  }
@@ -234,8 +246,8 @@ export default async (argv, prompter, _options) => {
234
246
  let sideA;
235
247
  let sideB;
236
248
  try {
237
- sideA = await loadSide(specA, cwd);
238
- sideB = await loadSide(specB, cwd);
249
+ sideA = await loadDiffSide(specA, cwd);
250
+ sideB = await loadDiffSide(specB, cwd);
239
251
  }
240
252
  catch (err) {
241
253
  await cliExitWithError(err instanceof Error ? err.message : String(err));
@@ -247,6 +259,27 @@ export default async (argv, prompter, _options) => {
247
259
  ...sideB.warnings.map(w => `${sideB.label}: ${w}`),
248
260
  ...result.warnings
249
261
  ];
262
+ let ledgerReport;
263
+ if (ledgerSpec) {
264
+ ledgerReport = await buildLedgerReport({
265
+ config: resolveDatabaseSpec(ledgerSpec),
266
+ spec: specB,
267
+ cwd,
268
+ changes: sideB.changes,
269
+ diff: result
270
+ });
271
+ if (emitLedgerTarget) {
272
+ if (ledgerReport.backfillSql) {
273
+ fs.mkdirSync(path.dirname(emitLedgerTarget), { recursive: true });
274
+ fs.writeFileSync(emitLedgerTarget, ledgerReport.backfillSql);
275
+ if (!sqlToStdout && !json)
276
+ log.success(`wrote ledger backfill (${ledgerReport.backfilled.length} change(s)) to ${emitLedgerTarget}`);
277
+ }
278
+ else if (!sqlToStdout && !json) {
279
+ log.info('no ledger backfill to emit (nothing satisfied-but-unrecorded).');
280
+ }
281
+ }
282
+ }
250
283
  if (json) {
251
284
  console.log(JSON.stringify({
252
285
  identical: result.identical,
@@ -258,15 +291,30 @@ export default async (argv, prompter, _options) => {
258
291
  revert: c.revert,
259
292
  verify: c.verify
260
293
  })),
261
- warnings
294
+ warnings,
295
+ ...(ledgerReport
296
+ ? {
297
+ ledger: {
298
+ entries: ledgerReport.classification.entries,
299
+ orphaned: ledgerReport.classification.orphaned,
300
+ outOfOrder: ledgerReport.classification.outOfOrder,
301
+ coverage: ledgerReport.coverage,
302
+ backfilled: ledgerReport.backfilled,
303
+ unprovable: ledgerReport.unprovable
304
+ }
305
+ }
306
+ : {})
262
307
  }, null, 2));
263
308
  }
264
309
  else {
265
310
  for (const warning of warnings)
266
311
  console.warn(`diff: ${warning}`);
267
312
  // Keep stdout clean when the SQL projection is piped there.
268
- if (!sqlToStdout)
313
+ if (!sqlToStdout) {
269
314
  printSummary(result, sideA.label, sideB.label);
315
+ if (ledgerReport)
316
+ printLedgerSummary(ledgerReport, ledgerSpec);
317
+ }
270
318
  }
271
319
  if (appendModuleDir) {
272
320
  const rows = deltaChangesToRows(result.changes);
@@ -1,10 +1,10 @@
1
+ import { pgDump } from '@pgpmjs/core';
1
2
  import { Logger } from '@pgpmjs/logger';
2
3
  import { QuoteUtils } from '@pgsql/quotes';
3
- import { spawn } from 'child_process';
4
4
  import fs from 'fs';
5
5
  import path from 'path';
6
6
  import { getPgPool } from 'pg-cache';
7
- import { getPgEnvOptions, getSpawnEnvWithPg } from 'pg-env';
7
+ import { getPgEnvOptions } from 'pg-env';
8
8
  import { getTargetDatabase } from '../utils';
9
9
  const log = new Logger('dump');
10
10
  const dumpUsageText = `
@@ -32,28 +32,6 @@ function nowStamp() {
32
32
  const pad = (n) => String(n).padStart(2, '0');
33
33
  return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
34
34
  }
35
- async function runPgDump(args, env) {
36
- await new Promise((resolve, reject) => {
37
- const child = spawn('pg_dump', args, {
38
- env,
39
- stdio: 'inherit',
40
- shell: false
41
- });
42
- child.on('error', (err) => {
43
- if (err.code === 'ENOENT') {
44
- log.error('pg_dump not found; ensure PostgreSQL client tools are installed and in PATH');
45
- }
46
- reject(err);
47
- });
48
- child.on('close', (code) => {
49
- if (code === 0) {
50
- resolve();
51
- return;
52
- }
53
- reject(new Error(`pg_dump exited with code ${code ?? 1}`));
54
- });
55
- });
56
- }
57
35
  async function resolveDatabaseId(dbname, databaseIdRaw) {
58
36
  const pool = getPgPool(getPgEnvOptions({ database: dbname }));
59
37
  const res = await pool.query(`select id, name from metaschema_public.database order by name`);
@@ -140,16 +118,13 @@ export default async (argv, prompter, _options) => {
140
118
  log.info(`database id ${databaseIdInfo.id}`);
141
119
  }
142
120
  const pgEnv = getPgEnvOptions({ database: dbname });
143
- const spawnEnv = getSpawnEnvWithPg(pgEnv);
144
- const args = [
145
- '--format=plain',
146
- '--no-owner',
147
- '--no-privileges',
148
- '--file',
149
- outPath,
150
- dbname
151
- ];
152
- await runPgDump(args, spawnEnv);
121
+ await pgDump({
122
+ config: pgEnv,
123
+ format: 'plain',
124
+ noOwner: true,
125
+ noPrivileges: true,
126
+ file: outPath
127
+ });
153
128
  if (databaseIdInfo) {
154
129
  const pruneSql = await buildPruneSql(dbname, databaseIdInfo.id);
155
130
  // Use writeFileSync with 'a' flag for explicit append as requested
@@ -1,6 +1,8 @@
1
+ import { buildBaselineBackfill } from '@pgpmjs/core';
1
2
  import { dumpCompatibilityWarnings, importDumpRows, linkTextualDeps, loadDumpSource } from '@pgpmjs/import';
2
3
  import { Logger } from '@pgpmjs/logger';
3
4
  import { CHANGE_GRANULARITIES, EXPORT_GRANULARITIES, isChangeGranularity, isExportGranularity, parsePartitionConfig, PartitionCycleError, partitionExportRows } from '@pgpmjs/transform';
5
+ import { writeFileSync } from 'fs';
4
6
  import { cliExitWithError } from 'inquirerer';
5
7
  import * as path from 'path';
6
8
  import { checkOverwrite, writePackage } from '../utils/emit-package';
@@ -53,6 +55,12 @@ Options:
53
55
  --emit-bundle <file> Also project the imported module into a
54
56
  content-addressed .bundle.tar.gz. Same single-package
55
57
  requirement as --emit-sql.
58
+ --baseline After writing each package, also write a
59
+ <pkg>/baseline.sql: an idempotent, log-only
60
+ pgpm_migrate backfill recording every generated
61
+ change as already deployed. Adopts the source
62
+ database — apply the script (after 'pgpm migrate
63
+ init'), then 'pgpm deploy' runs only real changes.
56
64
  --cwd <directory> Working directory (default: current directory)
57
65
  --dry-run Print the resulting plan/paths without writing
58
66
 
@@ -65,6 +73,7 @@ Examples:
65
73
  pgpm import ./sql-files --pkg my-app --out ./packages --with-data
66
74
  pgpm import dump.sql --pkg my-app --partition partition.json
67
75
  pgpm import dump.sql --pkg my-app --emit-sql my-app.sql
76
+ pgpm import dump.sql --pkg my-app --baseline
68
77
  `;
69
78
  const NAMING_STYLES = ['directory', 'flat'];
70
79
  export default async (argv, prompter, _options) => {
@@ -112,9 +121,13 @@ export default async (argv, prompter, _options) => {
112
121
  const emit = parseEmitProjectionTargets(argv, cwd);
113
122
  const emitRequested = hasEmitProjection(emit);
114
123
  const sqlToStdout = emit.emitSql === STDOUT_TARGET;
124
+ const baseline = Boolean(argv.baseline);
115
125
  if (emitRequested && dryRun) {
116
126
  await cliExitWithError('--emit-sql/--emit-bundle cannot be combined with --dry-run.');
117
127
  }
128
+ if (baseline && dryRun) {
129
+ await cliExitWithError('--baseline cannot be combined with --dry-run.');
130
+ }
118
131
  let source;
119
132
  try {
120
133
  source = loadDumpSource(path.resolve(cwd, input));
@@ -178,9 +191,11 @@ export default async (argv, prompter, _options) => {
178
191
  }
179
192
  }
180
193
  let firstDir;
194
+ const writtenDirs = [];
181
195
  for (const pkg of packages) {
182
196
  const dir = writePackage(outBase, pkg);
183
197
  firstDir ??= dir;
198
+ writtenDirs.push(dir);
184
199
  if (!sqlToStdout) {
185
200
  log.success(`wrote ${pkg.rows.length} changes to ${dir}`);
186
201
  }
@@ -188,6 +203,14 @@ export default async (argv, prompter, _options) => {
188
203
  if (emitRequested && firstDir) {
189
204
  await projectModule(firstDir, emit, sqlToStdout ? undefined : msg => log.success(msg));
190
205
  }
206
+ if (baseline) {
207
+ for (const dir of writtenDirs) {
208
+ const { entries, backfillSql } = await buildBaselineBackfill(dir);
209
+ const baselinePath = path.join(dir, 'baseline.sql');
210
+ writeFileSync(baselinePath, backfillSql);
211
+ log.success(`wrote baseline backfill for ${entries.length} change(s) to ${baselinePath}`);
212
+ }
213
+ }
191
214
  }
192
215
  const { summary } = result;
193
216
  if (!sqlToStdout) {
@@ -0,0 +1,105 @@
1
+ import { baselineBackfillEntries, loadPlanSideModules, PgpmMigrate } from '@pgpmjs/core';
2
+ import { emitLedgerBackfill } from '@pgpmjs/diff';
3
+ import { Logger } from '@pgpmjs/logger';
4
+ import { writeFileSync } from 'fs';
5
+ import * as path from 'path';
6
+ import { getPgEnvOptions } from 'pg-env';
7
+ import { getTargetDatabase } from '../../utils/database';
8
+ const log = new Logger('migrate-baseline');
9
+ const STDOUT_TARGET = '-';
10
+ export const baselineUsageText = `
11
+ Migrate Baseline Command:
12
+
13
+ pgpm migrate baseline [<workspace|module>] [OPTIONS]
14
+
15
+ Record a plan's changes as already deployed WITHOUT executing any DDL, so a
16
+ database that already carries the schema (imported from a dump, restored, or
17
+ otherwise adopted from outside pgpm) is brought to the plan's head in the
18
+ pgpm_migrate ledger. A subsequent 'pgpm deploy' then runs only genuinely new
19
+ changes.
20
+
21
+ Each change is recorded through the same pgpm_migrate.deploy procedure a real
22
+ deploy uses, with log-only mode (empty script, p_log_only => TRUE), in plan
23
+ order.
24
+
25
+ Options:
26
+ --help, -h Show this help message
27
+ --emit-ledger <file|-> Write the log-only backfill SQL instead of applying
28
+ it (- for stdout). Offline; no database is touched.
29
+ The target must already have the pgpm_migrate schema
30
+ (run 'pgpm migrate init' first).
31
+ --database <name> Database to apply the baseline to (skips the prompt)
32
+ -y, --yes Skip the confirmation prompt when applying
33
+ --no-tx Apply without wrapping each module in a transaction
34
+ --cwd <directory> Working directory (default: current directory)
35
+
36
+ Examples:
37
+ pgpm migrate baseline ./my-app --emit-ledger baseline.sql
38
+ pgpm migrate baseline --database mydb
39
+ pgpm migrate baseline ./ws --emit-ledger -
40
+ `;
41
+ export default async (argv, prompter, _options) => {
42
+ if (argv.help || argv.h) {
43
+ console.log(baselineUsageText);
44
+ process.exit(0);
45
+ }
46
+ const cwd = argv.cwd || process.cwd();
47
+ const spec = argv._?.filter(Boolean)[0] ?? cwd;
48
+ const modules = await loadPlanSideModules(spec, cwd);
49
+ const entries = await baselineBackfillEntries(modules);
50
+ if (entries.length === 0) {
51
+ log.warn('No plan changes with deploy scripts found; nothing to baseline.');
52
+ return argv;
53
+ }
54
+ const emitTarget = argv['emit-ledger'] ?? argv.emitLedger;
55
+ // Offline emit: write the log-only backfill SQL, touch no database.
56
+ if (typeof emitTarget === 'string' && emitTarget) {
57
+ const sql = emitLedgerBackfill(entries);
58
+ if (emitTarget === STDOUT_TARGET) {
59
+ process.stdout.write(sql);
60
+ }
61
+ else {
62
+ const outPath = path.resolve(cwd, emitTarget);
63
+ writeFileSync(outPath, sql);
64
+ log.success(`Wrote baseline backfill for ${entries.length} change(s) to ${outPath}`);
65
+ }
66
+ return argv;
67
+ }
68
+ // Apply: record every change log-only against the target database.
69
+ const database = await getTargetDatabase(argv, prompter, {
70
+ message: 'Select database to baseline'
71
+ });
72
+ const { yes } = await prompter.prompt(argv, [
73
+ {
74
+ name: 'yes',
75
+ type: 'confirm',
76
+ message: `Record ${entries.length} change(s) as deployed (log-only) in "${database}"?`,
77
+ required: true
78
+ }
79
+ ]);
80
+ if (!yes) {
81
+ log.info('Operation cancelled.');
82
+ return argv;
83
+ }
84
+ const config = getPgEnvOptions({ database });
85
+ const useTransaction = argv.tx !== false;
86
+ const client = new PgpmMigrate(config);
87
+ try {
88
+ let recorded = 0;
89
+ let skipped = 0;
90
+ for (const mod of modules) {
91
+ const result = await client.deploy({
92
+ modulePath: mod.modulePath,
93
+ logOnly: true,
94
+ useTransaction
95
+ });
96
+ recorded += result.deployed.length;
97
+ skipped += result.skipped.length;
98
+ }
99
+ log.success(`Baseline complete in "${database}": ${recorded} change(s) recorded, ${skipped} already present.`);
100
+ }
101
+ finally {
102
+ await client.close();
103
+ }
104
+ return argv;
105
+ };
@@ -1,4 +1,5 @@
1
1
  import { extractFirst } from 'inquirerer';
2
+ import baseline from './migrate/baseline';
2
3
  import deps from './migrate/deps';
3
4
  // Migrate subcommands
4
5
  import init from './migrate/init';
@@ -8,7 +9,8 @@ const subcommandMap = {
8
9
  init,
9
10
  status,
10
11
  list,
11
- deps
12
+ deps,
13
+ baseline
12
14
  };
13
15
  const migrateUsageText = `
14
16
  Migrate Commands:
@@ -17,6 +19,7 @@ Migrate Commands:
17
19
  pgpm migrate status Show current migration status
18
20
  pgpm migrate list List all changes (deployed and pending)
19
21
  pgpm migrate deps Show change dependencies
22
+ pgpm migrate baseline Record a plan as already deployed (log-only), adopting an existing database
20
23
 
21
24
  Options:
22
25
  --help, -h Show this help message
@@ -58,7 +61,8 @@ function getSubcommandDescription(cmd) {
58
61
  init: 'Initialize migration tracking in database',
59
62
  status: 'Show current migration status',
60
63
  list: 'List all changes (deployed and pending)',
61
- deps: 'Show change dependencies'
64
+ deps: 'Show change dependencies',
65
+ baseline: 'Record a plan as already deployed (log-only), adopting an existing database'
62
66
  };
63
67
  return descriptions[cmd] || '';
64
68
  }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { readFileSync } from 'fs';
3
+ import { getPgClientCommand } from 'pg-env';
3
4
  export const runCommand = (command, args) => {
4
5
  return new Promise((resolve) => {
5
6
  const child = spawn(command, args, { stdio: 'pipe' });
@@ -183,7 +184,8 @@ export function parsePsqlMajor(versionOutput) {
183
184
  return Number.isNaN(major) ? null : major;
184
185
  }
185
186
  export async function checkPsql(info, exec = runCommand) {
186
- const result = await exec('psql', ['--version']);
187
+ const [cmd, ...prefixArgs] = getPgClientCommand('psql');
188
+ const result = await exec(cmd, [...prefixArgs, '--version']);
187
189
  if (result.code !== 0) {
188
190
  return {
189
191
  name: 'psql',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgpm",
3
- "version": "5.21.2",
3
+ "version": "5.23.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,24 +46,24 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@inquirerer/utils": "^3.3.9",
49
- "@pgpmjs/core": "^7.21.4",
50
- "@pgpmjs/diff": "^0.1.2",
51
- "@pgpmjs/env": "^2.40.3",
52
- "@pgpmjs/export": "^1.15.3",
53
- "@pgpmjs/import": "^0.2.0",
54
- "@pgpmjs/logger": "^2.23.3",
55
- "@pgpmjs/slice": "^0.13.9",
56
- "@pgpmjs/transform": "^0.20.0",
57
- "@pgpmjs/types": "^2.49.3",
58
- "@pgsql/quotes": "^18.2.0",
49
+ "@pgpmjs/core": "^7.23.0",
50
+ "@pgpmjs/diff": "^0.3.0",
51
+ "@pgpmjs/env": "^2.41.1",
52
+ "@pgpmjs/export": "^1.16.1",
53
+ "@pgpmjs/import": "^0.3.1",
54
+ "@pgpmjs/logger": "^2.24.0",
55
+ "@pgpmjs/slice": "^0.14.1",
56
+ "@pgpmjs/transform": "^0.22.0",
57
+ "@pgpmjs/types": "^2.50.1",
58
+ "@pgsql/quotes": "^18.2.1",
59
59
  "appstash": "^0.7.0",
60
60
  "find-and-require-package-json": "^0.9.1",
61
61
  "genomic": "^5.6.2",
62
62
  "inquirerer": "^4.9.1",
63
63
  "js-yaml": "^4.1.0",
64
- "pg-cache": "^3.24.3",
65
- "pg-env": "^1.27.2",
66
- "pgsql-deparser": "^18.3.1",
64
+ "pg-cache": "^3.25.1",
65
+ "pg-env": "^1.29.0",
66
+ "pgsql-deparser": "^18.3.3",
67
67
  "semver": "^7.8.1",
68
68
  "shelljs": "^0.10.0",
69
69
  "yanse": "^0.2.1"
@@ -80,5 +80,5 @@
80
80
  "pg",
81
81
  "pgsql"
82
82
  ],
83
- "gitHead": "247e48d25b13524d992f00c1b2dc063884d1deb1"
83
+ "gitHead": "6da0705f8aa49d62dc2defb8b135b71674611483"
84
84
  }
package/utils/doctor.js CHANGED
@@ -16,6 +16,7 @@ exports.checkNode = checkNode;
16
16
  exports.summarizeChecks = summarizeChecks;
17
17
  const child_process_1 = require("child_process");
18
18
  const fs_1 = require("fs");
19
+ const pg_env_1 = require("pg-env");
19
20
  const runCommand = (command, args) => {
20
21
  return new Promise((resolve) => {
21
22
  const child = (0, child_process_1.spawn)(command, args, { stdio: 'pipe' });
@@ -200,7 +201,8 @@ function parsePsqlMajor(versionOutput) {
200
201
  return Number.isNaN(major) ? null : major;
201
202
  }
202
203
  async function checkPsql(info, exec = exports.runCommand) {
203
- const result = await exec('psql', ['--version']);
204
+ const [cmd, ...prefixArgs] = (0, pg_env_1.getPgClientCommand)('psql');
205
+ const result = await exec(cmd, [...prefixArgs, '--version']);
204
206
  if (result.code !== 0) {
205
207
  return {
206
208
  name: 'psql',