2020 * `types` — matching `pg_class` / `pg_proc` / `pg_type`), mirroring the routing
2121 * model already used by {@link qualifyUnqualified}. Resolution is
2222 * 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.
2336 */
2437
2538/** PostgreSQL object namespaces relevant to schema routing. */
@@ -33,6 +46,26 @@ export type ObjectNamespace = 'relation' | 'function' | 'type';
3346 */
3447export type RouteNamespace = ObjectNamespace | 'schema' | 'unknown' ;
3548
49+ /**
50+ * Where a specific object should be reached instead. Either field may be
51+ * omitted: omitting `schema` keeps the schema-level default (or the current
52+ * schema when the route has none), and omitting `name` keeps the object's own
53+ * name — so `{ name }` alone is a pure rebind and `{ schema }` alone is
54+ * equivalent to the shorthand string form.
55+ */
56+ export interface ObjectRoute {
57+ /** Target schema, or `null` to make the reference unqualified. */
58+ schema ?: string | null ;
59+ /** Target object name — rebinds the reference to a different object. */
60+ name ?: string ;
61+ }
62+
63+ /**
64+ * An object route target. The shorthand `string` form is the target schema,
65+ * identical to `{ schema: target }`.
66+ */
67+ export type ObjectRouteTarget = string | ObjectRoute ;
68+
3669/** Per-source-schema routing: a schema-level default plus per-object routes. */
3770export interface SchemaRoute {
3871 /**
@@ -41,12 +74,12 @@ export interface SchemaRoute {
4174 * and leave the rest (and the schema itself) untouched.
4275 */
4376 schema ?: string ;
44- /** Relation name (table/view/sequence/matview) → target schema. */
45- relations ?: Record < string , string > ;
46- /** Function/procedure/aggregate name → target schema. */
47- functions ?: Record < string , string > ;
48- /** Type/domain name → target schema. */
49- types ?: Record < string , string > ;
77+ /** Relation name (table/view/sequence/matview) → target schema or rebind . */
78+ relations ?: Record < string , ObjectRouteTarget > ;
79+ /** Function/procedure/aggregate name → target schema or rebind . */
80+ functions ?: Record < string , ObjectRouteTarget > ;
81+ /** Type/domain name → target schema or rebind . */
82+ types ?: Record < string , ObjectRouteTarget > ;
5083}
5184
5285/** The full routing specification: one {@link SchemaRoute} per source schema. */
@@ -106,6 +139,35 @@ export class SchemaRouter {
106139 return false ;
107140 }
108141
142+ /**
143+ * True when any object route changes a name or de-qualifies (rather than
144+ * only moving between schemas). Such rewrites cannot be expressed by the
145+ * string-level passes at all, so callers use this to require the AST path.
146+ */
147+ hasNameRebinds ( ) : boolean {
148+ return this . nameRebinds ( ) . length > 0 ;
149+ }
150+
151+ /**
152+ * Every object route that rebinds a name or de-qualifies, keyed by source
153+ * schema and namespace. Callers use this to report or verify substitutions.
154+ */
155+ nameRebinds ( ) : Array < { schema : string ; ns : ObjectNamespace ; from : string ; to : ObjectRoute } > {
156+ const out : Array < { schema : string ; ns : ObjectNamespace ; from : string ; to : ObjectRoute } > = [ ] ;
157+ for ( const [ schema , route ] of this . routes ) {
158+ for ( const ns of [ 'relation' , 'function' , 'type' ] as ObjectNamespace [ ] ) {
159+ const bucket = route [ NS_BUCKET [ ns ] ] ;
160+ if ( ! bucket ) continue ;
161+ for ( const [ from , target ] of Object . entries ( bucket ) ) {
162+ if ( typeof target === 'string' ) continue ;
163+ if ( target . name === undefined && target . schema !== null ) continue ;
164+ out . push ( { schema, ns, from, to : target } ) ;
165+ }
166+ }
167+ }
168+ return out ;
169+ }
170+
109171 /** Every source schema this router may touch. */
110172 sourceSchemas ( ) : string [ ] {
111173 return [ ...this . routes . keys ( ) ] ;
@@ -122,16 +184,42 @@ export class SchemaRouter {
122184 name ?: string ,
123185 ns : RouteNamespace = 'unknown'
124186 ) : string | undefined {
187+ // A `null` target de-qualifies the reference; the schema-only API cannot
188+ // express that, so it reads as "unchanged" here.
189+ return this . resolveObject ( sourceSchema , name , ns ) ?. schema ?? undefined ;
190+ }
191+
192+ /**
193+ * Resolve the full target for `(sourceSchema, name)` in namespace `ns` — both
194+ * the schema the reference should live in and, when the route rebinds, the
195+ * name it should be reached by. Returns `undefined` to leave it unchanged.
196+ *
197+ * In the result, `schema` is `null` when the reference should become
198+ * unqualified and `undefined` when only the name changes; `name` is
199+ * `undefined` when only the schema changes.
200+ */
201+ resolveObject (
202+ sourceSchema : string | undefined | null ,
203+ name ?: string ,
204+ ns : RouteNamespace = 'unknown'
205+ ) : ObjectRoute | undefined {
125206 if ( ! sourceSchema ) return undefined ;
126207 const route = this . routes . get ( sourceSchema ) ;
127208 if ( ! route ) return undefined ;
128209
129210 if ( name && ( ns === 'relation' || ns === 'function' || ns === 'type' ) ) {
130- const bucket = route [ NS_BUCKET [ ns ] ] ;
131- const mapped = bucket ?. [ name ] ;
132- if ( mapped !== undefined ) return mapped ;
211+ const target = route [ NS_BUCKET [ ns ] ] ?. [ name ] ;
212+ if ( target !== undefined ) {
213+ if ( typeof target === 'string' ) return { schema : target } ;
214+ // An object route naming no schema inherits the schema-level default,
215+ // so a pure rebind leaves placement alone.
216+ return {
217+ schema : target . schema !== undefined ? target . schema : route . schema ,
218+ name : target . name
219+ } ;
220+ }
133221 }
134- return route . schema ;
222+ return route . schema !== undefined ? { schema : route . schema } : undefined ;
135223 }
136224
137225 /**
0 commit comments