@pgsql/transform 18.1.0 → 18.3.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.
Files changed (113) hide show
  1. package/README.md +29 -73
  2. package/categorize.d.ts +52 -0
  3. package/categorize.js +56 -0
  4. package/esm/categorize.js +51 -0
  5. package/esm/facts.js +289 -0
  6. package/esm/fixture-closure.js +197 -0
  7. package/esm/index.js +7 -22
  8. package/esm/qualify.js +297 -0
  9. package/esm/round-trip-core.js +126 -0
  10. package/esm/round-trip.js +116 -0
  11. package/esm/transform.js +1169 -0
  12. package/facts.d.ts +69 -0
  13. package/facts.js +292 -0
  14. package/fixture-closure.d.ts +71 -0
  15. package/fixture-closure.js +200 -0
  16. package/index.d.ts +14 -28
  17. package/index.js +43 -76
  18. package/package.json +22 -14
  19. package/qualify.d.ts +125 -0
  20. package/qualify.js +303 -0
  21. package/round-trip-core.d.ts +60 -0
  22. package/round-trip-core.js +133 -0
  23. package/round-trip.d.ts +47 -0
  24. package/round-trip.js +123 -0
  25. package/transform.d.ts +259 -0
  26. package/transform.js +1190 -0
  27. package/13/enums.d.ts +0 -62
  28. package/13/enums.js +0 -2
  29. package/13/types.d.ts +0 -2103
  30. package/13/types.js +0 -2
  31. package/14/enums.d.ts +0 -62
  32. package/14/enums.js +0 -2
  33. package/14/types.d.ts +0 -2164
  34. package/14/types.js +0 -2
  35. package/15/enums.d.ts +0 -64
  36. package/15/enums.js +0 -2
  37. package/15/types.d.ts +0 -2222
  38. package/15/types.js +0 -2
  39. package/16/enums.d.ts +0 -69
  40. package/16/enums.js +0 -2
  41. package/16/types.d.ts +0 -2328
  42. package/16/types.js +0 -2
  43. package/17/enums.d.ts +0 -76
  44. package/17/enums.js +0 -2
  45. package/17/types.d.ts +0 -2488
  46. package/17/types.js +0 -2
  47. package/18/enums.d.ts +0 -78
  48. package/18/enums.js +0 -2
  49. package/18/types.d.ts +0 -2538
  50. package/18/types.js +0 -2
  51. package/esm/13/enums.js +0 -1
  52. package/esm/13/types.js +0 -1
  53. package/esm/14/enums.js +0 -1
  54. package/esm/14/types.js +0 -1
  55. package/esm/15/enums.js +0 -1
  56. package/esm/15/types.js +0 -1
  57. package/esm/16/enums.js +0 -1
  58. package/esm/16/types.js +0 -1
  59. package/esm/17/enums.js +0 -1
  60. package/esm/17/types.js +0 -1
  61. package/esm/18/enums.js +0 -1
  62. package/esm/18/types.js +0 -1
  63. package/esm/multi-version-transformer.js +0 -80
  64. package/esm/transformers/context.js +0 -1
  65. package/esm/transformers/v13-to-v14.js +0 -2767
  66. package/esm/transformers/v14-to-v15.js +0 -1218
  67. package/esm/transformers/v15-to-v16.js +0 -2935
  68. package/esm/transformers/v16-to-v17.js +0 -1526
  69. package/esm/transformers/v17-to-v18.js +0 -204
  70. package/esm/transformers-direct/index.js +0 -13
  71. package/esm/transformers-direct/v13-to-v17/index.js +0 -74
  72. package/esm/transformers-direct/v13-to-v18/index.js +0 -84
  73. package/esm/transformers-direct/v14-to-v17/index.js +0 -63
  74. package/esm/transformers-direct/v14-to-v18/index.js +0 -73
  75. package/esm/transformers-direct/v15-to-v17/index.js +0 -53
  76. package/esm/transformers-direct/v15-to-v18/index.js +0 -62
  77. package/esm/transformers-direct/v16-to-v17/index.js +0 -40
  78. package/esm/transformers-direct/v16-to-v18/index.js +0 -51
  79. package/esm/transformers-direct/v17-to-v18/index.js +0 -39
  80. package/multi-version-transformer.d.ts +0 -14
  81. package/multi-version-transformer.js +0 -84
  82. package/transformers/context.d.ts +0 -4
  83. package/transformers/context.js +0 -2
  84. package/transformers/v13-to-v14.d.ts +0 -255
  85. package/transformers/v13-to-v14.js +0 -2771
  86. package/transformers/v14-to-v15.d.ts +0 -615
  87. package/transformers/v14-to-v15.js +0 -1222
  88. package/transformers/v15-to-v16.d.ts +0 -632
  89. package/transformers/v15-to-v16.js +0 -2939
  90. package/transformers/v16-to-v17.d.ts +0 -640
  91. package/transformers/v16-to-v17.js +0 -1530
  92. package/transformers/v17-to-v18.d.ts +0 -54
  93. package/transformers/v17-to-v18.js +0 -208
  94. package/transformers-direct/index.d.ts +0 -13
  95. package/transformers-direct/index.js +0 -25
  96. package/transformers-direct/v13-to-v17/index.d.ts +0 -27
  97. package/transformers-direct/v13-to-v17/index.js +0 -78
  98. package/transformers-direct/v13-to-v18/index.d.ts +0 -28
  99. package/transformers-direct/v13-to-v18/index.js +0 -88
  100. package/transformers-direct/v14-to-v17/index.d.ts +0 -26
  101. package/transformers-direct/v14-to-v17/index.js +0 -67
  102. package/transformers-direct/v14-to-v18/index.d.ts +0 -27
  103. package/transformers-direct/v14-to-v18/index.js +0 -77
  104. package/transformers-direct/v15-to-v17/index.d.ts +0 -25
  105. package/transformers-direct/v15-to-v17/index.js +0 -57
  106. package/transformers-direct/v15-to-v18/index.d.ts +0 -26
  107. package/transformers-direct/v15-to-v18/index.js +0 -66
  108. package/transformers-direct/v16-to-v17/index.d.ts +0 -24
  109. package/transformers-direct/v16-to-v17/index.js +0 -44
  110. package/transformers-direct/v16-to-v18/index.d.ts +0 -25
  111. package/transformers-direct/v16-to-v18/index.js +0 -55
  112. package/transformers-direct/v17-to-v18/index.d.ts +0 -24
  113. package/transformers-direct/v17-to-v18/index.js +0 -43
@@ -0,0 +1,1169 @@
1
+ /**
2
+ * AST-based Schema Name Transformer (Core Logic)
3
+ *
4
+ * Pure transformation functions for converting schema names in SQL strings.
5
+ * No file I/O — takes SQL strings in and returns transformed SQL strings out.
6
+ *
7
+ * The transformer handles:
8
+ * - Schema-qualified identifiers in RangeVar nodes (schema.table)
9
+ * - Schema names in CreateSchemaStmt nodes
10
+ * - Schema-qualified function names in FuncCall and CreateFunctionStmt nodes
11
+ * - Schema-qualified type names in TypeName nodes
12
+ * - Schema names in GrantStmt objects array
13
+ * - Schema names in VariableSetStmt (SET search_path)
14
+ * - Schema names in AlterDefaultPrivilegesStmt
15
+ * - Schema names in DropStmt (DROP SCHEMA)
16
+ * - Schema-qualified trigger function names in CreateTrigStmt
17
+ * - Schema-qualified object references in CommentStmt
18
+ * - Schema-qualified names in DefineStmt (CREATE TYPE, CREATE AGGREGATE)
19
+ * - Schema-qualified names in CreateDomainStmt, CreateEnumStmt, AlterEnumStmt
20
+ * - Schema-qualified names in AlterDomainStmt, AlterTypeStmt
21
+ * - Schema-qualified names in ObjectWithArgs (ALTER FUNCTION, etc.)
22
+ * - Schema name in AlterObjectSchemaStmt (ALTER ... SET SCHEMA)
23
+ * - Schema names inside PL/pgSQL function bodies (hydrated AST)
24
+ * - Comment headers, verify function calls, JSON string values (regex fallback)
25
+ */
26
+ import { QuoteUtils } from '@pgsql/quotes';
27
+ import { walk as walkSql } from '@pgsql/traverse';
28
+ import { Deparser, parseSql, transformSync, walk as walkPlpgsql } from 'plpgsql-parser';
29
+ import { qualifyUnqualified } from './qualify';
30
+ import { captureTransformAsts, validateRoundTrip } from './round-trip';
31
+ /**
32
+ * Create a fresh result object
33
+ */
34
+ function createResult() {
35
+ return {
36
+ schemasFound: new Set(),
37
+ schemasTransformed: new Map(),
38
+ errors: [],
39
+ };
40
+ }
41
+ /**
42
+ * Transform a schema name if it exists in the mapping
43
+ */
44
+ export function transformSchemaName(schemaName, schemaMapping) {
45
+ if (!schemaName)
46
+ return schemaName;
47
+ return schemaMapping.get(schemaName) ?? schemaName;
48
+ }
49
+ /**
50
+ * Check if a schema name should be transformed
51
+ */
52
+ export function shouldTransformSchema(schemaName, schemaMapping) {
53
+ if (!schemaName)
54
+ return false;
55
+ return schemaMapping.has(schemaName);
56
+ }
57
+ /**
58
+ * Transform schema names in a String node array (used for funcname, names, etc.)
59
+ * These arrays contain String nodes like { String: { sval: 'schema_name' } }
60
+ */
61
+ export function transformNameList(names, schemaMapping, result) {
62
+ if (!names || names.length < 2)
63
+ return;
64
+ // For schema-qualified names, the first element is the schema
65
+ const first = names[0];
66
+ if (first?.String?.sval) {
67
+ const schemaName = first.String.sval;
68
+ if (shouldTransformSchema(schemaName, schemaMapping)) {
69
+ 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
+ }
75
+ }
76
+ }
77
+ }
78
+ /**
79
+ * Transform a bare schema-name field on a node (e.g. { String: { sval } }
80
+ * entries in GrantStmt.objects, RenameStmt.subname, CommentStmt.object).
81
+ * Returns the (possibly new) schema name.
82
+ */
83
+ export function transformSchemaNameField(container, field, schemaMapping, result) {
84
+ const schemaName = container?.[field];
85
+ if (typeof schemaName !== 'string')
86
+ return;
87
+ if (shouldTransformSchema(schemaName, schemaMapping)) {
88
+ result.schemasFound.add(schemaName);
89
+ const newName = schemaMapping.get(schemaName);
90
+ if (newName) {
91
+ container[field] = newName;
92
+ result.schemasTransformed.set(schemaName, newName);
93
+ }
94
+ }
95
+ }
96
+ /**
97
+ * Transform a RangeVar-like relation object (has schemaname/relname fields).
98
+ * Many statement nodes embed a relation that the walker does NOT auto-recurse into.
99
+ */
100
+ export function transformRelation(relation, schemaMapping, result) {
101
+ if (relation?.schemaname && shouldTransformSchema(relation.schemaname, schemaMapping)) {
102
+ const oldName = relation.schemaname;
103
+ result.schemasFound.add(oldName);
104
+ const newName = schemaMapping.get(oldName);
105
+ if (newName) {
106
+ relation.schemaname = newName;
107
+ result.schemasTransformed.set(oldName, newName);
108
+ }
109
+ }
110
+ }
111
+ /**
112
+ * Transform schema-qualified references embedded in a plain string
113
+ * (e.g. advisory-lock keys like 'schema-name.fn_name', COMMENT text,
114
+ * RAISE messages). Only occurrences of a mapped schema name immediately
115
+ * followed by a dot are rewritten.
116
+ */
117
+ export function transformSchemaRefsInString(str, schemaMapping, result) {
118
+ let out = str;
119
+ for (const [oldSchema, newSchema] of schemaMapping) {
120
+ const pattern = new RegExp(`(?<![\\w-])("?)${escapeRegexp(oldSchema)}\\1(?=\\.)`, 'g');
121
+ const before = out;
122
+ out = out.replace(pattern, `$1${newSchema}$1`);
123
+ if (out !== before) {
124
+ result.schemasFound.add(oldSchema);
125
+ result.schemasTransformed.set(oldSchema, newSchema);
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+ /**
131
+ * Create a SQL AST visitor that transforms schema names.
132
+ *
133
+ * The walker from @pgsql/traverse auto-recurses into child nodes, so visitors
134
+ * like RangeVar and TypeName fire for every occurrence regardless of parent.
135
+ * However, some node types carry schema names as plain strings or name lists
136
+ * that require explicit handlers.
137
+ */
138
+ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
139
+ const assumeSchemasExist = visitorOptions?.assumeSchemasExist;
140
+ return {
141
+ // Transform RangeVar nodes (table references)
142
+ 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
+ }
153
+ },
154
+ // Transform CreateSchemaStmt nodes
155
+ CreateSchemaStmt: (path) => {
156
+ const node = path.node;
157
+ if (node.schemaname && shouldTransformSchema(node.schemaname, schemaMapping)) {
158
+ const oldName = node.schemaname;
159
+ result.schemasFound.add(oldName);
160
+ const newName = schemaMapping.get(oldName);
161
+ if (newName) {
162
+ node.schemaname = newName;
163
+ result.schemasTransformed.set(oldName, newName);
164
+ }
165
+ }
166
+ if (node.schemaname && assumeSchemasExist?.has(node.schemaname)) {
167
+ node.if_not_exists = true;
168
+ }
169
+ },
170
+ // Transform FuncCall nodes (function calls with schema-qualified names)
171
+ FuncCall: (path) => {
172
+ const node = path.node;
173
+ transformNameList(node.funcname, schemaMapping, result);
174
+ },
175
+ // Transform CallStmt (CALL schema.procedure(...)).
176
+ // Note: The walker does NOT auto-recurse into CallStmt.funccall,
177
+ // so the FuncCall visitor above never sees it.
178
+ CallStmt: (path) => {
179
+ const node = path.node;
180
+ if (node.funccall?.funcname) {
181
+ transformNameList(node.funccall.funcname, schemaMapping, result);
182
+ }
183
+ },
184
+ // Transform TypeName nodes (type references with schema-qualified names)
185
+ TypeName: (path) => {
186
+ const node = path.node;
187
+ transformNameList(node.names, schemaMapping, result);
188
+ },
189
+ // Transform ColumnRef nodes (column references with schema-qualified names)
190
+ ColumnRef: (path) => {
191
+ const node = path.node;
192
+ transformNameList(node.fields, schemaMapping, result);
193
+ },
194
+ // Transform GrantStmt objects (GRANT ON SCHEMA schema;
195
+ // GRANT ... ON ALL TABLES/FUNCTIONS/SEQUENCES IN SCHEMA schema)
196
+ GrantStmt: (path) => {
197
+ const node = path.node;
198
+ // For schema-name targets, objects contains String nodes with schema names
199
+ if ((node.objtype === 'OBJECT_SCHEMA' || node.targtype === 'ACL_TARGET_ALL_IN_SCHEMA') && node.objects) {
200
+ for (const obj of node.objects) {
201
+ if (obj?.String) {
202
+ transformSchemaNameField(obj.String, 'sval', schemaMapping, result);
203
+ }
204
+ }
205
+ }
206
+ },
207
+ // Transform VariableSetStmt (SET search_path)
208
+ VariableSetStmt: (path) => {
209
+ const node = path.node;
210
+ if (node.name === 'search_path' && node.args) {
211
+ for (const arg of node.args) {
212
+ // search_path args can be String nodes or A_Const with sval
213
+ 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
+ }
223
+ }
224
+ 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
+ }
234
+ }
235
+ }
236
+ }
237
+ },
238
+ // Transform AlterDefaultPrivilegesStmt (ALTER DEFAULT PRIVILEGES IN SCHEMA)
239
+ AlterDefaultPrivilegesStmt: (path) => {
240
+ const node = path.node;
241
+ if (node.options) {
242
+ for (const opt of node.options) {
243
+ if (opt?.DefElem?.defname === 'schemas' && opt.DefElem.arg?.List?.items) {
244
+ for (const item of opt.DefElem.arg.List.items) {
245
+ 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
+ }
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+ },
261
+ // Transform DropStmt (DROP SCHEMA name; DROP TABLE/INDEX/POLICY/TRIGGER/
262
+ // TYPE/FUNCTION schema.obj). The walker does NOT auto-recurse into
263
+ // DropStmt.objects.
264
+ DropStmt: (path) => {
265
+ const node = path.node;
266
+ if (node.removeType !== 'OBJECT_SCHEMA' && Array.isArray(node.objects)) {
267
+ for (const obj of node.objects) {
268
+ if (obj?.List?.items) {
269
+ transformNameList(obj.List.items, schemaMapping, result);
270
+ }
271
+ else if (obj?.ObjectWithArgs?.objname) {
272
+ transformNameList(obj.ObjectWithArgs.objname, schemaMapping, result);
273
+ }
274
+ else if (obj?.TypeName?.names) {
275
+ transformNameList(obj.TypeName.names, schemaMapping, result);
276
+ }
277
+ }
278
+ return;
279
+ }
280
+ if (node.removeType === 'OBJECT_SCHEMA' && node.objects) {
281
+ for (const obj of node.objects) {
282
+ // DROP SCHEMA objects can be List of String nodes
283
+ if (obj?.List?.items) {
284
+ for (const item of obj.List.items) {
285
+ 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
+ }
295
+ }
296
+ }
297
+ }
298
+ 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
+ }
308
+ }
309
+ }
310
+ }
311
+ },
312
+ // Transform InsertStmt (INSERT INTO schema.table)
313
+ // Note: The walker does NOT auto-recurse into InsertStmt.relation.
314
+ InsertStmt: (path) => {
315
+ transformRelation(path.node.relation, schemaMapping, result);
316
+ },
317
+ // Transform CreateStmt (CREATE TABLE schema.table)
318
+ // Note: The walker does NOT auto-recurse into CreateStmt.relation.
319
+ CreateStmt: (path) => {
320
+ transformRelation(path.node.relation, schemaMapping, result);
321
+ },
322
+ // Transform UpdateStmt (UPDATE schema.table SET ...)
323
+ // Note: The walker does NOT auto-recurse into UpdateStmt.relation.
324
+ UpdateStmt: (path) => {
325
+ transformRelation(path.node.relation, schemaMapping, result);
326
+ },
327
+ // Transform DeleteStmt (DELETE FROM schema.table)
328
+ // Note: The walker does NOT auto-recurse into DeleteStmt.relation.
329
+ DeleteStmt: (path) => {
330
+ transformRelation(path.node.relation, schemaMapping, result);
331
+ },
332
+ // Transform MergeStmt (MERGE INTO schema.table USING ...)
333
+ // Note: The walker does NOT auto-recurse into MergeStmt.relation.
334
+ MergeStmt: (path) => {
335
+ transformRelation(path.node.relation, schemaMapping, result);
336
+ },
337
+ // Transform IndexStmt (CREATE INDEX ... ON schema.table)
338
+ // Note: The walker does NOT auto-recurse into IndexStmt.relation.
339
+ IndexStmt: (path) => {
340
+ transformRelation(path.node.relation, schemaMapping, result);
341
+ },
342
+ // Transform Constraint (REFERENCES schema.table for foreign keys)
343
+ // Note: The walker does NOT auto-recurse into Constraint.pktable.
344
+ Constraint: (path) => {
345
+ const node = path.node;
346
+ if (node.contype === 'CONSTR_FOREIGN' && node.pktable) {
347
+ transformRelation(node.pktable, schemaMapping, result);
348
+ }
349
+ },
350
+ // Transform CompositeTypeStmt (CREATE TYPE schema.name AS (...))
351
+ // Note: The walker does NOT auto-recurse into CompositeTypeStmt.typevar.
352
+ CompositeTypeStmt: (path) => {
353
+ transformRelation(path.node.typevar, schemaMapping, result);
354
+ },
355
+ // Transform ColumnDef (column definitions with schema-qualified types)
356
+ // Note: The walker does NOT auto-recurse into ColumnDef.typeName.
357
+ ColumnDef: (path) => {
358
+ const node = path.node;
359
+ if (node.typeName?.names) {
360
+ transformNameList(node.typeName.names, schemaMapping, result);
361
+ }
362
+ },
363
+ // Transform ViewStmt (CREATE VIEW schema.viewname AS ...)
364
+ // Note: The walker does NOT auto-recurse into ViewStmt.view.
365
+ ViewStmt: (path) => {
366
+ transformRelation(path.node.view, schemaMapping, result);
367
+ },
368
+ // Transform AlterTableStmt (ALTER TABLE schema.table ...)
369
+ // Note: The walker does NOT auto-recurse into AlterTableStmt.relation.
370
+ AlterTableStmt: (path) => {
371
+ transformRelation(path.node.relation, schemaMapping, result);
372
+ },
373
+ // Transform CreateSeqStmt (CREATE SEQUENCE schema.seqname)
374
+ // Note: The walker does NOT auto-recurse into CreateSeqStmt.sequence.
375
+ CreateSeqStmt: (path) => {
376
+ transformRelation(path.node.sequence, schemaMapping, result);
377
+ },
378
+ // Transform AlterSeqStmt (ALTER SEQUENCE schema.seqname [OWNED BY schema.table.col])
379
+ // Note: The walker does NOT auto-recurse into AlterSeqStmt.sequence.
380
+ // Also handles OWNED BY clause where the schema name is in DefElem.arg.List.items.
381
+ AlterSeqStmt: (path) => {
382
+ const node = path.node;
383
+ transformRelation(node.sequence, schemaMapping, result);
384
+ // Handle OWNED BY option: schema name is in the name list
385
+ if (node.options) {
386
+ for (const opt of node.options) {
387
+ if (opt?.DefElem?.defname === 'owned_by' && opt.DefElem?.arg?.List?.items) {
388
+ transformNameList(opt.DefElem.arg.List.items, schemaMapping, result);
389
+ }
390
+ }
391
+ }
392
+ },
393
+ // Transform CreatePolicyStmt (CREATE POLICY ... ON schema.table)
394
+ // Note: The walker does NOT auto-recurse into CreatePolicyStmt.table.
395
+ CreatePolicyStmt: (path) => {
396
+ transformRelation(path.node.table, schemaMapping, result);
397
+ },
398
+ // Transform RuleStmt (CREATE RULE ... ON schema.table)
399
+ // Note: The walker does NOT auto-recurse into RuleStmt.relation.
400
+ RuleStmt: (path) => {
401
+ transformRelation(path.node.relation, schemaMapping, result);
402
+ },
403
+ // Transform CopyStmt (COPY schema.table ...)
404
+ // Note: The walker does NOT auto-recurse into CopyStmt.relation.
405
+ CopyStmt: (path) => {
406
+ transformRelation(path.node.relation, schemaMapping, result);
407
+ },
408
+ // Transform ClusterStmt (CLUSTER schema.table USING index)
409
+ // Note: The walker does NOT auto-recurse into ClusterStmt.relation.
410
+ ClusterStmt: (path) => {
411
+ transformRelation(path.node.relation, schemaMapping, result);
412
+ },
413
+ // Transform VacuumRelation (VACUUM/ANALYZE schema.table)
414
+ // Note: The walker visits VacuumRelation but does NOT recurse into its relation.
415
+ VacuumRelation: (path) => {
416
+ transformRelation(path.node.relation, schemaMapping, result);
417
+ },
418
+ // Transform RefreshMatViewStmt (REFRESH MATERIALIZED VIEW schema.view)
419
+ // Note: The walker does NOT auto-recurse into RefreshMatViewStmt.relation.
420
+ RefreshMatViewStmt: (path) => {
421
+ transformRelation(path.node.relation, schemaMapping, result);
422
+ },
423
+ // Transform CreateTableAsStmt (CREATE MATERIALIZED VIEW schema.viewname AS ...)
424
+ // Note: The walker does NOT auto-recurse into CreateTableAsStmt.into.rel.
425
+ CreateTableAsStmt: (path) => {
426
+ transformRelation(path.node.into?.rel, schemaMapping, result);
427
+ },
428
+ // Transform TypeCast nodes (type casts like NULL::schema.type)
429
+ // Note: The walker visits TypeCast but does NOT recurse into the TypeName node.
430
+ TypeCast: (path) => {
431
+ const node = path.node;
432
+ if (node.typeName?.names) {
433
+ transformNameList(node.typeName.names, schemaMapping, result);
434
+ }
435
+ },
436
+ // Transform CreateFunctionStmt (CREATE FUNCTION schema.funcname)
437
+ // Also handles RETURNS [SETOF] schema.type via returnType.names
438
+ CreateFunctionStmt: (path) => {
439
+ const node = path.node;
440
+ transformNameList(node.funcname, schemaMapping, result);
441
+ // Transform the return type (e.g., RETURNS SETOF schema.tablename)
442
+ if (node.returnType?.names) {
443
+ transformNameList(node.returnType.names, schemaMapping, result);
444
+ }
445
+ // Transform parameter types (the walker does NOT auto-recurse into
446
+ // FunctionParameter.argType TypeName nodes)
447
+ if (Array.isArray(node.parameters)) {
448
+ for (const param of node.parameters) {
449
+ if (param?.FunctionParameter?.argType?.names) {
450
+ transformNameList(param.FunctionParameter.argType.names, schemaMapping, result);
451
+ }
452
+ }
453
+ }
454
+ // Transform schema-qualified references inside the function body
455
+ // (AS $$...$$ is a String under the 'as' DefElem, which the walker
456
+ // does not parse as SQL). LANGUAGE plpgsql bodies are additionally
457
+ // rebuilt from the PL/pgSQL AST by transformSync; for LANGUAGE sql
458
+ // functions this string is the deparsed body.
459
+ if (Array.isArray(node.options)) {
460
+ for (const opt of node.options) {
461
+ if (opt?.DefElem?.defname === 'as' && opt.DefElem.arg?.List?.items) {
462
+ 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);
465
+ }
466
+ }
467
+ }
468
+ }
469
+ }
470
+ },
471
+ // Transform CreateTrigStmt (CREATE TRIGGER ... EXECUTE PROCEDURE schema.func)
472
+ // Note: The walker does NOT auto-recurse into CreateTrigStmt.relation,
473
+ // so we must explicitly transform the RangeVar here.
474
+ CreateTrigStmt: (path) => {
475
+ const node = path.node;
476
+ transformNameList(node.funcname, schemaMapping, result);
477
+ transformRelation(node.relation, schemaMapping, result);
478
+ },
479
+ // Transform CommentStmt (COMMENT ON TABLE/COLUMN/FUNCTION schema.obj,
480
+ // COMMENT ON SCHEMA schema)
481
+ CommentStmt: (path) => {
482
+ const node = path.node;
483
+ // The object field structure varies by objtype.
484
+ // For tables/views/etc, object is { List: { items: [{ String: ... }, ...] } }
485
+ // For functions, object is { ObjectWithArgs: { objname: [...] } }
486
+ // For schemas, object is a bare { String: { sval } }
487
+ if (node.object) {
488
+ // Handle List-wrapped objects (COMMENT ON TABLE schema.tbl, COMMENT ON COLUMN schema.tbl.col)
489
+ if (node.object?.List?.items) {
490
+ transformNameList(node.object.List.items, schemaMapping, result);
491
+ }
492
+ // Handle ObjectWithArgs-style objects (COMMENT ON FUNCTION schema.func(args))
493
+ else if (node.object?.ObjectWithArgs?.objname) {
494
+ transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result);
495
+ }
496
+ // Handle bare schema names (COMMENT ON SCHEMA schema)
497
+ else if (node.object?.String && node.objtype === 'OBJECT_SCHEMA') {
498
+ transformSchemaNameField(node.object.String, 'sval', schemaMapping, result);
499
+ }
500
+ }
501
+ // Transform schema-qualified references inside the comment text itself
502
+ if (typeof node.comment === 'string') {
503
+ node.comment = transformSchemaRefsInString(node.comment, schemaMapping, result);
504
+ }
505
+ },
506
+ // Transform schema-qualified references embedded in string constants
507
+ // (e.g. advisory-lock keys: hashtextextended('schema.fn_name', 0))
508
+ A_Const: (path) => {
509
+ const node = path.node;
510
+ if (typeof node.sval?.sval === 'string' && node.sval.sval.includes('.')) {
511
+ node.sval.sval = transformSchemaRefsInString(node.sval.sval, schemaMapping, result);
512
+ }
513
+ },
514
+ // Transform DefineStmt (CREATE TYPE schema.name, CREATE AGGREGATE schema.agg)
515
+ DefineStmt: (path) => {
516
+ const node = path.node;
517
+ transformNameList(node.defnames, schemaMapping, result);
518
+ },
519
+ // Transform CreateDomainStmt (CREATE DOMAIN schema.domname)
520
+ CreateDomainStmt: (path) => {
521
+ const node = path.node;
522
+ transformNameList(node.domainname, schemaMapping, result);
523
+ },
524
+ // Transform CreateEnumStmt (CREATE TYPE schema.enumname AS ENUM)
525
+ CreateEnumStmt: (path) => {
526
+ const node = path.node;
527
+ transformNameList(node.typeName, schemaMapping, result);
528
+ },
529
+ // Transform AlterEnumStmt (ALTER TYPE schema.enumname ADD VALUE)
530
+ AlterEnumStmt: (path) => {
531
+ const node = path.node;
532
+ transformNameList(node.typeName, schemaMapping, result);
533
+ },
534
+ // Transform AlterDomainStmt (ALTER DOMAIN schema.domname)
535
+ AlterDomainStmt: (path) => {
536
+ const node = path.node;
537
+ transformNameList(node.typeName, schemaMapping, result);
538
+ },
539
+ // Transform AlterTypeStmt (ALTER TYPE schema.typename)
540
+ // Note: composite type alters use RangeVar (handled by RangeVar visitor),
541
+ // but some ALTER TYPE statements use typeName as a name list
542
+ AlterTypeStmt: (path) => {
543
+ const node = path.node;
544
+ transformNameList(node.typeName, schemaMapping, result);
545
+ },
546
+ // Transform ObjectWithArgs (used in ALTER FUNCTION, DROP FUNCTION with args, etc.)
547
+ ObjectWithArgs: (path) => {
548
+ const node = path.node;
549
+ transformNameList(node.objname, schemaMapping, result);
550
+ },
551
+ // Transform AlterObjectSchemaStmt (ALTER ... SET SCHEMA newschema)
552
+ // The walker does NOT auto-recurse into AlterObjectSchemaStmt.relation
553
+ // or .object, so we must transform relation, object names, and newschema.
554
+ AlterObjectSchemaStmt: (path) => {
555
+ const node = path.node;
556
+ transformRelation(node.relation, schemaMapping, result);
557
+ // Non-relation objects (ALTER TYPE/FUNCTION ... SET SCHEMA)
558
+ if (node.object?.List?.items) {
559
+ transformNameList(node.object.List.items, schemaMapping, result);
560
+ }
561
+ else if (node.object?.ObjectWithArgs?.objname) {
562
+ transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result);
563
+ }
564
+ else if (node.object?.TypeName?.names) {
565
+ transformNameList(node.object.TypeName.names, schemaMapping, result);
566
+ }
567
+ // Transform the newschema (destination schema)
568
+ transformSchemaNameField(node, 'newschema', schemaMapping, result);
569
+ },
570
+ // Transform RenameStmt (ALTER SCHEMA old RENAME TO new;
571
+ // ALTER TABLE schema.tbl RENAME ...). The walker does NOT auto-recurse
572
+ // into RenameStmt.relation or .object.
573
+ RenameStmt: (path) => {
574
+ const node = path.node;
575
+ transformRelation(node.relation, schemaMapping, result);
576
+ if (node.renameType === 'OBJECT_SCHEMA') {
577
+ // subname is the old schema name; newname is the target
578
+ transformSchemaNameField(node, 'subname', schemaMapping, result);
579
+ transformSchemaNameField(node, 'newname', schemaMapping, result);
580
+ }
581
+ if (node.object?.List?.items) {
582
+ transformNameList(node.object.List.items, schemaMapping, result);
583
+ }
584
+ else if (node.object?.ObjectWithArgs?.objname) {
585
+ transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result);
586
+ }
587
+ },
588
+ // Transform AlterFunctionStmt (ALTER FUNCTION schema.fn(...) SET ...)
589
+ AlterFunctionStmt: (path) => {
590
+ const node = path.node;
591
+ if (node.func?.objname) {
592
+ transformNameList(node.func.objname, schemaMapping, result);
593
+ }
594
+ },
595
+ // Transform AlterOwnerStmt (ALTER ... OWNER TO role)
596
+ AlterOwnerStmt: (path) => {
597
+ const node = path.node;
598
+ transformRelation(node.relation, schemaMapping, result);
599
+ if (node.object?.List?.items) {
600
+ transformNameList(node.object.List.items, schemaMapping, result);
601
+ }
602
+ else if (node.object?.ObjectWithArgs?.objname) {
603
+ transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result);
604
+ }
605
+ else if (node.object?.String && node.objectType === 'OBJECT_SCHEMA') {
606
+ transformSchemaNameField(node.object.String, 'sval', schemaMapping, result);
607
+ }
608
+ },
609
+ // Transform CreateCastStmt (CREATE CAST (src AS tgt) WITH FUNCTION fn)
610
+ CreateCastStmt: (path) => {
611
+ const node = path.node;
612
+ if (node.sourcetype?.names) {
613
+ transformNameList(node.sourcetype.names, schemaMapping, result);
614
+ }
615
+ if (node.targettype?.names) {
616
+ transformNameList(node.targettype.names, schemaMapping, result);
617
+ }
618
+ if (node.func?.objname) {
619
+ transformNameList(node.func.objname, schemaMapping, result);
620
+ }
621
+ if (Array.isArray(node.func?.objargs)) {
622
+ for (const arg of node.func.objargs) {
623
+ if (arg?.TypeName?.names) {
624
+ transformNameList(arg.TypeName.names, schemaMapping, result);
625
+ }
626
+ else if (arg?.names) {
627
+ transformNameList(arg.names, schemaMapping, result);
628
+ }
629
+ }
630
+ }
631
+ if (Array.isArray(node.func?.objfuncargs)) {
632
+ for (const param of node.func.objfuncargs) {
633
+ if (param?.FunctionParameter?.argType?.names) {
634
+ transformNameList(param.FunctionParameter.argType.names, schemaMapping, result);
635
+ }
636
+ }
637
+ }
638
+ },
639
+ // Transform CreateEventTrigStmt (CREATE EVENT TRIGGER ... EXECUTE FUNCTION schema.fn())
640
+ CreateEventTrigStmt: (path) => {
641
+ const node = path.node;
642
+ transformNameList(node.funcname, schemaMapping, result);
643
+ },
644
+ // Transform IndexElem opclass (CREATE INDEX ... (col schema.opclass))
645
+ IndexElem: (path) => {
646
+ const node = path.node;
647
+ transformNameList(node.opclass, schemaMapping, result);
648
+ },
649
+ // Transform SecLabelStmt object (SECURITY LABEL ... ON COLUMN schema.tbl.col)
650
+ SecLabelStmt: (path) => {
651
+ const node = path.node;
652
+ if (node.object?.List?.items) {
653
+ transformNameList(node.object.List.items, schemaMapping, result);
654
+ }
655
+ else if (node.object?.ObjectWithArgs?.objname) {
656
+ transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result);
657
+ }
658
+ },
659
+ // Transform DO block bodies. The body is an opaque string under the 'as'
660
+ // DefElem; schema-qualified references are rewritten with the strict
661
+ // schema-followed-by-dot pattern.
662
+ DoStmt: (path) => {
663
+ const node = path.node;
664
+ if (Array.isArray(node.args)) {
665
+ for (const arg of node.args) {
666
+ if (arg?.DefElem?.defname === 'as' && typeof arg.DefElem.arg?.String?.sval === 'string') {
667
+ arg.DefElem.arg.String.sval = transformSchemaRefsInString(arg.DefElem.arg.String.sval, schemaMapping, result);
668
+ }
669
+ }
670
+ }
671
+ },
672
+ };
673
+ }
674
+ /**
675
+ * Validate that no untransformed schema names remain in the output.
676
+ * Checks both schema-qualified references (schema.object) and standalone
677
+ * schema name contexts (ON SCHEMA, IN SCHEMA, CREATE SCHEMA, etc.).
678
+ *
679
+ * Throws an error if any schema names from the mapping are found in the output,
680
+ * indicating that the AST visitor is missing a handler for that node type.
681
+ */
682
+ export function validateNoUntransformedSchemas(content, schemaMapping) {
683
+ if (schemaMapping.size === 0) {
684
+ return;
685
+ }
686
+ for (const [oldSchema, newSchema] of schemaMapping) {
687
+ const escapedSchema = escapeRegexp(oldSchema);
688
+ // Pattern 1: quoted or unquoted schema name followed by dot (schema-qualified)
689
+ const dotPattern = new RegExp(`(?:"${escapedSchema}"|\\b${escapedSchema})(?=\\.)`, 'g');
690
+ // Pattern 2: standalone schema name in known SQL contexts
691
+ // Note: We don't use trailing \b because it fails after closing quotes
692
+ // (both '"' and whitespace are non-word characters, so no boundary exists).
693
+ const standalonePattern = new RegExp(`(?:ON\\s+SCHEMA\\s+|IN\\s+SCHEMA\\s+|CREATE\\s+SCHEMA\\s+|DROP\\s+SCHEMA\\s+(?:IF\\s+EXISTS\\s+)?|SET\\s+SCHEMA\\s+)` +
694
+ `(?:"${escapedSchema}"|\\b${escapedSchema}\\b)`, 'gi');
695
+ const dotMatches = content.match(dotPattern);
696
+ const standaloneMatches = content.match(standalonePattern);
697
+ const totalMatches = (dotMatches?.length || 0) + (standaloneMatches?.length || 0);
698
+ if (totalMatches > 0) {
699
+ const lines = content.split('\n');
700
+ const locations = [];
701
+ const combinedPattern = new RegExp(`(?:"${escapedSchema}"|\\b${escapedSchema})(?=\\.)|` +
702
+ `(?:ON\\s+SCHEMA\\s+|IN\\s+SCHEMA\\s+|CREATE\\s+SCHEMA\\s+|DROP\\s+SCHEMA\\s+(?:IF\\s+EXISTS\\s+)?|SET\\s+SCHEMA\\s+)` +
703
+ `(?:"${escapedSchema}"|\\b${escapedSchema}\\b)`, 'gi');
704
+ for (let i = 0; i < lines.length; i++) {
705
+ if (combinedPattern.test(lines[i])) {
706
+ locations.push(` Line ${i + 1}: ${lines[i].trim()}`);
707
+ }
708
+ combinedPattern.lastIndex = 0;
709
+ }
710
+ throw new Error(`AST transformation incomplete: found ${totalMatches} untransformed schema name(s) "${oldSchema}" ` +
711
+ `that should have been transformed to "${newSchema}". ` +
712
+ `This indicates a missing visitor handler in create_sql_visitor or walk_plpgsql_for_schemas.\n` +
713
+ `Locations:\n${locations.join('\n')}`);
714
+ }
715
+ }
716
+ }
717
+ /**
718
+ * Create a PL/pgSQL visitor that transforms schema names in PL/pgSQL-specific nodes.
719
+ */
720
+ export function createPlpgsqlVisitor(schemaMapping, result) {
721
+ return {
722
+ PLpgSQL_type: (path) => {
723
+ const node = path.node;
724
+ if (node.typname) {
725
+ for (const [oldSchema, newSchema] of schemaMapping.entries()) {
726
+ if (node.typname.startsWith(oldSchema + '.')) {
727
+ const typeName = node.typname.substring(oldSchema.length + 1);
728
+ result.schemasFound.add(oldSchema);
729
+ node.typname = newSchema + '.' + typeName;
730
+ result.schemasTransformed.set(oldSchema, newSchema);
731
+ break;
732
+ }
733
+ }
734
+ }
735
+ },
736
+ PLpgSQL_var: (path) => {
737
+ const node = path.node;
738
+ if (node.refname) {
739
+ for (const [oldSchema, newSchema] of schemaMapping.entries()) {
740
+ if (node.refname.startsWith(oldSchema + '.')) {
741
+ const rest = node.refname.substring(oldSchema.length + 1);
742
+ result.schemasFound.add(oldSchema);
743
+ node.refname = newSchema + '.' + rest;
744
+ result.schemasTransformed.set(oldSchema, newSchema);
745
+ break;
746
+ }
747
+ }
748
+ }
749
+ },
750
+ };
751
+ }
752
+ /**
753
+ * Transform a PLpgSQL_type typname using proper AST parsing.
754
+ */
755
+ export function transformPlpgsqlTypeAst(typname, schemaMapping, result) {
756
+ let suffix = '';
757
+ let baseTypname = typname;
758
+ const rowtypeMatch = typname.match(/(%rowtype|%type)$/i);
759
+ if (rowtypeMatch) {
760
+ suffix = rowtypeMatch[1];
761
+ baseTypname = typname.substring(0, typname.length - suffix.length);
762
+ }
763
+ let needsTransform = false;
764
+ for (const oldSchema of schemaMapping.keys()) {
765
+ if (baseTypname.startsWith(oldSchema + '.') || baseTypname.startsWith('"' + oldSchema + '".')) {
766
+ needsTransform = true;
767
+ break;
768
+ }
769
+ }
770
+ if (!needsTransform) {
771
+ return typname;
772
+ }
773
+ try {
774
+ const sql = `SELECT NULL::${baseTypname}`;
775
+ const parseResult = parseSql(sql);
776
+ if (!parseResult?.stmts?.[0]?.stmt) {
777
+ return transformPlpgsqlTypeString(typname, schemaMapping, result);
778
+ }
779
+ const sqlVisitor = {
780
+ TypeName: (path) => {
781
+ const typeNode = path.node;
782
+ if (typeNode.names && Array.isArray(typeNode.names)) {
783
+ transformNameList(typeNode.names, schemaMapping, result);
784
+ }
785
+ }
786
+ };
787
+ walkSql(parseResult.stmts[0].stmt, sqlVisitor);
788
+ const deparsed = Deparser.deparse(parseResult.stmts[0].stmt);
789
+ const match = deparsed.match(/SELECT\s+NULL::(.+)/i);
790
+ if (match) {
791
+ const transformedTypname = match[1].trim().replace(/;$/, '');
792
+ return transformedTypname + suffix;
793
+ }
794
+ return transformPlpgsqlTypeString(typname, schemaMapping, result);
795
+ }
796
+ catch {
797
+ return transformPlpgsqlTypeString(typname, schemaMapping, result);
798
+ }
799
+ }
800
+ /**
801
+ * Fallback string-based transformation for PLpgSQL_type typname.
802
+ * Uses @pgsql/quotes QuoteUtils for proper identifier quoting.
803
+ */
804
+ export function transformPlpgsqlTypeString(typname, schemaMapping, result) {
805
+ for (const [oldSchema, newSchema] of schemaMapping.entries()) {
806
+ // Unquoted schema: old_schema.rest
807
+ if (typname.startsWith(oldSchema + '.')) {
808
+ const rest = typname.substring(oldSchema.length + 1);
809
+ result.schemasFound.add(oldSchema);
810
+ result.schemasTransformed.set(oldSchema, newSchema);
811
+ return QuoteUtils.quoteIdentifier(newSchema) + '.' + rest;
812
+ }
813
+ // Quoted schema: "old_schema".rest
814
+ if (typname.startsWith('"' + oldSchema + '".')) {
815
+ const rest = typname.substring(oldSchema.length + 3);
816
+ result.schemasFound.add(oldSchema);
817
+ result.schemasTransformed.set(oldSchema, newSchema);
818
+ return QuoteUtils.quoteIdentifier(newSchema) + '.' + rest;
819
+ }
820
+ }
821
+ return typname;
822
+ }
823
+ /**
824
+ * Recursively walk the PL/pgSQL AST to transform schema names.
825
+ */
826
+ export function walkPlpgsqlForSchemas(node, schemaMapping, result) {
827
+ if (node === null || node === undefined || typeof node !== 'object') {
828
+ return;
829
+ }
830
+ if (Array.isArray(node)) {
831
+ for (let i = 0; i < node.length; i++) {
832
+ walkPlpgsqlForSchemas(node[i], schemaMapping, result);
833
+ }
834
+ return;
835
+ }
836
+ // Fallback for SQL expressions the plpgsql walker does not reach with the
837
+ // SQL visitor (e.g. statements inside EXCEPTION handler bodies). The
838
+ // deparser renders exprs from their query string, so rewriting mapped
839
+ // schema-followed-by-dot references here is safe and idempotent.
840
+ if ('PLpgSQL_expr' in node) {
841
+ const expr = node.PLpgSQL_expr;
842
+ if (typeof expr.query === 'string' && expr.query.includes('.')) {
843
+ expr.query = transformSchemaRefsInString(expr.query, schemaMapping, result);
844
+ }
845
+ else if (expr.query && typeof expr.query === 'object' && expr.query.kind === 'sql-stmt') {
846
+ // Hydrated exprs carry { original, parseResult }; the deparser renders
847
+ // from parseResult, so visit its statements with the SQL visitor.
848
+ const sqlVisitor = createSqlVisitor(schemaMapping, result);
849
+ if (Array.isArray(expr.query.parseResult?.stmts)) {
850
+ for (const stmt of expr.query.parseResult.stmts) {
851
+ if (stmt?.stmt) {
852
+ walkSql(stmt.stmt, sqlVisitor);
853
+ }
854
+ }
855
+ }
856
+ if (typeof expr.query.original === 'string' && expr.query.original.includes('.')) {
857
+ expr.query.original = transformSchemaRefsInString(expr.query.original, schemaMapping, result);
858
+ }
859
+ }
860
+ }
861
+ // Transform schema-qualified references inside RAISE message strings
862
+ if ('PLpgSQL_stmt_raise' in node) {
863
+ const raise = node.PLpgSQL_stmt_raise;
864
+ if (typeof raise.message === 'string' && raise.message.includes('.')) {
865
+ raise.message = transformSchemaRefsInString(raise.message, schemaMapping, result);
866
+ }
867
+ }
868
+ if ('PLpgSQL_type' in node) {
869
+ const plType = node.PLpgSQL_type;
870
+ if (plType.typname) {
871
+ if (typeof plType.typname === 'object' && plType.typname.kind === 'type-name') {
872
+ // Transform schema names directly in the typeNameNode.names array.
873
+ // We cannot use walkSql here because the raw typeNameNode is not
874
+ // wrapped in the expected AST envelope that the traverse walker needs.
875
+ if (plType.typname.typeNameNode?.names) {
876
+ transformNameList(plType.typname.typeNameNode.names, schemaMapping, result);
877
+ }
878
+ // The deparser can fall back to the 'original' string to render
879
+ // DECLARE types, so update it as well. Rewrite only the schema
880
+ // prefix within the string so array bounds ([]) and %rowtype/%type
881
+ // suffixes are preserved.
882
+ if (typeof plType.typname.original === 'string') {
883
+ plType.typname.original = transformPlpgsqlTypeString(plType.typname.original, schemaMapping, result);
884
+ }
885
+ }
886
+ else if (typeof plType.typname === 'string') {
887
+ plType.typname = transformPlpgsqlTypeAst(plType.typname, schemaMapping, result);
888
+ }
889
+ }
890
+ }
891
+ for (const value of Object.values(node)) {
892
+ walkPlpgsqlForSchemas(value, schemaMapping, result);
893
+ }
894
+ }
895
+ /**
896
+ * Escape a string for use in a regular expression
897
+ */
898
+ export function escapeRegexp(str) {
899
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
900
+ }
901
+ /**
902
+ * Extract pgpm header comments from the beginning of SQL content.
903
+ */
904
+ export function extractPgpmHeader(content) {
905
+ const lines = content.split('\n');
906
+ let headerEndIndex = 0;
907
+ for (let i = 0; i < lines.length; i++) {
908
+ const line = lines[i];
909
+ const trimmed = line.trim();
910
+ if (trimmed === '') {
911
+ headerEndIndex = i + 1;
912
+ continue;
913
+ }
914
+ if (trimmed === '--') {
915
+ headerEndIndex = i + 1;
916
+ continue;
917
+ }
918
+ if (trimmed.startsWith('-- Deploy') ||
919
+ trimmed.startsWith('-- Revert') ||
920
+ trimmed.startsWith('-- Verify') ||
921
+ trimmed.startsWith('-- requires:') ||
922
+ trimmed.startsWith('-- made with')) {
923
+ headerEndIndex = i + 1;
924
+ continue;
925
+ }
926
+ if (trimmed.startsWith('--')) {
927
+ headerEndIndex = i + 1;
928
+ continue;
929
+ }
930
+ // psql meta-command guard emitted by pgpm package exports, e.g.
931
+ // \echo Use "CREATE EXTENSION foo" to load this file. \quit
932
+ if (trimmed.startsWith('\\echo')) {
933
+ headerEndIndex = i + 1;
934
+ continue;
935
+ }
936
+ break;
937
+ }
938
+ const headerLines = lines.slice(0, headerEndIndex);
939
+ const bodyLines = lines.slice(headerEndIndex);
940
+ return {
941
+ header: headerLines.join('\n') + (headerLines.length > 0 ? '\n' : ''),
942
+ body: bodyLines.join('\n')
943
+ };
944
+ }
945
+ /**
946
+ * Transform comment headers (-- Deploy:, -- requires:, etc.)
947
+ * These are not part of the SQL AST, so we use regexp for these.
948
+ */
949
+ export function transformComments(content, schemaMapping, result) {
950
+ const commentPattern = /^(-- (?:Deploy|requires|Revert|Verify):?\s*)(.*)$/gm;
951
+ const schemas = Array.from(schemaMapping.keys()).sort((a, b) => b.length - a.length);
952
+ return content.replace(commentPattern, (match, prefix, pathPart) => {
953
+ let newPath = pathPart;
954
+ for (const schema of schemas) {
955
+ const newName = schemaMapping.get(schema);
956
+ if (!newName)
957
+ continue;
958
+ result.schemasFound.add(schema);
959
+ const pathPattern = new RegExp(`(schemas/)${escapeRegexp(schema)}(/|$)`, 'g');
960
+ const before = newPath;
961
+ newPath = newPath.replace(pathPattern, `$1${newName}$2`);
962
+ if (newPath !== before) {
963
+ result.schemasTransformed.set(schema, newName);
964
+ }
965
+ }
966
+ return prefix + newPath;
967
+ });
968
+ }
969
+ /**
970
+ * Transform verify function calls that use string literals.
971
+ * These are inside SQL strings and not part of the main AST.
972
+ */
973
+ export function transformVerifyCalls(content, schemaMapping, result) {
974
+ const schemas = Array.from(schemaMapping.keys()).sort((a, b) => b.length - a.length);
975
+ let newContent = content;
976
+ for (const schema of schemas) {
977
+ const newName = schemaMapping.get(schema);
978
+ if (!newName)
979
+ continue;
980
+ const escapedSchema = escapeRegexp(schema);
981
+ const verifyPattern = new RegExp(`(verify_(?:function|table|trigger|type|domain|view|index|constraint|schema|policy|table_grant|function_grant|sequence_grant|type_grant)\\s*\\(\\s*')${escapedSchema}(\\.|'\\s*\\))`, 'gi');
982
+ const before = newContent;
983
+ newContent = newContent.replace(verifyPattern, `$1${newName}$2`);
984
+ if (newContent !== before) {
985
+ result.schemasFound.add(schema);
986
+ result.schemasTransformed.set(schema, newName);
987
+ }
988
+ }
989
+ return newContent;
990
+ }
991
+ /**
992
+ * Transform schema names inside JSON/JSONB string values.
993
+ */
994
+ export function transformJsonStringValues(content, schemaMapping, result) {
995
+ const schemas = Array.from(schemaMapping.keys()).sort((a, b) => b.length - a.length);
996
+ let newContent = content;
997
+ for (const schema of schemas) {
998
+ const newName = schemaMapping.get(schema);
999
+ if (!newName)
1000
+ continue;
1001
+ const escapedSchema = escapeRegexp(schema);
1002
+ const jsonValuePattern = new RegExp(`(:")${escapedSchema}(")`, 'g');
1003
+ const before = newContent;
1004
+ newContent = newContent.replace(jsonValuePattern, `$1${newName}$2`);
1005
+ if (newContent !== before) {
1006
+ result.schemasFound.add(schema);
1007
+ result.schemasTransformed.set(schema, newName);
1008
+ }
1009
+ }
1010
+ return newContent;
1011
+ }
1012
+ /**
1013
+ * Transform a single SQL string using full AST-based transformation.
1014
+ *
1015
+ * This is the main entry point for transforming SQL content. It:
1016
+ * 1. Runs any user-supplied pre-passes (string-level)
1017
+ * 2. Extracts pgpm header comments and transforms them
1018
+ * 3. Runs full AST transformation on the SQL body
1019
+ * 4. Runs any user-supplied post-passes (string-level)
1020
+ * 5. Validates no untransformed schema names remain
1021
+ * 6. Returns the combined result
1022
+ *
1023
+ * App-specific string-level transforms (verify calls, JSON values, etc.)
1024
+ * are NOT included by default — pass them via `options.pre_passes` or
1025
+ * `options.post_passes`. The built-in passes `transform_verify_calls`
1026
+ * and `transform_json_string_values` are exported for convenience.
1027
+ */
1028
+ export function transformSql(content, schemaMapping, options, result) {
1029
+ // Support legacy signature: transform_sql(content, mapping, result?)
1030
+ let opts = {};
1031
+ let r;
1032
+ if (options && 'schemasFound' in options) {
1033
+ // Called with legacy signature: (content, mapping, result)
1034
+ r = options;
1035
+ }
1036
+ else {
1037
+ opts = options || {};
1038
+ r = result || createResult();
1039
+ }
1040
+ if (schemaMapping.size === 0) {
1041
+ return { content, result: r };
1042
+ }
1043
+ let newContent = content;
1044
+ // Run pre-passes (app-specific string-level transforms)
1045
+ if (opts.prePasses) {
1046
+ for (const pass of opts.prePasses) {
1047
+ newContent = pass(newContent, schemaMapping, r);
1048
+ }
1049
+ }
1050
+ // Main pass: AST transformation with header handling
1051
+ newContent = transformSqlContentAst(newContent, schemaMapping, r, opts);
1052
+ // Run post-passes (app-specific string-level transforms)
1053
+ if (opts.postPasses) {
1054
+ for (const pass of opts.postPasses) {
1055
+ newContent = pass(newContent, schemaMapping, r);
1056
+ }
1057
+ }
1058
+ return { content: newContent, result: r };
1059
+ }
1060
+ /**
1061
+ * Transform a single SQL statement string using AST.
1062
+ * Unlike transform_sql, this does NOT handle headers, verify calls, or JSON values.
1063
+ * Use this for testing individual SQL statements.
1064
+ */
1065
+ export function transformSqlStatement(sql, schemaMapping, result) {
1066
+ const r = result || createResult();
1067
+ if (schemaMapping.size === 0) {
1068
+ return { sql, result: r };
1069
+ }
1070
+ try {
1071
+ const transformed = transformSync(sql, (ctx) => {
1072
+ const sqlVisitor = createSqlVisitor(schemaMapping, r);
1073
+ if (ctx.sql?.stmts) {
1074
+ for (const stmt of ctx.sql.stmts) {
1075
+ if (stmt?.stmt) {
1076
+ walkSql(stmt.stmt, sqlVisitor);
1077
+ }
1078
+ }
1079
+ }
1080
+ for (const fn of ctx.functions) {
1081
+ if (fn.plpgsql?.hydrated) {
1082
+ walkPlpgsql(fn.plpgsql.hydrated, {}, {
1083
+ walkSqlExpressions: true,
1084
+ sqlVisitor: sqlVisitor
1085
+ });
1086
+ walkPlpgsqlForSchemas(fn.plpgsql.hydrated, schemaMapping, r);
1087
+ }
1088
+ }
1089
+ }, { hydrate: true, pretty: true });
1090
+ return { sql: transformed, result: r };
1091
+ }
1092
+ catch (error) {
1093
+ const errMsg = error instanceof Error ? error.message : String(error);
1094
+ r.errors.push({ file: 'inline', error: errMsg });
1095
+ throw error;
1096
+ }
1097
+ }
1098
+ /**
1099
+ * Internal: Transform SQL content with header extraction and AST transformation.
1100
+ */
1101
+ function transformSqlContentAst(content, schemaMapping, result, options) {
1102
+ if (schemaMapping.size === 0) {
1103
+ return content;
1104
+ }
1105
+ const roundTrip = options?.roundTrip;
1106
+ const assumeSchemasExist = options?.assumeSchemasExist?.length
1107
+ ? new Set(options.assumeSchemasExist)
1108
+ : undefined;
1109
+ const { header, body } = extractPgpmHeader(content);
1110
+ let transformedHeader = header;
1111
+ if (header.length > 0) {
1112
+ transformedHeader = transformComments(header, schemaMapping, result);
1113
+ }
1114
+ let transformedBody = body;
1115
+ if (options?.qualifyUnqualified && transformedBody.trim().length > 0) {
1116
+ try {
1117
+ transformedBody = qualifyUnqualified(transformedBody, options.qualifyUnqualified).sql;
1118
+ }
1119
+ catch (error) {
1120
+ const errMsg = error instanceof Error ? error.message : String(error);
1121
+ result.errors.push({ file: 'inline', error: errMsg });
1122
+ throw error;
1123
+ }
1124
+ }
1125
+ if (transformedBody.trim().length > 0) {
1126
+ let before;
1127
+ try {
1128
+ transformedBody = transformSync(transformedBody, (ctx) => {
1129
+ const sqlVisitor = createSqlVisitor(schemaMapping, result, { assumeSchemasExist });
1130
+ if (ctx.sql?.stmts) {
1131
+ for (const stmt of ctx.sql.stmts) {
1132
+ if (stmt?.stmt) {
1133
+ walkSql(stmt.stmt, sqlVisitor);
1134
+ }
1135
+ }
1136
+ }
1137
+ for (const fn of ctx.functions) {
1138
+ if (fn.plpgsql?.hydrated) {
1139
+ walkPlpgsql(fn.plpgsql.hydrated, {}, {
1140
+ walkSqlExpressions: true,
1141
+ sqlVisitor: sqlVisitor
1142
+ });
1143
+ walkPlpgsqlForSchemas(fn.plpgsql.hydrated, schemaMapping, result);
1144
+ }
1145
+ }
1146
+ if (roundTrip) {
1147
+ before = captureTransformAsts(ctx);
1148
+ }
1149
+ }, { hydrate: true, pretty: true });
1150
+ }
1151
+ catch (error) {
1152
+ const errMsg = error instanceof Error ? error.message : String(error);
1153
+ result.errors.push({ file: 'inline', error: errMsg });
1154
+ throw error;
1155
+ }
1156
+ validateNoUntransformedSchemas(transformedBody, schemaMapping);
1157
+ if (roundTrip && before) {
1158
+ try {
1159
+ validateRoundTrip(before, transformedBody);
1160
+ }
1161
+ catch (error) {
1162
+ const errMsg = error instanceof Error ? error.message : String(error);
1163
+ result.errors.push({ file: 'inline', error: errMsg });
1164
+ throw error;
1165
+ }
1166
+ }
1167
+ }
1168
+ return transformedHeader + transformedBody;
1169
+ }