@pgsql/transform 18.5.0 → 18.7.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 +54 -0
- package/esm/index.js +4 -0
- package/esm/role-router.js +62 -0
- package/esm/role-transform.js +129 -0
- package/esm/router.js +69 -5
- package/esm/transform.js +32 -9
- 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 +54 -0
- package/index.d.ts +10 -2
- 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
- package/router.d.ts +63 -6
- package/router.js +69 -5
- package/transform.js +32 -9
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Extension routing for the core transform.
|
|
4
|
+
*
|
|
5
|
+
* A PostgreSQL extension is installed into exactly one schema, and the objects
|
|
6
|
+
* it provides (functions, types, operators) live in that schema. Two databases
|
|
7
|
+
* can install the *same* extension into *different* schemas — one leaves it in
|
|
8
|
+
* `public`, another isolates it in a dedicated schema — so portable SQL must be
|
|
9
|
+
* able to move an extension's install site **and** rewrite every reference to
|
|
10
|
+
* the symbols it provides.
|
|
11
|
+
*
|
|
12
|
+
* This is a distinct problem from {@link SchemaRouter}, which routes objects the
|
|
13
|
+
* SQL itself creates. Here the objects are owned by an extension and are not
|
|
14
|
+
* declared in the script; the only way to know that a bare `crypt(...)` call
|
|
15
|
+
* belongs to `pgcrypto` (and must move when `pgcrypto` moves) is an
|
|
16
|
+
* **inventory** of which symbols each extension provides. That inventory is
|
|
17
|
+
* also **version-aware**: a symbol can graduate into the server core and cease
|
|
18
|
+
* to be extension-owned (the canonical case is `gen_random_uuid()`, provided by
|
|
19
|
+
* `pgcrypto` before PostgreSQL 13 and part of core `pg_catalog` from 13 on — it
|
|
20
|
+
* must never be routed on 13+).
|
|
21
|
+
*
|
|
22
|
+
* An {@link ExtensionRouter} answers two questions:
|
|
23
|
+
*
|
|
24
|
+
* - **install**: where should `CREATE EXTENSION <e>` (or `ALTER EXTENSION <e>
|
|
25
|
+
* SET SCHEMA`) place the extension?
|
|
26
|
+
* - **reference**: given a reference to a symbol `(schema, name)` in namespace
|
|
27
|
+
* `ns`, what schema should it live in now — including stripping the qualifier
|
|
28
|
+
* entirely (relying on `search_path`) when the target is "unqualified".
|
|
29
|
+
*
|
|
30
|
+
* A schema of `null` denotes the **unqualified** site: a bare reference
|
|
31
|
+
* (`crypt(...)`) or installing an extension with no explicit `SCHEMA` clause.
|
|
32
|
+
* This makes the router fully bidirectional: `null -> 'extensions'` qualifies
|
|
33
|
+
* bare references, and `'extensions' -> null` strips them back to bare.
|
|
34
|
+
*/
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ExtensionRouter = exports.COMMON_EXTENSIONS = void 0;
|
|
37
|
+
const DEFAULT_SERVER_VERSION = 9999;
|
|
38
|
+
/**
|
|
39
|
+
* A curated, platform-agnostic inventory of common contrib/extension symbols.
|
|
40
|
+
* Intentionally minimal and conservative — only well-known, stable symbols are
|
|
41
|
+
* listed so routing never touches something it cannot prove belongs to the
|
|
42
|
+
* extension. Callers extend this via {@link ExtensionRouterOptions.inventory}.
|
|
43
|
+
*/
|
|
44
|
+
exports.COMMON_EXTENSIONS = [
|
|
45
|
+
{
|
|
46
|
+
name: 'pgcrypto',
|
|
47
|
+
relocatable: true,
|
|
48
|
+
symbols: [
|
|
49
|
+
{ name: 'crypt' },
|
|
50
|
+
{ name: 'gen_salt' },
|
|
51
|
+
{ name: 'digest' },
|
|
52
|
+
{ name: 'hmac' },
|
|
53
|
+
{ name: 'encrypt' },
|
|
54
|
+
{ name: 'decrypt' },
|
|
55
|
+
{ name: 'encrypt_iv' },
|
|
56
|
+
{ name: 'decrypt_iv' },
|
|
57
|
+
{ name: 'gen_random_bytes' },
|
|
58
|
+
// Provided by pgcrypto before PG13; part of core pg_catalog from 13 on.
|
|
59
|
+
{ name: 'gen_random_uuid', coreSince: 13 },
|
|
60
|
+
{ name: 'pgp_sym_encrypt' },
|
|
61
|
+
{ name: 'pgp_sym_decrypt' },
|
|
62
|
+
{ name: 'pgp_pub_encrypt' },
|
|
63
|
+
{ name: 'pgp_pub_decrypt' }
|
|
64
|
+
]
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'uuid-ossp',
|
|
68
|
+
relocatable: true,
|
|
69
|
+
symbols: [
|
|
70
|
+
{ name: 'uuid_generate_v1' },
|
|
71
|
+
{ name: 'uuid_generate_v1mc' },
|
|
72
|
+
{ name: 'uuid_generate_v3' },
|
|
73
|
+
{ name: 'uuid_generate_v4' },
|
|
74
|
+
{ name: 'uuid_generate_v5' },
|
|
75
|
+
{ name: 'uuid_nil' }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'citext',
|
|
80
|
+
relocatable: true,
|
|
81
|
+
symbols: [
|
|
82
|
+
{ name: 'citext', namespace: 'type' },
|
|
83
|
+
{ name: 'citextin', namespace: 'function' },
|
|
84
|
+
{ name: 'citext_hash', namespace: 'function' }
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'pg_trgm',
|
|
89
|
+
relocatable: true,
|
|
90
|
+
symbols: [
|
|
91
|
+
{ name: 'similarity' },
|
|
92
|
+
{ name: 'show_trgm' },
|
|
93
|
+
{ name: 'word_similarity' },
|
|
94
|
+
{ name: 'strict_word_similarity' }
|
|
95
|
+
]
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'ltree',
|
|
99
|
+
relocatable: true,
|
|
100
|
+
symbols: [
|
|
101
|
+
{ name: 'ltree', namespace: 'type' },
|
|
102
|
+
{ name: 'lquery', namespace: 'type' },
|
|
103
|
+
{ name: 'ltxtquery', namespace: 'type' },
|
|
104
|
+
{ name: 'subltree' },
|
|
105
|
+
{ name: 'subpath' },
|
|
106
|
+
{ name: 'nlevel' },
|
|
107
|
+
{ name: 'lca' }
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'hstore',
|
|
112
|
+
relocatable: true,
|
|
113
|
+
symbols: [{ name: 'hstore', namespace: 'type' }]
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'unaccent',
|
|
117
|
+
relocatable: true,
|
|
118
|
+
symbols: [{ name: 'unaccent' }]
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
/**
|
|
122
|
+
* Resolves install-schema and symbol-reference routing for extensions, backed
|
|
123
|
+
* by a version-aware symbol inventory.
|
|
124
|
+
*/
|
|
125
|
+
class ExtensionRouter {
|
|
126
|
+
routes;
|
|
127
|
+
serverVersion;
|
|
128
|
+
definitions;
|
|
129
|
+
/** `${namespace}:${name}` -> providing extension (+ version predicate). */
|
|
130
|
+
symbolIndex;
|
|
131
|
+
constructor(routes = {}, options = {}) {
|
|
132
|
+
this.routes = routes instanceof Map ? new Map(routes) : new Map(Object.entries(routes));
|
|
133
|
+
this.serverVersion = options.serverVersion ?? DEFAULT_SERVER_VERSION;
|
|
134
|
+
const inventory = options.inventory ?? exports.COMMON_EXTENSIONS;
|
|
135
|
+
this.definitions = new Map(inventory.map(def => [def.name, def]));
|
|
136
|
+
this.symbolIndex = new Map();
|
|
137
|
+
for (const def of inventory) {
|
|
138
|
+
for (const sym of def.symbols) {
|
|
139
|
+
const key = symbolKey(sym.namespace ?? 'function', sym.name);
|
|
140
|
+
// First definition wins; inventories should not double-claim a symbol.
|
|
141
|
+
if (!this.symbolIndex.has(key)) {
|
|
142
|
+
this.symbolIndex.set(key, { extension: def.name, coreSince: sym.coreSince });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build a router that moves the given extensions (default: every inventoried
|
|
149
|
+
* one) to a single `targetSchema`, rewriting their references from `public`
|
|
150
|
+
* and from bare (unqualified) sites. Passing `null` as `targetSchema` strips
|
|
151
|
+
* qualification instead (the "repollute public / rely on search_path"
|
|
152
|
+
* direction).
|
|
153
|
+
*/
|
|
154
|
+
static toSchema(targetSchema, options = {}) {
|
|
155
|
+
const inventory = options.inventory ?? exports.COMMON_EXTENSIONS;
|
|
156
|
+
const names = options.extensions ?? inventory.map(d => d.name);
|
|
157
|
+
const from = options.from ?? ['public', null];
|
|
158
|
+
const spec = {};
|
|
159
|
+
for (const name of names)
|
|
160
|
+
spec[name] = { to: targetSchema, from };
|
|
161
|
+
return new ExtensionRouter(spec, options);
|
|
162
|
+
}
|
|
163
|
+
/** Coerce a spec, map, or existing router into a router. */
|
|
164
|
+
static from(source, options) {
|
|
165
|
+
if (source instanceof ExtensionRouter)
|
|
166
|
+
return source;
|
|
167
|
+
return new ExtensionRouter(source, options);
|
|
168
|
+
}
|
|
169
|
+
/** True when no extension routes are configured. */
|
|
170
|
+
get size() {
|
|
171
|
+
return this.routes.size;
|
|
172
|
+
}
|
|
173
|
+
/** Extension names this router may rewrite. */
|
|
174
|
+
routedExtensions() {
|
|
175
|
+
return [...this.routes.keys()];
|
|
176
|
+
}
|
|
177
|
+
/** True when any configured extension has inventoried symbols to rewrite. */
|
|
178
|
+
hasSymbolRoutes() {
|
|
179
|
+
for (const name of this.routes.keys()) {
|
|
180
|
+
const def = this.definitions.get(name);
|
|
181
|
+
if (def && !def.fixedSchema && def.symbols.length > 0)
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Resolve the install schema for `CREATE EXTENSION <extname>` /
|
|
188
|
+
* `ALTER EXTENSION <extname> SET SCHEMA`. Returns:
|
|
189
|
+
* - a string to place/keep the extension in that schema,
|
|
190
|
+
* - `null` to install with no explicit `SCHEMA` clause (server default),
|
|
191
|
+
* - `undefined` to leave the statement unchanged (no route, or the extension
|
|
192
|
+
* is pinned to a fixed schema).
|
|
193
|
+
*/
|
|
194
|
+
resolveInstall(extname) {
|
|
195
|
+
if (!extname)
|
|
196
|
+
return undefined;
|
|
197
|
+
const route = this.routes.get(extname);
|
|
198
|
+
if (!route)
|
|
199
|
+
return undefined;
|
|
200
|
+
if (this.definitions.get(extname)?.fixedSchema !== undefined)
|
|
201
|
+
return undefined;
|
|
202
|
+
return route.to;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Resolve a rewrite for a reference to `(schema, name)` in namespace `ns`,
|
|
206
|
+
* where `schema` is `null` for a bare reference. Returns the target
|
|
207
|
+
* ({@link SymbolRewrite}) or `undefined` to leave the reference unchanged.
|
|
208
|
+
*
|
|
209
|
+
* A reference is rewritten only when the inventory proves the symbol belongs
|
|
210
|
+
* to a routed extension, the symbol has not graduated to core at the target
|
|
211
|
+
* server version, the current schema is one the route rewrites `from`, and
|
|
212
|
+
* the destination actually differs from where it already is.
|
|
213
|
+
*/
|
|
214
|
+
resolveSymbol(schema, name, ns) {
|
|
215
|
+
if (!name)
|
|
216
|
+
return undefined;
|
|
217
|
+
const indexed = this.symbolIndex.get(symbolKey(ns, name));
|
|
218
|
+
if (!indexed)
|
|
219
|
+
return undefined;
|
|
220
|
+
// Graduated into core at/after the target server version — resolves from
|
|
221
|
+
// pg_catalog and must not be tied to the extension's schema.
|
|
222
|
+
if (indexed.coreSince !== undefined && this.serverVersion >= indexed.coreSince)
|
|
223
|
+
return undefined;
|
|
224
|
+
const route = this.routes.get(indexed.extension);
|
|
225
|
+
if (!route)
|
|
226
|
+
return undefined;
|
|
227
|
+
if (this.definitions.get(indexed.extension)?.fixedSchema !== undefined)
|
|
228
|
+
return undefined;
|
|
229
|
+
const current = schema ?? null;
|
|
230
|
+
if (route.from !== undefined && !route.from.includes(current))
|
|
231
|
+
return undefined;
|
|
232
|
+
if (current === route.to)
|
|
233
|
+
return undefined;
|
|
234
|
+
return { to: route.to };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
exports.ExtensionRouter = ExtensionRouter;
|
|
238
|
+
function symbolKey(ns, name) {
|
|
239
|
+
return `${ns}:${name}`;
|
|
240
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST transform that applies an {@link ExtensionRouter}.
|
|
3
|
+
*
|
|
4
|
+
* Unlike the schema transform — which only rewrites string *fields* already
|
|
5
|
+
* present on a node — moving an extension changes the **shape** of the AST:
|
|
6
|
+
*
|
|
7
|
+
* - `CREATE EXTENSION <e>` with no `SCHEMA` clause must gain one, which means
|
|
8
|
+
* constructing a new `DefElem` node and attaching it to a (possibly absent)
|
|
9
|
+
* `options` array; the reverse strips the `DefElem` (and empties `options`).
|
|
10
|
+
* - a bare `crypt(...)` reference gains a schema qualifier (a `String` node is
|
|
11
|
+
* prepended to `funcname`), and the reverse removes it.
|
|
12
|
+
*
|
|
13
|
+
* All rewrites are AST-precise and driven by the router's version-aware symbol
|
|
14
|
+
* inventory, so built-ins that merely share a name with an extension symbol
|
|
15
|
+
* (and symbols that have graduated into core) are left untouched.
|
|
16
|
+
*/
|
|
17
|
+
import type { ExtensionRouteSpec, ExtensionRouterOptions } from './extension-router';
|
|
18
|
+
import { ExtensionRouter } from './extension-router';
|
|
19
|
+
/** What an extension transform changed. */
|
|
20
|
+
export interface ExtensionTransformResult {
|
|
21
|
+
/** Extensions whose install schema was rewritten: extname -> target (null = unqualified). */
|
|
22
|
+
installsMoved: Map<string, string | null>;
|
|
23
|
+
/** Symbol references rewritten, counted by bare symbol name. */
|
|
24
|
+
symbolsRewritten: Map<string, number>;
|
|
25
|
+
}
|
|
26
|
+
/** Create a fresh result tracker. */
|
|
27
|
+
export declare function createExtensionResult(): ExtensionTransformResult;
|
|
28
|
+
/**
|
|
29
|
+
* Create a SQL AST visitor that applies extension routing. Composable with the
|
|
30
|
+
* walkers used by the core transform and by PL/pgSQL body traversal.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createExtensionVisitor(router: ExtensionRouter, result: ExtensionTransformResult): {
|
|
33
|
+
CreateExtensionStmt: (path: any) => void;
|
|
34
|
+
AlterObjectSchemaStmt: (path: any) => void;
|
|
35
|
+
FuncCall: (path: any) => void;
|
|
36
|
+
CallStmt: (path: any) => void;
|
|
37
|
+
TypeName: (path: any) => void;
|
|
38
|
+
CreateFunctionStmt: (path: any) => void;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Apply extension routing to a SQL string: parse -> walk -> deparse, including
|
|
42
|
+
* PL/pgSQL and `LANGUAGE sql` bodies. Returns the rewritten SQL and a summary
|
|
43
|
+
* of what changed.
|
|
44
|
+
*/
|
|
45
|
+
export declare function transformExtensions(sql: string, router: ExtensionRouter | ExtensionRouteSpec, options?: ExtensionRouterOptions): {
|
|
46
|
+
sql: string;
|
|
47
|
+
result: ExtensionTransformResult;
|
|
48
|
+
};
|
|
@@ -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));
|
package/index.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
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
|
-
export type { ObjectNamespace, RouteNamespace, RouteSpec, SchemaRoute, } from './router';
|
|
13
|
+
export type { ObjectNamespace, ObjectRoute, ObjectRouteTarget, RouteNamespace, RouteSpec, SchemaRoute, } from './router';
|
|
6
14
|
export { SchemaRouter } from './router';
|
|
7
15
|
export type { CapturedAsts } from './round-trip';
|
|
8
16
|
export { captureAstsFromSql, captureTransformAsts, firstDifference, normalizeParseTree, validateRoundTrip, } from './round-trip';
|
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; } });
|