pgpm 5.19.1 → 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
@@ -1,10 +1,10 @@
1
1
  import { Logger } from '@pgpmjs/logger';
2
- import { getPgEnvOptions, getSpawnEnvWithPg } from 'pg-env';
3
- import { getPgPool } from 'pg-cache';
2
+ import { QuoteUtils } from '@pgsql/quotes';
4
3
  import { spawn } from 'child_process';
5
4
  import fs from 'fs';
6
5
  import path from 'path';
7
- import { QuoteUtils } from '@pgsql/quotes';
6
+ import { getPgPool } from 'pg-cache';
7
+ import { getPgEnvOptions, getSpawnEnvWithPg } from 'pg-env';
8
8
  import { getTargetDatabase } from '../utils';
9
9
  const log = new Logger('dump');
10
10
  const dumpUsageText = `
@@ -1,6 +1,7 @@
1
1
  import { PgpmPackage } from '@pgpmjs/core';
2
- import { exportMigrations, exportGraphQL, GraphQLClient, graphqlRowToPostgresRow, isExportGranularity, EXPORT_GRANULARITIES } from '@pgpmjs/export';
3
2
  import { getEnvOptions } from '@pgpmjs/env';
3
+ import { exportGraphQL, exportMigrations, GraphQLClient, graphqlRowToPostgresRow } from '@pgpmjs/export';
4
+ import { EXPORT_GRANULARITIES, isExportGranularity, parsePartitionConfig } from '@pgpmjs/transform';
4
5
  import { getGitConfigInfo } from '@pgpmjs/types';
5
6
  import { resolve } from 'path';
6
7
  import { getPgPool } from 'pg-cache';
@@ -28,12 +29,16 @@ Options:
28
29
  Change paths are derived from the naming spec and
29
30
  requires from the statement graph. When omitted,
30
31
  sql_actions rows are exported unchanged.
32
+ --partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
33
+ splitting the exported database module into multiple
34
+ pgpm packages with derived cross-package requires.
31
35
  --cwd <directory> Working directory (default: current directory)
32
36
 
33
37
  Examples:
34
38
  pgpm export Export migrations from selected database (SQL mode)
35
39
  pgpm export --exclude-categories security,permissions,auth,memberships
36
40
  pgpm export --granularity consolidated
41
+ pgpm export --granularity object --partition partition.json
37
42
  pgpm export --graphql-endpoint 'http://[::1]:3002/graphql' --migrate-endpoint 'http://[::1]:3000/graphql' --migrate-host db_migrate.localhost:3000
38
43
  `;
39
44
  export default async (argv, prompter, _options) => {
@@ -65,6 +70,18 @@ export default async (argv, prompter, _options) => {
65
70
  return;
66
71
  }
67
72
  const granularity = granularityRaw;
73
+ const partitionRaw = argv.partition;
74
+ let partition;
75
+ if (typeof partitionRaw === 'string' && partitionRaw) {
76
+ try {
77
+ partition = parsePartitionConfig(resolve(cwd, partitionRaw));
78
+ }
79
+ catch (err) {
80
+ console.error(err instanceof Error ? err.message : String(err));
81
+ prompter.close();
82
+ return;
83
+ }
84
+ }
68
85
  if (graphqlEndpoint) {
69
86
  // =========================================================================
70
87
  // GraphQL export mode
@@ -162,7 +179,8 @@ export default async (argv, prompter, _options) => {
162
179
  argv,
163
180
  username,
164
181
  excludeCategories,
165
- granularity
182
+ granularity,
183
+ partition
166
184
  });
167
185
  }
168
186
  else {
@@ -283,7 +301,8 @@ export default async (argv, prompter, _options) => {
283
301
  argv,
284
302
  username,
285
303
  excludeCategories,
286
- granularity
304
+ granularity,
305
+ partition
287
306
  });
288
307
  }
289
308
  prompter.close();
@@ -0,0 +1,163 @@
1
+ import { dumpCompatibilityWarnings, importDumpRows, linkTextualDeps, loadDumpSource } from '@pgpmjs/import';
2
+ import { Logger } from '@pgpmjs/logger';
3
+ import { EXPORT_GRANULARITIES, isExportGranularity, parsePartitionConfig, PartitionCycleError, partitionExportRows } from '@pgpmjs/transform';
4
+ import { cliExitWithError } from 'inquirerer';
5
+ import * as path from 'path';
6
+ import { checkOverwrite, writePackage } from '../utils/emit-package';
7
+ const log = new Logger('import');
8
+ const importUsageText = `
9
+ Import Command:
10
+
11
+ pgpm import <dump.sql | dir-of-sql-files> --pkg <module-name> [OPTIONS]
12
+
13
+ pgpm-itize an arbitrary SQL dump (e.g. pg_dump --schema-only output): parse
14
+ it, classify every statement, and emit a complete deployable pgpm module —
15
+ module dir, pgpm.plan, and deploy/revert/verify trees with spec-derived
16
+ change paths, graph-derived requires, and generated revert/verify scripts.
17
+
18
+ Directory inputs concatenate their .sql files in sorted (lexicographic)
19
+ filename order.
20
+
21
+ Dump handling:
22
+ - pg_dump preamble noise (SET ..., SELECT set_config(...), psql backslash
23
+ commands) is skipped; COMMENT ON and GRANT statements are kept, attached
24
+ to their host object's change.
25
+ - CREATE EXTENSION statements become .control requires (pgpm owns
26
+ extension creation at deploy time).
27
+ - COPY ... FROM stdin blocks and INSERTs are skipped with a warning unless
28
+ --with-data is passed, which emits them as seed fixture changes (COPY
29
+ data is converted to INSERTs).
30
+ - Statements that classify to no object land in a misc/statements change
31
+ with a warning — never dropped silently.
32
+
33
+ Options:
34
+ --help, -h Show this help message
35
+ --pkg <name> Module name for the generated package (required)
36
+ --granularity <level> Granularity dial: atomic | object | consolidated
37
+ (default: object)
38
+ --naming <style> Change path naming style: directory | flat (default: directory)
39
+ --out <dir> Output base directory (default: current directory);
40
+ the module is written to <out>/<pkg>
41
+ --with-data Import COPY/INSERT data as seed fixture changes
42
+ --partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
43
+ splitting the import into multiple pgpm packages with
44
+ derived cross-package requires
45
+ --write Allow overwriting an existing module directory
46
+ --cwd <directory> Working directory (default: current directory)
47
+ --dry-run Print the resulting plan/paths without writing
48
+
49
+ Examples:
50
+ pgpm import dump.sql --pkg my-app
51
+ pgpm import dump.sql --pkg my-app --granularity consolidated --naming flat
52
+ pgpm import ./sql-files --pkg my-app --out ./packages --with-data
53
+ pgpm import dump.sql --pkg my-app --partition partition.json
54
+ `;
55
+ const NAMING_STYLES = ['directory', 'flat'];
56
+ export default async (argv, prompter, _options) => {
57
+ if (argv.help || argv.h) {
58
+ console.log(importUsageText);
59
+ process.exit(0);
60
+ }
61
+ const cwd = argv.cwd || process.cwd();
62
+ const input = argv._?.filter(Boolean)[0];
63
+ if (!input) {
64
+ await cliExitWithError('Missing input: pgpm import <dump.sql | dir-of-sql-files> --pkg <name>');
65
+ }
66
+ const pkgName = argv.pkg;
67
+ if (!pkgName || typeof pkgName !== 'string') {
68
+ await cliExitWithError('--pkg <module-name> is required.');
69
+ }
70
+ const granularityRaw = argv.granularity ?? 'object';
71
+ if (!isExportGranularity(granularityRaw)) {
72
+ await cliExitWithError(`Invalid --granularity "${granularityRaw}". Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
73
+ }
74
+ const granularity = granularityRaw;
75
+ const namingRaw = argv.naming ?? 'directory';
76
+ if (!NAMING_STYLES.includes(namingRaw)) {
77
+ await cliExitWithError(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
78
+ }
79
+ const naming = namingRaw;
80
+ let partition;
81
+ if (typeof argv.partition === 'string' && argv.partition) {
82
+ try {
83
+ partition = parsePartitionConfig(path.resolve(cwd, argv.partition));
84
+ }
85
+ catch (err) {
86
+ await cliExitWithError(err instanceof Error ? err.message : String(err));
87
+ }
88
+ }
89
+ const outBase = typeof argv.out === 'string' && argv.out ? path.resolve(cwd, argv.out) : path.resolve(cwd);
90
+ const withData = Boolean(argv['with-data'] ?? argv.withData);
91
+ const write = Boolean(argv.write);
92
+ const dryRun = Boolean(argv['dry-run'] ?? argv.dryRun);
93
+ let source;
94
+ try {
95
+ source = loadDumpSource(path.resolve(cwd, input));
96
+ }
97
+ catch (err) {
98
+ await cliExitWithError(err instanceof Error ? err.message : String(err));
99
+ return;
100
+ }
101
+ if (source.files.length > 1) {
102
+ log.info(`concatenated ${source.files.length} .sql files in sorted order`);
103
+ }
104
+ for (const warning of dumpCompatibilityWarnings(source)) {
105
+ console.warn(`\nWARNING: ${warning}\n`);
106
+ }
107
+ const result = await importDumpRows(source, { granularity, naming, withData });
108
+ let packages;
109
+ if (partition) {
110
+ try {
111
+ const partitioned = await partitionExportRows(result.rows, partition);
112
+ result.warnings.push(...partitioned.warnings.map(w => `partition: ${w}`));
113
+ packages = partitioned.packages.map(pkg => ({
114
+ name: pkg.name,
115
+ requires: [...result.controlRequires, ...pkg.requires],
116
+ rows: linkTextualDeps(pkg.rows)
117
+ }));
118
+ }
119
+ catch (err) {
120
+ if (err instanceof PartitionCycleError) {
121
+ await cliExitWithError(`Partition failed: ${err.message}`);
122
+ return;
123
+ }
124
+ throw err;
125
+ }
126
+ }
127
+ else {
128
+ packages = [{ name: pkgName, requires: result.controlRequires, rows: result.rows }];
129
+ }
130
+ for (const warning of result.warnings) {
131
+ console.warn(`import: ${warning}`);
132
+ }
133
+ if (dryRun) {
134
+ for (const pkg of packages) {
135
+ console.log(`package ${pkg.name} -> ${path.join(outBase, pkg.name)}`);
136
+ if (pkg.requires.length) {
137
+ console.log(` requires: ${pkg.requires.join(', ')}`);
138
+ }
139
+ for (const row of pkg.rows) {
140
+ const deps = row.deps?.length ? ` [${row.deps.join(' ')}]` : '';
141
+ console.log(` ${row.deploy}${deps}`);
142
+ }
143
+ }
144
+ }
145
+ else {
146
+ for (const pkg of packages) {
147
+ const guard = checkOverwrite(path.join(outBase, pkg.name), source.files[0], write);
148
+ if (guard) {
149
+ await cliExitWithError(guard);
150
+ }
151
+ }
152
+ for (const pkg of packages) {
153
+ const dir = writePackage(outBase, pkg);
154
+ log.success(`wrote ${pkg.rows.length} changes to ${dir}`);
155
+ }
156
+ }
157
+ const { summary } = result;
158
+ log.success(`import: ${summary.statements} statement(s) -> ${summary.changes} change(s), ` +
159
+ `${summary.skippedPreamble} preamble skipped, ${summary.skippedData} data skipped, ` +
160
+ `${summary.misc} in misc, ${result.warnings.length} warning(s)`);
161
+ prompter.close();
162
+ return argv;
163
+ };
@@ -1,6 +1,6 @@
1
+ import { DEFAULT_TEMPLATE_REPO, PgpmPackage, TEMPLATE_REPOS } from '@pgpmjs/core';
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
- import { DEFAULT_TEMPLATE_REPO, PgpmPackage, TEMPLATE_REPOS } from '@pgpmjs/core';
4
4
  /**
5
5
  * Resolve the template repo for an `init` invocation from its flags.
6
6
  *
@@ -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,236 @@
1
+ import { PgpmMigrate, PgpmPackage } from '@pgpmjs/core';
2
+ import { Logger } from '@pgpmjs/logger';
3
+ import { EXPORT_GRANULARITIES, 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 { catalogDifferences, withScratchDatabases } from '../utils/scratch-db';
10
+ export { checkOverwrite } from '../utils/emit-package';
11
+ const log = new Logger('transform');
12
+ const transformUsageText = `
13
+ Transform Command:
14
+
15
+ pgpm transform --granularity <atomic|object|consolidated> [OPTIONS]
16
+
17
+ Re-dial an existing pgpm module (or every module in a workspace) through the
18
+ dials pipeline: flatten the deploy scripts in plan order, re-project them at
19
+ the requested granularity with spec-derived change paths, graph-derived
20
+ requires, and generated revert/verify scripts.
21
+
22
+ Options:
23
+ --help, -h Show this help message
24
+ --granularity <level> Target granularity: atomic | object | consolidated (required)
25
+ --partition <file> Partition config (JSON: rules/defaultPackage/splitRiders)
26
+ splitting the module into multiple pgpm packages with
27
+ derived cross-package requires.
28
+ --naming <style> Change path naming style: directory | flat (default: directory)
29
+ --cwd <directory> Module or workspace directory (default: current directory)
30
+ --out <dir> Output directory (default: sibling <module>-<granularity>)
31
+ --write Allow writing over an existing/module directory
32
+ --check Deploy original and transformed output into scratch
33
+ databases and assert the catalogs are equivalent
34
+ --dry-run Print the resulting plan/paths without writing
35
+
36
+ Examples:
37
+ pgpm transform --granularity object
38
+ pgpm transform --granularity atomic --naming flat --out ./out
39
+ pgpm transform --granularity object --partition partition.json --check
40
+ `;
41
+ const NAMING_STYLES = ['directory', 'flat'];
42
+ /**
43
+ * Resolve the base directory package dir(s) are created in. Without --out,
44
+ * packages are written as siblings of the source module — a plain transform
45
+ * of `my-mod` at granularity `object` lands in `../my-mod-object`.
46
+ */
47
+ export const resolveOutBase = (modulePath, out) => {
48
+ if (out)
49
+ return path.resolve(out);
50
+ return path.dirname(modulePath);
51
+ };
52
+ /** Order partition packages so prerequisites come before dependents. */
53
+ export const orderPackages = (packages) => {
54
+ const byName = new Map(packages.map(pkg => [pkg.name, pkg]));
55
+ const ordered = [];
56
+ const seen = new Set();
57
+ const visit = (pkg) => {
58
+ if (seen.has(pkg.name))
59
+ return;
60
+ seen.add(pkg.name);
61
+ for (const req of pkg.requires) {
62
+ const dep = byName.get(req);
63
+ if (dep)
64
+ visit(dep);
65
+ }
66
+ ordered.push(pkg);
67
+ };
68
+ for (const pkg of packages)
69
+ visit(pkg);
70
+ return ordered;
71
+ };
72
+ const readControlRequires = (modulePath, name) => {
73
+ const controlPath = path.join(modulePath, `${name}.control`);
74
+ if (!fs.existsSync(controlPath))
75
+ return [];
76
+ const match = fs.readFileSync(controlPath, 'utf-8').match(/^requires\s*=\s*'([^']*)'\s*$/m);
77
+ if (!match)
78
+ return [];
79
+ return match[1].split(',').map(s => s.trim()).filter(Boolean);
80
+ };
81
+ const deployModules = async (config, modulePaths) => {
82
+ const client = new PgpmMigrate(config);
83
+ for (const modulePath of modulePaths) {
84
+ const result = await client.deploy({ modulePath });
85
+ if (result.failed) {
86
+ throw new Error(`deploy failed at change ${result.failed} (module ${modulePath})`);
87
+ }
88
+ }
89
+ };
90
+ /**
91
+ * Prove the transform is structurally lossless: deploy the original module and
92
+ * the transformed package(s) into two scratch databases and compare catalogs.
93
+ */
94
+ const runCheck = async (transformed) => {
95
+ const config = getPgEnvOptions();
96
+ const stamp = Date.now();
97
+ const names = [`pgpm_transform_check_a_${stamp}`, `pgpm_transform_check_b_${stamp}`];
98
+ return withScratchDatabases(config, names, async ([cfgOriginal, cfgTransformed]) => {
99
+ await deployModules(cfgOriginal, [transformed.modulePath]);
100
+ const orderedDirs = orderPackages(transformed.packages).map(pkg => path.join(transformed.outBase, pkg.name));
101
+ await deployModules(cfgTransformed, orderedDirs);
102
+ return catalogDifferences(cfgOriginal, cfgTransformed);
103
+ });
104
+ };
105
+ const transformModule = async (modulePath, granularity, naming, partition, out) => {
106
+ const source = loadModuleSource(modulePath);
107
+ const warnings = [...source.warnings];
108
+ const rows = source.changes.map(change => ({
109
+ name: change.name,
110
+ deploy: change.name,
111
+ deps: change.dependencies,
112
+ content: change.deploy
113
+ }));
114
+ const restructured = await restructureExportRows(rows, granularity, { naming });
115
+ warnings.push(...restructured.warnings.map(w => `restructure (${granularity}): ${w}`));
116
+ const outBase = resolveOutBase(modulePath, out);
117
+ let packages;
118
+ if (partition) {
119
+ const result = await partitionExportRows(restructured.rows, partition);
120
+ warnings.push(...result.warnings.map(w => `partition: ${w}`));
121
+ packages = result.packages;
122
+ }
123
+ else {
124
+ // Distinct package name so the output can live beside the source module
125
+ // in the same workspace without colliding.
126
+ packages = [{ name: `${source.name}-${granularity}`, requires: [], rows: restructured.rows }];
127
+ }
128
+ return { modulePath, name: source.name, outBase, packages, warnings };
129
+ };
130
+ const printDryRun = (transformed) => {
131
+ console.log(`module ${transformed.name} (${transformed.modulePath})`);
132
+ for (const pkg of transformed.packages) {
133
+ console.log(` package ${pkg.name} -> ${path.join(transformed.outBase, pkg.name)}`);
134
+ for (const row of pkg.rows) {
135
+ const deps = row.deps?.length ? ` [${row.deps.join(' ')}]` : '';
136
+ console.log(` ${row.deploy}${deps}`);
137
+ }
138
+ }
139
+ };
140
+ export default async (argv, prompter, _options) => {
141
+ if (argv.help || argv.h) {
142
+ console.log(transformUsageText);
143
+ process.exit(0);
144
+ }
145
+ const granularityRaw = argv.granularity;
146
+ if (granularityRaw === undefined) {
147
+ await cliExitWithError(`--granularity is required. Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
148
+ }
149
+ if (!isExportGranularity(granularityRaw)) {
150
+ await cliExitWithError(`Invalid --granularity "${granularityRaw}". Expected one of: ${EXPORT_GRANULARITIES.join(', ')}.`);
151
+ }
152
+ const granularity = granularityRaw;
153
+ const namingRaw = argv.naming ?? 'directory';
154
+ if (!NAMING_STYLES.includes(namingRaw)) {
155
+ await cliExitWithError(`Invalid --naming "${namingRaw}". Expected one of: ${NAMING_STYLES.join(', ')}.`);
156
+ }
157
+ const naming = namingRaw;
158
+ const cwd = argv.cwd || process.cwd();
159
+ let partition;
160
+ if (typeof argv.partition === 'string' && argv.partition) {
161
+ try {
162
+ partition = parsePartitionConfig(path.resolve(cwd, argv.partition));
163
+ }
164
+ catch (err) {
165
+ await cliExitWithError(err instanceof Error ? err.message : String(err));
166
+ }
167
+ }
168
+ const out = typeof argv.out === 'string' && argv.out ? path.resolve(cwd, argv.out) : undefined;
169
+ const write = Boolean(argv.write);
170
+ const check = Boolean(argv.check);
171
+ const dryRun = Boolean(argv['dry-run'] ?? argv.dryRun);
172
+ const pkg = new PgpmPackage(path.resolve(cwd));
173
+ let modulePaths;
174
+ if (pkg.isInModule()) {
175
+ modulePaths = [pkg.modulePath];
176
+ }
177
+ else if (pkg.workspacePath) {
178
+ const moduleMap = pkg.getModuleMap();
179
+ modulePaths = Object.values(moduleMap).map(mod => path.resolve(pkg.workspacePath, mod.path));
180
+ if (modulePaths.length === 0) {
181
+ await cliExitWithError('No modules found in workspace.');
182
+ }
183
+ if (out && modulePaths.length > 1) {
184
+ await cliExitWithError('--out is not supported when transforming a multi-module workspace.');
185
+ }
186
+ }
187
+ else {
188
+ await cliExitWithError('Not inside a pgpm module or workspace (pass --cwd <dir>).');
189
+ return;
190
+ }
191
+ for (const modulePath of modulePaths) {
192
+ let transformed;
193
+ try {
194
+ transformed = await transformModule(modulePath, granularity, naming, partition, out);
195
+ }
196
+ catch (err) {
197
+ if (err instanceof PartitionCycleError) {
198
+ await cliExitWithError(`Partition failed: ${err.message}`);
199
+ return;
200
+ }
201
+ throw err;
202
+ }
203
+ for (const warning of transformed.warnings) {
204
+ console.warn(`transform: ${warning}`);
205
+ }
206
+ if (dryRun) {
207
+ printDryRun(transformed);
208
+ continue;
209
+ }
210
+ for (const pkgRows of transformed.packages) {
211
+ const targetDir = path.join(transformed.outBase, pkgRows.name);
212
+ const guard = checkOverwrite(targetDir, modulePath, write);
213
+ if (guard) {
214
+ await cliExitWithError(guard);
215
+ }
216
+ }
217
+ const sourceRequires = readControlRequires(modulePath, transformed.name);
218
+ for (const pkgRows of transformed.packages) {
219
+ const dir = writePackage(transformed.outBase, pkgRows, sourceRequires);
220
+ log.success(`wrote ${pkgRows.rows.length} changes to ${dir}`);
221
+ }
222
+ if (check) {
223
+ log.info('running --check: deploying original and transformed output to scratch databases...');
224
+ const diffs = await runCheck(transformed);
225
+ if (diffs.length) {
226
+ console.error(`--check failed: catalogs differ (${diffs.length} differences):`);
227
+ for (const diff of diffs)
228
+ console.error(` ${diff}`);
229
+ await cliExitWithError('Transform is not structurally lossless.');
230
+ }
231
+ log.success('--check passed: catalogs are structurally equivalent.');
232
+ }
233
+ }
234
+ prompter.close();
235
+ return argv;
236
+ };
@@ -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 = {