pgpm 5.19.1 → 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.
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 +359 -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 +235 -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 +317 -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 +324 -0
  19. package/esm/commands/dump.js +3 -3
  20. package/esm/commands/export.js +22 -3
  21. package/esm/commands/import.js +200 -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 +278 -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 +82 -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 +36 -0
  44. package/utils/module-projections.js +123 -0
  45. package/utils/scratch-db.d.ts +16 -0
  46. package/utils/scratch-db.js +66 -0
@@ -1,10 +1,10 @@
1
- import { execSync } from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
4
1
  import { DEFAULT_TEMPLATE_REPO, DEFAULT_TEMPLATE_TOOL_NAME, inspectTemplate, PgpmPackage, resolveBoilerplateBaseDir, scaffoldTemplate, scanBoilerplates, SkillInstaller, sluggify, } from '@pgpmjs/core';
5
2
  import { resolveWorkspaceByType } from '@pgpmjs/env';
6
3
  import { errors } from '@pgpmjs/types';
4
+ import { execSync } from 'child_process';
5
+ import fs from 'fs';
7
6
  import { registerDefaultResolver } from 'inquirerer';
7
+ import path from 'path';
8
8
  import { isNoTtyRequested } from '../../utils';
9
9
  import { persistBoilerplateSource, readBoilerplateSource, resolveInitTemplateRepo, } from './boilerplate';
10
10
  const DEFAULT_MOTD = `
@@ -116,7 +116,6 @@ export default async (argv, prompter, _options) => {
116
116
  const project = new PgpmPackage(cwd);
117
117
  project.ensureModule();
118
118
  const info = project.getModuleInfo();
119
- info.version;
120
119
  await writePackage({
121
120
  version: info.version,
122
121
  extension: true,
@@ -1,8 +1,8 @@
1
1
  import { PgpmPackage } from '@pgpmjs/core';
2
2
  import { generateDryRunReport, slicePlan, writeSliceResult } from '@pgpmjs/slice';
3
3
  import { getGitConfigInfo } from '@pgpmjs/types';
4
+ import { existsSync, readFileSync } from 'fs';
4
5
  import { resolve } from 'path';
5
- import { readFileSync, existsSync } from 'fs';
6
6
  const sliceUsageText = `
7
7
  Slice Command:
8
8
 
@@ -2,8 +2,8 @@ import { PgpmPackage } from '@pgpmjs/core';
2
2
  import { getEnvOptions } from '@pgpmjs/env';
3
3
  import { Logger } from '@pgpmjs/logger';
4
4
  import path from 'path';
5
- import { getPgEnvOptions } from 'pg-env';
6
5
  import { getPgPool } from 'pg-cache';
6
+ import { getPgEnvOptions } from 'pg-env';
7
7
  const log = new Logger('test-packages');
8
8
  // ANSI color codes
9
9
  const RED = '\x1b[0;31m';
@@ -0,0 +1,278 @@
1
+ import { PgpmMigrate, PgpmPackage } from '@pgpmjs/core';
2
+ import { Logger } from '@pgpmjs/logger';
3
+ import { CHANGE_GRANULARITIES, EXPORT_GRANULARITIES, isChangeGranularity, isExportGranularity, loadModuleSource, parsePartitionConfig, PartitionCycleError, partitionExportRows, restructureExportRows } from '@pgpmjs/transform';
4
+ import * as fs from 'fs';
5
+ import { cliExitWithError } from 'inquirerer';
6
+ import * as path from 'path';
7
+ import { getPgEnvOptions } from 'pg-env';
8
+ import { checkOverwrite, writePackage } from '../utils/emit-package';
9
+ import { hasEmitProjection, parseEmitProjectionTargets, projectModule, STDOUT_TARGET } from '../utils/module-projections';
10
+ import { catalogDifferences, withScratchDatabases } from '../utils/scratch-db';
11
+ export { checkOverwrite } from '../utils/emit-package';
12
+ const log = new Logger('transform');
13
+ const transformUsageText = `
14
+ Transform Command:
15
+
16
+ pgpm transform --granularity <atomic|object|consolidated> [OPTIONS]
17
+
18
+ Re-dial an existing pgpm module (or every module in a workspace) through the
19
+ dials pipeline: flatten the deploy scripts in plan order, re-project them at
20
+ the requested granularity with spec-derived change paths, graph-derived
21
+ requires, and generated revert/verify scripts.
22
+
23
+ Options:
24
+ --help, -h Show this help message
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.
32
+ --partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
33
+ splitting the module into multiple pgpm packages with
34
+ derived cross-package requires.
35
+ --naming <style> Change path naming style: directory | flat (default: directory)
36
+ --cwd <directory> Module or workspace directory (default: current directory)
37
+ --out <dir> Output directory (default: sibling <module>-<granularity>)
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.
45
+ --check Deploy original and transformed output into scratch
46
+ databases and assert the catalogs are equivalent
47
+ --dry-run Print the resulting plan/paths without writing
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
+
52
+ Examples:
53
+ pgpm transform --granularity object
54
+ pgpm transform --granularity atomic --naming flat --out ./out
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
58
+ `;
59
+ const NAMING_STYLES = ['directory', 'flat'];
60
+ /**
61
+ * Resolve the base directory package dir(s) are created in. Without --out,
62
+ * packages are written as siblings of the source module — a plain transform
63
+ * of `my-mod` at granularity `object` lands in `../my-mod-object`.
64
+ */
65
+ export const resolveOutBase = (modulePath, out) => {
66
+ if (out)
67
+ return path.resolve(out);
68
+ return path.dirname(modulePath);
69
+ };
70
+ /** Order partition packages so prerequisites come before dependents. */
71
+ export const orderPackages = (packages) => {
72
+ const byName = new Map(packages.map(pkg => [pkg.name, pkg]));
73
+ const ordered = [];
74
+ const seen = new Set();
75
+ const visit = (pkg) => {
76
+ if (seen.has(pkg.name))
77
+ return;
78
+ seen.add(pkg.name);
79
+ for (const req of pkg.requires) {
80
+ const dep = byName.get(req);
81
+ if (dep)
82
+ visit(dep);
83
+ }
84
+ ordered.push(pkg);
85
+ };
86
+ for (const pkg of packages)
87
+ visit(pkg);
88
+ return ordered;
89
+ };
90
+ const readControlRequires = (modulePath, name) => {
91
+ const controlPath = path.join(modulePath, `${name}.control`);
92
+ if (!fs.existsSync(controlPath))
93
+ return [];
94
+ const match = fs.readFileSync(controlPath, 'utf-8').match(/^requires\s*=\s*'([^']*)'\s*$/m);
95
+ if (!match)
96
+ return [];
97
+ return match[1].split(',').map(s => s.trim()).filter(Boolean);
98
+ };
99
+ const deployModules = async (config, modulePaths) => {
100
+ const client = new PgpmMigrate(config);
101
+ for (const modulePath of modulePaths) {
102
+ const result = await client.deploy({ modulePath });
103
+ if (result.failed) {
104
+ throw new Error(`deploy failed at change ${result.failed} (module ${modulePath})`);
105
+ }
106
+ }
107
+ };
108
+ /**
109
+ * Prove the transform is structurally lossless: deploy the original module and
110
+ * the transformed package(s) into two scratch databases and compare catalogs.
111
+ */
112
+ const runCheck = async (transformed) => {
113
+ const config = getPgEnvOptions();
114
+ const stamp = Date.now();
115
+ const names = [`pgpm_transform_check_a_${stamp}`, `pgpm_transform_check_b_${stamp}`];
116
+ return withScratchDatabases(config, names, async ([cfgOriginal, cfgTransformed]) => {
117
+ await deployModules(cfgOriginal, [transformed.modulePath]);
118
+ const orderedDirs = orderPackages(transformed.packages).map(pkg => path.join(transformed.outBase, pkg.name));
119
+ await deployModules(cfgTransformed, orderedDirs);
120
+ return catalogDifferences(cfgOriginal, cfgTransformed);
121
+ });
122
+ };
123
+ const transformModule = async (modulePath, granularity, changeGranularity, naming, partition, out) => {
124
+ const source = loadModuleSource(modulePath);
125
+ const warnings = [...source.warnings];
126
+ const rows = source.changes.map(change => ({
127
+ name: change.name,
128
+ deploy: change.name,
129
+ deps: change.dependencies,
130
+ content: change.deploy
131
+ }));
132
+ const restructured = await restructureExportRows(rows, granularity, { naming, changeGranularity });
133
+ warnings.push(...restructured.warnings.map(w => `restructure (${granularity}): ${w}`));
134
+ const outBase = resolveOutBase(modulePath, out);
135
+ let packages;
136
+ if (partition) {
137
+ const result = await partitionExportRows(restructured.rows, partition);
138
+ warnings.push(...result.warnings.map(w => `partition: ${w}`));
139
+ packages = result.packages;
140
+ }
141
+ else {
142
+ // Distinct package name so the output can live beside the source module
143
+ // in the same workspace without colliding.
144
+ packages = [{ name: `${source.name}-${granularity}`, requires: [], rows: restructured.rows }];
145
+ }
146
+ return { modulePath, name: source.name, outBase, packages, warnings };
147
+ };
148
+ const printDryRun = (transformed) => {
149
+ console.log(`module ${transformed.name} (${transformed.modulePath})`);
150
+ for (const pkg of transformed.packages) {
151
+ console.log(` package ${pkg.name} -> ${path.join(transformed.outBase, pkg.name)}`);
152
+ for (const row of pkg.rows) {
153
+ const deps = row.deps?.length ? ` [${row.deps.join(' ')}]` : '';
154
+ console.log(` ${row.deploy}${deps}`);
155
+ }
156
+ }
157
+ };
158
+ export default async (argv, prompter, _options) => {
159
+ if (argv.help || argv.h) {
160
+ console.log(transformUsageText);
161
+ process.exit(0);
162
+ }
163
+ const granularityRaw = argv.granularity;
164
+ if (granularityRaw === undefined) {
165
+ await cliExitWithError(`--granularity is required. Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
166
+ }
167
+ if (!isExportGranularity(granularityRaw)) {
168
+ await cliExitWithError(`Invalid --granularity "${granularityRaw}". Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
169
+ }
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;
176
+ const namingRaw = argv.naming ?? 'directory';
177
+ if (!NAMING_STYLES.includes(namingRaw)) {
178
+ await cliExitWithError(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
179
+ }
180
+ const naming = namingRaw;
181
+ const cwd = argv.cwd || process.cwd();
182
+ let partition;
183
+ if (typeof argv.partition === 'string' && argv.partition) {
184
+ try {
185
+ partition = parsePartitionConfig(path.resolve(cwd, argv.partition));
186
+ }
187
+ catch (err) {
188
+ await cliExitWithError(err instanceof Error ? err.message : String(err));
189
+ }
190
+ }
191
+ const out = typeof argv.out === 'string' && argv.out ? path.resolve(cwd, argv.out) : undefined;
192
+ const write = Boolean(argv.write);
193
+ const check = Boolean(argv.check);
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;
198
+ const pkg = new PgpmPackage(path.resolve(cwd));
199
+ let modulePaths;
200
+ if (pkg.isInModule()) {
201
+ modulePaths = [pkg.modulePath];
202
+ }
203
+ else if (pkg.workspacePath) {
204
+ const moduleMap = pkg.getModuleMap();
205
+ modulePaths = Object.values(moduleMap).map(mod => path.resolve(pkg.workspacePath, mod.path));
206
+ if (modulePaths.length === 0) {
207
+ await cliExitWithError('No modules found in workspace.');
208
+ }
209
+ if (out && modulePaths.length > 1) {
210
+ await cliExitWithError('--out is not supported when transforming a multi-module workspace.');
211
+ }
212
+ }
213
+ else {
214
+ await cliExitWithError('Not inside a pgpm module or workspace (pass --cwd <dir>).');
215
+ return;
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
+ }
223
+ for (const modulePath of modulePaths) {
224
+ let transformed;
225
+ try {
226
+ transformed = await transformModule(modulePath, granularity, changeGranularity, naming, partition, out);
227
+ }
228
+ catch (err) {
229
+ if (err instanceof PartitionCycleError) {
230
+ await cliExitWithError(`Partition failed: ${err.message}`);
231
+ return;
232
+ }
233
+ throw err;
234
+ }
235
+ for (const warning of transformed.warnings) {
236
+ console.warn(`transform: ${warning}`);
237
+ }
238
+ if (dryRun) {
239
+ printDryRun(transformed);
240
+ continue;
241
+ }
242
+ for (const pkgRows of transformed.packages) {
243
+ const targetDir = path.join(transformed.outBase, pkgRows.name);
244
+ const guard = checkOverwrite(targetDir, modulePath, write);
245
+ if (guard) {
246
+ await cliExitWithError(guard);
247
+ }
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
+ }
252
+ const sourceRequires = readControlRequires(modulePath, transformed.name);
253
+ let firstDir;
254
+ for (const pkgRows of transformed.packages) {
255
+ const dir = writePackage(transformed.outBase, pkgRows, sourceRequires);
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));
263
+ }
264
+ if (check) {
265
+ log.info('running --check: deploying original and transformed output to scratch databases...');
266
+ const diffs = await runCheck(transformed);
267
+ if (diffs.length) {
268
+ console.error(`--check failed: catalogs differ (${diffs.length} differences):`);
269
+ for (const diff of diffs)
270
+ console.error(` ${diff}`);
271
+ await cliExitWithError('Transform is not structurally lossless.');
272
+ }
273
+ log.success('--check passed: catalogs are structurally equivalent.');
274
+ }
275
+ }
276
+ prompter.close();
277
+ return argv;
278
+ };
@@ -1,7 +1,7 @@
1
1
  import { suppressUpdateCheck } from '@inquirerer/utils';
2
2
  import { Logger } from '@pgpmjs/logger';
3
- import { cliExitWithError, getPackageJson } from 'inquirerer';
4
3
  import { spawn } from 'child_process';
4
+ import { cliExitWithError, getPackageJson } from 'inquirerer';
5
5
  import { fetchLatestVersion } from '../utils/npm-version';
6
6
  const log = new Logger('update');
7
7
  const updateUsageText = `
package/esm/commands.js CHANGED
@@ -7,12 +7,14 @@ import analyze from './commands/analyze';
7
7
  import cache from './commands/cache';
8
8
  import clear from './commands/clear';
9
9
  import deploy from './commands/deploy';
10
+ import diff from './commands/diff';
10
11
  import docker from './commands/docker';
11
12
  import doctor from './commands/doctor';
12
13
  import dump from './commands/dump';
13
14
  import env from './commands/env';
14
15
  import _export from './commands/export';
15
16
  import extension from './commands/extension';
17
+ import _import from './commands/import';
16
18
  import init from './commands/init';
17
19
  import install from './commands/install';
18
20
  import kill from './commands/kill';
@@ -28,6 +30,7 @@ import slice from './commands/slice';
28
30
  import syncVersions from './commands/sync-versions';
29
31
  import tag from './commands/tag';
30
32
  import testPackages from './commands/test-packages';
33
+ import transform from './commands/transform';
31
34
  import tune from './commands/tune';
32
35
  import updateCmd from './commands/update';
33
36
  import upgrade from './commands/upgrade';
@@ -47,6 +50,7 @@ const ENGINE_EXEMPT_COMMANDS = new Set([
47
50
  'doctor',
48
51
  'env',
49
52
  'extension',
53
+ 'import',
50
54
  'init',
51
55
  'install',
52
56
  'package',
@@ -79,6 +83,7 @@ export const createPgpmCommandMap = (skipPgTeardown = false) => {
79
83
  'admin-users': pgt(adminUsers),
80
84
  clear: pgt(clear),
81
85
  deploy: pgt(deploy),
86
+ diff: pgt(diff),
82
87
  docker,
83
88
  doctor,
84
89
  dump: pgt(dump),
@@ -91,6 +96,7 @@ export const createPgpmCommandMap = (skipPgTeardown = false) => {
91
96
  plan: pgt(plan),
92
97
  regen,
93
98
  export: pgt(_export),
99
+ import: pgt(_import),
94
100
  package: pgt(_package),
95
101
  tag: pgt(tag),
96
102
  kill: pgt(kill),
@@ -102,6 +108,7 @@ export const createPgpmCommandMap = (skipPgTeardown = false) => {
102
108
  slice,
103
109
  'sync-versions': syncVersions,
104
110
  'test-packages': pgt(testPackages),
111
+ transform: pgt(transform),
105
112
  tune: pgt(tune),
106
113
  upgrade: pgt(upgrade),
107
114
  up: pgt(upgrade),
@@ -12,6 +12,9 @@ export const usageText = `
12
12
  extension Manage module dependencies
13
13
  plan Generate module deployment plans
14
14
  regen Generate revert/verify scripts from deploy scripts
15
+ transform Re-dial a module through the dials pipeline (granularity/naming/partition)
16
+ import pgpm-itize an arbitrary SQL dump into a deployable module
17
+ diff Identity-keyed semantic diff between two schema sources (+ migration generation)
15
18
  package Package module for distribution
16
19
  materialize Transpile an apply proxy into a plain, committed module
17
20
  sync-versions Sync .control/Makefile/sql metadata to package.json versions
@@ -1,6 +1,6 @@
1
- import { PGPM_DRIVER_EXPORT, } from '@pgpmjs/types';
2
1
  import { createRequire } from 'node:module';
3
2
  import { resolve } from 'node:path';
3
+ import { PGPM_DRIVER_EXPORT, } from '@pgpmjs/types';
4
4
  /** Package name of the built-in PGlite driver plugin (the `--pglite` alias). */
5
5
  export const PGLITE_DRIVER_PLUGIN = '@pgpmjs/pglite-adapter';
6
6
  /**
@@ -0,0 +1 @@
1
+ export { checkOverwrite, writeControlFile, writeModule as writePackage } from '@pgpmjs/transform';
@@ -1,6 +1,6 @@
1
+ import { resolve } from 'node:path';
1
2
  import { getEnvOptions } from '@pgpmjs/env';
2
3
  import { BUILTIN_ENGINES, DEFAULT_ENGINE, } from '@pgpmjs/types';
3
- import { resolve } from 'node:path';
4
4
  import { activateDriver, driverOverrideFromArgv, PGLITE_DRIVER_PLUGIN } from './driver';
5
5
  /** Everything a real Postgres server can do — the built-in `pg` engine. */
6
6
  export const SERVER_CAPABILITIES = {
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Projections of an emitted pgpm module directory.
3
+ *
4
+ * A module directory is the canonical artifact the dials pipeline produces
5
+ * (`writeModule` from `@pgpmjs/transform`). "Linear SQL" and "bundle" are not
6
+ * separate emitters — they are pure projections of that one module, produced
7
+ * by the same battle-tested machinery `pgpm package` uses:
8
+ *
9
+ * - {@link emitModuleSql} -> consolidated single-file SQL (`packageModule`)
10
+ * - {@link emitModuleBundle} -> content-addressed bundle (`buildExecutableBundle`)
11
+ *
12
+ * They live in the CLI (not `@pgpmjs/transform`) because they reuse packaging
13
+ * and bundle code from `@pgpmjs/core`, and `core` already depends on
14
+ * `transform`.
15
+ */
16
+ import { buildExecutableBundle, packageModule, writeBundleArchiveFile } from '@pgpmjs/core';
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ /** Sentinel meaning "write to stdout" for a file-valued emit target. */
20
+ export const STDOUT_TARGET = '-';
21
+ /**
22
+ * Consolidate an emitted module directory into a single linear SQL script.
23
+ * Statements are resolved in plan order and deparsed once (comments and
24
+ * per-change file boundaries collapse away). When `target` is `-` the SQL is
25
+ * written to stdout; otherwise it is written to the given file path.
26
+ */
27
+ export const emitModuleSql = async (moduleDir, target) => {
28
+ const { sql } = await packageModule(moduleDir, { extension: false, usePlan: true, pretty: true });
29
+ if (target === STDOUT_TARGET) {
30
+ process.stdout.write(sql.endsWith('\n') ? sql : sql + '\n');
31
+ return;
32
+ }
33
+ const resolved = path.resolve(target);
34
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
35
+ fs.writeFileSync(resolved, sql.endsWith('\n') ? sql : sql + '\n');
36
+ };
37
+ /**
38
+ * Project an emitted module directory into a content-addressed bundle archive
39
+ * (`.bundle.tar.gz`) at `target` — the same executable bundle `pgpm package`
40
+ * stores beside the packaged SQL, but written to an arbitrary path.
41
+ */
42
+ export const emitModuleBundle = async (moduleDir, target) => {
43
+ const bundle = await buildExecutableBundle(moduleDir, { createdWith: '@pgpmjs/cli' });
44
+ const resolved = path.resolve(target);
45
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
46
+ writeBundleArchiveFile(bundle, resolved);
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
+ };
@@ -0,0 +1,61 @@
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
+ import { Logger } from '@pgpmjs/logger';
17
+ import { diffCatalogSnapshots, snapshotCatalog } from '@pgpmjs/transform';
18
+ import { getPgPool } from 'pg-cache';
19
+ const log = new Logger('scratch-db');
20
+ const createScratchDb = async (config, dbName) => {
21
+ const adminPool = getPgPool({ ...config, database: 'postgres' });
22
+ await adminPool.query(`DROP DATABASE IF EXISTS "${dbName}"`);
23
+ await adminPool.query(`CREATE DATABASE "${dbName}"`);
24
+ };
25
+ const dropScratchDb = async (config, dbName) => {
26
+ const adminPool = getPgPool({ ...config, database: 'postgres' });
27
+ await adminPool.query(`DROP DATABASE IF EXISTS "${dbName}" WITH (FORCE)`);
28
+ };
29
+ /**
30
+ * Create the named scratch databases, run `fn` with a per-database `PgConfig`
31
+ * for each (in the same order as `names`), and drop them all afterwards —
32
+ * even if `fn` throws. Drop failures are logged, never thrown, so they can't
33
+ * mask the original error.
34
+ */
35
+ export const withScratchDatabases = async (config, names, fn) => {
36
+ try {
37
+ for (const name of names)
38
+ await createScratchDb(config, name);
39
+ return await fn(names.map(name => ({ ...config, database: name })));
40
+ }
41
+ finally {
42
+ try {
43
+ for (const name of names)
44
+ await dropScratchDb(config, name);
45
+ }
46
+ catch (err) {
47
+ log.warn(`failed to drop scratch databases: ${err instanceof Error ? err.message : err}`);
48
+ }
49
+ }
50
+ };
51
+ /**
52
+ * Snapshot the catalogs of two databases and return their differences. An
53
+ * optional `normalize` transform is applied to both snapshots first (e.g.
54
+ * `withoutColumnOrder` when a drop+add migration cannot reproduce a fresh
55
+ * deploy's physical column ordinals). Empty result means equivalent.
56
+ */
57
+ export const catalogDifferences = async (configA, configB, normalize = snap => snap) => {
58
+ const snapA = await snapshotCatalog(getPgPool(configA));
59
+ const snapB = await snapshotCatalog(getPgPool(configB));
60
+ return diffCatalogSnapshots(normalize(snapA), normalize(snapB));
61
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgpm",
3
- "version": "5.19.1",
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,22 +46,24 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@inquirerer/utils": "^3.3.9",
49
- "@pgpmjs/core": "^7.21.2",
50
- "@pgpmjs/env": "^2.40.2",
51
- "@pgpmjs/export": "^1.14.2",
52
- "@pgpmjs/logger": "^2.23.2",
53
- "@pgpmjs/slice": "^0.13.7",
54
- "@pgpmjs/transform": "^0.18.0",
55
- "@pgpmjs/types": "^2.49.2",
56
- "@pgsql/quotes": "^18.1.0",
49
+ "@pgpmjs/core": "^7.21.4",
50
+ "@pgpmjs/diff": "^0.1.2",
51
+ "@pgpmjs/env": "^2.40.3",
52
+ "@pgpmjs/export": "^1.15.1",
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",
57
59
  "appstash": "^0.7.0",
58
60
  "find-and-require-package-json": "^0.9.1",
59
61
  "genomic": "^5.6.2",
60
62
  "inquirerer": "^4.9.1",
61
63
  "js-yaml": "^4.1.0",
62
- "pg-cache": "^3.24.2",
64
+ "pg-cache": "^3.24.3",
63
65
  "pg-env": "^1.27.2",
64
- "pgsql-deparser": "^18.2.0",
66
+ "pgsql-deparser": "^18.3.1",
65
67
  "semver": "^7.8.1",
66
68
  "shelljs": "^0.10.0",
67
69
  "yanse": "^0.2.1"
@@ -78,5 +80,5 @@
78
80
  "pg",
79
81
  "pgsql"
80
82
  ],
81
- "gitHead": "6d3e25dc5c9f93421dba49ddedf93239f87213a8"
83
+ "gitHead": "e6a7a57f88c9deb2a4acecaba2451320d1be8203"
82
84
  }
@@ -1 +1 @@
1
- export declare const usageText = "\n Usage: pgpm <command> [options]\n\n Core Database Operations:\n add Add database changes to plans and create SQL files\n deploy Deploy database changes and migrations\n verify Verify database state and migrations\n revert Revert database changes and migrations\n\n Project Management:\n init Initialize workspace or module\n extension Manage module dependencies\n plan Generate module deployment plans\n regen Generate revert/verify scripts from deploy scripts\n package Package module for distribution\n materialize Transpile an apply proxy into a plain, committed module\n sync-versions Sync .control/Makefile/sql metadata to package.json versions\n export Export database migrations from existing databases\n update Update pgpm to the latest version\n cache Manage cached templates (clean)\n upgrade Upgrade installed pgpm modules to latest versions (alias: up)\n\n Database Administration:\n dump Dump a database to a sql file\n kill Terminate database connections and optionally drop databases\n install Install database modules\n tag Add tags to changes for versioning\n clear Clear database state\n remove Remove database changes\n analyze Analyze database structure\n rename Rename database changes\n admin-users Manage admin users\n tune Tune PostgreSQL for throwaway environments (CI/test)\n\n Testing:\n test-packages Run integration tests on all workspace packages\n\n Migration Tools:\n migrate Migration management subcommands\n init Initialize migration tracking\n status Show migration status\n list List all changes\n deps Show change dependencies\n \n Development Tools:\n docker Manage Docker containers (start/stop/ls, --minio)\n doctor Check local dependencies (node, docker, psql) with install guidance\n env Manage environment variables (--supabase, --minio)\n test-packages Run integration tests on workspace packages\n \n Global Options:\n -h, --help Display this help information\n -v, --version Display version information\n --cwd <directory> Working directory (default: current directory)\n --engine <name> Migration backend: pg (default, Postgres server) or\n pglite (in-process WASM Postgres, no server). Also set by\n the \"engine\" key of pgpm.json or PGPM_ENGINE.\n --driver <pkg> Driver plugin package backing the engine (escape hatch)\n --pglite[=dataDir] Sugar for --engine pglite, persisted to <dataDir> if given\n\n Individual Command Help:\n pgpm <command> --help Display detailed help for specific command\n pgpm <command> -h Display detailed help for specific command\n\n Examples:\n pgpm deploy --help Show deploy command options\n pgpm init workspace Initialize new workspace\n pgpm install @pgpm/base32 Install a database module\n ";
1
+ export declare const usageText = "\n Usage: pgpm <command> [options]\n\n Core Database Operations:\n add Add database changes to plans and create SQL files\n deploy Deploy database changes and migrations\n verify Verify database state and migrations\n revert Revert database changes and migrations\n\n Project Management:\n init Initialize workspace or module\n extension Manage module dependencies\n plan Generate module deployment plans\n regen Generate revert/verify scripts from deploy scripts\n transform Re-dial a module through the dials pipeline (granularity/naming/partition)\n import pgpm-itize an arbitrary SQL dump into a deployable module\n diff Identity-keyed semantic diff between two schema sources (+ migration generation)\n package Package module for distribution\n materialize Transpile an apply proxy into a plain, committed module\n sync-versions Sync .control/Makefile/sql metadata to package.json versions\n export Export database migrations from existing databases\n update Update pgpm to the latest version\n cache Manage cached templates (clean)\n upgrade Upgrade installed pgpm modules to latest versions (alias: up)\n\n Database Administration:\n dump Dump a database to a sql file\n kill Terminate database connections and optionally drop databases\n install Install database modules\n tag Add tags to changes for versioning\n clear Clear database state\n remove Remove database changes\n analyze Analyze database structure\n rename Rename database changes\n admin-users Manage admin users\n tune Tune PostgreSQL for throwaway environments (CI/test)\n\n Testing:\n test-packages Run integration tests on all workspace packages\n\n Migration Tools:\n migrate Migration management subcommands\n init Initialize migration tracking\n status Show migration status\n list List all changes\n deps Show change dependencies\n \n Development Tools:\n docker Manage Docker containers (start/stop/ls, --minio)\n doctor Check local dependencies (node, docker, psql) with install guidance\n env Manage environment variables (--supabase, --minio)\n test-packages Run integration tests on workspace packages\n \n Global Options:\n -h, --help Display this help information\n -v, --version Display version information\n --cwd <directory> Working directory (default: current directory)\n --engine <name> Migration backend: pg (default, Postgres server) or\n pglite (in-process WASM Postgres, no server). Also set by\n the \"engine\" key of pgpm.json or PGPM_ENGINE.\n --driver <pkg> Driver plugin package backing the engine (escape hatch)\n --pglite[=dataDir] Sugar for --engine pglite, persisted to <dataDir> if given\n\n Individual Command Help:\n pgpm <command> --help Display detailed help for specific command\n pgpm <command> -h Display detailed help for specific command\n\n Examples:\n pgpm deploy --help Show deploy command options\n pgpm init workspace Initialize new workspace\n pgpm install @pgpm/base32 Install a database module\n ";
package/utils/display.js CHANGED
@@ -15,6 +15,9 @@ exports.usageText = `
15
15
  extension Manage module dependencies
16
16
  plan Generate module deployment plans
17
17
  regen Generate revert/verify scripts from deploy scripts
18
+ transform Re-dial a module through the dials pipeline (granularity/naming/partition)
19
+ import pgpm-itize an arbitrary SQL dump into a deployable module
20
+ diff Identity-keyed semantic diff between two schema sources (+ migration generation)
18
21
  package Package module for distribution
19
22
  materialize Transpile an apply proxy into a plain, committed module
20
23
  sync-versions Sync .control/Makefile/sql metadata to package.json versions
package/utils/driver.js CHANGED
@@ -4,9 +4,9 @@ exports.PGLITE_DRIVER_PLUGIN = void 0;
4
4
  exports.resolveDriverConfig = resolveDriverConfig;
5
5
  exports.driverOverrideFromArgv = driverOverrideFromArgv;
6
6
  exports.activateDriver = activateDriver;
7
- const types_1 = require("@pgpmjs/types");
8
7
  const node_module_1 = require("node:module");
9
8
  const node_path_1 = require("node:path");
9
+ const types_1 = require("@pgpmjs/types");
10
10
  /** Package name of the built-in PGlite driver plugin (the `--pglite` alias). */
11
11
  exports.PGLITE_DRIVER_PLUGIN = '@pgpmjs/pglite-adapter';
12
12
  /**