@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.
- package/README.md +29 -73
- package/categorize.d.ts +52 -0
- package/categorize.js +56 -0
- package/esm/categorize.js +51 -0
- package/esm/facts.js +289 -0
- package/esm/fixture-closure.js +197 -0
- package/esm/index.js +7 -22
- package/esm/qualify.js +297 -0
- package/esm/round-trip-core.js +126 -0
- package/esm/round-trip.js +116 -0
- package/esm/transform.js +1169 -0
- package/facts.d.ts +69 -0
- package/facts.js +292 -0
- package/fixture-closure.d.ts +71 -0
- package/fixture-closure.js +200 -0
- package/index.d.ts +14 -28
- package/index.js +43 -76
- package/package.json +22 -14
- package/qualify.d.ts +125 -0
- package/qualify.js +303 -0
- package/round-trip-core.d.ts +60 -0
- package/round-trip-core.js +133 -0
- package/round-trip.d.ts +47 -0
- package/round-trip.js +123 -0
- package/transform.d.ts +259 -0
- package/transform.js +1190 -0
- package/13/enums.d.ts +0 -62
- package/13/enums.js +0 -2
- package/13/types.d.ts +0 -2103
- package/13/types.js +0 -2
- package/14/enums.d.ts +0 -62
- package/14/enums.js +0 -2
- package/14/types.d.ts +0 -2164
- package/14/types.js +0 -2
- package/15/enums.d.ts +0 -64
- package/15/enums.js +0 -2
- package/15/types.d.ts +0 -2222
- package/15/types.js +0 -2
- package/16/enums.d.ts +0 -69
- package/16/enums.js +0 -2
- package/16/types.d.ts +0 -2328
- package/16/types.js +0 -2
- package/17/enums.d.ts +0 -76
- package/17/enums.js +0 -2
- package/17/types.d.ts +0 -2488
- package/17/types.js +0 -2
- package/18/enums.d.ts +0 -78
- package/18/enums.js +0 -2
- package/18/types.d.ts +0 -2538
- package/18/types.js +0 -2
- package/esm/13/enums.js +0 -1
- package/esm/13/types.js +0 -1
- package/esm/14/enums.js +0 -1
- package/esm/14/types.js +0 -1
- package/esm/15/enums.js +0 -1
- package/esm/15/types.js +0 -1
- package/esm/16/enums.js +0 -1
- package/esm/16/types.js +0 -1
- package/esm/17/enums.js +0 -1
- package/esm/17/types.js +0 -1
- package/esm/18/enums.js +0 -1
- package/esm/18/types.js +0 -1
- package/esm/multi-version-transformer.js +0 -80
- package/esm/transformers/context.js +0 -1
- package/esm/transformers/v13-to-v14.js +0 -2767
- package/esm/transformers/v14-to-v15.js +0 -1218
- package/esm/transformers/v15-to-v16.js +0 -2935
- package/esm/transformers/v16-to-v17.js +0 -1526
- package/esm/transformers/v17-to-v18.js +0 -204
- package/esm/transformers-direct/index.js +0 -13
- package/esm/transformers-direct/v13-to-v17/index.js +0 -74
- package/esm/transformers-direct/v13-to-v18/index.js +0 -84
- package/esm/transformers-direct/v14-to-v17/index.js +0 -63
- package/esm/transformers-direct/v14-to-v18/index.js +0 -73
- package/esm/transformers-direct/v15-to-v17/index.js +0 -53
- package/esm/transformers-direct/v15-to-v18/index.js +0 -62
- package/esm/transformers-direct/v16-to-v17/index.js +0 -40
- package/esm/transformers-direct/v16-to-v18/index.js +0 -51
- package/esm/transformers-direct/v17-to-v18/index.js +0 -39
- package/multi-version-transformer.d.ts +0 -14
- package/multi-version-transformer.js +0 -84
- package/transformers/context.d.ts +0 -4
- package/transformers/context.js +0 -2
- package/transformers/v13-to-v14.d.ts +0 -255
- package/transformers/v13-to-v14.js +0 -2771
- package/transformers/v14-to-v15.d.ts +0 -615
- package/transformers/v14-to-v15.js +0 -1222
- package/transformers/v15-to-v16.d.ts +0 -632
- package/transformers/v15-to-v16.js +0 -2939
- package/transformers/v16-to-v17.d.ts +0 -640
- package/transformers/v16-to-v17.js +0 -1530
- package/transformers/v17-to-v18.d.ts +0 -54
- package/transformers/v17-to-v18.js +0 -208
- package/transformers-direct/index.d.ts +0 -13
- package/transformers-direct/index.js +0 -25
- package/transformers-direct/v13-to-v17/index.d.ts +0 -27
- package/transformers-direct/v13-to-v17/index.js +0 -78
- package/transformers-direct/v13-to-v18/index.d.ts +0 -28
- package/transformers-direct/v13-to-v18/index.js +0 -88
- package/transformers-direct/v14-to-v17/index.d.ts +0 -26
- package/transformers-direct/v14-to-v17/index.js +0 -67
- package/transformers-direct/v14-to-v18/index.d.ts +0 -27
- package/transformers-direct/v14-to-v18/index.js +0 -77
- package/transformers-direct/v15-to-v17/index.d.ts +0 -25
- package/transformers-direct/v15-to-v17/index.js +0 -57
- package/transformers-direct/v15-to-v18/index.d.ts +0 -26
- package/transformers-direct/v15-to-v18/index.js +0 -66
- package/transformers-direct/v16-to-v17/index.d.ts +0 -24
- package/transformers-direct/v16-to-v17/index.js +0 -44
- package/transformers-direct/v16-to-v18/index.d.ts +0 -25
- package/transformers-direct/v16-to-v18/index.js +0 -55
- package/transformers-direct/v17-to-v18/index.d.ts +0 -24
- package/transformers-direct/v17-to-v18/index.js +0 -43
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared, dependency-free core for round-trip AST validation.
|
|
3
|
+
*
|
|
4
|
+
* This module is intentionally self-contained — it imports no parser or
|
|
5
|
+
* deparser — so it can be lifted verbatim into an upstream package (e.g.
|
|
6
|
+
* `@pgsql/round-trip`) and shared between:
|
|
7
|
+
*
|
|
8
|
+
* - pgsql-parser's test fixtures (`cleanTree` / `expectParseDeparse`), which
|
|
9
|
+
* parse → deparse → re-parse and assert the normalized ASTs are equal, and
|
|
10
|
+
* - downstream mutation-aware validators (this package's
|
|
11
|
+
* round-trip check), which compare a *mutated* AST against the AST obtained
|
|
12
|
+
* by re-parsing the deparsed output.
|
|
13
|
+
*
|
|
14
|
+
* The upstream `cleanTree` is expressed here as a preset over the generic
|
|
15
|
+
* `normalizeTree` engine; the downstream fortified normalizer layers extra
|
|
16
|
+
* volatile keys, lazy-datum filtering, and body-sentinel handling on top of
|
|
17
|
+
* the same engine (see round-trip.ts).
|
|
18
|
+
*/
|
|
19
|
+
export interface NormalizeTreeOptions {
|
|
20
|
+
/** Object keys dropped entirely (volatile / non-semantic fields). */
|
|
21
|
+
volatileKeys?: ReadonlySet<string>;
|
|
22
|
+
/** Sort object keys so comparison is insensitive to key order. Default false. */
|
|
23
|
+
sortKeys?: boolean;
|
|
24
|
+
/** Array items for which this returns true are removed before recursing. */
|
|
25
|
+
dropArrayItem?: (item: unknown) => boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Per-key transform hooks keyed by object-property name. The hook receives
|
|
28
|
+
* the value at that key plus a `recurse` callback (which re-enters
|
|
29
|
+
* `normalizeTree` with the same options) and returns the replacement value.
|
|
30
|
+
*/
|
|
31
|
+
keyHandlers?: Record<string, (value: any, recurse: (node: any) => any) => any>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Deep-clone an AST while dropping volatile keys, optionally sorting keys,
|
|
35
|
+
* filtering array items, and applying per-key transform hooks. Primitives are
|
|
36
|
+
* returned as-is; Dates are cloned (parity with the original upstream
|
|
37
|
+
* `transform` helper).
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeTree(node: any, options?: NormalizeTreeOptions): any;
|
|
40
|
+
/**
|
|
41
|
+
* Return the JSON path of the first structural difference between two trees,
|
|
42
|
+
* or null if they are deeply equal. Useful for actionable mismatch messages.
|
|
43
|
+
*/
|
|
44
|
+
export declare function firstDifference(a: any, b: any, path?: string): string | null;
|
|
45
|
+
/** Position/length fields stripped by the upstream `cleanTree`. */
|
|
46
|
+
export declare const CLEAN_TREE_VOLATILE_KEYS: ReadonlySet<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Trim the function/DO body string carried by a `DefElem` with defname "as".
|
|
49
|
+
* Mirrors the in-place trimming in pgsql-parser's `cleanTree` so the body
|
|
50
|
+
* text (whose surrounding whitespace legitimately shifts on deparse) does not
|
|
51
|
+
* cause spurious AST mismatches. Mutates the passed DefElem in place.
|
|
52
|
+
*/
|
|
53
|
+
export declare function trimDefElemBody(defElem: any): void;
|
|
54
|
+
/**
|
|
55
|
+
* Upstream-compatible normalization: strips `stmt_len` / `stmt_location` /
|
|
56
|
+
* `location` and trims `DefElem` "as" body strings. Equivalent to
|
|
57
|
+
* pgsql-parser's deparser `cleanTree`, provided here so upstream can adopt the
|
|
58
|
+
* shared core without behavior change.
|
|
59
|
+
*/
|
|
60
|
+
export declare function cleanTree(tree: any): any;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared, dependency-free core for round-trip AST validation.
|
|
4
|
+
*
|
|
5
|
+
* This module is intentionally self-contained — it imports no parser or
|
|
6
|
+
* deparser — so it can be lifted verbatim into an upstream package (e.g.
|
|
7
|
+
* `@pgsql/round-trip`) and shared between:
|
|
8
|
+
*
|
|
9
|
+
* - pgsql-parser's test fixtures (`cleanTree` / `expectParseDeparse`), which
|
|
10
|
+
* parse → deparse → re-parse and assert the normalized ASTs are equal, and
|
|
11
|
+
* - downstream mutation-aware validators (this package's
|
|
12
|
+
* round-trip check), which compare a *mutated* AST against the AST obtained
|
|
13
|
+
* by re-parsing the deparsed output.
|
|
14
|
+
*
|
|
15
|
+
* The upstream `cleanTree` is expressed here as a preset over the generic
|
|
16
|
+
* `normalizeTree` engine; the downstream fortified normalizer layers extra
|
|
17
|
+
* volatile keys, lazy-datum filtering, and body-sentinel handling on top of
|
|
18
|
+
* the same engine (see round-trip.ts).
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.CLEAN_TREE_VOLATILE_KEYS = void 0;
|
|
22
|
+
exports.normalizeTree = normalizeTree;
|
|
23
|
+
exports.firstDifference = firstDifference;
|
|
24
|
+
exports.trimDefElemBody = trimDefElemBody;
|
|
25
|
+
exports.cleanTree = cleanTree;
|
|
26
|
+
/**
|
|
27
|
+
* Deep-clone an AST while dropping volatile keys, optionally sorting keys,
|
|
28
|
+
* filtering array items, and applying per-key transform hooks. Primitives are
|
|
29
|
+
* returned as-is; Dates are cloned (parity with the original upstream
|
|
30
|
+
* `transform` helper).
|
|
31
|
+
*/
|
|
32
|
+
function normalizeTree(node, options = {}) {
|
|
33
|
+
const { volatileKeys, sortKeys, dropArrayItem, keyHandlers } = options;
|
|
34
|
+
const recurse = (n) => normalizeTree(n, options);
|
|
35
|
+
if (Array.isArray(node)) {
|
|
36
|
+
const items = dropArrayItem ? node.filter((i) => !dropArrayItem(i)) : node;
|
|
37
|
+
return items.map(recurse);
|
|
38
|
+
}
|
|
39
|
+
if (node instanceof Date) {
|
|
40
|
+
return new Date(node.getTime());
|
|
41
|
+
}
|
|
42
|
+
if (node && typeof node === 'object') {
|
|
43
|
+
const out = {};
|
|
44
|
+
const keys = sortKeys ? Object.keys(node).sort() : Object.keys(node);
|
|
45
|
+
for (const key of keys) {
|
|
46
|
+
if (volatileKeys?.has(key))
|
|
47
|
+
continue;
|
|
48
|
+
const handler = keyHandlers?.[key];
|
|
49
|
+
out[key] = handler ? handler(node[key], recurse) : recurse(node[key]);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
return node;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Return the JSON path of the first structural difference between two trees,
|
|
57
|
+
* or null if they are deeply equal. Useful for actionable mismatch messages.
|
|
58
|
+
*/
|
|
59
|
+
function firstDifference(a, b, path = '$') {
|
|
60
|
+
if (a === b)
|
|
61
|
+
return null;
|
|
62
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
63
|
+
if (a.length !== b.length) {
|
|
64
|
+
return `${path} (array length ${a.length} vs ${b.length})`;
|
|
65
|
+
}
|
|
66
|
+
for (let i = 0; i < a.length; i++) {
|
|
67
|
+
const diff = firstDifference(a[i], b[i], `${path}[${i}]`);
|
|
68
|
+
if (diff)
|
|
69
|
+
return diff;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
74
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
const diff = firstDifference(a[key], b[key], `${path}.${key}`);
|
|
77
|
+
if (diff)
|
|
78
|
+
return diff;
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return `${path} (${JSON.stringify(a)} vs ${JSON.stringify(b)})`;
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Upstream-compatible cleanTree preset
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
/** Position/length fields stripped by the upstream `cleanTree`. */
|
|
88
|
+
exports.CLEAN_TREE_VOLATILE_KEYS = new Set([
|
|
89
|
+
'stmt_len',
|
|
90
|
+
'stmt_location',
|
|
91
|
+
'location',
|
|
92
|
+
'rexpr_list_start',
|
|
93
|
+
'rexpr_list_end',
|
|
94
|
+
'list_start',
|
|
95
|
+
'list_end',
|
|
96
|
+
]);
|
|
97
|
+
/**
|
|
98
|
+
* Trim the function/DO body string carried by a `DefElem` with defname "as".
|
|
99
|
+
* Mirrors the in-place trimming in pgsql-parser's `cleanTree` so the body
|
|
100
|
+
* text (whose surrounding whitespace legitimately shifts on deparse) does not
|
|
101
|
+
* cause spurious AST mismatches. Mutates the passed DefElem in place.
|
|
102
|
+
*/
|
|
103
|
+
function trimDefElemBody(defElem) {
|
|
104
|
+
if (!defElem || defElem.defname !== 'as')
|
|
105
|
+
return;
|
|
106
|
+
const arg = defElem.arg;
|
|
107
|
+
if (Array.isArray(arg) && arg.length && arg[0]?.String) {
|
|
108
|
+
arg[0].String.sval = arg[0].String.sval.trim();
|
|
109
|
+
}
|
|
110
|
+
else if (arg?.List?.items?.length && arg.List.items[0]?.String) {
|
|
111
|
+
arg.List.items[0].String.sval = arg.List.items[0].String.sval.trim();
|
|
112
|
+
}
|
|
113
|
+
else if (arg?.String) {
|
|
114
|
+
arg.String.sval = arg.String.sval.trim();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Upstream-compatible normalization: strips `stmt_len` / `stmt_location` /
|
|
119
|
+
* `location` and trims `DefElem` "as" body strings. Equivalent to
|
|
120
|
+
* pgsql-parser's deparser `cleanTree`, provided here so upstream can adopt the
|
|
121
|
+
* shared core without behavior change.
|
|
122
|
+
*/
|
|
123
|
+
function cleanTree(tree) {
|
|
124
|
+
return normalizeTree(tree, {
|
|
125
|
+
volatileKeys: exports.CLEAN_TREE_VOLATILE_KEYS,
|
|
126
|
+
keyHandlers: {
|
|
127
|
+
DefElem: (defElem, recurse) => {
|
|
128
|
+
trimDefElemBody(defElem);
|
|
129
|
+
return recurse(defElem);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
package/round-trip.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-trip validation for the schema transform.
|
|
3
|
+
*
|
|
4
|
+
* After the transform deparses its mutated AST to SQL text, the emitted text
|
|
5
|
+
* must parse back to an AST that is structurally identical to the AST we
|
|
6
|
+
* deparsed (AST1 === AST2 after normalization). Any mismatch means the
|
|
7
|
+
* deparser dropped or mangled something (e.g. array bounds on a DECLAREd
|
|
8
|
+
* type), even though the output may still be syntactically valid SQL.
|
|
9
|
+
*
|
|
10
|
+
* Two comparisons are performed per file:
|
|
11
|
+
* - the SQL-level parse tree (with volatile fields and function body strings
|
|
12
|
+
* normalized — PL/pgSQL bodies are compared separately)
|
|
13
|
+
* - each PL/pgSQL function body, compared in dehydrated form so both sides
|
|
14
|
+
* go through the same hydrate -> dehydrate rendering
|
|
15
|
+
*/
|
|
16
|
+
import { firstDifference } from './round-trip-core';
|
|
17
|
+
export { firstDifference };
|
|
18
|
+
/**
|
|
19
|
+
* Fortified, mutation-aware normalizer built on the shared round-trip core.
|
|
20
|
+
* Beyond the upstream `cleanTree` (position stripping + body trimming), it
|
|
21
|
+
* additionally sorts keys, drops lazily-created recfield datums, and replaces
|
|
22
|
+
* DefElem "as" bodies with a sentinel (PL/pgSQL body fidelity is checked
|
|
23
|
+
* separately via the dehydrated PL/pgSQL ASTs).
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeParseTree(node: any): any;
|
|
26
|
+
export interface CapturedAsts {
|
|
27
|
+
sqlAst: any;
|
|
28
|
+
plpgsqlAsts: any[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Capture the normalized SQL parse tree and dehydrated PL/pgSQL function
|
|
32
|
+
* ASTs from a transform context (as produced by plpgsql-parser's
|
|
33
|
+
* transformSync callback). Call this AFTER mutating the context.
|
|
34
|
+
*/
|
|
35
|
+
export declare function captureTransformAsts(ctx: any): CapturedAsts;
|
|
36
|
+
/**
|
|
37
|
+
* Parse already-transformed SQL text and capture its ASTs the same way
|
|
38
|
+
* capture_transform_asts does for the pre-deparse context, so the two
|
|
39
|
+
* sides are directly comparable.
|
|
40
|
+
*/
|
|
41
|
+
export declare function captureAstsFromSql(sql: string): CapturedAsts;
|
|
42
|
+
/**
|
|
43
|
+
* Assert that the transformed AST (captured before deparse) matches the AST
|
|
44
|
+
* obtained by re-parsing the emitted SQL. Throws with the first differing
|
|
45
|
+
* path on mismatch.
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateRoundTrip(before: CapturedAsts, transformedSql: string): void;
|
package/round-trip.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Round-trip validation for the schema transform.
|
|
4
|
+
*
|
|
5
|
+
* After the transform deparses its mutated AST to SQL text, the emitted text
|
|
6
|
+
* must parse back to an AST that is structurally identical to the AST we
|
|
7
|
+
* deparsed (AST1 === AST2 after normalization). Any mismatch means the
|
|
8
|
+
* deparser dropped or mangled something (e.g. array bounds on a DECLAREd
|
|
9
|
+
* type), even though the output may still be syntactically valid SQL.
|
|
10
|
+
*
|
|
11
|
+
* Two comparisons are performed per file:
|
|
12
|
+
* - the SQL-level parse tree (with volatile fields and function body strings
|
|
13
|
+
* normalized — PL/pgSQL bodies are compared separately)
|
|
14
|
+
* - each PL/pgSQL function body, compared in dehydrated form so both sides
|
|
15
|
+
* go through the same hydrate -> dehydrate rendering
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.firstDifference = void 0;
|
|
19
|
+
exports.normalizeParseTree = normalizeParseTree;
|
|
20
|
+
exports.captureTransformAsts = captureTransformAsts;
|
|
21
|
+
exports.captureAstsFromSql = captureAstsFromSql;
|
|
22
|
+
exports.validateRoundTrip = validateRoundTrip;
|
|
23
|
+
const plpgsql_parser_1 = require("plpgsql-parser");
|
|
24
|
+
const round_trip_core_1 = require("./round-trip-core");
|
|
25
|
+
Object.defineProperty(exports, "firstDifference", { enumerable: true, get: function () { return round_trip_core_1.firstDifference; } });
|
|
26
|
+
/**
|
|
27
|
+
* Fields that legitimately differ between parses. Extends the upstream
|
|
28
|
+
* `cleanTree` volatile set (location/stmt_location/stmt_len) with:
|
|
29
|
+
* - lineno: source positions in PL/pgSQL nodes
|
|
30
|
+
* - dno/varno/recparentno: PL/pgSQL datum numbers — compiler bookkeeping
|
|
31
|
+
* assigned in parse-encounter order, which shifts when the deparser
|
|
32
|
+
* emits an equivalent-but-reordered statement (e.g. INTO placement).
|
|
33
|
+
* Structural identity is still checked via names/fieldnames.
|
|
34
|
+
*/
|
|
35
|
+
const VOLATILE_KEYS = new Set([
|
|
36
|
+
...round_trip_core_1.CLEAN_TREE_VOLATILE_KEYS,
|
|
37
|
+
'lineno',
|
|
38
|
+
'dno',
|
|
39
|
+
'varno',
|
|
40
|
+
'recparentno',
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* PLpgSQL_recfield datums are created lazily by the PL/pgSQL compiler as
|
|
44
|
+
* record.field references are encountered while parsing expressions, so
|
|
45
|
+
* their presence/order in the datums array depends on expression text
|
|
46
|
+
* layout, not semantics.
|
|
47
|
+
*/
|
|
48
|
+
function isRecfieldDatum(node) {
|
|
49
|
+
return !!node && typeof node === 'object' && 'PLpgSQL_recfield' in node;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Fortified, mutation-aware normalizer built on the shared round-trip core.
|
|
53
|
+
* Beyond the upstream `cleanTree` (position stripping + body trimming), it
|
|
54
|
+
* additionally sorts keys, drops lazily-created recfield datums, and replaces
|
|
55
|
+
* DefElem "as" bodies with a sentinel (PL/pgSQL body fidelity is checked
|
|
56
|
+
* separately via the dehydrated PL/pgSQL ASTs).
|
|
57
|
+
*/
|
|
58
|
+
function normalizeParseTree(node) {
|
|
59
|
+
return (0, round_trip_core_1.normalizeTree)(node, {
|
|
60
|
+
volatileKeys: VOLATILE_KEYS,
|
|
61
|
+
sortKeys: true,
|
|
62
|
+
dropArrayItem: isRecfieldDatum,
|
|
63
|
+
keyHandlers: {
|
|
64
|
+
DefElem: (defElem, recurse) => defElem?.defname === 'as'
|
|
65
|
+
? { ...recurse(defElem), arg: '<body>' }
|
|
66
|
+
: recurse(defElem),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Capture the normalized SQL parse tree and dehydrated PL/pgSQL function
|
|
72
|
+
* ASTs from a transform context (as produced by plpgsql-parser's
|
|
73
|
+
* transformSync callback). Call this AFTER mutating the context.
|
|
74
|
+
*/
|
|
75
|
+
function captureTransformAsts(ctx) {
|
|
76
|
+
const plpgsqlAsts = [];
|
|
77
|
+
for (const fn of ctx.functions) {
|
|
78
|
+
if (fn.plpgsql?.hydrated) {
|
|
79
|
+
plpgsqlAsts.push(normalizeParseTree((0, plpgsql_parser_1.dehydratePlpgsqlAst)(structuredClone(fn.plpgsql.hydrated))));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
sqlAst: normalizeParseTree(ctx.sql),
|
|
84
|
+
plpgsqlAsts,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse already-transformed SQL text and capture its ASTs the same way
|
|
89
|
+
* capture_transform_asts does for the pre-deparse context, so the two
|
|
90
|
+
* sides are directly comparable.
|
|
91
|
+
*/
|
|
92
|
+
function captureAstsFromSql(sql) {
|
|
93
|
+
let captured;
|
|
94
|
+
(0, plpgsql_parser_1.transformSync)(sql, (ctx) => {
|
|
95
|
+
captured = captureTransformAsts(ctx);
|
|
96
|
+
}, { hydrate: true, pretty: true });
|
|
97
|
+
if (!captured) {
|
|
98
|
+
throw new Error('round-trip: failed to capture ASTs from transformed SQL');
|
|
99
|
+
}
|
|
100
|
+
return captured;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Assert that the transformed AST (captured before deparse) matches the AST
|
|
104
|
+
* obtained by re-parsing the emitted SQL. Throws with the first differing
|
|
105
|
+
* path on mismatch.
|
|
106
|
+
*/
|
|
107
|
+
function validateRoundTrip(before, transformedSql) {
|
|
108
|
+
const after = captureAstsFromSql(transformedSql);
|
|
109
|
+
const sqlDiff = (0, round_trip_core_1.firstDifference)(before.sqlAst, after.sqlAst);
|
|
110
|
+
if (sqlDiff) {
|
|
111
|
+
throw new Error(`round-trip validation failed: SQL AST mismatch after deparse at ${sqlDiff}`);
|
|
112
|
+
}
|
|
113
|
+
if (before.plpgsqlAsts.length !== after.plpgsqlAsts.length) {
|
|
114
|
+
throw new Error(`round-trip validation failed: PL/pgSQL function count mismatch ` +
|
|
115
|
+
`(${before.plpgsqlAsts.length} before deparse vs ${after.plpgsqlAsts.length} after re-parse)`);
|
|
116
|
+
}
|
|
117
|
+
for (let i = 0; i < before.plpgsqlAsts.length; i++) {
|
|
118
|
+
const diff = (0, round_trip_core_1.firstDifference)(before.plpgsqlAsts[i], after.plpgsqlAsts[i]);
|
|
119
|
+
if (diff) {
|
|
120
|
+
throw new Error(`round-trip validation failed: PL/pgSQL AST mismatch in function #${i} at ${diff}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
package/transform.d.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
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 type { QualifyUnqualifiedOptions } from './qualify';
|
|
27
|
+
export interface SchemaTransformResult {
|
|
28
|
+
schemasFound: Set<string>;
|
|
29
|
+
schemasTransformed: Map<string, string>;
|
|
30
|
+
errors: Array<{
|
|
31
|
+
file: string;
|
|
32
|
+
error: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A pluggable string-level transform pass.
|
|
37
|
+
*
|
|
38
|
+
* Extension passes run on the raw SQL text before or after the core AST
|
|
39
|
+
* transformation. They exist for content that is opaque to the SQL parser
|
|
40
|
+
* (string literals, comments, JSON values, etc.).
|
|
41
|
+
*
|
|
42
|
+
* Each pass receives the current content string, the schema mapping, and the
|
|
43
|
+
* shared result tracker, and must return the (possibly transformed) content.
|
|
44
|
+
*/
|
|
45
|
+
export type SchemaTransformPass = (content: string, schemaMapping: Map<string, string>, result: SchemaTransformResult) => string;
|
|
46
|
+
/**
|
|
47
|
+
* Options for transform_sql.
|
|
48
|
+
*/
|
|
49
|
+
export interface TransformSqlOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Extension passes that run BEFORE the core AST transformation.
|
|
52
|
+
* Use these for app-specific string-level transforms that must happen
|
|
53
|
+
* before the parser sees the content (e.g. verify calls, JSON values).
|
|
54
|
+
*/
|
|
55
|
+
prePasses?: SchemaTransformPass[];
|
|
56
|
+
/**
|
|
57
|
+
* Extension passes that run AFTER the core AST transformation.
|
|
58
|
+
* Use these for app-specific transforms on the deparsed output.
|
|
59
|
+
*/
|
|
60
|
+
postPasses?: SchemaTransformPass[];
|
|
61
|
+
/**
|
|
62
|
+
* Validate that the emitted SQL re-parses to an AST structurally identical
|
|
63
|
+
* to the transformed AST that was deparsed (catches deparser fidelity bugs
|
|
64
|
+
* such as dropped array bounds). Adds a second parse per file.
|
|
65
|
+
*/
|
|
66
|
+
roundTrip?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Qualify unqualified object references BEFORE the schema mapping runs
|
|
69
|
+
* (an extra AST pass, opt-in). Pin unqualified references to a schema
|
|
70
|
+
* (typically `'public'`) so a mapping on that schema moves them too —
|
|
71
|
+
* the ingestion path for handwritten, unqualified SQL. Only names in the
|
|
72
|
+
* inventory (defaulting to objects the content itself creates) are
|
|
73
|
+
* qualified. See {@link qualifyUnqualified}.
|
|
74
|
+
*/
|
|
75
|
+
qualifyUnqualified?: QualifyUnqualifiedOptions;
|
|
76
|
+
/**
|
|
77
|
+
* Schema names (post-mapping) whose `CREATE SCHEMA` statements should be
|
|
78
|
+
* emitted as `CREATE SCHEMA IF NOT EXISTS`. Use when mapping into a schema
|
|
79
|
+
* that always exists — e.g. mapping a named schema onto `public`.
|
|
80
|
+
*/
|
|
81
|
+
assumeSchemasExist?: string[];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Transform a schema name if it exists in the mapping
|
|
85
|
+
*/
|
|
86
|
+
export declare function transformSchemaName(schemaName: string | undefined, schemaMapping: Map<string, string>): string | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Check if a schema name should be transformed
|
|
89
|
+
*/
|
|
90
|
+
export declare function shouldTransformSchema(schemaName: string | undefined, schemaMapping: Map<string, string>): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Transform schema names in a String node array (used for funcname, names, etc.)
|
|
93
|
+
* These arrays contain String nodes like { String: { sval: 'schema_name' } }
|
|
94
|
+
*/
|
|
95
|
+
export declare function transformNameList(names: any[] | undefined, schemaMapping: Map<string, string>, result: SchemaTransformResult): void;
|
|
96
|
+
/**
|
|
97
|
+
* Transform a bare schema-name field on a node (e.g. { String: { sval } }
|
|
98
|
+
* entries in GrantStmt.objects, RenameStmt.subname, CommentStmt.object).
|
|
99
|
+
* Returns the (possibly new) schema name.
|
|
100
|
+
*/
|
|
101
|
+
export declare function transformSchemaNameField(container: any, field: string, schemaMapping: Map<string, string>, result: SchemaTransformResult): void;
|
|
102
|
+
/**
|
|
103
|
+
* Transform a RangeVar-like relation object (has schemaname/relname fields).
|
|
104
|
+
* Many statement nodes embed a relation that the walker does NOT auto-recurse into.
|
|
105
|
+
*/
|
|
106
|
+
export declare function transformRelation(relation: any, schemaMapping: Map<string, string>, result: SchemaTransformResult): void;
|
|
107
|
+
/**
|
|
108
|
+
* Transform schema-qualified references embedded in a plain string
|
|
109
|
+
* (e.g. advisory-lock keys like 'schema-name.fn_name', COMMENT text,
|
|
110
|
+
* RAISE messages). Only occurrences of a mapped schema name immediately
|
|
111
|
+
* followed by a dot are rewritten.
|
|
112
|
+
*/
|
|
113
|
+
export declare function transformSchemaRefsInString(str: string, schemaMapping: Map<string, string>, result: SchemaTransformResult): string;
|
|
114
|
+
/**
|
|
115
|
+
* Create a SQL AST visitor that transforms schema names.
|
|
116
|
+
*
|
|
117
|
+
* The walker from @pgsql/traverse auto-recurses into child nodes, so visitors
|
|
118
|
+
* like RangeVar and TypeName fire for every occurrence regardless of parent.
|
|
119
|
+
* However, some node types carry schema names as plain strings or name lists
|
|
120
|
+
* that require explicit handlers.
|
|
121
|
+
*/
|
|
122
|
+
export declare function createSqlVisitor(schemaMapping: Map<string, string>, result: SchemaTransformResult, visitorOptions?: {
|
|
123
|
+
assumeSchemasExist?: Set<string>;
|
|
124
|
+
}): {
|
|
125
|
+
RangeVar: (path: any) => void;
|
|
126
|
+
CreateSchemaStmt: (path: any) => void;
|
|
127
|
+
FuncCall: (path: any) => void;
|
|
128
|
+
CallStmt: (path: any) => void;
|
|
129
|
+
TypeName: (path: any) => void;
|
|
130
|
+
ColumnRef: (path: any) => void;
|
|
131
|
+
GrantStmt: (path: any) => void;
|
|
132
|
+
VariableSetStmt: (path: any) => void;
|
|
133
|
+
AlterDefaultPrivilegesStmt: (path: any) => void;
|
|
134
|
+
DropStmt: (path: any) => void;
|
|
135
|
+
InsertStmt: (path: any) => void;
|
|
136
|
+
CreateStmt: (path: any) => void;
|
|
137
|
+
UpdateStmt: (path: any) => void;
|
|
138
|
+
DeleteStmt: (path: any) => void;
|
|
139
|
+
MergeStmt: (path: any) => void;
|
|
140
|
+
IndexStmt: (path: any) => void;
|
|
141
|
+
Constraint: (path: any) => void;
|
|
142
|
+
CompositeTypeStmt: (path: any) => void;
|
|
143
|
+
ColumnDef: (path: any) => void;
|
|
144
|
+
ViewStmt: (path: any) => void;
|
|
145
|
+
AlterTableStmt: (path: any) => void;
|
|
146
|
+
CreateSeqStmt: (path: any) => void;
|
|
147
|
+
AlterSeqStmt: (path: any) => void;
|
|
148
|
+
CreatePolicyStmt: (path: any) => void;
|
|
149
|
+
RuleStmt: (path: any) => void;
|
|
150
|
+
CopyStmt: (path: any) => void;
|
|
151
|
+
ClusterStmt: (path: any) => void;
|
|
152
|
+
VacuumRelation: (path: any) => void;
|
|
153
|
+
RefreshMatViewStmt: (path: any) => void;
|
|
154
|
+
CreateTableAsStmt: (path: any) => void;
|
|
155
|
+
TypeCast: (path: any) => void;
|
|
156
|
+
CreateFunctionStmt: (path: any) => void;
|
|
157
|
+
CreateTrigStmt: (path: any) => void;
|
|
158
|
+
CommentStmt: (path: any) => void;
|
|
159
|
+
A_Const: (path: any) => void;
|
|
160
|
+
DefineStmt: (path: any) => void;
|
|
161
|
+
CreateDomainStmt: (path: any) => void;
|
|
162
|
+
CreateEnumStmt: (path: any) => void;
|
|
163
|
+
AlterEnumStmt: (path: any) => void;
|
|
164
|
+
AlterDomainStmt: (path: any) => void;
|
|
165
|
+
AlterTypeStmt: (path: any) => void;
|
|
166
|
+
ObjectWithArgs: (path: any) => void;
|
|
167
|
+
AlterObjectSchemaStmt: (path: any) => void;
|
|
168
|
+
RenameStmt: (path: any) => void;
|
|
169
|
+
AlterFunctionStmt: (path: any) => void;
|
|
170
|
+
AlterOwnerStmt: (path: any) => void;
|
|
171
|
+
CreateCastStmt: (path: any) => void;
|
|
172
|
+
CreateEventTrigStmt: (path: any) => void;
|
|
173
|
+
IndexElem: (path: any) => void;
|
|
174
|
+
SecLabelStmt: (path: any) => void;
|
|
175
|
+
DoStmt: (path: any) => void;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Validate that no untransformed schema names remain in the output.
|
|
179
|
+
* Checks both schema-qualified references (schema.object) and standalone
|
|
180
|
+
* schema name contexts (ON SCHEMA, IN SCHEMA, CREATE SCHEMA, etc.).
|
|
181
|
+
*
|
|
182
|
+
* Throws an error if any schema names from the mapping are found in the output,
|
|
183
|
+
* indicating that the AST visitor is missing a handler for that node type.
|
|
184
|
+
*/
|
|
185
|
+
export declare function validateNoUntransformedSchemas(content: string, schemaMapping: Map<string, string>): void;
|
|
186
|
+
/**
|
|
187
|
+
* Create a PL/pgSQL visitor that transforms schema names in PL/pgSQL-specific nodes.
|
|
188
|
+
*/
|
|
189
|
+
export declare function createPlpgsqlVisitor(schemaMapping: Map<string, string>, result: SchemaTransformResult): {
|
|
190
|
+
PLpgSQL_type: (path: any) => void;
|
|
191
|
+
PLpgSQL_var: (path: any) => void;
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Transform a PLpgSQL_type typname using proper AST parsing.
|
|
195
|
+
*/
|
|
196
|
+
export declare function transformPlpgsqlTypeAst(typname: string, schemaMapping: Map<string, string>, result: SchemaTransformResult): string;
|
|
197
|
+
/**
|
|
198
|
+
* Fallback string-based transformation for PLpgSQL_type typname.
|
|
199
|
+
* Uses @pgsql/quotes QuoteUtils for proper identifier quoting.
|
|
200
|
+
*/
|
|
201
|
+
export declare function transformPlpgsqlTypeString(typname: string, schemaMapping: Map<string, string>, result: SchemaTransformResult): string;
|
|
202
|
+
/**
|
|
203
|
+
* Recursively walk the PL/pgSQL AST to transform schema names.
|
|
204
|
+
*/
|
|
205
|
+
export declare function walkPlpgsqlForSchemas(node: any, schemaMapping: Map<string, string>, result: SchemaTransformResult): void;
|
|
206
|
+
/**
|
|
207
|
+
* Escape a string for use in a regular expression
|
|
208
|
+
*/
|
|
209
|
+
export declare function escapeRegexp(str: string): string;
|
|
210
|
+
/**
|
|
211
|
+
* Extract pgpm header comments from the beginning of SQL content.
|
|
212
|
+
*/
|
|
213
|
+
export declare function extractPgpmHeader(content: string): {
|
|
214
|
+
header: string;
|
|
215
|
+
body: string;
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Transform comment headers (-- Deploy:, -- requires:, etc.)
|
|
219
|
+
* These are not part of the SQL AST, so we use regexp for these.
|
|
220
|
+
*/
|
|
221
|
+
export declare function transformComments(content: string, schemaMapping: Map<string, string>, result: SchemaTransformResult): string;
|
|
222
|
+
/**
|
|
223
|
+
* Transform verify function calls that use string literals.
|
|
224
|
+
* These are inside SQL strings and not part of the main AST.
|
|
225
|
+
*/
|
|
226
|
+
export declare function transformVerifyCalls(content: string, schemaMapping: Map<string, string>, result: SchemaTransformResult): string;
|
|
227
|
+
/**
|
|
228
|
+
* Transform schema names inside JSON/JSONB string values.
|
|
229
|
+
*/
|
|
230
|
+
export declare function transformJsonStringValues(content: string, schemaMapping: Map<string, string>, result: SchemaTransformResult): string;
|
|
231
|
+
/**
|
|
232
|
+
* Transform a single SQL string using full AST-based transformation.
|
|
233
|
+
*
|
|
234
|
+
* This is the main entry point for transforming SQL content. It:
|
|
235
|
+
* 1. Runs any user-supplied pre-passes (string-level)
|
|
236
|
+
* 2. Extracts pgpm header comments and transforms them
|
|
237
|
+
* 3. Runs full AST transformation on the SQL body
|
|
238
|
+
* 4. Runs any user-supplied post-passes (string-level)
|
|
239
|
+
* 5. Validates no untransformed schema names remain
|
|
240
|
+
* 6. Returns the combined result
|
|
241
|
+
*
|
|
242
|
+
* App-specific string-level transforms (verify calls, JSON values, etc.)
|
|
243
|
+
* are NOT included by default — pass them via `options.pre_passes` or
|
|
244
|
+
* `options.post_passes`. The built-in passes `transform_verify_calls`
|
|
245
|
+
* and `transform_json_string_values` are exported for convenience.
|
|
246
|
+
*/
|
|
247
|
+
export declare function transformSql(content: string, schemaMapping: Map<string, string>, options?: TransformSqlOptions | SchemaTransformResult, result?: SchemaTransformResult): {
|
|
248
|
+
content: string;
|
|
249
|
+
result: SchemaTransformResult;
|
|
250
|
+
};
|
|
251
|
+
/**
|
|
252
|
+
* Transform a single SQL statement string using AST.
|
|
253
|
+
* Unlike transform_sql, this does NOT handle headers, verify calls, or JSON values.
|
|
254
|
+
* Use this for testing individual SQL statements.
|
|
255
|
+
*/
|
|
256
|
+
export declare function transformSqlStatement(sql: string, schemaMapping: Map<string, string>, result?: SchemaTransformResult): {
|
|
257
|
+
sql: string;
|
|
258
|
+
result: SchemaTransformResult;
|
|
259
|
+
};
|