@pgsql/transform 18.3.1 → 18.5.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/facts.js +41 -1
- package/esm/index.js +1 -0
- package/esm/router.js +121 -0
- package/esm/transform.js +178 -121
- package/facts.js +40 -0
- package/index.d.ts +3 -1
- package/index.js +3 -1
- package/package.json +2 -2
- package/router.d.ts +94 -0
- package/router.js +125 -0
- package/transform.d.ts +21 -12
- package/transform.js +178 -121
package/esm/facts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { walk as walkSql } from '@pgsql/traverse';
|
|
2
|
-
import { transformSync, walk as walkPlpgsql } from 'plpgsql-parser';
|
|
2
|
+
import { parseSql, transformSync, walk as walkPlpgsql } from 'plpgsql-parser';
|
|
3
3
|
const SECURITY_TAGS = new Set([
|
|
4
4
|
'CreatePolicyStmt',
|
|
5
5
|
'AlterPolicyStmt',
|
|
@@ -241,6 +241,43 @@ function classifyOne(nodeTag, node) {
|
|
|
241
241
|
collectRoles(node, facts.roles);
|
|
242
242
|
return facts;
|
|
243
243
|
}
|
|
244
|
+
/** Read a `CreateFunctionStmt` DefElem option's scalar/list value. */
|
|
245
|
+
function functionOption(node, defname) {
|
|
246
|
+
for (const opt of node.options ?? []) {
|
|
247
|
+
if (opt?.DefElem?.defname === defname)
|
|
248
|
+
return opt.DefElem.arg;
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Collect references from a `LANGUAGE sql` function body supplied as a string
|
|
254
|
+
* literal (`AS $$ ... $$`). That body is an opaque String node the AST walker
|
|
255
|
+
* never parses, so — mirroring the schema transformer's body rewrite — parse
|
|
256
|
+
* it standalone and walk each statement with the facts visitor. The standard
|
|
257
|
+
* `BEGIN ATOMIC` / `RETURN` `sql_body` form is already part of the AST and is
|
|
258
|
+
* covered by the outer walk, so only the string form needs this.
|
|
259
|
+
*/
|
|
260
|
+
function collectSqlBodyReferences(node, facts) {
|
|
261
|
+
const language = functionOption(node, 'language')?.String?.sval;
|
|
262
|
+
if (typeof language !== 'string' || language.toLowerCase() !== 'sql')
|
|
263
|
+
return;
|
|
264
|
+
const asArg = functionOption(node, 'as');
|
|
265
|
+
const items = asArg?.List?.items ?? [];
|
|
266
|
+
const body = items[0]?.String?.sval;
|
|
267
|
+
if (typeof body !== 'string')
|
|
268
|
+
return;
|
|
269
|
+
try {
|
|
270
|
+
const stmts = parseSql(body)?.stmts ?? [];
|
|
271
|
+
const visitor = createFactsVisitor(facts, facts.bodyReferences);
|
|
272
|
+
for (const stmt of stmts) {
|
|
273
|
+
if (stmt?.stmt)
|
|
274
|
+
walkSql(stmt.stmt, visitor);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// A non-parseable body (C symbol name, etc.) contributes no references.
|
|
279
|
+
}
|
|
280
|
+
}
|
|
244
281
|
/**
|
|
245
282
|
* Classify each top-level statement in a SQL script into {@link StatementFacts}.
|
|
246
283
|
*
|
|
@@ -260,6 +297,9 @@ export function classifyStatements(sql) {
|
|
|
260
297
|
if (stmtNode) {
|
|
261
298
|
walkSql(stmtNode, createFactsVisitor(facts));
|
|
262
299
|
}
|
|
300
|
+
if (nodeTag === 'CreateFunctionStmt') {
|
|
301
|
+
collectSqlBodyReferences(node, facts);
|
|
302
|
+
}
|
|
263
303
|
allFacts.push(facts);
|
|
264
304
|
}
|
|
265
305
|
for (const fn of ctx.functions ?? []) {
|
package/esm/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { classifyStatements } from './facts';
|
|
2
2
|
export { collectCreatedObjects, createQualifyVisitor, mergeInventories, qualifyUnqualified, } from './qualify';
|
|
3
|
+
export { SchemaRouter } from './router';
|
|
3
4
|
export { captureAstsFromSql, captureTransformAsts, firstDifference, normalizeParseTree, validateRoundTrip, } from './round-trip';
|
|
4
5
|
export { CLEAN_TREE_VOLATILE_KEYS, cleanTree, normalizeTree, trimDefElemBody, } from './round-trip-core';
|
|
5
6
|
export { createPlpgsqlVisitor, createSqlVisitor, escapeRegexp, extractPgpmHeader, shouldTransformSchema, transformComments, transformJsonStringValues, transformNameList, transformPlpgsqlTypeAst, transformPlpgsqlTypeString, transformRelation, transformSchemaName, transformSql, transformSqlStatement, transformVerifyCalls, validateNoUntransformedSchemas, walkPlpgsqlForSchemas, } from './transform';
|
package/esm/router.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema routing for the core transform.
|
|
3
|
+
*
|
|
4
|
+
* The schema transform historically rewrote every reference to a source
|
|
5
|
+
* schema to a single target schema (`Map<oldSchema, newSchema>`). That is the
|
|
6
|
+
* degenerate, whole-schema case of a more general question asked at every
|
|
7
|
+
* schema-qualified occurrence:
|
|
8
|
+
*
|
|
9
|
+
* given a reference to `(schema, name)` in namespace `ns`, what schema
|
|
10
|
+
* should it live in now?
|
|
11
|
+
*
|
|
12
|
+
* A {@link SchemaRouter} answers exactly that. It unifies two dimensions:
|
|
13
|
+
*
|
|
14
|
+
* - **schema-level** default: move everything in a source schema to one target
|
|
15
|
+
* (the classic `Map` behaviour), and
|
|
16
|
+
* - **object-level** routes: send a specific object — a table, a function, a
|
|
17
|
+
* type — to its own target schema, independent of its siblings.
|
|
18
|
+
*
|
|
19
|
+
* Object routes are bucketed by PostgreSQL namespace (`relations`, `functions`,
|
|
20
|
+
* `types` — matching `pg_class` / `pg_proc` / `pg_type`), mirroring the routing
|
|
21
|
+
* model already used by {@link qualifyUnqualified}. Resolution is
|
|
22
|
+
* object-route-first, then the schema-level default, then "leave unchanged".
|
|
23
|
+
*/
|
|
24
|
+
const NS_BUCKET = {
|
|
25
|
+
relation: 'relations',
|
|
26
|
+
function: 'functions',
|
|
27
|
+
type: 'types'
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the target schema for any schema-qualified occurrence, unifying the
|
|
31
|
+
* whole-schema `Map` behaviour and per-object routing behind one `resolve`.
|
|
32
|
+
*/
|
|
33
|
+
export class SchemaRouter {
|
|
34
|
+
routes;
|
|
35
|
+
constructor(routes = {}) {
|
|
36
|
+
this.routes = routes instanceof Map ? new Map(routes) : new Map(Object.entries(routes));
|
|
37
|
+
}
|
|
38
|
+
/** Build a router from the classic whole-schema `Map<oldSchema,newSchema>`. */
|
|
39
|
+
static fromSchemaMap(mapping) {
|
|
40
|
+
const entries = mapping instanceof Map ? [...mapping.entries()] : Object.entries(mapping);
|
|
41
|
+
const spec = {};
|
|
42
|
+
for (const [from, to] of entries)
|
|
43
|
+
spec[from] = { schema: to };
|
|
44
|
+
return new SchemaRouter(spec);
|
|
45
|
+
}
|
|
46
|
+
/** Coerce a `Map`, plain mapping, or existing router into a router. */
|
|
47
|
+
static from(source) {
|
|
48
|
+
if (source instanceof SchemaRouter)
|
|
49
|
+
return source;
|
|
50
|
+
return SchemaRouter.fromSchemaMap(source);
|
|
51
|
+
}
|
|
52
|
+
/** True when this router might rewrite something in `sourceSchema`. */
|
|
53
|
+
has(sourceSchema) {
|
|
54
|
+
if (!sourceSchema)
|
|
55
|
+
return false;
|
|
56
|
+
return this.routes.has(sourceSchema);
|
|
57
|
+
}
|
|
58
|
+
/** True when the router carries no routes at all. */
|
|
59
|
+
get size() {
|
|
60
|
+
return this.routes.size;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* True when any route targets individual objects (as opposed to whole
|
|
64
|
+
* schemas). Object routes require AST-precise rewriting of opaque function
|
|
65
|
+
* bodies; whole-schema routes are handled by the cheaper string passes.
|
|
66
|
+
*/
|
|
67
|
+
hasObjectRoutes() {
|
|
68
|
+
for (const route of this.routes.values()) {
|
|
69
|
+
if (route.relations || route.functions || route.types)
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/** Every source schema this router may touch. */
|
|
75
|
+
sourceSchemas() {
|
|
76
|
+
return [...this.routes.keys()];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the target schema for `(sourceSchema, name)` in namespace `ns`,
|
|
80
|
+
* or `undefined` to leave it unchanged. Object routes win over the
|
|
81
|
+
* schema-level default; the default applies to schema-level operations and
|
|
82
|
+
* to any object without a specific route.
|
|
83
|
+
*/
|
|
84
|
+
resolve(sourceSchema, name, ns = 'unknown') {
|
|
85
|
+
if (!sourceSchema)
|
|
86
|
+
return undefined;
|
|
87
|
+
const route = this.routes.get(sourceSchema);
|
|
88
|
+
if (!route)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (name && (ns === 'relation' || ns === 'function' || ns === 'type')) {
|
|
91
|
+
const bucket = route[NS_BUCKET[ns]];
|
|
92
|
+
const mapped = bucket?.[name];
|
|
93
|
+
if (mapped !== undefined)
|
|
94
|
+
return mapped;
|
|
95
|
+
}
|
|
96
|
+
return route.schema;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Source schemas whose *every* object is guaranteed to move — i.e. those
|
|
100
|
+
* with a schema-level default. After a transform, none of these should
|
|
101
|
+
* survive as a qualifier; partially-routed schemas may legitimately remain,
|
|
102
|
+
* so they are excluded from the strict leftover check.
|
|
103
|
+
*/
|
|
104
|
+
fullyMovedSchemas() {
|
|
105
|
+
const out = new Map();
|
|
106
|
+
for (const [from, route] of this.routes) {
|
|
107
|
+
if (route.schema !== undefined)
|
|
108
|
+
out.set(from, route.schema);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A flat schema-level view (`oldSchema → newSchema`) for legacy string-level
|
|
114
|
+
* passes and comment/verify/JSON rewrites that operate per source schema.
|
|
115
|
+
* Only schema-level defaults are included; object-only routes carry no single
|
|
116
|
+
* schema answer and are omitted.
|
|
117
|
+
*/
|
|
118
|
+
schemaLevelMap() {
|
|
119
|
+
return this.fullyMovedSchemas();
|
|
120
|
+
}
|
|
121
|
+
}
|