@pgsql/transform 18.1.0 → 18.3.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.
Files changed (113) hide show
  1. package/README.md +29 -73
  2. package/categorize.d.ts +52 -0
  3. package/categorize.js +56 -0
  4. package/esm/categorize.js +51 -0
  5. package/esm/facts.js +289 -0
  6. package/esm/fixture-closure.js +197 -0
  7. package/esm/index.js +7 -22
  8. package/esm/qualify.js +297 -0
  9. package/esm/round-trip-core.js +126 -0
  10. package/esm/round-trip.js +116 -0
  11. package/esm/transform.js +1169 -0
  12. package/facts.d.ts +69 -0
  13. package/facts.js +292 -0
  14. package/fixture-closure.d.ts +71 -0
  15. package/fixture-closure.js +200 -0
  16. package/index.d.ts +14 -28
  17. package/index.js +43 -76
  18. package/package.json +22 -14
  19. package/qualify.d.ts +125 -0
  20. package/qualify.js +303 -0
  21. package/round-trip-core.d.ts +60 -0
  22. package/round-trip-core.js +133 -0
  23. package/round-trip.d.ts +47 -0
  24. package/round-trip.js +123 -0
  25. package/transform.d.ts +259 -0
  26. package/transform.js +1190 -0
  27. package/13/enums.d.ts +0 -62
  28. package/13/enums.js +0 -2
  29. package/13/types.d.ts +0 -2103
  30. package/13/types.js +0 -2
  31. package/14/enums.d.ts +0 -62
  32. package/14/enums.js +0 -2
  33. package/14/types.d.ts +0 -2164
  34. package/14/types.js +0 -2
  35. package/15/enums.d.ts +0 -64
  36. package/15/enums.js +0 -2
  37. package/15/types.d.ts +0 -2222
  38. package/15/types.js +0 -2
  39. package/16/enums.d.ts +0 -69
  40. package/16/enums.js +0 -2
  41. package/16/types.d.ts +0 -2328
  42. package/16/types.js +0 -2
  43. package/17/enums.d.ts +0 -76
  44. package/17/enums.js +0 -2
  45. package/17/types.d.ts +0 -2488
  46. package/17/types.js +0 -2
  47. package/18/enums.d.ts +0 -78
  48. package/18/enums.js +0 -2
  49. package/18/types.d.ts +0 -2538
  50. package/18/types.js +0 -2
  51. package/esm/13/enums.js +0 -1
  52. package/esm/13/types.js +0 -1
  53. package/esm/14/enums.js +0 -1
  54. package/esm/14/types.js +0 -1
  55. package/esm/15/enums.js +0 -1
  56. package/esm/15/types.js +0 -1
  57. package/esm/16/enums.js +0 -1
  58. package/esm/16/types.js +0 -1
  59. package/esm/17/enums.js +0 -1
  60. package/esm/17/types.js +0 -1
  61. package/esm/18/enums.js +0 -1
  62. package/esm/18/types.js +0 -1
  63. package/esm/multi-version-transformer.js +0 -80
  64. package/esm/transformers/context.js +0 -1
  65. package/esm/transformers/v13-to-v14.js +0 -2767
  66. package/esm/transformers/v14-to-v15.js +0 -1218
  67. package/esm/transformers/v15-to-v16.js +0 -2935
  68. package/esm/transformers/v16-to-v17.js +0 -1526
  69. package/esm/transformers/v17-to-v18.js +0 -204
  70. package/esm/transformers-direct/index.js +0 -13
  71. package/esm/transformers-direct/v13-to-v17/index.js +0 -74
  72. package/esm/transformers-direct/v13-to-v18/index.js +0 -84
  73. package/esm/transformers-direct/v14-to-v17/index.js +0 -63
  74. package/esm/transformers-direct/v14-to-v18/index.js +0 -73
  75. package/esm/transformers-direct/v15-to-v17/index.js +0 -53
  76. package/esm/transformers-direct/v15-to-v18/index.js +0 -62
  77. package/esm/transformers-direct/v16-to-v17/index.js +0 -40
  78. package/esm/transformers-direct/v16-to-v18/index.js +0 -51
  79. package/esm/transformers-direct/v17-to-v18/index.js +0 -39
  80. package/multi-version-transformer.d.ts +0 -14
  81. package/multi-version-transformer.js +0 -84
  82. package/transformers/context.d.ts +0 -4
  83. package/transformers/context.js +0 -2
  84. package/transformers/v13-to-v14.d.ts +0 -255
  85. package/transformers/v13-to-v14.js +0 -2771
  86. package/transformers/v14-to-v15.d.ts +0 -615
  87. package/transformers/v14-to-v15.js +0 -1222
  88. package/transformers/v15-to-v16.d.ts +0 -632
  89. package/transformers/v15-to-v16.js +0 -2939
  90. package/transformers/v16-to-v17.d.ts +0 -640
  91. package/transformers/v16-to-v17.js +0 -1530
  92. package/transformers/v17-to-v18.d.ts +0 -54
  93. package/transformers/v17-to-v18.js +0 -208
  94. package/transformers-direct/index.d.ts +0 -13
  95. package/transformers-direct/index.js +0 -25
  96. package/transformers-direct/v13-to-v17/index.d.ts +0 -27
  97. package/transformers-direct/v13-to-v17/index.js +0 -78
  98. package/transformers-direct/v13-to-v18/index.d.ts +0 -28
  99. package/transformers-direct/v13-to-v18/index.js +0 -88
  100. package/transformers-direct/v14-to-v17/index.d.ts +0 -26
  101. package/transformers-direct/v14-to-v17/index.js +0 -67
  102. package/transformers-direct/v14-to-v18/index.d.ts +0 -27
  103. package/transformers-direct/v14-to-v18/index.js +0 -77
  104. package/transformers-direct/v15-to-v17/index.d.ts +0 -25
  105. package/transformers-direct/v15-to-v17/index.js +0 -57
  106. package/transformers-direct/v15-to-v18/index.d.ts +0 -26
  107. package/transformers-direct/v15-to-v18/index.js +0 -66
  108. package/transformers-direct/v16-to-v17/index.d.ts +0 -24
  109. package/transformers-direct/v16-to-v17/index.js +0 -44
  110. package/transformers-direct/v16-to-v18/index.d.ts +0 -25
  111. package/transformers-direct/v16-to-v18/index.js +0 -55
  112. package/transformers-direct/v17-to-v18/index.d.ts +0 -24
  113. package/transformers-direct/v17-to-v18/index.js +0 -43
package/facts.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * A (possibly schema-qualified) object name extracted from a statement.
3
+ */
4
+ export interface QualifiedName {
5
+ schema: string | null;
6
+ name: string;
7
+ }
8
+ /**
9
+ * Coarse statement classification used for tier/package sorting.
10
+ */
11
+ export type StatementKind = 'schema' | 'table' | 'view' | 'index' | 'type' | 'function' | 'trigger' | 'policy' | 'grant' | 'rls_enable' | 'fk_constraint' | 'constraint' | 'comment' | 'seed_dml' | 'other';
12
+ /**
13
+ * AST-derived facts about a single top-level SQL statement.
14
+ *
15
+ * Read-only: classification never modifies the statement. Facts are the
16
+ * substrate for classifier-driven slicing (schema / functionality / security
17
+ * tiers) — replacing path/name-glob decisions with computed properties like
18
+ * "this trigger function references billing".
19
+ */
20
+ export interface StatementFacts {
21
+ /** Coarse category of the statement. */
22
+ kind: StatementKind;
23
+ /** The raw parser node tag (e.g. `CreateStmt`, `CreatePolicyStmt`). */
24
+ nodeTag: string;
25
+ /** Objects this statement creates or directly targets. */
26
+ creates: QualifiedName[];
27
+ /**
28
+ * Schema-qualified objects this statement references — tables, functions
29
+ * and types reached anywhere in the statement, including PL/pgSQL bodies.
30
+ * Unqualified references are omitted (they resolve via search_path and
31
+ * carry no cross-schema information).
32
+ */
33
+ references: QualifiedName[];
34
+ /**
35
+ * The subset of `references` reached only inside a PL/pgSQL body. These
36
+ * are late-binding: Postgres resolves them at call time, not at CREATE
37
+ * time, so they do not constrain deploy order (and legitimately form
38
+ * recursion cycles between functions).
39
+ */
40
+ bodyReferences: QualifiedName[];
41
+ /** Distinct schemas reached by `references`. */
42
+ referencedSchemas: string[];
43
+ /** Role names granted to, owning, or bound by this statement. */
44
+ roles: string[];
45
+ /** Foreign-key target tables (from column/table FK constraints). */
46
+ fkTargets: QualifiedName[];
47
+ /**
48
+ * Whether the statement is part of the security surface: policies, grants,
49
+ * RLS enable/force, security labels, ownership.
50
+ */
51
+ securityRelevant: boolean;
52
+ /** For functions: declared with SECURITY DEFINER. */
53
+ securityDefiner: boolean;
54
+ /**
55
+ * For functions: the body executes dynamic SQL (EXECUTE / EXECUTE ... USING
56
+ * / FOR ... IN EXECUTE). Analogous to `eval` — references inside the
57
+ * dynamic string are invisible to the AST, so edges from this statement
58
+ * are incomplete and slicing should treat it conservatively.
59
+ */
60
+ dynamicSql: boolean;
61
+ }
62
+ /**
63
+ * Classify each top-level statement in a SQL script into {@link StatementFacts}.
64
+ *
65
+ * Uses the same parser stack as the schema transformer (pgsql AST walk plus
66
+ * hydrated PL/pgSQL body walk), so references inside function bodies are
67
+ * included. The input SQL is never modified.
68
+ */
69
+ export declare function classifyStatements(sql: string): StatementFacts[];
package/facts.js ADDED
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyStatements = classifyStatements;
4
+ const traverse_1 = require("@pgsql/traverse");
5
+ const plpgsql_parser_1 = require("plpgsql-parser");
6
+ const SECURITY_TAGS = new Set([
7
+ 'CreatePolicyStmt',
8
+ 'AlterPolicyStmt',
9
+ 'GrantStmt',
10
+ 'GrantRoleStmt',
11
+ 'AlterDefaultPrivilegesStmt',
12
+ 'SecLabelStmt',
13
+ 'AlterOwnerStmt',
14
+ 'CreateRoleStmt',
15
+ 'AlterRoleStmt'
16
+ ]);
17
+ const KIND_BY_TAG = {
18
+ CreateSchemaStmt: 'schema',
19
+ CreateStmt: 'table',
20
+ ViewStmt: 'view',
21
+ IndexStmt: 'index',
22
+ CompositeTypeStmt: 'type',
23
+ CreateEnumStmt: 'type',
24
+ CreateDomainStmt: 'type',
25
+ CreateRangeStmt: 'type',
26
+ DefineStmt: 'type',
27
+ CreateFunctionStmt: 'function',
28
+ CreateTrigStmt: 'trigger',
29
+ CreateEventTrigStmt: 'trigger',
30
+ CreatePolicyStmt: 'policy',
31
+ AlterPolicyStmt: 'policy',
32
+ GrantStmt: 'grant',
33
+ GrantRoleStmt: 'grant',
34
+ AlterDefaultPrivilegesStmt: 'grant',
35
+ CommentStmt: 'comment',
36
+ InsertStmt: 'seed_dml',
37
+ UpdateStmt: 'seed_dml',
38
+ DeleteStmt: 'seed_dml'
39
+ };
40
+ function qn(schema, name) {
41
+ return { schema: schema ?? null, name };
42
+ }
43
+ function nameListToQualified(names) {
44
+ if (!Array.isArray(names) || names.length === 0)
45
+ return null;
46
+ const parts = names
47
+ .map((n) => n?.String?.sval)
48
+ .filter((s) => typeof s === 'string');
49
+ if (parts.length === 0)
50
+ return null;
51
+ if (parts.length === 1)
52
+ return qn(null, parts[0]);
53
+ return qn(parts[parts.length - 2], parts[parts.length - 1]);
54
+ }
55
+ function isCatalogSchema(schema) {
56
+ return schema === 'pg_catalog' || schema === 'information_schema';
57
+ }
58
+ function pushRef(refs, ref) {
59
+ if (!ref || !ref.schema || isCatalogSchema(ref.schema))
60
+ return;
61
+ if (refs.some(r => r.schema === ref.schema && r.name === ref.name))
62
+ return;
63
+ refs.push(ref);
64
+ }
65
+ function collectRoles(node, roles) {
66
+ const push = (role) => {
67
+ if (typeof role === 'string' && role.length > 0 && !roles.includes(role)) {
68
+ roles.push(role);
69
+ }
70
+ };
71
+ if (Array.isArray(node?.grantees)) {
72
+ for (const g of node.grantees)
73
+ push(g?.RoleSpec?.rolename);
74
+ }
75
+ if (Array.isArray(node?.roles)) {
76
+ for (const r of node.roles)
77
+ push(r?.RoleSpec?.rolename);
78
+ }
79
+ push(node?.newowner?.rolename);
80
+ push(node?.role?.rolename);
81
+ }
82
+ /**
83
+ * Create a read-only visitor that accumulates references, roles and FK
84
+ * targets into the provided facts object.
85
+ */
86
+ function createFactsVisitor(facts, bodyRefs) {
87
+ const push = (ref) => {
88
+ pushRef(facts.references, ref);
89
+ if (bodyRefs)
90
+ pushRef(bodyRefs, ref);
91
+ };
92
+ return {
93
+ RangeVar: (path) => {
94
+ const node = path.node;
95
+ if (node.schemaname) {
96
+ push(qn(node.schemaname, node.relname));
97
+ }
98
+ },
99
+ FuncCall: (path) => {
100
+ push(nameListToQualified(path.node.funcname));
101
+ },
102
+ TypeName: (path) => {
103
+ push(nameListToQualified(path.node.names));
104
+ },
105
+ ColumnRef: (path) => {
106
+ // schema.table.column references carry cross-schema information
107
+ const fields = path.node.fields;
108
+ if (Array.isArray(fields) && fields.length >= 3) {
109
+ const parts = fields
110
+ .map((f) => f?.String?.sval)
111
+ .filter((s) => typeof s === 'string');
112
+ if (parts.length >= 3) {
113
+ push(qn(parts[0], parts[1]));
114
+ }
115
+ }
116
+ },
117
+ Constraint: (path) => {
118
+ const node = path.node;
119
+ if (node.contype === 'CONSTR_FOREIGN' && node.pktable) {
120
+ const target = qn(node.pktable.schemaname ?? null, node.pktable.relname);
121
+ if (!facts.fkTargets.some(t => t.schema === target.schema && t.name === target.name)) {
122
+ facts.fkTargets.push(target);
123
+ }
124
+ if (target.schema)
125
+ pushRef(facts.references, target);
126
+ }
127
+ }
128
+ };
129
+ }
130
+ function classifyOne(nodeTag, node) {
131
+ const facts = {
132
+ kind: KIND_BY_TAG[nodeTag] ?? 'other',
133
+ nodeTag,
134
+ creates: [],
135
+ references: [],
136
+ referencedSchemas: [],
137
+ roles: [],
138
+ fkTargets: [],
139
+ bodyReferences: [],
140
+ securityRelevant: SECURITY_TAGS.has(nodeTag),
141
+ securityDefiner: false,
142
+ dynamicSql: false
143
+ };
144
+ switch (nodeTag) {
145
+ case 'CreateSchemaStmt':
146
+ facts.creates.push(qn(null, node.schemaname));
147
+ break;
148
+ case 'CreateStmt':
149
+ case 'ViewStmt': {
150
+ const rel = nodeTag === 'ViewStmt' ? node.view : node.relation;
151
+ if (rel)
152
+ facts.creates.push(qn(rel.schemaname ?? null, rel.relname));
153
+ break;
154
+ }
155
+ case 'IndexStmt':
156
+ if (node.relation) {
157
+ facts.creates.push(qn(node.relation.schemaname ?? null, node.idxname ?? node.relation.relname));
158
+ }
159
+ break;
160
+ case 'CreateFunctionStmt': {
161
+ const name = nameListToQualified(node.funcname);
162
+ if (name)
163
+ facts.creates.push(name);
164
+ for (const opt of node.options ?? []) {
165
+ const def = opt?.DefElem;
166
+ if (def?.defname === 'security' && def?.arg?.Boolean?.boolval === true) {
167
+ facts.securityDefiner = true;
168
+ }
169
+ }
170
+ break;
171
+ }
172
+ case 'CreateTrigStmt':
173
+ if (node.relation) {
174
+ // Trigger names are only unique per table; qualify with the table.
175
+ facts.creates.push(qn(node.relation.schemaname ?? null, `${node.relation.relname}.${node.trigname}`));
176
+ }
177
+ pushRef(facts.references, nameListToQualified(node.funcname));
178
+ break;
179
+ case 'CreatePolicyStmt':
180
+ case 'AlterPolicyStmt':
181
+ if (node.table) {
182
+ // Policy names are only unique per table; qualify with the table.
183
+ facts.creates.push(qn(node.table.schemaname ?? null, `${node.table.relname}.${node.policy_name}`));
184
+ // The guarded table lives on `node.table`; the generic RangeVar walker
185
+ // does not descend into it, so capture the reference explicitly.
186
+ if (node.table.schemaname) {
187
+ pushRef(facts.references, qn(node.table.schemaname, node.table.relname));
188
+ }
189
+ }
190
+ break;
191
+ case 'CreateSeqStmt':
192
+ if (node.sequence) {
193
+ facts.creates.push(qn(node.sequence.schemaname ?? null, node.sequence.relname));
194
+ }
195
+ break;
196
+ case 'CompositeTypeStmt':
197
+ if (node.typevar) {
198
+ facts.creates.push(qn(node.typevar.schemaname ?? null, node.typevar.relname));
199
+ }
200
+ break;
201
+ case 'CreateEnumStmt':
202
+ case 'CreateDomainStmt':
203
+ case 'CreateRangeStmt': {
204
+ const name = nameListToQualified(node.typeName ?? node.domainname);
205
+ if (name)
206
+ facts.creates.push(name);
207
+ break;
208
+ }
209
+ case 'AlterTableStmt': {
210
+ if (node.relation) {
211
+ facts.creates.push(qn(node.relation.schemaname ?? null, node.relation.relname));
212
+ }
213
+ const cmds = node.cmds ?? [];
214
+ for (const cmd of cmds) {
215
+ const subtype = cmd?.AlterTableCmd?.subtype;
216
+ if (subtype === 'AT_EnableRowSecurity' || subtype === 'AT_ForceRowSecurity' ||
217
+ subtype === 'AT_DisableRowSecurity' || subtype === 'AT_NoForceRowSecurity') {
218
+ facts.kind = 'rls_enable';
219
+ facts.securityRelevant = true;
220
+ }
221
+ else if (subtype === 'AT_AddConstraint') {
222
+ const contype = cmd?.AlterTableCmd?.def?.Constraint?.contype;
223
+ facts.kind = contype === 'CONSTR_FOREIGN' ? 'fk_constraint' : 'constraint';
224
+ }
225
+ else if (subtype === 'AT_ChangeOwner') {
226
+ facts.securityRelevant = true;
227
+ const owner = cmd?.AlterTableCmd?.newowner?.rolename;
228
+ if (owner && !facts.roles.includes(owner))
229
+ facts.roles.push(owner);
230
+ }
231
+ }
232
+ break;
233
+ }
234
+ case 'InsertStmt':
235
+ case 'UpdateStmt':
236
+ case 'DeleteStmt':
237
+ if (node.relation) {
238
+ facts.creates.push(qn(node.relation.schemaname ?? null, node.relation.relname));
239
+ }
240
+ break;
241
+ default:
242
+ break;
243
+ }
244
+ collectRoles(node, facts.roles);
245
+ return facts;
246
+ }
247
+ /**
248
+ * Classify each top-level statement in a SQL script into {@link StatementFacts}.
249
+ *
250
+ * Uses the same parser stack as the schema transformer (pgsql AST walk plus
251
+ * hydrated PL/pgSQL body walk), so references inside function bodies are
252
+ * included. The input SQL is never modified.
253
+ */
254
+ function classifyStatements(sql) {
255
+ const allFacts = [];
256
+ (0, plpgsql_parser_1.transformSync)(sql, (ctx) => {
257
+ const stmts = ctx.sql?.stmts ?? [];
258
+ for (const stmt of stmts) {
259
+ const stmtNode = stmt?.stmt;
260
+ const nodeTag = stmtNode ? Object.keys(stmtNode)[0] : 'other';
261
+ const node = stmtNode?.[nodeTag] ?? {};
262
+ const facts = classifyOne(nodeTag, node);
263
+ if (stmtNode) {
264
+ (0, traverse_1.walk)(stmtNode, createFactsVisitor(facts));
265
+ }
266
+ allFacts.push(facts);
267
+ }
268
+ for (const fn of ctx.functions ?? []) {
269
+ const facts = allFacts[fn.stmtIndex];
270
+ if (!facts || !fn.plpgsql?.hydrated)
271
+ continue;
272
+ // Refs already found outside the body (signature types, defaults)
273
+ // constrain deploy order and are not body-only.
274
+ const outer = new Set(facts.references.map(r => `${r.schema ?? '?'}.${r.name}`));
275
+ (0, plpgsql_parser_1.walk)(fn.plpgsql.hydrated, {
276
+ PLpgSQL_stmt_dynexecute: () => { facts.dynamicSql = true; },
277
+ PLpgSQL_stmt_dynfors: () => { facts.dynamicSql = true; }
278
+ }, {
279
+ walkSqlExpressions: true,
280
+ sqlVisitor: createFactsVisitor(facts, facts.bodyReferences)
281
+ });
282
+ facts.bodyReferences = facts.bodyReferences.filter(r => !outer.has(`${r.schema ?? '?'}.${r.name}`));
283
+ }
284
+ }, { hydrate: true });
285
+ for (const facts of allFacts) {
286
+ const selfRef = (r) => facts.creates.some(c => c.schema === r.schema && c.name === r.name);
287
+ facts.references = facts.references.filter(r => !selfRef(r));
288
+ facts.bodyReferences = facts.bodyReferences.filter(r => !selfRef(r));
289
+ facts.referencedSchemas = [...new Set(facts.references.map(r => r.schema).filter(Boolean))];
290
+ }
291
+ return allFacts;
292
+ }
@@ -0,0 +1,71 @@
1
+ import { CategoryProfile, ChangeCategory } from './categorize';
2
+ import { QualifiedName } from './facts';
3
+ /**
4
+ * A change plus its deploy SQL. Optional plan `dependencies` (from `pgpm.plan`)
5
+ * are followed as authoritative prerequisites in addition to AST-derived edges.
6
+ */
7
+ export interface ClosureInputChange {
8
+ name: string;
9
+ sql: string;
10
+ dependencies?: string[];
11
+ }
12
+ /** Why a change ended up in the closure. */
13
+ export type ClosureReason = 'selected' | 'prerequisite' | 'fixture';
14
+ /**
15
+ * A change included in the closure, with its category and the reason it was pulled.
16
+ */
17
+ export interface ClosureChange {
18
+ name: string;
19
+ category: ChangeCategory;
20
+ reason: ClosureReason;
21
+ }
22
+ /**
23
+ * The fixture closure of a schema slice.
24
+ *
25
+ * Everything the selected changes need to actually *work*: schema/functionality
26
+ * prerequisites they depend on, plus the fixtures that attach to them (policies,
27
+ * grants, RLS enables, role bindings, seed rows / SPRT-style support changes).
28
+ * References that nothing in the input produces are surfaced in `unresolved`
29
+ * rather than silently dropped.
30
+ */
31
+ export interface FixtureClosure {
32
+ /** Full closure (selection + prerequisites + fixtures) in input/plan order. */
33
+ order: string[];
34
+ /** Per-change detail in the same order as {@link order}. */
35
+ changes: ClosureChange[];
36
+ /** Closure members categorized as security/fixtures (attached fixtures). */
37
+ fixtures: string[];
38
+ /** Non-selected schema/functionality members pulled in as prerequisites. */
39
+ prerequisites: string[];
40
+ /** Roles required across the closure. */
41
+ roles: string[];
42
+ /** Requirements no input change produces — explicit, never dropped. */
43
+ unresolved: {
44
+ objects: QualifiedName[];
45
+ schemas: string[];
46
+ roles: string[];
47
+ };
48
+ }
49
+ export interface ResolveFixtureClosureOptions {
50
+ /** Category profile used to label changes (default {@link TIER_PROFILE}). */
51
+ profile?: CategoryProfile;
52
+ /**
53
+ * When false, do not follow AST references from selected changes to their
54
+ * schema/functionality producers (only pull attached fixtures + plan deps).
55
+ * Default true.
56
+ */
57
+ includePrerequisites?: boolean;
58
+ }
59
+ /**
60
+ * Resolve the fixture closure of a schema slice.
61
+ *
62
+ * Pure and deterministic — no I/O — and runs to a fixpoint in both directions:
63
+ * - forward: a selected change pulls in the producers of the objects/schemas/
64
+ * roles it references, plus its declared plan dependencies (prerequisites);
65
+ * - reverse: any fixture (security/fixtures-tier change) that attaches to an
66
+ * object already in the closure is pulled in (attached fixtures), and then
67
+ * resolved for its own prerequisites.
68
+ *
69
+ * Output order follows the input (plan) order, so it stays deploy-safe.
70
+ */
71
+ export declare function resolveFixtureClosure(allChanges: ClosureInputChange[], selection: string[], options?: ResolveFixtureClosureOptions): FixtureClosure;
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveFixtureClosure = resolveFixtureClosure;
4
+ const categorize_1 = require("./categorize");
5
+ const facts_1 = require("./facts");
6
+ function objectKey(schema, name) {
7
+ return `${schema ?? ''}\u0000${name}`;
8
+ }
9
+ function analyze(change, profile) {
10
+ const facts = (0, facts_1.classifyStatements)(change.sql);
11
+ const category = profile.categorize(facts, change.name);
12
+ const creates = [];
13
+ const createsSchemas = [];
14
+ const createsRoles = [];
15
+ const needsObjects = [];
16
+ const needsSchemas = new Set();
17
+ const needsRoles = new Set();
18
+ for (const f of facts) {
19
+ for (const c of f.creates) {
20
+ if (f.kind === 'schema')
21
+ createsSchemas.push(c.name);
22
+ else {
23
+ creates.push(c);
24
+ if (c.schema)
25
+ needsSchemas.add(c.schema);
26
+ }
27
+ }
28
+ if (f.nodeTag === 'CreateRoleStmt')
29
+ createsRoles.push(...f.roles);
30
+ for (const r of [...f.references, ...f.fkTargets]) {
31
+ if (r.schema)
32
+ needsObjects.push(r);
33
+ }
34
+ for (const s of f.referencedSchemas)
35
+ needsSchemas.add(s);
36
+ }
37
+ const created = new Set(createsRoles);
38
+ for (const f of facts) {
39
+ if (f.nodeTag === 'CreateRoleStmt')
40
+ continue;
41
+ for (const role of f.roles)
42
+ if (!created.has(role))
43
+ needsRoles.add(role);
44
+ }
45
+ for (const s of createsSchemas)
46
+ needsSchemas.delete(s);
47
+ return {
48
+ name: change.name,
49
+ category,
50
+ facts,
51
+ dependencies: change.dependencies ?? [],
52
+ creates,
53
+ createsSchemas,
54
+ createsRoles,
55
+ needsObjects,
56
+ needsSchemas: [...needsSchemas],
57
+ needsRoles: [...needsRoles],
58
+ isFixture: category === 'security' || category === 'fixtures'
59
+ };
60
+ }
61
+ /**
62
+ * Resolve the fixture closure of a schema slice.
63
+ *
64
+ * Pure and deterministic — no I/O — and runs to a fixpoint in both directions:
65
+ * - forward: a selected change pulls in the producers of the objects/schemas/
66
+ * roles it references, plus its declared plan dependencies (prerequisites);
67
+ * - reverse: any fixture (security/fixtures-tier change) that attaches to an
68
+ * object already in the closure is pulled in (attached fixtures), and then
69
+ * resolved for its own prerequisites.
70
+ *
71
+ * Output order follows the input (plan) order, so it stays deploy-safe.
72
+ */
73
+ function resolveFixtureClosure(allChanges, selection, options = {}) {
74
+ const profile = options.profile ?? categorize_1.TIER_PROFILE;
75
+ const includePrerequisites = options.includePrerequisites ?? true;
76
+ const analyses = allChanges.map(c => analyze(c, profile));
77
+ const byName = new Map(analyses.map(a => [a.name, a]));
78
+ const producerByObject = new Map();
79
+ const producerBySchema = new Map();
80
+ const producerByRole = new Map();
81
+ for (const a of analyses) {
82
+ for (const c of a.creates) {
83
+ const key = objectKey(c.schema, c.name);
84
+ if (!producerByObject.has(key))
85
+ producerByObject.set(key, a.name);
86
+ }
87
+ for (const s of a.createsSchemas) {
88
+ if (!producerBySchema.has(s))
89
+ producerBySchema.set(s, a.name);
90
+ }
91
+ for (const r of a.createsRoles) {
92
+ if (!producerByRole.has(r))
93
+ producerByRole.set(r, a.name);
94
+ }
95
+ }
96
+ const reason = new Map();
97
+ for (const name of selection) {
98
+ if (byName.has(name))
99
+ reason.set(name, 'selected');
100
+ }
101
+ const producedObjectKeys = new Set();
102
+ const producedSchemas = new Set();
103
+ const refreshProduced = () => {
104
+ producedObjectKeys.clear();
105
+ producedSchemas.clear();
106
+ for (const name of reason.keys()) {
107
+ const a = byName.get(name);
108
+ for (const c of a.creates)
109
+ producedObjectKeys.add(objectKey(c.schema, c.name));
110
+ for (const s of a.createsSchemas)
111
+ producedSchemas.add(s);
112
+ }
113
+ };
114
+ let changed = true;
115
+ while (changed) {
116
+ changed = false;
117
+ // forward: pull producers of what current members need
118
+ for (const name of [...reason.keys()]) {
119
+ const a = byName.get(name);
120
+ const pull = (producer, r) => {
121
+ if (!producer || reason.has(producer))
122
+ return;
123
+ const pa = byName.get(producer);
124
+ reason.set(producer, pa.isFixture ? 'fixture' : r);
125
+ changed = true;
126
+ };
127
+ for (const dep of a.dependencies) {
128
+ pull(byName.has(dep) ? dep : undefined, 'prerequisite');
129
+ }
130
+ if (!includePrerequisites)
131
+ continue;
132
+ for (const o of a.needsObjects)
133
+ pull(producerByObject.get(objectKey(o.schema, o.name)), 'prerequisite');
134
+ for (const s of a.needsSchemas)
135
+ pull(producerBySchema.get(s), 'prerequisite');
136
+ for (const role of a.needsRoles)
137
+ pull(producerByRole.get(role), 'prerequisite');
138
+ }
139
+ // reverse: pull attached fixtures that target current members
140
+ refreshProduced();
141
+ for (const a of analyses) {
142
+ if (reason.has(a.name) || !a.isFixture)
143
+ continue;
144
+ const objectAttaches = a.needsObjects.some(o => producedObjectKeys.has(objectKey(o.schema, o.name))) ||
145
+ a.creates.some(c => producedObjectKeys.has(objectKey(c.schema, c.name)));
146
+ // Schema-scoped fixtures (e.g. ALTER DEFAULT PRIVILEGES IN SCHEMA x) name no
147
+ // specific object; attach them by schema. Object-scoped fixtures attach only
148
+ // via their object refs, so a shared schema does not drag in every fixture.
149
+ const schemaScoped = a.needsObjects.length === 0 && a.creates.length === 0;
150
+ const schemaAttaches = schemaScoped &&
151
+ (a.needsSchemas.some(s => producedSchemas.has(s)) ||
152
+ a.createsSchemas.some(s => producedSchemas.has(s)));
153
+ if (objectAttaches || schemaAttaches) {
154
+ reason.set(a.name, 'fixture');
155
+ changed = true;
156
+ }
157
+ }
158
+ }
159
+ const order = analyses.filter(a => reason.has(a.name)).map(a => a.name);
160
+ const changes = order.map(name => {
161
+ const a = byName.get(name);
162
+ return { name, category: a.category, reason: reason.get(name) };
163
+ });
164
+ const roles = new Set();
165
+ const unresolvedObjects = [];
166
+ const unresolvedObjectKeys = new Set();
167
+ const unresolvedSchemas = new Set();
168
+ const unresolvedRoles = new Set();
169
+ for (const name of order) {
170
+ const a = byName.get(name);
171
+ for (const role of a.needsRoles) {
172
+ roles.add(role);
173
+ if (!producerByRole.has(role))
174
+ unresolvedRoles.add(role);
175
+ }
176
+ for (const o of a.needsObjects) {
177
+ const key = objectKey(o.schema, o.name);
178
+ if (!producerByObject.has(key) && !unresolvedObjectKeys.has(key)) {
179
+ unresolvedObjectKeys.add(key);
180
+ unresolvedObjects.push(o);
181
+ }
182
+ }
183
+ for (const s of a.needsSchemas) {
184
+ if (!producerBySchema.has(s))
185
+ unresolvedSchemas.add(s);
186
+ }
187
+ }
188
+ return {
189
+ order,
190
+ changes,
191
+ fixtures: changes.filter(c => c.reason === 'fixture').map(c => c.name),
192
+ prerequisites: changes.filter(c => c.reason === 'prerequisite').map(c => c.name),
193
+ roles: [...roles],
194
+ unresolved: {
195
+ objects: unresolvedObjects,
196
+ schemas: [...unresolvedSchemas],
197
+ roles: [...unresolvedRoles]
198
+ }
199
+ };
200
+ }
package/index.d.ts CHANGED
@@ -1,28 +1,14 @@
1
- import { Node as PG13Node } from './13/types';
2
- import { Node as PG14Node } from './14/types';
3
- import { Node as PG15Node } from './15/types';
4
- import { Node as PG16Node } from './16/types';
5
- import { Node as PG17Node } from './17/types';
6
- import { Node as PG18Node } from './18/types';
7
- import * as PG13Types from './13/types';
8
- import * as PG14Types from './14/types';
9
- import * as PG15Types from './15/types';
10
- import * as PG16Types from './16/types';
11
- import * as PG17Types from './17/types';
12
- import * as PG18Types from './18/types';
13
- export { ASTTransformer } from './multi-version-transformer';
14
- export { V13ToV14Transformer } from './transformers/v13-to-v14';
15
- export { V14ToV15Transformer } from './transformers/v14-to-v15';
16
- export { V15ToV16Transformer } from './transformers/v15-to-v16';
17
- export { V16ToV17Transformer } from './transformers/v16-to-v17';
18
- export { V17ToV18Transformer } from './transformers/v17-to-v18';
19
- export { PG13ToPG17Transformer } from './transformers-direct/v13-to-v17';
20
- export { PG14ToPG17Transformer } from './transformers-direct/v14-to-v17';
21
- export { PG15ToPG17Transformer } from './transformers-direct/v15-to-v17';
22
- export { PG16ToPG17Transformer } from './transformers-direct/v16-to-v17';
23
- export { PG13ToPG18Transformer } from './transformers-direct/v13-to-v18';
24
- export { PG14ToPG18Transformer } from './transformers-direct/v14-to-v18';
25
- export { PG15ToPG18Transformer } from './transformers-direct/v15-to-v18';
26
- export { PG16ToPG18Transformer } from './transformers-direct/v16-to-v18';
27
- export { PG17ToPG18Transformer } from './transformers-direct/v17-to-v18';
28
- export { PG13Node, PG14Node, PG15Node, PG16Node, PG17Node, PG18Node, PG13Types, PG14Types, PG15Types, PG16Types, PG17Types, PG18Types };
1
+ export type { CategoryProfile, ChangeCategory, } from './categorize';
2
+ export { buildCategoryOf, categorizeChange, TIER_PROFILE, } from './categorize';
3
+ export type { QualifiedName, StatementFacts, StatementKind, } from './facts';
4
+ export { classifyStatements } from './facts';
5
+ export type { ClosureChange, ClosureInputChange, ClosureReason, FixtureClosure, ResolveFixtureClosureOptions, } from './fixture-closure';
6
+ export { resolveFixtureClosure } from './fixture-closure';
7
+ export type { ObjectInventory, QualifyResult, QualifyRoutes, QualifyTargetSelector, QualifyUnqualifiedOptions, } from './qualify';
8
+ export { collectCreatedObjects, createQualifyVisitor, mergeInventories, qualifyUnqualified, } from './qualify';
9
+ export type { CapturedAsts } from './round-trip';
10
+ export { captureAstsFromSql, captureTransformAsts, firstDifference, normalizeParseTree, validateRoundTrip, } from './round-trip';
11
+ export type { NormalizeTreeOptions } from './round-trip-core';
12
+ export { CLEAN_TREE_VOLATILE_KEYS, cleanTree, normalizeTree, trimDefElemBody, } from './round-trip-core';
13
+ export type { SchemaTransformPass, SchemaTransformResult, TransformSqlOptions, } from './transform';
14
+ export { createPlpgsqlVisitor, createSqlVisitor, escapeRegexp, extractPgpmHeader, shouldTransformSchema, transformComments, transformJsonStringValues, transformNameList, transformPlpgsqlTypeAst, transformPlpgsqlTypeString, transformRelation, transformSchemaName, transformSql, transformSqlStatement, transformVerifyCalls, validateNoUntransformedSchemas, walkPlpgsqlForSchemas, } from './transform';