@pgpmjs/transform 0.18.0 → 0.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.
- package/bundle-driver.d.ts +1 -1
- package/catalog-check.d.ts +47 -0
- package/catalog-check.js +130 -0
- package/constraint-names.d.ts +36 -0
- package/constraint-names.js +75 -0
- package/esm/catalog-check.js +124 -0
- package/esm/constraint-names.js +71 -0
- package/esm/fixture-closure.js +1 -1
- package/esm/granularity-driver.js +84 -21
- package/esm/index.js +13 -6
- package/esm/module-emit.js +135 -0
- package/esm/module-source.js +54 -0
- package/esm/partition.js +89 -0
- package/esm/restructure.js +67 -0
- package/esm/semantic-diff-driver.js +124 -19
- package/esm/sub-object.js +80 -0
- package/fixture-closure.d.ts +1 -1
- package/fixture-closure.js +1 -1
- package/granularity-driver.d.ts +57 -0
- package/granularity-driver.js +86 -21
- package/index.d.ts +23 -10
- package/index.js +43 -16
- package/module-emit.d.ts +65 -0
- package/module-emit.js +175 -0
- package/module-source.d.ts +33 -0
- package/module-source.js +92 -0
- package/package.json +8 -7
- package/partition.d.ts +32 -0
- package/partition.js +127 -0
- package/restructure.d.ts +45 -0
- package/restructure.js +72 -0
- package/semantic-diff-driver.d.ts +3 -1
- package/semantic-diff-driver.js +124 -19
- package/sub-object.d.ts +31 -0
- package/sub-object.js +84 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** One change of a loaded module: headerless, unwrapped deploy SQL. */
|
|
2
|
+
export interface ModuleSourceChange {
|
|
3
|
+
/** Change name (plan token). */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Change names this change requires, as recorded in the plan. */
|
|
6
|
+
dependencies: string[];
|
|
7
|
+
/** Deploy SQL with the pgpm header and BEGIN/COMMIT wrapper stripped. */
|
|
8
|
+
deploy: string;
|
|
9
|
+
}
|
|
10
|
+
/** A pgpm module loaded from disk, flattened in plan order. */
|
|
11
|
+
export interface ModuleSource {
|
|
12
|
+
/** Module (project) name from pgpm.plan. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Absolute module directory. */
|
|
15
|
+
modulePath: string;
|
|
16
|
+
/** Changes in plan order. */
|
|
17
|
+
changes: ModuleSourceChange[];
|
|
18
|
+
/** Non-fatal notes (missing deploy scripts, etc.). */
|
|
19
|
+
warnings: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Strip the transaction wrapper some modules carry inside their scripts
|
|
23
|
+
* (standalone `BEGIN;` / `COMMIT;` / `ROLLBACK;` lines). The caller owns
|
|
24
|
+
* transaction control.
|
|
25
|
+
*/
|
|
26
|
+
export declare const stripTransactionWrapper: (sql: string) => string;
|
|
27
|
+
/**
|
|
28
|
+
* Load a pgpm module from disk: parse pgpm.plan and read each change's
|
|
29
|
+
* deploy script, stripping pgpm headers and transaction wrappers so the
|
|
30
|
+
* result can be flattened straight into the dials pipeline
|
|
31
|
+
* (`restructureChanges` / `partitionUnits`).
|
|
32
|
+
*/
|
|
33
|
+
export declare const loadModuleSource: (moduleDir: string) => ModuleSource;
|
package/module-source.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadModuleSource = exports.stripTransactionWrapper = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* Module ingestion for the dials pipeline: load an on-disk pgpm module into
|
|
39
|
+
* the flattened, classified change list the transform/partition drivers
|
|
40
|
+
* consume. `pgpm transform` uses this today; a future `pgpm import` (dumps)
|
|
41
|
+
* and `pgpm diff` are expected to reuse the same seam.
|
|
42
|
+
*/
|
|
43
|
+
const ast_1 = require("@pgpmjs/ast");
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const TX_LINE = /^\s*(BEGIN|COMMIT|ROLLBACK)\s*;\s*$/i;
|
|
46
|
+
/**
|
|
47
|
+
* Strip the transaction wrapper some modules carry inside their scripts
|
|
48
|
+
* (standalone `BEGIN;` / `COMMIT;` / `ROLLBACK;` lines). The caller owns
|
|
49
|
+
* transaction control.
|
|
50
|
+
*/
|
|
51
|
+
const stripTransactionWrapper = (sql) => sql
|
|
52
|
+
.split('\n')
|
|
53
|
+
.filter(line => !TX_LINE.test(line))
|
|
54
|
+
.join('\n')
|
|
55
|
+
.trim();
|
|
56
|
+
exports.stripTransactionWrapper = stripTransactionWrapper;
|
|
57
|
+
/**
|
|
58
|
+
* Load a pgpm module from disk: parse pgpm.plan and read each change's
|
|
59
|
+
* deploy script, stripping pgpm headers and transaction wrappers so the
|
|
60
|
+
* result can be flattened straight into the dials pipeline
|
|
61
|
+
* (`restructureChanges` / `partitionUnits`).
|
|
62
|
+
*/
|
|
63
|
+
const loadModuleSource = (moduleDir) => {
|
|
64
|
+
const modulePath = path.resolve(moduleDir);
|
|
65
|
+
const planResult = (0, ast_1.parsePlanFile)(path.join(modulePath, 'pgpm.plan'));
|
|
66
|
+
if (!planResult.data) {
|
|
67
|
+
const reasons = planResult.errors.map(e => e.message).join(', ');
|
|
68
|
+
throw new Error(`Failed to parse pgpm.plan in ${modulePath}: ${reasons}`);
|
|
69
|
+
}
|
|
70
|
+
const warnings = [];
|
|
71
|
+
const changes = [];
|
|
72
|
+
for (const change of planResult.data.changes) {
|
|
73
|
+
const raw = (0, ast_1.readScript)(modulePath, 'deploy', change.name);
|
|
74
|
+
if (!raw) {
|
|
75
|
+
warnings.push(`${change.name}: no deploy script found, skipping`);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const { body } = (0, ast_1.parsePgpmHeader)(raw);
|
|
79
|
+
changes.push({
|
|
80
|
+
name: change.name,
|
|
81
|
+
dependencies: change.dependencies ?? [],
|
|
82
|
+
deploy: (0, exports.stripTransactionWrapper)(body)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
name: planResult.data.package,
|
|
87
|
+
modulePath,
|
|
88
|
+
changes,
|
|
89
|
+
warnings
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
exports.loadModuleSource = loadModuleSource;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/transform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "Change-aware SQL transformation for PGPM — superset of @pgsql/transform plus change categorization, fixture closure, and bundle transpile/apply drivers",
|
|
6
6
|
"main": "index.js",
|
|
@@ -39,14 +39,15 @@
|
|
|
39
39
|
"plpgsql"
|
|
40
40
|
],
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@pgpmjs/bundle": "^0.13.
|
|
42
|
+
"@pgpmjs/bundle": "^0.13.3",
|
|
43
43
|
"makage": "^0.3.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@pgpmjs/
|
|
47
|
-
"@
|
|
48
|
-
"@pgsql/
|
|
49
|
-
"
|
|
46
|
+
"@pgpmjs/ast": "^0.10.3",
|
|
47
|
+
"@pgpmjs/naming-spec": "^0.2.0",
|
|
48
|
+
"@pgsql/scripts": "^18.3.1",
|
|
49
|
+
"@pgsql/transform": "^18.14.1",
|
|
50
|
+
"plpgsql-parser": "^18.5.1"
|
|
50
51
|
},
|
|
51
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "e6a7a57f88c9deb2a4acecaba2451320d1be8203"
|
|
52
53
|
}
|
package/partition.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partition dial wiring shared by `pgpm transform --partition` and
|
|
3
|
+
* `pgpm export --partition`: parse a partition config file, run
|
|
4
|
+
* `partitionUnits` (`@pgpmjs/transform`), and lift the emitted packages back
|
|
5
|
+
* onto the PgpmRow seam with generated revert/verify per change.
|
|
6
|
+
*/
|
|
7
|
+
import { PgpmRow } from '@pgpmjs/ast';
|
|
8
|
+
import { PartitionConfig } from './partition-driver';
|
|
9
|
+
export type { PartitionConfig } from './partition-driver';
|
|
10
|
+
/**
|
|
11
|
+
* Parse and validate a partition config file (JSON matching
|
|
12
|
+
* `PartitionConfig`: rules, defaultPackage, optional style/splitRiders).
|
|
13
|
+
* Throws with a message naming the offending field.
|
|
14
|
+
*/
|
|
15
|
+
export declare const parsePartitionConfig: (filePath: string) => PartitionConfig;
|
|
16
|
+
/** One partitioned package on the PgpmRow seam, ready to write to disk. */
|
|
17
|
+
export interface PartitionedPackageRows {
|
|
18
|
+
name: string;
|
|
19
|
+
/** Package names this package requires (cross-package edges). */
|
|
20
|
+
requires: string[];
|
|
21
|
+
rows: PgpmRow[];
|
|
22
|
+
}
|
|
23
|
+
export interface PartitionExportRowsResult {
|
|
24
|
+
packages: PartitionedPackageRows[];
|
|
25
|
+
warnings: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Partition changes into packages and generate revert/verify for every
|
|
29
|
+
* emitted change. Cross-package dependencies keep the `<pkg>:<path>`
|
|
30
|
+
* convention in `deps`. May throw `PartitionCycleError`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const partitionExportRows: (rows: PgpmRow[], config: PartitionConfig) => Promise<PartitionExportRowsResult>;
|
package/partition.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.partitionExportRows = exports.parsePartitionConfig = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
39
|
+
const partition_driver_1 = require("./partition-driver");
|
|
40
|
+
const regen_1 = require("./regen");
|
|
41
|
+
const PATH_STYLES = ['directory', 'flat'];
|
|
42
|
+
/**
|
|
43
|
+
* Parse and validate a partition config file (JSON matching
|
|
44
|
+
* `PartitionConfig`: rules, defaultPackage, optional style/splitRiders).
|
|
45
|
+
* Throws with a message naming the offending field.
|
|
46
|
+
*/
|
|
47
|
+
const parsePartitionConfig = (filePath) => {
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = fs.readFileSync(filePath, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error(`Partition config not found: ${filePath}`);
|
|
54
|
+
}
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(raw);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new Error(`Partition config is not valid JSON (${filePath}): ${err?.message ?? err}`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
63
|
+
throw new Error(`Partition config must be a JSON object (${filePath})`);
|
|
64
|
+
}
|
|
65
|
+
if (typeof parsed.defaultPackage !== 'string' || !parsed.defaultPackage) {
|
|
66
|
+
throw new Error('Partition config: "defaultPackage" must be a non-empty string');
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(parsed.rules)) {
|
|
69
|
+
throw new Error('Partition config: "rules" must be an array');
|
|
70
|
+
}
|
|
71
|
+
parsed.rules.forEach((rule, i) => {
|
|
72
|
+
if (typeof rule !== 'object' || rule === null) {
|
|
73
|
+
throw new Error(`Partition config: rules[${i}] must be an object`);
|
|
74
|
+
}
|
|
75
|
+
if (typeof rule.package !== 'string' || !rule.package) {
|
|
76
|
+
throw new Error(`Partition config: rules[${i}].package must be a non-empty string`);
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(rule.select) || rule.select.length === 0) {
|
|
79
|
+
throw new Error(`Partition config: rules[${i}].select must be a non-empty array of selectors`);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
if (parsed.style !== undefined && !PATH_STYLES.includes(parsed.style)) {
|
|
83
|
+
throw new Error(`Partition config: "style" must be one of: ${PATH_STYLES.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
if (parsed.splitRiders !== undefined && !Array.isArray(parsed.splitRiders)) {
|
|
86
|
+
throw new Error('Partition config: "splitRiders" must be an array of statement kinds');
|
|
87
|
+
}
|
|
88
|
+
return parsed;
|
|
89
|
+
};
|
|
90
|
+
exports.parsePartitionConfig = parsePartitionConfig;
|
|
91
|
+
/**
|
|
92
|
+
* Partition changes into packages and generate revert/verify for every
|
|
93
|
+
* emitted change. Cross-package dependencies keep the `<pkg>:<path>`
|
|
94
|
+
* convention in `deps`. May throw `PartitionCycleError`.
|
|
95
|
+
*/
|
|
96
|
+
const partitionExportRows = async (rows, config) => {
|
|
97
|
+
await (0, plpgsql_parser_1.loadModule)();
|
|
98
|
+
const input = rows.map(row => ({
|
|
99
|
+
name: row.deploy,
|
|
100
|
+
dependencies: row.deps ?? [],
|
|
101
|
+
deploy: row.content
|
|
102
|
+
}));
|
|
103
|
+
const { packages, warnings } = (0, partition_driver_1.partitionUnits)(input, config);
|
|
104
|
+
const result = packages.map(pkg => ({
|
|
105
|
+
name: pkg.name,
|
|
106
|
+
requires: pkg.requires,
|
|
107
|
+
rows: pkg.changes.map(change => {
|
|
108
|
+
const scripts = (0, regen_1.regenerateScripts)(change.deploy);
|
|
109
|
+
for (const warning of scripts.revert.warnings) {
|
|
110
|
+
warnings.push(`${pkg.name}:${change.name}: ${warning}`);
|
|
111
|
+
}
|
|
112
|
+
for (const warning of scripts.verify.warnings) {
|
|
113
|
+
warnings.push(`${pkg.name}:${change.name}: ${warning}`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
name: change.name,
|
|
117
|
+
deploy: change.name,
|
|
118
|
+
deps: change.dependencies,
|
|
119
|
+
content: change.deploy,
|
|
120
|
+
revert: scripts.revert.sql,
|
|
121
|
+
verify: scripts.verify.sql
|
|
122
|
+
};
|
|
123
|
+
})
|
|
124
|
+
}));
|
|
125
|
+
return { packages: result, warnings };
|
|
126
|
+
};
|
|
127
|
+
exports.partitionExportRows = partitionExportRows;
|
package/restructure.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Granularity dial for `pgpm export`: route exported sql_actions rows through
|
|
3
|
+
* the dials pipeline (`restructureChanges` in `@pgpmjs/transform`) so change
|
|
4
|
+
* paths are derived from the naming spec (`identityOf` + `pathFor`) and
|
|
5
|
+
* `requires` come from the statement dependency graph instead of the
|
|
6
|
+
* hand-chained deps recorded at action time.
|
|
7
|
+
*/
|
|
8
|
+
import { PgpmRow } from '@pgpmjs/ast';
|
|
9
|
+
import { PathStyle } from '@pgpmjs/naming-spec';
|
|
10
|
+
import { Granularity } from '@pgsql/transform';
|
|
11
|
+
import { ChangeGranularity } from './granularity-driver';
|
|
12
|
+
export type ExportGranularity = Granularity;
|
|
13
|
+
export declare const EXPORT_GRANULARITIES: readonly ExportGranularity[];
|
|
14
|
+
export declare const isExportGranularity: (value: unknown) => value is ExportGranularity;
|
|
15
|
+
export interface RestructureExportRowsResult {
|
|
16
|
+
rows: PgpmRow[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface RestructureExportRowsOptions {
|
|
20
|
+
/** Naming-spec rendering style for derived paths (default `directory`). */
|
|
21
|
+
naming?: PathStyle;
|
|
22
|
+
/**
|
|
23
|
+
* Change-level distribution (default `object`): with `alteration`, each
|
|
24
|
+
* `ADD COLUMN` / `ADD CONSTRAINT` becomes its own change. See
|
|
25
|
+
* {@link ChangeGranularity}.
|
|
26
|
+
*/
|
|
27
|
+
changeGranularity?: ChangeGranularity;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Restructure exported migration rows to the target granularity.
|
|
31
|
+
*
|
|
32
|
+
* The rows' deploy SQL is flattened in export order, restructured, and
|
|
33
|
+
* re-sliced into per-object changes whose paths come from the naming spec and
|
|
34
|
+
* whose deps come from the statement graph. When two changes derive the same
|
|
35
|
+
* path (the same object altered again), later occurrences get the spec's
|
|
36
|
+
* alteration convention via `alterationPathFor` (monotonic per-parent
|
|
37
|
+
* counter), with the parent added to their deps.
|
|
38
|
+
*
|
|
39
|
+
* Revert/verify scripts are not carried through from the original rows (the
|
|
40
|
+
* restructured grouping no longer matches them one-to-one); instead each
|
|
41
|
+
* change gets scripts generated from its own statements via
|
|
42
|
+
* `revertFor`/`verifyFor` (drops in reverse topological order, one existence
|
|
43
|
+
* check per created object).
|
|
44
|
+
*/
|
|
45
|
+
export declare const restructureExportRows: (rows: PgpmRow[], granularity: ExportGranularity, options?: RestructureExportRowsOptions) => Promise<RestructureExportRowsResult>;
|
package/restructure.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.restructureExportRows = exports.isExportGranularity = exports.EXPORT_GRANULARITIES = void 0;
|
|
4
|
+
const naming_spec_1 = require("@pgpmjs/naming-spec");
|
|
5
|
+
const transform_1 = require("@pgsql/transform");
|
|
6
|
+
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
7
|
+
const granularity_driver_1 = require("./granularity-driver");
|
|
8
|
+
exports.EXPORT_GRANULARITIES = ['atomic', 'object', 'consolidated'];
|
|
9
|
+
const isExportGranularity = (value) => typeof value === 'string' && exports.EXPORT_GRANULARITIES.includes(value);
|
|
10
|
+
exports.isExportGranularity = isExportGranularity;
|
|
11
|
+
/**
|
|
12
|
+
* Restructure exported migration rows to the target granularity.
|
|
13
|
+
*
|
|
14
|
+
* The rows' deploy SQL is flattened in export order, restructured, and
|
|
15
|
+
* re-sliced into per-object changes whose paths come from the naming spec and
|
|
16
|
+
* whose deps come from the statement graph. When two changes derive the same
|
|
17
|
+
* path (the same object altered again), later occurrences get the spec's
|
|
18
|
+
* alteration convention via `alterationPathFor` (monotonic per-parent
|
|
19
|
+
* counter), with the parent added to their deps.
|
|
20
|
+
*
|
|
21
|
+
* Revert/verify scripts are not carried through from the original rows (the
|
|
22
|
+
* restructured grouping no longer matches them one-to-one); instead each
|
|
23
|
+
* change gets scripts generated from its own statements via
|
|
24
|
+
* `revertFor`/`verifyFor` (drops in reverse topological order, one existence
|
|
25
|
+
* check per created object).
|
|
26
|
+
*/
|
|
27
|
+
const restructureExportRows = async (rows, granularity, options = {}) => {
|
|
28
|
+
await (0, plpgsql_parser_1.loadModule)();
|
|
29
|
+
const style = options.naming ?? 'directory';
|
|
30
|
+
const changeName = (facts) => {
|
|
31
|
+
const identity = (0, transform_1.identityOf)(facts);
|
|
32
|
+
if (identity)
|
|
33
|
+
return (0, naming_spec_1.pathFor)(identity, { style });
|
|
34
|
+
return 'misc/statements';
|
|
35
|
+
};
|
|
36
|
+
const { changes, warnings } = (0, granularity_driver_1.restructureChanges)(rows.map(row => ({
|
|
37
|
+
name: row.deploy,
|
|
38
|
+
dependencies: row.deps ?? [],
|
|
39
|
+
deploy: row.content
|
|
40
|
+
})), {
|
|
41
|
+
granularity,
|
|
42
|
+
changeGranularity: options.changeGranularity,
|
|
43
|
+
changeName,
|
|
44
|
+
subObjectName: identity => (0, naming_spec_1.pathFor)(identity, { style })
|
|
45
|
+
});
|
|
46
|
+
const counters = new Map();
|
|
47
|
+
const taken = new Set();
|
|
48
|
+
const restructured = changes.map(change => {
|
|
49
|
+
let deploy = change.name;
|
|
50
|
+
const deps = [...change.dependencies];
|
|
51
|
+
if (taken.has(deploy)) {
|
|
52
|
+
let n = (counters.get(change.name) ?? 0) + 1;
|
|
53
|
+
while (taken.has((0, naming_spec_1.alterationPathFor)(change.name, n)))
|
|
54
|
+
n++;
|
|
55
|
+
counters.set(change.name, n);
|
|
56
|
+
deploy = (0, naming_spec_1.alterationPathFor)(change.name, n);
|
|
57
|
+
if (!deps.includes(change.name))
|
|
58
|
+
deps.push(change.name);
|
|
59
|
+
}
|
|
60
|
+
taken.add(deploy);
|
|
61
|
+
return {
|
|
62
|
+
name: deploy,
|
|
63
|
+
deploy,
|
|
64
|
+
deps,
|
|
65
|
+
content: change.deploy,
|
|
66
|
+
revert: change.revert,
|
|
67
|
+
verify: change.verify
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
return { rows: restructured, warnings };
|
|
71
|
+
};
|
|
72
|
+
exports.restructureExportRows = restructureExportRows;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import { PathStyle } from '@pgpmjs/naming-spec';
|
|
30
30
|
import type { ObjectIdentity } from '@pgsql/transform';
|
|
31
31
|
import { Granularity } from '@pgsql/transform';
|
|
32
|
-
import { GranularityChange } from './granularity-driver';
|
|
32
|
+
import { ChangeGranularity, GranularityChange } from './granularity-driver';
|
|
33
33
|
/** How one object differs between the two sides. */
|
|
34
34
|
export type ObjectDelta = 'added' | 'removed' | 'modified';
|
|
35
35
|
export interface SemanticObjectDiff {
|
|
@@ -45,6 +45,8 @@ export interface SemanticObjectDiff {
|
|
|
45
45
|
export interface SemanticDiffOptions {
|
|
46
46
|
/** Granularity for the emitted CREATE/ALTER changes (default `object`). */
|
|
47
47
|
granularity?: Granularity;
|
|
48
|
+
/** Change-level distribution for emitted changes (default `object`). */
|
|
49
|
+
changeGranularity?: ChangeGranularity;
|
|
48
50
|
/** Naming-spec rendering style (default `directory`). */
|
|
49
51
|
style?: PathStyle;
|
|
50
52
|
}
|
package/semantic-diff-driver.js
CHANGED
|
@@ -34,6 +34,7 @@ const naming_spec_1 = require("@pgpmjs/naming-spec");
|
|
|
34
34
|
const scripts_1 = require("@pgsql/scripts");
|
|
35
35
|
const transform_1 = require("@pgsql/transform");
|
|
36
36
|
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
37
|
+
const constraint_names_1 = require("./constraint-names");
|
|
37
38
|
const granularity_driver_1 = require("./granularity-driver");
|
|
38
39
|
const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table ?? ''}\u0000${id.name}`;
|
|
39
40
|
/**
|
|
@@ -103,7 +104,7 @@ function groupingIdentity(f) {
|
|
|
103
104
|
const identity = (0, transform_1.identityOf)(f);
|
|
104
105
|
if (!identity)
|
|
105
106
|
return null;
|
|
106
|
-
if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'rls_enable') &&
|
|
107
|
+
if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'fk_constraint' || f.kind === 'rls_enable') &&
|
|
107
108
|
identity.kind !== 'table') {
|
|
108
109
|
return { kind: 'table', schema: identity.schema, name: identity.table ?? identity.name };
|
|
109
110
|
}
|
|
@@ -116,21 +117,61 @@ function fingerprint(text) {
|
|
|
116
117
|
}
|
|
117
118
|
/** Multiset fingerprint of a whole unit. */
|
|
118
119
|
const unitFingerprint = (unit) => unit.texts.map(fingerprint).sort().join('\n');
|
|
120
|
+
/** JSON with recursively sorted object keys — an insertion-order-proof fingerprint. */
|
|
121
|
+
function stablePrint(node) {
|
|
122
|
+
if (Array.isArray(node))
|
|
123
|
+
return `[${node.map(stablePrint).join(',')}]`;
|
|
124
|
+
if (node && typeof node === 'object') {
|
|
125
|
+
const entries = Object.entries(node)
|
|
126
|
+
.filter(([, v]) => v !== undefined)
|
|
127
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
128
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${stablePrint(v)}`);
|
|
129
|
+
return `{${entries.join(',')}}`;
|
|
130
|
+
}
|
|
131
|
+
return JSON.stringify(node);
|
|
132
|
+
}
|
|
133
|
+
/** Constraint kinds that can move between column, table-elt, and ALTER form. */
|
|
134
|
+
const RELOCATABLE = new Set(['CONSTR_PRIMARY', 'CONSTR_UNIQUE', 'CONSTR_FOREIGN', 'CONSTR_CHECK']);
|
|
119
135
|
/**
|
|
120
136
|
* Fold a table unit's statements (CREATE TABLE plus any ALTER TABLE
|
|
121
137
|
* ADD COLUMN / ADD CONSTRAINT) into an effective shape, so atomic and
|
|
122
138
|
* consolidated authorships of the same table compare equal.
|
|
139
|
+
*
|
|
140
|
+
* Constraint placement is representation, not semantics: `id bigint PRIMARY
|
|
141
|
+
* KEY`, a `PRIMARY KEY (id)` table elt, and `ALTER TABLE .. ADD CONSTRAINT
|
|
142
|
+
* t_pkey PRIMARY KEY (id)` all catalog identically. Relocatable constraints
|
|
143
|
+
* (PK/UNIQUE/FK/CHECK) are therefore lifted out of columns and ALTER
|
|
144
|
+
* commands into one canonical table-level set — keyed with the Postgres
|
|
145
|
+
* default name when unnamed, columns filled in from the owning column when
|
|
146
|
+
* column-attached — and PK columns gain the NOT NULL the catalog implies.
|
|
123
147
|
*/
|
|
124
148
|
function tableShape(unit) {
|
|
125
149
|
const shape = { relation: null, columns: new Map(), extras: new Map() };
|
|
150
|
+
const table = unit.identity.name;
|
|
151
|
+
const rawColumns = [];
|
|
152
|
+
const constraints = [];
|
|
153
|
+
const pkColumns = new Set();
|
|
126
154
|
const addColumn = (node) => {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
155
|
+
const colname = node.colname ?? '';
|
|
156
|
+
const attached = node.constraints ?? [];
|
|
157
|
+
const residual = [];
|
|
158
|
+
for (const item of attached) {
|
|
159
|
+
const c = item.Constraint;
|
|
160
|
+
if (c && RELOCATABLE.has(c.contype ?? '')) {
|
|
161
|
+
constraints.push({ node: c, column: colname });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
residual.push(item);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
rawColumns.push({ colname, def: { ...node, constraints: residual } });
|
|
168
|
+
};
|
|
169
|
+
const addConstraint = (node, column) => {
|
|
170
|
+
constraints.push({ node: node, column });
|
|
130
171
|
};
|
|
131
172
|
const addExtra = (node) => {
|
|
132
173
|
const clean = (0, transform_1.cleanTree)(node);
|
|
133
|
-
shape.extras.set(
|
|
174
|
+
shape.extras.set(stablePrint(clean), clean);
|
|
134
175
|
};
|
|
135
176
|
for (const text of unit.texts) {
|
|
136
177
|
for (const raw of (0, plpgsql_parser_1.parseSync)(text).sql.stmts) {
|
|
@@ -141,6 +182,9 @@ function tableShape(unit) {
|
|
|
141
182
|
for (const elt of elts) {
|
|
142
183
|
if (elt.ColumnDef)
|
|
143
184
|
addColumn(elt.ColumnDef);
|
|
185
|
+
else if (elt.Constraint && RELOCATABLE.has(elt.Constraint.contype ?? '')) {
|
|
186
|
+
addConstraint(elt.Constraint);
|
|
187
|
+
}
|
|
144
188
|
else
|
|
145
189
|
addExtra(elt);
|
|
146
190
|
}
|
|
@@ -156,6 +200,11 @@ function tableShape(unit) {
|
|
|
156
200
|
if (at.subtype === 'AT_AddColumn' && def?.ColumnDef) {
|
|
157
201
|
addColumn(def.ColumnDef);
|
|
158
202
|
}
|
|
203
|
+
else if (at.subtype === 'AT_AddConstraint' &&
|
|
204
|
+
def?.Constraint &&
|
|
205
|
+
RELOCATABLE.has(def.Constraint.contype ?? '')) {
|
|
206
|
+
addConstraint(def.Constraint);
|
|
207
|
+
}
|
|
159
208
|
else {
|
|
160
209
|
addExtra({ AlterTableCmd: at });
|
|
161
210
|
}
|
|
@@ -166,6 +215,44 @@ function tableShape(unit) {
|
|
|
166
215
|
}
|
|
167
216
|
}
|
|
168
217
|
}
|
|
218
|
+
for (const { node, column } of constraints) {
|
|
219
|
+
const canonical = { ...node };
|
|
220
|
+
canonical.conname = node.conname ?? (0, constraint_names_1.defaultConstraintName)(table, node, column) ?? undefined;
|
|
221
|
+
if (column && (node.keys ?? []).length === 0 && node.contype !== 'CONSTR_FOREIGN' && node.contype !== 'CONSTR_CHECK') {
|
|
222
|
+
canonical.keys = [{ String: { sval: column } }];
|
|
223
|
+
}
|
|
224
|
+
if (column && node.contype === 'CONSTR_FOREIGN' && (node.fk_attrs ?? []).length === 0) {
|
|
225
|
+
canonical.fk_attrs = [{ String: { sval: column } }];
|
|
226
|
+
}
|
|
227
|
+
if (node.contype === 'CONSTR_PRIMARY') {
|
|
228
|
+
const keyed = canonical.keys ?? [];
|
|
229
|
+
for (const k of keyed)
|
|
230
|
+
if (k.String?.sval)
|
|
231
|
+
pkColumns.add(k.String.sval);
|
|
232
|
+
}
|
|
233
|
+
addExtra({ Constraint: canonical });
|
|
234
|
+
}
|
|
235
|
+
for (const { colname, def } of rawColumns) {
|
|
236
|
+
const residual = (def.constraints ?? []).map(c => {
|
|
237
|
+
const constraint = c.Constraint;
|
|
238
|
+
// NOT NULL parses with parser-version-dependent extras (is_enforced,
|
|
239
|
+
// initially_valid); reduce to its semantic core so an authored NOT NULL
|
|
240
|
+
// equals the one a primary key implies.
|
|
241
|
+
if (constraint?.contype === 'CONSTR_NOTNULL') {
|
|
242
|
+
return { Constraint: { contype: 'CONSTR_NOTNULL', conname: constraint.conname } };
|
|
243
|
+
}
|
|
244
|
+
return c;
|
|
245
|
+
});
|
|
246
|
+
const hasNotNull = residual.some(c => c.Constraint?.contype === 'CONSTR_NOTNULL');
|
|
247
|
+
if (pkColumns.has(colname) && !hasNotNull) {
|
|
248
|
+
residual.push({ Constraint: { contype: 'CONSTR_NOTNULL' } });
|
|
249
|
+
}
|
|
250
|
+
const sorted = residual
|
|
251
|
+
.map(c => (0, transform_1.cleanTree)(c))
|
|
252
|
+
.sort((x, y) => stablePrint(x).localeCompare(stablePrint(y)));
|
|
253
|
+
const clean = (0, transform_1.cleanTree)({ ...def, constraints: sorted });
|
|
254
|
+
shape.columns.set(colname, { def: clean, print: stablePrint(clean) });
|
|
255
|
+
}
|
|
169
256
|
return shape;
|
|
170
257
|
}
|
|
171
258
|
const deparseStmt = (node) => `${plpgsql_parser_1.Deparser.deparse(node, { pretty: false })};`;
|
|
@@ -214,32 +301,46 @@ function diffTable(from, to, diff, warnings) {
|
|
|
214
301
|
stmts.push(alter({ subtype: 'AT_DropColumn', name }));
|
|
215
302
|
}
|
|
216
303
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
304
|
+
// Self-inverse ALTER TABLE subtypes: a removed extra with one of these
|
|
305
|
+
// undoes with the paired subtype rather than a DROP CONSTRAINT.
|
|
306
|
+
const INVERSE_SUBTYPE = {
|
|
307
|
+
AT_EnableRowSecurity: 'AT_DisableRowSecurity',
|
|
308
|
+
AT_DisableRowSecurity: 'AT_EnableRowSecurity',
|
|
309
|
+
AT_ForceRowSecurity: 'AT_NoForceRowSecurity',
|
|
310
|
+
AT_NoForceRowSecurity: 'AT_ForceRowSecurity'
|
|
311
|
+
};
|
|
312
|
+
// Removed extras drop before added ones: a changed constraint that keeps
|
|
313
|
+
// its name must DROP CONSTRAINT before the replacement ADD CONSTRAINT.
|
|
230
314
|
for (const [print, extra] of from.extras) {
|
|
231
315
|
if (to.extras.has(print))
|
|
232
316
|
continue;
|
|
233
317
|
const conname = extra.Constraint?.conname
|
|
234
318
|
?? extra
|
|
235
319
|
.AlterTableCmd?.def?.Constraint?.conname;
|
|
320
|
+
const subtype = extra.AlterTableCmd?.subtype;
|
|
236
321
|
if (conname) {
|
|
237
322
|
stmts.push(alter({ subtype: 'AT_DropConstraint', name: conname }));
|
|
238
323
|
}
|
|
324
|
+
else if (subtype && INVERSE_SUBTYPE[subtype]) {
|
|
325
|
+
stmts.push(alter({ subtype: INVERSE_SUBTYPE[subtype] }));
|
|
326
|
+
}
|
|
239
327
|
else {
|
|
240
328
|
warnings.push(`${diff.path}: removed table element has no name to drop; emit a manual ALTER`);
|
|
241
329
|
}
|
|
242
330
|
}
|
|
331
|
+
for (const [print, extra] of to.extras) {
|
|
332
|
+
if (!from.extras.has(print)) {
|
|
333
|
+
const at = extra.AlterTableCmd;
|
|
334
|
+
if (at)
|
|
335
|
+
stmts.push(alter(at));
|
|
336
|
+
else if (extra.Constraint) {
|
|
337
|
+
stmts.push(alter({ subtype: 'AT_AddConstraint', def: extra }));
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
warnings.push(`${diff.path}: unrecognized added table element; emit a manual ALTER`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
243
344
|
return stmts;
|
|
244
345
|
}
|
|
245
346
|
/**
|
|
@@ -375,7 +476,11 @@ function diffSchemas(from, to, options = {}) {
|
|
|
375
476
|
}
|
|
376
477
|
let forwardChanges = [];
|
|
377
478
|
if (forwardSql.length > 0) {
|
|
378
|
-
const { changes, warnings: w } = (0, granularity_driver_1.restructureChanges)([{ name: 'delta', dependencies: [], deploy: forwardSql.join('\n\n') }], {
|
|
479
|
+
const { changes, warnings: w } = (0, granularity_driver_1.restructureChanges)([{ name: 'delta', dependencies: [], deploy: forwardSql.join('\n\n') }], {
|
|
480
|
+
granularity: options.granularity ?? 'object',
|
|
481
|
+
changeGranularity: options.changeGranularity,
|
|
482
|
+
subObjectName: identity => (0, naming_spec_1.pathFor)(identity, { style })
|
|
483
|
+
});
|
|
379
484
|
forwardChanges = changes.map(c => ({
|
|
380
485
|
...c,
|
|
381
486
|
revert: inverseOf(c.deploy, c.name),
|