@pgpmjs/core 7.0.0 → 7.2.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 (102) hide show
  1. package/bundle/apply.d.ts +93 -0
  2. package/bundle/apply.js +137 -0
  3. package/bundle/index.d.ts +12 -0
  4. package/bundle/index.js +31 -0
  5. package/core/class/pgpm.d.ts +2 -2
  6. package/core/class/pgpm.js +6 -6
  7. package/esm/bundle/apply.js +131 -0
  8. package/esm/bundle/index.js +15 -0
  9. package/esm/core/class/pgpm.js +6 -6
  10. package/esm/index.js +7 -3
  11. package/esm/migrate/client.js +1 -1
  12. package/esm/migrate/utils/hash.js +3 -6
  13. package/esm/modules/modules.js +1 -1
  14. package/esm/packaging/package.js +40 -24
  15. package/esm/rebundle/index.js +4 -0
  16. package/esm/rebundle/module.js +111 -0
  17. package/esm/rebundle/rebundle.js +170 -0
  18. package/esm/rebundle/workspace.js +170 -0
  19. package/esm/refactor/rename.js +5 -27
  20. package/esm/resolution/deps.js +2 -2
  21. package/esm/resolution/resolve.js +2 -2
  22. package/esm/slice/slice.js +1 -1
  23. package/esm/workspace/minimal.js +73 -0
  24. package/index.d.ts +6 -2
  25. package/index.js +8 -4
  26. package/migrate/client.js +1 -1
  27. package/migrate/utils/hash.d.ts +2 -4
  28. package/migrate/utils/hash.js +4 -8
  29. package/modules/modules.d.ts +1 -1
  30. package/modules/modules.js +1 -1
  31. package/package.json +4 -2
  32. package/packaging/package.d.ts +23 -1
  33. package/packaging/package.js +42 -25
  34. package/rebundle/index.d.ts +4 -0
  35. package/{files/sql-scripts → rebundle}/index.js +4 -2
  36. package/rebundle/module.d.ts +23 -0
  37. package/rebundle/module.js +116 -0
  38. package/rebundle/rebundle.d.ts +26 -0
  39. package/rebundle/rebundle.js +175 -0
  40. package/rebundle/types.d.ts +185 -0
  41. package/rebundle/workspace.d.ts +15 -0
  42. package/rebundle/workspace.js +173 -0
  43. package/refactor/rename.d.ts +2 -8
  44. package/refactor/rename.js +6 -29
  45. package/resolution/deps.d.ts +1 -1
  46. package/resolution/deps.js +2 -2
  47. package/resolution/resolve.js +2 -2
  48. package/slice/slice.d.ts +1 -1
  49. package/slice/slice.js +1 -1
  50. package/slice/types.d.ts +1 -1
  51. package/workspace/minimal.d.ts +64 -0
  52. package/workspace/minimal.js +82 -0
  53. package/esm/files/extension/index.js +0 -3
  54. package/esm/files/extension/reader.js +0 -79
  55. package/esm/files/extension/writer.js +0 -99
  56. package/esm/files/index.js +0 -6
  57. package/esm/files/plan/generator.js +0 -82
  58. package/esm/files/plan/index.js +0 -5
  59. package/esm/files/plan/parser.js +0 -297
  60. package/esm/files/plan/validators.js +0 -181
  61. package/esm/files/plan/writer.js +0 -142
  62. package/esm/files/sql/header.js +0 -259
  63. package/esm/files/sql/index.js +0 -2
  64. package/esm/files/sql/writer.js +0 -107
  65. package/esm/files/sql-scripts/index.js +0 -2
  66. package/esm/files/sql-scripts/reader.js +0 -19
  67. package/esm/files/types/index.js +0 -1
  68. package/esm/utils/author.js +0 -11
  69. package/files/extension/index.d.ts +0 -2
  70. package/files/extension/index.js +0 -19
  71. package/files/extension/reader.d.ts +0 -24
  72. package/files/extension/reader.js +0 -86
  73. package/files/extension/writer.d.ts +0 -48
  74. package/files/extension/writer.js +0 -107
  75. package/files/index.d.ts +0 -5
  76. package/files/index.js +0 -22
  77. package/files/plan/generator.d.ts +0 -23
  78. package/files/plan/generator.js +0 -90
  79. package/files/plan/index.d.ts +0 -4
  80. package/files/plan/index.js +0 -21
  81. package/files/plan/parser.d.ts +0 -28
  82. package/files/plan/parser.js +0 -304
  83. package/files/plan/validators.d.ts +0 -52
  84. package/files/plan/validators.js +0 -187
  85. package/files/plan/writer.d.ts +0 -27
  86. package/files/plan/writer.js +0 -152
  87. package/files/sql/header.d.ts +0 -114
  88. package/files/sql/header.js +0 -266
  89. package/files/sql/index.d.ts +0 -2
  90. package/files/sql/index.js +0 -18
  91. package/files/sql/writer.d.ts +0 -12
  92. package/files/sql/writer.js +0 -114
  93. package/files/sql-scripts/index.d.ts +0 -1
  94. package/files/sql-scripts/reader.d.ts +0 -8
  95. package/files/sql-scripts/reader.js +0 -23
  96. package/files/types/index.d.ts +0 -46
  97. package/files/types/index.js +0 -17
  98. package/files/types/package.d.ts +0 -20
  99. package/utils/author.d.ts +0 -5
  100. package/utils/author.js +0 -14
  101. /package/esm/{files/types/package.js → rebundle/types.js} +0 -0
  102. /package/{files/types/package.js → rebundle/types.js} +0 -0
package/migrate/client.js CHANGED
@@ -6,7 +6,7 @@ const types_1 = require("@pgpmjs/types");
6
6
  const fs_1 = require("fs");
7
7
  const path_1 = require("path");
8
8
  const pg_cache_1 = require("pg-cache");
9
- const files_1 = require("../files");
9
+ const files_1 = require("@pgpmjs/ast/files");
10
10
  const deps_1 = require("../resolution/deps");
11
11
  const resolve_1 = require("../resolution/resolve");
12
12
  const clean_1 = require("./clean");
@@ -1,11 +1,9 @@
1
+ import { hashString } from '@pgpmjs/ast';
2
+ export { hashString };
1
3
  /**
2
4
  * Generate SHA256 hash of a file's contents
3
5
  */
4
6
  export declare function hashFile(filePath: string): Promise<string>;
5
- /**
6
- * Generate SHA256 hash of a string
7
- */
8
- export declare function hashString(content: string): string;
9
7
  /**
10
8
  * Generate SHA256 hash of a SQL file's parsed and cleaned AST
11
9
  */
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hashString = void 0;
3
4
  exports.hashFile = hashFile;
4
- exports.hashString = hashString;
5
5
  exports.hashSqlFile = hashSqlFile;
6
+ const ast_1 = require("@pgpmjs/ast");
7
+ Object.defineProperty(exports, "hashString", { enumerable: true, get: function () { return ast_1.hashString; } });
6
8
  const crypto_1 = require("crypto");
7
9
  const promises_1 = require("fs/promises");
8
10
  const pgsql_parser_1 = require("pgsql-parser");
@@ -14,12 +16,6 @@ async function hashFile(filePath) {
14
16
  const content = await (0, promises_1.readFile)(filePath, 'utf-8');
15
17
  return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
16
18
  }
17
- /**
18
- * Generate SHA256 hash of a string
19
- */
20
- function hashString(content) {
21
- return (0, crypto_1.createHash)('sha256').update(content).digest('hex');
22
- }
23
19
  /**
24
20
  * Generate SHA256 hash of a SQL file's parsed and cleaned AST
25
21
  */
@@ -28,5 +24,5 @@ async function hashSqlFile(filePath) {
28
24
  const parsed = await (0, pgsql_parser_1.parse)(content);
29
25
  const cleaned = (0, package_1.cleanTree)(parsed);
30
26
  const astString = JSON.stringify(cleaned);
31
- return hashString(astString);
27
+ return (0, ast_1.hashString)(astString);
32
28
  }
@@ -1,4 +1,4 @@
1
- import { Module } from '../files';
1
+ import { Module } from '@pgpmjs/ast/files';
2
2
  export type ModuleMap = Record<string, Module>;
3
3
  /**
4
4
  * Mapping from control file names (used in extensions list) to npm package names.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getExtensionsAndModulesChanges = exports.getExtensionsAndModules = exports.latestChangeAndVersion = exports.latestChange = exports.getMissingInstallableModules = exports.PGPM_MODULE_MAP = void 0;
4
- const files_1 = require("../files");
4
+ const files_1 = require("@pgpmjs/ast/files");
5
5
  const types_1 = require("@pgpmjs/types");
6
6
  /**
7
7
  * Mapping from control file names (used in extensions list) to npm package names.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgpmjs/core",
3
- "version": "7.0.0",
3
+ "version": "7.2.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "PGPM Package and Migration Tools",
6
6
  "main": "index.js",
@@ -48,6 +48,8 @@
48
48
  "makage": "^0.3.0"
49
49
  },
50
50
  "dependencies": {
51
+ "@pgpmjs/ast": "^0.1.0",
52
+ "@pgpmjs/bundle": "^0.1.0",
51
53
  "@pgpmjs/env": "^2.28.2",
52
54
  "@pgpmjs/logger": "^2.14.0",
53
55
  "@pgpmjs/server-utils": "^3.15.3",
@@ -64,5 +66,5 @@
64
66
  "pgsql-parser": "^18.1.1",
65
67
  "yanse": "^0.2.1"
66
68
  },
67
- "gitHead": "278757de075dcdaf3d7aca114644d64c10e49ba7"
69
+ "gitHead": "15a831fa029544068ee64e3386e4b08715d2b86d"
68
70
  }
@@ -10,7 +10,29 @@ interface WritePackageOptions extends PackageModuleOptions {
10
10
  version: string;
11
11
  packageDir: string;
12
12
  }
13
- export declare const packageModule: (packageDir: string, { usePlan, extension, pretty, functionDelimiter, outputDiff }?: PackageModuleOptions) => Promise<{
13
+ export interface MergeSqlOptions {
14
+ /** Strip `TransactionStmt`/`CreateExtensionStmt` before deparsing (extension mode) */
15
+ stripTransactions?: boolean;
16
+ /** Prepend a `\echo ... \quit` extension guard line */
17
+ echoExtensionName?: string;
18
+ pretty?: boolean;
19
+ functionDelimiter?: string;
20
+ }
21
+ /**
22
+ * Merge a concatenated SQL string into a single deparsed script.
23
+ *
24
+ * Parses the input, optionally strips transaction/extension statements, deparses
25
+ * with a stable function delimiter, and round-trips the result to detect any AST
26
+ * drift. This is the shared engine behind `packageModule` (whole-module packaging)
27
+ * and per-chunk rebundling — both must merge statements identically.
28
+ */
29
+ export declare const mergeSqlStatements: (sql: string, { stripTransactions, echoExtensionName, pretty, functionDelimiter }?: MergeSqlOptions) => Promise<{
30
+ sql: string;
31
+ diff?: boolean;
32
+ tree1?: string;
33
+ tree2?: string;
34
+ }>;
35
+ export declare const packageModule: (packageDir: string, { usePlan, extension, pretty, functionDelimiter }?: PackageModuleOptions) => Promise<{
14
36
  sql: string;
15
37
  diff?: boolean;
16
38
  tree1?: string;
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.writePackage = exports.packageModule = exports.cleanTree = void 0;
3
+ exports.writePackage = exports.packageModule = exports.mergeSqlStatements = exports.cleanTree = void 0;
4
4
  const logger_1 = require("@pgpmjs/logger");
5
5
  const fs_1 = require("fs");
6
6
  const path_1 = require("path");
7
7
  const pgsql_deparser_1 = require("pgsql-deparser");
8
8
  const pgsql_parser_1 = require("pgsql-parser");
9
- const files_1 = require("../files");
9
+ const files_1 = require("@pgpmjs/ast/files");
10
10
  const resolve_1 = require("../resolution/resolve");
11
11
  const transform_1 = require("./transform");
12
12
  const log = new logger_1.Logger('package');
@@ -19,8 +19,8 @@ const cleanTree = (tree) => {
19
19
  });
20
20
  };
21
21
  exports.cleanTree = cleanTree;
22
- const filterStatements = (stmts, extension) => {
23
- if (!extension)
22
+ const filterStatements = (stmts, stripTransactions) => {
23
+ if (!stripTransactions)
24
24
  return stmts;
25
25
  return stmts.filter(node => {
26
26
  const stmt = node.stmt;
@@ -28,7 +28,40 @@ const filterStatements = (stmts, extension) => {
28
28
  !stmt.hasOwnProperty('CreateExtensionStmt');
29
29
  });
30
30
  };
31
- const packageModule = async (packageDir, { usePlan = true, extension = true, pretty = true, functionDelimiter = '$EOFCODE$', outputDiff = false } = {}) => {
31
+ /**
32
+ * Merge a concatenated SQL string into a single deparsed script.
33
+ *
34
+ * Parses the input, optionally strips transaction/extension statements, deparses
35
+ * with a stable function delimiter, and round-trips the result to detect any AST
36
+ * drift. This is the shared engine behind `packageModule` (whole-module packaging)
37
+ * and per-chunk rebundling — both must merge statements identically.
38
+ */
39
+ const mergeSqlStatements = async (sql, { stripTransactions = false, echoExtensionName, pretty = true, functionDelimiter = '$EOFCODE$' } = {}) => {
40
+ if (!sql?.trim()) {
41
+ return { sql: '' };
42
+ }
43
+ const parsed = await (0, pgsql_parser_1.parse)(sql);
44
+ parsed.stmts = filterStatements(parsed.stmts, stripTransactions);
45
+ const topLine = echoExtensionName
46
+ ? `\\echo Use "CREATE EXTENSION ${echoExtensionName}" to load this file. \\quit\n`
47
+ : '';
48
+ const finalSql = await (0, pgsql_deparser_1.deparse)(parsed, { pretty, functionDelimiter });
49
+ const tree1 = parsed.stmts;
50
+ const reparsed = await (0, pgsql_parser_1.parse)(finalSql);
51
+ const tree2 = filterStatements(reparsed.stmts, stripTransactions);
52
+ const results = {
53
+ sql: `${topLine}${finalSql}`,
54
+ };
55
+ const diff = JSON.stringify((0, exports.cleanTree)(tree1)) !== JSON.stringify((0, exports.cleanTree)(tree2));
56
+ if (diff) {
57
+ results.diff = true;
58
+ results.tree1 = JSON.stringify((0, exports.cleanTree)(tree1), null, 2);
59
+ results.tree2 = JSON.stringify((0, exports.cleanTree)(tree2), null, 2);
60
+ }
61
+ return results;
62
+ };
63
+ exports.mergeSqlStatements = mergeSqlStatements;
64
+ const packageModule = async (packageDir, { usePlan = true, extension = true, pretty = true, functionDelimiter = '$EOFCODE$' } = {}) => {
32
65
  const resolveFn = usePlan ? resolve_1.resolveWithPlan : resolve_1.resolve;
33
66
  const sql = resolveFn(packageDir);
34
67
  if (!sql?.trim()) {
@@ -37,28 +70,12 @@ const packageModule = async (packageDir, { usePlan = true, extension = true, pre
37
70
  }
38
71
  const extname = (0, files_1.getExtensionName)(packageDir);
39
72
  try {
40
- const parsed = await (0, pgsql_parser_1.parse)(sql);
41
- parsed.stmts = filterStatements(parsed.stmts, extension);
42
- const topLine = extension
43
- ? `\\echo Use "CREATE EXTENSION ${extname}" to load this file. \\quit\n`
44
- : '';
45
- const finalSql = await (0, pgsql_deparser_1.deparse)(parsed, {
73
+ return await (0, exports.mergeSqlStatements)(sql, {
74
+ stripTransactions: extension,
75
+ echoExtensionName: extension ? extname : undefined,
46
76
  pretty,
47
- functionDelimiter
77
+ functionDelimiter,
48
78
  });
49
- const tree1 = parsed.stmts;
50
- const reparsed = await (0, pgsql_parser_1.parse)(finalSql);
51
- const tree2 = filterStatements(reparsed.stmts, extension);
52
- const results = {
53
- sql: `${topLine}${finalSql}`,
54
- };
55
- const diff = JSON.stringify((0, exports.cleanTree)(tree1)) !== JSON.stringify((0, exports.cleanTree)(tree2));
56
- if (diff) {
57
- results.diff = true;
58
- results.tree1 = JSON.stringify((0, exports.cleanTree)(tree1), null, 2);
59
- results.tree2 = JSON.stringify((0, exports.cleanTree)(tree2), null, 2);
60
- }
61
- return results;
62
79
  }
63
80
  catch (e) {
64
81
  log.error(`❌ Failed to parse SQL for ${packageDir}`);
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './rebundle';
3
+ export * from './module';
4
+ export * from './workspace';
@@ -14,5 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- // Re-export all sql-scripts functionality
18
- __exportStar(require("./reader"), exports);
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./rebundle"), exports);
19
+ __exportStar(require("./module"), exports);
20
+ __exportStar(require("./workspace"), exports);
@@ -0,0 +1,23 @@
1
+ import { Chunk, RebundleModuleOptions, RebundleModuleResult } from './types';
2
+ export declare const SCRIPT_DIRS: readonly ["deploy", "revert", "verify"];
3
+ export type ScriptDir = (typeof SCRIPT_DIRS)[number];
4
+ /**
5
+ * Merge one chunk's member scripts (in the correct order per direction) into a
6
+ * single deparsed, single-transaction migration string. Shared by the
7
+ * single-module (`rebundleModule`) and workspace (`rebundleWorkspace`) emitters.
8
+ */
9
+ export declare function mergeChunkScript(sourceDir: string, chunk: Chunk, scriptType: ScriptDir, opts?: {
10
+ pretty?: boolean;
11
+ functionDelimiter?: string;
12
+ }): Promise<string>;
13
+ /**
14
+ * Materialize a rebundled module: merge each chunk's member scripts into a
15
+ * single deparsed migration (reusing `packageModule`'s merge engine), write the
16
+ * chunk-quotient plan, and verify the result is byte-identical when packaged.
17
+ *
18
+ * Reuses existing plumbing throughout — `rebundlePlan` for the chunk model,
19
+ * `mergeSqlStatements`/`packageModule` for statement merging and the gate,
20
+ * `generatePlanContent` for the chunk plan, and the source control/Makefile
21
+ * verbatim.
22
+ */
23
+ export declare function rebundleModule(sourceDir: string, options: RebundleModuleOptions): Promise<RebundleModuleResult>;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SCRIPT_DIRS = void 0;
4
+ exports.mergeChunkScript = mergeChunkScript;
5
+ exports.rebundleModule = rebundleModule;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const parser_1 = require("@pgpmjs/ast/files/plan/parser");
9
+ const package_1 = require("../packaging/package");
10
+ const slice_1 = require("../slice/slice");
11
+ const rebundle_1 = require("./rebundle");
12
+ exports.SCRIPT_DIRS = ['deploy', 'revert', 'verify'];
13
+ /**
14
+ * Wrap a merged chunk body in a single transaction. Deploy/revert commit;
15
+ * verify rolls back (a verify script must leave no trace). The individual
16
+ * member files' own BEGIN/COMMIT were stripped during the merge, so each
17
+ * rebundled chunk deploys as exactly one transaction.
18
+ */
19
+ function wrapTransaction(body, scriptType) {
20
+ const trimmed = body.trim();
21
+ const tail = scriptType === 'verify' ? 'ROLLBACK;' : 'COMMIT;';
22
+ return `BEGIN;\n\n${trimmed}\n\n${tail}\n`;
23
+ }
24
+ /**
25
+ * Merge one chunk's member scripts (in the correct order per direction) into a
26
+ * single deparsed, single-transaction migration string. Shared by the
27
+ * single-module (`rebundleModule`) and workspace (`rebundleWorkspace`) emitters.
28
+ */
29
+ async function mergeChunkScript(sourceDir, chunk, scriptType, opts = {}) {
30
+ const merged = await (0, package_1.mergeSqlStatements)((0, rebundle_1.assembleChunkSql)(sourceDir, chunk, scriptType), {
31
+ stripTransactions: true,
32
+ pretty: opts.pretty ?? true,
33
+ functionDelimiter: opts.functionDelimiter ?? '$EOFCODE$',
34
+ });
35
+ return wrapTransaction(merged.sql, scriptType);
36
+ }
37
+ /**
38
+ * Copy any top-level files (control, Makefile, package.json, README, ...) from
39
+ * the source module, excluding the plan and the per-change script directories
40
+ * which are regenerated. The rebundled module stays the same extension — same
41
+ * name, same control `requires` — only its internal change set collapses.
42
+ */
43
+ function copyModuleMetadata(sourceDir, outputDir) {
44
+ for (const entry of (0, fs_1.readdirSync)(sourceDir, { withFileTypes: true })) {
45
+ if (entry.isDirectory())
46
+ continue;
47
+ if (entry.name === 'pgpm.plan')
48
+ continue;
49
+ (0, fs_1.copyFileSync)((0, path_1.join)(sourceDir, entry.name), (0, path_1.join)(outputDir, entry.name));
50
+ }
51
+ }
52
+ /**
53
+ * Materialize a rebundled module: merge each chunk's member scripts into a
54
+ * single deparsed migration (reusing `packageModule`'s merge engine), write the
55
+ * chunk-quotient plan, and verify the result is byte-identical when packaged.
56
+ *
57
+ * Reuses existing plumbing throughout — `rebundlePlan` for the chunk model,
58
+ * `mergeSqlStatements`/`packageModule` for statement merging and the gate,
59
+ * `generatePlanContent` for the chunk plan, and the source control/Makefile
60
+ * verbatim.
61
+ */
62
+ async function rebundleModule(sourceDir, options) {
63
+ const { outputDir, overwrite = false, pretty = true, functionDelimiter = '$EOFCODE$', ...strategy } = options;
64
+ if ((0, fs_1.existsSync)(outputDir) && (0, fs_1.readdirSync)(outputDir).length > 0 && !overwrite) {
65
+ throw new Error(`Output directory is not empty: ${outputDir}. Pass overwrite: true to replace.`);
66
+ }
67
+ const plan = (0, rebundle_1.rebundlePlan)(sourceDir, strategy);
68
+ // Tags anchor on the last member of the chunk that sealed at them; remap each
69
+ // to its chunk name so `deploy --to @tag` still lands on a real change.
70
+ const parsed = (0, parser_1.parsePlanFile)((0, path_1.join)(sourceDir, 'pgpm.plan'));
71
+ const sourceTags = parsed.data?.tags ?? [];
72
+ const memberToChunk = new Map();
73
+ for (const chunk of plan.chunks) {
74
+ for (const member of chunk.deploy)
75
+ memberToChunk.set(member, chunk.name);
76
+ }
77
+ const remappedTags = sourceTags.map(tag => ({
78
+ ...tag,
79
+ change: memberToChunk.get(tag.change) ?? tag.change,
80
+ }));
81
+ (0, fs_1.mkdirSync)(outputDir, { recursive: true });
82
+ for (const dir of exports.SCRIPT_DIRS)
83
+ (0, fs_1.mkdirSync)((0, path_1.join)(outputDir, dir), { recursive: true });
84
+ copyModuleMetadata(sourceDir, outputDir);
85
+ const written = [];
86
+ for (const chunk of plan.chunks) {
87
+ const paths = { chunk: chunk.name };
88
+ for (const dir of exports.SCRIPT_DIRS) {
89
+ const merged = await (0, package_1.mergeSqlStatements)((0, rebundle_1.assembleChunkSql)(sourceDir, chunk, dir), {
90
+ stripTransactions: true,
91
+ pretty,
92
+ functionDelimiter,
93
+ });
94
+ const rel = (0, path_1.join)(dir, `${chunk.name}.sql`);
95
+ (0, fs_1.writeFileSync)((0, path_1.join)(outputDir, rel), wrapTransaction(merged.sql, dir));
96
+ paths[dir] = rel;
97
+ }
98
+ written.push(paths);
99
+ }
100
+ // Chunk-quotient plan: one change per chunk, deps are earlier chunk names.
101
+ const planEntries = plan.chunks.map(chunk => ({
102
+ name: chunk.name,
103
+ dependencies: chunk.dependencies,
104
+ }));
105
+ (0, fs_1.writeFileSync)((0, path_1.join)(outputDir, 'pgpm.plan'), (0, slice_1.generatePlanContent)(plan.project, planEntries, remappedTags));
106
+ // Byte-identical gate: packaging the source and the rebundled module must
107
+ // produce the same consolidated extension SQL.
108
+ const sourcePkg = await (0, package_1.packageModule)(sourceDir, { pretty, functionDelimiter });
109
+ const outputPkg = await (0, package_1.packageModule)(outputDir, { pretty, functionDelimiter });
110
+ const invariant = {
111
+ ok: sourcePkg.sql === outputPkg.sql,
112
+ sourceDiff: !!sourcePkg.diff,
113
+ outputDiff: !!outputPkg.diff,
114
+ };
115
+ return { ...plan, outputDir, written, invariant };
116
+ }
@@ -0,0 +1,26 @@
1
+ import { Chunk, RebundleResult, RebundleStrategy } from './types';
2
+ /**
3
+ * Compute a rebundle plan for a pgpm module directory.
4
+ *
5
+ * Uses the existing plan parser, dependency graph, and package-dependency /
6
+ * deploy-order primitives from the slicer — chunks are treated exactly like
7
+ * packages for dependency and ordering purposes. No plan re-parsing or path
8
+ * logic is reinvented here.
9
+ */
10
+ export declare function rebundlePlan(moduleDir: string, strategy?: RebundleStrategy): RebundleResult;
11
+ /**
12
+ * Assemble the merged SQL for one chunk in the given direction by concatenating
13
+ * its member script files — the same file-concatenation `resolveWithPlan` uses,
14
+ * just scoped to the chunk. Members are already correctly ordered per direction.
15
+ */
16
+ export declare function assembleChunkSql(moduleDir: string, chunk: Chunk, scriptType?: 'deploy' | 'revert' | 'verify'): string;
17
+ /**
18
+ * Byte-identical gate: assembling all chunks in deploy order must reproduce the
19
+ * module's original resolved output in every direction the engine cares about
20
+ * (deploy topological, verify in deploy order, revert reversed). This is the
21
+ * invariant that makes any granularity-dial position safe.
22
+ */
23
+ export declare function verifyRebundleInvariant(moduleDir: string, result: RebundleResult): {
24
+ ok: boolean;
25
+ mismatches: ('deploy' | 'revert' | 'verify')[];
26
+ };
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rebundlePlan = rebundlePlan;
4
+ exports.assembleChunkSql = assembleChunkSql;
5
+ exports.verifyRebundleInvariant = verifyRebundleInvariant;
6
+ const fs_1 = require("fs");
7
+ const path_1 = require("path");
8
+ const parser_1 = require("@pgpmjs/ast/files/plan/parser");
9
+ const resolve_1 = require("../resolution/resolve");
10
+ const slice_1 = require("../slice/slice");
11
+ const parser_2 = require("@pgpmjs/ast/files/plan/parser");
12
+ /**
13
+ * Derive the boundary key for a change under the given strategy.
14
+ * Consecutive changes sharing a key may be merged into the same chunk.
15
+ */
16
+ function boundaryKey(changeName, strategy) {
17
+ if (strategy.boundary === 'none')
18
+ return '';
19
+ const folderKey = () => (0, slice_1.extractPackageFromPath)(changeName, strategy.depth ?? 1, strategy.prefixToStrip ?? 'schemas');
20
+ if (strategy.boundary === 'category') {
21
+ // Classifier/facts-driven seam: the caller decides each change's category;
22
+ // fall back to the folder key for changes it doesn't classify.
23
+ const category = strategy.categoryOf?.(changeName);
24
+ return category ?? folderKey();
25
+ }
26
+ return folderKey();
27
+ }
28
+ /**
29
+ * Assign changes to chunks by walking the source plan in order and greedily
30
+ * accumulating consecutive changes that share a boundary key, sealing a chunk
31
+ * when the key changes, the size cap is reached, or a member carries a tag
32
+ * (tags mark points you can `deploy --to @tag`, so they must stay chunk seams).
33
+ *
34
+ * Because chunks are contiguous runs of the plan, member and chunk order both
35
+ * follow the source, which is what guarantees byte-identical output.
36
+ */
37
+ function assignChunks(orderedChanges, graph, strategy) {
38
+ const chunks = [];
39
+ const seen = new Map();
40
+ const maxSize = strategy.maxChunkSize && strategy.maxChunkSize > 0 ? strategy.maxChunkSize : Infinity;
41
+ let current = null;
42
+ const seal = () => {
43
+ current = null;
44
+ };
45
+ for (const change of orderedChanges) {
46
+ const key = boundaryKey(change, strategy);
47
+ const changeHasTag = (graph.tags.get(change)?.length ?? 0) > 0;
48
+ if (current &&
49
+ current.key === key &&
50
+ current.members.length < maxSize) {
51
+ current.members.push(change);
52
+ }
53
+ else {
54
+ // Deterministic, collision-free chunk name: boundary key (or 'chunk'),
55
+ // suffixed with an ordinal when the same key splits across size caps.
56
+ const base = key || 'chunk';
57
+ const ordinal = (seen.get(base) ?? 0) + 1;
58
+ seen.set(base, ordinal);
59
+ const name = ordinal === 1 ? base : `${base}~${ordinal}`;
60
+ current = { name, members: [change], key };
61
+ chunks.push(current);
62
+ }
63
+ // A member that is a tag anchor seals the chunk after inclusion.
64
+ if (changeHasTag)
65
+ seal();
66
+ }
67
+ return chunks.map(({ name, members }) => ({ name, members }));
68
+ }
69
+ /**
70
+ * Compute a rebundle plan for a pgpm module directory.
71
+ *
72
+ * Uses the existing plan parser, dependency graph, and package-dependency /
73
+ * deploy-order primitives from the slicer — chunks are treated exactly like
74
+ * packages for dependency and ordering purposes. No plan re-parsing or path
75
+ * logic is reinvented here.
76
+ */
77
+ function rebundlePlan(moduleDir, strategy = {}) {
78
+ const planPath = (0, path_1.join)(moduleDir, 'pgpm.plan');
79
+ const planResult = (0, parser_2.parsePlanFile)(planPath);
80
+ if (!planResult.data) {
81
+ const msg = planResult.errors?.map(e => `Line ${e.line}: ${e.message}`).join('\n') || 'Unknown error';
82
+ throw new Error(`Failed to parse plan file: ${msg}`);
83
+ }
84
+ const graph = (0, slice_1.buildDependencyGraph)(planResult.data);
85
+ const orderedChanges = planResult.data.changes.map(c => c.name);
86
+ const warnings = [];
87
+ const rawChunks = assignChunks(orderedChanges, graph, strategy);
88
+ // Reverse lookup change -> chunk, reusing slice's package-dep machinery.
89
+ const assignments = new Map();
90
+ const changeToChunk = new Map();
91
+ for (const chunk of rawChunks) {
92
+ assignments.set(chunk.name, new Set(chunk.members));
93
+ for (const member of chunk.members)
94
+ changeToChunk.set(member, chunk.name);
95
+ }
96
+ const chunkDeps = (0, slice_1.buildPackageDependencies)(graph, assignments);
97
+ const cycle = (0, slice_1.detectPackageCycle)(chunkDeps);
98
+ if (cycle) {
99
+ warnings.push({
100
+ type: 'cycle_detected',
101
+ message: `Chunk cycle detected: ${cycle.join(' -> ')}`,
102
+ suggestedAction: 'Widen the boundary or raise maxChunkSize so cyclic changes share a chunk',
103
+ });
104
+ }
105
+ const deployOrder = (0, slice_1.computeDeployOrder)(chunkDeps);
106
+ // Because chunks are contiguous runs of the source plan, the greedy creation
107
+ // order is already a valid deploy order. If the dependency-derived order
108
+ // disagrees, the boundary split a back-edge — surface it rather than silently
109
+ // reordering (that would break byte-identical output).
110
+ const creationOrder = rawChunks.map(c => c.name);
111
+ const orderMatches = deployOrder.length === creationOrder.length &&
112
+ deployOrder.every((name, i) => name === creationOrder[i]);
113
+ if (!orderMatches) {
114
+ warnings.push({
115
+ type: 'heavy_cross_deps',
116
+ message: 'Chunk deploy order differs from source order; a boundary split a dependency edge',
117
+ suggestedAction: 'Adjust the boundary/maxChunkSize so dependent changes stay contiguous',
118
+ });
119
+ }
120
+ const chunkByName = new Map(rawChunks.map(c => [c.name, c.members]));
121
+ const chunks = creationOrder.map(name => {
122
+ const members = chunkByName.get(name);
123
+ return {
124
+ name,
125
+ deploy: members,
126
+ verify: members,
127
+ revert: [...members].reverse(),
128
+ dependencies: [...(chunkDeps.get(name) ?? new Set())].sort(),
129
+ };
130
+ });
131
+ return {
132
+ chunks,
133
+ deployOrder: creationOrder,
134
+ warnings,
135
+ sourceChanges: planResult.data.changes,
136
+ project: planResult.data.package,
137
+ };
138
+ }
139
+ /**
140
+ * Assemble the merged SQL for one chunk in the given direction by concatenating
141
+ * its member script files — the same file-concatenation `resolveWithPlan` uses,
142
+ * just scoped to the chunk. Members are already correctly ordered per direction.
143
+ */
144
+ function assembleChunkSql(moduleDir, chunk, scriptType = 'deploy') {
145
+ const members = scriptType === 'revert' ? chunk.revert : scriptType === 'verify' ? chunk.verify : chunk.deploy;
146
+ const parts = [];
147
+ for (const change of members) {
148
+ parts.push((0, fs_1.readFileSync)((0, path_1.join)(moduleDir, scriptType, `${change}.sql`), 'utf-8'));
149
+ }
150
+ return parts.join('\n');
151
+ }
152
+ /**
153
+ * Byte-identical gate: assembling all chunks in deploy order must reproduce the
154
+ * module's original resolved output in every direction the engine cares about
155
+ * (deploy topological, verify in deploy order, revert reversed). This is the
156
+ * invariant that makes any granularity-dial position safe.
157
+ */
158
+ function verifyRebundleInvariant(moduleDir, result) {
159
+ const mismatches = [];
160
+ const assembled = (scriptType) => {
161
+ const chunks = scriptType === 'revert' ? [...result.chunks].reverse() : result.chunks;
162
+ return chunks.map(chunk => assembleChunkSql(moduleDir, chunk, scriptType)).join('\n');
163
+ };
164
+ // deploy / revert have canonical resolvers; verify concatenates in deploy order.
165
+ const originalVerify = (0, parser_1.getChanges)((0, path_1.join)(moduleDir, 'pgpm.plan'))
166
+ .map(change => (0, fs_1.readFileSync)((0, path_1.join)(moduleDir, 'verify', `${change}.sql`), 'utf-8'))
167
+ .join('\n');
168
+ if (assembled('deploy') !== (0, resolve_1.resolveWithPlan)(moduleDir, 'deploy'))
169
+ mismatches.push('deploy');
170
+ if (assembled('revert') !== (0, resolve_1.resolveWithPlan)(moduleDir, 'revert'))
171
+ mismatches.push('revert');
172
+ if (assembled('verify') !== originalVerify)
173
+ mismatches.push('verify');
174
+ return { ok: mismatches.length === 0, mismatches };
175
+ }