@pgpmjs/core 7.0.0 → 7.1.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.
- package/esm/index.js +2 -0
- package/esm/packaging/package.js +39 -23
- package/esm/rebundle/index.js +4 -0
- package/esm/rebundle/module.js +111 -0
- package/esm/rebundle/rebundle.js +170 -0
- package/esm/rebundle/types.js +1 -0
- package/esm/rebundle/workspace.js +170 -0
- package/esm/workspace/minimal.js +73 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/package.json +2 -2
- package/packaging/package.d.ts +23 -1
- package/packaging/package.js +41 -24
- package/rebundle/index.d.ts +4 -0
- package/rebundle/index.js +20 -0
- package/rebundle/module.d.ts +23 -0
- package/rebundle/module.js +116 -0
- package/rebundle/rebundle.d.ts +26 -0
- package/rebundle/rebundle.js +175 -0
- package/rebundle/types.d.ts +185 -0
- package/rebundle/types.js +2 -0
- package/rebundle/workspace.d.ts +15 -0
- package/rebundle/workspace.js +173 -0
- package/workspace/minimal.d.ts +64 -0
- package/workspace/minimal.js +82 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/core",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PGPM Package and Migration Tools",
|
|
6
6
|
"main": "index.js",
|
|
@@ -64,5 +64,5 @@
|
|
|
64
64
|
"pgsql-parser": "^18.1.1",
|
|
65
65
|
"yanse": "^0.2.1"
|
|
66
66
|
},
|
|
67
|
-
"gitHead": "
|
|
67
|
+
"gitHead": "370a4754d9ad8caaa1446ff2337a7fbcd6ea75a5"
|
|
68
68
|
}
|
package/packaging/package.d.ts
CHANGED
|
@@ -10,7 +10,29 @@ interface WritePackageOptions extends PackageModuleOptions {
|
|
|
10
10
|
version: string;
|
|
11
11
|
packageDir: string;
|
|
12
12
|
}
|
|
13
|
-
export
|
|
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;
|
package/packaging/package.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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");
|
|
@@ -19,8 +19,8 @@ const cleanTree = (tree) => {
|
|
|
19
19
|
});
|
|
20
20
|
};
|
|
21
21
|
exports.cleanTree = cleanTree;
|
|
22
|
-
const filterStatements = (stmts,
|
|
23
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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("../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("../files/plan/parser");
|
|
9
|
+
const resolve_1 = require("../resolution/resolve");
|
|
10
|
+
const slice_1 = require("../slice/slice");
|
|
11
|
+
const parser_2 = require("../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
|
+
}
|