@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/transform.js CHANGED
@@ -28,6 +28,15 @@ import { walk as walkSql } from '@pgsql/traverse';
28
28
  import { Deparser, parseSql, transformSync, walk as walkPlpgsql } from 'plpgsql-parser';
29
29
  import { qualifyUnqualified } from './qualify';
30
30
  import { captureTransformAsts, validateRoundTrip } from './round-trip';
31
+ import { SchemaRouter } from './router';
32
+ /** Coerce any accepted mapping form into a {@link SchemaRouter}. */
33
+ function asRouter(mapping) {
34
+ return SchemaRouter.from(mapping);
35
+ }
36
+ /** Flat schema-level view for string-level passes that operate per source schema. */
37
+ function schemaLevelMap(mapping) {
38
+ return mapping instanceof SchemaRouter ? mapping.schemaLevelMap() : mapping;
39
+ }
31
40
  /**
32
41
  * Create a fresh result object
33
42
  */
@@ -57,21 +66,26 @@ export function shouldTransformSchema(schemaName, schemaMapping) {
57
66
  /**
58
67
  * Transform schema names in a String node array (used for funcname, names, etc.)
59
68
  * These arrays contain String nodes like { String: { sval: 'schema_name' } }
69
+ *
70
+ * `ns` names the namespace of the referenced object so object-level routes can
71
+ * apply; the object's own name is the last element of the list. When `ns` is
72
+ * `unknown` (the default) only the schema-level default applies — identical to
73
+ * the historic whole-schema behaviour.
60
74
  */
61
- export function transformNameList(names, schemaMapping, result) {
75
+ export function transformNameList(names, schemaMapping, result, ns = 'unknown') {
62
76
  if (!names || names.length < 2)
63
77
  return;
64
- // For schema-qualified names, the first element is the schema
78
+ const router = asRouter(schemaMapping);
79
+ // For schema-qualified names, the first element is the schema.
65
80
  const first = names[0];
66
81
  if (first?.String?.sval) {
67
82
  const schemaName = first.String.sval;
68
- if (shouldTransformSchema(schemaName, schemaMapping)) {
83
+ const objName = names[names.length - 1]?.String?.sval;
84
+ const newName = router.resolve(schemaName, objName, ns);
85
+ if (newName && newName !== schemaName) {
69
86
  result.schemasFound.add(schemaName);
70
- const newName = schemaMapping.get(schemaName);
71
- if (newName) {
72
- first.String.sval = newName;
73
- result.schemasTransformed.set(schemaName, newName);
74
- }
87
+ first.String.sval = newName;
88
+ result.schemasTransformed.set(schemaName, newName);
75
89
  }
76
90
  }
77
91
  }
@@ -84,13 +98,13 @@ export function transformSchemaNameField(container, field, schemaMapping, result
84
98
  const schemaName = container?.[field];
85
99
  if (typeof schemaName !== 'string')
86
100
  return;
87
- if (shouldTransformSchema(schemaName, schemaMapping)) {
101
+ // A bare schema name is an operation on the schema itself: only the
102
+ // schema-level default applies.
103
+ const newName = asRouter(schemaMapping).resolve(schemaName, undefined, 'schema');
104
+ if (newName && newName !== schemaName) {
88
105
  result.schemasFound.add(schemaName);
89
- const newName = schemaMapping.get(schemaName);
90
- if (newName) {
91
- container[field] = newName;
92
- result.schemasTransformed.set(schemaName, newName);
93
- }
106
+ container[field] = newName;
107
+ result.schemasTransformed.set(schemaName, newName);
94
108
  }
95
109
  }
96
110
  /**
@@ -98,14 +112,16 @@ export function transformSchemaNameField(container, field, schemaMapping, result
98
112
  * Many statement nodes embed a relation that the walker does NOT auto-recurse into.
99
113
  */
100
114
  export function transformRelation(relation, schemaMapping, result) {
101
- if (relation?.schemaname && shouldTransformSchema(relation.schemaname, schemaMapping)) {
102
- const oldName = relation.schemaname;
115
+ if (!relation?.schemaname)
116
+ return;
117
+ const oldName = relation.schemaname;
118
+ // A RangeVar names a relation (table/view/sequence/matview); route by the
119
+ // 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) {
103
122
  result.schemasFound.add(oldName);
104
- const newName = schemaMapping.get(oldName);
105
- if (newName) {
106
- relation.schemaname = newName;
107
- result.schemasTransformed.set(oldName, newName);
108
- }
123
+ relation.schemaname = newName;
124
+ result.schemasTransformed.set(oldName, newName);
109
125
  }
110
126
  }
111
127
  /**
@@ -116,7 +132,9 @@ export function transformRelation(relation, schemaMapping, result) {
116
132
  */
117
133
  export function transformSchemaRefsInString(str, schemaMapping, result) {
118
134
  let out = str;
119
- for (const [oldSchema, newSchema] of schemaMapping) {
135
+ // References embedded in opaque strings carry no object identity, so only
136
+ // whole-schema (schema-level default) routes can be applied here.
137
+ for (const [oldSchema, newSchema] of schemaLevelMap(schemaMapping)) {
120
138
  const pattern = new RegExp(`(?<![\\w-])("?)${escapeRegexp(oldSchema)}\\1(?=\\.)`, 'g');
121
139
  const before = out;
122
140
  out = out.replace(pattern, `$1${newSchema}$1`);
@@ -136,30 +154,59 @@ export function transformSchemaRefsInString(str, schemaMapping, result) {
136
154
  * that require explicit handlers.
137
155
  */
138
156
  export function createSqlVisitor(schemaMapping, result, visitorOptions) {
157
+ const router = asRouter(schemaMapping);
139
158
  const assumeSchemasExist = visitorOptions?.assumeSchemasExist;
159
+ const useAstBodies = router.hasObjectRoutes();
160
+ // Rewrite a bare schema-name string field in place (schema-level default).
161
+ const rewriteSchemaField = (container, key) => {
162
+ const schemaName = container?.[key];
163
+ if (typeof schemaName !== 'string')
164
+ return;
165
+ const newName = router.resolve(schemaName, undefined, 'schema');
166
+ if (newName && newName !== schemaName) {
167
+ result.schemasFound.add(schemaName);
168
+ container[key] = newName;
169
+ result.schemasTransformed.set(schemaName, newName);
170
+ }
171
+ };
172
+ // Namespace of the object a DROP/ALTER targets, from its removeType, so
173
+ // object routes apply to DROP TABLE/FUNCTION/TYPE (e.g. revert scripts).
174
+ const namespaceOfObjectType = (objType) => {
175
+ switch (objType) {
176
+ case 'OBJECT_TABLE':
177
+ case 'OBJECT_VIEW':
178
+ case 'OBJECT_SEQUENCE':
179
+ case 'OBJECT_MATVIEW':
180
+ case 'OBJECT_FOREIGN_TABLE':
181
+ case 'OBJECT_INDEX':
182
+ return 'relation';
183
+ case 'OBJECT_FUNCTION':
184
+ case 'OBJECT_PROCEDURE':
185
+ case 'OBJECT_AGGREGATE':
186
+ case 'OBJECT_ROUTINE':
187
+ return 'function';
188
+ case 'OBJECT_TYPE':
189
+ case 'OBJECT_DOMAIN':
190
+ return 'type';
191
+ default:
192
+ return 'unknown';
193
+ }
194
+ };
140
195
  return {
141
196
  // Transform RangeVar nodes (table references)
142
197
  RangeVar: (path) => {
143
- const node = path.node;
144
- if (node.schemaname && shouldTransformSchema(node.schemaname, schemaMapping)) {
145
- const oldName = node.schemaname;
146
- result.schemasFound.add(oldName);
147
- const newName = schemaMapping.get(oldName);
148
- if (newName) {
149
- node.schemaname = newName;
150
- result.schemasTransformed.set(oldName, newName);
151
- }
152
- }
198
+ transformRelation(path.node, router, result);
153
199
  },
154
200
  // Transform CreateSchemaStmt nodes
155
201
  CreateSchemaStmt: (path) => {
156
202
  const node = path.node;
157
- if (node.schemaname && shouldTransformSchema(node.schemaname, schemaMapping)) {
203
+ if (node.schemaname) {
158
204
  const oldName = node.schemaname;
159
- result.schemasFound.add(oldName);
160
- const newName = schemaMapping.get(oldName);
161
- if (newName) {
205
+ // The schema object itself: schema-level default only.
206
+ const newName = router.resolve(oldName, undefined, 'schema');
207
+ if (newName && newName !== oldName) {
162
208
  node.schemaname = newName;
209
+ result.schemasFound.add(oldName);
163
210
  result.schemasTransformed.set(oldName, newName);
164
211
  }
165
212
  }
@@ -170,7 +217,7 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
170
217
  // Transform FuncCall nodes (function calls with schema-qualified names)
171
218
  FuncCall: (path) => {
172
219
  const node = path.node;
173
- transformNameList(node.funcname, schemaMapping, result);
220
+ transformNameList(node.funcname, router, result, 'function');
174
221
  },
175
222
  // Transform CallStmt (CALL schema.procedure(...)).
176
223
  // Note: The walker does NOT auto-recurse into CallStmt.funccall,
@@ -178,18 +225,21 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
178
225
  CallStmt: (path) => {
179
226
  const node = path.node;
180
227
  if (node.funccall?.funcname) {
181
- transformNameList(node.funccall.funcname, schemaMapping, result);
228
+ transformNameList(node.funccall.funcname, router, result, 'function');
182
229
  }
183
230
  },
184
231
  // Transform TypeName nodes (type references with schema-qualified names)
185
232
  TypeName: (path) => {
186
233
  const node = path.node;
187
- transformNameList(node.names, schemaMapping, result);
234
+ transformNameList(node.names, router, result, 'type');
188
235
  },
189
- // Transform ColumnRef nodes (column references with schema-qualified names)
236
+ // Transform ColumnRef nodes (column references with schema-qualified names).
237
+ // A qualified column is schema.table.column — the routed object is the
238
+ // table, which we cannot disambiguate from the schema here, so only the
239
+ // schema-level default applies.
190
240
  ColumnRef: (path) => {
191
241
  const node = path.node;
192
- transformNameList(node.fields, schemaMapping, result);
242
+ transformNameList(node.fields, router, result);
193
243
  },
194
244
  // Transform GrantStmt objects (GRANT ON SCHEMA schema;
195
245
  // GRANT ... ON ALL TABLES/FUNCTIONS/SEQUENCES IN SCHEMA schema)
@@ -211,26 +261,10 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
211
261
  for (const arg of node.args) {
212
262
  // search_path args can be String nodes or A_Const with sval
213
263
  if (arg?.String?.sval) {
214
- const schemaName = arg.String.sval;
215
- if (shouldTransformSchema(schemaName, schemaMapping)) {
216
- result.schemasFound.add(schemaName);
217
- const newName = schemaMapping.get(schemaName);
218
- if (newName) {
219
- arg.String.sval = newName;
220
- result.schemasTransformed.set(schemaName, newName);
221
- }
222
- }
264
+ rewriteSchemaField(arg.String, 'sval');
223
265
  }
224
266
  else if (arg?.A_Const?.sval?.sval) {
225
- const schemaName = arg.A_Const.sval.sval;
226
- if (shouldTransformSchema(schemaName, schemaMapping)) {
227
- result.schemasFound.add(schemaName);
228
- const newName = schemaMapping.get(schemaName);
229
- if (newName) {
230
- arg.A_Const.sval.sval = newName;
231
- result.schemasTransformed.set(schemaName, newName);
232
- }
233
- }
267
+ rewriteSchemaField(arg.A_Const.sval, 'sval');
234
268
  }
235
269
  }
236
270
  }
@@ -243,15 +277,7 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
243
277
  if (opt?.DefElem?.defname === 'schemas' && opt.DefElem.arg?.List?.items) {
244
278
  for (const item of opt.DefElem.arg.List.items) {
245
279
  if (item?.String?.sval) {
246
- const schemaName = item.String.sval;
247
- if (shouldTransformSchema(schemaName, schemaMapping)) {
248
- result.schemasFound.add(schemaName);
249
- const newName = schemaMapping.get(schemaName);
250
- if (newName) {
251
- item.String.sval = newName;
252
- result.schemasTransformed.set(schemaName, newName);
253
- }
254
- }
280
+ rewriteSchemaField(item.String, 'sval');
255
281
  }
256
282
  }
257
283
  }
@@ -264,15 +290,16 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
264
290
  DropStmt: (path) => {
265
291
  const node = path.node;
266
292
  if (node.removeType !== 'OBJECT_SCHEMA' && Array.isArray(node.objects)) {
293
+ const ns = namespaceOfObjectType(node.removeType);
267
294
  for (const obj of node.objects) {
268
295
  if (obj?.List?.items) {
269
- transformNameList(obj.List.items, schemaMapping, result);
296
+ transformNameList(obj.List.items, router, result, ns);
270
297
  }
271
298
  else if (obj?.ObjectWithArgs?.objname) {
272
- transformNameList(obj.ObjectWithArgs.objname, schemaMapping, result);
299
+ transformNameList(obj.ObjectWithArgs.objname, router, result, ns);
273
300
  }
274
301
  else if (obj?.TypeName?.names) {
275
- transformNameList(obj.TypeName.names, schemaMapping, result);
302
+ transformNameList(obj.TypeName.names, router, result, ns);
276
303
  }
277
304
  }
278
305
  return;
@@ -283,28 +310,12 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
283
310
  if (obj?.List?.items) {
284
311
  for (const item of obj.List.items) {
285
312
  if (item?.String?.sval) {
286
- const schemaName = item.String.sval;
287
- if (shouldTransformSchema(schemaName, schemaMapping)) {
288
- result.schemasFound.add(schemaName);
289
- const newName = schemaMapping.get(schemaName);
290
- if (newName) {
291
- item.String.sval = newName;
292
- result.schemasTransformed.set(schemaName, newName);
293
- }
294
- }
313
+ rewriteSchemaField(item.String, 'sval');
295
314
  }
296
315
  }
297
316
  }
298
317
  else if (obj?.String?.sval) {
299
- const schemaName = obj.String.sval;
300
- if (shouldTransformSchema(schemaName, schemaMapping)) {
301
- result.schemasFound.add(schemaName);
302
- const newName = schemaMapping.get(schemaName);
303
- if (newName) {
304
- obj.String.sval = newName;
305
- result.schemasTransformed.set(schemaName, newName);
306
- }
307
- }
318
+ rewriteSchemaField(obj.String, 'sval');
308
319
  }
309
320
  }
310
321
  }
@@ -357,7 +368,7 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
357
368
  ColumnDef: (path) => {
358
369
  const node = path.node;
359
370
  if (node.typeName?.names) {
360
- transformNameList(node.typeName.names, schemaMapping, result);
371
+ transformNameList(node.typeName.names, schemaMapping, result, 'type');
361
372
  }
362
373
  },
363
374
  // Transform ViewStmt (CREATE VIEW schema.viewname AS ...)
@@ -430,24 +441,24 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
430
441
  TypeCast: (path) => {
431
442
  const node = path.node;
432
443
  if (node.typeName?.names) {
433
- transformNameList(node.typeName.names, schemaMapping, result);
444
+ transformNameList(node.typeName.names, schemaMapping, result, 'type');
434
445
  }
435
446
  },
436
447
  // Transform CreateFunctionStmt (CREATE FUNCTION schema.funcname)
437
448
  // Also handles RETURNS [SETOF] schema.type via returnType.names
438
449
  CreateFunctionStmt: (path) => {
439
450
  const node = path.node;
440
- transformNameList(node.funcname, schemaMapping, result);
451
+ transformNameList(node.funcname, schemaMapping, result, 'function');
441
452
  // Transform the return type (e.g., RETURNS SETOF schema.tablename)
442
453
  if (node.returnType?.names) {
443
- transformNameList(node.returnType.names, schemaMapping, result);
454
+ transformNameList(node.returnType.names, schemaMapping, result, 'type');
444
455
  }
445
456
  // Transform parameter types (the walker does NOT auto-recurse into
446
457
  // FunctionParameter.argType TypeName nodes)
447
458
  if (Array.isArray(node.parameters)) {
448
459
  for (const param of node.parameters) {
449
460
  if (param?.FunctionParameter?.argType?.names) {
450
- transformNameList(param.FunctionParameter.argType.names, schemaMapping, result);
461
+ transformNameList(param.FunctionParameter.argType.names, schemaMapping, result, 'type');
451
462
  }
452
463
  }
453
464
  }
@@ -460,8 +471,16 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
460
471
  for (const opt of node.options) {
461
472
  if (opt?.DefElem?.defname === 'as' && opt.DefElem.arg?.List?.items) {
462
473
  for (const item of opt.DefElem.arg.List.items) {
463
- if (typeof item?.String?.sval === 'string' && item.String.sval.includes('.')) {
464
- item.String.sval = transformSchemaRefsInString(item.String.sval, schemaMapping, result);
474
+ if (typeof item?.String?.sval !== 'string')
475
+ continue;
476
+ // With object routes, references inside a LANGUAGE sql body must
477
+ // be rewritten AST-precisely; whole-schema routes keep the
478
+ // cheaper (and quoting-preserving) string pass.
479
+ if (useAstBodies) {
480
+ item.String.sval = transformSqlBodyString(item.String.sval, router, result);
481
+ }
482
+ else if (item.String.sval.includes('.')) {
483
+ item.String.sval = transformSchemaRefsInString(item.String.sval, router, result);
465
484
  }
466
485
  }
467
486
  }
@@ -473,7 +492,7 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
473
492
  // so we must explicitly transform the RangeVar here.
474
493
  CreateTrigStmt: (path) => {
475
494
  const node = path.node;
476
- transformNameList(node.funcname, schemaMapping, result);
495
+ transformNameList(node.funcname, schemaMapping, result, 'function');
477
496
  transformRelation(node.relation, schemaMapping, result);
478
497
  },
479
498
  // Transform CommentStmt (COMMENT ON TABLE/COLUMN/FUNCTION schema.obj,
@@ -519,29 +538,29 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
519
538
  // Transform CreateDomainStmt (CREATE DOMAIN schema.domname)
520
539
  CreateDomainStmt: (path) => {
521
540
  const node = path.node;
522
- transformNameList(node.domainname, schemaMapping, result);
541
+ transformNameList(node.domainname, schemaMapping, result, 'type');
523
542
  },
524
543
  // Transform CreateEnumStmt (CREATE TYPE schema.enumname AS ENUM)
525
544
  CreateEnumStmt: (path) => {
526
545
  const node = path.node;
527
- transformNameList(node.typeName, schemaMapping, result);
546
+ transformNameList(node.typeName, schemaMapping, result, 'type');
528
547
  },
529
548
  // Transform AlterEnumStmt (ALTER TYPE schema.enumname ADD VALUE)
530
549
  AlterEnumStmt: (path) => {
531
550
  const node = path.node;
532
- transformNameList(node.typeName, schemaMapping, result);
551
+ transformNameList(node.typeName, schemaMapping, result, 'type');
533
552
  },
534
553
  // Transform AlterDomainStmt (ALTER DOMAIN schema.domname)
535
554
  AlterDomainStmt: (path) => {
536
555
  const node = path.node;
537
- transformNameList(node.typeName, schemaMapping, result);
556
+ transformNameList(node.typeName, schemaMapping, result, 'type');
538
557
  },
539
558
  // Transform AlterTypeStmt (ALTER TYPE schema.typename)
540
559
  // Note: composite type alters use RangeVar (handled by RangeVar visitor),
541
560
  // but some ALTER TYPE statements use typeName as a name list
542
561
  AlterTypeStmt: (path) => {
543
562
  const node = path.node;
544
- transformNameList(node.typeName, schemaMapping, result);
563
+ transformNameList(node.typeName, schemaMapping, result, 'type');
545
564
  },
546
565
  // Transform ObjectWithArgs (used in ALTER FUNCTION, DROP FUNCTION with args, etc.)
547
566
  ObjectWithArgs: (path) => {
@@ -589,7 +608,7 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
589
608
  AlterFunctionStmt: (path) => {
590
609
  const node = path.node;
591
610
  if (node.func?.objname) {
592
- transformNameList(node.func.objname, schemaMapping, result);
611
+ transformNameList(node.func.objname, schemaMapping, result, 'function');
593
612
  }
594
613
  },
595
614
  // Transform AlterOwnerStmt (ALTER ... OWNER TO role)
@@ -610,28 +629,28 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
610
629
  CreateCastStmt: (path) => {
611
630
  const node = path.node;
612
631
  if (node.sourcetype?.names) {
613
- transformNameList(node.sourcetype.names, schemaMapping, result);
632
+ transformNameList(node.sourcetype.names, schemaMapping, result, 'type');
614
633
  }
615
634
  if (node.targettype?.names) {
616
- transformNameList(node.targettype.names, schemaMapping, result);
635
+ transformNameList(node.targettype.names, schemaMapping, result, 'type');
617
636
  }
618
637
  if (node.func?.objname) {
619
- transformNameList(node.func.objname, schemaMapping, result);
638
+ transformNameList(node.func.objname, schemaMapping, result, 'function');
620
639
  }
621
640
  if (Array.isArray(node.func?.objargs)) {
622
641
  for (const arg of node.func.objargs) {
623
642
  if (arg?.TypeName?.names) {
624
- transformNameList(arg.TypeName.names, schemaMapping, result);
643
+ transformNameList(arg.TypeName.names, schemaMapping, result, 'type');
625
644
  }
626
645
  else if (arg?.names) {
627
- transformNameList(arg.names, schemaMapping, result);
646
+ transformNameList(arg.names, schemaMapping, result, 'type');
628
647
  }
629
648
  }
630
649
  }
631
650
  if (Array.isArray(node.func?.objfuncargs)) {
632
651
  for (const param of node.func.objfuncargs) {
633
652
  if (param?.FunctionParameter?.argType?.names) {
634
- transformNameList(param.FunctionParameter.argType.names, schemaMapping, result);
653
+ transformNameList(param.FunctionParameter.argType.names, schemaMapping, result, 'type');
635
654
  }
636
655
  }
637
656
  }
@@ -639,7 +658,7 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
639
658
  // Transform CreateEventTrigStmt (CREATE EVENT TRIGGER ... EXECUTE FUNCTION schema.fn())
640
659
  CreateEventTrigStmt: (path) => {
641
660
  const node = path.node;
642
- transformNameList(node.funcname, schemaMapping, result);
661
+ transformNameList(node.funcname, schemaMapping, result, 'function');
643
662
  },
644
663
  // Transform IndexElem opclass (CREATE INDEX ... (col schema.opclass))
645
664
  IndexElem: (path) => {
@@ -680,10 +699,14 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
680
699
  * indicating that the AST visitor is missing a handler for that node type.
681
700
  */
682
701
  export function validateNoUntransformedSchemas(content, schemaMapping) {
683
- if (schemaMapping.size === 0) {
702
+ // Only schemas with a schema-level default are guaranteed to move entirely;
703
+ // partially (object-only) routed schemas may legitimately keep some
704
+ // references, so they are excluded from the strict leftover check.
705
+ const moved = schemaMapping instanceof SchemaRouter ? schemaMapping.fullyMovedSchemas() : schemaMapping;
706
+ if (moved.size === 0) {
684
707
  return;
685
708
  }
686
- for (const [oldSchema, newSchema] of schemaMapping) {
709
+ for (const [oldSchema, newSchema] of moved) {
687
710
  const escapedSchema = escapeRegexp(oldSchema);
688
711
  // Pattern 1: quoted or unquoted schema name followed by dot (schema-qualified)
689
712
  const dotPattern = new RegExp(`(?:"${escapedSchema}"|\\b${escapedSchema})(?=\\.)`, 'g');
@@ -718,11 +741,12 @@ export function validateNoUntransformedSchemas(content, schemaMapping) {
718
741
  * Create a PL/pgSQL visitor that transforms schema names in PL/pgSQL-specific nodes.
719
742
  */
720
743
  export function createPlpgsqlVisitor(schemaMapping, result) {
744
+ const schemaMap = schemaLevelMap(schemaMapping);
721
745
  return {
722
746
  PLpgSQL_type: (path) => {
723
747
  const node = path.node;
724
748
  if (node.typname) {
725
- for (const [oldSchema, newSchema] of schemaMapping.entries()) {
749
+ for (const [oldSchema, newSchema] of schemaMap.entries()) {
726
750
  if (node.typname.startsWith(oldSchema + '.')) {
727
751
  const typeName = node.typname.substring(oldSchema.length + 1);
728
752
  result.schemasFound.add(oldSchema);
@@ -736,7 +760,7 @@ export function createPlpgsqlVisitor(schemaMapping, result) {
736
760
  PLpgSQL_var: (path) => {
737
761
  const node = path.node;
738
762
  if (node.refname) {
739
- for (const [oldSchema, newSchema] of schemaMapping.entries()) {
763
+ for (const [oldSchema, newSchema] of schemaMap.entries()) {
740
764
  if (node.refname.startsWith(oldSchema + '.')) {
741
765
  const rest = node.refname.substring(oldSchema.length + 1);
742
766
  result.schemasFound.add(oldSchema);
@@ -753,6 +777,7 @@ export function createPlpgsqlVisitor(schemaMapping, result) {
753
777
  * Transform a PLpgSQL_type typname using proper AST parsing.
754
778
  */
755
779
  export function transformPlpgsqlTypeAst(typname, schemaMapping, result) {
780
+ const schemaMap = schemaLevelMap(schemaMapping);
756
781
  let suffix = '';
757
782
  let baseTypname = typname;
758
783
  const rowtypeMatch = typname.match(/(%rowtype|%type)$/i);
@@ -761,7 +786,7 @@ export function transformPlpgsqlTypeAst(typname, schemaMapping, result) {
761
786
  baseTypname = typname.substring(0, typname.length - suffix.length);
762
787
  }
763
788
  let needsTransform = false;
764
- for (const oldSchema of schemaMapping.keys()) {
789
+ for (const oldSchema of schemaMap.keys()) {
765
790
  if (baseTypname.startsWith(oldSchema + '.') || baseTypname.startsWith('"' + oldSchema + '".')) {
766
791
  needsTransform = true;
767
792
  break;
@@ -780,7 +805,7 @@ export function transformPlpgsqlTypeAst(typname, schemaMapping, result) {
780
805
  TypeName: (path) => {
781
806
  const typeNode = path.node;
782
807
  if (typeNode.names && Array.isArray(typeNode.names)) {
783
- transformNameList(typeNode.names, schemaMapping, result);
808
+ transformNameList(typeNode.names, schemaMapping, result, 'type');
784
809
  }
785
810
  }
786
811
  };
@@ -802,7 +827,7 @@ export function transformPlpgsqlTypeAst(typname, schemaMapping, result) {
802
827
  * Uses @pgsql/quotes QuoteUtils for proper identifier quoting.
803
828
  */
804
829
  export function transformPlpgsqlTypeString(typname, schemaMapping, result) {
805
- for (const [oldSchema, newSchema] of schemaMapping.entries()) {
830
+ for (const [oldSchema, newSchema] of schemaLevelMap(schemaMapping).entries()) {
806
831
  // Unquoted schema: old_schema.rest
807
832
  if (typname.startsWith(oldSchema + '.')) {
808
833
  const rest = typname.substring(oldSchema.length + 1);
@@ -873,7 +898,7 @@ export function walkPlpgsqlForSchemas(node, schemaMapping, result) {
873
898
  // We cannot use walkSql here because the raw typeNameNode is not
874
899
  // wrapped in the expected AST envelope that the traverse walker needs.
875
900
  if (plType.typname.typeNameNode?.names) {
876
- transformNameList(plType.typname.typeNameNode.names, schemaMapping, result);
901
+ transformNameList(plType.typname.typeNameNode.names, schemaMapping, result, 'type');
877
902
  }
878
903
  // The deparser can fall back to the 'original' string to render
879
904
  // DECLARE types, so update it as well. Rewrite only the schema
@@ -892,6 +917,35 @@ export function walkPlpgsqlForSchemas(node, schemaMapping, result) {
892
917
  walkPlpgsqlForSchemas(value, schemaMapping, result);
893
918
  }
894
919
  }
920
+ /**
921
+ * Rewrite a `LANGUAGE sql` function-body string using the full AST visitor so
922
+ * object-level routes reach references inside the body (the body is an opaque
923
+ * String node the outer walker never parses). Parses each statement, walks it
924
+ * with the router-aware SQL visitor, and deparses. Falls back to the
925
+ * schema-level string pass for anything that does not parse standalone (e.g.
926
+ * PL/pgSQL blocks or C symbol names).
927
+ */
928
+ function transformSqlBodyString(body, router, result) {
929
+ try {
930
+ const parseResult = parseSql(body);
931
+ const stmts = parseResult?.stmts ?? [];
932
+ if (stmts.length === 0) {
933
+ return body.includes('.') ? transformSchemaRefsInString(body, router, result) : body;
934
+ }
935
+ const visitor = createSqlVisitor(router, result);
936
+ const pieces = [];
937
+ for (const stmt of stmts) {
938
+ if (!stmt?.stmt)
939
+ continue;
940
+ walkSql(stmt.stmt, visitor);
941
+ pieces.push(Deparser.deparse(stmt.stmt));
942
+ }
943
+ return pieces.join(';\n');
944
+ }
945
+ catch {
946
+ return body.includes('.') ? transformSchemaRefsInString(body, router, result) : body;
947
+ }
948
+ }
895
949
  /**
896
950
  * Escape a string for use in a regular expression
897
951
  */
@@ -1040,11 +1094,13 @@ export function transformSql(content, schemaMapping, options, result) {
1040
1094
  if (schemaMapping.size === 0) {
1041
1095
  return { content, result: r };
1042
1096
  }
1097
+ // String-level passes operate on whole schemas; give them the flat view.
1098
+ const passMap = schemaLevelMap(schemaMapping);
1043
1099
  let newContent = content;
1044
1100
  // Run pre-passes (app-specific string-level transforms)
1045
1101
  if (opts.prePasses) {
1046
1102
  for (const pass of opts.prePasses) {
1047
- newContent = pass(newContent, schemaMapping, r);
1103
+ newContent = pass(newContent, passMap, r);
1048
1104
  }
1049
1105
  }
1050
1106
  // Main pass: AST transformation with header handling
@@ -1052,7 +1108,7 @@ export function transformSql(content, schemaMapping, options, result) {
1052
1108
  // Run post-passes (app-specific string-level transforms)
1053
1109
  if (opts.postPasses) {
1054
1110
  for (const pass of opts.postPasses) {
1055
- newContent = pass(newContent, schemaMapping, r);
1111
+ newContent = pass(newContent, passMap, r);
1056
1112
  }
1057
1113
  }
1058
1114
  return { content: newContent, result: r };
@@ -1109,7 +1165,8 @@ function transformSqlContentAst(content, schemaMapping, result, options) {
1109
1165
  const { header, body } = extractPgpmHeader(content);
1110
1166
  let transformedHeader = header;
1111
1167
  if (header.length > 0) {
1112
- transformedHeader = transformComments(header, schemaMapping, result);
1168
+ // Header/path rewrites (-- Deploy: schemas/<schema>/...) are whole-schema.
1169
+ transformedHeader = transformComments(header, schemaLevelMap(schemaMapping), result);
1113
1170
  }
1114
1171
  let transformedBody = body;
1115
1172
  if (options?.qualifyUnqualified && transformedBody.trim().length > 0) {
package/facts.js CHANGED
@@ -244,6 +244,43 @@ function classifyOne(nodeTag, node) {
244
244
  collectRoles(node, facts.roles);
245
245
  return facts;
246
246
  }
247
+ /** Read a `CreateFunctionStmt` DefElem option's scalar/list value. */
248
+ function functionOption(node, defname) {
249
+ for (const opt of node.options ?? []) {
250
+ if (opt?.DefElem?.defname === defname)
251
+ return opt.DefElem.arg;
252
+ }
253
+ return undefined;
254
+ }
255
+ /**
256
+ * Collect references from a `LANGUAGE sql` function body supplied as a string
257
+ * literal (`AS $$ ... $$`). That body is an opaque String node the AST walker
258
+ * never parses, so — mirroring the schema transformer's body rewrite — parse
259
+ * it standalone and walk each statement with the facts visitor. The standard
260
+ * `BEGIN ATOMIC` / `RETURN` `sql_body` form is already part of the AST and is
261
+ * covered by the outer walk, so only the string form needs this.
262
+ */
263
+ function collectSqlBodyReferences(node, facts) {
264
+ const language = functionOption(node, 'language')?.String?.sval;
265
+ if (typeof language !== 'string' || language.toLowerCase() !== 'sql')
266
+ return;
267
+ const asArg = functionOption(node, 'as');
268
+ const items = asArg?.List?.items ?? [];
269
+ const body = items[0]?.String?.sval;
270
+ if (typeof body !== 'string')
271
+ return;
272
+ try {
273
+ const stmts = (0, plpgsql_parser_1.parseSql)(body)?.stmts ?? [];
274
+ const visitor = createFactsVisitor(facts, facts.bodyReferences);
275
+ for (const stmt of stmts) {
276
+ if (stmt?.stmt)
277
+ (0, traverse_1.walk)(stmt.stmt, visitor);
278
+ }
279
+ }
280
+ catch {
281
+ // A non-parseable body (C symbol name, etc.) contributes no references.
282
+ }
283
+ }
247
284
  /**
248
285
  * Classify each top-level statement in a SQL script into {@link StatementFacts}.
249
286
  *
@@ -263,6 +300,9 @@ function classifyStatements(sql) {
263
300
  if (stmtNode) {
264
301
  (0, traverse_1.walk)(stmtNode, createFactsVisitor(facts));
265
302
  }
303
+ if (nodeTag === 'CreateFunctionStmt') {
304
+ collectSqlBodyReferences(node, facts);
305
+ }
266
306
  allFacts.push(facts);
267
307
  }
268
308
  for (const fn of ctx.functions ?? []) {