@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,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic diff driver: identity-keyed schema delta between two deploy
|
|
3
|
+
* surfaces.
|
|
4
|
+
*
|
|
5
|
+
* Where `diffBundles` (in `@pgpmjs/bundle`) compares change names and content
|
|
6
|
+
* digests — which breaks the moment a dial regroups or renames changes — this
|
|
7
|
+
* driver re-keys the comparison on object identity (`identityOf`), so the
|
|
8
|
+
* same schema authored at different granularities or partitions diffs as
|
|
9
|
+
* equal. Objects are compared at the AST level (normalized parse trees, so
|
|
10
|
+
* formatting never matters); tables are compared column-by-column and
|
|
11
|
+
* constraint-by-constraint so an added column emits `ALTER TABLE ADD COLUMN`,
|
|
12
|
+
* not a table rebuild.
|
|
13
|
+
*
|
|
14
|
+
* The emitted delta is a set of pgpm changes: DROPs for removed objects
|
|
15
|
+
* (reverse topological order), then CREATE/ALTER changes for added and
|
|
16
|
+
* modified objects routed through the granularity pipeline
|
|
17
|
+
* (`restructureChanges`), so names come from the naming spec and `requires`
|
|
18
|
+
* from the statement graph — the same derivation every other projection uses.
|
|
19
|
+
* Every emitted change carries a full deploy/revert/verify triple: drops are
|
|
20
|
+
* `@pgsql/scripts`' `revertFor` inverses of the object's own deploy
|
|
21
|
+
* statements (never hand-templated), reverts recreate what was dropped, and
|
|
22
|
+
* verifies come from `verifyFor`.
|
|
23
|
+
*
|
|
24
|
+
* Requires `loadModule()` from `plpgsql-parser` before use.
|
|
25
|
+
*/
|
|
26
|
+
import { pathFor } from '@pgpmjs/naming-spec';
|
|
27
|
+
import { revertFor, verifyFor } from '@pgsql/scripts';
|
|
28
|
+
import { buildStatementGraph, classifyStatements, cleanTree, identityOf } from '@pgsql/transform';
|
|
29
|
+
import { Deparser, parseSync } from 'plpgsql-parser';
|
|
30
|
+
import { restructureChanges } from './granularity-driver';
|
|
31
|
+
const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table ?? ''}\u0000${id.name}`;
|
|
32
|
+
/**
|
|
33
|
+
* Group one side's script into identity-keyed object units. Statements with
|
|
34
|
+
* no identity (grants, comments) attach to the unit of the first object they
|
|
35
|
+
* reference, falling back to the preceding statement's unit; truly homeless
|
|
36
|
+
* statements are reported via `warnings`.
|
|
37
|
+
*/
|
|
38
|
+
function readSide(sql, warnings, label) {
|
|
39
|
+
const facts = classifyStatements(sql);
|
|
40
|
+
const graph = buildStatementGraph(facts);
|
|
41
|
+
const units = new Map();
|
|
42
|
+
const order = [];
|
|
43
|
+
const unitOf = new Array(facts.length).fill(null);
|
|
44
|
+
const byObject = new Map();
|
|
45
|
+
facts.forEach((f, i) => {
|
|
46
|
+
const text = sql.slice(f.span.start, f.span.start + f.span.len).trim();
|
|
47
|
+
if (!text)
|
|
48
|
+
return;
|
|
49
|
+
let identity = groupingIdentity(f);
|
|
50
|
+
if (identity && identity.kind === 'other') {
|
|
51
|
+
const asTable = identityKey({ kind: 'table', schema: identity.schema, name: identity.name });
|
|
52
|
+
if (units.has(asTable))
|
|
53
|
+
identity = units.get(asTable).identity;
|
|
54
|
+
}
|
|
55
|
+
let key = null;
|
|
56
|
+
if (identity) {
|
|
57
|
+
key = identityKey(identity);
|
|
58
|
+
if (!units.has(key)) {
|
|
59
|
+
units.set(key, { identity, statements: [], texts: [] });
|
|
60
|
+
order.push(key);
|
|
61
|
+
}
|
|
62
|
+
if (!identity.table) {
|
|
63
|
+
byObject.set(`${identity.schema ?? ''}.${identity.name}`, key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
for (const r of f.references) {
|
|
68
|
+
const host = byObject.get(`${r.schema ?? ''}.${r.name}`);
|
|
69
|
+
if (host) {
|
|
70
|
+
key = host;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!key && i > 0)
|
|
75
|
+
key = unitOf[i - 1];
|
|
76
|
+
}
|
|
77
|
+
if (!key) {
|
|
78
|
+
warnings.push(`${label}: statement ${i} (${f.kind}) belongs to no object; ignored in the diff`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
unitOf[i] = key;
|
|
82
|
+
const unit = units.get(key);
|
|
83
|
+
unit.statements.push(i);
|
|
84
|
+
unit.texts.push(text.endsWith(';') ? text : `${text};`);
|
|
85
|
+
});
|
|
86
|
+
return { facts, units, order, graph };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The identity a statement groups under. Table-shaping statements — ALTER
|
|
90
|
+
* TABLE column/constraint additions, RLS enablement — fold into their table's
|
|
91
|
+
* unit so that atomic and consolidated authorships of the same table land in
|
|
92
|
+
* the same unit and compare shape-to-shape. Generic ALTERs (`kind: 'other'`)
|
|
93
|
+
* fold in `readSide` when a table unit for the same object already exists.
|
|
94
|
+
*/
|
|
95
|
+
function groupingIdentity(f) {
|
|
96
|
+
const identity = identityOf(f);
|
|
97
|
+
if (!identity)
|
|
98
|
+
return null;
|
|
99
|
+
if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'rls_enable') &&
|
|
100
|
+
identity.kind !== 'table') {
|
|
101
|
+
return { kind: 'table', schema: identity.schema, name: identity.table ?? identity.name };
|
|
102
|
+
}
|
|
103
|
+
return identity;
|
|
104
|
+
}
|
|
105
|
+
/** Normalized-AST fingerprint of one statement (formatting/position-proof). */
|
|
106
|
+
function fingerprint(text) {
|
|
107
|
+
const parsed = parseSync(text);
|
|
108
|
+
return JSON.stringify(cleanTree(parsed.sql.stmts.map(s => s.stmt)));
|
|
109
|
+
}
|
|
110
|
+
/** Multiset fingerprint of a whole unit. */
|
|
111
|
+
const unitFingerprint = (unit) => unit.texts.map(fingerprint).sort().join('\n');
|
|
112
|
+
/**
|
|
113
|
+
* Fold a table unit's statements (CREATE TABLE plus any ALTER TABLE
|
|
114
|
+
* ADD COLUMN / ADD CONSTRAINT) into an effective shape, so atomic and
|
|
115
|
+
* consolidated authorships of the same table compare equal.
|
|
116
|
+
*/
|
|
117
|
+
function tableShape(unit) {
|
|
118
|
+
const shape = { relation: null, columns: new Map(), extras: new Map() };
|
|
119
|
+
const addColumn = (node) => {
|
|
120
|
+
const def = node;
|
|
121
|
+
const clean = cleanTree(node);
|
|
122
|
+
shape.columns.set(def.colname ?? '', { def: clean, print: JSON.stringify(clean) });
|
|
123
|
+
};
|
|
124
|
+
const addExtra = (node) => {
|
|
125
|
+
const clean = cleanTree(node);
|
|
126
|
+
shape.extras.set(JSON.stringify(clean), clean);
|
|
127
|
+
};
|
|
128
|
+
for (const text of unit.texts) {
|
|
129
|
+
for (const raw of parseSync(text).sql.stmts) {
|
|
130
|
+
const stmt = raw.stmt;
|
|
131
|
+
if (stmt.CreateStmt) {
|
|
132
|
+
shape.relation = stmt.CreateStmt.relation;
|
|
133
|
+
const elts = stmt.CreateStmt.tableElts ?? [];
|
|
134
|
+
for (const elt of elts) {
|
|
135
|
+
if (elt.ColumnDef)
|
|
136
|
+
addColumn(elt.ColumnDef);
|
|
137
|
+
else
|
|
138
|
+
addExtra(elt);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (stmt.AlterTableStmt) {
|
|
142
|
+
shape.relation ??= stmt.AlterTableStmt.relation;
|
|
143
|
+
const cmds = stmt.AlterTableStmt.cmds ?? [];
|
|
144
|
+
for (const cmd of cmds) {
|
|
145
|
+
const at = cmd.AlterTableCmd;
|
|
146
|
+
if (!at)
|
|
147
|
+
continue;
|
|
148
|
+
const def = at.def;
|
|
149
|
+
if (at.subtype === 'AT_AddColumn' && def?.ColumnDef) {
|
|
150
|
+
addColumn(def.ColumnDef);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
addExtra({ AlterTableCmd: at });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
addExtra(stmt);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return shape;
|
|
163
|
+
}
|
|
164
|
+
const deparseStmt = (node) => `${Deparser.deparse(node, { pretty: false })};`;
|
|
165
|
+
/** Column-level ALTERs between two table shapes. */
|
|
166
|
+
function diffTable(from, to, diff, warnings) {
|
|
167
|
+
const stmts = [];
|
|
168
|
+
const relation = to.relation ?? from.relation;
|
|
169
|
+
const alter = (cmd) => deparseStmt({
|
|
170
|
+
AlterTableStmt: {
|
|
171
|
+
relation,
|
|
172
|
+
cmds: [{ AlterTableCmd: { behavior: 'DROP_RESTRICT', ...cmd } }],
|
|
173
|
+
objtype: 'OBJECT_TABLE'
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
diff.columnsAdded = [];
|
|
177
|
+
diff.columnsRemoved = [];
|
|
178
|
+
diff.columnsModified = [];
|
|
179
|
+
for (const [name, col] of to.columns) {
|
|
180
|
+
const prior = from.columns.get(name);
|
|
181
|
+
if (!prior) {
|
|
182
|
+
diff.columnsAdded.push(name);
|
|
183
|
+
stmts.push(alter({ subtype: 'AT_AddColumn', def: { ColumnDef: col.def } }));
|
|
184
|
+
}
|
|
185
|
+
else if (prior.print !== col.print) {
|
|
186
|
+
diff.columnsModified.push(name);
|
|
187
|
+
const fromDef = prior.def;
|
|
188
|
+
const toDef = col.def;
|
|
189
|
+
const typeChanged = JSON.stringify(fromDef.typeName) !== JSON.stringify(toDef.typeName);
|
|
190
|
+
const restChanged = JSON.stringify({ ...fromDef, typeName: null }) !==
|
|
191
|
+
JSON.stringify({ ...toDef, typeName: null });
|
|
192
|
+
if (typeChanged) {
|
|
193
|
+
stmts.push(alter({
|
|
194
|
+
subtype: 'AT_AlterColumnType',
|
|
195
|
+
name,
|
|
196
|
+
def: { ColumnDef: { typeName: toDef.typeName } }
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
if (restChanged) {
|
|
200
|
+
warnings.push(`${diff.path}: column "${name}" changed beyond its type (constraints/defaults); emit a manual ALTER`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const name of from.columns.keys()) {
|
|
205
|
+
if (!to.columns.has(name)) {
|
|
206
|
+
diff.columnsRemoved.push(name);
|
|
207
|
+
stmts.push(alter({ subtype: 'AT_DropColumn', name }));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const [print, extra] of to.extras) {
|
|
211
|
+
if (!from.extras.has(print)) {
|
|
212
|
+
const at = extra.AlterTableCmd;
|
|
213
|
+
if (at)
|
|
214
|
+
stmts.push(alter(at));
|
|
215
|
+
else if (extra.Constraint) {
|
|
216
|
+
stmts.push(alter({ subtype: 'AT_AddConstraint', def: extra }));
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
warnings.push(`${diff.path}: unrecognized added table element; emit a manual ALTER`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const [print, extra] of from.extras) {
|
|
224
|
+
if (to.extras.has(print))
|
|
225
|
+
continue;
|
|
226
|
+
const conname = extra.Constraint?.conname
|
|
227
|
+
?? extra
|
|
228
|
+
.AlterTableCmd?.def?.Constraint?.conname;
|
|
229
|
+
if (conname) {
|
|
230
|
+
stmts.push(alter({ subtype: 'AT_DropConstraint', name: conname }));
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
warnings.push(`${diff.path}: removed table element has no name to drop; emit a manual ALTER`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return stmts;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Diff two deploy surfaces by object identity and emit the migration delta.
|
|
240
|
+
*
|
|
241
|
+
* `from`/`to` are whole deploy scripts (flatten changes in plan order to
|
|
242
|
+
* diff bundles/modules). The result's `changes` deploy `to`'s shape on top
|
|
243
|
+
* of `from`: reverse-topological DROPs, then CREATE/ALTERs named and
|
|
244
|
+
* ordered by `restructureChanges`.
|
|
245
|
+
*/
|
|
246
|
+
export function diffSchemas(from, to, options = {}) {
|
|
247
|
+
const style = options.style ?? 'directory';
|
|
248
|
+
const warnings = [];
|
|
249
|
+
const a = readSide(from, warnings, 'from');
|
|
250
|
+
const b = readSide(to, warnings, 'to');
|
|
251
|
+
const objects = [];
|
|
252
|
+
const forwardSql = [];
|
|
253
|
+
const modifiedChanges = [];
|
|
254
|
+
const dropUnits = [];
|
|
255
|
+
/** `@pgsql/scripts` inverse of a deploy script (drops in reverse topo order). */
|
|
256
|
+
const inverseOf = (deploy, context) => {
|
|
257
|
+
const { sql, warnings: w } = revertFor(classifyStatements(deploy));
|
|
258
|
+
warnings.push(...w.map(x => `${context}: ${x}`));
|
|
259
|
+
return sql;
|
|
260
|
+
};
|
|
261
|
+
/** `@pgsql/scripts` existence checks for a deploy script. */
|
|
262
|
+
const verifyOf = (deploy, context) => {
|
|
263
|
+
const { sql, warnings: w } = verifyFor(classifyStatements(deploy));
|
|
264
|
+
warnings.push(...w.map(x => `${context}: ${x}`));
|
|
265
|
+
return sql;
|
|
266
|
+
};
|
|
267
|
+
for (const key of b.order) {
|
|
268
|
+
const unit = b.units.get(key);
|
|
269
|
+
const prior = a.units.get(key);
|
|
270
|
+
const path = pathFor(unit.identity, { style });
|
|
271
|
+
if (!prior) {
|
|
272
|
+
objects.push({ identity: unit.identity, path, delta: 'added' });
|
|
273
|
+
forwardSql.push(...unit.texts);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (unit.identity.kind === 'table') {
|
|
277
|
+
const shapeFrom = tableShape(prior);
|
|
278
|
+
const shapeTo = tableShape(unit);
|
|
279
|
+
const diff = { identity: unit.identity, path, delta: 'modified' };
|
|
280
|
+
const stmts = diffTable(shapeFrom, shapeTo, diff, warnings);
|
|
281
|
+
const columnsTouched = diff.columnsAdded.length + diff.columnsRemoved.length + diff.columnsModified.length > 0;
|
|
282
|
+
if (stmts.length > 0 || columnsTouched) {
|
|
283
|
+
objects.push(diff);
|
|
284
|
+
if (stmts.length > 0) {
|
|
285
|
+
// The inverse alteration is the same diff with the sides swapped.
|
|
286
|
+
const scratch = { identity: unit.identity, path, delta: 'modified' };
|
|
287
|
+
const deploy = stmts.join('\n');
|
|
288
|
+
modifiedChanges.push({
|
|
289
|
+
name: `${path}/alter`,
|
|
290
|
+
dependencies: [],
|
|
291
|
+
deploy,
|
|
292
|
+
revert: diffTable(shapeTo, shapeFrom, scratch, []).join('\n'),
|
|
293
|
+
verify: verifyOf(deploy, path)
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (unitFingerprint(prior) !== unitFingerprint(unit)) {
|
|
300
|
+
objects.push({ identity: unit.identity, path, delta: 'modified' });
|
|
301
|
+
const priorDeploy = prior.texts.join('\n');
|
|
302
|
+
const newDeploy = unit.texts.join('\n');
|
|
303
|
+
modifiedChanges.push({
|
|
304
|
+
name: path,
|
|
305
|
+
dependencies: [],
|
|
306
|
+
deploy: [inverseOf(priorDeploy, path), newDeploy].filter(Boolean).join('\n'),
|
|
307
|
+
revert: [inverseOf(newDeploy, path), priorDeploy].filter(Boolean).join('\n'),
|
|
308
|
+
verify: verifyOf(newDeploy, path)
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const key of a.order) {
|
|
313
|
+
if (b.units.has(key))
|
|
314
|
+
continue;
|
|
315
|
+
const unit = a.units.get(key);
|
|
316
|
+
objects.push({ identity: unit.identity, path: pathFor(unit.identity, { style }), delta: 'removed' });
|
|
317
|
+
dropUnits.push(unit);
|
|
318
|
+
}
|
|
319
|
+
// Removed objects drop in reverse topological order of the `from` graph
|
|
320
|
+
// (dependents before dependencies), one change per object.
|
|
321
|
+
const position = new Map(dropUnits.map(u => [u, Math.min(...u.statements.map(s => a.graph.order.indexOf(s)))]));
|
|
322
|
+
dropUnits.sort((x, y) => position.get(y) - position.get(x));
|
|
323
|
+
const dropChanges = [];
|
|
324
|
+
for (const unit of dropUnits) {
|
|
325
|
+
const path = pathFor(unit.identity, { style });
|
|
326
|
+
const recreate = unit.texts.join('\n');
|
|
327
|
+
const drop = inverseOf(recreate, path);
|
|
328
|
+
if (!drop.trim() || drop.trim().startsWith('--')) {
|
|
329
|
+
warnings.push(`${path}: no DROP derivable for kind "${unit.identity.kind}"; drop manually`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
dropChanges.push({
|
|
333
|
+
name: `${path}/drop`,
|
|
334
|
+
dependencies: dropChanges.length > 0 ? [dropChanges[dropChanges.length - 1].name] : [],
|
|
335
|
+
deploy: drop,
|
|
336
|
+
revert: recreate,
|
|
337
|
+
verify: ''
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
let forwardChanges = [];
|
|
341
|
+
if (forwardSql.length > 0) {
|
|
342
|
+
const { changes, warnings: w } = restructureChanges([{ name: 'delta', dependencies: [], deploy: forwardSql.join('\n\n') }], { granularity: options.granularity ?? 'object' });
|
|
343
|
+
forwardChanges = changes.map(c => ({
|
|
344
|
+
...c,
|
|
345
|
+
revert: inverseOf(c.deploy, c.name),
|
|
346
|
+
verify: verifyOf(c.deploy, c.name)
|
|
347
|
+
}));
|
|
348
|
+
warnings.push(...w);
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
identical: objects.length === 0,
|
|
352
|
+
objects,
|
|
353
|
+
changes: [...dropChanges, ...forwardChanges, ...modifiedChanges],
|
|
354
|
+
warnings
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Identity-keyed diff over pgpm changes: flattens each side's deploy scripts
|
|
359
|
+
* in plan order and delegates to {@link diffSchemas}. Change names never
|
|
360
|
+
* enter the comparison — regrouping, renaming, or repartitioning the same
|
|
361
|
+
* schema yields an identical diff.
|
|
362
|
+
*/
|
|
363
|
+
export function diffChangeSets(from, to, options = {}) {
|
|
364
|
+
const flatten = (changes) => changes.map(c => c.deploy.trim()).filter(Boolean).join('\n\n');
|
|
365
|
+
return diffSchemas(flatten(from), flatten(to), options);
|
|
366
|
+
}
|
package/granularity-driver.d.ts
CHANGED
|
@@ -1,52 +1,49 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Granularity driver: restructure a pgpm module's deploy surface between the
|
|
3
|
-
* atomic, object, and consolidated shapes.
|
|
4
|
-
*
|
|
5
|
-
* The upstream pass (`restructureSql` in `@pgsql/transform`) rewrites one SQL
|
|
6
|
-
* script between equivalent shapes, guarded by the statement dependency
|
|
7
|
-
* graph. This driver lifts that to the pgpm change model: it flattens a
|
|
8
|
-
* module's deploy scripts in plan order into one program, restructures it to
|
|
9
|
-
* the target granularity, then re-slices the result into changes — one change
|
|
10
|
-
* per created object — with change dependencies recomputed from the statement
|
|
11
|
-
* graph. Like the other drivers in this package it is structurally typed on
|
|
12
|
-
* the bundle seams: no dependency on `@pgpmjs/bundle` or `@pgpmjs/core`.
|
|
13
|
-
*
|
|
14
|
-
* - `atomic` — the machine-emitted shape: bare CREATE TABLE plus one
|
|
15
|
-
* ALTER per column/constraint.
|
|
16
|
-
* - `object` — each table fully baked; cross-object statements
|
|
17
|
-
* (FKs, indexes, triggers, policies) stay separate.
|
|
18
|
-
* - `consolidated` — additionally inlines FKs proven safe by the graph.
|
|
19
|
-
*/
|
|
20
1
|
import type { Granularity, StatementFacts } from '@pgsql/transform';
|
|
21
2
|
export type { Granularity } from '@pgsql/transform';
|
|
22
3
|
/** A change's deploy surface going into or out of the restructure. */
|
|
23
4
|
export interface GranularityChange {
|
|
24
|
-
/** Change name (plan token, e.g. `schemas/app/tables/users`). */
|
|
5
|
+
/** Change name (plan token, e.g. `schemas/app/tables/users/table`). */
|
|
25
6
|
name: string;
|
|
26
7
|
/** Change names this change requires (within the same module). */
|
|
27
8
|
dependencies: string[];
|
|
28
9
|
/** Deploy SQL (headerless — the caller owns pgpm headers). */
|
|
29
10
|
deploy: string;
|
|
30
11
|
}
|
|
12
|
+
/** A restructured change: deploy plus generated revert/verify scripts. */
|
|
13
|
+
export interface RestructuredChange extends GranularityChange {
|
|
14
|
+
/**
|
|
15
|
+
* Generated revert SQL (headerless): mechanical inverses of the change's
|
|
16
|
+
* statements in reverse topological order within the group, via
|
|
17
|
+
* `revertFor`. Non-invertible statements leave a `-- revert not
|
|
18
|
+
* derivable` comment and a warning.
|
|
19
|
+
*/
|
|
20
|
+
revert: string;
|
|
21
|
+
/**
|
|
22
|
+
* Generated verify SQL (headerless): one raise-on-failure existence check
|
|
23
|
+
* per created object, via `verifyFor`.
|
|
24
|
+
*/
|
|
25
|
+
verify: string;
|
|
26
|
+
}
|
|
31
27
|
export interface RestructureModuleOptions {
|
|
32
28
|
granularity: Granularity;
|
|
33
29
|
/**
|
|
34
30
|
* Derive a change name for a statement group from the facts of its primary
|
|
35
|
-
* (creating) statement. Defaults to {@link defaultChangeName}:
|
|
36
|
-
* `
|
|
31
|
+
* (creating) statement. Defaults to {@link defaultChangeName}: naming spec
|
|
32
|
+
* v1 paths (`identityOf` + `pathFor`).
|
|
37
33
|
*/
|
|
38
34
|
changeName?: (facts: StatementFacts) => string;
|
|
39
35
|
}
|
|
40
36
|
export interface RestructureModuleResult {
|
|
41
37
|
/** Restructured changes in deploy order, dependencies recomputed. */
|
|
42
|
-
changes:
|
|
38
|
+
changes: RestructuredChange[];
|
|
43
39
|
/** Non-fatal notes (folds rejected to preserve ordering, etc.). */
|
|
44
40
|
warnings: string[];
|
|
45
41
|
}
|
|
46
42
|
/**
|
|
47
|
-
* Default
|
|
48
|
-
* `
|
|
49
|
-
*
|
|
43
|
+
* Default change name for a statement group: the object's canonical naming
|
|
44
|
+
* spec v1 path — `identityOf(facts)` (Postgres-native identity, from
|
|
45
|
+
* `@pgsql/transform`) rendered through `pathFor` (`@pgpmjs/naming-spec`).
|
|
46
|
+
* Paths are pure projections of identity, never authored.
|
|
50
47
|
*/
|
|
51
48
|
export declare function defaultChangeName(facts: StatementFacts): string;
|
|
52
49
|
/**
|
package/granularity-driver.js
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.defaultChangeName = defaultChangeName;
|
|
4
4
|
exports.restructureChanges = restructureChanges;
|
|
5
|
+
/**
|
|
6
|
+
* Granularity driver: restructure a pgpm module's deploy surface between the
|
|
7
|
+
* atomic, object, and consolidated shapes.
|
|
8
|
+
*
|
|
9
|
+
* The upstream pass (`restructureSql` in `@pgsql/transform`) rewrites one SQL
|
|
10
|
+
* script between equivalent shapes, guarded by the statement dependency
|
|
11
|
+
* graph. This driver lifts that to the pgpm change model: it flattens a
|
|
12
|
+
* module's deploy scripts in plan order into one program, restructures it to
|
|
13
|
+
* the target granularity, then re-slices the result into changes — one change
|
|
14
|
+
* per created object — with change dependencies recomputed from the statement
|
|
15
|
+
* graph. Like the other drivers in this package it is structurally typed on
|
|
16
|
+
* the bundle seams: no dependency on `@pgpmjs/bundle` or `@pgpmjs/core`.
|
|
17
|
+
*
|
|
18
|
+
* - `atomic` — the machine-emitted shape: bare CREATE TABLE plus one
|
|
19
|
+
* ALTER per column/constraint.
|
|
20
|
+
* - `object` — each table fully baked; cross-object statements
|
|
21
|
+
* (FKs, indexes, triggers, policies) stay separate.
|
|
22
|
+
* - `consolidated` — additionally inlines FKs proven safe by the graph.
|
|
23
|
+
*/
|
|
24
|
+
const naming_spec_1 = require("@pgpmjs/naming-spec");
|
|
25
|
+
const scripts_1 = require("@pgsql/scripts");
|
|
5
26
|
const transform_1 = require("@pgsql/transform");
|
|
6
27
|
const KIND_DIRS = {
|
|
7
28
|
table: 'tables',
|
|
@@ -14,21 +35,15 @@ const KIND_DIRS = {
|
|
|
14
35
|
seed_dml: 'fixtures'
|
|
15
36
|
};
|
|
16
37
|
/**
|
|
17
|
-
* Default
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
38
|
+
* Default change name for a statement group: the object's canonical naming
|
|
39
|
+
* spec v1 path — `identityOf(facts)` (Postgres-native identity, from
|
|
40
|
+
* `@pgsql/transform`) rendered through `pathFor` (`@pgpmjs/naming-spec`).
|
|
41
|
+
* Paths are pure projections of identity, never authored.
|
|
20
42
|
*/
|
|
21
43
|
function defaultChangeName(facts) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
return
|
|
25
|
-
if (created) {
|
|
26
|
-
const dir = KIND_DIRS[facts.kind] ?? 'objects';
|
|
27
|
-
const schema = created.schema ?? 'public';
|
|
28
|
-
// Trigger/policy names are table-qualified (`table.trigger`).
|
|
29
|
-
const name = created.name.replace(/\./g, '/');
|
|
30
|
-
return `schemas/${schema}/${dir}/${name}`;
|
|
31
|
-
}
|
|
44
|
+
const identity = (0, transform_1.identityOf)(facts);
|
|
45
|
+
if (identity)
|
|
46
|
+
return (0, naming_spec_1.pathFor)(identity);
|
|
32
47
|
return 'misc/statements';
|
|
33
48
|
}
|
|
34
49
|
/**
|
|
@@ -89,13 +104,16 @@ function restructureChanges(changes, options) {
|
|
|
89
104
|
schemaGroup.set(f.creates[0].name, groupOf[i]);
|
|
90
105
|
}
|
|
91
106
|
});
|
|
92
|
-
// Slice statement text per group, in the emitted (topological) order
|
|
107
|
+
// Slice statement text per group, in the emitted (topological) order,
|
|
108
|
+
// and collect each group's facts for revert/verify generation.
|
|
93
109
|
const groupSql = groupNames.map(() => []);
|
|
110
|
+
const groupStatements = groupNames.map(() => []);
|
|
94
111
|
facts.forEach((f, i) => {
|
|
95
112
|
const text = sql.slice(f.span.start, f.span.start + f.span.len).trim();
|
|
96
113
|
const g = groupOf[i];
|
|
97
114
|
if (g !== -1 && text) {
|
|
98
115
|
groupSql[g].push(text.endsWith(';') ? text : `${text};`);
|
|
116
|
+
groupStatements[g].push(f);
|
|
99
117
|
}
|
|
100
118
|
});
|
|
101
119
|
// Change dependencies = statement edges projected onto groups, plus
|
|
@@ -132,10 +150,22 @@ function restructureChanges(changes, options) {
|
|
|
132
150
|
});
|
|
133
151
|
const result = order
|
|
134
152
|
.filter(g => groupSql[g].length > 0)
|
|
135
|
-
.map(g =>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
.map(g => {
|
|
154
|
+
const revert = (0, scripts_1.revertFor)(groupStatements[g]);
|
|
155
|
+
const verify = (0, scripts_1.verifyFor)(groupStatements[g]);
|
|
156
|
+
for (const warning of revert.warnings) {
|
|
157
|
+
warnings.push(`${groupNames[g]}: ${warning}`);
|
|
158
|
+
}
|
|
159
|
+
for (const warning of verify.warnings) {
|
|
160
|
+
warnings.push(`${groupNames[g]}: ${warning}`);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
name: groupNames[g],
|
|
164
|
+
dependencies: [...groupDeps[g]].map(d => groupNames[d]).sort(),
|
|
165
|
+
deploy: groupSql[g].join('\n\n'),
|
|
166
|
+
revert: revert.sql,
|
|
167
|
+
verify: verify.sql
|
|
168
|
+
};
|
|
169
|
+
});
|
|
140
170
|
return { changes: result, warnings };
|
|
141
171
|
}
|
package/index.d.ts
CHANGED
|
@@ -2,11 +2,17 @@ export * from '@pgsql/transform';
|
|
|
2
2
|
export { loadModule } from 'plpgsql-parser';
|
|
3
3
|
export type { BundleScriptContext, ExtensionRoutingInput, NamespaceValidatorOptions, SchemaObjectRoute, SchemaTranspiler, SchemaTranspilerOptions, } from './bundle-driver';
|
|
4
4
|
export { buildExtensionRouter, buildSchemaRouter, makeNamespaceValidator, makeSchemaTranspiler, } from './bundle-driver';
|
|
5
|
-
export type { GranularityChange, RestructureModuleOptions, RestructureModuleResult, } from './granularity-driver';
|
|
5
|
+
export type { GranularityChange, RestructuredChange, RestructureModuleOptions, RestructureModuleResult, } from './granularity-driver';
|
|
6
6
|
export { defaultChangeName, restructureChanges, } from './granularity-driver';
|
|
7
|
+
export type { DiffInputChange, ObjectDelta, SemanticDeltaChange, SemanticDiffOptions, SemanticDiffResult, SemanticObjectDiff, } from './semantic-diff-driver';
|
|
8
|
+
export { diffChangeSets, diffSchemas, } from './semantic-diff-driver';
|
|
9
|
+
export type { PartitionConfig, PartitionedChange, PartitionedPackage, PartitionInputChange, PartitionRule, PartitionUnit, PartitionUnitsResult, UnitSelector, } from './partition-driver';
|
|
10
|
+
export { PartitionCycleError, partitionUnits, RESIDUAL_UNIT_PATH, } from './partition-driver';
|
|
7
11
|
export type { CategoryProfile, ChangeCategory, } from './categorize';
|
|
8
12
|
export { buildCategoryOf, categorizeChange, TIER_PROFILE, } from './categorize';
|
|
9
13
|
export type { ClosureChange, ClosureInputChange, ClosureReason, FixtureClosure, ResolveFixtureClosureOptions, } from './fixture-closure';
|
|
10
14
|
export { resolveFixtureClosure } from './fixture-closure';
|
|
15
|
+
export type { GeneratedScript, RegeneratedScripts, } from './regen';
|
|
16
|
+
export { classifyScript, isStubScript, regenerateScripts, } from './regen';
|
|
11
17
|
export type { SqlProgram, SqlStatementAst, SqlStatementSpan, } from './program';
|
|
12
18
|
export { emitSqlProgram, parseSqlProgram } from './program';
|
package/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.parseSqlProgram = exports.emitSqlProgram = exports.resolveFixtureClosure = exports.TIER_PROFILE = exports.categorizeChange = exports.buildCategoryOf = exports.restructureChanges = exports.defaultChangeName = exports.makeSchemaTranspiler = exports.makeNamespaceValidator = exports.buildSchemaRouter = exports.buildExtensionRouter = exports.loadModule = void 0;
|
|
17
|
+
exports.parseSqlProgram = exports.emitSqlProgram = exports.regenerateScripts = exports.isStubScript = exports.classifyScript = exports.resolveFixtureClosure = exports.TIER_PROFILE = exports.categorizeChange = exports.buildCategoryOf = exports.RESIDUAL_UNIT_PATH = exports.partitionUnits = exports.PartitionCycleError = exports.diffSchemas = exports.diffChangeSets = exports.restructureChanges = exports.defaultChangeName = exports.makeSchemaTranspiler = exports.makeNamespaceValidator = exports.buildSchemaRouter = exports.buildExtensionRouter = exports.loadModule = void 0;
|
|
18
18
|
__exportStar(require("@pgsql/transform"), exports);
|
|
19
19
|
var plpgsql_parser_1 = require("plpgsql-parser");
|
|
20
20
|
Object.defineProperty(exports, "loadModule", { enumerable: true, get: function () { return plpgsql_parser_1.loadModule; } });
|
|
@@ -26,12 +26,23 @@ Object.defineProperty(exports, "makeSchemaTranspiler", { enumerable: true, get:
|
|
|
26
26
|
var granularity_driver_1 = require("./granularity-driver");
|
|
27
27
|
Object.defineProperty(exports, "defaultChangeName", { enumerable: true, get: function () { return granularity_driver_1.defaultChangeName; } });
|
|
28
28
|
Object.defineProperty(exports, "restructureChanges", { enumerable: true, get: function () { return granularity_driver_1.restructureChanges; } });
|
|
29
|
+
var semantic_diff_driver_1 = require("./semantic-diff-driver");
|
|
30
|
+
Object.defineProperty(exports, "diffChangeSets", { enumerable: true, get: function () { return semantic_diff_driver_1.diffChangeSets; } });
|
|
31
|
+
Object.defineProperty(exports, "diffSchemas", { enumerable: true, get: function () { return semantic_diff_driver_1.diffSchemas; } });
|
|
32
|
+
var partition_driver_1 = require("./partition-driver");
|
|
33
|
+
Object.defineProperty(exports, "PartitionCycleError", { enumerable: true, get: function () { return partition_driver_1.PartitionCycleError; } });
|
|
34
|
+
Object.defineProperty(exports, "partitionUnits", { enumerable: true, get: function () { return partition_driver_1.partitionUnits; } });
|
|
35
|
+
Object.defineProperty(exports, "RESIDUAL_UNIT_PATH", { enumerable: true, get: function () { return partition_driver_1.RESIDUAL_UNIT_PATH; } });
|
|
29
36
|
var categorize_1 = require("./categorize");
|
|
30
37
|
Object.defineProperty(exports, "buildCategoryOf", { enumerable: true, get: function () { return categorize_1.buildCategoryOf; } });
|
|
31
38
|
Object.defineProperty(exports, "categorizeChange", { enumerable: true, get: function () { return categorize_1.categorizeChange; } });
|
|
32
39
|
Object.defineProperty(exports, "TIER_PROFILE", { enumerable: true, get: function () { return categorize_1.TIER_PROFILE; } });
|
|
33
40
|
var fixture_closure_1 = require("./fixture-closure");
|
|
34
41
|
Object.defineProperty(exports, "resolveFixtureClosure", { enumerable: true, get: function () { return fixture_closure_1.resolveFixtureClosure; } });
|
|
42
|
+
var regen_1 = require("./regen");
|
|
43
|
+
Object.defineProperty(exports, "classifyScript", { enumerable: true, get: function () { return regen_1.classifyScript; } });
|
|
44
|
+
Object.defineProperty(exports, "isStubScript", { enumerable: true, get: function () { return regen_1.isStubScript; } });
|
|
45
|
+
Object.defineProperty(exports, "regenerateScripts", { enumerable: true, get: function () { return regen_1.regenerateScripts; } });
|
|
35
46
|
var program_1 = require("./program");
|
|
36
47
|
Object.defineProperty(exports, "emitSqlProgram", { enumerable: true, get: function () { return program_1.emitSqlProgram; } });
|
|
37
48
|
Object.defineProperty(exports, "parseSqlProgram", { enumerable: true, get: function () { return program_1.parseSqlProgram; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgpmjs/transform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|
|
@@ -43,8 +43,10 @@
|
|
|
43
43
|
"makage": "^0.3.0"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@
|
|
47
|
-
"
|
|
46
|
+
"@pgpmjs/naming-spec": "^0.1.2",
|
|
47
|
+
"@pgsql/scripts": "^18.1.0",
|
|
48
|
+
"@pgsql/transform": "^18.11.0",
|
|
49
|
+
"plpgsql-parser": "^18.2.3"
|
|
48
50
|
},
|
|
49
|
-
"gitHead": "
|
|
51
|
+
"gitHead": "45cd961cf956d2d28e4d09b20db4204982c33593"
|
|
50
52
|
}
|