@pgsql/transform 18.8.0 → 18.10.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/restructure.js ADDED
@@ -0,0 +1,331 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.restructureSql = restructureSql;
4
+ exports.orderStatements = orderStatements;
5
+ /**
6
+ * Granularity restructuring: rewrite a DDL script between equivalent shapes
7
+ * without changing the schema it produces.
8
+ *
9
+ * Three levels, two directions:
10
+ *
11
+ * - `atomic` — every table is a bare `CREATE TABLE ()` followed by one
12
+ * `ALTER TABLE ADD COLUMN` per column and one `ADD CONSTRAINT` per
13
+ * constraint (the shape machine emitters produce).
14
+ * - `object` — each table is fully baked: columns and same-table constraints
15
+ * (PK / UNIQUE / CHECK / NOT NULL / DEFAULT) fold into the `CREATE TABLE`;
16
+ * cross-object statements (FKs, indexes, triggers, policies) stay separate.
17
+ * - `consolidated` — additionally inlines foreign keys into the table
18
+ * definition whenever the dependency graph proves it safe (the referenced
19
+ * table can be ordered first); FKs on cycles stay as `ALTER TABLE`.
20
+ *
21
+ * Every fold is validated against the {@link buildStatementGraph} statement
22
+ * graph: a candidate merge is rejected if it would create an ordering cycle,
23
+ * so the pass degrades gracefully to the atomic form instead of emitting an
24
+ * undeployable script. Output statements are re-emitted in the graph's
25
+ * stable topological order.
26
+ */
27
+ const plpgsql_parser_1 = require("plpgsql-parser");
28
+ const facts_1 = require("./facts");
29
+ const graph_1 = require("./graph");
30
+ const relKey = (rel) => rel ? `${rel.schemaname ?? ''}.${rel.relname}` : null;
31
+ const sameRel = (a, b) => !!a && !!b && relKey(a) === relKey(b);
32
+ /**
33
+ * Restructure a DDL script to the requested granularity. The input and
34
+ * output scripts deploy to identical schemas; only statement shape and
35
+ * order change.
36
+ */
37
+ function restructureSql(sql, options) {
38
+ const warnings = [];
39
+ const parsed = (0, plpgsql_parser_1.parseSql)(sql);
40
+ const stmts = (parsed?.stmts ?? [])
41
+ .map((s) => s?.stmt)
42
+ .filter(Boolean);
43
+ let outStmts;
44
+ let folded = 0;
45
+ let exploded = 0;
46
+ if (options.granularity === 'atomic') {
47
+ const result = explodeStatements(stmts);
48
+ outStmts = result.stmts;
49
+ exploded = result.exploded;
50
+ }
51
+ else {
52
+ const facts = (0, facts_1.classifyStatements)(sql);
53
+ const graph = (0, graph_1.buildStatementGraph)(facts);
54
+ const result = foldStatements(stmts, graph, options.granularity === 'consolidated', warnings);
55
+ outStmts = result.stmts;
56
+ folded = result.folded;
57
+ }
58
+ const ordered = orderStatements(outStmts);
59
+ const text = ordered.map(s => `${plpgsql_parser_1.Deparser.deparse(s)};`).join('\n\n');
60
+ return { sql: text, folded, exploded, warnings };
61
+ }
62
+ /**
63
+ * Re-emit statements in the stable topological order of their dependency
64
+ * graph. Statements are deparsed and re-classified so the ordering reflects
65
+ * exactly what will be emitted.
66
+ */
67
+ function orderStatements(stmts) {
68
+ if (stmts.length <= 1)
69
+ return stmts;
70
+ const script = stmts.map(s => `${plpgsql_parser_1.Deparser.deparse(s)};`).join('\n');
71
+ const graph = (0, graph_1.buildStatementGraph)((0, facts_1.classifyStatements)(script));
72
+ if (graph.order.length !== stmts.length)
73
+ return stmts;
74
+ return graph.order.map(i => stmts[i]);
75
+ }
76
+ /**
77
+ * Fold `ALTER TABLE` commands into their table's `CREATE TABLE` statement.
78
+ * `inlineFks` additionally folds `ADD FOREIGN KEY` when the graph proves the
79
+ * referenced table can be created first.
80
+ */
81
+ function foldStatements(stmts, graph, inlineFks, warnings) {
82
+ // Locate the CREATE TABLE for each relation.
83
+ const createIndex = new Map();
84
+ stmts.forEach((s, i) => {
85
+ const create = s?.CreateStmt;
86
+ if (create?.relation) {
87
+ const key = relKey(create.relation);
88
+ if (key && !createIndex.has(key))
89
+ createIndex.set(key, i);
90
+ }
91
+ });
92
+ // Fold bookkeeping is a union-find over statements: folding j into the
93
+ // CREATE at i merges their graph nodes, and the merged node's dependency
94
+ // set is the union of both. Reachability is then computed over merged
95
+ // nodes so a second fold cannot silently complete a cycle the first one
96
+ // started (the mutual-FK case).
97
+ const rep = new Map();
98
+ const find = (v) => {
99
+ let r = v;
100
+ while (rep.has(r))
101
+ r = rep.get(r);
102
+ return r;
103
+ };
104
+ // Per-statement dependencies: typed graph edges plus the implicit edge
105
+ // from every ALTER to its own table's CREATE (the facts model records the
106
+ // alter as *targeting* the table, so the graph carries no self edge).
107
+ const deps = new Map();
108
+ stmts.forEach((s, j) => {
109
+ const set = new Set();
110
+ for (const e of graph.nodes[j]?.out ?? []) {
111
+ if (e.kind !== 'late')
112
+ set.add(e.to);
113
+ }
114
+ const alterKey = relKey(s?.AlterTableStmt?.relation);
115
+ const own = alterKey ? createIndex.get(alterKey) : undefined;
116
+ if (own !== undefined && own !== j)
117
+ set.add(own);
118
+ deps.set(j, set);
119
+ });
120
+ const depsOf = (v) => [...(deps.get(find(v)) ?? [])].map(find).filter(t => t !== find(v));
121
+ const dependsOn = (from, target) => {
122
+ const goal = find(target);
123
+ const seen = new Set();
124
+ const stack = [find(from)];
125
+ while (stack.length > 0) {
126
+ const v = stack.pop();
127
+ if (v === goal)
128
+ return true;
129
+ if (seen.has(v))
130
+ continue;
131
+ seen.add(v);
132
+ stack.push(...depsOf(v));
133
+ }
134
+ return false;
135
+ };
136
+ /**
137
+ * A command at statement `j` may fold into the CREATE at statement `i`
138
+ * iff none of j's dependencies (other than i itself) transitively depend
139
+ * on i — otherwise the merged node would sit on a cycle.
140
+ */
141
+ const safeToFold = (j, i) => !depsOf(j).some(t => t !== find(i) && dependsOn(t, i));
142
+ /** Merge statement `j` into the CREATE at `i`. */
143
+ const absorb = (i, j) => {
144
+ const target = find(i);
145
+ const merged = deps.get(target) ?? new Set();
146
+ for (const t of deps.get(find(j)) ?? [])
147
+ merged.add(t);
148
+ deps.set(target, merged);
149
+ if (find(j) !== target)
150
+ rep.set(find(j), target);
151
+ };
152
+ const remove = new Set();
153
+ let folded = 0;
154
+ stmts.forEach((s, j) => {
155
+ const alter = s?.AlterTableStmt;
156
+ if (!alter || alter.objtype !== 'OBJECT_TABLE')
157
+ return;
158
+ const key = relKey(alter.relation);
159
+ const i = key ? createIndex.get(key) : undefined;
160
+ if (i === undefined || i === j)
161
+ return;
162
+ const create = stmts[i].CreateStmt;
163
+ const remaining = [];
164
+ for (const wrapped of alter.cmds ?? []) {
165
+ const cmd = wrapped?.AlterTableCmd;
166
+ if (!cmd) {
167
+ remaining.push(wrapped);
168
+ continue;
169
+ }
170
+ const constraint = cmd.def?.Constraint;
171
+ const isFk = cmd.subtype === 'AT_AddConstraint' && constraint?.contype === 'CONSTR_FOREIGN';
172
+ const selfFk = isFk && sameRel(constraint?.pktable, alter.relation);
173
+ let foldable = false;
174
+ switch (cmd.subtype) {
175
+ case 'AT_AddColumn':
176
+ foldable = !!cmd.def?.ColumnDef;
177
+ break;
178
+ case 'AT_ColumnDefault':
179
+ foldable = !!cmd.name && cmd.def !== undefined;
180
+ break;
181
+ case 'AT_SetNotNull':
182
+ foldable = !!cmd.name;
183
+ break;
184
+ case 'AT_AddConstraint':
185
+ foldable = isFk ? selfFk || inlineFks : true;
186
+ break;
187
+ default:
188
+ foldable = false;
189
+ }
190
+ if (foldable && !selfFk && !safeToFold(j, i)) {
191
+ warnings.push(`kept atomic: statement ${j} on ${key} would create an ordering cycle if folded`);
192
+ foldable = false;
193
+ }
194
+ if (!foldable) {
195
+ remaining.push(wrapped);
196
+ continue;
197
+ }
198
+ if (!applyFold(create, cmd)) {
199
+ remaining.push(wrapped);
200
+ continue;
201
+ }
202
+ absorb(i, j);
203
+ folded++;
204
+ }
205
+ if (remaining.length === 0) {
206
+ remove.add(j);
207
+ }
208
+ else {
209
+ alter.cmds = remaining;
210
+ }
211
+ });
212
+ return { stmts: stmts.filter((_, i) => !remove.has(i)), folded };
213
+ }
214
+ /** Apply one foldable ALTER TABLE command onto a CreateStmt. */
215
+ function applyFold(create, cmd) {
216
+ create.tableElts = create.tableElts ?? [];
217
+ const findColumn = (name) => create.tableElts.find((e) => e?.ColumnDef?.colname === name)?.ColumnDef;
218
+ switch (cmd.subtype) {
219
+ case 'AT_AddColumn':
220
+ create.tableElts.push({ ColumnDef: cmd.def.ColumnDef });
221
+ return true;
222
+ case 'AT_ColumnDefault': {
223
+ const col = findColumn(cmd.name);
224
+ if (!col)
225
+ return false;
226
+ col.constraints = col.constraints ?? [];
227
+ col.constraints.push({ Constraint: { contype: 'CONSTR_DEFAULT', raw_expr: cmd.def } });
228
+ return true;
229
+ }
230
+ case 'AT_SetNotNull': {
231
+ const col = findColumn(cmd.name);
232
+ if (!col)
233
+ return false;
234
+ col.constraints = col.constraints ?? [];
235
+ col.constraints.push({ Constraint: { contype: 'CONSTR_NOTNULL' } });
236
+ return true;
237
+ }
238
+ case 'AT_AddConstraint':
239
+ create.tableElts.push({ Constraint: cmd.def.Constraint });
240
+ return true;
241
+ default:
242
+ return false;
243
+ }
244
+ }
245
+ /** Column-level constraint types that stay inline on `ADD COLUMN`. */
246
+ const INLINE_COLUMN_CONSTRAINTS = new Set(['CONSTR_DEFAULT', 'CONSTR_NOTNULL', 'CONSTR_NULL', 'CONSTR_IDENTITY', 'CONSTR_GENERATED']);
247
+ /**
248
+ * Explode consolidated `CREATE TABLE` statements into the atomic shape:
249
+ * bare create, one `ADD COLUMN` per column (keeping column-local defaults /
250
+ * NOT NULL inline), one `ADD CONSTRAINT` per table-level or key constraint.
251
+ */
252
+ function explodeStatements(stmts) {
253
+ const out = [];
254
+ let exploded = 0;
255
+ const alterFor = (relation, cmd) => ({
256
+ AlterTableStmt: {
257
+ objtype: 'OBJECT_TABLE',
258
+ relation,
259
+ cmds: [{ AlterTableCmd: cmd }]
260
+ }
261
+ });
262
+ for (const s of stmts) {
263
+ const create = s?.CreateStmt;
264
+ const elts = create?.tableElts ?? [];
265
+ // Typed tables / partitions / inheritance keep their shape.
266
+ if (!create || elts.length === 0 || create.ofTypename || create.partbound || create.inhRelations) {
267
+ out.push(s);
268
+ continue;
269
+ }
270
+ const relation = create.relation;
271
+ const columns = [];
272
+ const constraints = [];
273
+ for (const elt of elts) {
274
+ if (elt?.ColumnDef)
275
+ columns.push(elt.ColumnDef);
276
+ else if (elt?.Constraint)
277
+ constraints.push(elt.Constraint);
278
+ else {
279
+ // Unknown element (e.g. LIKE clause): keep the table intact.
280
+ columns.length = 0;
281
+ constraints.length = 0;
282
+ break;
283
+ }
284
+ }
285
+ if (columns.length === 0 && constraints.length === 0) {
286
+ out.push(s);
287
+ continue;
288
+ }
289
+ out.push({ CreateStmt: { ...create, tableElts: [] } });
290
+ for (const col of columns) {
291
+ const inline = [];
292
+ for (const wrapped of col.constraints ?? []) {
293
+ const c = wrapped?.Constraint;
294
+ if (c && INLINE_COLUMN_CONSTRAINTS.has(c.contype)) {
295
+ inline.push(wrapped);
296
+ }
297
+ else if (c) {
298
+ // Promote a column constraint (PK/UNIQUE/CHECK/FK) to table level.
299
+ constraints.push(columnConstraintToTable(c, col.colname));
300
+ }
301
+ }
302
+ out.push(alterFor(relation, {
303
+ subtype: 'AT_AddColumn',
304
+ def: { ColumnDef: { ...col, constraints: inline.length > 0 ? inline : undefined } }
305
+ }));
306
+ exploded++;
307
+ }
308
+ for (const constraint of constraints) {
309
+ out.push(alterFor(relation, {
310
+ subtype: 'AT_AddConstraint',
311
+ def: { Constraint: constraint }
312
+ }));
313
+ exploded++;
314
+ }
315
+ }
316
+ return { stmts: out, exploded };
317
+ }
318
+ /** Rewrite a column-level constraint as its table-level equivalent. */
319
+ function columnConstraintToTable(constraint, colname) {
320
+ const strNode = (sval) => ({ String: { sval } });
321
+ switch (constraint.contype) {
322
+ case 'CONSTR_PRIMARY':
323
+ case 'CONSTR_UNIQUE':
324
+ return { ...constraint, keys: [strNode(colname)] };
325
+ case 'CONSTR_FOREIGN':
326
+ return { ...constraint, fk_attrs: [strNode(colname)] };
327
+ default:
328
+ // CHECK and others are valid table constraints as-is.
329
+ return constraint;
330
+ }
331
+ }