@pgpmjs/transform 0.19.0 → 0.21.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.
@@ -28,8 +28,10 @@
28
28
  */
29
29
  import { pathFor } from '@pgpmjs/naming-spec';
30
30
  import { existenceCheck, invertStatement, revertFor, verifyFor } from '@pgsql/scripts';
31
- import { buildStatementGraph, classifyStatements, cleanTree, identityOf } from '@pgsql/transform';
31
+ import { classifyStatements } from '@pgsql/semantics';
32
+ import { buildStatementGraph, cleanTree, identityOf } from '@pgsql/transform';
32
33
  import { Deparser, parseSync } from 'plpgsql-parser';
34
+ import { defaultConstraintName } from './constraint-names';
33
35
  import { restructureChanges } from './granularity-driver';
34
36
  const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table ?? ''}\u0000${id.name}`;
35
37
  /**
@@ -99,7 +101,7 @@ function groupingIdentity(f) {
99
101
  const identity = identityOf(f);
100
102
  if (!identity)
101
103
  return null;
102
- if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'rls_enable') &&
104
+ if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'fk_constraint' || f.kind === 'rls_enable') &&
103
105
  identity.kind !== 'table') {
104
106
  return { kind: 'table', schema: identity.schema, name: identity.table ?? identity.name };
105
107
  }
@@ -112,21 +114,61 @@ function fingerprint(text) {
112
114
  }
113
115
  /** Multiset fingerprint of a whole unit. */
114
116
  const unitFingerprint = (unit) => unit.texts.map(fingerprint).sort().join('\n');
117
+ /** JSON with recursively sorted object keys — an insertion-order-proof fingerprint. */
118
+ function stablePrint(node) {
119
+ if (Array.isArray(node))
120
+ return `[${node.map(stablePrint).join(',')}]`;
121
+ if (node && typeof node === 'object') {
122
+ const entries = Object.entries(node)
123
+ .filter(([, v]) => v !== undefined)
124
+ .sort(([a], [b]) => a.localeCompare(b))
125
+ .map(([k, v]) => `${JSON.stringify(k)}:${stablePrint(v)}`);
126
+ return `{${entries.join(',')}}`;
127
+ }
128
+ return JSON.stringify(node);
129
+ }
130
+ /** Constraint kinds that can move between column, table-elt, and ALTER form. */
131
+ const RELOCATABLE = new Set(['CONSTR_PRIMARY', 'CONSTR_UNIQUE', 'CONSTR_FOREIGN', 'CONSTR_CHECK']);
115
132
  /**
116
133
  * Fold a table unit's statements (CREATE TABLE plus any ALTER TABLE
117
134
  * ADD COLUMN / ADD CONSTRAINT) into an effective shape, so atomic and
118
135
  * consolidated authorships of the same table compare equal.
136
+ *
137
+ * Constraint placement is representation, not semantics: `id bigint PRIMARY
138
+ * KEY`, a `PRIMARY KEY (id)` table elt, and `ALTER TABLE .. ADD CONSTRAINT
139
+ * t_pkey PRIMARY KEY (id)` all catalog identically. Relocatable constraints
140
+ * (PK/UNIQUE/FK/CHECK) are therefore lifted out of columns and ALTER
141
+ * commands into one canonical table-level set — keyed with the Postgres
142
+ * default name when unnamed, columns filled in from the owning column when
143
+ * column-attached — and PK columns gain the NOT NULL the catalog implies.
119
144
  */
120
145
  function tableShape(unit) {
121
146
  const shape = { relation: null, columns: new Map(), extras: new Map() };
147
+ const table = unit.identity.name;
148
+ const rawColumns = [];
149
+ const constraints = [];
150
+ const pkColumns = new Set();
122
151
  const addColumn = (node) => {
123
- const def = node;
124
- const clean = cleanTree(node);
125
- shape.columns.set(def.colname ?? '', { def: clean, print: JSON.stringify(clean) });
152
+ const colname = node.colname ?? '';
153
+ const attached = node.constraints ?? [];
154
+ const residual = [];
155
+ for (const item of attached) {
156
+ const c = item.Constraint;
157
+ if (c && RELOCATABLE.has(c.contype ?? '')) {
158
+ constraints.push({ node: c, column: colname });
159
+ }
160
+ else {
161
+ residual.push(item);
162
+ }
163
+ }
164
+ rawColumns.push({ colname, def: { ...node, constraints: residual } });
165
+ };
166
+ const addConstraint = (node, column) => {
167
+ constraints.push({ node: node, column });
126
168
  };
127
169
  const addExtra = (node) => {
128
170
  const clean = cleanTree(node);
129
- shape.extras.set(JSON.stringify(clean), clean);
171
+ shape.extras.set(stablePrint(clean), clean);
130
172
  };
131
173
  for (const text of unit.texts) {
132
174
  for (const raw of parseSync(text).sql.stmts) {
@@ -137,6 +179,9 @@ function tableShape(unit) {
137
179
  for (const elt of elts) {
138
180
  if (elt.ColumnDef)
139
181
  addColumn(elt.ColumnDef);
182
+ else if (elt.Constraint && RELOCATABLE.has(elt.Constraint.contype ?? '')) {
183
+ addConstraint(elt.Constraint);
184
+ }
140
185
  else
141
186
  addExtra(elt);
142
187
  }
@@ -152,6 +197,11 @@ function tableShape(unit) {
152
197
  if (at.subtype === 'AT_AddColumn' && def?.ColumnDef) {
153
198
  addColumn(def.ColumnDef);
154
199
  }
200
+ else if (at.subtype === 'AT_AddConstraint' &&
201
+ def?.Constraint &&
202
+ RELOCATABLE.has(def.Constraint.contype ?? '')) {
203
+ addConstraint(def.Constraint);
204
+ }
155
205
  else {
156
206
  addExtra({ AlterTableCmd: at });
157
207
  }
@@ -162,6 +212,44 @@ function tableShape(unit) {
162
212
  }
163
213
  }
164
214
  }
215
+ for (const { node, column } of constraints) {
216
+ const canonical = { ...node };
217
+ canonical.conname = node.conname ?? defaultConstraintName(table, node, column) ?? undefined;
218
+ if (column && (node.keys ?? []).length === 0 && node.contype !== 'CONSTR_FOREIGN' && node.contype !== 'CONSTR_CHECK') {
219
+ canonical.keys = [{ String: { sval: column } }];
220
+ }
221
+ if (column && node.contype === 'CONSTR_FOREIGN' && (node.fk_attrs ?? []).length === 0) {
222
+ canonical.fk_attrs = [{ String: { sval: column } }];
223
+ }
224
+ if (node.contype === 'CONSTR_PRIMARY') {
225
+ const keyed = canonical.keys ?? [];
226
+ for (const k of keyed)
227
+ if (k.String?.sval)
228
+ pkColumns.add(k.String.sval);
229
+ }
230
+ addExtra({ Constraint: canonical });
231
+ }
232
+ for (const { colname, def } of rawColumns) {
233
+ const residual = (def.constraints ?? []).map(c => {
234
+ const constraint = c.Constraint;
235
+ // NOT NULL parses with parser-version-dependent extras (is_enforced,
236
+ // initially_valid); reduce to its semantic core so an authored NOT NULL
237
+ // equals the one a primary key implies.
238
+ if (constraint?.contype === 'CONSTR_NOTNULL') {
239
+ return { Constraint: { contype: 'CONSTR_NOTNULL', conname: constraint.conname } };
240
+ }
241
+ return c;
242
+ });
243
+ const hasNotNull = residual.some(c => c.Constraint?.contype === 'CONSTR_NOTNULL');
244
+ if (pkColumns.has(colname) && !hasNotNull) {
245
+ residual.push({ Constraint: { contype: 'CONSTR_NOTNULL' } });
246
+ }
247
+ const sorted = residual
248
+ .map(c => cleanTree(c))
249
+ .sort((x, y) => stablePrint(x).localeCompare(stablePrint(y)));
250
+ const clean = cleanTree({ ...def, constraints: sorted });
251
+ shape.columns.set(colname, { def: clean, print: stablePrint(clean) });
252
+ }
165
253
  return shape;
166
254
  }
167
255
  const deparseStmt = (node) => `${Deparser.deparse(node, { pretty: false })};`;
@@ -385,7 +473,11 @@ export function diffSchemas(from, to, options = {}) {
385
473
  }
386
474
  let forwardChanges = [];
387
475
  if (forwardSql.length > 0) {
388
- const { changes, warnings: w } = restructureChanges([{ name: 'delta', dependencies: [], deploy: forwardSql.join('\n\n') }], { granularity: options.granularity ?? 'object' });
476
+ const { changes, warnings: w } = restructureChanges([{ name: 'delta', dependencies: [], deploy: forwardSql.join('\n\n') }], {
477
+ granularity: options.granularity ?? 'object',
478
+ changeGranularity: options.changeGranularity,
479
+ subObjectName: identity => pathFor(identity, { style })
480
+ });
389
481
  forwardChanges = changes.map(c => ({
390
482
  ...c,
391
483
  revert: inverseOf(c.deploy, c.name),
@@ -0,0 +1,80 @@
1
+ import { Deparser, parseSync } from 'plpgsql-parser';
2
+ import { defaultConstraintName } from './constraint-names';
3
+ /** The single AlterTableCmd of a one-command ALTER TABLE, or null. */
4
+ function singleCmd(stmt) {
5
+ const cmds = stmt.cmds ?? [];
6
+ if (cmds.length !== 1)
7
+ return null;
8
+ return cmds[0].AlterTableCmd ?? null;
9
+ }
10
+ /**
11
+ * The column or constraint a one-command `ALTER TABLE .. ADD COLUMN` /
12
+ * `ADD CONSTRAINT` statement adds, as a naming-spec identity
13
+ * (`kind: 'column' | 'constraint'`, `table` set). Unnamed constraints get
14
+ * their Postgres default name. Returns `null` for anything else — multi-
15
+ * command ALTERs, other subtypes, non-ALTER statements — so callers fall
16
+ * back to object-level grouping.
17
+ */
18
+ export function subObjectIdentityOf(facts) {
19
+ const stmt = facts.stmt?.AlterTableStmt;
20
+ if (!stmt?.relation?.relname)
21
+ return null;
22
+ const cmd = singleCmd(stmt);
23
+ if (!cmd)
24
+ return null;
25
+ const schema = stmt.relation.schemaname ?? null;
26
+ const table = stmt.relation.relname;
27
+ if (cmd.subtype === 'AT_AddColumn' && cmd.def?.ColumnDef?.colname) {
28
+ return { kind: 'column', schema, name: cmd.def.ColumnDef.colname, table };
29
+ }
30
+ if (cmd.subtype === 'AT_AddConstraint' && cmd.def?.Constraint) {
31
+ const constraint = cmd.def.Constraint;
32
+ const name = constraint.conname ?? defaultConstraintName(table, constraint);
33
+ if (!name)
34
+ return null;
35
+ return { kind: 'constraint', schema, name, table };
36
+ }
37
+ return null;
38
+ }
39
+ /**
40
+ * Rewrite a script so every unnamed `ALTER TABLE .. ADD <constraint>` carries
41
+ * its Postgres default name (`ADD PRIMARY KEY (id)` becomes `ADD CONSTRAINT
42
+ * {table}_pkey PRIMARY KEY (id)`) — the exact name the catalog would assign
43
+ * anyway. Naming them makes each constraint change independently revertible
44
+ * (`DROP CONSTRAINT <name>`) and verifiable. Statements that need no rewrite
45
+ * keep their original text.
46
+ */
47
+ export function nameUnnamedConstraints(sql) {
48
+ const parsed = parseSync(sql);
49
+ const pieces = [];
50
+ for (const raw of parsed.sql.stmts) {
51
+ const node = raw.stmt;
52
+ const stmt = node.AlterTableStmt;
53
+ const table = stmt?.relation?.relname;
54
+ let rewritten = false;
55
+ if (stmt && table) {
56
+ for (const cmd of stmt.cmds ?? []) {
57
+ const at = cmd.AlterTableCmd;
58
+ const constraint = at?.subtype === 'AT_AddConstraint' ? at.def?.Constraint : undefined;
59
+ if (constraint && !constraint.conname) {
60
+ const name = defaultConstraintName(table, constraint);
61
+ if (name) {
62
+ constraint.conname = name;
63
+ rewritten = true;
64
+ }
65
+ }
66
+ }
67
+ }
68
+ if (rewritten) {
69
+ pieces.push(`${Deparser.deparse(node, { pretty: true })};`);
70
+ }
71
+ else {
72
+ const start = raw.stmt_location ?? 0;
73
+ const len = raw.stmt_len;
74
+ const text = len === undefined ? sql.slice(start) : sql.slice(start, start + len);
75
+ const trimmed = text.trim();
76
+ pieces.push(trimmed.endsWith(';') ? trimmed : `${trimmed};`);
77
+ }
78
+ }
79
+ return pieces.join('\n\n');
80
+ }
@@ -1,4 +1,4 @@
1
- import { QualifiedName } from '@pgsql/transform';
1
+ import { QualifiedName } from '@pgsql/semantics';
2
2
  import { CategoryProfile, ChangeCategory } from './categorize';
3
3
  /**
4
4
  * A change plus its deploy SQL. Optional plan `dependencies` (from `pgpm.plan`)
@@ -1,13 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveFixtureClosure = resolveFixtureClosure;
4
- const transform_1 = require("@pgsql/transform");
4
+ const semantics_1 = require("@pgsql/semantics");
5
5
  const categorize_1 = require("./categorize");
6
6
  function objectKey(schema, name) {
7
7
  return `${schema ?? ''}\u0000${name}`;
8
8
  }
9
9
  function analyze(change, profile) {
10
- const facts = (0, transform_1.classifyStatements)(change.sql);
10
+ const facts = (0, semantics_1.classifyStatements)(change.sql);
11
11
  const category = profile.categorize(facts, change.name);
12
12
  const creates = [];
13
13
  const createsSchemas = [];
@@ -1,5 +1,42 @@
1
- import type { Granularity, StatementFacts } from '@pgsql/transform';
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
+ import type { ObjectIdentity as NamingIdentity } from '@pgpmjs/naming-spec';
21
+ import type { StatementFacts } from '@pgsql/semantics';
22
+ import type { Granularity } from '@pgsql/transform';
2
23
  export type { Granularity } from '@pgsql/transform';
24
+ /**
25
+ * How statements are distributed across pgpm changes — orthogonal to
26
+ * `Granularity`, which shapes the SQL *within* a change.
27
+ *
28
+ * - `alteration` — one change per alteration: every `ADD COLUMN` /
29
+ * `ADD CONSTRAINT` becomes its own plan entry with its own
30
+ * deploy/revert/verify and graph-derived requires, so a
31
+ * single column can deploy or revert independently.
32
+ * - `object` — one change per created object (the default): a table's
33
+ * CREATE and all its ALTERs share one plan entry.
34
+ * - `single` — one change for the whole module: every statement lands in
35
+ * a single plan entry with one deploy/revert/verify.
36
+ */
37
+ export type ChangeGranularity = 'alteration' | 'object' | 'single';
38
+ export declare const CHANGE_GRANULARITIES: readonly ChangeGranularity[];
39
+ export declare const isChangeGranularity: (value: string) => value is ChangeGranularity;
3
40
  /** A change's deploy surface going into or out of the restructure. */
4
41
  export interface GranularityChange {
5
42
  /** Change name (plan token, e.g. `schemas/app/tables/users/table`). */
@@ -26,6 +63,27 @@ export interface RestructuredChange extends GranularityChange {
26
63
  }
27
64
  export interface RestructureModuleOptions {
28
65
  granularity: Granularity;
66
+ /**
67
+ * Change-level distribution (default `object`). With `alteration`, every
68
+ * single-command `ALTER TABLE ADD COLUMN` / `ADD CONSTRAINT` in the
69
+ * restructured script becomes its own change, named by the sub-object's
70
+ * naming-spec path (`.../columns/{name}/column`,
71
+ * `.../constraints/{name}/constraint`); unnamed constraints are first
72
+ * given their Postgres default name so each change stays revertible.
73
+ * With `single`, the whole restructured script becomes one change.
74
+ */
75
+ changeGranularity?: ChangeGranularity;
76
+ /**
77
+ * Plan token for the single change (used only with `changeGranularity:
78
+ * 'single'`). Defaults to `module/init`.
79
+ */
80
+ singleChangeName?: string;
81
+ /**
82
+ * Derive a change name for an alteration group from its sub-object
83
+ * identity (used only with `changeGranularity: 'alteration'`). Defaults to
84
+ * `pathFor` in `directory` style.
85
+ */
86
+ subObjectName?: (identity: NamingIdentity) => string;
29
87
  /**
30
88
  * Derive a change name for a statement group from the facts of its primary
31
89
  * (creating) statement. Defaults to {@link defaultChangeName}: naming spec
@@ -1,29 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isChangeGranularity = exports.CHANGE_GRANULARITIES = void 0;
3
4
  exports.defaultChangeName = defaultChangeName;
4
5
  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
6
  const naming_spec_1 = require("@pgpmjs/naming-spec");
25
7
  const scripts_1 = require("@pgsql/scripts");
8
+ const semantics_1 = require("@pgsql/semantics");
26
9
  const transform_1 = require("@pgsql/transform");
10
+ const sub_object_1 = require("./sub-object");
11
+ exports.CHANGE_GRANULARITIES = ['alteration', 'object', 'single'];
12
+ const isChangeGranularity = (value) => exports.CHANGE_GRANULARITIES.includes(value);
13
+ exports.isChangeGranularity = isChangeGranularity;
27
14
  const KIND_DIRS = {
28
15
  table: 'tables',
29
16
  view: 'views',
@@ -57,22 +44,50 @@ function defaultChangeName(facts) {
57
44
  */
58
45
  function restructureChanges(changes, options) {
59
46
  const nameFor = options.changeName ?? defaultChangeName;
47
+ const subNameFor = options.subObjectName ?? ((identity) => (0, naming_spec_1.pathFor)(identity));
48
+ const alteration = options.changeGranularity === 'alteration';
60
49
  const flattened = changes
61
50
  .map(c => c.deploy.trim())
62
51
  .filter(Boolean)
63
52
  .join('\n\n');
64
- const { sql, warnings } = (0, transform_1.restructureSql)(flattened, {
53
+ const restructured = (0, transform_1.restructureSql)(flattened, {
65
54
  granularity: options.granularity
66
55
  });
56
+ const warnings = restructured.warnings;
57
+ const sql = alteration ? (0, sub_object_1.nameUnnamedConstraints)(restructured.sql) : restructured.sql;
67
58
  // Re-classify the emitted script; group statements by the object they
68
59
  // target (creates[0]), so a table's CREATE and its remaining ALTERs land
69
60
  // in the same change regardless of statement kind.
70
- const facts = (0, transform_1.classifyStatements)(sql);
61
+ const facts = (0, semantics_1.classifyStatements)(sql);
62
+ if (options.changeGranularity === 'single') {
63
+ const name = options.singleChangeName ?? 'module/init';
64
+ const inputNames = new Set(changes.map(c => c.name));
65
+ const dependencies = [...new Set(changes.flatMap(c => c.dependencies).filter(d => !inputNames.has(d)))].sort();
66
+ const statements = facts
67
+ .map(f => sql.slice(f.span.start, f.span.start + f.span.len).trim())
68
+ .filter(Boolean)
69
+ .map(text => (text.endsWith(';') ? text : `${text};`));
70
+ const revert = (0, scripts_1.revertFor)(facts);
71
+ const verify = (0, scripts_1.verifyFor)(facts);
72
+ for (const warning of [...revert.warnings, ...verify.warnings]) {
73
+ warnings.push(`${name}: ${warning}`);
74
+ }
75
+ return {
76
+ changes: statements.length > 0
77
+ ? [{ name, dependencies, deploy: statements.join('\n\n'), revert: revert.sql, verify: verify.sql }]
78
+ : [],
79
+ warnings
80
+ };
81
+ }
71
82
  const graph = (0, transform_1.buildStatementGraph)(facts);
72
83
  const groupOf = new Array(facts.length).fill(-1);
73
84
  const groupKeys = [];
74
85
  const groupFacts = [];
75
86
  const groupKeyToIndex = new Map();
87
+ /** Sub-object path for alteration groups; null means name via `nameFor`. */
88
+ const groupSubName = [];
89
+ /** For alteration groups: the object-group key of the owning table. */
90
+ const groupOwnerKey = [];
76
91
  facts.forEach((f, i) => {
77
92
  const created = f.creates[0];
78
93
  if (!created) {
@@ -82,12 +97,31 @@ function restructureChanges(changes, options) {
82
97
  groupOf[i] = groupOf[i - 1];
83
98
  return;
84
99
  }
100
+ if (alteration) {
101
+ const sub = (0, sub_object_1.subObjectIdentityOf)(f);
102
+ if (sub) {
103
+ const key = `sub\u0000${sub.kind}\u0000${sub.schema ?? ''}\u0000${sub.table}\u0000${sub.name}`;
104
+ let g = groupKeyToIndex.get(key);
105
+ if (g === undefined) {
106
+ g = groupKeys.length;
107
+ groupKeys.push(key);
108
+ groupFacts.push(f);
109
+ groupSubName.push(subNameFor(sub));
110
+ groupOwnerKey.push(`${sub.schema ?? ''}.${sub.table}`);
111
+ groupKeyToIndex.set(key, g);
112
+ }
113
+ groupOf[i] = g;
114
+ return;
115
+ }
116
+ }
85
117
  const key = `${created.schema ?? ''}.${created.name}`;
86
118
  let g = groupKeyToIndex.get(key);
87
119
  if (g === undefined) {
88
120
  g = groupKeys.length;
89
121
  groupKeys.push(key);
90
122
  groupFacts.push(f);
123
+ groupSubName.push(null);
124
+ groupOwnerKey.push(null);
91
125
  groupKeyToIndex.set(key, g);
92
126
  }
93
127
  else if (!(groupFacts[g].kind in KIND_DIRS) && groupFacts[g].kind !== 'schema' && (f.kind in KIND_DIRS || f.kind === 'schema')) {
@@ -96,7 +130,7 @@ function restructureChanges(changes, options) {
96
130
  }
97
131
  groupOf[i] = g;
98
132
  });
99
- const groupNames = groupFacts.map(nameFor);
133
+ const groupNames = groupFacts.map((f, g) => groupSubName[g] ?? nameFor(f));
100
134
  // Schema producers, for schema-level change dependencies.
101
135
  const schemaGroup = new Map();
102
136
  facts.forEach((f, i) => {
@@ -142,6 +176,38 @@ function restructureChanges(changes, options) {
142
176
  groupDeps[from].add(to);
143
177
  }
144
178
  });
179
+ // Alteration groups depend on their table's own change, and constraint
180
+ // groups additionally on the column changes they key on — the statement
181
+ // graph keys everything by the table, so these finer edges are added here.
182
+ if (alteration) {
183
+ const columnGroup = new Map();
184
+ groupKeys.forEach((key, g) => {
185
+ const parts = key.split('\u0000');
186
+ if (parts[0] === 'sub' && parts[1] === 'column') {
187
+ columnGroup.set(`${parts[2]}.${parts[3]}.${parts[4]}`, g);
188
+ }
189
+ });
190
+ groupKeys.forEach((key, g) => {
191
+ const owner = groupOwnerKey[g];
192
+ if (owner === null)
193
+ return;
194
+ const tableGroup = groupKeyToIndex.get(owner);
195
+ if (tableGroup !== undefined && tableGroup !== g)
196
+ groupDeps[g].add(tableGroup);
197
+ const parts = key.split('\u0000');
198
+ if (parts[1] !== 'constraint')
199
+ return;
200
+ const constraint = groupFacts[g].stmt?.AlterTableStmt?.cmds?.[0]?.AlterTableCmd?.def?.Constraint;
201
+ const referenced = [...(constraint?.keys ?? []), ...(constraint?.fk_attrs ?? [])]
202
+ .map(k => k.String?.sval)
203
+ .filter((s) => typeof s === 'string');
204
+ for (const col of referenced) {
205
+ const to = columnGroup.get(`${owner}.${col}`);
206
+ if (to !== undefined && to !== g)
207
+ groupDeps[g].add(to);
208
+ }
209
+ });
210
+ }
145
211
  // Emit groups in first-statement order (already topological).
146
212
  const order = [...groupNames.keys()].sort((a, b) => {
147
213
  const firstA = groupOf.indexOf(a);
package/index.d.ts CHANGED
@@ -4,12 +4,14 @@ export type { CatalogQueryable, CatalogSnapshot } from './catalog-check';
4
4
  export { diffCatalogSnapshots, snapshotCatalog, withoutColumnOrder } from './catalog-check';
5
5
  export type { CategoryProfile, ChangeCategory, } from './categorize';
6
6
  export { buildCategoryOf, categorizeChange, TIER_PROFILE, } from './categorize';
7
+ export type { ConstraintNode } from './constraint-names';
8
+ export { defaultConstraintName, firstColumnRef } from './constraint-names';
7
9
  export type { ClosureChange, ClosureInputChange, ClosureReason, FixtureClosure, ResolveFixtureClosureOptions, } from './fixture-closure';
8
10
  export { resolveFixtureClosure } from './fixture-closure';
9
- export type { GranularityChange, RestructuredChange, RestructureModuleOptions, RestructureModuleResult, } from './granularity-driver';
10
- export { defaultChangeName, restructureChanges, } from './granularity-driver';
11
- export type { PgpmModuleModel } from './module-emit';
12
- export { checkOverwrite, writeControlFile, writeModule } from './module-emit';
11
+ export type { ChangeGranularity, GranularityChange, RestructuredChange, RestructureModuleOptions, RestructureModuleResult, } from './granularity-driver';
12
+ export { CHANGE_GRANULARITIES, defaultChangeName, isChangeGranularity, restructureChanges, } from './granularity-driver';
13
+ export type { AppendModuleResult, PgpmModuleModel } from './module-emit';
14
+ export { appendModule, checkOverwrite, writeControlFile, writeModule } from './module-emit';
13
15
  export type { ModuleSource, ModuleSourceChange } from './module-source';
14
16
  export { loadModuleSource, stripTransactionWrapper } from './module-source';
15
17
  export type { PartitionedPackageRows, PartitionExportRowsResult } from './partition';
@@ -24,5 +26,6 @@ export type { ExportGranularity, RestructureExportRowsOptions, RestructureExport
24
26
  export { EXPORT_GRANULARITIES, isExportGranularity, restructureExportRows } from './restructure';
25
27
  export type { DiffInputChange, ObjectDelta, SemanticDeltaChange, SemanticDiffOptions, SemanticDiffResult, SemanticObjectDiff, } from './semantic-diff-driver';
26
28
  export { diffChangeSets, diffSchemas, } from './semantic-diff-driver';
29
+ export { nameUnnamedConstraints, subObjectIdentityOf } from './sub-object';
27
30
  export * from '@pgsql/transform';
28
31
  export { loadModule } from 'plpgsql-parser';
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.loadModule = exports.diffSchemas = exports.diffChangeSets = exports.restructureExportRows = exports.isExportGranularity = exports.EXPORT_GRANULARITIES = exports.regenerateScripts = exports.isStubScript = exports.classifyScript = exports.parseSqlProgram = exports.emitSqlProgram = exports.RESIDUAL_UNIT_PATH = exports.partitionUnits = exports.PartitionCycleError = exports.partitionExportRows = exports.parsePartitionConfig = exports.stripTransactionWrapper = exports.loadModuleSource = exports.writeModule = exports.writeControlFile = exports.checkOverwrite = exports.restructureChanges = exports.defaultChangeName = exports.resolveFixtureClosure = exports.TIER_PROFILE = exports.categorizeChange = exports.buildCategoryOf = exports.withoutColumnOrder = exports.snapshotCatalog = exports.diffCatalogSnapshots = exports.makeSchemaTranspiler = exports.makeNamespaceValidator = exports.buildSchemaRouter = exports.buildExtensionRouter = void 0;
17
+ exports.loadModule = exports.subObjectIdentityOf = exports.nameUnnamedConstraints = exports.diffSchemas = exports.diffChangeSets = exports.restructureExportRows = exports.isExportGranularity = exports.EXPORT_GRANULARITIES = exports.regenerateScripts = exports.isStubScript = exports.classifyScript = exports.parseSqlProgram = exports.emitSqlProgram = exports.RESIDUAL_UNIT_PATH = exports.partitionUnits = exports.PartitionCycleError = exports.partitionExportRows = exports.parsePartitionConfig = exports.stripTransactionWrapper = exports.loadModuleSource = exports.writeModule = exports.writeControlFile = exports.checkOverwrite = exports.appendModule = exports.restructureChanges = exports.isChangeGranularity = exports.defaultChangeName = exports.CHANGE_GRANULARITIES = exports.resolveFixtureClosure = exports.firstColumnRef = exports.defaultConstraintName = exports.TIER_PROFILE = exports.categorizeChange = exports.buildCategoryOf = exports.withoutColumnOrder = exports.snapshotCatalog = exports.diffCatalogSnapshots = exports.makeSchemaTranspiler = exports.makeNamespaceValidator = exports.buildSchemaRouter = exports.buildExtensionRouter = void 0;
18
18
  var bundle_driver_1 = require("./bundle-driver");
19
19
  Object.defineProperty(exports, "buildExtensionRouter", { enumerable: true, get: function () { return bundle_driver_1.buildExtensionRouter; } });
20
20
  Object.defineProperty(exports, "buildSchemaRouter", { enumerable: true, get: function () { return bundle_driver_1.buildSchemaRouter; } });
@@ -28,12 +28,18 @@ var categorize_1 = require("./categorize");
28
28
  Object.defineProperty(exports, "buildCategoryOf", { enumerable: true, get: function () { return categorize_1.buildCategoryOf; } });
29
29
  Object.defineProperty(exports, "categorizeChange", { enumerable: true, get: function () { return categorize_1.categorizeChange; } });
30
30
  Object.defineProperty(exports, "TIER_PROFILE", { enumerable: true, get: function () { return categorize_1.TIER_PROFILE; } });
31
+ var constraint_names_1 = require("./constraint-names");
32
+ Object.defineProperty(exports, "defaultConstraintName", { enumerable: true, get: function () { return constraint_names_1.defaultConstraintName; } });
33
+ Object.defineProperty(exports, "firstColumnRef", { enumerable: true, get: function () { return constraint_names_1.firstColumnRef; } });
31
34
  var fixture_closure_1 = require("./fixture-closure");
32
35
  Object.defineProperty(exports, "resolveFixtureClosure", { enumerable: true, get: function () { return fixture_closure_1.resolveFixtureClosure; } });
33
36
  var granularity_driver_1 = require("./granularity-driver");
37
+ Object.defineProperty(exports, "CHANGE_GRANULARITIES", { enumerable: true, get: function () { return granularity_driver_1.CHANGE_GRANULARITIES; } });
34
38
  Object.defineProperty(exports, "defaultChangeName", { enumerable: true, get: function () { return granularity_driver_1.defaultChangeName; } });
39
+ Object.defineProperty(exports, "isChangeGranularity", { enumerable: true, get: function () { return granularity_driver_1.isChangeGranularity; } });
35
40
  Object.defineProperty(exports, "restructureChanges", { enumerable: true, get: function () { return granularity_driver_1.restructureChanges; } });
36
41
  var module_emit_1 = require("./module-emit");
42
+ Object.defineProperty(exports, "appendModule", { enumerable: true, get: function () { return module_emit_1.appendModule; } });
37
43
  Object.defineProperty(exports, "checkOverwrite", { enumerable: true, get: function () { return module_emit_1.checkOverwrite; } });
38
44
  Object.defineProperty(exports, "writeControlFile", { enumerable: true, get: function () { return module_emit_1.writeControlFile; } });
39
45
  Object.defineProperty(exports, "writeModule", { enumerable: true, get: function () { return module_emit_1.writeModule; } });
@@ -61,6 +67,9 @@ Object.defineProperty(exports, "restructureExportRows", { enumerable: true, get:
61
67
  var semantic_diff_driver_1 = require("./semantic-diff-driver");
62
68
  Object.defineProperty(exports, "diffChangeSets", { enumerable: true, get: function () { return semantic_diff_driver_1.diffChangeSets; } });
63
69
  Object.defineProperty(exports, "diffSchemas", { enumerable: true, get: function () { return semantic_diff_driver_1.diffSchemas; } });
70
+ var sub_object_1 = require("./sub-object");
71
+ Object.defineProperty(exports, "nameUnnamedConstraints", { enumerable: true, get: function () { return sub_object_1.nameUnnamedConstraints; } });
72
+ Object.defineProperty(exports, "subObjectIdentityOf", { enumerable: true, get: function () { return sub_object_1.subObjectIdentityOf; } });
64
73
  __exportStar(require("@pgsql/transform"), exports);
65
74
  var plpgsql_parser_1 = require("plpgsql-parser");
66
75
  Object.defineProperty(exports, "loadModule", { enumerable: true, get: function () { return plpgsql_parser_1.loadModule; } });
package/module-emit.d.ts CHANGED
@@ -32,6 +32,32 @@ export declare const writeControlFile: (dir: string, name: string, requires: str
32
32
  * leaves stale scripts behind. Returns the module directory.
33
33
  */
34
34
  export declare const writeModule: (outBase: string, model: PgpmModuleModel, extraRequires?: string[]) => string;
35
+ /** Result of appending changes into an existing module (see {@link appendModule}). */
36
+ export interface AppendModuleResult {
37
+ /** The module directory that was appended to. */
38
+ dir: string;
39
+ /** Change names that were added to the plan. */
40
+ added: string[];
41
+ /** Change names skipped because they already exist in the plan. */
42
+ skipped: string[];
43
+ /** Non-fatal notices (skips, dropped dangling dependencies). */
44
+ warnings: string[];
45
+ }
46
+ /**
47
+ * Append plan-ordered {@link PgpmRow}s into an *existing* module rather than
48
+ * writing a fresh package. Existing changes, their scripts, and the `.control`
49
+ * file are left untouched; only the new changes are written (deploy/revert/
50
+ * verify) and appended to `pgpm.plan` after the current changes.
51
+ *
52
+ * Rows whose change name already exists are skipped (never overwritten).
53
+ * A new change's dependency bracket is filtered to names that resolve within
54
+ * the plan (existing changes, other appended changes, or `pkg:`-external
55
+ * references); a dangling internal dependency is dropped with a warning
56
+ * (plan order still sequences it after the current changes).
57
+ */
58
+ export declare const appendModule: (moduleDir: string, rows: PgpmRow[], options?: {
59
+ author?: string;
60
+ }) => AppendModuleResult;
35
61
  /**
36
62
  * Guard against clobbering: writing into the source directory requires
37
63
  * `--write`, as does overwriting any existing package directory.