@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.
package/module-emit.js CHANGED
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.checkOverwrite = exports.writeModule = exports.writeControlFile = void 0;
36
+ exports.checkOverwrite = exports.appendModule = exports.writeModule = exports.writeControlFile = void 0;
37
37
  /**
38
38
  * Module emission — the single seam every dials command renders through.
39
39
  *
@@ -86,6 +86,77 @@ const writeModule = (outBase, model, extraRequires = []) => {
86
86
  return dir;
87
87
  };
88
88
  exports.writeModule = writeModule;
89
+ /**
90
+ * Append plan-ordered {@link PgpmRow}s into an *existing* module rather than
91
+ * writing a fresh package. Existing changes, their scripts, and the `.control`
92
+ * file are left untouched; only the new changes are written (deploy/revert/
93
+ * verify) and appended to `pgpm.plan` after the current changes.
94
+ *
95
+ * Rows whose change name already exists are skipped (never overwritten).
96
+ * A new change's dependency bracket is filtered to names that resolve within
97
+ * the plan (existing changes, other appended changes, or `pkg:`-external
98
+ * references); a dangling internal dependency is dropped with a warning
99
+ * (plan order still sequences it after the current changes).
100
+ */
101
+ const appendModule = (moduleDir, rows, options = {}) => {
102
+ const planPath = path.join(moduleDir, 'pgpm.plan');
103
+ if (!fs.existsSync(planPath)) {
104
+ throw new Error(`No pgpm.plan found at ${planPath}; append mode expects an existing pgpm module directory.`);
105
+ }
106
+ const parsed = (0, ast_1.parsePlanFile)(planPath);
107
+ if (!parsed.data) {
108
+ throw new Error(`Failed to parse ${planPath}: ${parsed.errors
109
+ .map(e => `line ${e.line}: ${e.message}`)
110
+ .join('; ')}`);
111
+ }
112
+ const plan = parsed.data;
113
+ const existingNames = new Set(plan.changes.map(c => c.name));
114
+ const incomingNames = new Set(rows.map(r => r.deploy));
115
+ const appended = new Set();
116
+ const added = [];
117
+ const skipped = [];
118
+ const warnings = [];
119
+ const newRows = [];
120
+ const { fullName, email } = (0, ast_1.parseAuthor)(options.author || 'constructive');
121
+ const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
122
+ for (const row of rows) {
123
+ if (existingNames.has(row.deploy) || appended.has(row.deploy)) {
124
+ skipped.push(row.deploy);
125
+ warnings.push(`change ${row.deploy} already exists in ${plan.package}; left untouched`);
126
+ continue;
127
+ }
128
+ const deps = (row.deps ?? []).filter(dep => {
129
+ if (dep.includes(':'))
130
+ return true; // cross-package (pkg:change) external
131
+ if (existingNames.has(dep) || incomingNames.has(dep))
132
+ return true;
133
+ warnings.push(`change ${row.deploy}: dropped dependency ${dep} (not present in ${plan.package})`);
134
+ return false;
135
+ });
136
+ plan.changes.push({
137
+ name: row.deploy,
138
+ dependencies: deps,
139
+ timestamp,
140
+ planner: fullName,
141
+ email: email || `${fullName}@constructive.io`,
142
+ comment: `add ${row.name ?? row.deploy}`
143
+ });
144
+ newRows.push({ ...row, deps });
145
+ appended.add(row.deploy);
146
+ added.push(row.deploy);
147
+ }
148
+ if (newRows.length) {
149
+ const opts = {
150
+ outdir: path.dirname(moduleDir),
151
+ name: path.basename(moduleDir),
152
+ replacer: (str) => str
153
+ };
154
+ (0, ast_1.writePgpmFiles)(newRows, opts);
155
+ (0, ast_1.writePlanFile)(planPath, plan);
156
+ }
157
+ return { dir: moduleDir, added, skipped, warnings };
158
+ };
159
+ exports.appendModule = appendModule;
89
160
  /**
90
161
  * Guard against clobbering: writing into the source directory requires
91
162
  * `--write`, as does overwriting any existing package directory.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgpmjs/transform",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Change-aware SQL transformation for PGPM — superset of @pgsql/transform plus change categorization, fixture closure, and bundle transpile/apply drivers",
6
6
  "main": "index.js",
@@ -39,15 +39,16 @@
39
39
  "plpgsql"
40
40
  ],
41
41
  "devDependencies": {
42
- "@pgpmjs/bundle": "^0.13.3",
42
+ "@pgpmjs/bundle": "^0.14.0",
43
43
  "makage": "^0.3.0"
44
44
  },
45
45
  "dependencies": {
46
- "@pgpmjs/ast": "^0.10.3",
47
- "@pgpmjs/naming-spec": "^0.1.2",
48
- "@pgsql/scripts": "^18.3.1",
49
- "@pgsql/transform": "^18.14.1",
46
+ "@pgpmjs/ast": "^0.11.0",
47
+ "@pgpmjs/naming-spec": "^0.3.0",
48
+ "@pgsql/scripts": "^18.3.2",
49
+ "@pgsql/semantics": "^18.1.0",
50
+ "@pgsql/transform": "^18.15.0",
50
51
  "plpgsql-parser": "^18.5.1"
51
52
  },
52
- "gitHead": "c20b6be96b39bf71c0e2df0f4b51e05deacd006b"
53
+ "gitHead": "d508453a32a2f1d70ff6916871046615226e6f33"
53
54
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Partition dial: project one deploy surface into a set of pgpm packages.
3
3
  *
4
- * The object graph (statements classified by `@pgsql/transform`, grouped
4
+ * The object graph (statements classified by `@pgsql/semantics`, grouped
5
5
  * into identity-keyed units) is the source of truth; which package a unit
6
6
  * lives in is a *derived projection* of a declarative partition config.
7
7
  * Units can be assigned by schema, by object kind, by explicit cherry-pick
@@ -21,7 +21,8 @@
21
21
  * pgpm.plan + deploy trees, feed a bundle, ...).
22
22
  */
23
23
  import { PathStyle } from '@pgpmjs/naming-spec';
24
- import { ObjectIdentity, ObjectIdentityKind, StatementKind } from '@pgsql/transform';
24
+ import { StatementKind } from '@pgsql/semantics';
25
+ import { ObjectIdentity, ObjectIdentityKind } from '@pgsql/transform';
25
26
  /** One deployable unit: an object (by identity) and the statements that build it. */
26
27
  export interface PartitionUnit {
27
28
  /** The object's identity, or `null` for split riders and the residual unit. */
@@ -5,7 +5,7 @@ exports.partitionUnits = partitionUnits;
5
5
  /**
6
6
  * Partition dial: project one deploy surface into a set of pgpm packages.
7
7
  *
8
- * The object graph (statements classified by `@pgsql/transform`, grouped
8
+ * The object graph (statements classified by `@pgsql/semantics`, grouped
9
9
  * into identity-keyed units) is the source of truth; which package a unit
10
10
  * lives in is a *derived projection* of a declarative partition config.
11
11
  * Units can be assigned by schema, by object kind, by explicit cherry-pick
@@ -25,6 +25,7 @@ exports.partitionUnits = partitionUnits;
25
25
  * pgpm.plan + deploy trees, feed a bundle, ...).
26
26
  */
27
27
  const naming_spec_1 = require("@pgpmjs/naming-spec");
28
+ const semantics_1 = require("@pgsql/semantics");
28
29
  const transform_1 = require("@pgsql/transform");
29
30
  /** A partition whose packages require each other in a cycle — unshippable. */
30
31
  class PartitionCycleError extends Error {
@@ -84,7 +85,7 @@ exports.RESIDUAL_UNIT_PATH = 'misc/statements';
84
85
  * otherwise) that depend on their host unit.
85
86
  */
86
87
  function buildUnitGraph(sql, style, splitRiders) {
87
- const facts = (0, transform_1.classifyStatements)(sql);
88
+ const facts = (0, semantics_1.classifyStatements)(sql);
88
89
  const graph = (0, transform_1.buildStatementGraph)(facts);
89
90
  const unitOf = new Array(facts.length).fill(-1);
90
91
  const units = [];
package/program.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * untouched statements). A program with no dirty statements emits its source
10
10
  * byte-identically, so content-addressed hashes are preserved.
11
11
  */
12
- import type { StatementFacts } from '@pgsql/transform';
12
+ import type { StatementFacts } from '@pgsql/semantics';
13
13
  /** A statement's location in the source script (byte offsets). */
14
14
  export type SqlStatementSpan = StatementFacts['span'];
15
15
  /** One statement of a parsed SQL script. */
package/program.js CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseSqlProgram = parseSqlProgram;
4
4
  exports.emitSqlProgram = emitSqlProgram;
5
- const transform_1 = require("@pgsql/transform");
5
+ const semantics_1 = require("@pgsql/semantics");
6
6
  const plpgsql_parser_1 = require("plpgsql-parser");
7
7
  /**
8
8
  * Parse a SQL script into a statement program. Requires `loadModule()` from
@@ -11,7 +11,7 @@ const plpgsql_parser_1 = require("plpgsql-parser");
11
11
  function parseSqlProgram(source) {
12
12
  const parseResult = (0, plpgsql_parser_1.parseSql)(source);
13
13
  const stmts = parseResult?.stmts ?? [];
14
- const facts = (0, transform_1.classifyStatements)(source);
14
+ const facts = (0, semantics_1.classifyStatements)(source);
15
15
  const statements = [];
16
16
  for (let i = 0; i < stmts.length; i++) {
17
17
  const rawStmt = stmts[i];
package/regen.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * Requires `loadModule()` from `plpgsql-parser` to have completed.
13
13
  */
14
14
  import type { GeneratedScript } from '@pgsql/scripts';
15
- import type { StatementFacts } from '@pgsql/transform';
15
+ import type { StatementFacts } from '@pgsql/semantics';
16
16
  export type { GeneratedScript } from '@pgsql/scripts';
17
17
  /** Generated revert and verify scripts for one change's deploy SQL. */
18
18
  export interface RegeneratedScripts {
package/regen.js CHANGED
@@ -4,13 +4,13 @@ exports.classifyScript = classifyScript;
4
4
  exports.isStubScript = isStubScript;
5
5
  exports.regenerateScripts = regenerateScripts;
6
6
  const scripts_1 = require("@pgsql/scripts");
7
- const transform_1 = require("@pgsql/transform");
7
+ const semantics_1 = require("@pgsql/semantics");
8
8
  /**
9
9
  * Classify a pgpm script's substantive statements: every top-level statement
10
10
  * except transaction control (`BEGIN`/`COMMIT`/`ROLLBACK` wrappers).
11
11
  */
12
12
  function classifyScript(sql) {
13
- return (0, transform_1.classifyStatements)(sql).filter(f => f.nodeTag !== 'TransactionStmt');
13
+ return (0, semantics_1.classifyStatements)(sql).filter(f => f.nodeTag !== 'TransactionStmt');
14
14
  }
15
15
  /**
16
16
  * Whether a script is an empty stub: nothing but comments, whitespace, and
package/restructure.d.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import { PgpmRow } from '@pgpmjs/ast';
9
9
  import { PathStyle } from '@pgpmjs/naming-spec';
10
10
  import { Granularity } from '@pgsql/transform';
11
+ import { ChangeGranularity } from './granularity-driver';
11
12
  export type ExportGranularity = Granularity;
12
13
  export declare const EXPORT_GRANULARITIES: readonly ExportGranularity[];
13
14
  export declare const isExportGranularity: (value: unknown) => value is ExportGranularity;
@@ -18,6 +19,12 @@ export interface RestructureExportRowsResult {
18
19
  export interface RestructureExportRowsOptions {
19
20
  /** Naming-spec rendering style for derived paths (default `directory`). */
20
21
  naming?: PathStyle;
22
+ /**
23
+ * Change-level distribution (default `object`): with `alteration`, each
24
+ * `ADD COLUMN` / `ADD CONSTRAINT` becomes its own change. See
25
+ * {@link ChangeGranularity}.
26
+ */
27
+ changeGranularity?: ChangeGranularity;
21
28
  }
22
29
  /**
23
30
  * Restructure exported migration rows to the target granularity.
package/restructure.js CHANGED
@@ -37,7 +37,12 @@ const restructureExportRows = async (rows, granularity, options = {}) => {
37
37
  name: row.deploy,
38
38
  dependencies: row.deps ?? [],
39
39
  deploy: row.content
40
- })), { granularity, changeName });
40
+ })), {
41
+ granularity,
42
+ changeGranularity: options.changeGranularity,
43
+ changeName,
44
+ subObjectName: identity => (0, naming_spec_1.pathFor)(identity, { style })
45
+ });
41
46
  const counters = new Map();
42
47
  const taken = new Set();
43
48
  const restructured = changes.map(change => {
@@ -29,7 +29,7 @@
29
29
  import { PathStyle } from '@pgpmjs/naming-spec';
30
30
  import type { ObjectIdentity } from '@pgsql/transform';
31
31
  import { Granularity } from '@pgsql/transform';
32
- import { GranularityChange } from './granularity-driver';
32
+ import { ChangeGranularity, GranularityChange } from './granularity-driver';
33
33
  /** How one object differs between the two sides. */
34
34
  export type ObjectDelta = 'added' | 'removed' | 'modified';
35
35
  export interface SemanticObjectDiff {
@@ -45,6 +45,8 @@ export interface SemanticObjectDiff {
45
45
  export interface SemanticDiffOptions {
46
46
  /** Granularity for the emitted CREATE/ALTER changes (default `object`). */
47
47
  granularity?: Granularity;
48
+ /** Change-level distribution for emitted changes (default `object`). */
49
+ changeGranularity?: ChangeGranularity;
48
50
  /** Naming-spec rendering style (default `directory`). */
49
51
  style?: PathStyle;
50
52
  }
@@ -32,8 +32,10 @@ exports.diffChangeSets = diffChangeSets;
32
32
  */
33
33
  const naming_spec_1 = require("@pgpmjs/naming-spec");
34
34
  const scripts_1 = require("@pgsql/scripts");
35
+ const semantics_1 = require("@pgsql/semantics");
35
36
  const transform_1 = require("@pgsql/transform");
36
37
  const plpgsql_parser_1 = require("plpgsql-parser");
38
+ const constraint_names_1 = require("./constraint-names");
37
39
  const granularity_driver_1 = require("./granularity-driver");
38
40
  const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table ?? ''}\u0000${id.name}`;
39
41
  /**
@@ -43,7 +45,7 @@ const identityKey = (id) => `${id.kind}\u0000${id.schema ?? ''}\u0000${id.table
43
45
  * statements are reported via `warnings`.
44
46
  */
45
47
  function readSide(sql, warnings, label) {
46
- const facts = (0, transform_1.classifyStatements)(sql);
48
+ const facts = (0, semantics_1.classifyStatements)(sql);
47
49
  const graph = (0, transform_1.buildStatementGraph)(facts);
48
50
  const units = new Map();
49
51
  const order = [];
@@ -103,7 +105,7 @@ function groupingIdentity(f) {
103
105
  const identity = (0, transform_1.identityOf)(f);
104
106
  if (!identity)
105
107
  return null;
106
- if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'rls_enable') &&
108
+ if ((f.kind === 'table' || f.kind === 'constraint' || f.kind === 'fk_constraint' || f.kind === 'rls_enable') &&
107
109
  identity.kind !== 'table') {
108
110
  return { kind: 'table', schema: identity.schema, name: identity.table ?? identity.name };
109
111
  }
@@ -116,21 +118,61 @@ function fingerprint(text) {
116
118
  }
117
119
  /** Multiset fingerprint of a whole unit. */
118
120
  const unitFingerprint = (unit) => unit.texts.map(fingerprint).sort().join('\n');
121
+ /** JSON with recursively sorted object keys — an insertion-order-proof fingerprint. */
122
+ function stablePrint(node) {
123
+ if (Array.isArray(node))
124
+ return `[${node.map(stablePrint).join(',')}]`;
125
+ if (node && typeof node === 'object') {
126
+ const entries = Object.entries(node)
127
+ .filter(([, v]) => v !== undefined)
128
+ .sort(([a], [b]) => a.localeCompare(b))
129
+ .map(([k, v]) => `${JSON.stringify(k)}:${stablePrint(v)}`);
130
+ return `{${entries.join(',')}}`;
131
+ }
132
+ return JSON.stringify(node);
133
+ }
134
+ /** Constraint kinds that can move between column, table-elt, and ALTER form. */
135
+ const RELOCATABLE = new Set(['CONSTR_PRIMARY', 'CONSTR_UNIQUE', 'CONSTR_FOREIGN', 'CONSTR_CHECK']);
119
136
  /**
120
137
  * Fold a table unit's statements (CREATE TABLE plus any ALTER TABLE
121
138
  * ADD COLUMN / ADD CONSTRAINT) into an effective shape, so atomic and
122
139
  * consolidated authorships of the same table compare equal.
140
+ *
141
+ * Constraint placement is representation, not semantics: `id bigint PRIMARY
142
+ * KEY`, a `PRIMARY KEY (id)` table elt, and `ALTER TABLE .. ADD CONSTRAINT
143
+ * t_pkey PRIMARY KEY (id)` all catalog identically. Relocatable constraints
144
+ * (PK/UNIQUE/FK/CHECK) are therefore lifted out of columns and ALTER
145
+ * commands into one canonical table-level set — keyed with the Postgres
146
+ * default name when unnamed, columns filled in from the owning column when
147
+ * column-attached — and PK columns gain the NOT NULL the catalog implies.
123
148
  */
124
149
  function tableShape(unit) {
125
150
  const shape = { relation: null, columns: new Map(), extras: new Map() };
151
+ const table = unit.identity.name;
152
+ const rawColumns = [];
153
+ const constraints = [];
154
+ const pkColumns = new Set();
126
155
  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) });
156
+ const colname = node.colname ?? '';
157
+ const attached = node.constraints ?? [];
158
+ const residual = [];
159
+ for (const item of attached) {
160
+ const c = item.Constraint;
161
+ if (c && RELOCATABLE.has(c.contype ?? '')) {
162
+ constraints.push({ node: c, column: colname });
163
+ }
164
+ else {
165
+ residual.push(item);
166
+ }
167
+ }
168
+ rawColumns.push({ colname, def: { ...node, constraints: residual } });
169
+ };
170
+ const addConstraint = (node, column) => {
171
+ constraints.push({ node: node, column });
130
172
  };
131
173
  const addExtra = (node) => {
132
174
  const clean = (0, transform_1.cleanTree)(node);
133
- shape.extras.set(JSON.stringify(clean), clean);
175
+ shape.extras.set(stablePrint(clean), clean);
134
176
  };
135
177
  for (const text of unit.texts) {
136
178
  for (const raw of (0, plpgsql_parser_1.parseSync)(text).sql.stmts) {
@@ -141,6 +183,9 @@ function tableShape(unit) {
141
183
  for (const elt of elts) {
142
184
  if (elt.ColumnDef)
143
185
  addColumn(elt.ColumnDef);
186
+ else if (elt.Constraint && RELOCATABLE.has(elt.Constraint.contype ?? '')) {
187
+ addConstraint(elt.Constraint);
188
+ }
144
189
  else
145
190
  addExtra(elt);
146
191
  }
@@ -156,6 +201,11 @@ function tableShape(unit) {
156
201
  if (at.subtype === 'AT_AddColumn' && def?.ColumnDef) {
157
202
  addColumn(def.ColumnDef);
158
203
  }
204
+ else if (at.subtype === 'AT_AddConstraint' &&
205
+ def?.Constraint &&
206
+ RELOCATABLE.has(def.Constraint.contype ?? '')) {
207
+ addConstraint(def.Constraint);
208
+ }
159
209
  else {
160
210
  addExtra({ AlterTableCmd: at });
161
211
  }
@@ -166,6 +216,44 @@ function tableShape(unit) {
166
216
  }
167
217
  }
168
218
  }
219
+ for (const { node, column } of constraints) {
220
+ const canonical = { ...node };
221
+ canonical.conname = node.conname ?? (0, constraint_names_1.defaultConstraintName)(table, node, column) ?? undefined;
222
+ if (column && (node.keys ?? []).length === 0 && node.contype !== 'CONSTR_FOREIGN' && node.contype !== 'CONSTR_CHECK') {
223
+ canonical.keys = [{ String: { sval: column } }];
224
+ }
225
+ if (column && node.contype === 'CONSTR_FOREIGN' && (node.fk_attrs ?? []).length === 0) {
226
+ canonical.fk_attrs = [{ String: { sval: column } }];
227
+ }
228
+ if (node.contype === 'CONSTR_PRIMARY') {
229
+ const keyed = canonical.keys ?? [];
230
+ for (const k of keyed)
231
+ if (k.String?.sval)
232
+ pkColumns.add(k.String.sval);
233
+ }
234
+ addExtra({ Constraint: canonical });
235
+ }
236
+ for (const { colname, def } of rawColumns) {
237
+ const residual = (def.constraints ?? []).map(c => {
238
+ const constraint = c.Constraint;
239
+ // NOT NULL parses with parser-version-dependent extras (is_enforced,
240
+ // initially_valid); reduce to its semantic core so an authored NOT NULL
241
+ // equals the one a primary key implies.
242
+ if (constraint?.contype === 'CONSTR_NOTNULL') {
243
+ return { Constraint: { contype: 'CONSTR_NOTNULL', conname: constraint.conname } };
244
+ }
245
+ return c;
246
+ });
247
+ const hasNotNull = residual.some(c => c.Constraint?.contype === 'CONSTR_NOTNULL');
248
+ if (pkColumns.has(colname) && !hasNotNull) {
249
+ residual.push({ Constraint: { contype: 'CONSTR_NOTNULL' } });
250
+ }
251
+ const sorted = residual
252
+ .map(c => (0, transform_1.cleanTree)(c))
253
+ .sort((x, y) => stablePrint(x).localeCompare(stablePrint(y)));
254
+ const clean = (0, transform_1.cleanTree)({ ...def, constraints: sorted });
255
+ shape.columns.set(colname, { def: clean, print: stablePrint(clean) });
256
+ }
169
257
  return shape;
170
258
  }
171
259
  const deparseStmt = (node) => `${plpgsql_parser_1.Deparser.deparse(node, { pretty: false })};`;
@@ -280,7 +368,7 @@ function diffSchemas(from, to, options = {}) {
280
368
  * statement to surface its not-derivable comment and warning.
281
369
  */
282
370
  const inverseOf = (deploy, context) => {
283
- const facts = (0, transform_1.classifyStatements)(deploy);
371
+ const facts = (0, semantics_1.classifyStatements)(deploy);
284
372
  const graph = (0, transform_1.buildStatementGraph)(facts);
285
373
  const pieces = [];
286
374
  for (const i of [...graph.order].reverse()) {
@@ -303,7 +391,7 @@ function diffSchemas(from, to, options = {}) {
303
391
  */
304
392
  const verifyOf = (deploy, context) => {
305
393
  const pieces = [];
306
- for (const fact of (0, transform_1.classifyStatements)(deploy)) {
394
+ for (const fact of (0, semantics_1.classifyStatements)(deploy)) {
307
395
  const nodes = (0, scripts_1.existenceCheck)(fact);
308
396
  if (nodes === null) {
309
397
  const { warnings: w } = (0, scripts_1.verifyFor)([fact]);
@@ -389,7 +477,11 @@ function diffSchemas(from, to, options = {}) {
389
477
  }
390
478
  let forwardChanges = [];
391
479
  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' });
480
+ const { changes, warnings: w } = (0, granularity_driver_1.restructureChanges)([{ name: 'delta', dependencies: [], deploy: forwardSql.join('\n\n') }], {
481
+ granularity: options.granularity ?? 'object',
482
+ changeGranularity: options.changeGranularity,
483
+ subObjectName: identity => (0, naming_spec_1.pathFor)(identity, { style })
484
+ });
393
485
  forwardChanges = changes.map(c => ({
394
486
  ...c,
395
487
  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/semantics';
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
+ }