Skip to content

Commit b530f17

Browse files
committed
feat(plpgsql-deparser): add dehydratePlpgsqlAst and demo test
- Add dehydratePlpgsqlAst() function that converts hydrated AST back to string format - For 'assign' kind, reconstructs string from target/value fields (enables AST-level modifications) - For other kinds, uses original string - Add hydrate-demo.test.ts demonstrating full pipeline with big-function.sql: - Parse: 68 expressions parsed (20 assignments, 48 SQL expressions, 0 failures) - Hydrate: Convert query strings to structured objects - Modify: Change assignment targets (v_discount -> v_discount_MODIFIED, v_tax -> v_tax_MODIFIED) and default values (0 -> 888) - Dehydrate: Convert back to string format - Deparse: Output modified PL/pgSQL code This proves the full parse -> hydrate -> modify -> dehydrate -> deparse pipeline works.
1 parent c8e23a1 commit b530f17

File tree

3 files changed

+215
-1
lines changed

3 files changed

+215
-1
lines changed
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { loadModule, parsePlPgSQLSync } from '@libpg-query/parser';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
import { hydratePlpgsqlAst, dehydratePlpgsqlAst, PLpgSQLParseResult, deparseSync } from '../src';
5+
6+
describe('hydrate demonstration with big-function.sql', () => {
7+
beforeAll(async () => {
8+
await loadModule();
9+
});
10+
11+
it('should parse, hydrate, modify, and deparse big-function.sql', () => {
12+
const fixturePath = path.join(__dirname, '../../../__fixtures__/plpgsql-pretty/big-function.sql');
13+
const sql = fs.readFileSync(fixturePath, 'utf-8');
14+
15+
const parsed = parsePlPgSQLSync(sql) as unknown as PLpgSQLParseResult;
16+
17+
console.log('\n=== HYDRATION STATS ===');
18+
const { ast: hydratedAst, errors, stats } = hydratePlpgsqlAst(parsed);
19+
console.log('Total expressions:', stats.totalExpressions);
20+
console.log('Parsed expressions:', stats.parsedExpressions);
21+
console.log('Assignment expressions:', stats.assignmentExpressions);
22+
console.log('SQL expressions:', stats.sqlExpressions);
23+
console.log('Failed expressions:', stats.failedExpressions);
24+
console.log('Raw expressions:', stats.rawExpressions);
25+
26+
if (errors.length > 0) {
27+
console.log('\nErrors:', errors.slice(0, 5));
28+
}
29+
30+
console.log('\n=== SAMPLE HYDRATED EXPRESSIONS ===');
31+
const sampleExprs = collectHydratedExprs(hydratedAst, 5);
32+
sampleExprs.forEach((expr, i) => {
33+
console.log(`\n[${i + 1}] Kind: ${expr.kind}`);
34+
console.log(` Original: "${expr.original}"`);
35+
if (expr.kind === 'assign') {
36+
console.log(` Target: "${expr.target}"`);
37+
console.log(` Value: "${expr.value}"`);
38+
console.log(` Has targetExpr: ${!!expr.targetExpr}`);
39+
console.log(` Has valueExpr: ${!!expr.valueExpr}`);
40+
} else if (expr.kind === 'sql-expr') {
41+
console.log(` Has expr AST: ${!!expr.expr}`);
42+
}
43+
});
44+
45+
console.log('\n=== MODIFYING AST ===');
46+
const modifiedAst = modifyAst(JSON.parse(JSON.stringify(hydratedAst)));
47+
48+
console.log('\n=== DEHYDRATING MODIFIED AST ===');
49+
const dehydratedAst = dehydratePlpgsqlAst(modifiedAst);
50+
51+
console.log('\n=== DEPARSING DEHYDRATED AST ===');
52+
const deparsed = deparseSync(dehydratedAst);
53+
54+
console.log('\n=== VERIFICATION: Changes Applied ===');
55+
expect(deparsed).toContain('v_discount_MODIFIED');
56+
expect(deparsed).toContain('v_tax_MODIFIED');
57+
expect(deparsed).toContain('888');
58+
59+
console.log('Found v_discount_MODIFIED:', deparsed.includes('v_discount_MODIFIED'));
60+
console.log('Found v_tax_MODIFIED:', deparsed.includes('v_tax_MODIFIED'));
61+
console.log('Found 888 (modified default values):', deparsed.includes('888'));
62+
63+
console.log('\n=== DEPARSED OUTPUT (first 2000 chars) ===');
64+
console.log(deparsed.substring(0, 2000));
65+
66+
expect(stats.totalExpressions).toBeGreaterThan(0);
67+
expect(stats.parsedExpressions).toBeGreaterThan(0);
68+
});
69+
});
70+
71+
function collectHydratedExprs(obj: any, limit: number): any[] {
72+
const results: any[] = [];
73+
74+
function walk(node: any): void {
75+
if (results.length >= limit) return;
76+
if (node === null || node === undefined) return;
77+
78+
if (typeof node === 'object') {
79+
if ('PLpgSQL_expr' in node) {
80+
const query = node.PLpgSQL_expr.query;
81+
if (query && typeof query === 'object' && 'kind' in query) {
82+
results.push(query);
83+
}
84+
}
85+
86+
for (const value of Object.values(node)) {
87+
walk(value);
88+
}
89+
}
90+
91+
if (Array.isArray(node)) {
92+
for (const item of node) {
93+
walk(item);
94+
}
95+
}
96+
}
97+
98+
walk(obj);
99+
return results;
100+
}
101+
102+
function modifyAst(ast: any): any {
103+
let modCount = 0;
104+
let assignModCount = 0;
105+
106+
function walk(node: any): void {
107+
if (node === null || node === undefined) return;
108+
109+
if (typeof node === 'object') {
110+
if ('PLpgSQL_expr' in node) {
111+
const query = node.PLpgSQL_expr.query;
112+
113+
if (typeof query === 'object' && query.kind === 'assign') {
114+
if (query.target === 'v_discount' && assignModCount === 0) {
115+
console.log(` Modifying assignment target: "${query.target}" -> "v_discount_MODIFIED"`);
116+
query.target = 'v_discount_MODIFIED';
117+
assignModCount++;
118+
modCount++;
119+
}
120+
if (query.target === 'v_tax' && assignModCount === 1) {
121+
console.log(` Modifying assignment target: "${query.target}" -> "v_tax_MODIFIED"`);
122+
query.target = 'v_tax_MODIFIED';
123+
assignModCount++;
124+
modCount++;
125+
}
126+
if (query.value === '0' && modCount < 5) {
127+
console.log(` Modifying assignment value: "${query.value}" -> "999"`);
128+
query.value = '999';
129+
modCount++;
130+
}
131+
}
132+
133+
if (typeof query === 'object' && query.kind === 'sql-expr') {
134+
if (query.original === '0' && modCount < 8) {
135+
console.log(` Modifying sql-expr value: "${query.original}" -> "888"`);
136+
query.original = '888';
137+
modCount++;
138+
}
139+
}
140+
}
141+
142+
for (const value of Object.values(node)) {
143+
walk(value);
144+
}
145+
}
146+
147+
if (Array.isArray(node)) {
148+
for (const item of node) {
149+
walk(item);
150+
}
151+
}
152+
}
153+
154+
walk(ast);
155+
console.log(` Total modifications: ${modCount}`);
156+
return ast;
157+
}

packages/plpgsql-deparser/src/hydrate.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,3 +349,60 @@ export function getOriginalQuery(query: string | HydratedExprQuery): string {
349349
}
350350
return query.original;
351351
}
352+
353+
export function dehydratePlpgsqlAst<T>(ast: T): T {
354+
return dehydrateNode(ast) as T;
355+
}
356+
357+
function dehydrateNode(node: any): any {
358+
if (node === null || node === undefined) {
359+
return node;
360+
}
361+
362+
if (Array.isArray(node)) {
363+
return node.map(item => dehydrateNode(item));
364+
}
365+
366+
if (typeof node !== 'object') {
367+
return node;
368+
}
369+
370+
if ('PLpgSQL_expr' in node) {
371+
const expr = node.PLpgSQL_expr;
372+
const query = expr.query;
373+
374+
let dehydratedQuery: string;
375+
if (typeof query === 'string') {
376+
dehydratedQuery = query;
377+
} else if (isHydratedExpr(query)) {
378+
dehydratedQuery = dehydrateQuery(query);
379+
} else {
380+
dehydratedQuery = String(query);
381+
}
382+
383+
return {
384+
PLpgSQL_expr: {
385+
...expr,
386+
query: dehydratedQuery,
387+
},
388+
};
389+
}
390+
391+
const result: any = {};
392+
for (const [key, value] of Object.entries(node)) {
393+
result[key] = dehydrateNode(value);
394+
}
395+
return result;
396+
}
397+
398+
function dehydrateQuery(query: HydratedExprQuery): string {
399+
switch (query.kind) {
400+
case 'assign':
401+
return `${query.target} := ${query.value}`;
402+
case 'sql-expr':
403+
case 'sql-stmt':
404+
case 'raw':
405+
default:
406+
return query.original;
407+
}
408+
}

packages/plpgsql-deparser/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ export const deparseFunction = async (
2121
export { PLpgSQLDeparser, PLpgSQLDeparserOptions };
2222
export * from './types';
2323
export * from './hydrate-types';
24-
export { hydratePlpgsqlAst, isHydratedExpr, getOriginalQuery } from './hydrate';
24+
export { hydratePlpgsqlAst, dehydratePlpgsqlAst, isHydratedExpr, getOriginalQuery } from './hydrate';

0 commit comments

Comments
 (0)