@@ -304,6 +304,105 @@ export function transformSchemaRefsInString(
304304 return out ;
305305}
306306
307+ /**
308+ * Cast types whose string operand is an object reference rather than opaque
309+ * text. `'sch.tbl'::regclass` is a relation the parser resolves, and the cast
310+ * names the namespace it resolves in — so the reference can be routed from the
311+ * cast, semantically, instead of pattern-matched. This reaches what the string
312+ * passes cannot: a bare `'sch'::regnamespace` has no dot to match on.
313+ */
314+ const IDENTITY_CAST_NAMESPACES : Record < string , RouteNamespace > = {
315+ regclass : 'relation' ,
316+ regcollation : 'unknown' ,
317+ regconfig : 'unknown' ,
318+ regdictionary : 'unknown' ,
319+ regnamespace : 'schema' ,
320+ regoper : 'unknown' ,
321+ regoperator : 'unknown' ,
322+ regproc : 'function' ,
323+ regprocedure : 'function' ,
324+ regtype : 'type'
325+ } ;
326+
327+ /**
328+ * The namespace a cast resolves its operand in, or `undefined` when the target
329+ * type is not an object-identity type.
330+ */
331+ export function identityCastNamespace ( typeName : any ) : RouteNamespace | undefined {
332+ const names = typeName ?. names ;
333+ if ( ! Array . isArray ( names ) || names . length === 0 ) return undefined ;
334+ // Only `pg_catalog`-qualified (or unqualified) reg* types are the built-ins.
335+ if ( names . length > 1 && names [ 0 ] ?. String ?. sval !== 'pg_catalog' ) return undefined ;
336+ const typeIdent = names [ names . length - 1 ] ?. String ?. sval ;
337+ if ( typeof typeIdent !== 'string' ) return undefined ;
338+ return IDENTITY_CAST_NAMESPACES [ typeIdent . toLowerCase ( ) ] ;
339+ }
340+
341+ const IDENTITY_TOKEN = String . raw `(?:"(?:[^"]|"")*"|[^\s".,()[\]]+)` ;
342+ const BARE_IDENTITY_RE = new RegExp ( `^\\s*(${ IDENTITY_TOKEN } )\\s*$` ) ;
343+ const QUALIFIED_IDENTITY_RE = new RegExp ( `^\\s*(${ IDENTITY_TOKEN } )\\.([\\s\\S]+)$` ) ;
344+
345+ function unquoteIdentifier ( token : string ) : string {
346+ if ( token . length > 1 && token . startsWith ( '"' ) && token . endsWith ( '"' ) ) {
347+ return token . slice ( 1 , - 1 ) . replace ( / " " / g, '"' ) ;
348+ }
349+ return token ;
350+ }
351+
352+ /**
353+ * Route the object reference carried in the string operand of an identity
354+ * cast. `ns` comes from the cast's target type.
355+ *
356+ * Unqualified operands (`'tbl'::regclass`) name no schema — they resolve
357+ * through `search_path` — so they are returned unchanged.
358+ */
359+ export function transformIdentityCastLiteral (
360+ sval : string ,
361+ ns : RouteNamespace ,
362+ schemaMapping : SchemaMappingInput ,
363+ result : SchemaTransformResult
364+ ) : string {
365+ const router = asRouter ( schemaMapping ) ;
366+
367+ if ( ns === 'schema' ) {
368+ const bare = BARE_IDENTITY_RE . exec ( sval ) ;
369+ if ( ! bare ) return sval ;
370+ const schemaName = unquoteIdentifier ( bare [ 1 ] ) ;
371+ const newName = router . resolve ( schemaName , undefined , 'schema' ) ;
372+ if ( ! newName || newName === schemaName ) return sval ;
373+ result . schemasFound . add ( schemaName ) ;
374+ result . schemasTransformed . set ( schemaName , newName ) ;
375+ return QuoteUtils . quoteIdentifier ( newName ) ;
376+ }
377+
378+ // `regprocedure` and `regoperator` carry an argument list after the name, and
379+ // an argument type can itself be schema-qualified
380+ // (`fn("my-schema".row_type)`), so the list is routed as ordinary schema refs.
381+ const argsAt = sval . indexOf ( '(' ) ;
382+ const head = argsAt === - 1 ? sval : sval . slice ( 0 , argsAt ) ;
383+ const args =
384+ argsAt === - 1 ? '' : transformSchemaRefsInString ( sval . slice ( argsAt ) , router , result ) ;
385+
386+ const qualified = QUALIFIED_IDENTITY_RE . exec ( head ) ;
387+ if ( ! qualified ) return `${ head } ${ args } ` ;
388+ const schemaName = unquoteIdentifier ( qualified [ 1 ] ) ;
389+ const objName = unquoteIdentifier ( qualified [ 2 ] . trim ( ) ) ;
390+
391+ const target = router . resolveObject ( schemaName , objName , ns ) ;
392+ if ( ! target ) return `${ head } ${ args } ` ;
393+ const rebound = target . name !== undefined && target . name !== objName ;
394+ const requalified = target . schema === null || ( ! ! target . schema && target . schema !== schemaName ) ;
395+ if ( ! rebound && ! requalified ) return `${ head } ${ args } ` ;
396+
397+ result . schemasFound . add ( schemaName ) ;
398+ const nameToken = QuoteUtils . quoteIdentifier ( rebound ? target . name ! : objName ) ;
399+ if ( target . schema === null ) return `${ nameToken } ${ args } ` ;
400+ if ( target . schema && target . schema !== schemaName ) {
401+ result . schemasTransformed . set ( schemaName , target . schema ) ;
402+ }
403+ return `${ QuoteUtils . quoteIdentifier ( target . schema ?? schemaName ) } .${ nameToken } ${ args } ` ;
404+ }
405+
307406/**
308407 * Create a SQL AST visitor that transforms schema names.
309408 *
@@ -569,10 +668,26 @@ export function createSqlVisitor(
569668 A_Const : ( path : any ) => {
570669 const node = path . node ;
571670 if ( typeof node . sval ?. sval === 'string' && node . sval . sval . includes ( '.' ) ) {
671+ if ( ! claimSite ( result , node . sval , 'sval' ) ) return ;
572672 node . sval . sval = transformSchemaRefsInString ( node . sval . sval , schemaMapping , result ) ;
573673 }
574674 } ,
575675
676+ // Object identities carried as the operand of an identity cast
677+ // ('sch.tbl'::regclass, 'sch.fn(uuid)'::regprocedure, 'sch'::regnamespace).
678+ // The cast names the namespace, so these route through the same object
679+ // routes as a RangeVar rather than through a string pattern — and the bare
680+ // schema form, invisible to every string pass, routes here.
681+ TypeCast : ( path : any ) => {
682+ const node = path . node ;
683+ const sval = node ?. arg ?. A_Const ?. sval ;
684+ if ( typeof sval ?. sval !== 'string' ) return ;
685+ const ns = identityCastNamespace ( node . typeName ) ;
686+ if ( ! ns ) return ;
687+ if ( ! claimSite ( result , sval , 'sval' ) ) return ;
688+ sval . sval = transformIdentityCastLiteral ( sval . sval , ns , router , result ) ;
689+ } ,
690+
576691 // Transform DefineStmt (CREATE TYPE schema.name, CREATE AGGREGATE schema.agg)
577692 DefineStmt : ( path : any ) => {
578693 const node = path . node ;
0 commit comments