@pgpmjs/transform 0.19.0 → 0.20.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/constraint-names.d.ts +36 -0
- package/constraint-names.js +75 -0
- package/esm/constraint-names.js +71 -0
- package/esm/granularity-driver.js +84 -21
- package/esm/index.js +4 -2
- package/esm/module-emit.js +71 -1
- package/esm/restructure.js +6 -1
- package/esm/semantic-diff-driver.js +97 -6
- package/esm/sub-object.js +80 -0
- package/granularity-driver.d.ts +57 -0
- package/granularity-driver.js +86 -21
- package/index.d.ts +7 -4
- package/index.js +10 -1
- package/module-emit.d.ts +26 -0
- package/module-emit.js +72 -1
- package/package.json +3 -3
- package/restructure.d.ts +7 -0
- package/restructure.js +6 -1
- package/semantic-diff-driver.d.ts +3 -1
- package/semantic-diff-driver.js +97 -6
- package/sub-object.d.ts +31 -0
- package/sub-object.js +84 -0
package/semantic-diff-driver.js
CHANGED
|
@@ -34,6 +34,7 @@ const naming_spec_1 = require("@pgpmjs/naming-spec");
|
|
|
34
34
|
const scripts_1 = require("@pgsql/scripts");
|
|
35
35
|
const transform_1 = require("@pgsql/transform");
|
|
36
36
|
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
37
|
+
const constraint_names_1 = require("./constraint-names");
|
|
37
38
|
const granularity_driver_1 = require("./granularity-driver");
|
|
38
39
|
const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table ?? ''}\u0000${id.name}`;
|
|
39
40
|
/**
|
|
@@ -103,7 +104,7 @@ function groupingIdentity(f) {
|
|
|
103
104
|
const identity = (0, transform_1.identityOf)(f);
|
|
104
105
|
if (!identity)
|
|
105
106
|
return null;
|
|
106
|
-
if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'rls_enable') &&
|
|
107
|
+
if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'fk_constraint' || f.kind === 'rls_enable') &&
|
|
107
108
|
identity.kind !== 'table') {
|
|
108
109
|
return { kind: 'table', schema: identity.schema, name: identity.table ?? identity.name };
|
|
109
110
|
}
|
|
@@ -116,21 +117,61 @@ function fingerprint(text) {
|
|
|
116
117
|
}
|
|
117
118
|
/** Multiset fingerprint of a whole unit. */
|
|
118
119
|
const unitFingerprint = (unit) => unit.texts.map(fingerprint).sort().join('\n');
|
|
120
|
+
/** JSON with recursively sorted object keys — an insertion-order-proof fingerprint. */
|
|
121
|
+
function stablePrint(node) {
|
|
122
|
+
if (Array.isArray(node))
|
|
123
|
+
return `[${node.map(stablePrint).join(',')}]`;
|
|
124
|
+
if (node && typeof node === 'object') {
|
|
125
|
+
const entries = Object.entries(node)
|
|
126
|
+
.filter(([, v]) => v !== undefined)
|
|
127
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
128
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${stablePrint(v)}`);
|
|
129
|
+
return `{${entries.join(',')}}`;
|
|
130
|
+
}
|
|
131
|
+
return JSON.stringify(node);
|
|
132
|
+
}
|
|
133
|
+
/** Constraint kinds that can move between column, table-elt, and ALTER form. */
|
|
134
|
+
const RELOCATABLE = new Set(['CONSTR_PRIMARY', 'CONSTR_UNIQUE', 'CONSTR_FOREIGN', 'CONSTR_CHECK']);
|
|
119
135
|
/**
|
|
120
136
|
* Fold a table unit's statements (CREATE TABLE plus any ALTER TABLE
|
|
121
137
|
* ADD COLUMN / ADD CONSTRAINT) into an effective shape, so atomic and
|
|
122
138
|
* consolidated authorships of the same table compare equal.
|
|
139
|
+
*
|
|
140
|
+
* Constraint placement is representation, not semantics: `id bigint PRIMARY
|
|
141
|
+
* KEY`, a `PRIMARY KEY (id)` table elt, and `ALTER TABLE .. ADD CONSTRAINT
|
|
142
|
+
* t_pkey PRIMARY KEY (id)` all catalog identically. Relocatable constraints
|
|
143
|
+
* (PK/UNIQUE/FK/CHECK) are therefore lifted out of columns and ALTER
|
|
144
|
+
* commands into one canonical table-level set — keyed with the Postgres
|
|
145
|
+
* default name when unnamed, columns filled in from the owning column when
|
|
146
|
+
* column-attached — and PK columns gain the NOT NULL the catalog implies.
|
|
123
147
|
*/
|
|
124
148
|
function tableShape(unit) {
|
|
125
149
|
const shape = { relation: null, columns: new Map(), extras: new Map() };
|
|
150
|
+
const table = unit.identity.name;
|
|
151
|
+
const rawColumns = [];
|
|
152
|
+
const constraints = [];
|
|
153
|
+
const pkColumns = new Set();
|
|
126
154
|
const addColumn = (node) => {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
155
|
+
const colname = node.colname ?? '';
|
|
156
|
+
const attached = node.constraints ?? [];
|
|
157
|
+
const residual = [];
|
|
158
|
+
for (const item of attached) {
|
|
159
|
+
const c = item.Constraint;
|
|
160
|
+
if (c && RELOCATABLE.has(c.contype ?? '')) {
|
|
161
|
+
constraints.push({ node: c, column: colname });
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
residual.push(item);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
rawColumns.push({ colname, def: { ...node, constraints: residual } });
|
|
168
|
+
};
|
|
169
|
+
const addConstraint = (node, column) => {
|
|
170
|
+
constraints.push({ node: node, column });
|
|
130
171
|
};
|
|
131
172
|
const addExtra = (node) => {
|
|
132
173
|
const clean = (0, transform_1.cleanTree)(node);
|
|
133
|
-
shape.extras.set(
|
|
174
|
+
shape.extras.set(stablePrint(clean), clean);
|
|
134
175
|
};
|
|
135
176
|
for (const text of unit.texts) {
|
|
136
177
|
for (const raw of (0, plpgsql_parser_1.parseSync)(text).sql.stmts) {
|
|
@@ -141,6 +182,9 @@ function tableShape(unit) {
|
|
|
141
182
|
for (const elt of elts) {
|
|
142
183
|
if (elt.ColumnDef)
|
|
143
184
|
addColumn(elt.ColumnDef);
|
|
185
|
+
else if (elt.Constraint && RELOCATABLE.has(elt.Constraint.contype ?? '')) {
|
|
186
|
+
addConstraint(elt.Constraint);
|
|
187
|
+
}
|
|
144
188
|
else
|
|
145
189
|
addExtra(elt);
|
|
146
190
|
}
|
|
@@ -156,6 +200,11 @@ function tableShape(unit) {
|
|
|
156
200
|
if (at.subtype === 'AT_AddColumn' && def?.ColumnDef) {
|
|
157
201
|
addColumn(def.ColumnDef);
|
|
158
202
|
}
|
|
203
|
+
else if (at.subtype === 'AT_AddConstraint' &&
|
|
204
|
+
def?.Constraint &&
|
|
205
|
+
RELOCATABLE.has(def.Constraint.contype ?? '')) {
|
|
206
|
+
addConstraint(def.Constraint);
|
|
207
|
+
}
|
|
159
208
|
else {
|
|
160
209
|
addExtra({ AlterTableCmd: at });
|
|
161
210
|
}
|
|
@@ -166,6 +215,44 @@ function tableShape(unit) {
|
|
|
166
215
|
}
|
|
167
216
|
}
|
|
168
217
|
}
|
|
218
|
+
for (const { node, column } of constraints) {
|
|
219
|
+
const canonical = { ...node };
|
|
220
|
+
canonical.conname = node.conname ?? (0, constraint_names_1.defaultConstraintName)(table, node, column) ?? undefined;
|
|
221
|
+
if (column && (node.keys ?? []).length === 0 && node.contype !== 'CONSTR_FOREIGN' && node.contype !== 'CONSTR_CHECK') {
|
|
222
|
+
canonical.keys = [{ String: { sval: column } }];
|
|
223
|
+
}
|
|
224
|
+
if (column && node.contype === 'CONSTR_FOREIGN' && (node.fk_attrs ?? []).length === 0) {
|
|
225
|
+
canonical.fk_attrs = [{ String: { sval: column } }];
|
|
226
|
+
}
|
|
227
|
+
if (node.contype === 'CONSTR_PRIMARY') {
|
|
228
|
+
const keyed = canonical.keys ?? [];
|
|
229
|
+
for (const k of keyed)
|
|
230
|
+
if (k.String?.sval)
|
|
231
|
+
pkColumns.add(k.String.sval);
|
|
232
|
+
}
|
|
233
|
+
addExtra({ Constraint: canonical });
|
|
234
|
+
}
|
|
235
|
+
for (const { colname, def } of rawColumns) {
|
|
236
|
+
const residual = (def.constraints ?? []).map(c => {
|
|
237
|
+
const constraint = c.Constraint;
|
|
238
|
+
// NOT NULL parses with parser-version-dependent extras (is_enforced,
|
|
239
|
+
// initially_valid); reduce to its semantic core so an authored NOT NULL
|
|
240
|
+
// equals the one a primary key implies.
|
|
241
|
+
if (constraint?.contype === 'CONSTR_NOTNULL') {
|
|
242
|
+
return { Constraint: { contype: 'CONSTR_NOTNULL', conname: constraint.conname } };
|
|
243
|
+
}
|
|
244
|
+
return c;
|
|
245
|
+
});
|
|
246
|
+
const hasNotNull = residual.some(c => c.Constraint?.contype === 'CONSTR_NOTNULL');
|
|
247
|
+
if (pkColumns.has(colname) && !hasNotNull) {
|
|
248
|
+
residual.push({ Constraint: { contype: 'CONSTR_NOTNULL' } });
|
|
249
|
+
}
|
|
250
|
+
const sorted = residual
|
|
251
|
+
.map(c => (0, transform_1.cleanTree)(c))
|
|
252
|
+
.sort((x, y) => stablePrint(x).localeCompare(stablePrint(y)));
|
|
253
|
+
const clean = (0, transform_1.cleanTree)({ ...def, constraints: sorted });
|
|
254
|
+
shape.columns.set(colname, { def: clean, print: stablePrint(clean) });
|
|
255
|
+
}
|
|
169
256
|
return shape;
|
|
170
257
|
}
|
|
171
258
|
const deparseStmt = (node) => `${plpgsql_parser_1.Deparser.deparse(node, { pretty: false })};`;
|
|
@@ -389,7 +476,11 @@ function diffSchemas(from, to, options = {}) {
|
|
|
389
476
|
}
|
|
390
477
|
let forwardChanges = [];
|
|
391
478
|
if (forwardSql.length > 0) {
|
|
392
|
-
const { changes, warnings: w } = (0, granularity_driver_1.restructureChanges)([{ name: 'delta', dependencies: [], deploy: forwardSql.join('\n\n') }], {
|
|
479
|
+
const { changes, warnings: w } = (0, granularity_driver_1.restructureChanges)([{ name: 'delta', dependencies: [], deploy: forwardSql.join('\n\n') }], {
|
|
480
|
+
granularity: options.granularity ?? 'object',
|
|
481
|
+
changeGranularity: options.changeGranularity,
|
|
482
|
+
subObjectName: identity => (0, naming_spec_1.pathFor)(identity, { style })
|
|
483
|
+
});
|
|
393
484
|
forwardChanges = changes.map(c => ({
|
|
394
485
|
...c,
|
|
395
486
|
revert: inverseOf(c.deploy, c.name),
|
package/sub-object.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-object identities: the column or constraint a single `ALTER TABLE`
|
|
3
|
+
* statement adds.
|
|
4
|
+
*
|
|
5
|
+
* `identityOf` summarizes an ALTER TABLE by the table it targets — the right
|
|
6
|
+
* grouping key for object-level changes, but lossy one level down: the added
|
|
7
|
+
* column's `colname` and the added constraint's `conname` stay behind in the
|
|
8
|
+
* raw parse node (`facts.stmt`). This module recovers them, giving the
|
|
9
|
+
* change-granularity pipeline a stable identity (and naming-spec path) for
|
|
10
|
+
* every alteration.
|
|
11
|
+
*/
|
|
12
|
+
import type { ObjectIdentity } from '@pgpmjs/naming-spec';
|
|
13
|
+
import type { StatementFacts } from '@pgsql/transform';
|
|
14
|
+
/**
|
|
15
|
+
* The column or constraint a one-command `ALTER TABLE .. ADD COLUMN` /
|
|
16
|
+
* `ADD CONSTRAINT` statement adds, as a naming-spec identity
|
|
17
|
+
* (`kind: 'column' | 'constraint'`, `table` set). Unnamed constraints get
|
|
18
|
+
* their Postgres default name. Returns `null` for anything else — multi-
|
|
19
|
+
* command ALTERs, other subtypes, non-ALTER statements — so callers fall
|
|
20
|
+
* back to object-level grouping.
|
|
21
|
+
*/
|
|
22
|
+
export declare function subObjectIdentityOf(facts: StatementFacts): ObjectIdentity | null;
|
|
23
|
+
/**
|
|
24
|
+
* Rewrite a script so every unnamed `ALTER TABLE .. ADD <constraint>` carries
|
|
25
|
+
* its Postgres default name (`ADD PRIMARY KEY (id)` becomes `ADD CONSTRAINT
|
|
26
|
+
* {table}_pkey PRIMARY KEY (id)`) — the exact name the catalog would assign
|
|
27
|
+
* anyway. Naming them makes each constraint change independently revertible
|
|
28
|
+
* (`DROP CONSTRAINT <name>`) and verifiable. Statements that need no rewrite
|
|
29
|
+
* keep their original text.
|
|
30
|
+
*/
|
|
31
|
+
export declare function nameUnnamedConstraints(sql: string): string;
|
package/sub-object.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.subObjectIdentityOf = subObjectIdentityOf;
|
|
4
|
+
exports.nameUnnamedConstraints = nameUnnamedConstraints;
|
|
5
|
+
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
6
|
+
const constraint_names_1 = require("./constraint-names");
|
|
7
|
+
/** The single AlterTableCmd of a one-command ALTER TABLE, or null. */
|
|
8
|
+
function singleCmd(stmt) {
|
|
9
|
+
const cmds = stmt.cmds ?? [];
|
|
10
|
+
if (cmds.length !== 1)
|
|
11
|
+
return null;
|
|
12
|
+
return cmds[0].AlterTableCmd ?? null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The column or constraint a one-command `ALTER TABLE .. ADD COLUMN` /
|
|
16
|
+
* `ADD CONSTRAINT` statement adds, as a naming-spec identity
|
|
17
|
+
* (`kind: 'column' | 'constraint'`, `table` set). Unnamed constraints get
|
|
18
|
+
* their Postgres default name. Returns `null` for anything else — multi-
|
|
19
|
+
* command ALTERs, other subtypes, non-ALTER statements — so callers fall
|
|
20
|
+
* back to object-level grouping.
|
|
21
|
+
*/
|
|
22
|
+
function subObjectIdentityOf(facts) {
|
|
23
|
+
const stmt = facts.stmt?.AlterTableStmt;
|
|
24
|
+
if (!stmt?.relation?.relname)
|
|
25
|
+
return null;
|
|
26
|
+
const cmd = singleCmd(stmt);
|
|
27
|
+
if (!cmd)
|
|
28
|
+
return null;
|
|
29
|
+
const schema = stmt.relation.schemaname ?? null;
|
|
30
|
+
const table = stmt.relation.relname;
|
|
31
|
+
if (cmd.subtype === 'AT_AddColumn' && cmd.def?.ColumnDef?.colname) {
|
|
32
|
+
return { kind: 'column', schema, name: cmd.def.ColumnDef.colname, table };
|
|
33
|
+
}
|
|
34
|
+
if (cmd.subtype === 'AT_AddConstraint' && cmd.def?.Constraint) {
|
|
35
|
+
const constraint = cmd.def.Constraint;
|
|
36
|
+
const name = constraint.conname ?? (0, constraint_names_1.defaultConstraintName)(table, constraint);
|
|
37
|
+
if (!name)
|
|
38
|
+
return null;
|
|
39
|
+
return { kind: 'constraint', schema, name, table };
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Rewrite a script so every unnamed `ALTER TABLE .. ADD <constraint>` carries
|
|
45
|
+
* its Postgres default name (`ADD PRIMARY KEY (id)` becomes `ADD CONSTRAINT
|
|
46
|
+
* {table}_pkey PRIMARY KEY (id)`) — the exact name the catalog would assign
|
|
47
|
+
* anyway. Naming them makes each constraint change independently revertible
|
|
48
|
+
* (`DROP CONSTRAINT <name>`) and verifiable. Statements that need no rewrite
|
|
49
|
+
* keep their original text.
|
|
50
|
+
*/
|
|
51
|
+
function nameUnnamedConstraints(sql) {
|
|
52
|
+
const parsed = (0, plpgsql_parser_1.parseSync)(sql);
|
|
53
|
+
const pieces = [];
|
|
54
|
+
for (const raw of parsed.sql.stmts) {
|
|
55
|
+
const node = raw.stmt;
|
|
56
|
+
const stmt = node.AlterTableStmt;
|
|
57
|
+
const table = stmt?.relation?.relname;
|
|
58
|
+
let rewritten = false;
|
|
59
|
+
if (stmt && table) {
|
|
60
|
+
for (const cmd of stmt.cmds ?? []) {
|
|
61
|
+
const at = cmd.AlterTableCmd;
|
|
62
|
+
const constraint = at?.subtype === 'AT_AddConstraint' ? at.def?.Constraint : undefined;
|
|
63
|
+
if (constraint && !constraint.conname) {
|
|
64
|
+
const name = (0, constraint_names_1.defaultConstraintName)(table, constraint);
|
|
65
|
+
if (name) {
|
|
66
|
+
constraint.conname = name;
|
|
67
|
+
rewritten = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (rewritten) {
|
|
73
|
+
pieces.push(`${plpgsql_parser_1.Deparser.deparse(node, { pretty: true })};`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const start = raw.stmt_location ?? 0;
|
|
77
|
+
const len = raw.stmt_len;
|
|
78
|
+
const text = len === undefined ? sql.slice(start) : sql.slice(start, start + len);
|
|
79
|
+
const trimmed = text.trim();
|
|
80
|
+
pieces.push(trimmed.endsWith(';') ? trimmed : `${trimmed};`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return pieces.join('\n\n');
|
|
84
|
+
}
|