@pgsql/transform 18.4.1 → 18.6.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/esm/extension-router.js +236 -0
- package/esm/extension-transform.js +202 -0
- package/esm/facts.js +95 -1
- package/esm/index.js +4 -0
- package/esm/role-router.js +62 -0
- package/esm/role-transform.js +129 -0
- package/extension-router.d.ts +161 -0
- package/extension-router.js +240 -0
- package/extension-transform.d.ts +48 -0
- package/extension-transform.js +207 -0
- package/facts.d.ts +37 -1
- package/facts.js +94 -0
- package/index.d.ts +9 -1
- package/index.js +14 -1
- package/package.json +2 -2
- package/role-router.d.ts +42 -0
- package/role-router.js +66 -0
- package/role-transform.d.ts +50 -0
- package/role-transform.js +134 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* AST transform that applies an {@link ExtensionRouter}.
|
|
4
|
+
*
|
|
5
|
+
* Unlike the schema transform — which only rewrites string *fields* already
|
|
6
|
+
* present on a node — moving an extension changes the **shape** of the AST:
|
|
7
|
+
*
|
|
8
|
+
* - `CREATE EXTENSION <e>` with no `SCHEMA` clause must gain one, which means
|
|
9
|
+
* constructing a new `DefElem` node and attaching it to a (possibly absent)
|
|
10
|
+
* `options` array; the reverse strips the `DefElem` (and empties `options`).
|
|
11
|
+
* - a bare `crypt(...)` reference gains a schema qualifier (a `String` node is
|
|
12
|
+
* prepended to `funcname`), and the reverse removes it.
|
|
13
|
+
*
|
|
14
|
+
* All rewrites are AST-precise and driven by the router's version-aware symbol
|
|
15
|
+
* inventory, so built-ins that merely share a name with an extension symbol
|
|
16
|
+
* (and symbols that have graduated into core) are left untouched.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.createExtensionResult = createExtensionResult;
|
|
20
|
+
exports.createExtensionVisitor = createExtensionVisitor;
|
|
21
|
+
exports.transformExtensions = transformExtensions;
|
|
22
|
+
const traverse_1 = require("@pgsql/traverse");
|
|
23
|
+
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
24
|
+
const extension_router_1 = require("./extension-router");
|
|
25
|
+
/** Create a fresh result tracker. */
|
|
26
|
+
function createExtensionResult() {
|
|
27
|
+
return { installsMoved: new Map(), symbolsRewritten: new Map() };
|
|
28
|
+
}
|
|
29
|
+
function schemaStringNode(schema) {
|
|
30
|
+
return { String: { sval: schema } };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Rewrite a (possibly schema-qualified) name list in place against the router.
|
|
34
|
+
* `names` is the node's own array (`funcname`, `TypeName.names`, ...); it is
|
|
35
|
+
* mutated in place so the enclosing node keeps referring to it.
|
|
36
|
+
*/
|
|
37
|
+
function rewriteNameList(names, router, ns, result) {
|
|
38
|
+
if (!Array.isArray(names) || names.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
const parts = names.map((n) => n?.String?.sval);
|
|
41
|
+
if (parts.some(p => typeof p !== 'string'))
|
|
42
|
+
return; // non-identifier element (e.g. %type)
|
|
43
|
+
const name = parts[parts.length - 1];
|
|
44
|
+
const schema = parts.length >= 2 ? parts[parts.length - 2] : null;
|
|
45
|
+
const rewrite = router.resolveSymbol(schema, name, ns);
|
|
46
|
+
if (!rewrite)
|
|
47
|
+
return;
|
|
48
|
+
if (rewrite.to === null) {
|
|
49
|
+
// Strip qualification: keep only the bare name node.
|
|
50
|
+
names.splice(0, names.length - 1);
|
|
51
|
+
}
|
|
52
|
+
else if (schema === null) {
|
|
53
|
+
// Qualify a bare reference: prepend the target schema.
|
|
54
|
+
names.unshift(schemaStringNode(rewrite.to));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Requalify: rewrite the existing schema element.
|
|
58
|
+
names[names.length - 2].String.sval = rewrite.to;
|
|
59
|
+
}
|
|
60
|
+
result.symbolsRewritten.set(name, (result.symbolsRewritten.get(name) ?? 0) + 1);
|
|
61
|
+
}
|
|
62
|
+
/** Read/replace the `schema` DefElem inside a `CreateExtensionStmt.options`. */
|
|
63
|
+
function setExtensionSchemaOption(node, target) {
|
|
64
|
+
const options = Array.isArray(node.options) ? node.options : [];
|
|
65
|
+
const idx = options.findIndex((opt) => opt?.DefElem?.defname === 'schema');
|
|
66
|
+
if (target === null) {
|
|
67
|
+
// Install with no explicit SCHEMA clause: remove any existing option.
|
|
68
|
+
if (idx === -1)
|
|
69
|
+
return false;
|
|
70
|
+
options.splice(idx, 1);
|
|
71
|
+
if (options.length === 0)
|
|
72
|
+
delete node.options;
|
|
73
|
+
else
|
|
74
|
+
node.options = options;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const defElem = {
|
|
78
|
+
DefElem: {
|
|
79
|
+
defname: 'schema',
|
|
80
|
+
arg: { String: { sval: target } },
|
|
81
|
+
defaction: 'DEFELEM_UNSPEC'
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
if (idx === -1) {
|
|
85
|
+
options.push(defElem);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const existing = options[idx].DefElem;
|
|
89
|
+
if (existing.arg?.String?.sval === target)
|
|
90
|
+
return false;
|
|
91
|
+
existing.arg = { String: { sval: target } };
|
|
92
|
+
}
|
|
93
|
+
node.options = options;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Create a SQL AST visitor that applies extension routing. Composable with the
|
|
98
|
+
* walkers used by the core transform and by PL/pgSQL body traversal.
|
|
99
|
+
*/
|
|
100
|
+
function createExtensionVisitor(router, result) {
|
|
101
|
+
const rewriteBody = router.hasSymbolRoutes();
|
|
102
|
+
return {
|
|
103
|
+
CreateExtensionStmt: (path) => {
|
|
104
|
+
const node = path.node;
|
|
105
|
+
const target = router.resolveInstall(node.extname);
|
|
106
|
+
if (target === undefined)
|
|
107
|
+
return;
|
|
108
|
+
if (setExtensionSchemaOption(node, target)) {
|
|
109
|
+
result.installsMoved.set(node.extname, target);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
// ALTER EXTENSION <e> SET SCHEMA <newschema>. The walker does not recurse
|
|
113
|
+
// into AlterObjectSchemaStmt.object; a null target has no SET SCHEMA form
|
|
114
|
+
// (a schema must be named), so it is left unchanged.
|
|
115
|
+
AlterObjectSchemaStmt: (path) => {
|
|
116
|
+
const node = path.node;
|
|
117
|
+
if (node.objectType !== 'OBJECT_EXTENSION')
|
|
118
|
+
return;
|
|
119
|
+
const extname = node.object?.String?.sval;
|
|
120
|
+
const target = router.resolveInstall(extname);
|
|
121
|
+
if (typeof target !== 'string' || target === node.newschema)
|
|
122
|
+
return;
|
|
123
|
+
node.newschema = target;
|
|
124
|
+
result.installsMoved.set(extname, target);
|
|
125
|
+
},
|
|
126
|
+
FuncCall: (path) => {
|
|
127
|
+
rewriteNameList(path.node.funcname, router, 'function', result);
|
|
128
|
+
},
|
|
129
|
+
// The walker does not auto-recurse into CallStmt.funccall.
|
|
130
|
+
CallStmt: (path) => {
|
|
131
|
+
if (path.node.funccall) {
|
|
132
|
+
rewriteNameList(path.node.funccall.funcname, router, 'function', result);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
TypeName: (path) => {
|
|
136
|
+
rewriteNameList(path.node.names, router, 'type', result);
|
|
137
|
+
},
|
|
138
|
+
CreateFunctionStmt: (path) => {
|
|
139
|
+
const node = path.node;
|
|
140
|
+
if (!rewriteBody)
|
|
141
|
+
return;
|
|
142
|
+
// LANGUAGE sql bodies are opaque strings to the walker; parse and rewrite
|
|
143
|
+
// them separately (PL/pgSQL bodies ride the hydrated walk in transformExtensions).
|
|
144
|
+
const isPlpgsql = (node.options ?? []).some((opt) => opt?.DefElem?.defname === 'language' &&
|
|
145
|
+
opt.DefElem.arg?.String?.sval === 'plpgsql');
|
|
146
|
+
if (isPlpgsql || !Array.isArray(node.options))
|
|
147
|
+
return;
|
|
148
|
+
for (const opt of node.options) {
|
|
149
|
+
if (opt?.DefElem?.defname === 'as' && opt.DefElem.arg?.List?.items) {
|
|
150
|
+
for (const item of opt.DefElem.arg.List.items) {
|
|
151
|
+
if (typeof item?.String?.sval === 'string') {
|
|
152
|
+
try {
|
|
153
|
+
item.String.sval = rewriteSqlBodyString(item.String.sval, router, result);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// Not parseable standalone (e.g. C symbol names) — leave as-is.
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Rewrite extension references inside a `LANGUAGE sql` body string. */
|
|
166
|
+
function rewriteSqlBodyString(body, router, result) {
|
|
167
|
+
const stmts = (0, plpgsql_parser_1.parseSql)(body)?.stmts ?? [];
|
|
168
|
+
if (stmts.length === 0)
|
|
169
|
+
return body;
|
|
170
|
+
const visitor = createExtensionVisitor(router, result);
|
|
171
|
+
const pieces = [];
|
|
172
|
+
for (const stmt of stmts) {
|
|
173
|
+
if (!stmt?.stmt)
|
|
174
|
+
continue;
|
|
175
|
+
(0, traverse_1.walk)(stmt.stmt, visitor);
|
|
176
|
+
pieces.push(plpgsql_parser_1.Deparser.deparse(stmt.stmt));
|
|
177
|
+
}
|
|
178
|
+
return pieces.join(';\n');
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Apply extension routing to a SQL string: parse -> walk -> deparse, including
|
|
182
|
+
* PL/pgSQL and `LANGUAGE sql` bodies. Returns the rewritten SQL and a summary
|
|
183
|
+
* of what changed.
|
|
184
|
+
*/
|
|
185
|
+
function transformExtensions(sql, router, options = {}) {
|
|
186
|
+
const resolved = extension_router_1.ExtensionRouter.from(router, options);
|
|
187
|
+
const result = createExtensionResult();
|
|
188
|
+
const out = (0, plpgsql_parser_1.transformSync)(sql, (ctx) => {
|
|
189
|
+
const stmts = ctx.sql?.stmts ?? [];
|
|
190
|
+
const visitor = createExtensionVisitor(resolved, result);
|
|
191
|
+
for (const stmt of stmts) {
|
|
192
|
+
if (stmt?.stmt)
|
|
193
|
+
(0, traverse_1.walk)(stmt.stmt, visitor);
|
|
194
|
+
}
|
|
195
|
+
if (resolved.hasSymbolRoutes()) {
|
|
196
|
+
for (const fn of ctx.functions ?? []) {
|
|
197
|
+
if (fn.plpgsql?.hydrated) {
|
|
198
|
+
(0, plpgsql_parser_1.walk)(fn.plpgsql.hydrated, {}, {
|
|
199
|
+
walkSqlExpressions: true,
|
|
200
|
+
sqlVisitor: visitor
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}, { hydrate: true, pretty: true });
|
|
206
|
+
return { sql: out, result };
|
|
207
|
+
}
|
package/facts.d.ts
CHANGED
|
@@ -8,7 +8,38 @@ export interface QualifiedName {
|
|
|
8
8
|
/**
|
|
9
9
|
* Coarse statement classification used for tier/package sorting.
|
|
10
10
|
*/
|
|
11
|
-
export type StatementKind = 'schema' | 'table' | 'view' | 'index' | 'type' | 'function' | 'trigger' | 'policy' | 'grant' | 'rls_enable' | 'fk_constraint' | 'constraint' | 'comment' | 'seed_dml' | 'other';
|
|
11
|
+
export type StatementKind = 'schema' | 'extension' | 'table' | 'view' | 'index' | 'type' | 'function' | 'trigger' | 'policy' | 'grant' | 'rls_enable' | 'fk_constraint' | 'constraint' | 'comment' | 'seed_dml' | 'other';
|
|
12
|
+
/**
|
|
13
|
+
* The action a statement performs on a PostgreSQL extension.
|
|
14
|
+
*
|
|
15
|
+
* - `create` — `CREATE EXTENSION` (`CreateExtensionStmt`).
|
|
16
|
+
* - `set_schema` — `ALTER EXTENSION ... SET SCHEMA` (`AlterObjectSchemaStmt`
|
|
17
|
+
* with `objectType: OBJECT_EXTENSION`); only succeeds for relocatable
|
|
18
|
+
* extensions, so it is surfaced as its own action.
|
|
19
|
+
* - `drop` — `DROP EXTENSION` (`DropStmt` with `removeType: OBJECT_EXTENSION`).
|
|
20
|
+
*/
|
|
21
|
+
export type ExtensionAction = 'create' | 'set_schema' | 'drop';
|
|
22
|
+
/**
|
|
23
|
+
* Facts about an extension-level statement. Unlike ordinary objects, an
|
|
24
|
+
* extension is installed into exactly one schema and its member objects are
|
|
25
|
+
* renamed with it, so the relevant fact is the extension name plus the schema
|
|
26
|
+
* it is (being) placed in.
|
|
27
|
+
*/
|
|
28
|
+
export interface ExtensionFact {
|
|
29
|
+
/** The extension name (`CREATE EXTENSION <name>`). */
|
|
30
|
+
name: string;
|
|
31
|
+
/**
|
|
32
|
+
* The schema the statement places the extension in, or `null` when none is
|
|
33
|
+
* specified (`CREATE EXTENSION <name>` with no `SCHEMA` clause installs into
|
|
34
|
+
* the current default — typically `public` or the extension's fixed schema).
|
|
35
|
+
* `DROP EXTENSION` carries no schema.
|
|
36
|
+
*/
|
|
37
|
+
schema: string | null;
|
|
38
|
+
/** Which extension operation this statement performs. */
|
|
39
|
+
action: ExtensionAction;
|
|
40
|
+
/** `CREATE EXTENSION IF NOT EXISTS`. */
|
|
41
|
+
ifNotExists?: boolean;
|
|
42
|
+
}
|
|
12
43
|
/**
|
|
13
44
|
* AST-derived facts about a single top-level SQL statement.
|
|
14
45
|
*
|
|
@@ -42,6 +73,11 @@ export interface StatementFacts {
|
|
|
42
73
|
referencedSchemas: string[];
|
|
43
74
|
/** Role names granted to, owning, or bound by this statement. */
|
|
44
75
|
roles: string[];
|
|
76
|
+
/**
|
|
77
|
+
* For extension-level statements (`kind: 'extension'`): the extension being
|
|
78
|
+
* created, relocated, or dropped. Absent for every other statement.
|
|
79
|
+
*/
|
|
80
|
+
extension?: ExtensionFact;
|
|
45
81
|
/** Foreign-key target tables (from column/table FK constraints). */
|
|
46
82
|
fkTargets: QualifiedName[];
|
|
47
83
|
/**
|
package/facts.js
CHANGED
|
@@ -16,6 +16,7 @@ const SECURITY_TAGS = new Set([
|
|
|
16
16
|
]);
|
|
17
17
|
const KIND_BY_TAG = {
|
|
18
18
|
CreateSchemaStmt: 'schema',
|
|
19
|
+
CreateExtensionStmt: 'extension',
|
|
19
20
|
CreateStmt: 'table',
|
|
20
21
|
ViewStmt: 'view',
|
|
21
22
|
IndexStmt: 'index',
|
|
@@ -40,6 +41,23 @@ const KIND_BY_TAG = {
|
|
|
40
41
|
function qn(schema, name) {
|
|
41
42
|
return { schema: schema ?? null, name };
|
|
42
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Read the `SCHEMA <name>` clause of a `CreateExtensionStmt` from its options
|
|
46
|
+
* (`{ DefElem: { defname: 'schema', arg: { String: { sval } } } }`), or `null`
|
|
47
|
+
* when the statement specifies no schema.
|
|
48
|
+
*/
|
|
49
|
+
function extensionSchemaOption(options) {
|
|
50
|
+
if (!Array.isArray(options))
|
|
51
|
+
return null;
|
|
52
|
+
for (const opt of options) {
|
|
53
|
+
const def = opt?.DefElem;
|
|
54
|
+
if (def?.defname === 'schema') {
|
|
55
|
+
const sval = def.arg?.String?.sval;
|
|
56
|
+
return typeof sval === 'string' ? sval : null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
43
61
|
function nameListToQualified(names) {
|
|
44
62
|
if (!Array.isArray(names) || names.length === 0)
|
|
45
63
|
return null;
|
|
@@ -145,6 +163,14 @@ function classifyOne(nodeTag, node) {
|
|
|
145
163
|
case 'CreateSchemaStmt':
|
|
146
164
|
facts.creates.push(qn(null, node.schemaname));
|
|
147
165
|
break;
|
|
166
|
+
case 'CreateExtensionStmt':
|
|
167
|
+
facts.extension = {
|
|
168
|
+
name: node.extname,
|
|
169
|
+
schema: extensionSchemaOption(node.options),
|
|
170
|
+
action: 'create',
|
|
171
|
+
ifNotExists: node.if_not_exists === true
|
|
172
|
+
};
|
|
173
|
+
break;
|
|
148
174
|
case 'CreateStmt':
|
|
149
175
|
case 'ViewStmt': {
|
|
150
176
|
const rel = nodeTag === 'ViewStmt' ? node.view : node.relation;
|
|
@@ -206,6 +232,34 @@ function classifyOne(nodeTag, node) {
|
|
|
206
232
|
facts.creates.push(name);
|
|
207
233
|
break;
|
|
208
234
|
}
|
|
235
|
+
case 'AlterObjectSchemaStmt':
|
|
236
|
+
// ALTER EXTENSION <name> SET SCHEMA <newschema>. Other object types
|
|
237
|
+
// (TABLE/TYPE/FUNCTION ... SET SCHEMA) keep their default classification.
|
|
238
|
+
if (node.objectType === 'OBJECT_EXTENSION') {
|
|
239
|
+
const name = node.object?.String?.sval;
|
|
240
|
+
if (typeof name === 'string') {
|
|
241
|
+
facts.kind = 'extension';
|
|
242
|
+
facts.extension = {
|
|
243
|
+
name,
|
|
244
|
+
schema: typeof node.newschema === 'string' ? node.newschema : null,
|
|
245
|
+
action: 'set_schema'
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
case 'DropStmt':
|
|
251
|
+
// DROP EXTENSION <name> [, ...]. Extension names are bare String nodes.
|
|
252
|
+
// Other DROP object types keep their default classification. When
|
|
253
|
+
// several extensions are dropped in one statement the first names the
|
|
254
|
+
// fact; the full list stays available via the raw node.
|
|
255
|
+
if (node.removeType === 'OBJECT_EXTENSION' && Array.isArray(node.objects)) {
|
|
256
|
+
const name = node.objects[0]?.String?.sval;
|
|
257
|
+
if (typeof name === 'string') {
|
|
258
|
+
facts.kind = 'extension';
|
|
259
|
+
facts.extension = { name, schema: null, action: 'drop' };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
break;
|
|
209
263
|
case 'AlterTableStmt': {
|
|
210
264
|
if (node.relation) {
|
|
211
265
|
facts.creates.push(qn(node.relation.schemaname ?? null, node.relation.relname));
|
|
@@ -244,6 +298,43 @@ function classifyOne(nodeTag, node) {
|
|
|
244
298
|
collectRoles(node, facts.roles);
|
|
245
299
|
return facts;
|
|
246
300
|
}
|
|
301
|
+
/** Read a `CreateFunctionStmt` DefElem option's scalar/list value. */
|
|
302
|
+
function functionOption(node, defname) {
|
|
303
|
+
for (const opt of node.options ?? []) {
|
|
304
|
+
if (opt?.DefElem?.defname === defname)
|
|
305
|
+
return opt.DefElem.arg;
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Collect references from a `LANGUAGE sql` function body supplied as a string
|
|
311
|
+
* literal (`AS $$ ... $$`). That body is an opaque String node the AST walker
|
|
312
|
+
* never parses, so — mirroring the schema transformer's body rewrite — parse
|
|
313
|
+
* it standalone and walk each statement with the facts visitor. The standard
|
|
314
|
+
* `BEGIN ATOMIC` / `RETURN` `sql_body` form is already part of the AST and is
|
|
315
|
+
* covered by the outer walk, so only the string form needs this.
|
|
316
|
+
*/
|
|
317
|
+
function collectSqlBodyReferences(node, facts) {
|
|
318
|
+
const language = functionOption(node, 'language')?.String?.sval;
|
|
319
|
+
if (typeof language !== 'string' || language.toLowerCase() !== 'sql')
|
|
320
|
+
return;
|
|
321
|
+
const asArg = functionOption(node, 'as');
|
|
322
|
+
const items = asArg?.List?.items ?? [];
|
|
323
|
+
const body = items[0]?.String?.sval;
|
|
324
|
+
if (typeof body !== 'string')
|
|
325
|
+
return;
|
|
326
|
+
try {
|
|
327
|
+
const stmts = (0, plpgsql_parser_1.parseSql)(body)?.stmts ?? [];
|
|
328
|
+
const visitor = createFactsVisitor(facts, facts.bodyReferences);
|
|
329
|
+
for (const stmt of stmts) {
|
|
330
|
+
if (stmt?.stmt)
|
|
331
|
+
(0, traverse_1.walk)(stmt.stmt, visitor);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// A non-parseable body (C symbol name, etc.) contributes no references.
|
|
336
|
+
}
|
|
337
|
+
}
|
|
247
338
|
/**
|
|
248
339
|
* Classify each top-level statement in a SQL script into {@link StatementFacts}.
|
|
249
340
|
*
|
|
@@ -263,6 +354,9 @@ function classifyStatements(sql) {
|
|
|
263
354
|
if (stmtNode) {
|
|
264
355
|
(0, traverse_1.walk)(stmtNode, createFactsVisitor(facts));
|
|
265
356
|
}
|
|
357
|
+
if (nodeTag === 'CreateFunctionStmt') {
|
|
358
|
+
collectSqlBodyReferences(node, facts);
|
|
359
|
+
}
|
|
266
360
|
allFacts.push(facts);
|
|
267
361
|
}
|
|
268
362
|
for (const fn of ctx.functions ?? []) {
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
export type { QualifiedName, StatementFacts, StatementKind, } from './facts';
|
|
1
|
+
export type { ExtensionAction, ExtensionFact, QualifiedName, StatementFacts, StatementKind, } from './facts';
|
|
2
2
|
export { classifyStatements } from './facts';
|
|
3
|
+
export type { ExtensionDefinition, ExtensionRoute, ExtensionRouteSpec, ExtensionRouterOptions, ExtensionSymbol, ExtensionSymbolNamespace, SymbolRewrite, } from './extension-router';
|
|
4
|
+
export { COMMON_EXTENSIONS, ExtensionRouter } from './extension-router';
|
|
5
|
+
export type { ExtensionTransformResult } from './extension-transform';
|
|
6
|
+
export { createExtensionResult, createExtensionVisitor, transformExtensions, } from './extension-transform';
|
|
7
|
+
export type { RoleRouteSpec } from './role-router';
|
|
8
|
+
export { RoleRouter } from './role-router';
|
|
9
|
+
export type { RoleTransformResult } from './role-transform';
|
|
10
|
+
export { createRoleResult, createRoleVisitor, transformRoles, } from './role-transform';
|
|
3
11
|
export type { ObjectInventory, QualifyResult, QualifyRoutes, QualifyTargetSelector, QualifyUnqualifiedOptions, } from './qualify';
|
|
4
12
|
export { collectCreatedObjects, createQualifyVisitor, mergeInventories, qualifyUnqualified, } from './qualify';
|
|
5
13
|
export type { ObjectNamespace, RouteNamespace, RouteSpec, SchemaRoute, } from './router';
|
package/index.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
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.SchemaRouter = exports.qualifyUnqualified = exports.mergeInventories = exports.createQualifyVisitor = exports.collectCreatedObjects = exports.classifyStatements = void 0;
|
|
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.SchemaRouter = exports.qualifyUnqualified = exports.mergeInventories = exports.createQualifyVisitor = exports.collectCreatedObjects = exports.transformRoles = exports.createRoleVisitor = exports.createRoleResult = exports.RoleRouter = exports.transformExtensions = exports.createExtensionVisitor = exports.createExtensionResult = exports.ExtensionRouter = exports.COMMON_EXTENSIONS = exports.classifyStatements = void 0;
|
|
4
4
|
var facts_1 = require("./facts");
|
|
5
5
|
Object.defineProperty(exports, "classifyStatements", { enumerable: true, get: function () { return facts_1.classifyStatements; } });
|
|
6
|
+
var extension_router_1 = require("./extension-router");
|
|
7
|
+
Object.defineProperty(exports, "COMMON_EXTENSIONS", { enumerable: true, get: function () { return extension_router_1.COMMON_EXTENSIONS; } });
|
|
8
|
+
Object.defineProperty(exports, "ExtensionRouter", { enumerable: true, get: function () { return extension_router_1.ExtensionRouter; } });
|
|
9
|
+
var extension_transform_1 = require("./extension-transform");
|
|
10
|
+
Object.defineProperty(exports, "createExtensionResult", { enumerable: true, get: function () { return extension_transform_1.createExtensionResult; } });
|
|
11
|
+
Object.defineProperty(exports, "createExtensionVisitor", { enumerable: true, get: function () { return extension_transform_1.createExtensionVisitor; } });
|
|
12
|
+
Object.defineProperty(exports, "transformExtensions", { enumerable: true, get: function () { return extension_transform_1.transformExtensions; } });
|
|
13
|
+
var role_router_1 = require("./role-router");
|
|
14
|
+
Object.defineProperty(exports, "RoleRouter", { enumerable: true, get: function () { return role_router_1.RoleRouter; } });
|
|
15
|
+
var role_transform_1 = require("./role-transform");
|
|
16
|
+
Object.defineProperty(exports, "createRoleResult", { enumerable: true, get: function () { return role_transform_1.createRoleResult; } });
|
|
17
|
+
Object.defineProperty(exports, "createRoleVisitor", { enumerable: true, get: function () { return role_transform_1.createRoleVisitor; } });
|
|
18
|
+
Object.defineProperty(exports, "transformRoles", { enumerable: true, get: function () { return role_transform_1.transformRoles; } });
|
|
6
19
|
var qualify_1 = require("./qualify");
|
|
7
20
|
Object.defineProperty(exports, "collectCreatedObjects", { enumerable: true, get: function () { return qualify_1.collectCreatedObjects; } });
|
|
8
21
|
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
|
+
"version": "18.6.0",
|
|
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": "6d3c28348fab5cace76f7c0167f3b73e8f9f67c8"
|
|
53
53
|
}
|
package/role-router.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-name routing for the core transform.
|
|
3
|
+
*
|
|
4
|
+
* Two databases can express the *same* access model with *different* role
|
|
5
|
+
* names — one calls the unauthenticated role `anonymous`, another `anon`; one
|
|
6
|
+
* calls the privileged role `administrator`, another something else. Portable
|
|
7
|
+
* SQL must be able to translate role identifiers between these conventions.
|
|
8
|
+
*
|
|
9
|
+
* Scope, deliberately narrow: a {@link RoleRouter} only renames role
|
|
10
|
+
* *identifiers* in the AST positions where a role name legitimately appears
|
|
11
|
+
* (grants, ownership, policies, default privileges, role membership, role
|
|
12
|
+
* settings). It does **not** alter role *attributes* (`BYPASSRLS`, `LOGIN`,
|
|
13
|
+
* ...): two roles that share a name across conventions do not necessarily
|
|
14
|
+
* share privilege scope, and inventing attribute changes would silently
|
|
15
|
+
* misrepresent the security model. Privilege-semantics reconciliation is a
|
|
16
|
+
* downstream, policy-level concern; the router restricts itself to the
|
|
17
|
+
* deterministic, reversible part — the name.
|
|
18
|
+
*
|
|
19
|
+
* Renaming is a pure identifier substitution, so a router is trivially
|
|
20
|
+
* invertible ({@link RoleRouter.invert}) for bidirectional translation.
|
|
21
|
+
*/
|
|
22
|
+
/** Role-rename specification: source role name -> target role name. */
|
|
23
|
+
export type RoleRouteSpec = Record<string, string>;
|
|
24
|
+
/** Resolves a target role name for a source role name. */
|
|
25
|
+
export declare class RoleRouter {
|
|
26
|
+
private readonly map;
|
|
27
|
+
constructor(routes?: RoleRouteSpec | Map<string, string>);
|
|
28
|
+
/** Coerce a spec, map, or existing router into a router. */
|
|
29
|
+
static from(source: RoleRouter | RoleRouteSpec | Map<string, string>): RoleRouter;
|
|
30
|
+
/** Number of configured renames. */
|
|
31
|
+
get size(): number;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the target name for `role`, or `undefined` when it is not routed
|
|
34
|
+
* or already at its destination.
|
|
35
|
+
*/
|
|
36
|
+
resolve(role: string | undefined | null): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The inverse router (target -> source), for translating in the opposite
|
|
39
|
+
* direction. Throws if the mapping is not one-to-one.
|
|
40
|
+
*/
|
|
41
|
+
invert(): RoleRouter;
|
|
42
|
+
}
|
package/role-router.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Role-name routing for the core transform.
|
|
4
|
+
*
|
|
5
|
+
* Two databases can express the *same* access model with *different* role
|
|
6
|
+
* names — one calls the unauthenticated role `anonymous`, another `anon`; one
|
|
7
|
+
* calls the privileged role `administrator`, another something else. Portable
|
|
8
|
+
* SQL must be able to translate role identifiers between these conventions.
|
|
9
|
+
*
|
|
10
|
+
* Scope, deliberately narrow: a {@link RoleRouter} only renames role
|
|
11
|
+
* *identifiers* in the AST positions where a role name legitimately appears
|
|
12
|
+
* (grants, ownership, policies, default privileges, role membership, role
|
|
13
|
+
* settings). It does **not** alter role *attributes* (`BYPASSRLS`, `LOGIN`,
|
|
14
|
+
* ...): two roles that share a name across conventions do not necessarily
|
|
15
|
+
* share privilege scope, and inventing attribute changes would silently
|
|
16
|
+
* misrepresent the security model. Privilege-semantics reconciliation is a
|
|
17
|
+
* downstream, policy-level concern; the router restricts itself to the
|
|
18
|
+
* deterministic, reversible part — the name.
|
|
19
|
+
*
|
|
20
|
+
* Renaming is a pure identifier substitution, so a router is trivially
|
|
21
|
+
* invertible ({@link RoleRouter.invert}) for bidirectional translation.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.RoleRouter = void 0;
|
|
25
|
+
/** Resolves a target role name for a source role name. */
|
|
26
|
+
class RoleRouter {
|
|
27
|
+
map;
|
|
28
|
+
constructor(routes = {}) {
|
|
29
|
+
this.map = routes instanceof Map ? new Map(routes) : new Map(Object.entries(routes));
|
|
30
|
+
}
|
|
31
|
+
/** Coerce a spec, map, or existing router into a router. */
|
|
32
|
+
static from(source) {
|
|
33
|
+
return source instanceof RoleRouter ? source : new RoleRouter(source);
|
|
34
|
+
}
|
|
35
|
+
/** Number of configured renames. */
|
|
36
|
+
get size() {
|
|
37
|
+
return this.map.size;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the target name for `role`, or `undefined` when it is not routed
|
|
41
|
+
* or already at its destination.
|
|
42
|
+
*/
|
|
43
|
+
resolve(role) {
|
|
44
|
+
if (!role)
|
|
45
|
+
return undefined;
|
|
46
|
+
const target = this.map.get(role);
|
|
47
|
+
if (target === undefined || target === role)
|
|
48
|
+
return undefined;
|
|
49
|
+
return target;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The inverse router (target -> source), for translating in the opposite
|
|
53
|
+
* direction. Throws if the mapping is not one-to-one.
|
|
54
|
+
*/
|
|
55
|
+
invert() {
|
|
56
|
+
const inverted = new Map();
|
|
57
|
+
for (const [from, to] of this.map) {
|
|
58
|
+
if (inverted.has(to)) {
|
|
59
|
+
throw new Error(`RoleRouter.invert: mapping is not one-to-one (two sources map to "${to}")`);
|
|
60
|
+
}
|
|
61
|
+
inverted.set(to, from);
|
|
62
|
+
}
|
|
63
|
+
return new RoleRouter(inverted);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.RoleRouter = RoleRouter;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST transform that applies a {@link RoleRouter}.
|
|
3
|
+
*
|
|
4
|
+
* Role identifiers surface in many statement forms. Most flow through a single
|
|
5
|
+
* `RoleSpec` node (grantees, ownership `... OWNER TO`, `CREATE POLICY ... TO`,
|
|
6
|
+
* `ALTER DEFAULT PRIVILEGES [FOR ROLE ...] ... TO`, `ALTER ROLE`, `DROP ROLE`,
|
|
7
|
+
* role-membership grantees, `GRANTED BY`, `REASSIGN OWNED BY`), which the
|
|
8
|
+
* traversal reaches automatically. Four positions carry a role name outside a
|
|
9
|
+
* `RoleSpec` and are handled explicitly:
|
|
10
|
+
*
|
|
11
|
+
* - `CREATE ROLE <name>` — a bare string on `CreateRoleStmt.role`.
|
|
12
|
+
* - `GRANT <role> TO ...` — the *granted* roles are `AccessPriv.priv_name`
|
|
13
|
+
* strings on `GrantRoleStmt.granted_roles` (not `RoleSpec`s).
|
|
14
|
+
* - `SET ROLE` / `SET SESSION AUTHORIZATION` — the role is a string constant
|
|
15
|
+
* in `VariableSetStmt.args`.
|
|
16
|
+
* - `ALTER ROLE <old> RENAME TO <new>` — `RenameStmt` with string
|
|
17
|
+
* `subname`/`newname`.
|
|
18
|
+
*
|
|
19
|
+
* Reserved role specifications (`PUBLIC`, `CURRENT_USER`, `SESSION_USER`, ...)
|
|
20
|
+
* carry no `rolename` and a non-`CSTRING` `roletype`, so they are never
|
|
21
|
+
* rewritten.
|
|
22
|
+
*/
|
|
23
|
+
import type { RoleRouteSpec } from './role-router';
|
|
24
|
+
import { RoleRouter } from './role-router';
|
|
25
|
+
/** What a role transform changed: role name -> number of positions rewritten. */
|
|
26
|
+
export interface RoleTransformResult {
|
|
27
|
+
rolesRenamed: Map<string, number>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createRoleResult(): RoleTransformResult;
|
|
30
|
+
/**
|
|
31
|
+
* Create a SQL AST visitor that renames role identifiers against a router.
|
|
32
|
+
* Composable with the walkers used by the core transform.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createRoleVisitor(router: RoleRouter, result: RoleTransformResult): {
|
|
35
|
+
RoleSpec: (path: any) => void;
|
|
36
|
+
CreateRoleStmt: (path: any) => void;
|
|
37
|
+
GrantRoleStmt: (path: any) => void;
|
|
38
|
+
VariableSetStmt: (path: any) => void;
|
|
39
|
+
RenameStmt: (path: any) => void;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Apply role renaming to a SQL string: parse -> walk -> deparse. Role names in
|
|
43
|
+
* PL/pgSQL / `LANGUAGE sql` bodies are string literals (e.g. inside dynamic
|
|
44
|
+
* `EXECUTE`) and are intentionally not rewritten — see the module docs on why
|
|
45
|
+
* arbitrary string contents are out of scope.
|
|
46
|
+
*/
|
|
47
|
+
export declare function transformRoles(sql: string, router: RoleRouter | RoleRouteSpec | Map<string, string>): {
|
|
48
|
+
sql: string;
|
|
49
|
+
result: RoleTransformResult;
|
|
50
|
+
};
|