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