@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/bundle-driver.js CHANGED
@@ -13,8 +13,8 @@ exports.buildExtensionRouter = buildExtensionRouter;
13
13
  exports.buildSchemaRouter = buildSchemaRouter;
14
14
  exports.makeSchemaTranspiler = makeSchemaTranspiler;
15
15
  exports.makeNamespaceValidator = makeNamespaceValidator;
16
+ const semantics_1 = require("@pgsql/semantics");
16
17
  const transform_1 = require("@pgsql/transform");
17
- const transform_2 = require("@pgsql/transform");
18
18
  /**
19
19
  * Build an {@link ExtensionRouter} from the declarative {@link ExtensionRoutingInput}:
20
20
  * an explicit `routes` spec when given, otherwise the "move everything matched
@@ -27,8 +27,8 @@ function buildExtensionRouter(input) {
27
27
  if (input.inventory)
28
28
  opts.inventory = input.inventory;
29
29
  if (input.routes)
30
- return new transform_2.ExtensionRouter(input.routes, opts);
31
- return transform_2.ExtensionRouter.toSchema(input.toSchema ?? null, {
30
+ return new transform_1.ExtensionRouter(input.routes, opts);
31
+ return transform_1.ExtensionRouter.toSchema(input.toSchema ?? null, {
32
32
  ...opts,
33
33
  extensions: input.only,
34
34
  from: input.from
@@ -75,7 +75,7 @@ function buildSchemaRouter(options) {
75
75
  ...(route.toName !== undefined ? { name: route.toName } : {})
76
76
  };
77
77
  }
78
- return new transform_2.SchemaRouter(spec);
78
+ return new transform_1.SchemaRouter(spec);
79
79
  }
80
80
  /**
81
81
  * Rewrite the schema segment of a pgpm change path to the object's routed
@@ -125,14 +125,14 @@ function makeSchemaTranspiler(options) {
125
125
  // after schema routing on each script. Each is a self-contained parse →
126
126
  // rewrite → deparse pass, so they compose by threading the SQL through.
127
127
  const extRouter = options.extensions ? buildExtensionRouter(options.extensions) : undefined;
128
- const roleRouter = options.roles ? transform_2.RoleRouter.from(options.roles) : undefined;
129
- const extensionResult = extRouter ? (0, transform_2.createExtensionResult)() : undefined;
130
- const roleResult = roleRouter ? (0, transform_2.createRoleResult)() : undefined;
128
+ const roleRouter = options.roles ? transform_1.RoleRouter.from(options.roles) : undefined;
129
+ const extensionResult = extRouter ? (0, transform_1.createExtensionResult)() : undefined;
130
+ const roleResult = roleRouter ? (0, transform_1.createRoleResult)() : undefined;
131
131
  const renameChange = (name) => renameChangePath(name, router);
132
132
  const transformScript = (sql, _ctx) => {
133
- let out = (0, transform_2.transformSql)(sql, router, options.transform, result).content;
133
+ let out = (0, transform_1.transformSql)(sql, router, options.transform, result).content;
134
134
  if (extRouter) {
135
- const r = (0, transform_2.transformExtensions)(out, extRouter);
135
+ const r = (0, transform_1.transformExtensions)(out, extRouter);
136
136
  out = r.sql;
137
137
  for (const [name, schema] of r.result.installsMoved) {
138
138
  extensionResult.installsMoved.set(name, schema);
@@ -142,7 +142,7 @@ function makeSchemaTranspiler(options) {
142
142
  }
143
143
  }
144
144
  if (roleRouter) {
145
- const r = (0, transform_2.transformRoles)(out, roleRouter);
145
+ const r = (0, transform_1.transformRoles)(out, roleRouter);
146
146
  out = r.sql;
147
147
  for (const [name, count] of r.result.rolesRenamed) {
148
148
  roleResult.rolesRenamed.set(name, (roleResult.rolesRenamed.get(name) ?? 0) + count);
@@ -162,7 +162,7 @@ function makeNamespaceValidator(options) {
162
162
  const allowed = new Set(options.allowedSchemas);
163
163
  return (sql, _ctx) => {
164
164
  const violations = new Set();
165
- for (const facts of (0, transform_1.classifyStatements)(sql)) {
165
+ for (const facts of (0, semantics_1.classifyStatements)(sql)) {
166
166
  for (const ref of [...facts.creates, ...facts.references, ...facts.fkTargets]) {
167
167
  if (ref.schema && !allowed.has(ref.schema)) {
168
168
  violations.add(`${facts.nodeTag}: ${ref.schema}.${ref.name}`);
package/categorize.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { StatementFacts } from '@pgsql/transform';
1
+ import { StatementFacts } from '@pgsql/semantics';
2
2
  /**
3
3
  * A category is a chunk-seam key a change is assigned to. Free-form string; the
4
4
  * built-in {@link TIER_PROFILE} uses `'security' | 'functionality' | 'fixtures'
package/categorize.js CHANGED
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TIER_PROFILE = void 0;
4
4
  exports.categorizeChange = categorizeChange;
5
5
  exports.buildCategoryOf = buildCategoryOf;
6
- const transform_1 = require("@pgsql/transform");
6
+ const semantics_1 = require("@pgsql/semantics");
7
7
  /**
8
8
  * Default tier profile: isolates the security surface and procedural code out
9
9
  * of the schema base. Precedence, highest first — a change is placed in the
@@ -37,7 +37,7 @@ exports.TIER_PROFILE = {
37
37
  * (defaults to {@link TIER_PROFILE}).
38
38
  */
39
39
  function categorizeChange(sql, changeName, profile = exports.TIER_PROFILE) {
40
- return profile.categorize((0, transform_1.classifyStatements)(sql), changeName);
40
+ return profile.categorize((0, semantics_1.classifyStatements)(sql), changeName);
41
41
  }
42
42
  /**
43
43
  * Build a `categoryOf(changeName)` seam function from a map of change name to
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Postgres default constraint names.
3
+ *
4
+ * When a constraint is authored without a name, Postgres derives one at
5
+ * deploy time (`ChooseConstraintName` in the server): `{table}_pkey` for
6
+ * primary keys, `{table}_{cols}_key` for unique constraints,
7
+ * `{table}_{col}_fkey` for foreign keys, `{table}_{col}_check` (first
8
+ * referenced column) or `{table}_check` for check constraints. Synthesizing
9
+ * the same names makes unnamed authorship comparable to (and revertible as)
10
+ * what the catalog will actually contain.
11
+ */
12
+ /** A parsed `Constraint` node (structural — the fields this module reads). */
13
+ export interface ConstraintNode {
14
+ contype?: string;
15
+ conname?: string;
16
+ keys?: {
17
+ String?: {
18
+ sval?: string;
19
+ };
20
+ }[];
21
+ fk_attrs?: {
22
+ String?: {
23
+ sval?: string;
24
+ };
25
+ }[];
26
+ raw_expr?: unknown;
27
+ }
28
+ /** First `ColumnRef` column name reached in an expression tree, DFS order. */
29
+ export declare function firstColumnRef(node: unknown): string | null;
30
+ /**
31
+ * The name Postgres would assign to an unnamed constraint on `table`, given
32
+ * the constraint node and (for column-attached constraints) the owning
33
+ * column. Returns `null` for kinds without a stable derivation (exclusion
34
+ * constraints and anything unrecognized).
35
+ */
36
+ export declare function defaultConstraintName(table: string, constraint: ConstraintNode, column?: string): string | null;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ /**
3
+ * Postgres default constraint names.
4
+ *
5
+ * When a constraint is authored without a name, Postgres derives one at
6
+ * deploy time (`ChooseConstraintName` in the server): `{table}_pkey` for
7
+ * primary keys, `{table}_{cols}_key` for unique constraints,
8
+ * `{table}_{col}_fkey` for foreign keys, `{table}_{col}_check` (first
9
+ * referenced column) or `{table}_check` for check constraints. Synthesizing
10
+ * the same names makes unnamed authorship comparable to (and revertible as)
11
+ * what the catalog will actually contain.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.firstColumnRef = firstColumnRef;
15
+ exports.defaultConstraintName = defaultConstraintName;
16
+ const svals = (items) => (items ?? []).map(k => k.String?.sval).filter((s) => typeof s === 'string');
17
+ /** First `ColumnRef` column name reached in an expression tree, DFS order. */
18
+ function firstColumnRef(node) {
19
+ if (Array.isArray(node)) {
20
+ for (const item of node) {
21
+ const found = firstColumnRef(item);
22
+ if (found)
23
+ return found;
24
+ }
25
+ return null;
26
+ }
27
+ if (node && typeof node === 'object') {
28
+ const record = node;
29
+ const ref = record.ColumnRef;
30
+ if (ref?.fields) {
31
+ const names = svals(ref.fields);
32
+ if (names.length > 0)
33
+ return names[names.length - 1];
34
+ }
35
+ for (const value of Object.values(record)) {
36
+ const found = firstColumnRef(value);
37
+ if (found)
38
+ return found;
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+ /**
44
+ * The name Postgres would assign to an unnamed constraint on `table`, given
45
+ * the constraint node and (for column-attached constraints) the owning
46
+ * column. Returns `null` for kinds without a stable derivation (exclusion
47
+ * constraints and anything unrecognized).
48
+ */
49
+ function defaultConstraintName(table, constraint, column) {
50
+ const cols = () => {
51
+ const keyed = svals(constraint.keys);
52
+ if (keyed.length > 0)
53
+ return keyed;
54
+ return column ? [column] : [];
55
+ };
56
+ switch (constraint.contype) {
57
+ case 'CONSTR_PRIMARY':
58
+ return `${table}_pkey`;
59
+ case 'CONSTR_UNIQUE': {
60
+ const names = cols();
61
+ return names.length > 0 ? `${table}_${names.join('_')}_key` : null;
62
+ }
63
+ case 'CONSTR_FOREIGN': {
64
+ const attrs = svals(constraint.fk_attrs);
65
+ const names = attrs.length > 0 ? attrs : column ? [column] : [];
66
+ return names.length > 0 ? `${table}_${names.join('_')}_fkey` : null;
67
+ }
68
+ case 'CONSTR_CHECK': {
69
+ const col = column ?? firstColumnRef(constraint.raw_expr);
70
+ return col ? `${table}_${col}_check` : `${table}_check`;
71
+ }
72
+ default:
73
+ return null;
74
+ }
75
+ }
@@ -7,7 +7,7 @@
7
7
  * purpose — no dependency on `@pgpmjs/bundle`/`@pgpmjs/core` — so the driver
8
8
  * stays a pure function factory over `transformSql` and `classifyStatements`.
9
9
  */
10
- import { classifyStatements } from '@pgsql/transform';
10
+ import { classifyStatements } from '@pgsql/semantics';
11
11
  import { createExtensionResult, createRoleResult, ExtensionRouter, RoleRouter, SchemaRouter, transformExtensions, transformRoles, transformSql } from '@pgsql/transform';
12
12
  /**
13
13
  * Build an {@link ExtensionRouter} from the declarative {@link ExtensionRoutingInput}:
package/esm/categorize.js CHANGED
@@ -1,4 +1,4 @@
1
- import { classifyStatements } from '@pgsql/transform';
1
+ import { classifyStatements } from '@pgsql/semantics';
2
2
  /**
3
3
  * Default tier profile: isolates the security surface and procedural code out
4
4
  * of the schema base. Precedence, highest first — a change is placed in the
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Postgres default constraint names.
3
+ *
4
+ * When a constraint is authored without a name, Postgres derives one at
5
+ * deploy time (`ChooseConstraintName` in the server): `{table}_pkey` for
6
+ * primary keys, `{table}_{cols}_key` for unique constraints,
7
+ * `{table}_{col}_fkey` for foreign keys, `{table}_{col}_check` (first
8
+ * referenced column) or `{table}_check` for check constraints. Synthesizing
9
+ * the same names makes unnamed authorship comparable to (and revertible as)
10
+ * what the catalog will actually contain.
11
+ */
12
+ const svals = (items) => (items ?? []).map(k => k.String?.sval).filter((s) => typeof s === 'string');
13
+ /** First `ColumnRef` column name reached in an expression tree, DFS order. */
14
+ export function firstColumnRef(node) {
15
+ if (Array.isArray(node)) {
16
+ for (const item of node) {
17
+ const found = firstColumnRef(item);
18
+ if (found)
19
+ return found;
20
+ }
21
+ return null;
22
+ }
23
+ if (node && typeof node === 'object') {
24
+ const record = node;
25
+ const ref = record.ColumnRef;
26
+ if (ref?.fields) {
27
+ const names = svals(ref.fields);
28
+ if (names.length > 0)
29
+ return names[names.length - 1];
30
+ }
31
+ for (const value of Object.values(record)) {
32
+ const found = firstColumnRef(value);
33
+ if (found)
34
+ return found;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ /**
40
+ * The name Postgres would assign to an unnamed constraint on `table`, given
41
+ * the constraint node and (for column-attached constraints) the owning
42
+ * column. Returns `null` for kinds without a stable derivation (exclusion
43
+ * constraints and anything unrecognized).
44
+ */
45
+ export function defaultConstraintName(table, constraint, column) {
46
+ const cols = () => {
47
+ const keyed = svals(constraint.keys);
48
+ if (keyed.length > 0)
49
+ return keyed;
50
+ return column ? [column] : [];
51
+ };
52
+ switch (constraint.contype) {
53
+ case 'CONSTR_PRIMARY':
54
+ return `${table}_pkey`;
55
+ case 'CONSTR_UNIQUE': {
56
+ const names = cols();
57
+ return names.length > 0 ? `${table}_${names.join('_')}_key` : null;
58
+ }
59
+ case 'CONSTR_FOREIGN': {
60
+ const attrs = svals(constraint.fk_attrs);
61
+ const names = attrs.length > 0 ? attrs : column ? [column] : [];
62
+ return names.length > 0 ? `${table}_${names.join('_')}_fkey` : null;
63
+ }
64
+ case 'CONSTR_CHECK': {
65
+ const col = column ?? firstColumnRef(constraint.raw_expr);
66
+ return col ? `${table}_${col}_check` : `${table}_check`;
67
+ }
68
+ default:
69
+ return null;
70
+ }
71
+ }
@@ -1,4 +1,4 @@
1
- import { classifyStatements } from '@pgsql/transform';
1
+ import { classifyStatements } from '@pgsql/semantics';
2
2
  import { TIER_PROFILE } from './categorize';
3
3
  function objectKey(schema, name) {
4
4
  return `${schema ?? ''}\u0000${name}`;
@@ -1,25 +1,10 @@
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
1
  import { pathFor } from '@pgpmjs/naming-spec';
21
2
  import { revertFor, verifyFor } from '@pgsql/scripts';
22
- import { buildStatementGraph, classifyStatements, identityOf, restructureSql } from '@pgsql/transform';
3
+ import { classifyStatements } from '@pgsql/semantics';
4
+ import { buildStatementGraph, identityOf, restructureSql } from '@pgsql/transform';
5
+ import { nameUnnamedConstraints, subObjectIdentityOf } from './sub-object';
6
+ export const CHANGE_GRANULARITIES = ['alteration', 'object', 'single'];
7
+ export const isChangeGranularity = (value) => CHANGE_GRANULARITIES.includes(value);
23
8
  const KIND_DIRS = {
24
9
  table: 'tables',
25
10
  view: 'views',
@@ -53,22 +38,50 @@ export function defaultChangeName(facts) {
53
38
  */
54
39
  export function restructureChanges(changes, options) {
55
40
  const nameFor = options.changeName ?? defaultChangeName;
41
+ const subNameFor = options.subObjectName ?? ((identity) => pathFor(identity));
42
+ const alteration = options.changeGranularity === 'alteration';
56
43
  const flattened = changes
57
44
  .map(c => c.deploy.trim())
58
45
  .filter(Boolean)
59
46
  .join('\n\n');
60
- const { sql, warnings } = restructureSql(flattened, {
47
+ const restructured = restructureSql(flattened, {
61
48
  granularity: options.granularity
62
49
  });
50
+ const warnings = restructured.warnings;
51
+ const sql = alteration ? nameUnnamedConstraints(restructured.sql) : restructured.sql;
63
52
  // Re-classify the emitted script; group statements by the object they
64
53
  // target (creates[0]), so a table's CREATE and its remaining ALTERs land
65
54
  // in the same change regardless of statement kind.
66
55
  const facts = classifyStatements(sql);
56
+ if (options.changeGranularity === 'single') {
57
+ const name = options.singleChangeName ?? 'module/init';
58
+ const inputNames = new Set(changes.map(c => c.name));
59
+ const dependencies = [...new Set(changes.flatMap(c => c.dependencies).filter(d => !inputNames.has(d)))].sort();
60
+ const statements = facts
61
+ .map(f => sql.slice(f.span.start, f.span.start + f.span.len).trim())
62
+ .filter(Boolean)
63
+ .map(text => (text.endsWith(';') ? text : `${text};`));
64
+ const revert = revertFor(facts);
65
+ const verify = verifyFor(facts);
66
+ for (const warning of [...revert.warnings, ...verify.warnings]) {
67
+ warnings.push(`${name}: ${warning}`);
68
+ }
69
+ return {
70
+ changes: statements.length > 0
71
+ ? [{ name, dependencies, deploy: statements.join('\n\n'), revert: revert.sql, verify: verify.sql }]
72
+ : [],
73
+ warnings
74
+ };
75
+ }
67
76
  const graph = buildStatementGraph(facts);
68
77
  const groupOf = new Array(facts.length).fill(-1);
69
78
  const groupKeys = [];
70
79
  const groupFacts = [];
71
80
  const groupKeyToIndex = new Map();
81
+ /** Sub-object path for alteration groups; null means name via `nameFor`. */
82
+ const groupSubName = [];
83
+ /** For alteration groups: the object-group key of the owning table. */
84
+ const groupOwnerKey = [];
72
85
  facts.forEach((f, i) => {
73
86
  const created = f.creates[0];
74
87
  if (!created) {
@@ -78,12 +91,31 @@ export function restructureChanges(changes, options) {
78
91
  groupOf[i] = groupOf[i - 1];
79
92
  return;
80
93
  }
94
+ if (alteration) {
95
+ const sub = subObjectIdentityOf(f);
96
+ if (sub) {
97
+ const key = `sub\u0000${sub.kind}\u0000${sub.schema ?? ''}\u0000${sub.table}\u0000${sub.name}`;
98
+ let g = groupKeyToIndex.get(key);
99
+ if (g === undefined) {
100
+ g = groupKeys.length;
101
+ groupKeys.push(key);
102
+ groupFacts.push(f);
103
+ groupSubName.push(subNameFor(sub));
104
+ groupOwnerKey.push(`${sub.schema ?? ''}.${sub.table}`);
105
+ groupKeyToIndex.set(key, g);
106
+ }
107
+ groupOf[i] = g;
108
+ return;
109
+ }
110
+ }
81
111
  const key = `${created.schema ?? ''}.${created.name}`;
82
112
  let g = groupKeyToIndex.get(key);
83
113
  if (g === undefined) {
84
114
  g = groupKeys.length;
85
115
  groupKeys.push(key);
86
116
  groupFacts.push(f);
117
+ groupSubName.push(null);
118
+ groupOwnerKey.push(null);
87
119
  groupKeyToIndex.set(key, g);
88
120
  }
89
121
  else if (!(groupFacts[g].kind in KIND_DIRS) && groupFacts[g].kind !== 'schema' && (f.kind in KIND_DIRS || f.kind === 'schema')) {
@@ -92,7 +124,7 @@ export function restructureChanges(changes, options) {
92
124
  }
93
125
  groupOf[i] = g;
94
126
  });
95
- const groupNames = groupFacts.map(nameFor);
127
+ const groupNames = groupFacts.map((f, g) => groupSubName[g] ?? nameFor(f));
96
128
  // Schema producers, for schema-level change dependencies.
97
129
  const schemaGroup = new Map();
98
130
  facts.forEach((f, i) => {
@@ -138,6 +170,38 @@ export function restructureChanges(changes, options) {
138
170
  groupDeps[from].add(to);
139
171
  }
140
172
  });
173
+ // Alteration groups depend on their table's own change, and constraint
174
+ // groups additionally on the column changes they key on — the statement
175
+ // graph keys everything by the table, so these finer edges are added here.
176
+ if (alteration) {
177
+ const columnGroup = new Map();
178
+ groupKeys.forEach((key, g) => {
179
+ const parts = key.split('\u0000');
180
+ if (parts[0] === 'sub' && parts[1] === 'column') {
181
+ columnGroup.set(`${parts[2]}.${parts[3]}.${parts[4]}`, g);
182
+ }
183
+ });
184
+ groupKeys.forEach((key, g) => {
185
+ const owner = groupOwnerKey[g];
186
+ if (owner === null)
187
+ return;
188
+ const tableGroup = groupKeyToIndex.get(owner);
189
+ if (tableGroup !== undefined && tableGroup !== g)
190
+ groupDeps[g].add(tableGroup);
191
+ const parts = key.split('\u0000');
192
+ if (parts[1] !== 'constraint')
193
+ return;
194
+ const constraint = groupFacts[g].stmt?.AlterTableStmt?.cmds?.[0]?.AlterTableCmd?.def?.Constraint;
195
+ const referenced = [...(constraint?.keys ?? []), ...(constraint?.fk_attrs ?? [])]
196
+ .map(k => k.String?.sval)
197
+ .filter((s) => typeof s === 'string');
198
+ for (const col of referenced) {
199
+ const to = columnGroup.get(`${owner}.${col}`);
200
+ if (to !== undefined && to !== g)
201
+ groupDeps[g].add(to);
202
+ }
203
+ });
204
+ }
141
205
  // Emit groups in first-statement order (already topological).
142
206
  const order = [...groupNames.keys()].sort((a, b) => {
143
207
  const firstA = groupOf.indexOf(a);
package/esm/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  export { buildExtensionRouter, buildSchemaRouter, makeNamespaceValidator, makeSchemaTranspiler, } from './bundle-driver';
2
2
  export { diffCatalogSnapshots, snapshotCatalog, withoutColumnOrder } from './catalog-check';
3
3
  export { buildCategoryOf, categorizeChange, TIER_PROFILE, } from './categorize';
4
+ export { defaultConstraintName, firstColumnRef } from './constraint-names';
4
5
  export { resolveFixtureClosure } from './fixture-closure';
5
- export { defaultChangeName, restructureChanges, } from './granularity-driver';
6
- export { checkOverwrite, writeControlFile, writeModule } from './module-emit';
6
+ export { CHANGE_GRANULARITIES, defaultChangeName, isChangeGranularity, restructureChanges, } from './granularity-driver';
7
+ export { appendModule, checkOverwrite, writeControlFile, writeModule } from './module-emit';
7
8
  export { loadModuleSource, stripTransactionWrapper } from './module-source';
8
9
  export { parsePartitionConfig, partitionExportRows } from './partition';
9
10
  export { PartitionCycleError, partitionUnits, RESIDUAL_UNIT_PATH, } from './partition-driver';
@@ -11,5 +12,6 @@ export { emitSqlProgram, parseSqlProgram } from './program';
11
12
  export { classifyScript, isStubScript, regenerateScripts, } from './regen';
12
13
  export { EXPORT_GRANULARITIES, isExportGranularity, restructureExportRows } from './restructure';
13
14
  export { diffChangeSets, diffSchemas, } from './semantic-diff-driver';
15
+ export { nameUnnamedConstraints, subObjectIdentityOf } from './sub-object';
14
16
  export * from '@pgsql/transform';
15
17
  export { loadModule } from 'plpgsql-parser';
@@ -9,7 +9,7 @@
9
9
  * `pgpm import` all emit byte-identical module layouts instead of each
10
10
  * carrying its own copy of the writer.
11
11
  */
12
- import { writePgpmFiles, writePgpmPlan } from '@pgpmjs/ast';
12
+ import { parseAuthor, parsePlanFile, writePgpmFiles, writePgpmPlan, writePlanFile } from '@pgpmjs/ast';
13
13
  import * as fs from 'fs';
14
14
  import * as path from 'path';
15
15
  /** Write a module's Postgres extension `.control` file. */
@@ -48,6 +48,76 @@ export const writeModule = (outBase, model, extraRequires = []) => {
48
48
  writePgpmFiles(model.rows, opts);
49
49
  return dir;
50
50
  };
51
+ /**
52
+ * Append plan-ordered {@link PgpmRow}s into an *existing* module rather than
53
+ * writing a fresh package. Existing changes, their scripts, and the `.control`
54
+ * file are left untouched; only the new changes are written (deploy/revert/
55
+ * verify) and appended to `pgpm.plan` after the current changes.
56
+ *
57
+ * Rows whose change name already exists are skipped (never overwritten).
58
+ * A new change's dependency bracket is filtered to names that resolve within
59
+ * the plan (existing changes, other appended changes, or `pkg:`-external
60
+ * references); a dangling internal dependency is dropped with a warning
61
+ * (plan order still sequences it after the current changes).
62
+ */
63
+ export const appendModule = (moduleDir, rows, options = {}) => {
64
+ const planPath = path.join(moduleDir, 'pgpm.plan');
65
+ if (!fs.existsSync(planPath)) {
66
+ throw new Error(`No pgpm.plan found at ${planPath}; append mode expects an existing pgpm module directory.`);
67
+ }
68
+ const parsed = parsePlanFile(planPath);
69
+ if (!parsed.data) {
70
+ throw new Error(`Failed to parse ${planPath}: ${parsed.errors
71
+ .map(e => `line ${e.line}: ${e.message}`)
72
+ .join('; ')}`);
73
+ }
74
+ const plan = parsed.data;
75
+ const existingNames = new Set(plan.changes.map(c => c.name));
76
+ const incomingNames = new Set(rows.map(r => r.deploy));
77
+ const appended = new Set();
78
+ const added = [];
79
+ const skipped = [];
80
+ const warnings = [];
81
+ const newRows = [];
82
+ const { fullName, email } = parseAuthor(options.author || 'constructive');
83
+ const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
84
+ for (const row of rows) {
85
+ if (existingNames.has(row.deploy) || appended.has(row.deploy)) {
86
+ skipped.push(row.deploy);
87
+ warnings.push(`change ${row.deploy} already exists in ${plan.package}; left untouched`);
88
+ continue;
89
+ }
90
+ const deps = (row.deps ?? []).filter(dep => {
91
+ if (dep.includes(':'))
92
+ return true; // cross-package (pkg:change) external
93
+ if (existingNames.has(dep) || incomingNames.has(dep))
94
+ return true;
95
+ warnings.push(`change ${row.deploy}: dropped dependency ${dep} (not present in ${plan.package})`);
96
+ return false;
97
+ });
98
+ plan.changes.push({
99
+ name: row.deploy,
100
+ dependencies: deps,
101
+ timestamp,
102
+ planner: fullName,
103
+ email: email || `${fullName}@constructive.io`,
104
+ comment: `add ${row.name ?? row.deploy}`
105
+ });
106
+ newRows.push({ ...row, deps });
107
+ appended.add(row.deploy);
108
+ added.push(row.deploy);
109
+ }
110
+ if (newRows.length) {
111
+ const opts = {
112
+ outdir: path.dirname(moduleDir),
113
+ name: path.basename(moduleDir),
114
+ replacer: (str) => str
115
+ };
116
+ writePgpmFiles(newRows, opts);
117
+ writePlanFile(planPath, plan);
118
+ }
119
+ return { dir: moduleDir, added, skipped, warnings };
120
+ };
51
121
  /**
52
122
  * Guard against clobbering: writing into the source directory requires
53
123
  * `--write`, as does overwriting any existing package directory.
@@ -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 { pathFor } from '@pgpmjs/naming-spec';
24
- import { buildStatementGraph, classifyStatements, identityOf } from '@pgsql/transform';
24
+ import { classifyStatements } from '@pgsql/semantics';
25
+ import { buildStatementGraph, identityOf } from '@pgsql/transform';
25
26
  /** A partition whose packages require each other in a cycle — unshippable. */
26
27
  export class PartitionCycleError extends Error {
27
28
  packages;
package/esm/program.js CHANGED
@@ -1,4 +1,4 @@
1
- import { classifyStatements } from '@pgsql/transform';
1
+ import { classifyStatements } from '@pgsql/semantics';
2
2
  import { Deparser, parseSql } from 'plpgsql-parser';
3
3
  /**
4
4
  * Parse a SQL script into a statement program. Requires `loadModule()` from
package/esm/regen.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { revertFor, verifyFor } from '@pgsql/scripts';
2
- import { classifyStatements } from '@pgsql/transform';
2
+ import { classifyStatements } from '@pgsql/semantics';
3
3
  /**
4
4
  * Classify a pgpm script's substantive statements: every top-level statement
5
5
  * except transaction control (`BEGIN`/`COMMIT`/`ROLLBACK` wrappers).
@@ -33,7 +33,12 @@ export const restructureExportRows = async (rows, granularity, options = {}) =>
33
33
  name: row.deploy,
34
34
  dependencies: row.deps ?? [],
35
35
  deploy: row.content
36
- })), { granularity, changeName });
36
+ })), {
37
+ granularity,
38
+ changeGranularity: options.changeGranularity,
39
+ changeName,
40
+ subObjectName: identity => pathFor(identity, { style })
41
+ });
37
42
  const counters = new Map();
38
43
  const taken = new Set();
39
44
  const restructured = changes.map(change => {