@pgsql/transform 18.6.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.
package/esm/router.js CHANGED
@@ -20,6 +20,19 @@
20
20
  * `types` — matching `pg_class` / `pg_proc` / `pg_type`), mirroring the routing
21
21
  * model already used by {@link qualifyUnqualified}. Resolution is
22
22
  * object-route-first, then the schema-level default, then "leave unchanged".
23
+ *
24
+ * An object route may also *rebind* — change the object's **name**, not just
25
+ * the schema it lives in. Routing preserves identity (the same function, a new
26
+ * address); rebinding repoints a reference at a *different* object, which is
27
+ * what lets one implementation of a contract be substituted for another:
28
+ *
29
+ * ```ts
30
+ * { auth: { functions: { uid: { schema: null, name: 'current_user_id' } } } }
31
+ * // auth.uid() -> current_user_id()
32
+ * ```
33
+ *
34
+ * A `null` target schema de-qualifies the reference (relying on `search_path`),
35
+ * matching the convention used by the extension router.
23
36
  */
24
37
  const NS_BUCKET = {
25
38
  relation: 'relations',
@@ -71,6 +84,36 @@ export class SchemaRouter {
71
84
  }
72
85
  return false;
73
86
  }
87
+ /**
88
+ * True when any object route changes a name or de-qualifies (rather than
89
+ * only moving between schemas). Such rewrites cannot be expressed by the
90
+ * string-level passes at all, so callers use this to require the AST path.
91
+ */
92
+ hasNameRebinds() {
93
+ return this.nameRebinds().length > 0;
94
+ }
95
+ /**
96
+ * Every object route that rebinds a name or de-qualifies, keyed by source
97
+ * schema and namespace. Callers use this to report or verify substitutions.
98
+ */
99
+ nameRebinds() {
100
+ const out = [];
101
+ for (const [schema, route] of this.routes) {
102
+ for (const ns of ['relation', 'function', 'type']) {
103
+ const bucket = route[NS_BUCKET[ns]];
104
+ if (!bucket)
105
+ continue;
106
+ for (const [from, target] of Object.entries(bucket)) {
107
+ if (typeof target === 'string')
108
+ continue;
109
+ if (target.name === undefined && target.schema !== null)
110
+ continue;
111
+ out.push({ schema, ns, from, to: target });
112
+ }
113
+ }
114
+ }
115
+ return out;
116
+ }
74
117
  /** Every source schema this router may touch. */
75
118
  sourceSchemas() {
76
119
  return [...this.routes.keys()];
@@ -82,18 +125,39 @@ export class SchemaRouter {
82
125
  * to any object without a specific route.
83
126
  */
84
127
  resolve(sourceSchema, name, ns = 'unknown') {
128
+ // A `null` target de-qualifies the reference; the schema-only API cannot
129
+ // express that, so it reads as "unchanged" here.
130
+ return this.resolveObject(sourceSchema, name, ns)?.schema ?? undefined;
131
+ }
132
+ /**
133
+ * Resolve the full target for `(sourceSchema, name)` in namespace `ns` — both
134
+ * the schema the reference should live in and, when the route rebinds, the
135
+ * name it should be reached by. Returns `undefined` to leave it unchanged.
136
+ *
137
+ * In the result, `schema` is `null` when the reference should become
138
+ * unqualified and `undefined` when only the name changes; `name` is
139
+ * `undefined` when only the schema changes.
140
+ */
141
+ resolveObject(sourceSchema, name, ns = 'unknown') {
85
142
  if (!sourceSchema)
86
143
  return undefined;
87
144
  const route = this.routes.get(sourceSchema);
88
145
  if (!route)
89
146
  return undefined;
90
147
  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;
148
+ const target = route[NS_BUCKET[ns]]?.[name];
149
+ if (target !== undefined) {
150
+ if (typeof target === 'string')
151
+ return { schema: target };
152
+ // An object route naming no schema inherits the schema-level default,
153
+ // so a pure rebind leaves placement alone.
154
+ return {
155
+ schema: target.schema !== undefined ? target.schema : route.schema,
156
+ name: target.name
157
+ };
158
+ }
95
159
  }
96
- return route.schema;
160
+ return route.schema !== undefined ? { schema: route.schema } : undefined;
97
161
  }
98
162
  /**
99
163
  * Source schemas whose *every* object is guaranteed to move — i.e. those
package/esm/transform.js CHANGED
@@ -80,12 +80,24 @@ export function transformNameList(names, schemaMapping, result, ns = 'unknown')
80
80
  const first = names[0];
81
81
  if (first?.String?.sval) {
82
82
  const schemaName = first.String.sval;
83
- const objName = names[names.length - 1]?.String?.sval;
84
- const newName = router.resolve(schemaName, objName, ns);
85
- if (newName && newName !== schemaName) {
83
+ const last = names[names.length - 1];
84
+ const objName = last?.String?.sval;
85
+ const target = router.resolveObject(schemaName, objName, ns);
86
+ if (!target)
87
+ return;
88
+ if (target.name !== undefined && last?.String?.sval) {
86
89
  result.schemasFound.add(schemaName);
87
- first.String.sval = newName;
88
- result.schemasTransformed.set(schemaName, newName);
90
+ last.String.sval = target.name;
91
+ }
92
+ if (target.schema === null) {
93
+ // De-qualify: drop the schema element and rely on search_path.
94
+ result.schemasFound.add(schemaName);
95
+ names.splice(0, 1);
96
+ }
97
+ else if (target.schema && target.schema !== schemaName) {
98
+ result.schemasFound.add(schemaName);
99
+ first.String.sval = target.schema;
100
+ result.schemasTransformed.set(schemaName, target.schema);
89
101
  }
90
102
  }
91
103
  }
@@ -117,11 +129,22 @@ export function transformRelation(relation, schemaMapping, result) {
117
129
  const oldName = relation.schemaname;
118
130
  // A RangeVar names a relation (table/view/sequence/matview); route by the
119
131
  // relation name so object-level routes can send it to its own schema.
120
- const newName = asRouter(schemaMapping).resolve(oldName, relation.relname, 'relation');
121
- if (newName && newName !== oldName) {
132
+ const target = asRouter(schemaMapping).resolveObject(oldName, relation.relname, 'relation');
133
+ if (!target)
134
+ return;
135
+ if (target.name !== undefined && relation.relname) {
136
+ result.schemasFound.add(oldName);
137
+ relation.relname = target.name;
138
+ }
139
+ if (target.schema === null) {
140
+ // De-qualify: drop the schema qualifier and rely on search_path.
141
+ result.schemasFound.add(oldName);
142
+ delete relation.schemaname;
143
+ }
144
+ else if (target.schema && target.schema !== oldName) {
122
145
  result.schemasFound.add(oldName);
123
- relation.schemaname = newName;
124
- result.schemasTransformed.set(oldName, newName);
146
+ relation.schemaname = target.schema;
147
+ result.schemasTransformed.set(oldName, target.schema);
125
148
  }
126
149
  }
127
150
  /**
package/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export type { RoleTransformResult } from './role-transform';
10
10
  export { createRoleResult, createRoleVisitor, transformRoles, } from './role-transform';
11
11
  export type { ObjectInventory, QualifyResult, QualifyRoutes, QualifyTargetSelector, QualifyUnqualifiedOptions, } from './qualify';
12
12
  export { collectCreatedObjects, createQualifyVisitor, mergeInventories, qualifyUnqualified, } from './qualify';
13
- export type { ObjectNamespace, RouteNamespace, RouteSpec, SchemaRoute, } from './router';
13
+ export type { ObjectNamespace, ObjectRoute, ObjectRouteTarget, RouteNamespace, RouteSpec, SchemaRoute, } from './router';
14
14
  export { SchemaRouter } from './router';
15
15
  export type { CapturedAsts } from './round-trip';
16
16
  export { captureAstsFromSql, captureTransformAsts, firstDifference, normalizeParseTree, validateRoundTrip, } from './round-trip';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgsql/transform",
3
- "version": "18.6.0",
3
+ "version": "18.7.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": "6d3c28348fab5cace76f7c0167f3b73e8f9f67c8"
52
+ "gitHead": "057e427849438bfea946d6d85557603ba7fea134"
53
53
  }
package/router.d.ts CHANGED
@@ -20,6 +20,19 @@
20
20
  * `types` — matching `pg_class` / `pg_proc` / `pg_type`), mirroring the routing
21
21
  * model already used by {@link qualifyUnqualified}. Resolution is
22
22
  * object-route-first, then the schema-level default, then "leave unchanged".
23
+ *
24
+ * An object route may also *rebind* — change the object's **name**, not just
25
+ * the schema it lives in. Routing preserves identity (the same function, a new
26
+ * address); rebinding repoints a reference at a *different* object, which is
27
+ * what lets one implementation of a contract be substituted for another:
28
+ *
29
+ * ```ts
30
+ * { auth: { functions: { uid: { schema: null, name: 'current_user_id' } } } }
31
+ * // auth.uid() -> current_user_id()
32
+ * ```
33
+ *
34
+ * A `null` target schema de-qualifies the reference (relying on `search_path`),
35
+ * matching the convention used by the extension router.
23
36
  */
24
37
  /** PostgreSQL object namespaces relevant to schema routing. */
25
38
  export type ObjectNamespace = 'relation' | 'function' | 'type';
@@ -30,6 +43,24 @@ export type ObjectNamespace = 'relation' | 'function' | 'type';
30
43
  * which case only the schema-level default applies.
31
44
  */
32
45
  export type RouteNamespace = ObjectNamespace | 'schema' | 'unknown';
46
+ /**
47
+ * Where a specific object should be reached instead. Either field may be
48
+ * omitted: omitting `schema` keeps the schema-level default (or the current
49
+ * schema when the route has none), and omitting `name` keeps the object's own
50
+ * name — so `{ name }` alone is a pure rebind and `{ schema }` alone is
51
+ * equivalent to the shorthand string form.
52
+ */
53
+ export interface ObjectRoute {
54
+ /** Target schema, or `null` to make the reference unqualified. */
55
+ schema?: string | null;
56
+ /** Target object name — rebinds the reference to a different object. */
57
+ name?: string;
58
+ }
59
+ /**
60
+ * An object route target. The shorthand `string` form is the target schema,
61
+ * identical to `{ schema: target }`.
62
+ */
63
+ export type ObjectRouteTarget = string | ObjectRoute;
33
64
  /** Per-source-schema routing: a schema-level default plus per-object routes. */
34
65
  export interface SchemaRoute {
35
66
  /**
@@ -38,12 +69,12 @@ export interface SchemaRoute {
38
69
  * and leave the rest (and the schema itself) untouched.
39
70
  */
40
71
  schema?: string;
41
- /** Relation name (table/view/sequence/matview) → target schema. */
42
- relations?: Record<string, string>;
43
- /** Function/procedure/aggregate name → target schema. */
44
- functions?: Record<string, string>;
45
- /** Type/domain name → target schema. */
46
- types?: Record<string, string>;
72
+ /** Relation name (table/view/sequence/matview) → target schema or rebind. */
73
+ relations?: Record<string, ObjectRouteTarget>;
74
+ /** Function/procedure/aggregate name → target schema or rebind. */
75
+ functions?: Record<string, ObjectRouteTarget>;
76
+ /** Type/domain name → target schema or rebind. */
77
+ types?: Record<string, ObjectRouteTarget>;
47
78
  }
48
79
  /** The full routing specification: one {@link SchemaRoute} per source schema. */
49
80
  export type RouteSpec = Record<string, SchemaRoute>;
@@ -68,6 +99,22 @@ export declare class SchemaRouter {
68
99
  * bodies; whole-schema routes are handled by the cheaper string passes.
69
100
  */
70
101
  hasObjectRoutes(): boolean;
102
+ /**
103
+ * True when any object route changes a name or de-qualifies (rather than
104
+ * only moving between schemas). Such rewrites cannot be expressed by the
105
+ * string-level passes at all, so callers use this to require the AST path.
106
+ */
107
+ hasNameRebinds(): boolean;
108
+ /**
109
+ * Every object route that rebinds a name or de-qualifies, keyed by source
110
+ * schema and namespace. Callers use this to report or verify substitutions.
111
+ */
112
+ nameRebinds(): Array<{
113
+ schema: string;
114
+ ns: ObjectNamespace;
115
+ from: string;
116
+ to: ObjectRoute;
117
+ }>;
71
118
  /** Every source schema this router may touch. */
72
119
  sourceSchemas(): string[];
73
120
  /**
@@ -77,6 +124,16 @@ export declare class SchemaRouter {
77
124
  * to any object without a specific route.
78
125
  */
79
126
  resolve(sourceSchema: string | undefined | null, name?: string, ns?: RouteNamespace): string | undefined;
127
+ /**
128
+ * Resolve the full target for `(sourceSchema, name)` in namespace `ns` — both
129
+ * the schema the reference should live in and, when the route rebinds, the
130
+ * name it should be reached by. Returns `undefined` to leave it unchanged.
131
+ *
132
+ * In the result, `schema` is `null` when the reference should become
133
+ * unqualified and `undefined` when only the name changes; `name` is
134
+ * `undefined` when only the schema changes.
135
+ */
136
+ resolveObject(sourceSchema: string | undefined | null, name?: string, ns?: RouteNamespace): ObjectRoute | undefined;
80
137
  /**
81
138
  * Source schemas whose *every* object is guaranteed to move — i.e. those
82
139
  * with a schema-level default. After a transform, none of these should
package/router.js CHANGED
@@ -21,6 +21,19 @@
21
21
  * `types` — matching `pg_class` / `pg_proc` / `pg_type`), mirroring the routing
22
22
  * model already used by {@link qualifyUnqualified}. Resolution is
23
23
  * object-route-first, then the schema-level default, then "leave unchanged".
24
+ *
25
+ * An object route may also *rebind* — change the object's **name**, not just
26
+ * the schema it lives in. Routing preserves identity (the same function, a new
27
+ * address); rebinding repoints a reference at a *different* object, which is
28
+ * what lets one implementation of a contract be substituted for another:
29
+ *
30
+ * ```ts
31
+ * { auth: { functions: { uid: { schema: null, name: 'current_user_id' } } } }
32
+ * // auth.uid() -> current_user_id()
33
+ * ```
34
+ *
35
+ * A `null` target schema de-qualifies the reference (relying on `search_path`),
36
+ * matching the convention used by the extension router.
24
37
  */
25
38
  Object.defineProperty(exports, "__esModule", { value: true });
26
39
  exports.SchemaRouter = void 0;
@@ -74,6 +87,36 @@ class SchemaRouter {
74
87
  }
75
88
  return false;
76
89
  }
90
+ /**
91
+ * True when any object route changes a name or de-qualifies (rather than
92
+ * only moving between schemas). Such rewrites cannot be expressed by the
93
+ * string-level passes at all, so callers use this to require the AST path.
94
+ */
95
+ hasNameRebinds() {
96
+ return this.nameRebinds().length > 0;
97
+ }
98
+ /**
99
+ * Every object route that rebinds a name or de-qualifies, keyed by source
100
+ * schema and namespace. Callers use this to report or verify substitutions.
101
+ */
102
+ nameRebinds() {
103
+ const out = [];
104
+ for (const [schema, route] of this.routes) {
105
+ for (const ns of ['relation', 'function', 'type']) {
106
+ const bucket = route[NS_BUCKET[ns]];
107
+ if (!bucket)
108
+ continue;
109
+ for (const [from, target] of Object.entries(bucket)) {
110
+ if (typeof target === 'string')
111
+ continue;
112
+ if (target.name === undefined && target.schema !== null)
113
+ continue;
114
+ out.push({ schema, ns, from, to: target });
115
+ }
116
+ }
117
+ }
118
+ return out;
119
+ }
77
120
  /** Every source schema this router may touch. */
78
121
  sourceSchemas() {
79
122
  return [...this.routes.keys()];
@@ -85,18 +128,39 @@ class SchemaRouter {
85
128
  * to any object without a specific route.
86
129
  */
87
130
  resolve(sourceSchema, name, ns = 'unknown') {
131
+ // A `null` target de-qualifies the reference; the schema-only API cannot
132
+ // express that, so it reads as "unchanged" here.
133
+ return this.resolveObject(sourceSchema, name, ns)?.schema ?? undefined;
134
+ }
135
+ /**
136
+ * Resolve the full target for `(sourceSchema, name)` in namespace `ns` — both
137
+ * the schema the reference should live in and, when the route rebinds, the
138
+ * name it should be reached by. Returns `undefined` to leave it unchanged.
139
+ *
140
+ * In the result, `schema` is `null` when the reference should become
141
+ * unqualified and `undefined` when only the name changes; `name` is
142
+ * `undefined` when only the schema changes.
143
+ */
144
+ resolveObject(sourceSchema, name, ns = 'unknown') {
88
145
  if (!sourceSchema)
89
146
  return undefined;
90
147
  const route = this.routes.get(sourceSchema);
91
148
  if (!route)
92
149
  return undefined;
93
150
  if (name && (ns === 'relation' || ns === 'function' || ns === 'type')) {
94
- const bucket = route[NS_BUCKET[ns]];
95
- const mapped = bucket?.[name];
96
- if (mapped !== undefined)
97
- return mapped;
151
+ const target = route[NS_BUCKET[ns]]?.[name];
152
+ if (target !== undefined) {
153
+ if (typeof target === 'string')
154
+ return { schema: target };
155
+ // An object route naming no schema inherits the schema-level default,
156
+ // so a pure rebind leaves placement alone.
157
+ return {
158
+ schema: target.schema !== undefined ? target.schema : route.schema,
159
+ name: target.name
160
+ };
161
+ }
98
162
  }
99
- return route.schema;
163
+ return route.schema !== undefined ? { schema: route.schema } : undefined;
100
164
  }
101
165
  /**
102
166
  * Source schemas whose *every* object is guaranteed to move — i.e. those
package/transform.js CHANGED
@@ -101,12 +101,24 @@ function transformNameList(names, schemaMapping, result, ns = 'unknown') {
101
101
  const first = names[0];
102
102
  if (first?.String?.sval) {
103
103
  const schemaName = first.String.sval;
104
- const objName = names[names.length - 1]?.String?.sval;
105
- const newName = router.resolve(schemaName, objName, ns);
106
- if (newName && newName !== schemaName) {
104
+ const last = names[names.length - 1];
105
+ const objName = last?.String?.sval;
106
+ const target = router.resolveObject(schemaName, objName, ns);
107
+ if (!target)
108
+ return;
109
+ if (target.name !== undefined && last?.String?.sval) {
107
110
  result.schemasFound.add(schemaName);
108
- first.String.sval = newName;
109
- result.schemasTransformed.set(schemaName, newName);
111
+ last.String.sval = target.name;
112
+ }
113
+ if (target.schema === null) {
114
+ // De-qualify: drop the schema element and rely on search_path.
115
+ result.schemasFound.add(schemaName);
116
+ names.splice(0, 1);
117
+ }
118
+ else if (target.schema && target.schema !== schemaName) {
119
+ result.schemasFound.add(schemaName);
120
+ first.String.sval = target.schema;
121
+ result.schemasTransformed.set(schemaName, target.schema);
110
122
  }
111
123
  }
112
124
  }
@@ -138,11 +150,22 @@ function transformRelation(relation, schemaMapping, result) {
138
150
  const oldName = relation.schemaname;
139
151
  // A RangeVar names a relation (table/view/sequence/matview); route by the
140
152
  // relation name so object-level routes can send it to its own schema.
141
- const newName = asRouter(schemaMapping).resolve(oldName, relation.relname, 'relation');
142
- if (newName && newName !== oldName) {
153
+ const target = asRouter(schemaMapping).resolveObject(oldName, relation.relname, 'relation');
154
+ if (!target)
155
+ return;
156
+ if (target.name !== undefined && relation.relname) {
157
+ result.schemasFound.add(oldName);
158
+ relation.relname = target.name;
159
+ }
160
+ if (target.schema === null) {
161
+ // De-qualify: drop the schema qualifier and rely on search_path.
162
+ result.schemasFound.add(oldName);
163
+ delete relation.schemaname;
164
+ }
165
+ else if (target.schema && target.schema !== oldName) {
143
166
  result.schemasFound.add(oldName);
144
- relation.schemaname = newName;
145
- result.schemasTransformed.set(oldName, newName);
167
+ relation.schemaname = target.schema;
168
+ result.schemasTransformed.set(oldName, target.schema);
146
169
  }
147
170
  }
148
171
  /**