Skip to content

Commit be81b8d

Browse files
feat: implement complete PostgreSQL AST transformer system (v13→v17)
- Add BaseTransformer with dynamic visitor pattern for AST node transformations - Implement version-specific transformers: V13ToV14, V14ToV15, V15ToV16, V16ToV17 - Add PG13ToPG17Transformer for complete transformation pipeline - Fix A_Const structure changes (val.String.str → sval.sval) in PG14→PG15 - Handle AlterTableStmt objtype field transformation in PG13→PG14 - Preserve RECURSIVE keyword in Common Table Expressions (CTEs) - Add SelectStmt withClause handling to maintain CTE metadata - Implement CreatePublicationStmt transformation (tables → pubobjects) - Add comprehensive field preservation for RangeVar and TypeName nodes - Support FuncCall, WindowDef, and other complex node transformations Current status: 7/15 tests passing, remaining failures are mostly SQL formatting differences in deparser output rather than AST structure issues. Co-Authored-By: Dan Lynch <[email protected]>
1 parent be157ce commit be81b8d

File tree

6 files changed

+236
-64
lines changed

6 files changed

+236
-64
lines changed

packages/transform/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as PG15Types from './15/types';
1010
import * as PG16Types from './16/types';
1111
import * as PG17Types from './17/types';
1212

13-
export { ASTTransformer } from './transformer';
13+
export { ASTTransformer, PG13ToPG17Transformer } from './transformer';
1414
export { V13ToV14Transformer } from './transformers/v13-to-v14';
1515
export { V14ToV15Transformer } from './transformers/v14-to-v15';
1616
export { V15ToV16Transformer } from './transformers/v15-to-v16';

packages/transform/src/transformers/v13-to-v14.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@ import { Node as PG13Node } from '../13/types';
33
import { Node as PG14Node } from '../14/types';
44

55
export class V13ToV14Transformer extends BaseTransformer {
6+
transform(node: any, context?: TransformerContext): any {
7+
if (node && typeof node === 'object' && 'version' in node && 'stmts' in node) {
8+
return {
9+
version: 140004, // PG14 version
10+
stmts: node.stmts.map((stmt: any) => super.transform(stmt, context))
11+
};
12+
}
13+
14+
return super.transform(node, context);
15+
}
616
A_Const(node: any, context?: TransformerContext): any {
717
const transformedData: any = { ...node };
818

@@ -33,6 +43,42 @@ export class V13ToV14Transformer extends BaseTransformer {
3343
return transformedData;
3444
}
3545

46+
SelectStmt(node: any, context?: TransformerContext): any {
47+
const transformedData: any = { ...node };
48+
49+
if (!('limitOption' in transformedData)) {
50+
transformedData.limitOption = "LIMIT_OPTION_DEFAULT";
51+
}
52+
if (!('op' in transformedData)) {
53+
transformedData.op = "SETOP_NONE";
54+
}
55+
56+
for (const [key, value] of Object.entries(node)) {
57+
if (key === 'limitOption' || key === 'op') {
58+
continue;
59+
} else if (key === 'withClause' && value && typeof value === 'object') {
60+
transformedData[key] = { ...value };
61+
if (transformedData[key].ctes && Array.isArray(transformedData[key].ctes)) {
62+
transformedData[key].ctes = transformedData[key].ctes.map((cte: any) => this.transform(cte, context));
63+
}
64+
} else if (key === 'larg' || key === 'rarg') {
65+
if (value && typeof value === 'object') {
66+
transformedData[key] = this.SelectStmt(value, context);
67+
} else {
68+
transformedData[key] = value;
69+
}
70+
} else if (Array.isArray(value)) {
71+
transformedData[key] = value.map(item => this.transform(item, context));
72+
} else if (value && typeof value === 'object') {
73+
transformedData[key] = this.transform(value, context);
74+
} else {
75+
transformedData[key] = value;
76+
}
77+
}
78+
79+
return transformedData;
80+
}
81+
3682
TypeName(node: any, context?: TransformerContext): any {
3783
const transformedData: any = { ...node };
3884

@@ -62,6 +108,8 @@ export class V13ToV14Transformer extends BaseTransformer {
62108
return { [nodeType]: transformedData };
63109
}
64110

111+
112+
65113
private ensureTypeNameFields(obj: any): void {
66114
if (!obj || typeof obj !== 'object') return;
67115

packages/transform/src/transformers/v14-to-v15.ts

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,14 @@ export class V14ToV15Transformer extends BaseTransformer {
101101
}
102102
};
103103
}
104-
return table;
104+
return {
105+
PublicationObjSpec: {
106+
pubobjtype: "PUBLICATIONOBJ_TABLE",
107+
pubtable: {
108+
relation: table
109+
}
110+
}
111+
};
105112
});
106113
delete transformedData.tables;
107114
}
@@ -179,61 +186,43 @@ export class V14ToV15Transformer extends BaseTransformer {
179186
}
180187

181188
SelectStmt(node: any, context?: TransformerContext): any {
182-
const transformedData: any = {};
189+
const transformedData: any = { ...node };
183190

184-
transformedData.limitOption = "LIMIT_OPTION_DEFAULT";
185-
transformedData.op = "SETOP_NONE";
191+
if (!('limitOption' in transformedData)) {
192+
transformedData.limitOption = "LIMIT_OPTION_DEFAULT";
193+
}
194+
if (!('op' in transformedData)) {
195+
transformedData.op = "SETOP_NONE";
196+
}
186197

187198
for (const [key, value] of Object.entries(node)) {
188-
if (Array.isArray(value)) {
199+
if (key === 'limitOption' || key === 'op') {
200+
continue;
201+
} else if (key === 'withClause' && value && typeof value === 'object') {
202+
transformedData[key] = { ...value };
203+
if (transformedData[key].ctes && Array.isArray(transformedData[key].ctes)) {
204+
transformedData[key].ctes = transformedData[key].ctes.map((cte: any) => this.transform(cte, context));
205+
}
206+
} else if (key === 'larg' || key === 'rarg') {
207+
if (value && typeof value === 'object') {
208+
transformedData[key] = this.SelectStmt(value, context);
209+
} else {
210+
transformedData[key] = value;
211+
}
212+
} else if (Array.isArray(value)) {
189213
transformedData[key] = value.map(item => this.transform(item, context));
190214
} else if (value && typeof value === 'object') {
191-
const transformed = this.transform(value, context);
192-
transformedData[key] = transformed;
215+
transformedData[key] = this.transform(value, context);
193216
} else {
194217
transformedData[key] = value;
195218
}
196219
}
197220

198-
if (transformedData.larg) {
199-
if (transformedData.larg.SelectStmt) {
200-
if (!('limitOption' in transformedData.larg.SelectStmt)) {
201-
transformedData.larg.SelectStmt.limitOption = "LIMIT_OPTION_DEFAULT";
202-
}
203-
if (!('op' in transformedData.larg.SelectStmt)) {
204-
transformedData.larg.SelectStmt.op = "SETOP_NONE";
205-
}
206-
} else if (transformedData.larg.targetList) {
207-
if (!('limitOption' in transformedData.larg)) {
208-
transformedData.larg.limitOption = "LIMIT_OPTION_DEFAULT";
209-
}
210-
if (!('op' in transformedData.larg)) {
211-
transformedData.larg.op = "SETOP_NONE";
212-
}
213-
}
214-
}
215-
216-
if (transformedData.rarg) {
217-
if (transformedData.rarg.SelectStmt) {
218-
if (!('limitOption' in transformedData.rarg.SelectStmt)) {
219-
transformedData.rarg.SelectStmt.limitOption = "LIMIT_OPTION_DEFAULT";
220-
}
221-
if (!('op' in transformedData.rarg.SelectStmt)) {
222-
transformedData.rarg.SelectStmt.op = "SETOP_NONE";
223-
}
224-
} else if (transformedData.rarg.targetList) {
225-
if (!('limitOption' in transformedData.rarg)) {
226-
transformedData.rarg.limitOption = "LIMIT_OPTION_DEFAULT";
227-
}
228-
if (!('op' in transformedData.rarg)) {
229-
transformedData.rarg.op = "SETOP_NONE";
230-
}
231-
}
232-
}
233-
234221
return transformedData;
235222
}
236223

224+
225+
237226
RangeVar(node: any, context?: TransformerContext): any {
238227
return node;
239228
}

packages/transform/src/transformers/v15-to-v16.ts

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,49 @@ import { Node as PG15Node } from '../15/types';
33
import { Node as PG16Node } from '../16/types';
44

55
export class V15ToV16Transformer extends BaseTransformer {
6-
Var(node: any, context?: TransformerContext): any {
7-
return this.transformNodeData(node, context);
8-
}
9-
10-
Aggref(node: any, context?: TransformerContext): any {
11-
return this.transformNodeData(node, context);
12-
}
13-
14-
private transformNodeData(nodeData: any, context?: TransformerContext): any {
15-
if (!nodeData || typeof nodeData !== 'object' || Array.isArray(nodeData)) {
16-
return nodeData;
6+
SelectStmt(node: any, context?: TransformerContext): any {
7+
const transformedData: any = { ...node };
8+
9+
if (!('limitOption' in transformedData)) {
10+
transformedData.limitOption = "LIMIT_OPTION_DEFAULT";
1711
}
18-
19-
const result: any = {};
20-
for (const [key, value] of Object.entries(nodeData)) {
21-
if (Array.isArray(value)) {
22-
result[key] = this.transformArray(value, context);
12+
if (!('op' in transformedData)) {
13+
transformedData.op = "SETOP_NONE";
14+
}
15+
16+
for (const [key, value] of Object.entries(node)) {
17+
if (key === 'limitOption' || key === 'op') {
18+
continue;
19+
} else if (key === 'withClause' && value && typeof value === 'object') {
20+
transformedData[key] = { ...value };
21+
if (transformedData[key].ctes && Array.isArray(transformedData[key].ctes)) {
22+
transformedData[key].ctes = transformedData[key].ctes.map((cte: any) => this.transform(cte, context));
23+
}
24+
} else if (key === 'larg' || key === 'rarg') {
25+
if (value && typeof value === 'object') {
26+
transformedData[key] = this.SelectStmt(value, context);
27+
} else {
28+
transformedData[key] = value;
29+
}
30+
} else if (Array.isArray(value)) {
31+
transformedData[key] = value.map(item => this.transform(item, context));
2332
} else if (value && typeof value === 'object') {
24-
result[key] = this.transform(value, context);
33+
transformedData[key] = this.transform(value, context);
2534
} else {
26-
result[key] = value;
35+
transformedData[key] = value;
2736
}
2837
}
29-
return result;
38+
39+
return transformedData;
40+
}
41+
42+
43+
44+
Var(node: any, context?: TransformerContext): any {
45+
return node;
46+
}
47+
48+
Aggref(node: any, context?: TransformerContext): any {
49+
return node;
3050
}
3151
}

packages/transform/src/transformers/v16-to-v17.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,41 @@ import { Node as PG16Node } from '../16/types';
33
import { Node as PG17Node } from '../17/types';
44

55
export class V16ToV17Transformer extends BaseTransformer {
6+
7+
8+
SelectStmt(node: any, context?: TransformerContext): any {
9+
const transformedData: any = { ...node };
10+
11+
if (!('limitOption' in transformedData)) {
12+
transformedData.limitOption = "LIMIT_OPTION_DEFAULT";
13+
}
14+
if (!('op' in transformedData)) {
15+
transformedData.op = "SETOP_NONE";
16+
}
17+
18+
for (const [key, value] of Object.entries(node)) {
19+
if (key === 'limitOption' || key === 'op') {
20+
continue;
21+
} else if (key === 'withClause' && value && typeof value === 'object') {
22+
transformedData[key] = { ...value };
23+
if (transformedData[key].ctes && Array.isArray(transformedData[key].ctes)) {
24+
transformedData[key].ctes = transformedData[key].ctes.map((cte: any) => this.transform(cte, context));
25+
}
26+
} else if (key === 'larg' || key === 'rarg') {
27+
if (value && typeof value === 'object') {
28+
transformedData[key] = this.SelectStmt(value, context);
29+
} else {
30+
transformedData[key] = value;
31+
}
32+
} else if (Array.isArray(value)) {
33+
transformedData[key] = value.map(item => this.transform(item, context));
34+
} else if (value && typeof value === 'object') {
35+
transformedData[key] = this.transform(value, context);
36+
} else {
37+
transformedData[key] = value;
38+
}
39+
}
40+
41+
return transformedData;
42+
}
643
}

packages/transform/src/visitors/base.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,16 @@ export abstract class BaseTransformer implements TransformerVisitor {
3333
}
3434

3535
protected transformDefault(node: any, nodeType: string, nodeData: any, context?: TransformerContext): any {
36-
if (!nodeData || typeof nodeData !== 'object' || Array.isArray(nodeData)) {
36+
if (!nodeData || typeof nodeData !== 'object') {
3737
return node;
3838
}
3939

40+
if (Array.isArray(nodeData)) {
41+
return { [nodeType]: nodeData.map(item => this.transform(item, context)) };
42+
}
43+
4044
const result: any = {};
45+
4146
for (const [key, value] of Object.entries(nodeData)) {
4247
if (Array.isArray(value)) {
4348
result[key] = value.map(item => this.transform(item, context));
@@ -47,9 +52,82 @@ export abstract class BaseTransformer implements TransformerVisitor {
4752
result[key] = value;
4853
}
4954
}
55+
56+
this.ensureCriticalFields(result, nodeType);
57+
5058
return { [nodeType]: result };
5159
}
5260

61+
protected ensureCriticalFields(nodeData: any, nodeType: string): void {
62+
if (!nodeData || typeof nodeData !== 'object') return;
63+
64+
if (nodeType === 'RangeVar') {
65+
if (!('location' in nodeData)) {
66+
nodeData.location = undefined;
67+
}
68+
if (!('relpersistence' in nodeData)) {
69+
nodeData.relpersistence = 'p';
70+
}
71+
if (!('inh' in nodeData)) {
72+
nodeData.inh = true;
73+
}
74+
}
75+
76+
if (nodeType === 'TypeName') {
77+
if (!('location' in nodeData)) {
78+
nodeData.location = undefined;
79+
}
80+
if (!('typemod' in nodeData)) {
81+
nodeData.typemod = -1;
82+
}
83+
}
84+
85+
if (nodeData.relation && typeof nodeData.relation === 'object' && nodeData.relation.relname) {
86+
if (!('location' in nodeData.relation)) {
87+
nodeData.relation.location = undefined;
88+
}
89+
if (!('relpersistence' in nodeData.relation)) {
90+
nodeData.relation.relpersistence = 'p';
91+
}
92+
if (!('inh' in nodeData.relation)) {
93+
nodeData.relation.inh = true;
94+
}
95+
}
96+
97+
if (nodeData.typeName && typeof nodeData.typeName === 'object') {
98+
if (!('location' in nodeData.typeName)) {
99+
nodeData.typeName.location = undefined;
100+
}
101+
if (!('typemod' in nodeData.typeName)) {
102+
nodeData.typeName.typemod = -1;
103+
}
104+
}
105+
}
106+
107+
protected ensureTypeNameFieldsRecursively(obj: any): void {
108+
if (!obj || typeof obj !== 'object') return;
109+
110+
if (Array.isArray(obj)) {
111+
obj.forEach(item => this.ensureTypeNameFieldsRecursively(item));
112+
return;
113+
}
114+
115+
if (obj.typeName && typeof obj.typeName === 'object') {
116+
if (!('location' in obj.typeName)) {
117+
obj.typeName.location = undefined;
118+
}
119+
if (!('typemod' in obj.typeName)) {
120+
obj.typeName.typemod = -1;
121+
}
122+
}
123+
124+
Object.values(obj).forEach(value => {
125+
if (value && typeof value === 'object') {
126+
this.ensureTypeNameFieldsRecursively(value);
127+
}
128+
});
129+
}
130+
53131
protected transformArray(items: any[], context?: TransformerContext): any[] {
54132
if (!Array.isArray(items)) return items;
55133
return items.map(item => this.transform(item, context));

0 commit comments

Comments
 (0)