@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/esm/graph.js +184 -0
- package/esm/index.js +3 -0
- package/esm/naming.js +65 -0
- package/esm/restructure.js +327 -0
- package/graph.d.ts +83 -0
- package/graph.js +187 -0
- package/index.d.ts +6 -0
- package/index.js +8 -1
- package/naming.d.ts +43 -0
- package/naming.js +68 -0
- package/package.json +2 -2
- package/restructure.d.ts +29 -0
- package/restructure.js +331 -0
package/esm/graph.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
const keyOf = (q) => `${q.schema ?? ''}.${q.name}`;
|
|
2
|
+
/**
|
|
3
|
+
* Build the typed statement dependency graph for a classified script.
|
|
4
|
+
*
|
|
5
|
+
* Edges only exist between statements of the same script: a reference with
|
|
6
|
+
* no in-script producer is an external dependency and induces no edge (the
|
|
7
|
+
* caller decides what to do with those — pgpm expresses them as
|
|
8
|
+
* cross-package requires).
|
|
9
|
+
*/
|
|
10
|
+
export function buildStatementGraph(facts) {
|
|
11
|
+
const nodes = facts.map((f, index) => ({
|
|
12
|
+
index,
|
|
13
|
+
facts: f,
|
|
14
|
+
out: [],
|
|
15
|
+
in: []
|
|
16
|
+
}));
|
|
17
|
+
const producers = new Map();
|
|
18
|
+
facts.forEach((f, i) => {
|
|
19
|
+
for (const created of f.creates) {
|
|
20
|
+
const key = keyOf(created);
|
|
21
|
+
const list = producers.get(key) ?? [];
|
|
22
|
+
list.push(i);
|
|
23
|
+
producers.set(key, list);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
// The producer a reference binds to is the closest preceding statement
|
|
27
|
+
// that creates the object (redefinitions shadow earlier ones); when the
|
|
28
|
+
// reference precedes every producer, it binds to the first one — that is
|
|
29
|
+
// exactly the forward edge a reordering pass must satisfy.
|
|
30
|
+
const bind = (ref, from) => {
|
|
31
|
+
const list = producers.get(keyOf(ref));
|
|
32
|
+
if (!list || list.length === 0)
|
|
33
|
+
return undefined;
|
|
34
|
+
let found;
|
|
35
|
+
for (const i of list) {
|
|
36
|
+
if (i === from)
|
|
37
|
+
return undefined; // self-dependency: never an edge
|
|
38
|
+
if (i < from)
|
|
39
|
+
found = i;
|
|
40
|
+
}
|
|
41
|
+
return found ?? list[0];
|
|
42
|
+
};
|
|
43
|
+
const edges = [];
|
|
44
|
+
const addEdge = (from, to, kind, via) => {
|
|
45
|
+
if (edges.some(e => e.from === from && e.to === to && e.kind === kind))
|
|
46
|
+
return;
|
|
47
|
+
const edge = { from, to, kind, via };
|
|
48
|
+
edges.push(edge);
|
|
49
|
+
nodes[from].out.push(edge);
|
|
50
|
+
nodes[to].in.push(edge);
|
|
51
|
+
};
|
|
52
|
+
facts.forEach((f, i) => {
|
|
53
|
+
const bodyOnly = new Set(f.bodyReferences.map(keyOf));
|
|
54
|
+
const fkKeys = new Set(f.fkTargets.map(keyOf));
|
|
55
|
+
for (const ref of f.references) {
|
|
56
|
+
const to = bind(ref, i);
|
|
57
|
+
if (to === undefined)
|
|
58
|
+
continue;
|
|
59
|
+
const key = keyOf(ref);
|
|
60
|
+
const kind = fkKeys.has(key) ? 'fk' : bodyOnly.has(key) ? 'late' : 'hard';
|
|
61
|
+
addEdge(i, to, kind, ref);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
const components = condense(nodes);
|
|
65
|
+
const order = stableTopoOrder(nodes, components);
|
|
66
|
+
return { nodes, edges, producers, components, order };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Tarjan SCC over `hard` + `fk` edges (`late` edges are ignored — they are
|
|
70
|
+
* call-time bindings and legitimately cyclic). Components are returned in
|
|
71
|
+
* reverse-topological completion order and then re-sorted topologically with
|
|
72
|
+
* source-order tie-breaking by {@link stableTopoOrder}; here we only sort
|
|
73
|
+
* each component's members and order components by their smallest member so
|
|
74
|
+
* output is deterministic.
|
|
75
|
+
*/
|
|
76
|
+
function condense(nodes) {
|
|
77
|
+
const n = nodes.length;
|
|
78
|
+
const indexOf = new Array(n).fill(-1);
|
|
79
|
+
const low = new Array(n).fill(0);
|
|
80
|
+
const onStack = new Array(n).fill(false);
|
|
81
|
+
const stack = [];
|
|
82
|
+
const components = [];
|
|
83
|
+
let counter = 0;
|
|
84
|
+
const orderingEdges = (v) => nodes[v].out.filter(e => e.kind !== 'late').map(e => e.to);
|
|
85
|
+
// Iterative Tarjan (DDL scripts can be tens of thousands of statements).
|
|
86
|
+
const visit = (root) => {
|
|
87
|
+
const frames = [{ v: root, edges: orderingEdges(root), i: 0 }];
|
|
88
|
+
indexOf[root] = low[root] = counter++;
|
|
89
|
+
stack.push(root);
|
|
90
|
+
onStack[root] = true;
|
|
91
|
+
while (frames.length > 0) {
|
|
92
|
+
const frame = frames[frames.length - 1];
|
|
93
|
+
if (frame.i < frame.edges.length) {
|
|
94
|
+
const w = frame.edges[frame.i++];
|
|
95
|
+
if (indexOf[w] === -1) {
|
|
96
|
+
indexOf[w] = low[w] = counter++;
|
|
97
|
+
stack.push(w);
|
|
98
|
+
onStack[w] = true;
|
|
99
|
+
frames.push({ v: w, edges: orderingEdges(w), i: 0 });
|
|
100
|
+
}
|
|
101
|
+
else if (onStack[w]) {
|
|
102
|
+
low[frame.v] = Math.min(low[frame.v], indexOf[w]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
frames.pop();
|
|
107
|
+
if (frames.length > 0) {
|
|
108
|
+
const parent = frames[frames.length - 1];
|
|
109
|
+
low[parent.v] = Math.min(low[parent.v], low[frame.v]);
|
|
110
|
+
}
|
|
111
|
+
if (low[frame.v] === indexOf[frame.v]) {
|
|
112
|
+
const component = [];
|
|
113
|
+
for (;;) {
|
|
114
|
+
const w = stack.pop();
|
|
115
|
+
onStack[w] = false;
|
|
116
|
+
component.push(w);
|
|
117
|
+
if (w === frame.v)
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
component.sort((a, b) => a - b);
|
|
121
|
+
components.push(component);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
for (let v = 0; v < n; v++) {
|
|
127
|
+
if (indexOf[v] === -1)
|
|
128
|
+
visit(v);
|
|
129
|
+
}
|
|
130
|
+
components.sort((a, b) => a[0] - b[0]);
|
|
131
|
+
return components;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Topologically order the condensation with source order breaking ties
|
|
135
|
+
* (Kahn's algorithm over components, min-heap on smallest member index),
|
|
136
|
+
* then flatten: members of each component stay in source order.
|
|
137
|
+
*/
|
|
138
|
+
function stableTopoOrder(nodes, components) {
|
|
139
|
+
const componentOf = new Array(nodes.length).fill(0);
|
|
140
|
+
components.forEach((members, c) => {
|
|
141
|
+
for (const m of members)
|
|
142
|
+
componentOf[m] = c;
|
|
143
|
+
});
|
|
144
|
+
const succ = components.map(() => new Set());
|
|
145
|
+
const indegree = new Array(components.length).fill(0);
|
|
146
|
+
for (const node of nodes) {
|
|
147
|
+
for (const e of node.out) {
|
|
148
|
+
if (e.kind === 'late')
|
|
149
|
+
continue;
|
|
150
|
+
const from = componentOf[e.from];
|
|
151
|
+
const to = componentOf[e.to];
|
|
152
|
+
if (from === to)
|
|
153
|
+
continue;
|
|
154
|
+
// Dependency edge from → to means `to` must come first.
|
|
155
|
+
if (!succ[to].has(from)) {
|
|
156
|
+
succ[to].add(from);
|
|
157
|
+
indegree[from]++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const ready = [];
|
|
162
|
+
for (let c = 0; c < components.length; c++) {
|
|
163
|
+
if (indegree[c] === 0)
|
|
164
|
+
ready.push(c);
|
|
165
|
+
}
|
|
166
|
+
const takeMin = () => {
|
|
167
|
+
let best = 0;
|
|
168
|
+
for (let i = 1; i < ready.length; i++) {
|
|
169
|
+
if (components[ready[i]][0] < components[ready[best]][0])
|
|
170
|
+
best = i;
|
|
171
|
+
}
|
|
172
|
+
return ready.splice(best, 1)[0];
|
|
173
|
+
};
|
|
174
|
+
const order = [];
|
|
175
|
+
while (ready.length > 0) {
|
|
176
|
+
const c = takeMin();
|
|
177
|
+
order.push(...components[c]);
|
|
178
|
+
for (const next of succ[c]) {
|
|
179
|
+
if (--indegree[next] === 0)
|
|
180
|
+
ready.push(next);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return order;
|
|
184
|
+
}
|
package/esm/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { classifyStatements } from './facts';
|
|
2
|
+
export { buildStatementGraph } from './graph';
|
|
3
|
+
export { identityOf } from './naming';
|
|
4
|
+
export { orderStatements, restructureSql } from './restructure';
|
|
2
5
|
export { COMMON_EXTENSIONS, ExtensionRouter } from './extension-router';
|
|
3
6
|
export { createExtensionResult, createExtensionVisitor, transformExtensions, } from './extension-transform';
|
|
4
7
|
export { RoleRouter } from './role-router';
|
package/esm/naming.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the identity of the object a statement primarily creates or
|
|
3
|
+
* targets, or `null` when the statement creates nothing (grants, comments —
|
|
4
|
+
* such statements ride with the change of the object they attach to).
|
|
5
|
+
*
|
|
6
|
+
* Table-scoped kinds are recovered from the classifier's table-qualified
|
|
7
|
+
* names (`table.trigger`) and, for indexes and constraints, from the
|
|
8
|
+
* targeted relation.
|
|
9
|
+
*/
|
|
10
|
+
export function identityOf(facts) {
|
|
11
|
+
if (facts.kind === 'extension' && facts.extension) {
|
|
12
|
+
return { kind: 'extension', schema: null, name: facts.extension.name };
|
|
13
|
+
}
|
|
14
|
+
const created = facts.creates[0];
|
|
15
|
+
if (!created)
|
|
16
|
+
return null;
|
|
17
|
+
switch (facts.kind) {
|
|
18
|
+
case 'schema':
|
|
19
|
+
return { kind: 'schema', schema: null, name: created.name };
|
|
20
|
+
case 'trigger':
|
|
21
|
+
case 'policy': {
|
|
22
|
+
const dot = created.name.indexOf('.');
|
|
23
|
+
if (dot > 0) {
|
|
24
|
+
return {
|
|
25
|
+
kind: facts.kind,
|
|
26
|
+
schema: created.schema,
|
|
27
|
+
name: created.name.slice(dot + 1),
|
|
28
|
+
table: created.name.slice(0, dot)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { kind: facts.kind, schema: created.schema, name: created.name };
|
|
32
|
+
}
|
|
33
|
+
case 'index': {
|
|
34
|
+
// IndexStmt records the index name in creates and the indexed relation
|
|
35
|
+
// in references (same-schema RangeVar).
|
|
36
|
+
const rel = facts.references.find(r => r.schema === created.schema) ?? facts.references[0];
|
|
37
|
+
return {
|
|
38
|
+
kind: 'index',
|
|
39
|
+
schema: created.schema,
|
|
40
|
+
name: created.name,
|
|
41
|
+
table: rel?.name
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
case 'fk_constraint':
|
|
45
|
+
case 'constraint':
|
|
46
|
+
case 'rls_enable':
|
|
47
|
+
// ALTER TABLE statements target their table.
|
|
48
|
+
return { kind: 'constraint', schema: created.schema, name: created.name, table: created.name };
|
|
49
|
+
case 'seed_dml':
|
|
50
|
+
return { kind: 'seed_dml', schema: created.schema, name: created.name, table: created.name };
|
|
51
|
+
case 'table':
|
|
52
|
+
// AlterTableStmt facts also classify as `table`-targeting; the created
|
|
53
|
+
// name is the table either way.
|
|
54
|
+
return { kind: 'table', schema: created.schema, name: created.name };
|
|
55
|
+
case 'view':
|
|
56
|
+
case 'function':
|
|
57
|
+
case 'type':
|
|
58
|
+
return { kind: facts.kind, schema: created.schema, name: created.name };
|
|
59
|
+
default:
|
|
60
|
+
if (facts.nodeTag === 'CreateSeqStmt') {
|
|
61
|
+
return { kind: 'sequence', schema: created.schema, name: created.name };
|
|
62
|
+
}
|
|
63
|
+
return { kind: 'other', schema: created.schema, name: created.name };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Granularity restructuring: rewrite a DDL script between equivalent shapes
|
|
3
|
+
* without changing the schema it produces.
|
|
4
|
+
*
|
|
5
|
+
* Three levels, two directions:
|
|
6
|
+
*
|
|
7
|
+
* - `atomic` — every table is a bare `CREATE TABLE ()` followed by one
|
|
8
|
+
* `ALTER TABLE ADD COLUMN` per column and one `ADD CONSTRAINT` per
|
|
9
|
+
* constraint (the shape machine emitters produce).
|
|
10
|
+
* - `object` — each table is fully baked: columns and same-table constraints
|
|
11
|
+
* (PK / UNIQUE / CHECK / NOT NULL / DEFAULT) fold into the `CREATE TABLE`;
|
|
12
|
+
* cross-object statements (FKs, indexes, triggers, policies) stay separate.
|
|
13
|
+
* - `consolidated` — additionally inlines foreign keys into the table
|
|
14
|
+
* definition whenever the dependency graph proves it safe (the referenced
|
|
15
|
+
* table can be ordered first); FKs on cycles stay as `ALTER TABLE`.
|
|
16
|
+
*
|
|
17
|
+
* Every fold is validated against the {@link buildStatementGraph} statement
|
|
18
|
+
* graph: a candidate merge is rejected if it would create an ordering cycle,
|
|
19
|
+
* so the pass degrades gracefully to the atomic form instead of emitting an
|
|
20
|
+
* undeployable script. Output statements are re-emitted in the graph's
|
|
21
|
+
* stable topological order.
|
|
22
|
+
*/
|
|
23
|
+
import { Deparser, parseSql } from 'plpgsql-parser';
|
|
24
|
+
import { classifyStatements } from './facts';
|
|
25
|
+
import { buildStatementGraph } from './graph';
|
|
26
|
+
const relKey = (rel) => rel ? `${rel.schemaname ?? ''}.${rel.relname}` : null;
|
|
27
|
+
const sameRel = (a, b) => !!a && !!b && relKey(a) === relKey(b);
|
|
28
|
+
/**
|
|
29
|
+
* Restructure a DDL script to the requested granularity. The input and
|
|
30
|
+
* output scripts deploy to identical schemas; only statement shape and
|
|
31
|
+
* order change.
|
|
32
|
+
*/
|
|
33
|
+
export function restructureSql(sql, options) {
|
|
34
|
+
const warnings = [];
|
|
35
|
+
const parsed = parseSql(sql);
|
|
36
|
+
const stmts = (parsed?.stmts ?? [])
|
|
37
|
+
.map((s) => s?.stmt)
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
let outStmts;
|
|
40
|
+
let folded = 0;
|
|
41
|
+
let exploded = 0;
|
|
42
|
+
if (options.granularity === 'atomic') {
|
|
43
|
+
const result = explodeStatements(stmts);
|
|
44
|
+
outStmts = result.stmts;
|
|
45
|
+
exploded = result.exploded;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const facts = classifyStatements(sql);
|
|
49
|
+
const graph = buildStatementGraph(facts);
|
|
50
|
+
const result = foldStatements(stmts, graph, options.granularity === 'consolidated', warnings);
|
|
51
|
+
outStmts = result.stmts;
|
|
52
|
+
folded = result.folded;
|
|
53
|
+
}
|
|
54
|
+
const ordered = orderStatements(outStmts);
|
|
55
|
+
const text = ordered.map(s => `${Deparser.deparse(s)};`).join('\n\n');
|
|
56
|
+
return { sql: text, folded, exploded, warnings };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Re-emit statements in the stable topological order of their dependency
|
|
60
|
+
* graph. Statements are deparsed and re-classified so the ordering reflects
|
|
61
|
+
* exactly what will be emitted.
|
|
62
|
+
*/
|
|
63
|
+
export function orderStatements(stmts) {
|
|
64
|
+
if (stmts.length <= 1)
|
|
65
|
+
return stmts;
|
|
66
|
+
const script = stmts.map(s => `${Deparser.deparse(s)};`).join('\n');
|
|
67
|
+
const graph = buildStatementGraph(classifyStatements(script));
|
|
68
|
+
if (graph.order.length !== stmts.length)
|
|
69
|
+
return stmts;
|
|
70
|
+
return graph.order.map(i => stmts[i]);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fold `ALTER TABLE` commands into their table's `CREATE TABLE` statement.
|
|
74
|
+
* `inlineFks` additionally folds `ADD FOREIGN KEY` when the graph proves the
|
|
75
|
+
* referenced table can be created first.
|
|
76
|
+
*/
|
|
77
|
+
function foldStatements(stmts, graph, inlineFks, warnings) {
|
|
78
|
+
// Locate the CREATE TABLE for each relation.
|
|
79
|
+
const createIndex = new Map();
|
|
80
|
+
stmts.forEach((s, i) => {
|
|
81
|
+
const create = s?.CreateStmt;
|
|
82
|
+
if (create?.relation) {
|
|
83
|
+
const key = relKey(create.relation);
|
|
84
|
+
if (key && !createIndex.has(key))
|
|
85
|
+
createIndex.set(key, i);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// Fold bookkeeping is a union-find over statements: folding j into the
|
|
89
|
+
// CREATE at i merges their graph nodes, and the merged node's dependency
|
|
90
|
+
// set is the union of both. Reachability is then computed over merged
|
|
91
|
+
// nodes so a second fold cannot silently complete a cycle the first one
|
|
92
|
+
// started (the mutual-FK case).
|
|
93
|
+
const rep = new Map();
|
|
94
|
+
const find = (v) => {
|
|
95
|
+
let r = v;
|
|
96
|
+
while (rep.has(r))
|
|
97
|
+
r = rep.get(r);
|
|
98
|
+
return r;
|
|
99
|
+
};
|
|
100
|
+
// Per-statement dependencies: typed graph edges plus the implicit edge
|
|
101
|
+
// from every ALTER to its own table's CREATE (the facts model records the
|
|
102
|
+
// alter as *targeting* the table, so the graph carries no self edge).
|
|
103
|
+
const deps = new Map();
|
|
104
|
+
stmts.forEach((s, j) => {
|
|
105
|
+
const set = new Set();
|
|
106
|
+
for (const e of graph.nodes[j]?.out ?? []) {
|
|
107
|
+
if (e.kind !== 'late')
|
|
108
|
+
set.add(e.to);
|
|
109
|
+
}
|
|
110
|
+
const alterKey = relKey(s?.AlterTableStmt?.relation);
|
|
111
|
+
const own = alterKey ? createIndex.get(alterKey) : undefined;
|
|
112
|
+
if (own !== undefined && own !== j)
|
|
113
|
+
set.add(own);
|
|
114
|
+
deps.set(j, set);
|
|
115
|
+
});
|
|
116
|
+
const depsOf = (v) => [...(deps.get(find(v)) ?? [])].map(find).filter(t => t !== find(v));
|
|
117
|
+
const dependsOn = (from, target) => {
|
|
118
|
+
const goal = find(target);
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const stack = [find(from)];
|
|
121
|
+
while (stack.length > 0) {
|
|
122
|
+
const v = stack.pop();
|
|
123
|
+
if (v === goal)
|
|
124
|
+
return true;
|
|
125
|
+
if (seen.has(v))
|
|
126
|
+
continue;
|
|
127
|
+
seen.add(v);
|
|
128
|
+
stack.push(...depsOf(v));
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* A command at statement `j` may fold into the CREATE at statement `i`
|
|
134
|
+
* iff none of j's dependencies (other than i itself) transitively depend
|
|
135
|
+
* on i — otherwise the merged node would sit on a cycle.
|
|
136
|
+
*/
|
|
137
|
+
const safeToFold = (j, i) => !depsOf(j).some(t => t !== find(i) && dependsOn(t, i));
|
|
138
|
+
/** Merge statement `j` into the CREATE at `i`. */
|
|
139
|
+
const absorb = (i, j) => {
|
|
140
|
+
const target = find(i);
|
|
141
|
+
const merged = deps.get(target) ?? new Set();
|
|
142
|
+
for (const t of deps.get(find(j)) ?? [])
|
|
143
|
+
merged.add(t);
|
|
144
|
+
deps.set(target, merged);
|
|
145
|
+
if (find(j) !== target)
|
|
146
|
+
rep.set(find(j), target);
|
|
147
|
+
};
|
|
148
|
+
const remove = new Set();
|
|
149
|
+
let folded = 0;
|
|
150
|
+
stmts.forEach((s, j) => {
|
|
151
|
+
const alter = s?.AlterTableStmt;
|
|
152
|
+
if (!alter || alter.objtype !== 'OBJECT_TABLE')
|
|
153
|
+
return;
|
|
154
|
+
const key = relKey(alter.relation);
|
|
155
|
+
const i = key ? createIndex.get(key) : undefined;
|
|
156
|
+
if (i === undefined || i === j)
|
|
157
|
+
return;
|
|
158
|
+
const create = stmts[i].CreateStmt;
|
|
159
|
+
const remaining = [];
|
|
160
|
+
for (const wrapped of alter.cmds ?? []) {
|
|
161
|
+
const cmd = wrapped?.AlterTableCmd;
|
|
162
|
+
if (!cmd) {
|
|
163
|
+
remaining.push(wrapped);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const constraint = cmd.def?.Constraint;
|
|
167
|
+
const isFk = cmd.subtype === 'AT_AddConstraint' && constraint?.contype === 'CONSTR_FOREIGN';
|
|
168
|
+
const selfFk = isFk && sameRel(constraint?.pktable, alter.relation);
|
|
169
|
+
let foldable = false;
|
|
170
|
+
switch (cmd.subtype) {
|
|
171
|
+
case 'AT_AddColumn':
|
|
172
|
+
foldable = !!cmd.def?.ColumnDef;
|
|
173
|
+
break;
|
|
174
|
+
case 'AT_ColumnDefault':
|
|
175
|
+
foldable = !!cmd.name && cmd.def !== undefined;
|
|
176
|
+
break;
|
|
177
|
+
case 'AT_SetNotNull':
|
|
178
|
+
foldable = !!cmd.name;
|
|
179
|
+
break;
|
|
180
|
+
case 'AT_AddConstraint':
|
|
181
|
+
foldable = isFk ? selfFk || inlineFks : true;
|
|
182
|
+
break;
|
|
183
|
+
default:
|
|
184
|
+
foldable = false;
|
|
185
|
+
}
|
|
186
|
+
if (foldable && !selfFk && !safeToFold(j, i)) {
|
|
187
|
+
warnings.push(`kept atomic: statement ${j} on ${key} would create an ordering cycle if folded`);
|
|
188
|
+
foldable = false;
|
|
189
|
+
}
|
|
190
|
+
if (!foldable) {
|
|
191
|
+
remaining.push(wrapped);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!applyFold(create, cmd)) {
|
|
195
|
+
remaining.push(wrapped);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
absorb(i, j);
|
|
199
|
+
folded++;
|
|
200
|
+
}
|
|
201
|
+
if (remaining.length === 0) {
|
|
202
|
+
remove.add(j);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
alter.cmds = remaining;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return { stmts: stmts.filter((_, i) => !remove.has(i)), folded };
|
|
209
|
+
}
|
|
210
|
+
/** Apply one foldable ALTER TABLE command onto a CreateStmt. */
|
|
211
|
+
function applyFold(create, cmd) {
|
|
212
|
+
create.tableElts = create.tableElts ?? [];
|
|
213
|
+
const findColumn = (name) => create.tableElts.find((e) => e?.ColumnDef?.colname === name)?.ColumnDef;
|
|
214
|
+
switch (cmd.subtype) {
|
|
215
|
+
case 'AT_AddColumn':
|
|
216
|
+
create.tableElts.push({ ColumnDef: cmd.def.ColumnDef });
|
|
217
|
+
return true;
|
|
218
|
+
case 'AT_ColumnDefault': {
|
|
219
|
+
const col = findColumn(cmd.name);
|
|
220
|
+
if (!col)
|
|
221
|
+
return false;
|
|
222
|
+
col.constraints = col.constraints ?? [];
|
|
223
|
+
col.constraints.push({ Constraint: { contype: 'CONSTR_DEFAULT', raw_expr: cmd.def } });
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
case 'AT_SetNotNull': {
|
|
227
|
+
const col = findColumn(cmd.name);
|
|
228
|
+
if (!col)
|
|
229
|
+
return false;
|
|
230
|
+
col.constraints = col.constraints ?? [];
|
|
231
|
+
col.constraints.push({ Constraint: { contype: 'CONSTR_NOTNULL' } });
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
case 'AT_AddConstraint':
|
|
235
|
+
create.tableElts.push({ Constraint: cmd.def.Constraint });
|
|
236
|
+
return true;
|
|
237
|
+
default:
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Column-level constraint types that stay inline on `ADD COLUMN`. */
|
|
242
|
+
const INLINE_COLUMN_CONSTRAINTS = new Set(['CONSTR_DEFAULT', 'CONSTR_NOTNULL', 'CONSTR_NULL', 'CONSTR_IDENTITY', 'CONSTR_GENERATED']);
|
|
243
|
+
/**
|
|
244
|
+
* Explode consolidated `CREATE TABLE` statements into the atomic shape:
|
|
245
|
+
* bare create, one `ADD COLUMN` per column (keeping column-local defaults /
|
|
246
|
+
* NOT NULL inline), one `ADD CONSTRAINT` per table-level or key constraint.
|
|
247
|
+
*/
|
|
248
|
+
function explodeStatements(stmts) {
|
|
249
|
+
const out = [];
|
|
250
|
+
let exploded = 0;
|
|
251
|
+
const alterFor = (relation, cmd) => ({
|
|
252
|
+
AlterTableStmt: {
|
|
253
|
+
objtype: 'OBJECT_TABLE',
|
|
254
|
+
relation,
|
|
255
|
+
cmds: [{ AlterTableCmd: cmd }]
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
for (const s of stmts) {
|
|
259
|
+
const create = s?.CreateStmt;
|
|
260
|
+
const elts = create?.tableElts ?? [];
|
|
261
|
+
// Typed tables / partitions / inheritance keep their shape.
|
|
262
|
+
if (!create || elts.length === 0 || create.ofTypename || create.partbound || create.inhRelations) {
|
|
263
|
+
out.push(s);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const relation = create.relation;
|
|
267
|
+
const columns = [];
|
|
268
|
+
const constraints = [];
|
|
269
|
+
for (const elt of elts) {
|
|
270
|
+
if (elt?.ColumnDef)
|
|
271
|
+
columns.push(elt.ColumnDef);
|
|
272
|
+
else if (elt?.Constraint)
|
|
273
|
+
constraints.push(elt.Constraint);
|
|
274
|
+
else {
|
|
275
|
+
// Unknown element (e.g. LIKE clause): keep the table intact.
|
|
276
|
+
columns.length = 0;
|
|
277
|
+
constraints.length = 0;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (columns.length === 0 && constraints.length === 0) {
|
|
282
|
+
out.push(s);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
out.push({ CreateStmt: { ...create, tableElts: [] } });
|
|
286
|
+
for (const col of columns) {
|
|
287
|
+
const inline = [];
|
|
288
|
+
for (const wrapped of col.constraints ?? []) {
|
|
289
|
+
const c = wrapped?.Constraint;
|
|
290
|
+
if (c && INLINE_COLUMN_CONSTRAINTS.has(c.contype)) {
|
|
291
|
+
inline.push(wrapped);
|
|
292
|
+
}
|
|
293
|
+
else if (c) {
|
|
294
|
+
// Promote a column constraint (PK/UNIQUE/CHECK/FK) to table level.
|
|
295
|
+
constraints.push(columnConstraintToTable(c, col.colname));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
out.push(alterFor(relation, {
|
|
299
|
+
subtype: 'AT_AddColumn',
|
|
300
|
+
def: { ColumnDef: { ...col, constraints: inline.length > 0 ? inline : undefined } }
|
|
301
|
+
}));
|
|
302
|
+
exploded++;
|
|
303
|
+
}
|
|
304
|
+
for (const constraint of constraints) {
|
|
305
|
+
out.push(alterFor(relation, {
|
|
306
|
+
subtype: 'AT_AddConstraint',
|
|
307
|
+
def: { Constraint: constraint }
|
|
308
|
+
}));
|
|
309
|
+
exploded++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return { stmts: out, exploded };
|
|
313
|
+
}
|
|
314
|
+
/** Rewrite a column-level constraint as its table-level equivalent. */
|
|
315
|
+
function columnConstraintToTable(constraint, colname) {
|
|
316
|
+
const strNode = (sval) => ({ String: { sval } });
|
|
317
|
+
switch (constraint.contype) {
|
|
318
|
+
case 'CONSTR_PRIMARY':
|
|
319
|
+
case 'CONSTR_UNIQUE':
|
|
320
|
+
return { ...constraint, keys: [strNode(colname)] };
|
|
321
|
+
case 'CONSTR_FOREIGN':
|
|
322
|
+
return { ...constraint, fk_attrs: [strNode(colname)] };
|
|
323
|
+
default:
|
|
324
|
+
// CHECK and others are valid table constraints as-is.
|
|
325
|
+
return constraint;
|
|
326
|
+
}
|
|
327
|
+
}
|
package/graph.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Statement-level dependency graph over classified SQL.
|
|
3
|
+
*
|
|
4
|
+
* {@link classifyStatements} reduces each top-level statement to
|
|
5
|
+
* {@link StatementFacts} — what it creates, what it references, which of
|
|
6
|
+
* those references live only inside PL/pgSQL bodies. This module turns a
|
|
7
|
+
* sequence of facts into a typed dependency graph and computes the orderings
|
|
8
|
+
* the granularity passes (consolidate / atomize) need:
|
|
9
|
+
*
|
|
10
|
+
* - **typed edges**: `hard` edges (the referenced object must exist at
|
|
11
|
+
* CREATE time), `fk` edges (foreign-key targets — hard, but singled out
|
|
12
|
+
* because they are the edges FK inlining must respect), and `late` edges
|
|
13
|
+
* (PL/pgSQL body references, resolved at call time — they never constrain
|
|
14
|
+
* deploy order and may legitimately form cycles).
|
|
15
|
+
* - **SCC condensation**: strongly connected components over hard+fk edges.
|
|
16
|
+
* A well-formed DDL script condenses to singleton components; any larger
|
|
17
|
+
* component is a genuine ordering cycle (e.g. mutually referencing FKs)
|
|
18
|
+
* that granularity passes must leave in atomic form.
|
|
19
|
+
* - **topological order** of the condensation, stable with respect to the
|
|
20
|
+
* original statement order (ties keep source order), so re-emission is
|
|
21
|
+
* deterministic and minimally surprising.
|
|
22
|
+
*/
|
|
23
|
+
import { QualifiedName, StatementFacts } from './facts';
|
|
24
|
+
/**
|
|
25
|
+
* How a dependency edge constrains ordering.
|
|
26
|
+
*
|
|
27
|
+
* - `hard` — name must resolve when the dependent statement executes.
|
|
28
|
+
* - `fk` — a hard edge arising from a foreign-key target; distinguished so
|
|
29
|
+
* folding passes can decide whether an FK may be inlined into its table.
|
|
30
|
+
* - `late` — reached only inside a PL/pgSQL body; resolved at call time,
|
|
31
|
+
* so it does not constrain deploy order.
|
|
32
|
+
*/
|
|
33
|
+
export type EdgeKind = 'hard' | 'fk' | 'late';
|
|
34
|
+
/** A directed dependency: statement `from` depends on statement `to`. */
|
|
35
|
+
export interface StatementEdge {
|
|
36
|
+
from: number;
|
|
37
|
+
to: number;
|
|
38
|
+
kind: EdgeKind;
|
|
39
|
+
/** The referenced object that induced this edge. */
|
|
40
|
+
via: QualifiedName;
|
|
41
|
+
}
|
|
42
|
+
/** A node in the statement graph: one top-level statement. */
|
|
43
|
+
export interface StatementNode {
|
|
44
|
+
/** Index of the statement in the classified script. */
|
|
45
|
+
index: number;
|
|
46
|
+
facts: StatementFacts;
|
|
47
|
+
/** Outgoing edges (this statement's dependencies). */
|
|
48
|
+
out: StatementEdge[];
|
|
49
|
+
/** Incoming edges (statements that depend on this one). */
|
|
50
|
+
in: StatementEdge[];
|
|
51
|
+
}
|
|
52
|
+
/** The statement-level dependency graph for one SQL script. */
|
|
53
|
+
export interface StatementGraph {
|
|
54
|
+
nodes: StatementNode[];
|
|
55
|
+
edges: StatementEdge[];
|
|
56
|
+
/**
|
|
57
|
+
* `schema.name` → indices of the statements that create that object.
|
|
58
|
+
* Trigger and policy names are table-qualified (`table.trigger`), matching
|
|
59
|
+
* {@link StatementFacts.creates}.
|
|
60
|
+
*/
|
|
61
|
+
producers: Map<string, number[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Strongly connected components over `hard` + `fk` edges, in topological
|
|
64
|
+
* order of the condensation. Singleton components are the common case;
|
|
65
|
+
* larger ones are genuine DDL ordering cycles.
|
|
66
|
+
*/
|
|
67
|
+
components: number[][];
|
|
68
|
+
/**
|
|
69
|
+
* A stable topological order of all statements: components in condensation
|
|
70
|
+
* order, members of a component in source order. Ties between independent
|
|
71
|
+
* components keep source order.
|
|
72
|
+
*/
|
|
73
|
+
order: number[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build the typed statement dependency graph for a classified script.
|
|
77
|
+
*
|
|
78
|
+
* Edges only exist between statements of the same script: a reference with
|
|
79
|
+
* no in-script producer is an external dependency and induces no edge (the
|
|
80
|
+
* caller decides what to do with those — pgpm expresses them as
|
|
81
|
+
* cross-package requires).
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildStatementGraph(facts: StatementFacts[]): StatementGraph;
|