@pgsql/transform 18.15.2 → 18.16.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/index.js +1 -1
- package/esm/transform.js +163 -10
- package/index.d.ts +1 -1
- package/index.js +3 -1
- package/package.json +2 -2
- package/transform.d.ts +14 -0
- package/transform.js +165 -10
package/esm/index.js
CHANGED
|
@@ -10,4 +10,4 @@ export { createRoleResult, createRoleVisitor, transformRoles, } from './role-tra
|
|
|
10
10
|
export { captureAstsFromSql, captureTransformAsts, firstDifference, normalizeParseTree, validateRoundTrip, } from './round-trip';
|
|
11
11
|
export { CLEAN_TREE_VOLATILE_KEYS, cleanTree, normalizeTree, trimDefElemBody, } from './round-trip-core';
|
|
12
12
|
export { SchemaRouter } from './router';
|
|
13
|
-
export { createPlpgsqlVisitor, createSqlVisitor, escapeRegexp, extractPgpmHeader, shouldTransformSchema, transformComments, transformJsonStringValues, transformNameList, transformPlpgsqlTypeAst, transformPlpgsqlTypeString, transformRelation, transformSchemaName, transformSql, transformSqlStatement, transformVerifyCalls, validateNoUntransformedSchemas, walkPlpgsqlForSchemas, } from './transform';
|
|
13
|
+
export { createPlpgsqlVisitor, createSqlVisitor, escapeRegexp, extractPgpmHeader, identityCastNamespace, shouldTransformSchema, transformComments, transformIdentityCastLiteral, transformJsonStringValues, transformNameList, transformPlpgsqlTypeAst, transformPlpgsqlTypeString, transformRelation, transformSchemaName, transformSql, transformSqlStatement, transformVerifyCalls, validateNoUntransformedSchemas, walkPlpgsqlForSchemas, } from './transform';
|
package/esm/transform.js
CHANGED
|
@@ -197,7 +197,9 @@ export function transformSchemaRefsInString(str, schemaMapping, result) {
|
|
|
197
197
|
// References embedded in opaque strings carry no object identity, so only
|
|
198
198
|
// whole-schema (schema-level default) routes can be applied here.
|
|
199
199
|
for (const [oldSchema, newSchema] of schemaLevelMap(schemaMapping)) {
|
|
200
|
-
|
|
200
|
+
if (!out.includes(oldSchema))
|
|
201
|
+
continue;
|
|
202
|
+
const pattern = cachedRegExp(`(?<![\\w-])("?)${cachedEscapeRegexp(oldSchema)}\\1(?=\\.)`, 'g');
|
|
201
203
|
const before = out;
|
|
202
204
|
out = out.replace(pattern, `$1${newSchema}$1`);
|
|
203
205
|
if (out !== before) {
|
|
@@ -207,6 +209,98 @@ export function transformSchemaRefsInString(str, schemaMapping, result) {
|
|
|
207
209
|
}
|
|
208
210
|
return out;
|
|
209
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Cast types whose string operand is an object reference rather than opaque
|
|
214
|
+
* text. `'sch.tbl'::regclass` is a relation the parser resolves, and the cast
|
|
215
|
+
* names the namespace it resolves in — so the reference can be routed from the
|
|
216
|
+
* cast, semantically, instead of pattern-matched. This reaches what the string
|
|
217
|
+
* passes cannot: a bare `'sch'::regnamespace` has no dot to match on.
|
|
218
|
+
*/
|
|
219
|
+
const IDENTITY_CAST_NAMESPACES = {
|
|
220
|
+
regclass: 'relation',
|
|
221
|
+
regcollation: 'unknown',
|
|
222
|
+
regconfig: 'unknown',
|
|
223
|
+
regdictionary: 'unknown',
|
|
224
|
+
regnamespace: 'schema',
|
|
225
|
+
regoper: 'unknown',
|
|
226
|
+
regoperator: 'unknown',
|
|
227
|
+
regproc: 'function',
|
|
228
|
+
regprocedure: 'function',
|
|
229
|
+
regtype: 'type'
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* The namespace a cast resolves its operand in, or `undefined` when the target
|
|
233
|
+
* type is not an object-identity type.
|
|
234
|
+
*/
|
|
235
|
+
export function identityCastNamespace(typeName) {
|
|
236
|
+
const names = typeName?.names;
|
|
237
|
+
if (!Array.isArray(names) || names.length === 0)
|
|
238
|
+
return undefined;
|
|
239
|
+
// Only `pg_catalog`-qualified (or unqualified) reg* types are the built-ins.
|
|
240
|
+
if (names.length > 1 && names[0]?.String?.sval !== 'pg_catalog')
|
|
241
|
+
return undefined;
|
|
242
|
+
const typeIdent = names[names.length - 1]?.String?.sval;
|
|
243
|
+
if (typeof typeIdent !== 'string')
|
|
244
|
+
return undefined;
|
|
245
|
+
return IDENTITY_CAST_NAMESPACES[typeIdent.toLowerCase()];
|
|
246
|
+
}
|
|
247
|
+
const IDENTITY_TOKEN = String.raw `(?:"(?:[^"]|"")*"|[^\s".,()[\]]+)`;
|
|
248
|
+
const BARE_IDENTITY_RE = new RegExp(`^\\s*(${IDENTITY_TOKEN})\\s*$`);
|
|
249
|
+
const QUALIFIED_IDENTITY_RE = new RegExp(`^\\s*(${IDENTITY_TOKEN})\\.([\\s\\S]+)$`);
|
|
250
|
+
function unquoteIdentifier(token) {
|
|
251
|
+
if (token.length > 1 && token.startsWith('"') && token.endsWith('"')) {
|
|
252
|
+
return token.slice(1, -1).replace(/""/g, '"');
|
|
253
|
+
}
|
|
254
|
+
return token;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Route the object reference carried in the string operand of an identity
|
|
258
|
+
* cast. `ns` comes from the cast's target type.
|
|
259
|
+
*
|
|
260
|
+
* Unqualified operands (`'tbl'::regclass`) name no schema — they resolve
|
|
261
|
+
* through `search_path` — so they are returned unchanged.
|
|
262
|
+
*/
|
|
263
|
+
export function transformIdentityCastLiteral(sval, ns, schemaMapping, result) {
|
|
264
|
+
const router = asRouter(schemaMapping);
|
|
265
|
+
if (ns === 'schema') {
|
|
266
|
+
const bare = BARE_IDENTITY_RE.exec(sval);
|
|
267
|
+
if (!bare)
|
|
268
|
+
return sval;
|
|
269
|
+
const schemaName = unquoteIdentifier(bare[1]);
|
|
270
|
+
const newName = router.resolve(schemaName, undefined, 'schema');
|
|
271
|
+
if (!newName || newName === schemaName)
|
|
272
|
+
return sval;
|
|
273
|
+
result.schemasFound.add(schemaName);
|
|
274
|
+
result.schemasTransformed.set(schemaName, newName);
|
|
275
|
+
return QuoteUtils.quoteIdentifier(newName);
|
|
276
|
+
}
|
|
277
|
+
// `regprocedure` and `regoperator` carry an argument list after the name, and
|
|
278
|
+
// an argument type can itself be schema-qualified
|
|
279
|
+
// (`fn("my-schema".row_type)`), so the list is routed as ordinary schema refs.
|
|
280
|
+
const argsAt = sval.indexOf('(');
|
|
281
|
+
const head = argsAt === -1 ? sval : sval.slice(0, argsAt);
|
|
282
|
+
const args = argsAt === -1 ? '' : transformSchemaRefsInString(sval.slice(argsAt), router, result);
|
|
283
|
+
const qualified = QUALIFIED_IDENTITY_RE.exec(head);
|
|
284
|
+
if (!qualified)
|
|
285
|
+
return `${head}${args}`;
|
|
286
|
+
const schemaName = unquoteIdentifier(qualified[1]);
|
|
287
|
+
const objName = unquoteIdentifier(qualified[2].trim());
|
|
288
|
+
const target = router.resolveObject(schemaName, objName, ns);
|
|
289
|
+
if (!target)
|
|
290
|
+
return `${head}${args}`;
|
|
291
|
+
const rebound = target.name !== undefined && target.name !== objName;
|
|
292
|
+
const requalified = target.schema === null || (!!target.schema && target.schema !== schemaName);
|
|
293
|
+
if (!rebound && !requalified)
|
|
294
|
+
return `${head}${args}`;
|
|
295
|
+
result.schemasFound.add(schemaName);
|
|
296
|
+
const nameToken = QuoteUtils.quoteIdentifier(rebound ? target.name : objName);
|
|
297
|
+
if (target.schema === null)
|
|
298
|
+
return `${nameToken}${args}`;
|
|
299
|
+
if (target.schema && target.schema !== schemaName) {
|
|
300
|
+
result.schemasTransformed.set(schemaName, target.schema);
|
|
301
|
+
}
|
|
302
|
+
return `${QuoteUtils.quoteIdentifier(target.schema ?? schemaName)}.${nameToken}${args}`;
|
|
303
|
+
}
|
|
210
304
|
/**
|
|
211
305
|
* Create a SQL AST visitor that transforms schema names.
|
|
212
306
|
*
|
|
@@ -459,9 +553,28 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
|
|
|
459
553
|
A_Const: (path) => {
|
|
460
554
|
const node = path.node;
|
|
461
555
|
if (typeof node.sval?.sval === 'string' && node.sval.sval.includes('.')) {
|
|
556
|
+
if (!claimSite(result, node.sval, 'sval'))
|
|
557
|
+
return;
|
|
462
558
|
node.sval.sval = transformSchemaRefsInString(node.sval.sval, schemaMapping, result);
|
|
463
559
|
}
|
|
464
560
|
},
|
|
561
|
+
// Object identities carried as the operand of an identity cast
|
|
562
|
+
// ('sch.tbl'::regclass, 'sch.fn(uuid)'::regprocedure, 'sch'::regnamespace).
|
|
563
|
+
// The cast names the namespace, so these route through the same object
|
|
564
|
+
// routes as a RangeVar rather than through a string pattern — and the bare
|
|
565
|
+
// schema form, invisible to every string pass, routes here.
|
|
566
|
+
TypeCast: (path) => {
|
|
567
|
+
const node = path.node;
|
|
568
|
+
const sval = node?.arg?.A_Const?.sval;
|
|
569
|
+
if (typeof sval?.sval !== 'string')
|
|
570
|
+
return;
|
|
571
|
+
const ns = identityCastNamespace(node.typeName);
|
|
572
|
+
if (!ns)
|
|
573
|
+
return;
|
|
574
|
+
if (!claimSite(result, sval, 'sval'))
|
|
575
|
+
return;
|
|
576
|
+
sval.sval = transformIdentityCastLiteral(sval.sval, ns, router, result);
|
|
577
|
+
},
|
|
465
578
|
// Transform DefineStmt (CREATE TYPE schema.name, CREATE AGGREGATE schema.agg)
|
|
466
579
|
DefineStmt: (path) => {
|
|
467
580
|
const node = path.node;
|
|
@@ -618,14 +731,19 @@ export function validateNoUntransformedSchemas(content, schemaMapping) {
|
|
|
618
731
|
if (moved.size === 0) {
|
|
619
732
|
return;
|
|
620
733
|
}
|
|
734
|
+
// Every pattern below requires a literal occurrence of the schema name, so a
|
|
735
|
+
// single case-insensitive substring scan rules out most schemas up front.
|
|
736
|
+
const lowerContent = content.toLowerCase();
|
|
621
737
|
for (const [oldSchema, newSchema] of moved) {
|
|
622
|
-
|
|
738
|
+
if (!lowerContent.includes(oldSchema.toLowerCase()))
|
|
739
|
+
continue;
|
|
740
|
+
const escapedSchema = cachedEscapeRegexp(oldSchema);
|
|
623
741
|
// Pattern 1: quoted or unquoted schema name followed by dot (schema-qualified)
|
|
624
|
-
const dotPattern =
|
|
742
|
+
const dotPattern = cachedRegExp(`(?:"${escapedSchema}"|\\b${escapedSchema})(?=\\.)`, 'g');
|
|
625
743
|
// Pattern 2: standalone schema name in known SQL contexts
|
|
626
744
|
// Note: We don't use trailing \b because it fails after closing quotes
|
|
627
745
|
// (both '"' and whitespace are non-word characters, so no boundary exists).
|
|
628
|
-
const standalonePattern =
|
|
746
|
+
const standalonePattern = cachedRegExp(`(?:ON\\s+SCHEMA\\s+|IN\\s+SCHEMA\\s+|CREATE\\s+SCHEMA\\s+|DROP\\s+SCHEMA\\s+(?:IF\\s+EXISTS\\s+)?|SET\\s+SCHEMA\\s+)` +
|
|
629
747
|
`(?:"${escapedSchema}"|\\b${escapedSchema}\\b)`, 'gi');
|
|
630
748
|
const dotMatches = content.match(dotPattern);
|
|
631
749
|
const standaloneMatches = content.match(standalonePattern);
|
|
@@ -633,7 +751,7 @@ export function validateNoUntransformedSchemas(content, schemaMapping) {
|
|
|
633
751
|
if (totalMatches > 0) {
|
|
634
752
|
const lines = content.split('\n');
|
|
635
753
|
const locations = [];
|
|
636
|
-
const combinedPattern =
|
|
754
|
+
const combinedPattern = cachedRegExp(`(?:"${escapedSchema}"|\\b${escapedSchema})(?=\\.)|` +
|
|
637
755
|
`(?:ON\\s+SCHEMA\\s+|IN\\s+SCHEMA\\s+|CREATE\\s+SCHEMA\\s+|DROP\\s+SCHEMA\\s+(?:IF\\s+EXISTS\\s+)?|SET\\s+SCHEMA\\s+)` +
|
|
638
756
|
`(?:"${escapedSchema}"|\\b${escapedSchema}\\b)`, 'gi');
|
|
639
757
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -878,6 +996,34 @@ function transformSchemasInContext(ctx, schemaMapping, result, visitorOptions) {
|
|
|
878
996
|
export function escapeRegexp(str) {
|
|
879
997
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
880
998
|
}
|
|
999
|
+
/**
|
|
1000
|
+
* Compiling a pattern costs far more than running it, and the string-level
|
|
1001
|
+
* passes below rebuild the same handful of per-schema patterns for every file
|
|
1002
|
+
* they touch — on a large corpus that is hundreds of thousands of identical
|
|
1003
|
+
* compiles. Patterns are keyed by source + flags and reused; each returned
|
|
1004
|
+
* RegExp is stateful (`g`/`y` carry `lastIndex`), so callers must only use it
|
|
1005
|
+
* with `String#replace`/`String#match`, which reset it, or reset it
|
|
1006
|
+
* themselves, exactly as the throwaway instances required.
|
|
1007
|
+
*/
|
|
1008
|
+
const regExpCache = new Map();
|
|
1009
|
+
function cachedRegExp(source, flags) {
|
|
1010
|
+
const key = `${flags}\u0000${source}`;
|
|
1011
|
+
let re = regExpCache.get(key);
|
|
1012
|
+
if (!re) {
|
|
1013
|
+
re = new RegExp(source, flags);
|
|
1014
|
+
regExpCache.set(key, re);
|
|
1015
|
+
}
|
|
1016
|
+
return re;
|
|
1017
|
+
}
|
|
1018
|
+
const escapeRegexpCache = new Map();
|
|
1019
|
+
function cachedEscapeRegexp(str) {
|
|
1020
|
+
let escaped = escapeRegexpCache.get(str);
|
|
1021
|
+
if (escaped === undefined) {
|
|
1022
|
+
escaped = escapeRegexp(str);
|
|
1023
|
+
escapeRegexpCache.set(str, escaped);
|
|
1024
|
+
}
|
|
1025
|
+
return escaped;
|
|
1026
|
+
}
|
|
881
1027
|
/**
|
|
882
1028
|
* Extract pgpm header comments from the beginning of SQL content.
|
|
883
1029
|
*/
|
|
@@ -936,7 +1082,7 @@ export function transformComments(content, schemaMapping, result) {
|
|
|
936
1082
|
if (!newName)
|
|
937
1083
|
continue;
|
|
938
1084
|
result.schemasFound.add(schema);
|
|
939
|
-
const pathPattern =
|
|
1085
|
+
const pathPattern = cachedRegExp(`(schemas/)${cachedEscapeRegexp(schema)}(/|$)`, 'g');
|
|
940
1086
|
const before = newPath;
|
|
941
1087
|
newPath = newPath.replace(pathPattern, `$1${newName}$2`);
|
|
942
1088
|
if (newPath !== before) {
|
|
@@ -953,15 +1099,20 @@ export function transformComments(content, schemaMapping, result) {
|
|
|
953
1099
|
export function transformVerifyCalls(content, schemaMapping, result) {
|
|
954
1100
|
const schemas = Array.from(schemaMapping.keys()).sort((a, b) => b.length - a.length);
|
|
955
1101
|
let newContent = content;
|
|
1102
|
+
// The pattern needs the schema name spelled out, case-insensitively.
|
|
1103
|
+
let lowerContent = content.toLowerCase();
|
|
956
1104
|
for (const schema of schemas) {
|
|
957
1105
|
const newName = schemaMapping.get(schema);
|
|
958
1106
|
if (!newName)
|
|
959
1107
|
continue;
|
|
960
|
-
|
|
961
|
-
|
|
1108
|
+
if (!lowerContent.includes(schema.toLowerCase()))
|
|
1109
|
+
continue;
|
|
1110
|
+
const escapedSchema = cachedEscapeRegexp(schema);
|
|
1111
|
+
const verifyPattern = cachedRegExp(`(verify_(?:function|table|trigger|type|domain|view|index|constraint|schema|policy|table_grant|function_grant|sequence_grant|type_grant)\\s*\\(\\s*')${escapedSchema}(\\.|'\\s*\\))`, 'gi');
|
|
962
1112
|
const before = newContent;
|
|
963
1113
|
newContent = newContent.replace(verifyPattern, `$1${newName}$2`);
|
|
964
1114
|
if (newContent !== before) {
|
|
1115
|
+
lowerContent = newContent.toLowerCase();
|
|
965
1116
|
result.schemasFound.add(schema);
|
|
966
1117
|
result.schemasTransformed.set(schema, newName);
|
|
967
1118
|
}
|
|
@@ -978,8 +1129,10 @@ export function transformJsonStringValues(content, schemaMapping, result) {
|
|
|
978
1129
|
const newName = schemaMapping.get(schema);
|
|
979
1130
|
if (!newName)
|
|
980
1131
|
continue;
|
|
981
|
-
|
|
982
|
-
|
|
1132
|
+
if (!newContent.includes(schema))
|
|
1133
|
+
continue;
|
|
1134
|
+
const escapedSchema = cachedEscapeRegexp(schema);
|
|
1135
|
+
const jsonValuePattern = cachedRegExp(`(:")${escapedSchema}(")`, 'g');
|
|
983
1136
|
const before = newContent;
|
|
984
1137
|
newContent = newContent.replace(jsonValuePattern, `$1${newName}$2`);
|
|
985
1138
|
if (newContent !== before) {
|
package/index.d.ts
CHANGED
|
@@ -23,4 +23,4 @@ export { CLEAN_TREE_VOLATILE_KEYS, cleanTree, normalizeTree, trimDefElemBody, }
|
|
|
23
23
|
export type { ObjectNamespace, ObjectRoute, ObjectRouteTarget, RouteNamespace, RouteSpec, SchemaRoute, } from './router';
|
|
24
24
|
export { SchemaRouter } from './router';
|
|
25
25
|
export type { SchemaMappingInput, SchemaTransformPass, SchemaTransformResult, TransformSqlOptions, } from './transform';
|
|
26
|
-
export { createPlpgsqlVisitor, createSqlVisitor, escapeRegexp, extractPgpmHeader, shouldTransformSchema, transformComments, transformJsonStringValues, transformNameList, transformPlpgsqlTypeAst, transformPlpgsqlTypeString, transformRelation, transformSchemaName, transformSql, transformSqlStatement, transformVerifyCalls, validateNoUntransformedSchemas, walkPlpgsqlForSchemas, } from './transform';
|
|
26
|
+
export { createPlpgsqlVisitor, createSqlVisitor, escapeRegexp, extractPgpmHeader, identityCastNamespace, shouldTransformSchema, transformComments, transformIdentityCastLiteral, transformJsonStringValues, transformNameList, transformPlpgsqlTypeAst, transformPlpgsqlTypeString, transformRelation, transformSchemaName, transformSql, transformSqlStatement, transformVerifyCalls, validateNoUntransformedSchemas, walkPlpgsqlForSchemas, } from './transform';
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.walkPlpgsqlForSchemas = exports.validateNoUntransformedSchemas = exports.transformVerifyCalls = exports.transformSqlStatement = exports.transformSql = exports.transformSchemaName = exports.transformRelation = exports.transformPlpgsqlTypeString = exports.transformPlpgsqlTypeAst = exports.transformNameList = exports.transformJsonStringValues = exports.transformComments = exports.shouldTransformSchema = exports.extractPgpmHeader = exports.escapeRegexp = exports.createSqlVisitor = exports.createPlpgsqlVisitor = exports.SchemaRouter = exports.trimDefElemBody = exports.normalizeTree = exports.cleanTree = exports.CLEAN_TREE_VOLATILE_KEYS = exports.validateRoundTrip = exports.normalizeParseTree = exports.firstDifference = exports.captureTransformAsts = exports.captureAstsFromSql = exports.transformRoles = exports.createRoleVisitor = exports.createRoleResult = exports.RoleRouter = exports.restructureSql = exports.orderStatements = exports.qualifyUnqualified = exports.mergeInventories = exports.createQualifyVisitor = exports.collectCreatedObjects = exports.identityOf = exports.buildStatementGraph = exports.classifyStatements = exports.transformExtensions = exports.createExtensionVisitor = exports.createExtensionResult = exports.ExtensionRouter = exports.COMMON_EXTENSIONS = void 0;
|
|
3
|
+
exports.walkPlpgsqlForSchemas = exports.validateNoUntransformedSchemas = exports.transformVerifyCalls = exports.transformSqlStatement = exports.transformSql = exports.transformSchemaName = exports.transformRelation = exports.transformPlpgsqlTypeString = exports.transformPlpgsqlTypeAst = exports.transformNameList = exports.transformJsonStringValues = exports.transformIdentityCastLiteral = exports.transformComments = exports.shouldTransformSchema = exports.identityCastNamespace = exports.extractPgpmHeader = exports.escapeRegexp = exports.createSqlVisitor = exports.createPlpgsqlVisitor = exports.SchemaRouter = exports.trimDefElemBody = exports.normalizeTree = exports.cleanTree = exports.CLEAN_TREE_VOLATILE_KEYS = exports.validateRoundTrip = exports.normalizeParseTree = exports.firstDifference = exports.captureTransformAsts = exports.captureAstsFromSql = exports.transformRoles = exports.createRoleVisitor = exports.createRoleResult = exports.RoleRouter = exports.restructureSql = exports.orderStatements = exports.qualifyUnqualified = exports.mergeInventories = exports.createQualifyVisitor = exports.collectCreatedObjects = exports.identityOf = exports.buildStatementGraph = exports.classifyStatements = exports.transformExtensions = exports.createExtensionVisitor = exports.createExtensionResult = exports.ExtensionRouter = exports.COMMON_EXTENSIONS = void 0;
|
|
4
4
|
var extension_router_1 = require("./extension-router");
|
|
5
5
|
Object.defineProperty(exports, "COMMON_EXTENSIONS", { enumerable: true, get: function () { return extension_router_1.COMMON_EXTENSIONS; } });
|
|
6
6
|
Object.defineProperty(exports, "ExtensionRouter", { enumerable: true, get: function () { return extension_router_1.ExtensionRouter; } });
|
|
@@ -46,8 +46,10 @@ Object.defineProperty(exports, "createPlpgsqlVisitor", { enumerable: true, get:
|
|
|
46
46
|
Object.defineProperty(exports, "createSqlVisitor", { enumerable: true, get: function () { return transform_1.createSqlVisitor; } });
|
|
47
47
|
Object.defineProperty(exports, "escapeRegexp", { enumerable: true, get: function () { return transform_1.escapeRegexp; } });
|
|
48
48
|
Object.defineProperty(exports, "extractPgpmHeader", { enumerable: true, get: function () { return transform_1.extractPgpmHeader; } });
|
|
49
|
+
Object.defineProperty(exports, "identityCastNamespace", { enumerable: true, get: function () { return transform_1.identityCastNamespace; } });
|
|
49
50
|
Object.defineProperty(exports, "shouldTransformSchema", { enumerable: true, get: function () { return transform_1.shouldTransformSchema; } });
|
|
50
51
|
Object.defineProperty(exports, "transformComments", { enumerable: true, get: function () { return transform_1.transformComments; } });
|
|
52
|
+
Object.defineProperty(exports, "transformIdentityCastLiteral", { enumerable: true, get: function () { return transform_1.transformIdentityCastLiteral; } });
|
|
51
53
|
Object.defineProperty(exports, "transformJsonStringValues", { enumerable: true, get: function () { return transform_1.transformJsonStringValues; } });
|
|
52
54
|
Object.defineProperty(exports, "transformNameList", { enumerable: true, get: function () { return transform_1.transformNameList; } });
|
|
53
55
|
Object.defineProperty(exports, "transformPlpgsqlTypeAst", { enumerable: true, get: function () { return transform_1.transformPlpgsqlTypeAst; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pgsql/transform",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.16.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "AST-based SQL transformation, qualification, classification and closure analysis for PostgreSQL",
|
|
6
6
|
"main": "index.js",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"classify",
|
|
51
51
|
"plpgsql"
|
|
52
52
|
],
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "9ecd247f9f0ef696a95e1f582d7353eb6bb693d4"
|
|
54
54
|
}
|
package/transform.d.ts
CHANGED
|
@@ -123,6 +123,19 @@ export declare function transformRelation(relation: any, schemaMapping: SchemaMa
|
|
|
123
123
|
* followed by a dot are rewritten.
|
|
124
124
|
*/
|
|
125
125
|
export declare function transformSchemaRefsInString(str: string, schemaMapping: SchemaMappingInput, result: SchemaTransformResult): string;
|
|
126
|
+
/**
|
|
127
|
+
* The namespace a cast resolves its operand in, or `undefined` when the target
|
|
128
|
+
* type is not an object-identity type.
|
|
129
|
+
*/
|
|
130
|
+
export declare function identityCastNamespace(typeName: any): RouteNamespace | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Route the object reference carried in the string operand of an identity
|
|
133
|
+
* cast. `ns` comes from the cast's target type.
|
|
134
|
+
*
|
|
135
|
+
* Unqualified operands (`'tbl'::regclass`) name no schema — they resolve
|
|
136
|
+
* through `search_path` — so they are returned unchanged.
|
|
137
|
+
*/
|
|
138
|
+
export declare function transformIdentityCastLiteral(sval: string, ns: RouteNamespace, schemaMapping: SchemaMappingInput, result: SchemaTransformResult): string;
|
|
126
139
|
/**
|
|
127
140
|
* Create a SQL AST visitor that transforms schema names.
|
|
128
141
|
*
|
|
@@ -148,6 +161,7 @@ export declare function createSqlVisitor(schemaMapping: SchemaMappingInput, resu
|
|
|
148
161
|
CreateTrigStmt: (path: any) => void;
|
|
149
162
|
CommentStmt: (path: any) => void;
|
|
150
163
|
A_Const: (path: any) => void;
|
|
164
|
+
TypeCast: (path: any) => void;
|
|
151
165
|
DefineStmt: (path: any) => void;
|
|
152
166
|
CreateDomainStmt: (path: any) => void;
|
|
153
167
|
CreateEnumStmt: (path: any) => void;
|
package/transform.js
CHANGED
|
@@ -31,6 +31,8 @@ exports.transformNameList = transformNameList;
|
|
|
31
31
|
exports.transformSchemaNameField = transformSchemaNameField;
|
|
32
32
|
exports.transformRelation = transformRelation;
|
|
33
33
|
exports.transformSchemaRefsInString = transformSchemaRefsInString;
|
|
34
|
+
exports.identityCastNamespace = identityCastNamespace;
|
|
35
|
+
exports.transformIdentityCastLiteral = transformIdentityCastLiteral;
|
|
34
36
|
exports.createSqlVisitor = createSqlVisitor;
|
|
35
37
|
exports.validateNoUntransformedSchemas = validateNoUntransformedSchemas;
|
|
36
38
|
exports.createPlpgsqlVisitor = createPlpgsqlVisitor;
|
|
@@ -218,7 +220,9 @@ function transformSchemaRefsInString(str, schemaMapping, result) {
|
|
|
218
220
|
// References embedded in opaque strings carry no object identity, so only
|
|
219
221
|
// whole-schema (schema-level default) routes can be applied here.
|
|
220
222
|
for (const [oldSchema, newSchema] of schemaLevelMap(schemaMapping)) {
|
|
221
|
-
|
|
223
|
+
if (!out.includes(oldSchema))
|
|
224
|
+
continue;
|
|
225
|
+
const pattern = cachedRegExp(`(?<![\\w-])("?)${cachedEscapeRegexp(oldSchema)}\\1(?=\\.)`, 'g');
|
|
222
226
|
const before = out;
|
|
223
227
|
out = out.replace(pattern, `$1${newSchema}$1`);
|
|
224
228
|
if (out !== before) {
|
|
@@ -228,6 +232,98 @@ function transformSchemaRefsInString(str, schemaMapping, result) {
|
|
|
228
232
|
}
|
|
229
233
|
return out;
|
|
230
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Cast types whose string operand is an object reference rather than opaque
|
|
237
|
+
* text. `'sch.tbl'::regclass` is a relation the parser resolves, and the cast
|
|
238
|
+
* names the namespace it resolves in — so the reference can be routed from the
|
|
239
|
+
* cast, semantically, instead of pattern-matched. This reaches what the string
|
|
240
|
+
* passes cannot: a bare `'sch'::regnamespace` has no dot to match on.
|
|
241
|
+
*/
|
|
242
|
+
const IDENTITY_CAST_NAMESPACES = {
|
|
243
|
+
regclass: 'relation',
|
|
244
|
+
regcollation: 'unknown',
|
|
245
|
+
regconfig: 'unknown',
|
|
246
|
+
regdictionary: 'unknown',
|
|
247
|
+
regnamespace: 'schema',
|
|
248
|
+
regoper: 'unknown',
|
|
249
|
+
regoperator: 'unknown',
|
|
250
|
+
regproc: 'function',
|
|
251
|
+
regprocedure: 'function',
|
|
252
|
+
regtype: 'type'
|
|
253
|
+
};
|
|
254
|
+
/**
|
|
255
|
+
* The namespace a cast resolves its operand in, or `undefined` when the target
|
|
256
|
+
* type is not an object-identity type.
|
|
257
|
+
*/
|
|
258
|
+
function identityCastNamespace(typeName) {
|
|
259
|
+
const names = typeName?.names;
|
|
260
|
+
if (!Array.isArray(names) || names.length === 0)
|
|
261
|
+
return undefined;
|
|
262
|
+
// Only `pg_catalog`-qualified (or unqualified) reg* types are the built-ins.
|
|
263
|
+
if (names.length > 1 && names[0]?.String?.sval !== 'pg_catalog')
|
|
264
|
+
return undefined;
|
|
265
|
+
const typeIdent = names[names.length - 1]?.String?.sval;
|
|
266
|
+
if (typeof typeIdent !== 'string')
|
|
267
|
+
return undefined;
|
|
268
|
+
return IDENTITY_CAST_NAMESPACES[typeIdent.toLowerCase()];
|
|
269
|
+
}
|
|
270
|
+
const IDENTITY_TOKEN = String.raw `(?:"(?:[^"]|"")*"|[^\s".,()[\]]+)`;
|
|
271
|
+
const BARE_IDENTITY_RE = new RegExp(`^\\s*(${IDENTITY_TOKEN})\\s*$`);
|
|
272
|
+
const QUALIFIED_IDENTITY_RE = new RegExp(`^\\s*(${IDENTITY_TOKEN})\\.([\\s\\S]+)$`);
|
|
273
|
+
function unquoteIdentifier(token) {
|
|
274
|
+
if (token.length > 1 && token.startsWith('"') && token.endsWith('"')) {
|
|
275
|
+
return token.slice(1, -1).replace(/""/g, '"');
|
|
276
|
+
}
|
|
277
|
+
return token;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Route the object reference carried in the string operand of an identity
|
|
281
|
+
* cast. `ns` comes from the cast's target type.
|
|
282
|
+
*
|
|
283
|
+
* Unqualified operands (`'tbl'::regclass`) name no schema — they resolve
|
|
284
|
+
* through `search_path` — so they are returned unchanged.
|
|
285
|
+
*/
|
|
286
|
+
function transformIdentityCastLiteral(sval, ns, schemaMapping, result) {
|
|
287
|
+
const router = asRouter(schemaMapping);
|
|
288
|
+
if (ns === 'schema') {
|
|
289
|
+
const bare = BARE_IDENTITY_RE.exec(sval);
|
|
290
|
+
if (!bare)
|
|
291
|
+
return sval;
|
|
292
|
+
const schemaName = unquoteIdentifier(bare[1]);
|
|
293
|
+
const newName = router.resolve(schemaName, undefined, 'schema');
|
|
294
|
+
if (!newName || newName === schemaName)
|
|
295
|
+
return sval;
|
|
296
|
+
result.schemasFound.add(schemaName);
|
|
297
|
+
result.schemasTransformed.set(schemaName, newName);
|
|
298
|
+
return quotes_1.QuoteUtils.quoteIdentifier(newName);
|
|
299
|
+
}
|
|
300
|
+
// `regprocedure` and `regoperator` carry an argument list after the name, and
|
|
301
|
+
// an argument type can itself be schema-qualified
|
|
302
|
+
// (`fn("my-schema".row_type)`), so the list is routed as ordinary schema refs.
|
|
303
|
+
const argsAt = sval.indexOf('(');
|
|
304
|
+
const head = argsAt === -1 ? sval : sval.slice(0, argsAt);
|
|
305
|
+
const args = argsAt === -1 ? '' : transformSchemaRefsInString(sval.slice(argsAt), router, result);
|
|
306
|
+
const qualified = QUALIFIED_IDENTITY_RE.exec(head);
|
|
307
|
+
if (!qualified)
|
|
308
|
+
return `${head}${args}`;
|
|
309
|
+
const schemaName = unquoteIdentifier(qualified[1]);
|
|
310
|
+
const objName = unquoteIdentifier(qualified[2].trim());
|
|
311
|
+
const target = router.resolveObject(schemaName, objName, ns);
|
|
312
|
+
if (!target)
|
|
313
|
+
return `${head}${args}`;
|
|
314
|
+
const rebound = target.name !== undefined && target.name !== objName;
|
|
315
|
+
const requalified = target.schema === null || (!!target.schema && target.schema !== schemaName);
|
|
316
|
+
if (!rebound && !requalified)
|
|
317
|
+
return `${head}${args}`;
|
|
318
|
+
result.schemasFound.add(schemaName);
|
|
319
|
+
const nameToken = quotes_1.QuoteUtils.quoteIdentifier(rebound ? target.name : objName);
|
|
320
|
+
if (target.schema === null)
|
|
321
|
+
return `${nameToken}${args}`;
|
|
322
|
+
if (target.schema && target.schema !== schemaName) {
|
|
323
|
+
result.schemasTransformed.set(schemaName, target.schema);
|
|
324
|
+
}
|
|
325
|
+
return `${quotes_1.QuoteUtils.quoteIdentifier(target.schema ?? schemaName)}.${nameToken}${args}`;
|
|
326
|
+
}
|
|
231
327
|
/**
|
|
232
328
|
* Create a SQL AST visitor that transforms schema names.
|
|
233
329
|
*
|
|
@@ -480,9 +576,28 @@ function createSqlVisitor(schemaMapping, result, visitorOptions) {
|
|
|
480
576
|
A_Const: (path) => {
|
|
481
577
|
const node = path.node;
|
|
482
578
|
if (typeof node.sval?.sval === 'string' && node.sval.sval.includes('.')) {
|
|
579
|
+
if (!claimSite(result, node.sval, 'sval'))
|
|
580
|
+
return;
|
|
483
581
|
node.sval.sval = transformSchemaRefsInString(node.sval.sval, schemaMapping, result);
|
|
484
582
|
}
|
|
485
583
|
},
|
|
584
|
+
// Object identities carried as the operand of an identity cast
|
|
585
|
+
// ('sch.tbl'::regclass, 'sch.fn(uuid)'::regprocedure, 'sch'::regnamespace).
|
|
586
|
+
// The cast names the namespace, so these route through the same object
|
|
587
|
+
// routes as a RangeVar rather than through a string pattern — and the bare
|
|
588
|
+
// schema form, invisible to every string pass, routes here.
|
|
589
|
+
TypeCast: (path) => {
|
|
590
|
+
const node = path.node;
|
|
591
|
+
const sval = node?.arg?.A_Const?.sval;
|
|
592
|
+
if (typeof sval?.sval !== 'string')
|
|
593
|
+
return;
|
|
594
|
+
const ns = identityCastNamespace(node.typeName);
|
|
595
|
+
if (!ns)
|
|
596
|
+
return;
|
|
597
|
+
if (!claimSite(result, sval, 'sval'))
|
|
598
|
+
return;
|
|
599
|
+
sval.sval = transformIdentityCastLiteral(sval.sval, ns, router, result);
|
|
600
|
+
},
|
|
486
601
|
// Transform DefineStmt (CREATE TYPE schema.name, CREATE AGGREGATE schema.agg)
|
|
487
602
|
DefineStmt: (path) => {
|
|
488
603
|
const node = path.node;
|
|
@@ -639,14 +754,19 @@ function validateNoUntransformedSchemas(content, schemaMapping) {
|
|
|
639
754
|
if (moved.size === 0) {
|
|
640
755
|
return;
|
|
641
756
|
}
|
|
757
|
+
// Every pattern below requires a literal occurrence of the schema name, so a
|
|
758
|
+
// single case-insensitive substring scan rules out most schemas up front.
|
|
759
|
+
const lowerContent = content.toLowerCase();
|
|
642
760
|
for (const [oldSchema, newSchema] of moved) {
|
|
643
|
-
|
|
761
|
+
if (!lowerContent.includes(oldSchema.toLowerCase()))
|
|
762
|
+
continue;
|
|
763
|
+
const escapedSchema = cachedEscapeRegexp(oldSchema);
|
|
644
764
|
// Pattern 1: quoted or unquoted schema name followed by dot (schema-qualified)
|
|
645
|
-
const dotPattern =
|
|
765
|
+
const dotPattern = cachedRegExp(`(?:"${escapedSchema}"|\\b${escapedSchema})(?=\\.)`, 'g');
|
|
646
766
|
// Pattern 2: standalone schema name in known SQL contexts
|
|
647
767
|
// Note: We don't use trailing \b because it fails after closing quotes
|
|
648
768
|
// (both '"' and whitespace are non-word characters, so no boundary exists).
|
|
649
|
-
const standalonePattern =
|
|
769
|
+
const standalonePattern = cachedRegExp(`(?:ON\\s+SCHEMA\\s+|IN\\s+SCHEMA\\s+|CREATE\\s+SCHEMA\\s+|DROP\\s+SCHEMA\\s+(?:IF\\s+EXISTS\\s+)?|SET\\s+SCHEMA\\s+)` +
|
|
650
770
|
`(?:"${escapedSchema}"|\\b${escapedSchema}\\b)`, 'gi');
|
|
651
771
|
const dotMatches = content.match(dotPattern);
|
|
652
772
|
const standaloneMatches = content.match(standalonePattern);
|
|
@@ -654,7 +774,7 @@ function validateNoUntransformedSchemas(content, schemaMapping) {
|
|
|
654
774
|
if (totalMatches > 0) {
|
|
655
775
|
const lines = content.split('\n');
|
|
656
776
|
const locations = [];
|
|
657
|
-
const combinedPattern =
|
|
777
|
+
const combinedPattern = cachedRegExp(`(?:"${escapedSchema}"|\\b${escapedSchema})(?=\\.)|` +
|
|
658
778
|
`(?:ON\\s+SCHEMA\\s+|IN\\s+SCHEMA\\s+|CREATE\\s+SCHEMA\\s+|DROP\\s+SCHEMA\\s+(?:IF\\s+EXISTS\\s+)?|SET\\s+SCHEMA\\s+)` +
|
|
659
779
|
`(?:"${escapedSchema}"|\\b${escapedSchema}\\b)`, 'gi');
|
|
660
780
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -899,6 +1019,34 @@ function transformSchemasInContext(ctx, schemaMapping, result, visitorOptions) {
|
|
|
899
1019
|
function escapeRegexp(str) {
|
|
900
1020
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
901
1021
|
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Compiling a pattern costs far more than running it, and the string-level
|
|
1024
|
+
* passes below rebuild the same handful of per-schema patterns for every file
|
|
1025
|
+
* they touch — on a large corpus that is hundreds of thousands of identical
|
|
1026
|
+
* compiles. Patterns are keyed by source + flags and reused; each returned
|
|
1027
|
+
* RegExp is stateful (`g`/`y` carry `lastIndex`), so callers must only use it
|
|
1028
|
+
* with `String#replace`/`String#match`, which reset it, or reset it
|
|
1029
|
+
* themselves, exactly as the throwaway instances required.
|
|
1030
|
+
*/
|
|
1031
|
+
const regExpCache = new Map();
|
|
1032
|
+
function cachedRegExp(source, flags) {
|
|
1033
|
+
const key = `${flags}\u0000${source}`;
|
|
1034
|
+
let re = regExpCache.get(key);
|
|
1035
|
+
if (!re) {
|
|
1036
|
+
re = new RegExp(source, flags);
|
|
1037
|
+
regExpCache.set(key, re);
|
|
1038
|
+
}
|
|
1039
|
+
return re;
|
|
1040
|
+
}
|
|
1041
|
+
const escapeRegexpCache = new Map();
|
|
1042
|
+
function cachedEscapeRegexp(str) {
|
|
1043
|
+
let escaped = escapeRegexpCache.get(str);
|
|
1044
|
+
if (escaped === undefined) {
|
|
1045
|
+
escaped = escapeRegexp(str);
|
|
1046
|
+
escapeRegexpCache.set(str, escaped);
|
|
1047
|
+
}
|
|
1048
|
+
return escaped;
|
|
1049
|
+
}
|
|
902
1050
|
/**
|
|
903
1051
|
* Extract pgpm header comments from the beginning of SQL content.
|
|
904
1052
|
*/
|
|
@@ -957,7 +1105,7 @@ function transformComments(content, schemaMapping, result) {
|
|
|
957
1105
|
if (!newName)
|
|
958
1106
|
continue;
|
|
959
1107
|
result.schemasFound.add(schema);
|
|
960
|
-
const pathPattern =
|
|
1108
|
+
const pathPattern = cachedRegExp(`(schemas/)${cachedEscapeRegexp(schema)}(/|$)`, 'g');
|
|
961
1109
|
const before = newPath;
|
|
962
1110
|
newPath = newPath.replace(pathPattern, `$1${newName}$2`);
|
|
963
1111
|
if (newPath !== before) {
|
|
@@ -974,15 +1122,20 @@ function transformComments(content, schemaMapping, result) {
|
|
|
974
1122
|
function transformVerifyCalls(content, schemaMapping, result) {
|
|
975
1123
|
const schemas = Array.from(schemaMapping.keys()).sort((a, b) => b.length - a.length);
|
|
976
1124
|
let newContent = content;
|
|
1125
|
+
// The pattern needs the schema name spelled out, case-insensitively.
|
|
1126
|
+
let lowerContent = content.toLowerCase();
|
|
977
1127
|
for (const schema of schemas) {
|
|
978
1128
|
const newName = schemaMapping.get(schema);
|
|
979
1129
|
if (!newName)
|
|
980
1130
|
continue;
|
|
981
|
-
|
|
982
|
-
|
|
1131
|
+
if (!lowerContent.includes(schema.toLowerCase()))
|
|
1132
|
+
continue;
|
|
1133
|
+
const escapedSchema = cachedEscapeRegexp(schema);
|
|
1134
|
+
const verifyPattern = cachedRegExp(`(verify_(?:function|table|trigger|type|domain|view|index|constraint|schema|policy|table_grant|function_grant|sequence_grant|type_grant)\\s*\\(\\s*')${escapedSchema}(\\.|'\\s*\\))`, 'gi');
|
|
983
1135
|
const before = newContent;
|
|
984
1136
|
newContent = newContent.replace(verifyPattern, `$1${newName}$2`);
|
|
985
1137
|
if (newContent !== before) {
|
|
1138
|
+
lowerContent = newContent.toLowerCase();
|
|
986
1139
|
result.schemasFound.add(schema);
|
|
987
1140
|
result.schemasTransformed.set(schema, newName);
|
|
988
1141
|
}
|
|
@@ -999,8 +1152,10 @@ function transformJsonStringValues(content, schemaMapping, result) {
|
|
|
999
1152
|
const newName = schemaMapping.get(schema);
|
|
1000
1153
|
if (!newName)
|
|
1001
1154
|
continue;
|
|
1002
|
-
|
|
1003
|
-
|
|
1155
|
+
if (!newContent.includes(schema))
|
|
1156
|
+
continue;
|
|
1157
|
+
const escapedSchema = cachedEscapeRegexp(schema);
|
|
1158
|
+
const jsonValuePattern = cachedRegExp(`(:")${escapedSchema}(")`, 'g');
|
|
1004
1159
|
const before = newContent;
|
|
1005
1160
|
newContent = newContent.replace(jsonValuePattern, `$1${newName}$2`);
|
|
1006
1161
|
if (newContent !== before) {
|