@pgpmjs/transform 0.15.3 → 0.17.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/granularity-driver.js +50 -20
- package/esm/index.js +3 -0
- package/esm/partition-driver.js +363 -0
- package/esm/regen.js +39 -0
- package/esm/semantic-diff-driver.js +366 -0
- package/granularity-driver.d.ts +23 -26
- package/granularity-driver.js +49 -19
- package/index.d.ts +7 -1
- package/index.js +12 -1
- package/package.json +6 -4
- package/partition-driver.d.ts +154 -0
- package/partition-driver.js +368 -0
- package/regen.d.ts +41 -0
- package/regen.js +44 -0
- package/semantic-diff-driver.d.ts +87 -0
- package/semantic-diff-driver.js +370 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partition dial: project one deploy surface into a set of pgpm packages.
|
|
3
|
+
*
|
|
4
|
+
* The object graph (statements classified by `@pgsql/transform`, grouped
|
|
5
|
+
* into identity-keyed units) is the source of truth; which package a unit
|
|
6
|
+
* lives in is a *derived projection* of a declarative partition config.
|
|
7
|
+
* Units can be assigned by schema, by object kind, by explicit cherry-pick
|
|
8
|
+
* (identity or derived path), or dragged in as the dependency closure of a
|
|
9
|
+
* package's seeds. `requires` headers are a mechanical post-pass over the
|
|
10
|
+
* statement graph: intra-package edges become `requires: <path>`,
|
|
11
|
+
* cross-package edges become `requires: <pkg>:<path>` — both rendered
|
|
12
|
+
* through the naming spec, never authored.
|
|
13
|
+
*
|
|
14
|
+
* A partition is rejected (hard error) when packages require each other in
|
|
15
|
+
* a cycle: that is the one unshippable configuration, because no package
|
|
16
|
+
* install order can satisfy it.
|
|
17
|
+
*
|
|
18
|
+
* Like the other drivers in this package the emit shape is structurally
|
|
19
|
+
* typed on the change seam — no dependency on `@pgpmjs/bundle` or
|
|
20
|
+
* `@pgpmjs/core`. Callers materialize packages however they like (write
|
|
21
|
+
* pgpm.plan + deploy trees, feed a bundle, ...).
|
|
22
|
+
*/
|
|
23
|
+
import { PathStyle } from '@pgpmjs/naming-spec';
|
|
24
|
+
import { ObjectIdentity, ObjectIdentityKind, StatementKind } from '@pgsql/transform';
|
|
25
|
+
/** One deployable unit: an object (by identity) and the statements that build it. */
|
|
26
|
+
export interface PartitionUnit {
|
|
27
|
+
/** The object's identity, or `null` for split riders and the residual unit. */
|
|
28
|
+
identity: ObjectIdentity | null;
|
|
29
|
+
/** Statement kind of the unit's primary statement (`grant` for split grants, ...). */
|
|
30
|
+
primaryKind: StatementKind;
|
|
31
|
+
/** Canonical change path derived from the identity via the naming spec. */
|
|
32
|
+
path: string;
|
|
33
|
+
/** Indexes into the classified statement list, in source order. */
|
|
34
|
+
statements: number[];
|
|
35
|
+
/** The unit's deploy SQL (statement text joined in source order). */
|
|
36
|
+
sql: string;
|
|
37
|
+
/**
|
|
38
|
+
* A statement in this unit executes dynamic SQL: its edges are incomplete,
|
|
39
|
+
* so assignments involving it cannot be proven safe by the graph alone.
|
|
40
|
+
*/
|
|
41
|
+
dynamicSql: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Matches units for assignment. Every given field must match (AND); array
|
|
45
|
+
* values match any element (OR). Omitted fields match everything, so
|
|
46
|
+
* `{ schema: 'billing' }` selects a whole schema, `{ kind: 'policy' }`
|
|
47
|
+
* selects every policy, and `{ path: 'schemas/app/tables/users/table' }`
|
|
48
|
+
* cherry-picks one object.
|
|
49
|
+
*/
|
|
50
|
+
export interface UnitSelector {
|
|
51
|
+
kind?: ObjectIdentityKind | ObjectIdentityKind[];
|
|
52
|
+
/**
|
|
53
|
+
* Statement-level kind of the unit's primary statement — selects units
|
|
54
|
+
* that have no object identity, like grants split out via
|
|
55
|
+
* {@link PartitionConfig.splitRiders} (`statementKind: 'grant'`).
|
|
56
|
+
*/
|
|
57
|
+
statementKind?: StatementKind | StatementKind[];
|
|
58
|
+
schema?: string | string[];
|
|
59
|
+
name?: string | string[];
|
|
60
|
+
/** Owning table, for table-scoped kinds (trigger/policy/index/constraint/seed). */
|
|
61
|
+
table?: string | string[];
|
|
62
|
+
/** The derived naming-spec path — exact cherry-pick. */
|
|
63
|
+
path?: string | string[];
|
|
64
|
+
}
|
|
65
|
+
/** Assigns matching units to a package. Rules are tried in order; first match wins. */
|
|
66
|
+
export interface PartitionRule {
|
|
67
|
+
package: string;
|
|
68
|
+
/** A unit matching ANY of these selectors is assigned to `package`. */
|
|
69
|
+
select: UnitSelector[];
|
|
70
|
+
/**
|
|
71
|
+
* Also pull in the transitive dependency closure (hard + fk edges) of the
|
|
72
|
+
* selected units. Only units that would otherwise land in the default
|
|
73
|
+
* package are pulled — a unit another rule claimed stays where it is and
|
|
74
|
+
* becomes an ordinary cross-package dependency.
|
|
75
|
+
*/
|
|
76
|
+
closure?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface PartitionConfig {
|
|
79
|
+
/** Assignment rules, in priority order. */
|
|
80
|
+
rules: PartitionRule[];
|
|
81
|
+
/** Package for units no rule matches. */
|
|
82
|
+
defaultPackage: string;
|
|
83
|
+
/** Naming-spec rendering style for derived paths (default `directory`). */
|
|
84
|
+
style?: PathStyle;
|
|
85
|
+
/**
|
|
86
|
+
* Statement kinds to split out of their host unit into standalone units
|
|
87
|
+
* (e.g. `['grant']` makes every GRANT its own selectable unit named
|
|
88
|
+
* `<hostPath>/grants/<role>`), so security surface can be partitioned
|
|
89
|
+
* independently of the objects it attaches to. Split units depend on
|
|
90
|
+
* their host, so ordering stays correct wherever they are assigned.
|
|
91
|
+
*/
|
|
92
|
+
splitRiders?: StatementKind[];
|
|
93
|
+
}
|
|
94
|
+
/** One emitted change of a partitioned package. */
|
|
95
|
+
export interface PartitionedChange {
|
|
96
|
+
/** Change path (naming spec). */
|
|
97
|
+
name: string;
|
|
98
|
+
/**
|
|
99
|
+
* Same-package dependencies as plain paths, cross-package dependencies as
|
|
100
|
+
* `<pkg>:<path>` — the pgpm cross-package requires convention.
|
|
101
|
+
*/
|
|
102
|
+
dependencies: string[];
|
|
103
|
+
deploy: string;
|
|
104
|
+
}
|
|
105
|
+
export interface PartitionedPackage {
|
|
106
|
+
name: string;
|
|
107
|
+
/** Changes in a valid deploy order (statement-graph topological order). */
|
|
108
|
+
changes: PartitionedChange[];
|
|
109
|
+
/** Package names this package requires (from cross-package edges). */
|
|
110
|
+
requires: string[];
|
|
111
|
+
}
|
|
112
|
+
export interface PartitionUnitsResult {
|
|
113
|
+
packages: PartitionedPackage[];
|
|
114
|
+
/** unit path → package name, for every unit. */
|
|
115
|
+
assignments: Map<string, string>;
|
|
116
|
+
/** Units pulled in by closure rules: path → the rule's package. */
|
|
117
|
+
closureIncluded: Map<string, string>;
|
|
118
|
+
warnings: string[];
|
|
119
|
+
}
|
|
120
|
+
/** A partition whose packages require each other in a cycle — unshippable. */
|
|
121
|
+
export declare class PartitionCycleError extends Error {
|
|
122
|
+
/** The packages forming the cycle. */
|
|
123
|
+
readonly packages: string[];
|
|
124
|
+
/** One offending edge per hop: `fromPkg:fromPath -> toPkg:toPath`. */
|
|
125
|
+
readonly edges: string[];
|
|
126
|
+
constructor(
|
|
127
|
+
/** The packages forming the cycle. */
|
|
128
|
+
packages: string[],
|
|
129
|
+
/** One offending edge per hop: `fromPkg:fromPath -> toPkg:toPath`. */
|
|
130
|
+
edges: string[]);
|
|
131
|
+
}
|
|
132
|
+
/** Path of the residual unit for statements attributable to no object. */
|
|
133
|
+
export declare const RESIDUAL_UNIT_PATH = "misc/statements";
|
|
134
|
+
/** A change's deploy surface going into the partition (same seam as the granularity driver). */
|
|
135
|
+
export interface PartitionInputChange {
|
|
136
|
+
name: string;
|
|
137
|
+
dependencies: string[];
|
|
138
|
+
deploy: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Partition a deploy surface into packages.
|
|
142
|
+
*
|
|
143
|
+
* Accepts either raw SQL or pgpm changes (whose deploy scripts are flattened
|
|
144
|
+
* in the given order — run the granularity dial first if a different unit
|
|
145
|
+
* shape is wanted). Requires `loadModule()` from `plpgsql-parser` first.
|
|
146
|
+
*
|
|
147
|
+
* Assignment: for each unit the first matching rule wins; unmatched units go
|
|
148
|
+
* to `defaultPackage`. Closure rules then pull the transitive dependency
|
|
149
|
+
* closure (hard + fk) of their units out of the default package. Finally the
|
|
150
|
+
* requires post-pass renders every unit dependency through the naming spec —
|
|
151
|
+
* same package as `<path>`, cross-package as `<pkg>:<path>` — and the
|
|
152
|
+
* package-level graph is checked for cycles ({@link PartitionCycleError}).
|
|
153
|
+
*/
|
|
154
|
+
export declare function partitionUnits(input: string | PartitionInputChange[], config: PartitionConfig): PartitionUnitsResult;
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RESIDUAL_UNIT_PATH = exports.PartitionCycleError = void 0;
|
|
4
|
+
exports.partitionUnits = partitionUnits;
|
|
5
|
+
/**
|
|
6
|
+
* Partition dial: project one deploy surface into a set of pgpm packages.
|
|
7
|
+
*
|
|
8
|
+
* The object graph (statements classified by `@pgsql/transform`, grouped
|
|
9
|
+
* into identity-keyed units) is the source of truth; which package a unit
|
|
10
|
+
* lives in is a *derived projection* of a declarative partition config.
|
|
11
|
+
* Units can be assigned by schema, by object kind, by explicit cherry-pick
|
|
12
|
+
* (identity or derived path), or dragged in as the dependency closure of a
|
|
13
|
+
* package's seeds. `requires` headers are a mechanical post-pass over the
|
|
14
|
+
* statement graph: intra-package edges become `requires: <path>`,
|
|
15
|
+
* cross-package edges become `requires: <pkg>:<path>` — both rendered
|
|
16
|
+
* through the naming spec, never authored.
|
|
17
|
+
*
|
|
18
|
+
* A partition is rejected (hard error) when packages require each other in
|
|
19
|
+
* a cycle: that is the one unshippable configuration, because no package
|
|
20
|
+
* install order can satisfy it.
|
|
21
|
+
*
|
|
22
|
+
* Like the other drivers in this package the emit shape is structurally
|
|
23
|
+
* typed on the change seam — no dependency on `@pgpmjs/bundle` or
|
|
24
|
+
* `@pgpmjs/core`. Callers materialize packages however they like (write
|
|
25
|
+
* pgpm.plan + deploy trees, feed a bundle, ...).
|
|
26
|
+
*/
|
|
27
|
+
const naming_spec_1 = require("@pgpmjs/naming-spec");
|
|
28
|
+
const transform_1 = require("@pgsql/transform");
|
|
29
|
+
/** A partition whose packages require each other in a cycle — unshippable. */
|
|
30
|
+
class PartitionCycleError extends Error {
|
|
31
|
+
packages;
|
|
32
|
+
edges;
|
|
33
|
+
constructor(
|
|
34
|
+
/** The packages forming the cycle. */
|
|
35
|
+
packages,
|
|
36
|
+
/** One offending edge per hop: `fromPkg:fromPath -> toPkg:toPath`. */
|
|
37
|
+
edges) {
|
|
38
|
+
super(`cross-package dependency cycle: ${packages.join(' -> ')} -> ${packages[0]}\n` +
|
|
39
|
+
edges.map(e => ` ${e}`).join('\n'));
|
|
40
|
+
this.packages = packages;
|
|
41
|
+
this.edges = edges;
|
|
42
|
+
this.name = 'PartitionCycleError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.PartitionCycleError = PartitionCycleError;
|
|
46
|
+
const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table ?? ''}\u0000${id.name}`;
|
|
47
|
+
const matchField = (value, wanted) => {
|
|
48
|
+
if (wanted === undefined)
|
|
49
|
+
return true;
|
|
50
|
+
return Array.isArray(wanted) ? wanted.includes(value) : wanted === value;
|
|
51
|
+
};
|
|
52
|
+
function matches(unit, selector) {
|
|
53
|
+
if (!matchField(unit.path, selector.path))
|
|
54
|
+
return false;
|
|
55
|
+
if (!matchField(unit.primaryKind, selector.statementKind))
|
|
56
|
+
return false;
|
|
57
|
+
const id = unit.identity;
|
|
58
|
+
if (selector.kind === undefined && selector.schema === undefined &&
|
|
59
|
+
selector.name === undefined && selector.table === undefined) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (!id)
|
|
63
|
+
return false;
|
|
64
|
+
// A schema object's own name counts as its schema, so `{ schema: 'x' }`
|
|
65
|
+
// selects the schema together with everything in it.
|
|
66
|
+
const schema = id.kind === 'schema' ? id.name : id.schema;
|
|
67
|
+
return (matchField(id.kind, selector.kind) &&
|
|
68
|
+
matchField(schema, selector.schema) &&
|
|
69
|
+
matchField(id.name, selector.name) &&
|
|
70
|
+
matchField(id.table, selector.table));
|
|
71
|
+
}
|
|
72
|
+
/** Path of the residual unit for statements attributable to no object. */
|
|
73
|
+
exports.RESIDUAL_UNIT_PATH = 'misc/statements';
|
|
74
|
+
/**
|
|
75
|
+
* Group a classified script into identity-keyed units and compute unit-level
|
|
76
|
+
* dependencies from the statement graph.
|
|
77
|
+
*
|
|
78
|
+
* Statements with no identity of their own (grants, comments) ride with the
|
|
79
|
+
* unit of the object they target — resolved through the statement graph's
|
|
80
|
+
* edges — falling back to the closest preceding statement's unit; lead-in
|
|
81
|
+
* statements with no unit at all land in a residual unit so nothing is
|
|
82
|
+
* silently dropped. Kinds listed in `splitRiders` become standalone units
|
|
83
|
+
* (named `<hostPath>/grants/<role>` for grants, `<hostPath>/<kind>/<n>`
|
|
84
|
+
* otherwise) that depend on their host unit.
|
|
85
|
+
*/
|
|
86
|
+
function buildUnitGraph(sql, style, splitRiders) {
|
|
87
|
+
const facts = (0, transform_1.classifyStatements)(sql);
|
|
88
|
+
const graph = (0, transform_1.buildStatementGraph)(facts);
|
|
89
|
+
const unitOf = new Array(facts.length).fill(-1);
|
|
90
|
+
const units = [];
|
|
91
|
+
const byIdentity = new Map();
|
|
92
|
+
const takenPaths = new Set();
|
|
93
|
+
const newUnit = (identity, primaryKind, path) => {
|
|
94
|
+
units.push({ identity, primaryKind, path, statements: [], sql: '', dynamicSql: false });
|
|
95
|
+
takenPaths.add(path);
|
|
96
|
+
return units.length - 1;
|
|
97
|
+
};
|
|
98
|
+
// Naming-spec path for a unit; ENABLE ROW LEVEL SECURITY uses the spec's
|
|
99
|
+
// dedicated rls template rather than its constraint-shaped identity.
|
|
100
|
+
const unitPath = (identity, kind) => {
|
|
101
|
+
if (kind === 'rls_enable' && identity.table) {
|
|
102
|
+
return `schemas/${identity.schema ?? 'public'}/tables/${identity.table}/policies/enable_row_level_security`;
|
|
103
|
+
}
|
|
104
|
+
return (0, naming_spec_1.pathFor)(identity, { style });
|
|
105
|
+
};
|
|
106
|
+
const unitFor = (identity, kind) => {
|
|
107
|
+
const key = identity ? identityKey(identity) : exports.RESIDUAL_UNIT_PATH;
|
|
108
|
+
let u = byIdentity.get(key);
|
|
109
|
+
if (u === undefined) {
|
|
110
|
+
u = newUnit(identity, kind, identity ? unitPath(identity, kind) : exports.RESIDUAL_UNIT_PATH);
|
|
111
|
+
byIdentity.set(key, u);
|
|
112
|
+
}
|
|
113
|
+
return u;
|
|
114
|
+
};
|
|
115
|
+
// Object-owning units by qualified name, for rider host resolution. Only
|
|
116
|
+
// top-level objects qualify (table-scoped identities share the table's
|
|
117
|
+
// qualified name and must not shadow it).
|
|
118
|
+
const objectUnits = new Map();
|
|
119
|
+
const registerObject = (identity, u) => {
|
|
120
|
+
if (identity.table)
|
|
121
|
+
return;
|
|
122
|
+
objectUnits.set(`${identity.schema ?? ''}.${identity.name}`, u);
|
|
123
|
+
};
|
|
124
|
+
/** Host unit of a rider statement: the unit owning the first object it
|
|
125
|
+
* references, falling back to the preceding statement's unit. */
|
|
126
|
+
const hostOf = (i) => {
|
|
127
|
+
for (const r of facts[i].references) {
|
|
128
|
+
const u = objectUnits.get(`${r.schema ?? ''}.${r.name}`);
|
|
129
|
+
if (u !== undefined)
|
|
130
|
+
return u;
|
|
131
|
+
}
|
|
132
|
+
if (i > 0 && unitOf[i - 1] !== -1)
|
|
133
|
+
return unitOf[i - 1];
|
|
134
|
+
return -1;
|
|
135
|
+
};
|
|
136
|
+
const riderPath = (f, host) => {
|
|
137
|
+
const base = host !== -1 ? units[host].path : exports.RESIDUAL_UNIT_PATH;
|
|
138
|
+
const dir = f.kind === 'grant' ? 'grants' : f.kind;
|
|
139
|
+
const label = f.roles.length > 0 ? f.roles.join('_') : f.kind;
|
|
140
|
+
let path = `${base}/${dir}/${label}`;
|
|
141
|
+
for (let n = 2; takenPaths.has(path); n++)
|
|
142
|
+
path = `${base}/${dir}/${label}-${n}`;
|
|
143
|
+
return path;
|
|
144
|
+
};
|
|
145
|
+
const hostUnit = new Map();
|
|
146
|
+
facts.forEach((f, i) => {
|
|
147
|
+
const identity = (0, transform_1.identityOf)(f);
|
|
148
|
+
if (identity && !splitRiders.has(f.kind)) {
|
|
149
|
+
unitOf[i] = unitFor(identity, f.kind);
|
|
150
|
+
registerObject(identity, unitOf[i]);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const host = hostOf(i);
|
|
154
|
+
if (splitRiders.has(f.kind)) {
|
|
155
|
+
unitOf[i] = newUnit(identity, f.kind, identity ? unitPath(identity, f.kind) : riderPath(f, host));
|
|
156
|
+
if (identity)
|
|
157
|
+
registerObject(identity, unitOf[i]);
|
|
158
|
+
if (host !== -1)
|
|
159
|
+
hostUnit.set(unitOf[i], host);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// No identity: ride with the host unit.
|
|
163
|
+
if (host !== -1) {
|
|
164
|
+
unitOf[i] = host;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
unitOf[i] = unitFor(null, f.kind);
|
|
168
|
+
});
|
|
169
|
+
facts.forEach((f, i) => {
|
|
170
|
+
const text = sql.slice(f.span.start, f.span.start + f.span.len).trim();
|
|
171
|
+
if (!text)
|
|
172
|
+
return;
|
|
173
|
+
const unit = units[unitOf[i]];
|
|
174
|
+
unit.statements.push(i);
|
|
175
|
+
unit.sql += (unit.sql ? '\n\n' : '') + (text.endsWith(';') ? text : `${text};`);
|
|
176
|
+
if (f.dynamicSql)
|
|
177
|
+
unit.dynamicSql = true;
|
|
178
|
+
});
|
|
179
|
+
// Unit dependencies: statement edges (late edges do not constrain deploy
|
|
180
|
+
// order), plus schema membership (an object requires its schema's unit).
|
|
181
|
+
const deps = units.map(() => new Set());
|
|
182
|
+
for (const edge of graph.edges) {
|
|
183
|
+
if (edge.kind === 'late')
|
|
184
|
+
continue;
|
|
185
|
+
const from = unitOf[edge.from];
|
|
186
|
+
const to = unitOf[edge.to];
|
|
187
|
+
if (from !== -1 && to !== -1 && from !== to)
|
|
188
|
+
deps[from].add(to);
|
|
189
|
+
}
|
|
190
|
+
// A split rider requires its host object; a table-scoped object (trigger,
|
|
191
|
+
// policy, index, constraint, RLS) requires its table.
|
|
192
|
+
for (const [u, host] of hostUnit) {
|
|
193
|
+
if (u !== host)
|
|
194
|
+
deps[u].add(host);
|
|
195
|
+
}
|
|
196
|
+
units.forEach((u, i) => {
|
|
197
|
+
const id = u.identity;
|
|
198
|
+
if (!id?.table)
|
|
199
|
+
return;
|
|
200
|
+
const table = objectUnits.get(`${id.schema ?? ''}.${id.table}`);
|
|
201
|
+
if (table !== undefined && table !== i)
|
|
202
|
+
deps[i].add(table);
|
|
203
|
+
});
|
|
204
|
+
const schemaUnit = new Map();
|
|
205
|
+
units.forEach((u, i) => {
|
|
206
|
+
if (u.identity?.kind === 'schema')
|
|
207
|
+
schemaUnit.set(u.identity.name, i);
|
|
208
|
+
});
|
|
209
|
+
facts.forEach((f, i) => {
|
|
210
|
+
const from = unitOf[i];
|
|
211
|
+
if (from === -1)
|
|
212
|
+
return;
|
|
213
|
+
const schemas = new Set(f.referencedSchemas);
|
|
214
|
+
for (const created of f.creates) {
|
|
215
|
+
if (created.schema)
|
|
216
|
+
schemas.add(created.schema);
|
|
217
|
+
}
|
|
218
|
+
for (const schema of schemas) {
|
|
219
|
+
const to = schemaUnit.get(schema);
|
|
220
|
+
if (to !== undefined && to !== from)
|
|
221
|
+
deps[from].add(to);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
return { facts, units, unitOf, deps, source: sql };
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Partition a deploy surface into packages.
|
|
228
|
+
*
|
|
229
|
+
* Accepts either raw SQL or pgpm changes (whose deploy scripts are flattened
|
|
230
|
+
* in the given order — run the granularity dial first if a different unit
|
|
231
|
+
* shape is wanted). Requires `loadModule()` from `plpgsql-parser` first.
|
|
232
|
+
*
|
|
233
|
+
* Assignment: for each unit the first matching rule wins; unmatched units go
|
|
234
|
+
* to `defaultPackage`. Closure rules then pull the transitive dependency
|
|
235
|
+
* closure (hard + fk) of their units out of the default package. Finally the
|
|
236
|
+
* requires post-pass renders every unit dependency through the naming spec —
|
|
237
|
+
* same package as `<path>`, cross-package as `<pkg>:<path>` — and the
|
|
238
|
+
* package-level graph is checked for cycles ({@link PartitionCycleError}).
|
|
239
|
+
*/
|
|
240
|
+
function partitionUnits(input, config) {
|
|
241
|
+
const style = config.style ?? 'directory';
|
|
242
|
+
const sql = typeof input === 'string'
|
|
243
|
+
? input
|
|
244
|
+
: input.map(c => c.deploy.trim()).filter(Boolean).join('\n\n');
|
|
245
|
+
const { units, deps } = buildUnitGraph(sql, style, new Set(config.splitRiders ?? []));
|
|
246
|
+
const warnings = [];
|
|
247
|
+
// 1. Assignment: first matching rule wins.
|
|
248
|
+
const packageOf = units.map(unit => {
|
|
249
|
+
for (const rule of config.rules) {
|
|
250
|
+
if (rule.select.some(s => matches(unit, s)))
|
|
251
|
+
return rule.package;
|
|
252
|
+
}
|
|
253
|
+
return config.defaultPackage;
|
|
254
|
+
});
|
|
255
|
+
// 2. Closure: pull each closure rule's dependency closure out of the
|
|
256
|
+
// default package (never out of another rule's package).
|
|
257
|
+
const closureIncluded = new Map();
|
|
258
|
+
for (const rule of config.rules) {
|
|
259
|
+
if (!rule.closure)
|
|
260
|
+
continue;
|
|
261
|
+
const queue = units.map((_, i) => i).filter(i => packageOf[i] === rule.package);
|
|
262
|
+
const seen = new Set(queue);
|
|
263
|
+
while (queue.length > 0) {
|
|
264
|
+
const current = queue.shift();
|
|
265
|
+
for (const dep of deps[current]) {
|
|
266
|
+
if (seen.has(dep))
|
|
267
|
+
continue;
|
|
268
|
+
seen.add(dep);
|
|
269
|
+
if (packageOf[dep] === rule.package) {
|
|
270
|
+
queue.push(dep);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (packageOf[dep] !== config.defaultPackage)
|
|
274
|
+
continue;
|
|
275
|
+
packageOf[dep] = rule.package;
|
|
276
|
+
closureIncluded.set(units[dep].path, rule.package);
|
|
277
|
+
queue.push(dep);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Dynamic SQL makes edges incomplete: surface it, do not guess.
|
|
282
|
+
units.forEach((unit, i) => {
|
|
283
|
+
if (unit.dynamicSql && unit.statements.length > 0) {
|
|
284
|
+
warnings.push(`${units[i].path} (${packageOf[i]}) executes dynamic SQL; hidden references cannot be checked against the partition`);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
// 3. Requires post-pass + package-level graph.
|
|
288
|
+
const pkgNames = [...new Set([...packageOf, config.defaultPackage])];
|
|
289
|
+
const pkgIndex = new Map(pkgNames.map((p, i) => [p, i]));
|
|
290
|
+
const pkgEdges = new Map();
|
|
291
|
+
const changesByPkg = new Map();
|
|
292
|
+
for (const p of pkgNames)
|
|
293
|
+
changesByPkg.set(p, []);
|
|
294
|
+
units.forEach((unit, i) => {
|
|
295
|
+
if (unit.statements.length === 0)
|
|
296
|
+
return;
|
|
297
|
+
const pkg = packageOf[i];
|
|
298
|
+
const dependencies = [...deps[i]]
|
|
299
|
+
.filter(d => units[d].statements.length > 0)
|
|
300
|
+
.map(d => {
|
|
301
|
+
const depPkg = packageOf[d];
|
|
302
|
+
if (depPkg === pkg)
|
|
303
|
+
return units[d].path;
|
|
304
|
+
const key = `${pkg}\u0000${depPkg}`;
|
|
305
|
+
if (!pkgEdges.has(key)) {
|
|
306
|
+
pkgEdges.set(key, {
|
|
307
|
+
from: pkgIndex.get(pkg),
|
|
308
|
+
to: pkgIndex.get(depPkg),
|
|
309
|
+
via: `${pkg}:${unit.path} -> ${depPkg}:${units[d].path}`
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return `${depPkg}:${units[d].path}`;
|
|
313
|
+
})
|
|
314
|
+
.sort();
|
|
315
|
+
changesByPkg.get(pkg).push({ name: unit.path, dependencies, deploy: unit.sql });
|
|
316
|
+
});
|
|
317
|
+
// 4. Cross-package cycle detection over the package graph.
|
|
318
|
+
detectCycles(pkgNames, [...pkgEdges.values()]);
|
|
319
|
+
const assignments = new Map();
|
|
320
|
+
units.forEach((unit, i) => {
|
|
321
|
+
if (unit.statements.length > 0)
|
|
322
|
+
assignments.set(unit.path, packageOf[i]);
|
|
323
|
+
});
|
|
324
|
+
const packages = pkgNames
|
|
325
|
+
.map(name => ({
|
|
326
|
+
name,
|
|
327
|
+
changes: changesByPkg.get(name),
|
|
328
|
+
requires: [...new Set([...pkgEdges.values()]
|
|
329
|
+
.filter(e => pkgNames[e.from] === name)
|
|
330
|
+
.map(e => pkgNames[e.to]))].sort()
|
|
331
|
+
}))
|
|
332
|
+
.filter(p => p.changes.length > 0);
|
|
333
|
+
return { packages, assignments, closureIncluded, warnings };
|
|
334
|
+
}
|
|
335
|
+
function detectCycles(pkgNames, edges) {
|
|
336
|
+
const out = new Map();
|
|
337
|
+
for (const e of edges) {
|
|
338
|
+
const list = out.get(e.from) ?? [];
|
|
339
|
+
list.push({ to: e.to, via: e.via });
|
|
340
|
+
out.set(e.from, list);
|
|
341
|
+
}
|
|
342
|
+
const state = new Array(pkgNames.length).fill(0); // 0 unseen, 1 on stack, 2 done
|
|
343
|
+
const stack = [];
|
|
344
|
+
const viaStack = [];
|
|
345
|
+
const visit = (v) => {
|
|
346
|
+
state[v] = 1;
|
|
347
|
+
stack.push(v);
|
|
348
|
+
for (const { to, via } of out.get(v) ?? []) {
|
|
349
|
+
if (state[to] === 2)
|
|
350
|
+
continue;
|
|
351
|
+
viaStack.push(via);
|
|
352
|
+
if (state[to] === 1) {
|
|
353
|
+
const start = stack.indexOf(to);
|
|
354
|
+
const cyclePkgs = stack.slice(start).map(i => pkgNames[i]);
|
|
355
|
+
const cycleEdges = viaStack.slice(viaStack.length - cyclePkgs.length);
|
|
356
|
+
throw new PartitionCycleError(cyclePkgs, cycleEdges);
|
|
357
|
+
}
|
|
358
|
+
visit(to);
|
|
359
|
+
viaStack.pop();
|
|
360
|
+
}
|
|
361
|
+
stack.pop();
|
|
362
|
+
state[v] = 2;
|
|
363
|
+
};
|
|
364
|
+
for (let v = 0; v < pkgNames.length; v++) {
|
|
365
|
+
if (state[v] === 0)
|
|
366
|
+
visit(v);
|
|
367
|
+
}
|
|
368
|
+
}
|
package/regen.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regeneration driver: derive revert/verify scripts for a single pgpm
|
|
3
|
+
* change from its deploy SQL.
|
|
4
|
+
*
|
|
5
|
+
* Deploy scripts are classified into statement facts (`classifyStatements`)
|
|
6
|
+
* and fed through `revertFor`/`verifyFor` from `@pgsql/scripts`: revert is
|
|
7
|
+
* the mechanical inverses in reverse topological order, verify is one
|
|
8
|
+
* raise-on-failure existence check per created object. Transaction control
|
|
9
|
+
* statements (the BEGIN/COMMIT wrapper some modules carry inside their
|
|
10
|
+
* scripts) are excluded before classification — the caller owns the wrapper.
|
|
11
|
+
*
|
|
12
|
+
* Requires `loadModule()` from `plpgsql-parser` to have completed.
|
|
13
|
+
*/
|
|
14
|
+
import type { GeneratedScript } from '@pgsql/scripts';
|
|
15
|
+
import type { StatementFacts } from '@pgsql/transform';
|
|
16
|
+
export type { GeneratedScript } from '@pgsql/scripts';
|
|
17
|
+
/** Generated revert and verify scripts for one change's deploy SQL. */
|
|
18
|
+
export interface RegeneratedScripts {
|
|
19
|
+
revert: GeneratedScript;
|
|
20
|
+
verify: GeneratedScript;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Classify a pgpm script's substantive statements: every top-level statement
|
|
24
|
+
* except transaction control (`BEGIN`/`COMMIT`/`ROLLBACK` wrappers).
|
|
25
|
+
*/
|
|
26
|
+
export declare function classifyScript(sql: string): StatementFacts[];
|
|
27
|
+
/**
|
|
28
|
+
* Whether a script is an empty stub: nothing but comments, whitespace, and
|
|
29
|
+
* a transaction wrapper — no substantive statements. Scaffolded
|
|
30
|
+
* `-- Add your revert SQL here` files and bare `BEGIN; ... COMMIT;` shells
|
|
31
|
+
* are stubs. A script that fails to parse is NOT a stub (never treat content
|
|
32
|
+
* we cannot understand as safe to overwrite).
|
|
33
|
+
*/
|
|
34
|
+
export declare function isStubScript(sql: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Generate revert and verify scripts (headerless, unwrapped) for one
|
|
37
|
+
* change's deploy SQL. Statements with no derivable inverse contribute a
|
|
38
|
+
* `-- revert not derivable: <reason>` comment and a warning; statements
|
|
39
|
+
* whose effect cannot be existence-checked add a verify warning.
|
|
40
|
+
*/
|
|
41
|
+
export declare function regenerateScripts(deploySql: string): RegeneratedScripts;
|
package/regen.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.classifyScript = classifyScript;
|
|
4
|
+
exports.isStubScript = isStubScript;
|
|
5
|
+
exports.regenerateScripts = regenerateScripts;
|
|
6
|
+
const scripts_1 = require("@pgsql/scripts");
|
|
7
|
+
const transform_1 = require("@pgsql/transform");
|
|
8
|
+
/**
|
|
9
|
+
* Classify a pgpm script's substantive statements: every top-level statement
|
|
10
|
+
* except transaction control (`BEGIN`/`COMMIT`/`ROLLBACK` wrappers).
|
|
11
|
+
*/
|
|
12
|
+
function classifyScript(sql) {
|
|
13
|
+
return (0, transform_1.classifyStatements)(sql).filter(f => f.nodeTag !== 'TransactionStmt');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Whether a script is an empty stub: nothing but comments, whitespace, and
|
|
17
|
+
* a transaction wrapper — no substantive statements. Scaffolded
|
|
18
|
+
* `-- Add your revert SQL here` files and bare `BEGIN; ... COMMIT;` shells
|
|
19
|
+
* are stubs. A script that fails to parse is NOT a stub (never treat content
|
|
20
|
+
* we cannot understand as safe to overwrite).
|
|
21
|
+
*/
|
|
22
|
+
function isStubScript(sql) {
|
|
23
|
+
if (!sql || !sql.trim())
|
|
24
|
+
return true;
|
|
25
|
+
try {
|
|
26
|
+
return classifyScript(sql).length === 0;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate revert and verify scripts (headerless, unwrapped) for one
|
|
34
|
+
* change's deploy SQL. Statements with no derivable inverse contribute a
|
|
35
|
+
* `-- revert not derivable: <reason>` comment and a warning; statements
|
|
36
|
+
* whose effect cannot be existence-checked add a verify warning.
|
|
37
|
+
*/
|
|
38
|
+
function regenerateScripts(deploySql) {
|
|
39
|
+
const facts = classifyScript(deploySql);
|
|
40
|
+
return {
|
|
41
|
+
revert: (0, scripts_1.revertFor)(facts),
|
|
42
|
+
verify: (0, scripts_1.verifyFor)(facts)
|
|
43
|
+
};
|
|
44
|
+
}
|