@pgsql/transform 18.3.0 → 18.3.1
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/README.md +0 -8
- package/esm/index.js +0 -2
- package/index.d.ts +0 -4
- package/index.js +1 -7
- package/package.json +2 -2
- package/categorize.d.ts +0 -52
- package/categorize.js +0 -56
- package/esm/categorize.js +0 -51
- package/esm/fixture-closure.js +0 -197
- package/fixture-closure.d.ts +0 -71
- package/fixture-closure.js +0 -200
package/README.md
CHANGED
|
@@ -54,14 +54,6 @@ const facts = classifyStatements(sql);
|
|
|
54
54
|
|
|
55
55
|
Qualify unqualified object references against an inventory of known objects, with multi-schema routing support.
|
|
56
56
|
|
|
57
|
-
### `resolveFixtureClosure` — transitive dependency closure
|
|
58
|
-
|
|
59
|
-
Given a set of changes (name + SQL + optional declared dependencies), compute the transitive closure of a selection: forward producers of every referenced object/schema/role, plus attached fixtures (policies/grants/RLS targeting closure members), with explicit unresolved-reference reporting.
|
|
60
|
-
|
|
61
|
-
### `categorizeChange` / `buildCategoryOf` — change categorization
|
|
62
|
-
|
|
63
|
-
Profile-driven categorization of migration changes (e.g. schema / functionality / security / fixtures) from their AST facts.
|
|
64
|
-
|
|
65
57
|
### Round-trip validation
|
|
66
58
|
|
|
67
59
|
`normalizeTree` / `cleanTree` / `validateRoundTrip` — dependency-free AST normalization and mutation-aware parse→deparse→re-parse validation.
|
package/esm/index.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
export { buildCategoryOf, categorizeChange, TIER_PROFILE, } from './categorize';
|
|
2
1
|
export { classifyStatements } from './facts';
|
|
3
|
-
export { resolveFixtureClosure } from './fixture-closure';
|
|
4
2
|
export { collectCreatedObjects, createQualifyVisitor, mergeInventories, qualifyUnqualified, } from './qualify';
|
|
5
3
|
export { captureAstsFromSql, captureTransformAsts, firstDifference, normalizeParseTree, validateRoundTrip, } from './round-trip';
|
|
6
4
|
export { CLEAN_TREE_VOLATILE_KEYS, cleanTree, normalizeTree, trimDefElemBody, } from './round-trip-core';
|
package/index.d.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
export type { CategoryProfile, ChangeCategory, } from './categorize';
|
|
2
|
-
export { buildCategoryOf, categorizeChange, TIER_PROFILE, } from './categorize';
|
|
3
1
|
export type { QualifiedName, StatementFacts, StatementKind, } from './facts';
|
|
4
2
|
export { classifyStatements } from './facts';
|
|
5
|
-
export type { ClosureChange, ClosureInputChange, ClosureReason, FixtureClosure, ResolveFixtureClosureOptions, } from './fixture-closure';
|
|
6
|
-
export { resolveFixtureClosure } from './fixture-closure';
|
|
7
3
|
export type { ObjectInventory, QualifyResult, QualifyRoutes, QualifyTargetSelector, QualifyUnqualifiedOptions, } from './qualify';
|
|
8
4
|
export { collectCreatedObjects, createQualifyVisitor, mergeInventories, qualifyUnqualified, } from './qualify';
|
|
9
5
|
export type { CapturedAsts } from './round-trip';
|
package/index.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.walkPlpgsqlForSchemas = exports.validateNoUntransformedSchemas = exports.transformVerifyCalls = exports.transformSqlStatement = exports.transformSql = exports.transformSchemaName = exports.transformRelation = exports.transformPlpgsqlTypeString = exports.transformPlpgsqlTypeAst = exports.transformNameList = exports.transformJsonStringValues = exports.transformComments = exports.shouldTransformSchema = exports.extractPgpmHeader = exports.escapeRegexp = exports.createSqlVisitor = exports.createPlpgsqlVisitor = exports.trimDefElemBody = exports.normalizeTree = exports.cleanTree = exports.CLEAN_TREE_VOLATILE_KEYS = exports.validateRoundTrip = exports.normalizeParseTree = exports.firstDifference = exports.captureTransformAsts = exports.captureAstsFromSql = exports.qualifyUnqualified = exports.mergeInventories = exports.createQualifyVisitor = exports.collectCreatedObjects = exports.
|
|
4
|
-
var categorize_1 = require("./categorize");
|
|
5
|
-
Object.defineProperty(exports, "buildCategoryOf", { enumerable: true, get: function () { return categorize_1.buildCategoryOf; } });
|
|
6
|
-
Object.defineProperty(exports, "categorizeChange", { enumerable: true, get: function () { return categorize_1.categorizeChange; } });
|
|
7
|
-
Object.defineProperty(exports, "TIER_PROFILE", { enumerable: true, get: function () { return categorize_1.TIER_PROFILE; } });
|
|
3
|
+
exports.walkPlpgsqlForSchemas = exports.validateNoUntransformedSchemas = exports.transformVerifyCalls = exports.transformSqlStatement = exports.transformSql = exports.transformSchemaName = exports.transformRelation = exports.transformPlpgsqlTypeString = exports.transformPlpgsqlTypeAst = exports.transformNameList = exports.transformJsonStringValues = exports.transformComments = exports.shouldTransformSchema = exports.extractPgpmHeader = exports.escapeRegexp = exports.createSqlVisitor = exports.createPlpgsqlVisitor = exports.trimDefElemBody = exports.normalizeTree = exports.cleanTree = exports.CLEAN_TREE_VOLATILE_KEYS = exports.validateRoundTrip = exports.normalizeParseTree = exports.firstDifference = exports.captureTransformAsts = exports.captureAstsFromSql = exports.qualifyUnqualified = exports.mergeInventories = exports.createQualifyVisitor = exports.collectCreatedObjects = exports.classifyStatements = void 0;
|
|
8
4
|
var facts_1 = require("./facts");
|
|
9
5
|
Object.defineProperty(exports, "classifyStatements", { enumerable: true, get: function () { return facts_1.classifyStatements; } });
|
|
10
|
-
var fixture_closure_1 = require("./fixture-closure");
|
|
11
|
-
Object.defineProperty(exports, "resolveFixtureClosure", { enumerable: true, get: function () { return fixture_closure_1.resolveFixtureClosure; } });
|
|
12
6
|
var qualify_1 = require("./qualify");
|
|
13
7
|
Object.defineProperty(exports, "collectCreatedObjects", { enumerable: true, get: function () { return qualify_1.collectCreatedObjects; } });
|
|
14
8
|
Object.defineProperty(exports, "createQualifyVisitor", { enumerable: true, get: function () { return qualify_1.createQualifyVisitor; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgsql/transform",
|
|
3
|
-
"version": "18.3.
|
|
3
|
+
"version": "18.3.1",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "AST-based SQL transformation, qualification, classification and closure analysis for PostgreSQL",
|
|
6
6
|
"main": "index.js",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"classify",
|
|
50
50
|
"plpgsql"
|
|
51
51
|
],
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "ef29a3eb49d020f309f7289dbf1fd1e642ed5008"
|
|
53
53
|
}
|
package/categorize.d.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { StatementFacts } from './facts';
|
|
2
|
-
/**
|
|
3
|
-
* A category is a chunk-seam key a change is assigned to. Free-form string; the
|
|
4
|
-
* built-in {@link TIER_PROFILE} uses `'security' | 'functionality' | 'fixtures'
|
|
5
|
-
* | 'schema'`.
|
|
6
|
-
*
|
|
7
|
-
* This module is the classifier-driven counterpart to `@pgpmjs/core`'s
|
|
8
|
-
* `boundary: 'category'` rebundle mode: core stays classifier-agnostic and only
|
|
9
|
-
* consumes a `categoryOf(changeName)` seam function, while the rules that decide
|
|
10
|
-
* *what* each change is (the profile) live here, next to the AST facts.
|
|
11
|
-
*/
|
|
12
|
-
export type ChangeCategory = string;
|
|
13
|
-
/**
|
|
14
|
-
* Decides a change's category from the AST facts of its (deploy) statements.
|
|
15
|
-
* A profile is a pure function of the facts — swap it to carve a monolith along
|
|
16
|
-
* different seams (e.g. per-role admin/users modules) without touching core.
|
|
17
|
-
*/
|
|
18
|
-
export interface CategoryProfile {
|
|
19
|
-
/** Identifier for the profile (for logging / provenance). */
|
|
20
|
-
name: string;
|
|
21
|
-
/** Map a change's statement facts to its category. */
|
|
22
|
-
categorize: (facts: StatementFacts[], changeName: string) => ChangeCategory;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Default tier profile: isolates the security surface and procedural code out
|
|
26
|
-
* of the schema base. Precedence, highest first — a change is placed in the
|
|
27
|
-
* most specialized tier any of its statements reaches:
|
|
28
|
-
*
|
|
29
|
-
* security any securityRelevant statement (policy/grant/RLS/role/owner)
|
|
30
|
-
* functionality functions and triggers
|
|
31
|
-
* fixtures seed DML (INSERT/UPDATE/DELETE)
|
|
32
|
-
* schema schemas, tables, views, indexes, types, constraints (base)
|
|
33
|
-
*
|
|
34
|
-
* Ordering/dependencies across the resulting chunks are still resolved by
|
|
35
|
-
* core's rebundle graph, so this only decides *naming/grouping*, never deploy
|
|
36
|
-
* order.
|
|
37
|
-
*/
|
|
38
|
-
export declare const TIER_PROFILE: CategoryProfile;
|
|
39
|
-
/**
|
|
40
|
-
* Classify a single change's deploy SQL into its category under `profile`
|
|
41
|
-
* (defaults to {@link TIER_PROFILE}).
|
|
42
|
-
*/
|
|
43
|
-
export declare function categorizeChange(sql: string, changeName: string, profile?: CategoryProfile): ChangeCategory;
|
|
44
|
-
/**
|
|
45
|
-
* Build a `categoryOf(changeName)` seam function from a map of change name to
|
|
46
|
-
* its deploy SQL — ready to pass straight to `@pgpmjs/core`'s rebundle
|
|
47
|
-
* `boundary: 'category'` / `categoryOf` options.
|
|
48
|
-
*
|
|
49
|
-
* Changes absent from `changeSql` return `undefined`, letting core fall back to
|
|
50
|
-
* its folder key for anything the classifier did not see.
|
|
51
|
-
*/
|
|
52
|
-
export declare function buildCategoryOf(changeSql: Record<string, string>, profile?: CategoryProfile): (changeName: string) => ChangeCategory | undefined;
|
package/categorize.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TIER_PROFILE = void 0;
|
|
4
|
-
exports.categorizeChange = categorizeChange;
|
|
5
|
-
exports.buildCategoryOf = buildCategoryOf;
|
|
6
|
-
const facts_1 = require("./facts");
|
|
7
|
-
/**
|
|
8
|
-
* Default tier profile: isolates the security surface and procedural code out
|
|
9
|
-
* of the schema base. Precedence, highest first — a change is placed in the
|
|
10
|
-
* most specialized tier any of its statements reaches:
|
|
11
|
-
*
|
|
12
|
-
* security any securityRelevant statement (policy/grant/RLS/role/owner)
|
|
13
|
-
* functionality functions and triggers
|
|
14
|
-
* fixtures seed DML (INSERT/UPDATE/DELETE)
|
|
15
|
-
* schema schemas, tables, views, indexes, types, constraints (base)
|
|
16
|
-
*
|
|
17
|
-
* Ordering/dependencies across the resulting chunks are still resolved by
|
|
18
|
-
* core's rebundle graph, so this only decides *naming/grouping*, never deploy
|
|
19
|
-
* order.
|
|
20
|
-
*/
|
|
21
|
-
exports.TIER_PROFILE = {
|
|
22
|
-
name: 'tier',
|
|
23
|
-
categorize(facts) {
|
|
24
|
-
if (facts.length === 0)
|
|
25
|
-
return 'schema';
|
|
26
|
-
if (facts.some(f => f.securityRelevant))
|
|
27
|
-
return 'security';
|
|
28
|
-
if (facts.some(f => f.kind === 'function' || f.kind === 'trigger'))
|
|
29
|
-
return 'functionality';
|
|
30
|
-
if (facts.some(f => f.kind === 'seed_dml'))
|
|
31
|
-
return 'fixtures';
|
|
32
|
-
return 'schema';
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Classify a single change's deploy SQL into its category under `profile`
|
|
37
|
-
* (defaults to {@link TIER_PROFILE}).
|
|
38
|
-
*/
|
|
39
|
-
function categorizeChange(sql, changeName, profile = exports.TIER_PROFILE) {
|
|
40
|
-
return profile.categorize((0, facts_1.classifyStatements)(sql), changeName);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Build a `categoryOf(changeName)` seam function from a map of change name to
|
|
44
|
-
* its deploy SQL — ready to pass straight to `@pgpmjs/core`'s rebundle
|
|
45
|
-
* `boundary: 'category'` / `categoryOf` options.
|
|
46
|
-
*
|
|
47
|
-
* Changes absent from `changeSql` return `undefined`, letting core fall back to
|
|
48
|
-
* its folder key for anything the classifier did not see.
|
|
49
|
-
*/
|
|
50
|
-
function buildCategoryOf(changeSql, profile = exports.TIER_PROFILE) {
|
|
51
|
-
const categories = new Map();
|
|
52
|
-
for (const [name, sql] of Object.entries(changeSql)) {
|
|
53
|
-
categories.set(name, categorizeChange(sql, name, profile));
|
|
54
|
-
}
|
|
55
|
-
return (changeName) => categories.get(changeName);
|
|
56
|
-
}
|
package/esm/categorize.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { classifyStatements } from './facts';
|
|
2
|
-
/**
|
|
3
|
-
* Default tier profile: isolates the security surface and procedural code out
|
|
4
|
-
* of the schema base. Precedence, highest first — a change is placed in the
|
|
5
|
-
* most specialized tier any of its statements reaches:
|
|
6
|
-
*
|
|
7
|
-
* security any securityRelevant statement (policy/grant/RLS/role/owner)
|
|
8
|
-
* functionality functions and triggers
|
|
9
|
-
* fixtures seed DML (INSERT/UPDATE/DELETE)
|
|
10
|
-
* schema schemas, tables, views, indexes, types, constraints (base)
|
|
11
|
-
*
|
|
12
|
-
* Ordering/dependencies across the resulting chunks are still resolved by
|
|
13
|
-
* core's rebundle graph, so this only decides *naming/grouping*, never deploy
|
|
14
|
-
* order.
|
|
15
|
-
*/
|
|
16
|
-
export const TIER_PROFILE = {
|
|
17
|
-
name: 'tier',
|
|
18
|
-
categorize(facts) {
|
|
19
|
-
if (facts.length === 0)
|
|
20
|
-
return 'schema';
|
|
21
|
-
if (facts.some(f => f.securityRelevant))
|
|
22
|
-
return 'security';
|
|
23
|
-
if (facts.some(f => f.kind === 'function' || f.kind === 'trigger'))
|
|
24
|
-
return 'functionality';
|
|
25
|
-
if (facts.some(f => f.kind === 'seed_dml'))
|
|
26
|
-
return 'fixtures';
|
|
27
|
-
return 'schema';
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
/**
|
|
31
|
-
* Classify a single change's deploy SQL into its category under `profile`
|
|
32
|
-
* (defaults to {@link TIER_PROFILE}).
|
|
33
|
-
*/
|
|
34
|
-
export function categorizeChange(sql, changeName, profile = TIER_PROFILE) {
|
|
35
|
-
return profile.categorize(classifyStatements(sql), changeName);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Build a `categoryOf(changeName)` seam function from a map of change name to
|
|
39
|
-
* its deploy SQL — ready to pass straight to `@pgpmjs/core`'s rebundle
|
|
40
|
-
* `boundary: 'category'` / `categoryOf` options.
|
|
41
|
-
*
|
|
42
|
-
* Changes absent from `changeSql` return `undefined`, letting core fall back to
|
|
43
|
-
* its folder key for anything the classifier did not see.
|
|
44
|
-
*/
|
|
45
|
-
export function buildCategoryOf(changeSql, profile = TIER_PROFILE) {
|
|
46
|
-
const categories = new Map();
|
|
47
|
-
for (const [name, sql] of Object.entries(changeSql)) {
|
|
48
|
-
categories.set(name, categorizeChange(sql, name, profile));
|
|
49
|
-
}
|
|
50
|
-
return (changeName) => categories.get(changeName);
|
|
51
|
-
}
|
package/esm/fixture-closure.js
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { TIER_PROFILE } from './categorize';
|
|
2
|
-
import { classifyStatements } from './facts';
|
|
3
|
-
function objectKey(schema, name) {
|
|
4
|
-
return `${schema ?? ''}\u0000${name}`;
|
|
5
|
-
}
|
|
6
|
-
function analyze(change, profile) {
|
|
7
|
-
const facts = classifyStatements(change.sql);
|
|
8
|
-
const category = profile.categorize(facts, change.name);
|
|
9
|
-
const creates = [];
|
|
10
|
-
const createsSchemas = [];
|
|
11
|
-
const createsRoles = [];
|
|
12
|
-
const needsObjects = [];
|
|
13
|
-
const needsSchemas = new Set();
|
|
14
|
-
const needsRoles = new Set();
|
|
15
|
-
for (const f of facts) {
|
|
16
|
-
for (const c of f.creates) {
|
|
17
|
-
if (f.kind === 'schema')
|
|
18
|
-
createsSchemas.push(c.name);
|
|
19
|
-
else {
|
|
20
|
-
creates.push(c);
|
|
21
|
-
if (c.schema)
|
|
22
|
-
needsSchemas.add(c.schema);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
if (f.nodeTag === 'CreateRoleStmt')
|
|
26
|
-
createsRoles.push(...f.roles);
|
|
27
|
-
for (const r of [...f.references, ...f.fkTargets]) {
|
|
28
|
-
if (r.schema)
|
|
29
|
-
needsObjects.push(r);
|
|
30
|
-
}
|
|
31
|
-
for (const s of f.referencedSchemas)
|
|
32
|
-
needsSchemas.add(s);
|
|
33
|
-
}
|
|
34
|
-
const created = new Set(createsRoles);
|
|
35
|
-
for (const f of facts) {
|
|
36
|
-
if (f.nodeTag === 'CreateRoleStmt')
|
|
37
|
-
continue;
|
|
38
|
-
for (const role of f.roles)
|
|
39
|
-
if (!created.has(role))
|
|
40
|
-
needsRoles.add(role);
|
|
41
|
-
}
|
|
42
|
-
for (const s of createsSchemas)
|
|
43
|
-
needsSchemas.delete(s);
|
|
44
|
-
return {
|
|
45
|
-
name: change.name,
|
|
46
|
-
category,
|
|
47
|
-
facts,
|
|
48
|
-
dependencies: change.dependencies ?? [],
|
|
49
|
-
creates,
|
|
50
|
-
createsSchemas,
|
|
51
|
-
createsRoles,
|
|
52
|
-
needsObjects,
|
|
53
|
-
needsSchemas: [...needsSchemas],
|
|
54
|
-
needsRoles: [...needsRoles],
|
|
55
|
-
isFixture: category === 'security' || category === 'fixtures'
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Resolve the fixture closure of a schema slice.
|
|
60
|
-
*
|
|
61
|
-
* Pure and deterministic — no I/O — and runs to a fixpoint in both directions:
|
|
62
|
-
* - forward: a selected change pulls in the producers of the objects/schemas/
|
|
63
|
-
* roles it references, plus its declared plan dependencies (prerequisites);
|
|
64
|
-
* - reverse: any fixture (security/fixtures-tier change) that attaches to an
|
|
65
|
-
* object already in the closure is pulled in (attached fixtures), and then
|
|
66
|
-
* resolved for its own prerequisites.
|
|
67
|
-
*
|
|
68
|
-
* Output order follows the input (plan) order, so it stays deploy-safe.
|
|
69
|
-
*/
|
|
70
|
-
export function resolveFixtureClosure(allChanges, selection, options = {}) {
|
|
71
|
-
const profile = options.profile ?? TIER_PROFILE;
|
|
72
|
-
const includePrerequisites = options.includePrerequisites ?? true;
|
|
73
|
-
const analyses = allChanges.map(c => analyze(c, profile));
|
|
74
|
-
const byName = new Map(analyses.map(a => [a.name, a]));
|
|
75
|
-
const producerByObject = new Map();
|
|
76
|
-
const producerBySchema = new Map();
|
|
77
|
-
const producerByRole = new Map();
|
|
78
|
-
for (const a of analyses) {
|
|
79
|
-
for (const c of a.creates) {
|
|
80
|
-
const key = objectKey(c.schema, c.name);
|
|
81
|
-
if (!producerByObject.has(key))
|
|
82
|
-
producerByObject.set(key, a.name);
|
|
83
|
-
}
|
|
84
|
-
for (const s of a.createsSchemas) {
|
|
85
|
-
if (!producerBySchema.has(s))
|
|
86
|
-
producerBySchema.set(s, a.name);
|
|
87
|
-
}
|
|
88
|
-
for (const r of a.createsRoles) {
|
|
89
|
-
if (!producerByRole.has(r))
|
|
90
|
-
producerByRole.set(r, a.name);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const reason = new Map();
|
|
94
|
-
for (const name of selection) {
|
|
95
|
-
if (byName.has(name))
|
|
96
|
-
reason.set(name, 'selected');
|
|
97
|
-
}
|
|
98
|
-
const producedObjectKeys = new Set();
|
|
99
|
-
const producedSchemas = new Set();
|
|
100
|
-
const refreshProduced = () => {
|
|
101
|
-
producedObjectKeys.clear();
|
|
102
|
-
producedSchemas.clear();
|
|
103
|
-
for (const name of reason.keys()) {
|
|
104
|
-
const a = byName.get(name);
|
|
105
|
-
for (const c of a.creates)
|
|
106
|
-
producedObjectKeys.add(objectKey(c.schema, c.name));
|
|
107
|
-
for (const s of a.createsSchemas)
|
|
108
|
-
producedSchemas.add(s);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
let changed = true;
|
|
112
|
-
while (changed) {
|
|
113
|
-
changed = false;
|
|
114
|
-
// forward: pull producers of what current members need
|
|
115
|
-
for (const name of [...reason.keys()]) {
|
|
116
|
-
const a = byName.get(name);
|
|
117
|
-
const pull = (producer, r) => {
|
|
118
|
-
if (!producer || reason.has(producer))
|
|
119
|
-
return;
|
|
120
|
-
const pa = byName.get(producer);
|
|
121
|
-
reason.set(producer, pa.isFixture ? 'fixture' : r);
|
|
122
|
-
changed = true;
|
|
123
|
-
};
|
|
124
|
-
for (const dep of a.dependencies) {
|
|
125
|
-
pull(byName.has(dep) ? dep : undefined, 'prerequisite');
|
|
126
|
-
}
|
|
127
|
-
if (!includePrerequisites)
|
|
128
|
-
continue;
|
|
129
|
-
for (const o of a.needsObjects)
|
|
130
|
-
pull(producerByObject.get(objectKey(o.schema, o.name)), 'prerequisite');
|
|
131
|
-
for (const s of a.needsSchemas)
|
|
132
|
-
pull(producerBySchema.get(s), 'prerequisite');
|
|
133
|
-
for (const role of a.needsRoles)
|
|
134
|
-
pull(producerByRole.get(role), 'prerequisite');
|
|
135
|
-
}
|
|
136
|
-
// reverse: pull attached fixtures that target current members
|
|
137
|
-
refreshProduced();
|
|
138
|
-
for (const a of analyses) {
|
|
139
|
-
if (reason.has(a.name) || !a.isFixture)
|
|
140
|
-
continue;
|
|
141
|
-
const objectAttaches = a.needsObjects.some(o => producedObjectKeys.has(objectKey(o.schema, o.name))) ||
|
|
142
|
-
a.creates.some(c => producedObjectKeys.has(objectKey(c.schema, c.name)));
|
|
143
|
-
// Schema-scoped fixtures (e.g. ALTER DEFAULT PRIVILEGES IN SCHEMA x) name no
|
|
144
|
-
// specific object; attach them by schema. Object-scoped fixtures attach only
|
|
145
|
-
// via their object refs, so a shared schema does not drag in every fixture.
|
|
146
|
-
const schemaScoped = a.needsObjects.length === 0 && a.creates.length === 0;
|
|
147
|
-
const schemaAttaches = schemaScoped &&
|
|
148
|
-
(a.needsSchemas.some(s => producedSchemas.has(s)) ||
|
|
149
|
-
a.createsSchemas.some(s => producedSchemas.has(s)));
|
|
150
|
-
if (objectAttaches || schemaAttaches) {
|
|
151
|
-
reason.set(a.name, 'fixture');
|
|
152
|
-
changed = true;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const order = analyses.filter(a => reason.has(a.name)).map(a => a.name);
|
|
157
|
-
const changes = order.map(name => {
|
|
158
|
-
const a = byName.get(name);
|
|
159
|
-
return { name, category: a.category, reason: reason.get(name) };
|
|
160
|
-
});
|
|
161
|
-
const roles = new Set();
|
|
162
|
-
const unresolvedObjects = [];
|
|
163
|
-
const unresolvedObjectKeys = new Set();
|
|
164
|
-
const unresolvedSchemas = new Set();
|
|
165
|
-
const unresolvedRoles = new Set();
|
|
166
|
-
for (const name of order) {
|
|
167
|
-
const a = byName.get(name);
|
|
168
|
-
for (const role of a.needsRoles) {
|
|
169
|
-
roles.add(role);
|
|
170
|
-
if (!producerByRole.has(role))
|
|
171
|
-
unresolvedRoles.add(role);
|
|
172
|
-
}
|
|
173
|
-
for (const o of a.needsObjects) {
|
|
174
|
-
const key = objectKey(o.schema, o.name);
|
|
175
|
-
if (!producerByObject.has(key) && !unresolvedObjectKeys.has(key)) {
|
|
176
|
-
unresolvedObjectKeys.add(key);
|
|
177
|
-
unresolvedObjects.push(o);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
for (const s of a.needsSchemas) {
|
|
181
|
-
if (!producerBySchema.has(s))
|
|
182
|
-
unresolvedSchemas.add(s);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return {
|
|
186
|
-
order,
|
|
187
|
-
changes,
|
|
188
|
-
fixtures: changes.filter(c => c.reason === 'fixture').map(c => c.name),
|
|
189
|
-
prerequisites: changes.filter(c => c.reason === 'prerequisite').map(c => c.name),
|
|
190
|
-
roles: [...roles],
|
|
191
|
-
unresolved: {
|
|
192
|
-
objects: unresolvedObjects,
|
|
193
|
-
schemas: [...unresolvedSchemas],
|
|
194
|
-
roles: [...unresolvedRoles]
|
|
195
|
-
}
|
|
196
|
-
};
|
|
197
|
-
}
|
package/fixture-closure.d.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
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;
|
package/fixture-closure.js
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
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
|
-
}
|