@pgsql/transform 18.5.0 → 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 +54 -0
- 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 +54 -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,129 @@
|
|
|
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 { walk as walkSql } from '@pgsql/traverse';
|
|
24
|
+
import { transformSync } from 'plpgsql-parser';
|
|
25
|
+
import { RoleRouter } from './role-router';
|
|
26
|
+
export function createRoleResult() {
|
|
27
|
+
return { rolesRenamed: new Map() };
|
|
28
|
+
}
|
|
29
|
+
const ROLE_SET_PARAMS = new Set(['role', 'session_authorization']);
|
|
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 function createRoleVisitor(router, result) {
|
|
35
|
+
const record = (name) => {
|
|
36
|
+
result.rolesRenamed.set(name, (result.rolesRenamed.get(name) ?? 0) + 1);
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
RoleSpec: (path) => {
|
|
40
|
+
const node = path.node;
|
|
41
|
+
// Only named roles (ROLESPEC_CSTRING) carry a rolename; PUBLIC /
|
|
42
|
+
// CURRENT_USER / SESSION_USER are encoded by roletype and left alone.
|
|
43
|
+
if (node.roletype !== 'ROLESPEC_CSTRING')
|
|
44
|
+
return;
|
|
45
|
+
const target = router.resolve(node.rolename);
|
|
46
|
+
if (target === undefined)
|
|
47
|
+
return;
|
|
48
|
+
node.rolename = target;
|
|
49
|
+
record(node.rolename);
|
|
50
|
+
},
|
|
51
|
+
CreateRoleStmt: (path) => {
|
|
52
|
+
const node = path.node;
|
|
53
|
+
const target = router.resolve(node.role);
|
|
54
|
+
if (target === undefined)
|
|
55
|
+
return;
|
|
56
|
+
node.role = target;
|
|
57
|
+
record(target);
|
|
58
|
+
},
|
|
59
|
+
GrantRoleStmt: (path) => {
|
|
60
|
+
// granted_roles are role names carried as AccessPriv.priv_name (the
|
|
61
|
+
// grantee_roles RoleSpecs are handled by the RoleSpec visitor).
|
|
62
|
+
const granted = path.node.granted_roles;
|
|
63
|
+
if (!Array.isArray(granted))
|
|
64
|
+
return;
|
|
65
|
+
for (const g of granted) {
|
|
66
|
+
const priv = g?.AccessPriv;
|
|
67
|
+
if (!priv || typeof priv.priv_name !== 'string')
|
|
68
|
+
continue;
|
|
69
|
+
const target = router.resolve(priv.priv_name);
|
|
70
|
+
if (target === undefined)
|
|
71
|
+
continue;
|
|
72
|
+
priv.priv_name = target;
|
|
73
|
+
record(target);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
VariableSetStmt: (path) => {
|
|
77
|
+
const node = path.node;
|
|
78
|
+
if (!ROLE_SET_PARAMS.has(node.name) || !Array.isArray(node.args))
|
|
79
|
+
return;
|
|
80
|
+
for (const arg of node.args) {
|
|
81
|
+
const sval = arg?.A_Const?.sval?.sval ?? arg?.String?.sval;
|
|
82
|
+
if (typeof sval !== 'string')
|
|
83
|
+
continue;
|
|
84
|
+
const target = router.resolve(sval);
|
|
85
|
+
if (target === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
if (arg.A_Const?.sval)
|
|
88
|
+
arg.A_Const.sval.sval = target;
|
|
89
|
+
else if (arg.String)
|
|
90
|
+
arg.String.sval = target;
|
|
91
|
+
record(target);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
RenameStmt: (path) => {
|
|
95
|
+
const node = path.node;
|
|
96
|
+
if (node.renameType !== 'OBJECT_ROLE')
|
|
97
|
+
return;
|
|
98
|
+
const subTarget = router.resolve(node.subname);
|
|
99
|
+
if (subTarget !== undefined) {
|
|
100
|
+
node.subname = subTarget;
|
|
101
|
+
record(subTarget);
|
|
102
|
+
}
|
|
103
|
+
const newTarget = router.resolve(node.newname);
|
|
104
|
+
if (newTarget !== undefined) {
|
|
105
|
+
node.newname = newTarget;
|
|
106
|
+
record(newTarget);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Apply role renaming to a SQL string: parse -> walk -> deparse. Role names in
|
|
113
|
+
* PL/pgSQL / `LANGUAGE sql` bodies are string literals (e.g. inside dynamic
|
|
114
|
+
* `EXECUTE`) and are intentionally not rewritten — see the module docs on why
|
|
115
|
+
* arbitrary string contents are out of scope.
|
|
116
|
+
*/
|
|
117
|
+
export function transformRoles(sql, router) {
|
|
118
|
+
const resolved = RoleRouter.from(router);
|
|
119
|
+
const result = createRoleResult();
|
|
120
|
+
const out = transformSync(sql, (ctx) => {
|
|
121
|
+
const stmts = ctx.sql?.stmts ?? [];
|
|
122
|
+
const visitor = createRoleVisitor(resolved, result);
|
|
123
|
+
for (const stmt of stmts) {
|
|
124
|
+
if (stmt?.stmt)
|
|
125
|
+
walkSql(stmt.stmt, visitor);
|
|
126
|
+
}
|
|
127
|
+
}, { hydrate: true, pretty: true });
|
|
128
|
+
return { sql: out, result };
|
|
129
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extension routing for the core transform.
|
|
3
|
+
*
|
|
4
|
+
* A PostgreSQL extension is installed into exactly one schema, and the objects
|
|
5
|
+
* it provides (functions, types, operators) live in that schema. Two databases
|
|
6
|
+
* can install the *same* extension into *different* schemas — one leaves it in
|
|
7
|
+
* `public`, another isolates it in a dedicated schema — so portable SQL must be
|
|
8
|
+
* able to move an extension's install site **and** rewrite every reference to
|
|
9
|
+
* the symbols it provides.
|
|
10
|
+
*
|
|
11
|
+
* This is a distinct problem from {@link SchemaRouter}, which routes objects the
|
|
12
|
+
* SQL itself creates. Here the objects are owned by an extension and are not
|
|
13
|
+
* declared in the script; the only way to know that a bare `crypt(...)` call
|
|
14
|
+
* belongs to `pgcrypto` (and must move when `pgcrypto` moves) is an
|
|
15
|
+
* **inventory** of which symbols each extension provides. That inventory is
|
|
16
|
+
* also **version-aware**: a symbol can graduate into the server core and cease
|
|
17
|
+
* to be extension-owned (the canonical case is `gen_random_uuid()`, provided by
|
|
18
|
+
* `pgcrypto` before PostgreSQL 13 and part of core `pg_catalog` from 13 on — it
|
|
19
|
+
* must never be routed on 13+).
|
|
20
|
+
*
|
|
21
|
+
* An {@link ExtensionRouter} answers two questions:
|
|
22
|
+
*
|
|
23
|
+
* - **install**: where should `CREATE EXTENSION <e>` (or `ALTER EXTENSION <e>
|
|
24
|
+
* SET SCHEMA`) place the extension?
|
|
25
|
+
* - **reference**: given a reference to a symbol `(schema, name)` in namespace
|
|
26
|
+
* `ns`, what schema should it live in now — including stripping the qualifier
|
|
27
|
+
* entirely (relying on `search_path`) when the target is "unqualified".
|
|
28
|
+
*
|
|
29
|
+
* A schema of `null` denotes the **unqualified** site: a bare reference
|
|
30
|
+
* (`crypt(...)`) or installing an extension with no explicit `SCHEMA` clause.
|
|
31
|
+
* This makes the router fully bidirectional: `null -> 'extensions'` qualifies
|
|
32
|
+
* bare references, and `'extensions' -> null` strips them back to bare.
|
|
33
|
+
*/
|
|
34
|
+
/** PostgreSQL namespaces an extension symbol can occupy. */
|
|
35
|
+
export type ExtensionSymbolNamespace = 'function' | 'type' | 'operator';
|
|
36
|
+
/** A single symbol provided by an extension. */
|
|
37
|
+
export interface ExtensionSymbol {
|
|
38
|
+
/** Bare symbol name (e.g. `crypt`, `citext`, `gen_random_uuid`). */
|
|
39
|
+
name: string;
|
|
40
|
+
/** Which namespace the symbol occupies. Defaults to `function`. */
|
|
41
|
+
namespace?: ExtensionSymbolNamespace;
|
|
42
|
+
/**
|
|
43
|
+
* Major PostgreSQL version at/after which this symbol is part of the server
|
|
44
|
+
* core (`pg_catalog`) rather than the extension. At or above this version
|
|
45
|
+
* the symbol is **never routed** — it resolves from `pg_catalog` regardless
|
|
46
|
+
* of where the extension lives. The classic case is `gen_random_uuid`
|
|
47
|
+
* (`coreSince: 13`).
|
|
48
|
+
*/
|
|
49
|
+
coreSince?: number;
|
|
50
|
+
}
|
|
51
|
+
/** The set of symbols an extension provides. */
|
|
52
|
+
export interface ExtensionDefinition {
|
|
53
|
+
/** Extension name as used in `CREATE EXTENSION <name>`. */
|
|
54
|
+
name: string;
|
|
55
|
+
/**
|
|
56
|
+
* Whether the extension is relocatable (`ALTER EXTENSION ... SET SCHEMA`
|
|
57
|
+
* succeeds). Informational for callers deciding *how* to move an existing
|
|
58
|
+
* install; the router never emits `SET SCHEMA` itself.
|
|
59
|
+
*/
|
|
60
|
+
relocatable?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* A schema the extension is pinned to and cannot be moved out of (fixed by
|
|
63
|
+
* its control file, e.g. a geocoder that always creates its own schema).
|
|
64
|
+
* When set, the router refuses to route the extension's install or symbols.
|
|
65
|
+
*/
|
|
66
|
+
fixedSchema?: string;
|
|
67
|
+
/** Symbols the extension provides. */
|
|
68
|
+
symbols: ExtensionSymbol[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A routing rule for one extension. `to` is the destination install/reference
|
|
72
|
+
* schema (or `null` to strip qualification / install without a `SCHEMA`
|
|
73
|
+
* clause). `from` lists the schemas currently holding the extension that
|
|
74
|
+
* should be rewritten; a `null` entry additionally routes bare references.
|
|
75
|
+
* When `from` is omitted, references in **any** schema (and bare ones) are
|
|
76
|
+
* routed — useful when the source layout is unknown.
|
|
77
|
+
*/
|
|
78
|
+
export interface ExtensionRoute {
|
|
79
|
+
to: string | null;
|
|
80
|
+
from?: (string | null)[];
|
|
81
|
+
}
|
|
82
|
+
/** Routing specification keyed by extension name. */
|
|
83
|
+
export type ExtensionRouteSpec = Record<string, ExtensionRoute>;
|
|
84
|
+
/** A concrete rewrite instruction: from a schema (or bare) to a schema (or bare). */
|
|
85
|
+
export interface SymbolRewrite {
|
|
86
|
+
/** Target schema, or `null` to make the reference unqualified. */
|
|
87
|
+
to: string | null;
|
|
88
|
+
}
|
|
89
|
+
export interface ExtensionRouterOptions {
|
|
90
|
+
/**
|
|
91
|
+
* The symbol inventory. Defaults to {@link COMMON_EXTENSIONS}. Callers can
|
|
92
|
+
* pass an augmented or replacement inventory (e.g. extended from live-catalog
|
|
93
|
+
* introspection).
|
|
94
|
+
*/
|
|
95
|
+
inventory?: ExtensionDefinition[];
|
|
96
|
+
/**
|
|
97
|
+
* Target PostgreSQL major version, used to apply {@link ExtensionSymbol.coreSince}.
|
|
98
|
+
* Defaults to a high value so version-graduated symbols (e.g. `gen_random_uuid`)
|
|
99
|
+
* are treated as core and never routed unless a caller opts into an older
|
|
100
|
+
* version.
|
|
101
|
+
*/
|
|
102
|
+
serverVersion?: number;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A curated, platform-agnostic inventory of common contrib/extension symbols.
|
|
106
|
+
* Intentionally minimal and conservative — only well-known, stable symbols are
|
|
107
|
+
* listed so routing never touches something it cannot prove belongs to the
|
|
108
|
+
* extension. Callers extend this via {@link ExtensionRouterOptions.inventory}.
|
|
109
|
+
*/
|
|
110
|
+
export declare const COMMON_EXTENSIONS: ExtensionDefinition[];
|
|
111
|
+
/**
|
|
112
|
+
* Resolves install-schema and symbol-reference routing for extensions, backed
|
|
113
|
+
* by a version-aware symbol inventory.
|
|
114
|
+
*/
|
|
115
|
+
export declare class ExtensionRouter {
|
|
116
|
+
private readonly routes;
|
|
117
|
+
private readonly serverVersion;
|
|
118
|
+
private readonly definitions;
|
|
119
|
+
/** `${namespace}:${name}` -> providing extension (+ version predicate). */
|
|
120
|
+
private readonly symbolIndex;
|
|
121
|
+
constructor(routes?: ExtensionRouteSpec | Map<string, ExtensionRoute>, options?: ExtensionRouterOptions);
|
|
122
|
+
/**
|
|
123
|
+
* Build a router that moves the given extensions (default: every inventoried
|
|
124
|
+
* one) to a single `targetSchema`, rewriting their references from `public`
|
|
125
|
+
* and from bare (unqualified) sites. Passing `null` as `targetSchema` strips
|
|
126
|
+
* qualification instead (the "repollute public / rely on search_path"
|
|
127
|
+
* direction).
|
|
128
|
+
*/
|
|
129
|
+
static toSchema(targetSchema: string | null, options?: ExtensionRouterOptions & {
|
|
130
|
+
extensions?: string[];
|
|
131
|
+
from?: (string | null)[];
|
|
132
|
+
}): ExtensionRouter;
|
|
133
|
+
/** Coerce a spec, map, or existing router into a router. */
|
|
134
|
+
static from(source: ExtensionRouter | ExtensionRouteSpec | Map<string, ExtensionRoute>, options?: ExtensionRouterOptions): ExtensionRouter;
|
|
135
|
+
/** True when no extension routes are configured. */
|
|
136
|
+
get size(): number;
|
|
137
|
+
/** Extension names this router may rewrite. */
|
|
138
|
+
routedExtensions(): string[];
|
|
139
|
+
/** True when any configured extension has inventoried symbols to rewrite. */
|
|
140
|
+
hasSymbolRoutes(): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Resolve the install schema for `CREATE EXTENSION <extname>` /
|
|
143
|
+
* `ALTER EXTENSION <extname> SET SCHEMA`. Returns:
|
|
144
|
+
* - a string to place/keep the extension in that schema,
|
|
145
|
+
* - `null` to install with no explicit `SCHEMA` clause (server default),
|
|
146
|
+
* - `undefined` to leave the statement unchanged (no route, or the extension
|
|
147
|
+
* is pinned to a fixed schema).
|
|
148
|
+
*/
|
|
149
|
+
resolveInstall(extname: string | undefined | null): string | null | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* Resolve a rewrite for a reference to `(schema, name)` in namespace `ns`,
|
|
152
|
+
* where `schema` is `null` for a bare reference. Returns the target
|
|
153
|
+
* ({@link SymbolRewrite}) or `undefined` to leave the reference unchanged.
|
|
154
|
+
*
|
|
155
|
+
* A reference is rewritten only when the inventory proves the symbol belongs
|
|
156
|
+
* to a routed extension, the symbol has not graduated to core at the target
|
|
157
|
+
* server version, the current schema is one the route rewrites `from`, and
|
|
158
|
+
* the destination actually differs from where it already is.
|
|
159
|
+
*/
|
|
160
|
+
resolveSymbol(schema: string | null | undefined, name: string | undefined, ns: ExtensionSymbolNamespace): SymbolRewrite | undefined;
|
|
161
|
+
}
|
|
@@ -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
|
+
};
|