@pgsql/transform 18.4.1 → 18.6.0

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