@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/transform.js CHANGED
@@ -49,6 +49,15 @@ const traverse_1 = require("@pgsql/traverse");
49
49
  const plpgsql_parser_1 = require("plpgsql-parser");
50
50
  const qualify_1 = require("./qualify");
51
51
  const round_trip_1 = require("./round-trip");
52
+ const router_1 = require("./router");
53
+ /** Coerce any accepted mapping form into a {@link SchemaRouter}. */
54
+ function asRouter(mapping) {
55
+ return router_1.SchemaRouter.from(mapping);
56
+ }
57
+ /** Flat schema-level view for string-level passes that operate per source schema. */
58
+ function schemaLevelMap(mapping) {
59
+ return mapping instanceof router_1.SchemaRouter ? mapping.schemaLevelMap() : mapping;
60
+ }
52
61
  /**
53
62
  * Create a fresh result object
54
63
  */
@@ -78,21 +87,26 @@ function shouldTransformSchema(schemaName, schemaMapping) {
78
87
  /**
79
88
  * Transform schema names in a String node array (used for funcname, names, etc.)
80
89
  * These arrays contain String nodes like { String: { sval: 'schema_name' } }
90
+ *
91
+ * `ns` names the namespace of the referenced object so object-level routes can
92
+ * apply; the object's own name is the last element of the list. When `ns` is
93
+ * `unknown` (the default) only the schema-level default applies — identical to
94
+ * the historic whole-schema behaviour.
81
95
  */
82
- function transformNameList(names, schemaMapping, result) {
96
+ function transformNameList(names, schemaMapping, result, ns = 'unknown') {
83
97
  if (!names || names.length < 2)
84
98
  return;
85
- // For schema-qualified names, the first element is the schema
99
+ const router = asRouter(schemaMapping);
100
+ // For schema-qualified names, the first element is the schema.
86
101
  const first = names[0];
87
102
  if (first?.String?.sval) {
88
103
  const schemaName = first.String.sval;
89
- if (shouldTransformSchema(schemaName, schemaMapping)) {
104
+ const objName = names[names.length - 1]?.String?.sval;
105
+ const newName = router.resolve(schemaName, objName, ns);
106
+ if (newName && newName !== schemaName) {
90
107
  result.schemasFound.add(schemaName);
91
- const newName = schemaMapping.get(schemaName);
92
- if (newName) {
93
- first.String.sval = newName;
94
- result.schemasTransformed.set(schemaName, newName);
95
- }
108
+ first.String.sval = newName;
109
+ result.schemasTransformed.set(schemaName, newName);
96
110
  }
97
111
  }
98
112
  }
@@ -105,13 +119,13 @@ function transformSchemaNameField(container, field, schemaMapping, result) {
105
119
  const schemaName = container?.[field];
106
120
  if (typeof schemaName !== 'string')
107
121
  return;
108
- if (shouldTransformSchema(schemaName, schemaMapping)) {
122
+ // A bare schema name is an operation on the schema itself: only the
123
+ // schema-level default applies.
124
+ const newName = asRouter(schemaMapping).resolve(schemaName, undefined, 'schema');
125
+ if (newName && newName !== schemaName) {
109
126
  result.schemasFound.add(schemaName);
110
- const newName = schemaMapping.get(schemaName);
111
- if (newName) {
112
- container[field] = newName;
113
- result.schemasTransformed.set(schemaName, newName);
114
- }
127
+ container[field] = newName;
128
+ result.schemasTransformed.set(schemaName, newName);
115
129
  }
116
130
  }
117
131
  /**
@@ -119,14 +133,16 @@ function transformSchemaNameField(container, field, schemaMapping, result) {
119
133
  * Many statement nodes embed a relation that the walker does NOT auto-recurse into.
120
134
  */
121
135
  function transformRelation(relation, schemaMapping, result) {
122
- if (relation?.schemaname && shouldTransformSchema(relation.schemaname, schemaMapping)) {
123
- const oldName = relation.schemaname;
136
+ if (!relation?.schemaname)
137
+ return;
138
+ const oldName = relation.schemaname;
139
+ // A RangeVar names a relation (table/view/sequence/matview); route by the
140
+ // 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) {
124
143
  result.schemasFound.add(oldName);
125
- const newName = schemaMapping.get(oldName);
126
- if (newName) {
127
- relation.schemaname = newName;
128
- result.schemasTransformed.set(oldName, newName);
129
- }
144
+ relation.schemaname = newName;
145
+ result.schemasTransformed.set(oldName, newName);
130
146
  }
131
147
  }
132
148
  /**
@@ -137,7 +153,9 @@ function transformRelation(relation, schemaMapping, result) {
137
153
  */
138
154
  function transformSchemaRefsInString(str, schemaMapping, result) {
139
155
  let out = str;
140
- for (const [oldSchema, newSchema] of schemaMapping) {
156
+ // References embedded in opaque strings carry no object identity, so only
157
+ // whole-schema (schema-level default) routes can be applied here.
158
+ for (const [oldSchema, newSchema] of schemaLevelMap(schemaMapping)) {
141
159
  const pattern = new RegExp(`(?<![\\w-])("?)${escapeRegexp(oldSchema)}\\1(?=\\.)`, 'g');
142
160
  const before = out;
143
161
  out = out.replace(pattern, `$1${newSchema}$1`);
@@ -157,30 +175,59 @@ function transformSchemaRefsInString(str, schemaMapping, result) {
157
175
  * that require explicit handlers.
158
176
  */
159
177
  function createSqlVisitor(schemaMapping, result, visitorOptions) {
178
+ const router = asRouter(schemaMapping);
160
179
  const assumeSchemasExist = visitorOptions?.assumeSchemasExist;
180
+ const useAstBodies = router.hasObjectRoutes();
181
+ // Rewrite a bare schema-name string field in place (schema-level default).
182
+ const rewriteSchemaField = (container, key) => {
183
+ const schemaName = container?.[key];
184
+ if (typeof schemaName !== 'string')
185
+ return;
186
+ const newName = router.resolve(schemaName, undefined, 'schema');
187
+ if (newName && newName !== schemaName) {
188
+ result.schemasFound.add(schemaName);
189
+ container[key] = newName;
190
+ result.schemasTransformed.set(schemaName, newName);
191
+ }
192
+ };
193
+ // Namespace of the object a DROP/ALTER targets, from its removeType, so
194
+ // object routes apply to DROP TABLE/FUNCTION/TYPE (e.g. revert scripts).
195
+ const namespaceOfObjectType = (objType) => {
196
+ switch (objType) {
197
+ case 'OBJECT_TABLE':
198
+ case 'OBJECT_VIEW':
199
+ case 'OBJECT_SEQUENCE':
200
+ case 'OBJECT_MATVIEW':
201
+ case 'OBJECT_FOREIGN_TABLE':
202
+ case 'OBJECT_INDEX':
203
+ return 'relation';
204
+ case 'OBJECT_FUNCTION':
205
+ case 'OBJECT_PROCEDURE':
206
+ case 'OBJECT_AGGREGATE':
207
+ case 'OBJECT_ROUTINE':
208
+ return 'function';
209
+ case 'OBJECT_TYPE':
210
+ case 'OBJECT_DOMAIN':
211
+ return 'type';
212
+ default:
213
+ return 'unknown';
214
+ }
215
+ };
161
216
  return {
162
217
  // Transform RangeVar nodes (table references)
163
218
  RangeVar: (path) => {
164
- const node = path.node;
165
- if (node.schemaname && shouldTransformSchema(node.schemaname, schemaMapping)) {
166
- const oldName = node.schemaname;
167
- result.schemasFound.add(oldName);
168
- const newName = schemaMapping.get(oldName);
169
- if (newName) {
170
- node.schemaname = newName;
171
- result.schemasTransformed.set(oldName, newName);
172
- }
173
- }
219
+ transformRelation(path.node, router, result);
174
220
  },
175
221
  // Transform CreateSchemaStmt nodes
176
222
  CreateSchemaStmt: (path) => {
177
223
  const node = path.node;
178
- if (node.schemaname && shouldTransformSchema(node.schemaname, schemaMapping)) {
224
+ if (node.schemaname) {
179
225
  const oldName = node.schemaname;
180
- result.schemasFound.add(oldName);
181
- const newName = schemaMapping.get(oldName);
182
- if (newName) {
226
+ // The schema object itself: schema-level default only.
227
+ const newName = router.resolve(oldName, undefined, 'schema');
228
+ if (newName && newName !== oldName) {
183
229
  node.schemaname = newName;
230
+ result.schemasFound.add(oldName);
184
231
  result.schemasTransformed.set(oldName, newName);
185
232
  }
186
233
  }
@@ -191,7 +238,7 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
191
238
  // Transform FuncCall nodes (function calls with schema-qualified names)
192
239
  FuncCall: (path) => {
193
240
  const node = path.node;
194
- transformNameList(node.funcname, schemaMapping, result);
241
+ transformNameList(node.funcname, router, result, 'function');
195
242
  },
196
243
  // Transform CallStmt (CALL schema.procedure(...)).
197
244
  // Note: The walker does NOT auto-recurse into CallStmt.funccall,
@@ -199,18 +246,21 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
199
246
  CallStmt: (path) => {
200
247
  const node = path.node;
201
248
  if (node.funccall?.funcname) {
202
- transformNameList(node.funccall.funcname, schemaMapping, result);
249
+ transformNameList(node.funccall.funcname, router, result, 'function');
203
250
  }
204
251
  },
205
252
  // Transform TypeName nodes (type references with schema-qualified names)
206
253
  TypeName: (path) => {
207
254
  const node = path.node;
208
- transformNameList(node.names, schemaMapping, result);
255
+ transformNameList(node.names, router, result, 'type');
209
256
  },
210
- // Transform ColumnRef nodes (column references with schema-qualified names)
257
+ // Transform ColumnRef nodes (column references with schema-qualified names).
258
+ // A qualified column is schema.table.column — the routed object is the
259
+ // table, which we cannot disambiguate from the schema here, so only the
260
+ // schema-level default applies.
211
261
  ColumnRef: (path) => {
212
262
  const node = path.node;
213
- transformNameList(node.fields, schemaMapping, result);
263
+ transformNameList(node.fields, router, result);
214
264
  },
215
265
  // Transform GrantStmt objects (GRANT ON SCHEMA schema;
216
266
  // GRANT ... ON ALL TABLES/FUNCTIONS/SEQUENCES IN SCHEMA schema)
@@ -232,26 +282,10 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
232
282
  for (const arg of node.args) {
233
283
  // search_path args can be String nodes or A_Const with sval
234
284
  if (arg?.String?.sval) {
235
- const schemaName = arg.String.sval;
236
- if (shouldTransformSchema(schemaName, schemaMapping)) {
237
- result.schemasFound.add(schemaName);
238
- const newName = schemaMapping.get(schemaName);
239
- if (newName) {
240
- arg.String.sval = newName;
241
- result.schemasTransformed.set(schemaName, newName);
242
- }
243
- }
285
+ rewriteSchemaField(arg.String, 'sval');
244
286
  }
245
287
  else if (arg?.A_Const?.sval?.sval) {
246
- const schemaName = arg.A_Const.sval.sval;
247
- if (shouldTransformSchema(schemaName, schemaMapping)) {
248
- result.schemasFound.add(schemaName);
249
- const newName = schemaMapping.get(schemaName);
250
- if (newName) {
251
- arg.A_Const.sval.sval = newName;
252
- result.schemasTransformed.set(schemaName, newName);
253
- }
254
- }
288
+ rewriteSchemaField(arg.A_Const.sval, 'sval');
255
289
  }
256
290
  }
257
291
  }
@@ -264,15 +298,7 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
264
298
  if (opt?.DefElem?.defname === 'schemas' && opt.DefElem.arg?.List?.items) {
265
299
  for (const item of opt.DefElem.arg.List.items) {
266
300
  if (item?.String?.sval) {
267
- const schemaName = item.String.sval;
268
- if (shouldTransformSchema(schemaName, schemaMapping)) {
269
- result.schemasFound.add(schemaName);
270
- const newName = schemaMapping.get(schemaName);
271
- if (newName) {
272
- item.String.sval = newName;
273
- result.schemasTransformed.set(schemaName, newName);
274
- }
275
- }
301
+ rewriteSchemaField(item.String, 'sval');
276
302
  }
277
303
  }
278
304
  }
@@ -285,15 +311,16 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
285
311
  DropStmt: (path) => {
286
312
  const node = path.node;
287
313
  if (node.removeType !== 'OBJECT_SCHEMA' && Array.isArray(node.objects)) {
314
+ const ns = namespaceOfObjectType(node.removeType);
288
315
  for (const obj of node.objects) {
289
316
  if (obj?.List?.items) {
290
- transformNameList(obj.List.items, schemaMapping, result);
317
+ transformNameList(obj.List.items, router, result, ns);
291
318
  }
292
319
  else if (obj?.ObjectWithArgs?.objname) {
293
- transformNameList(obj.ObjectWithArgs.objname, schemaMapping, result);
320
+ transformNameList(obj.ObjectWithArgs.objname, router, result, ns);
294
321
  }
295
322
  else if (obj?.TypeName?.names) {
296
- transformNameList(obj.TypeName.names, schemaMapping, result);
323
+ transformNameList(obj.TypeName.names, router, result, ns);
297
324
  }
298
325
  }
299
326
  return;
@@ -304,28 +331,12 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
304
331
  if (obj?.List?.items) {
305
332
  for (const item of obj.List.items) {
306
333
  if (item?.String?.sval) {
307
- const schemaName = item.String.sval;
308
- if (shouldTransformSchema(schemaName, schemaMapping)) {
309
- result.schemasFound.add(schemaName);
310
- const newName = schemaMapping.get(schemaName);
311
- if (newName) {
312
- item.String.sval = newName;
313
- result.schemasTransformed.set(schemaName, newName);
314
- }
315
- }
334
+ rewriteSchemaField(item.String, 'sval');
316
335
  }
317
336
  }
318
337
  }
319
338
  else if (obj?.String?.sval) {
320
- const schemaName = obj.String.sval;
321
- if (shouldTransformSchema(schemaName, schemaMapping)) {
322
- result.schemasFound.add(schemaName);
323
- const newName = schemaMapping.get(schemaName);
324
- if (newName) {
325
- obj.String.sval = newName;
326
- result.schemasTransformed.set(schemaName, newName);
327
- }
328
- }
339
+ rewriteSchemaField(obj.String, 'sval');
329
340
  }
330
341
  }
331
342
  }
@@ -378,7 +389,7 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
378
389
  ColumnDef: (path) => {
379
390
  const node = path.node;
380
391
  if (node.typeName?.names) {
381
- transformNameList(node.typeName.names, schemaMapping, result);
392
+ transformNameList(node.typeName.names, schemaMapping, result, 'type');
382
393
  }
383
394
  },
384
395
  // Transform ViewStmt (CREATE VIEW schema.viewname AS ...)
@@ -451,24 +462,24 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
451
462
  TypeCast: (path) => {
452
463
  const node = path.node;
453
464
  if (node.typeName?.names) {
454
- transformNameList(node.typeName.names, schemaMapping, result);
465
+ transformNameList(node.typeName.names, schemaMapping, result, 'type');
455
466
  }
456
467
  },
457
468
  // Transform CreateFunctionStmt (CREATE FUNCTION schema.funcname)
458
469
  // Also handles RETURNS [SETOF] schema.type via returnType.names
459
470
  CreateFunctionStmt: (path) => {
460
471
  const node = path.node;
461
- transformNameList(node.funcname, schemaMapping, result);
472
+ transformNameList(node.funcname, schemaMapping, result, 'function');
462
473
  // Transform the return type (e.g., RETURNS SETOF schema.tablename)
463
474
  if (node.returnType?.names) {
464
- transformNameList(node.returnType.names, schemaMapping, result);
475
+ transformNameList(node.returnType.names, schemaMapping, result, 'type');
465
476
  }
466
477
  // Transform parameter types (the walker does NOT auto-recurse into
467
478
  // FunctionParameter.argType TypeName nodes)
468
479
  if (Array.isArray(node.parameters)) {
469
480
  for (const param of node.parameters) {
470
481
  if (param?.FunctionParameter?.argType?.names) {
471
- transformNameList(param.FunctionParameter.argType.names, schemaMapping, result);
482
+ transformNameList(param.FunctionParameter.argType.names, schemaMapping, result, 'type');
472
483
  }
473
484
  }
474
485
  }
@@ -481,8 +492,16 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
481
492
  for (const opt of node.options) {
482
493
  if (opt?.DefElem?.defname === 'as' && opt.DefElem.arg?.List?.items) {
483
494
  for (const item of opt.DefElem.arg.List.items) {
484
- if (typeof item?.String?.sval === 'string' && item.String.sval.includes('.')) {
485
- item.String.sval = transformSchemaRefsInString(item.String.sval, schemaMapping, result);
495
+ if (typeof item?.String?.sval !== 'string')
496
+ continue;
497
+ // With object routes, references inside a LANGUAGE sql body must
498
+ // be rewritten AST-precisely; whole-schema routes keep the
499
+ // cheaper (and quoting-preserving) string pass.
500
+ if (useAstBodies) {
501
+ item.String.sval = transformSqlBodyString(item.String.sval, router, result);
502
+ }
503
+ else if (item.String.sval.includes('.')) {
504
+ item.String.sval = transformSchemaRefsInString(item.String.sval, router, result);
486
505
  }
487
506
  }
488
507
  }
@@ -494,7 +513,7 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
494
513
  // so we must explicitly transform the RangeVar here.
495
514
  CreateTrigStmt: (path) => {
496
515
  const node = path.node;
497
- transformNameList(node.funcname, schemaMapping, result);
516
+ transformNameList(node.funcname, schemaMapping, result, 'function');
498
517
  transformRelation(node.relation, schemaMapping, result);
499
518
  },
500
519
  // Transform CommentStmt (COMMENT ON TABLE/COLUMN/FUNCTION schema.obj,
@@ -540,29 +559,29 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
540
559
  // Transform CreateDomainStmt (CREATE DOMAIN schema.domname)
541
560
  CreateDomainStmt: (path) => {
542
561
  const node = path.node;
543
- transformNameList(node.domainname, schemaMapping, result);
562
+ transformNameList(node.domainname, schemaMapping, result, 'type');
544
563
  },
545
564
  // Transform CreateEnumStmt (CREATE TYPE schema.enumname AS ENUM)
546
565
  CreateEnumStmt: (path) => {
547
566
  const node = path.node;
548
- transformNameList(node.typeName, schemaMapping, result);
567
+ transformNameList(node.typeName, schemaMapping, result, 'type');
549
568
  },
550
569
  // Transform AlterEnumStmt (ALTER TYPE schema.enumname ADD VALUE)
551
570
  AlterEnumStmt: (path) => {
552
571
  const node = path.node;
553
- transformNameList(node.typeName, schemaMapping, result);
572
+ transformNameList(node.typeName, schemaMapping, result, 'type');
554
573
  },
555
574
  // Transform AlterDomainStmt (ALTER DOMAIN schema.domname)
556
575
  AlterDomainStmt: (path) => {
557
576
  const node = path.node;
558
- transformNameList(node.typeName, schemaMapping, result);
577
+ transformNameList(node.typeName, schemaMapping, result, 'type');
559
578
  },
560
579
  // Transform AlterTypeStmt (ALTER TYPE schema.typename)
561
580
  // Note: composite type alters use RangeVar (handled by RangeVar visitor),
562
581
  // but some ALTER TYPE statements use typeName as a name list
563
582
  AlterTypeStmt: (path) => {
564
583
  const node = path.node;
565
- transformNameList(node.typeName, schemaMapping, result);
584
+ transformNameList(node.typeName, schemaMapping, result, 'type');
566
585
  },
567
586
  // Transform ObjectWithArgs (used in ALTER FUNCTION, DROP FUNCTION with args, etc.)
568
587
  ObjectWithArgs: (path) => {
@@ -610,7 +629,7 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
610
629
  AlterFunctionStmt: (path) => {
611
630
  const node = path.node;
612
631
  if (node.func?.objname) {
613
- transformNameList(node.func.objname, schemaMapping, result);
632
+ transformNameList(node.func.objname, schemaMapping, result, 'function');
614
633
  }
615
634
  },
616
635
  // Transform AlterOwnerStmt (ALTER ... OWNER TO role)
@@ -631,28 +650,28 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
631
650
  CreateCastStmt: (path) => {
632
651
  const node = path.node;
633
652
  if (node.sourcetype?.names) {
634
- transformNameList(node.sourcetype.names, schemaMapping, result);
653
+ transformNameList(node.sourcetype.names, schemaMapping, result, 'type');
635
654
  }
636
655
  if (node.targettype?.names) {
637
- transformNameList(node.targettype.names, schemaMapping, result);
656
+ transformNameList(node.targettype.names, schemaMapping, result, 'type');
638
657
  }
639
658
  if (node.func?.objname) {
640
- transformNameList(node.func.objname, schemaMapping, result);
659
+ transformNameList(node.func.objname, schemaMapping, result, 'function');
641
660
  }
642
661
  if (Array.isArray(node.func?.objargs)) {
643
662
  for (const arg of node.func.objargs) {
644
663
  if (arg?.TypeName?.names) {
645
- transformNameList(arg.TypeName.names, schemaMapping, result);
664
+ transformNameList(arg.TypeName.names, schemaMapping, result, 'type');
646
665
  }
647
666
  else if (arg?.names) {
648
- transformNameList(arg.names, schemaMapping, result);
667
+ transformNameList(arg.names, schemaMapping, result, 'type');
649
668
  }
650
669
  }
651
670
  }
652
671
  if (Array.isArray(node.func?.objfuncargs)) {
653
672
  for (const param of node.func.objfuncargs) {
654
673
  if (param?.FunctionParameter?.argType?.names) {
655
- transformNameList(param.FunctionParameter.argType.names, schemaMapping, result);
674
+ transformNameList(param.FunctionParameter.argType.names, schemaMapping, result, 'type');
656
675
  }
657
676
  }
658
677
  }
@@ -660,7 +679,7 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
660
679
  // Transform CreateEventTrigStmt (CREATE EVENT TRIGGER ... EXECUTE FUNCTION schema.fn())
661
680
  CreateEventTrigStmt: (path) => {
662
681
  const node = path.node;
663
- transformNameList(node.funcname, schemaMapping, result);
682
+ transformNameList(node.funcname, schemaMapping, result, 'function');
664
683
  },
665
684
  // Transform IndexElem opclass (CREATE INDEX ... (col schema.opclass))
666
685
  IndexElem: (path) => {
@@ -701,10 +720,14 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
701
720
  * indicating that the AST visitor is missing a handler for that node type.
702
721
  */
703
722
  function validateNoUntransformedSchemas(content, schemaMapping) {
704
- if (schemaMapping.size === 0) {
723
+ // Only schemas with a schema-level default are guaranteed to move entirely;
724
+ // partially (object-only) routed schemas may legitimately keep some
725
+ // references, so they are excluded from the strict leftover check.
726
+ const moved = schemaMapping instanceof router_1.SchemaRouter ? schemaMapping.fullyMovedSchemas() : schemaMapping;
727
+ if (moved.size === 0) {
705
728
  return;
706
729
  }
707
- for (const [oldSchema, newSchema] of schemaMapping) {
730
+ for (const [oldSchema, newSchema] of moved) {
708
731
  const escapedSchema = escapeRegexp(oldSchema);
709
732
  // Pattern 1: quoted or unquoted schema name followed by dot (schema-qualified)
710
733
  const dotPattern = new RegExp(`(?:"${escapedSchema}"|\\b${escapedSchema})(?=\\.)`, 'g');
@@ -739,11 +762,12 @@ function validateNoUntransformedSchemas(content, schemaMapping) {
739
762
  * Create a PL/pgSQL visitor that transforms schema names in PL/pgSQL-specific nodes.
740
763
  */
741
764
  function createPlpgsqlVisitor(schemaMapping, result) {
765
+ const schemaMap = schemaLevelMap(schemaMapping);
742
766
  return {
743
767
  PLpgSQL_type: (path) => {
744
768
  const node = path.node;
745
769
  if (node.typname) {
746
- for (const [oldSchema, newSchema] of schemaMapping.entries()) {
770
+ for (const [oldSchema, newSchema] of schemaMap.entries()) {
747
771
  if (node.typname.startsWith(oldSchema + '.')) {
748
772
  const typeName = node.typname.substring(oldSchema.length + 1);
749
773
  result.schemasFound.add(oldSchema);
@@ -757,7 +781,7 @@ function createPlpgsqlVisitor(schemaMapping, result) {
757
781
  PLpgSQL_var: (path) => {
758
782
  const node = path.node;
759
783
  if (node.refname) {
760
- for (const [oldSchema, newSchema] of schemaMapping.entries()) {
784
+ for (const [oldSchema, newSchema] of schemaMap.entries()) {
761
785
  if (node.refname.startsWith(oldSchema + '.')) {
762
786
  const rest = node.refname.substring(oldSchema.length + 1);
763
787
  result.schemasFound.add(oldSchema);
@@ -774,6 +798,7 @@ function createPlpgsqlVisitor(schemaMapping, result) {
774
798
  * Transform a PLpgSQL_type typname using proper AST parsing.
775
799
  */
776
800
  function transformPlpgsqlTypeAst(typname, schemaMapping, result) {
801
+ const schemaMap = schemaLevelMap(schemaMapping);
777
802
  let suffix = '';
778
803
  let baseTypname = typname;
779
804
  const rowtypeMatch = typname.match(/(%rowtype|%type)$/i);
@@ -782,7 +807,7 @@ function transformPlpgsqlTypeAst(typname, schemaMapping, result) {
782
807
  baseTypname = typname.substring(0, typname.length - suffix.length);
783
808
  }
784
809
  let needsTransform = false;
785
- for (const oldSchema of schemaMapping.keys()) {
810
+ for (const oldSchema of schemaMap.keys()) {
786
811
  if (baseTypname.startsWith(oldSchema + '.') || baseTypname.startsWith('"' + oldSchema + '".')) {
787
812
  needsTransform = true;
788
813
  break;
@@ -801,7 +826,7 @@ function transformPlpgsqlTypeAst(typname, schemaMapping, result) {
801
826
  TypeName: (path) => {
802
827
  const typeNode = path.node;
803
828
  if (typeNode.names && Array.isArray(typeNode.names)) {
804
- transformNameList(typeNode.names, schemaMapping, result);
829
+ transformNameList(typeNode.names, schemaMapping, result, 'type');
805
830
  }
806
831
  }
807
832
  };
@@ -823,7 +848,7 @@ function transformPlpgsqlTypeAst(typname, schemaMapping, result) {
823
848
  * Uses @pgsql/quotes QuoteUtils for proper identifier quoting.
824
849
  */
825
850
  function transformPlpgsqlTypeString(typname, schemaMapping, result) {
826
- for (const [oldSchema, newSchema] of schemaMapping.entries()) {
851
+ for (const [oldSchema, newSchema] of schemaLevelMap(schemaMapping).entries()) {
827
852
  // Unquoted schema: old_schema.rest
828
853
  if (typname.startsWith(oldSchema + '.')) {
829
854
  const rest = typname.substring(oldSchema.length + 1);
@@ -894,7 +919,7 @@ function walkPlpgsqlForSchemas(node, schemaMapping, result) {
894
919
  // We cannot use walkSql here because the raw typeNameNode is not
895
920
  // wrapped in the expected AST envelope that the traverse walker needs.
896
921
  if (plType.typname.typeNameNode?.names) {
897
- transformNameList(plType.typname.typeNameNode.names, schemaMapping, result);
922
+ transformNameList(plType.typname.typeNameNode.names, schemaMapping, result, 'type');
898
923
  }
899
924
  // The deparser can fall back to the 'original' string to render
900
925
  // DECLARE types, so update it as well. Rewrite only the schema
@@ -913,6 +938,35 @@ function walkPlpgsqlForSchemas(node, schemaMapping, result) {
913
938
  walkPlpgsqlForSchemas(value, schemaMapping, result);
914
939
  }
915
940
  }
941
+ /**
942
+ * Rewrite a `LANGUAGE sql` function-body string using the full AST visitor so
943
+ * object-level routes reach references inside the body (the body is an opaque
944
+ * String node the outer walker never parses). Parses each statement, walks it
945
+ * with the router-aware SQL visitor, and deparses. Falls back to the
946
+ * schema-level string pass for anything that does not parse standalone (e.g.
947
+ * PL/pgSQL blocks or C symbol names).
948
+ */
949
+ function transformSqlBodyString(body, router, result) {
950
+ try {
951
+ const parseResult = (0, plpgsql_parser_1.parseSql)(body);
952
+ const stmts = parseResult?.stmts ?? [];
953
+ if (stmts.length === 0) {
954
+ return body.includes('.') ? transformSchemaRefsInString(body, router, result) : body;
955
+ }
956
+ const visitor = createSqlVisitor(router, result);
957
+ const pieces = [];
958
+ for (const stmt of stmts) {
959
+ if (!stmt?.stmt)
960
+ continue;
961
+ (0, traverse_1.walk)(stmt.stmt, visitor);
962
+ pieces.push(plpgsql_parser_1.Deparser.deparse(stmt.stmt));
963
+ }
964
+ return pieces.join(';\n');
965
+ }
966
+ catch {
967
+ return body.includes('.') ? transformSchemaRefsInString(body, router, result) : body;
968
+ }
969
+ }
916
970
  /**
917
971
  * Escape a string for use in a regular expression
918
972
  */
@@ -1061,11 +1115,13 @@ function transformSql(content, schemaMapping, options, result) {
1061
1115
  if (schemaMapping.size === 0) {
1062
1116
  return { content, result: r };
1063
1117
  }
1118
+ // String-level passes operate on whole schemas; give them the flat view.
1119
+ const passMap = schemaLevelMap(schemaMapping);
1064
1120
  let newContent = content;
1065
1121
  // Run pre-passes (app-specific string-level transforms)
1066
1122
  if (opts.prePasses) {
1067
1123
  for (const pass of opts.prePasses) {
1068
- newContent = pass(newContent, schemaMapping, r);
1124
+ newContent = pass(newContent, passMap, r);
1069
1125
  }
1070
1126
  }
1071
1127
  // Main pass: AST transformation with header handling
@@ -1073,7 +1129,7 @@ function transformSql(content, schemaMapping, options, result) {
1073
1129
  // Run post-passes (app-specific string-level transforms)
1074
1130
  if (opts.postPasses) {
1075
1131
  for (const pass of opts.postPasses) {
1076
- newContent = pass(newContent, schemaMapping, r);
1132
+ newContent = pass(newContent, passMap, r);
1077
1133
  }
1078
1134
  }
1079
1135
  return { content: newContent, result: r };
@@ -1130,7 +1186,8 @@ function transformSqlContentAst(content, schemaMapping, result, options) {
1130
1186
  const { header, body } = extractPgpmHeader(content);
1131
1187
  let transformedHeader = header;
1132
1188
  if (header.length > 0) {
1133
- transformedHeader = transformComments(header, schemaMapping, result);
1189
+ // Header/path rewrites (-- Deploy: schemas/<schema>/...) are whole-schema.
1190
+ transformedHeader = transformComments(header, schemaLevelMap(schemaMapping), result);
1134
1191
  }
1135
1192
  let transformedBody = body;
1136
1193
  if (options?.qualifyUnqualified && transformedBody.trim().length > 0) {