@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.
@@ -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
+ };