@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.
@@ -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 def = node;
128
- const clean = (0, transform_1.cleanTree)(node);
129
- shape.columns.set(def.colname ?? '', { def: clean, print: JSON.stringify(clean) });
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(JSON.stringify(clean), clean);
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') }], { granularity: options.granularity ?? 'object' });
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),
@@ -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
+ }