@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.
@@ -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
+ }