Skip to content

Commit a240d13

Browse files
committed
refactor(deparser): improve entry point handling with proper type guards
- Add comprehensive documentation explaining all entry points and structures - Add type guards isParseResult() and isWrappedParseResult() for type safety - Add ParseResult method to handle wrapped ParseResult nodes properly - Update RawStmt method to handle empty statements and semicolon addition - Refactor constructor to use type guards and wrap bare ParseResult for consistency - Update deparseQuery() to filter empty results - Remove unused stmt() and version() methods - Handle empty objects gracefully in visit() method - Add comprehensive test suite with 14 test cases covering all scenarios BREAKING CHANGE: Removed unused stmt() and version() methods. Use deparse() with appropriate node types instead. The deparser now properly handles: 1. ParseResult from libpg-query (bare or wrapped) 2. Wrapped RawStmt nodes 3. Arrays of Nodes 4. Single Node statements Note: ParseResult.stmts contains RawStmt objects directly (not wrapped as nodes)
1 parent 7d274a5 commit a240d13

File tree

3 files changed

+231
-44
lines changed

3 files changed

+231
-44
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@
4343
"ts-jest": "^29.1.1",
4444
"ts-node": "^10.9.2",
4545
"typescript": "^5.1.6"
46-
}
47-
}
46+
},
47+
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
48+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { parse } from 'libpg-query';
2+
import { Deparser } from '../src/deparser';
3+
import * as t from '@pgsql/types';
4+
5+
describe('Entry Point Refactoring', () => {
6+
const sql = `
7+
SELECT * FROM users WHERE id = 1;
8+
INSERT INTO logs (message) VALUES ('test');
9+
`;
10+
11+
let parseResult: t.ParseResult;
12+
13+
beforeAll(async () => {
14+
parseResult = await parse(sql);
15+
});
16+
17+
describe('ParseResult handling', () => {
18+
it('should handle bare ParseResult (duck-typed)', () => {
19+
const result = Deparser.deparse(parseResult);
20+
expect(result).toContain('SELECT * FROM users WHERE id = 1');
21+
expect(result).toContain('INSERT INTO logs (message) VALUES (\'test\')');
22+
});
23+
24+
it('should handle wrapped ParseResult', () => {
25+
const wrappedParseResult = { ParseResult: parseResult } as t.Node;
26+
const result = Deparser.deparse(wrappedParseResult);
27+
expect(result).toContain('SELECT * FROM users WHERE id = 1');
28+
expect(result).toContain('INSERT INTO logs (message) VALUES (\'test\')');
29+
});
30+
31+
it('should preserve semicolons based on stmt_len', () => {
32+
const result = Deparser.deparse(parseResult);
33+
// The first statement should have a semicolon if stmt_len is set
34+
const lines = result.split('\n').filter(line => line.trim());
35+
if (parseResult.stmts?.[0]?.stmt_len) {
36+
expect(lines[0]).toMatch(/;$/);
37+
}
38+
});
39+
});
40+
41+
describe('RawStmt handling', () => {
42+
it('should handle wrapped RawStmt', () => {
43+
const rawStmt = parseResult.stmts![0];
44+
const wrappedRawStmt = { RawStmt: rawStmt } as t.Node;
45+
const result = Deparser.deparse(wrappedRawStmt);
46+
expect(result).toContain('SELECT * FROM users WHERE id = 1');
47+
});
48+
49+
it('should add semicolon when stmt_len is present', () => {
50+
const rawStmt = parseResult.stmts![0];
51+
if (rawStmt.stmt_len) {
52+
const wrappedRawStmt = { RawStmt: rawStmt } as t.Node;
53+
const result = Deparser.deparse(wrappedRawStmt);
54+
expect(result).toMatch(/;$/);
55+
}
56+
});
57+
});
58+
59+
describe('Array handling', () => {
60+
it('should handle array of statements', () => {
61+
const statements = parseResult.stmts!.map(rawStmt => rawStmt.stmt!);
62+
const result = Deparser.deparse(statements);
63+
expect(result).toContain('SELECT * FROM users WHERE id = 1');
64+
expect(result).toContain('INSERT INTO logs (message) VALUES (\'test\')');
65+
});
66+
67+
it('should handle array of wrapped nodes', () => {
68+
const wrappedNodes = parseResult.stmts!.map(rawStmt => ({ RawStmt: rawStmt } as t.Node));
69+
const result = Deparser.deparse(wrappedNodes);
70+
expect(result).toContain('SELECT * FROM users WHERE id = 1');
71+
expect(result).toContain('INSERT INTO logs (message) VALUES (\'test\')');
72+
});
73+
});
74+
75+
describe('Single node handling', () => {
76+
it('should handle single statement', () => {
77+
const stmt = parseResult.stmts![0].stmt!;
78+
const result = Deparser.deparse(stmt);
79+
expect(result).toContain('SELECT * FROM users WHERE id = 1');
80+
});
81+
});
82+
83+
describe('Edge cases', () => {
84+
it('should handle empty ParseResult', () => {
85+
const emptyParseResult: t.ParseResult = { stmts: [] };
86+
const result = Deparser.deparse(emptyParseResult);
87+
expect(result).toBe('');
88+
});
89+
90+
it('should handle ParseResult with undefined stmts', () => {
91+
const parseResultNoStmts: t.ParseResult = {};
92+
const result = Deparser.deparse(parseResultNoStmts);
93+
expect(result).toBe('');
94+
});
95+
96+
it('should handle RawStmt with undefined stmt', () => {
97+
const rawStmtNoStmt: t.RawStmt = {};
98+
const wrappedRawStmt = { RawStmt: rawStmtNoStmt } as t.Node;
99+
const result = Deparser.deparse(wrappedRawStmt);
100+
expect(result).toBe('');
101+
});
102+
103+
it('should handle empty array', () => {
104+
const result = Deparser.deparse([]);
105+
expect(result).toBe('');
106+
});
107+
});
108+
109+
describe('Type guards', () => {
110+
it('should correctly identify bare ParseResult', () => {
111+
const deparser = new Deparser(parseResult);
112+
// The tree should contain a wrapped ParseResult
113+
expect(deparser['tree'].length).toBe(1);
114+
const node = deparser['tree'][0];
115+
expect(node).toHaveProperty('ParseResult');
116+
});
117+
118+
it('should not treat wrapped ParseResult as bare', () => {
119+
const wrapped = { ParseResult: parseResult } as t.Node;
120+
const deparser = new Deparser(wrapped);
121+
// The tree should contain the wrapped node as-is
122+
expect(deparser['tree'].length).toBe(1);
123+
expect(deparser['tree'][0]).toBe(wrapped);
124+
});
125+
});
126+
});

packages/deparser/src/deparser.ts

Lines changed: 102 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,48 @@ export interface DeparserOptions {
1414
functionDelimiterFallback?: string; // Default: '$EOFCODE$'
1515
}
1616

17+
// Type guards for better type safety
18+
function isParseResult(obj: any): obj is t.ParseResult {
19+
// A ParseResult is an object that could have stmts (but not required)
20+
// and is not already wrapped as a Node
21+
// IMPORTANT: ParseResult.stmts contains RawStmt objects directly (not wrapped)
22+
// Example: { version: 170004, stmts: [{ stmt: {...}, stmt_len: 32 }] }
23+
return obj && typeof obj === 'object' &&
24+
!Array.isArray(obj) &&
25+
!('ParseResult' in obj) &&
26+
!('RawStmt' in obj) &&
27+
// Check if it looks like a ParseResult (has stmts or version)
28+
('stmts' in obj || 'version' in obj);
29+
}
30+
31+
function isWrappedParseResult(obj: any): obj is { ParseResult: t.ParseResult } {
32+
return obj && typeof obj === 'object' && 'ParseResult' in obj;
33+
}
34+
35+
/**
36+
* Deparser - Converts PostgreSQL AST nodes back to SQL strings
37+
*
38+
* Entry Points:
39+
* 1. ParseResult (from libpg-query) - The complete parse result
40+
* Structure: { version: number, stmts: RawStmt[] }
41+
* Note: stmts contains RawStmt objects directly, NOT wrapped as { RawStmt: ... }
42+
* Example: { version: 170004, stmts: [{ stmt: {...}, stmt_len: 32 }] }
43+
*
44+
* 2. Wrapped ParseResult - When explicitly wrapped as a Node
45+
* Structure: { ParseResult: { version: number, stmts: RawStmt[] } }
46+
*
47+
* 3. Wrapped RawStmt - When explicitly wrapped as a Node
48+
* Structure: { RawStmt: { stmt: Node, stmt_len?: number } }
49+
*
50+
* 4. Array of Nodes - Multiple statements to deparse
51+
* Can be: Node[] (e.g., SelectStmt, InsertStmt, etc.)
52+
*
53+
* 5. Single Node - Individual statement node
54+
* Example: { SelectStmt: {...} }, { InsertStmt: {...} }, etc.
55+
*
56+
* The deparser automatically detects bare ParseResult objects for backward
57+
* compatibility and wraps them internally for consistent processing.
58+
*/
1759
export class Deparser implements DeparserVisitor {
1860
private formatter: SqlFormatter;
1961
private tree: Node[];
@@ -29,30 +71,44 @@ export class Deparser implements DeparserVisitor {
2971
...opts
3072
};
3173

32-
// Handle ParseResult objects
33-
if (tree && typeof tree === 'object' && !Array.isArray(tree) && 'stmts' in tree) {
34-
// This is a ParseResult
35-
const parseResult = tree as t.ParseResult;
36-
// Extract the actual Node from each RawStmt
37-
this.tree = (parseResult.stmts || []).map(rawStmt => rawStmt.stmt).filter(stmt => stmt !== undefined) as Node[];
38-
}
39-
// Handle arrays of Node
40-
else if (Array.isArray(tree)) {
74+
// Handle different input types
75+
if (isParseResult(tree)) {
76+
// Duck-typed ParseResult (backward compatibility)
77+
// Wrap it as a proper Node for consistent handling
78+
this.tree = [{ ParseResult: tree } as Node];
79+
} else if (Array.isArray(tree)) {
80+
// Array of Nodes
4181
this.tree = tree;
42-
}
43-
// Handle single Node
44-
else {
82+
} else {
83+
// Single Node (including wrapped ParseResult)
4584
this.tree = [tree as Node];
4685
}
4786
}
4887

88+
/**
89+
* Static method to deparse PostgreSQL AST nodes to SQL
90+
* @param query - Can be:
91+
* - ParseResult from libpg-query (e.g., { version: 170004, stmts: [...] })
92+
* - Wrapped ParseResult node (e.g., { ParseResult: {...} })
93+
* - Wrapped RawStmt node (e.g., { RawStmt: {...} })
94+
* - Array of Nodes
95+
* - Single Node (e.g., { SelectStmt: {...} })
96+
* @param opts - Deparser options for formatting
97+
* @returns The deparsed SQL string
98+
*/
4999
static deparse(query: Node | Node[] | t.ParseResult, opts: DeparserOptions = {}): string {
50100
return new Deparser(query, opts).deparseQuery();
51101
}
52102

53103
deparseQuery(): string {
54104
return this.tree
55-
.map(node => this.deparse(node))
105+
.map(node => {
106+
// All nodes should go through the standard deparse method
107+
// which will route to the appropriate handler
108+
const result = this.deparse(node);
109+
return result || '';
110+
})
111+
.filter(result => result !== '')
56112
.join(this.formatter.newline() + this.formatter.newline());
57113
}
58114

@@ -88,6 +144,12 @@ export class Deparser implements DeparserVisitor {
88144

89145
visit(node: Node, context: DeparserContext = { parentNodeTypes: [] }): string {
90146
const nodeType = this.getNodeType(node);
147+
148+
// Handle empty objects
149+
if (!nodeType) {
150+
return '';
151+
}
152+
91153
const nodeData = this.getNodeData(node);
92154

93155
const methodName = nodeType as keyof this;
@@ -116,27 +178,37 @@ export class Deparser implements DeparserVisitor {
116178
return node;
117179
}
118180

119-
RawStmt(node: t.RawStmt, context: DeparserContext): string {
120-
if (node.stmt_len) {
121-
return this.deparse(node.stmt, context) + ';';
181+
ParseResult(node: t.ParseResult, context: DeparserContext): string {
182+
if (!node.stmts || node.stmts.length === 0) {
183+
return '';
122184
}
123-
return this.deparse(node.stmt, context);
185+
186+
// Deparse each RawStmt in the ParseResult
187+
// Note: node.stmts contains RawStmt objects directly (not wrapped)
188+
// Each element has structure: { stmt: Node, stmt_len?: number, stmt_location?: number }
189+
return node.stmts
190+
.filter((rawStmt: t.RawStmt) => rawStmt != null)
191+
.map((rawStmt: t.RawStmt) => this.RawStmt(rawStmt, context))
192+
.filter((result: string) => result !== '')
193+
.join(this.formatter.newline() + this.formatter.newline());
124194
}
125195

126-
stmt(node: any, context: DeparserContext = { parentNodeTypes: [] }): string {
127-
// Handle stmt wrapper nodes that contain the actual statement
128-
const keys = Object.keys(node);
129-
if (keys.length === 1) {
130-
const statementType = keys[0];
131-
const methodName = statementType as keyof this;
132-
if (typeof this[methodName] === 'function') {
133-
return (this[methodName] as any)(node[statementType], context);
134-
}
135-
throw new Error(`Deparser does not handle statement type: ${statementType}`);
196+
RawStmt(node: t.RawStmt, context: DeparserContext): string {
197+
if (!node.stmt) {
198+
return '';
136199
}
137-
return '';
200+
201+
const deparsedStmt = this.deparse(node.stmt, context);
202+
203+
// Add semicolon if stmt_len is provided (indicates it had one in original)
204+
if (node.stmt_len) {
205+
return deparsedStmt + ';';
206+
}
207+
return deparsedStmt;
138208
}
139209

210+
211+
140212
SelectStmt(node: t.SelectStmt, context: DeparserContext): string {
141213
const output: string[] = [];
142214

@@ -1326,7 +1398,7 @@ export class Deparser implements DeparserVisitor {
13261398

13271399
let args: string | null = null;
13281400
if (node.typmods) {
1329-
const isInterval = names.some(name => {
1401+
const isInterval = names.some((name: any) => {
13301402
const nameStr = typeof name === 'string' ? name : (name.String?.sval || name.String?.str);
13311403
return nameStr === 'interval';
13321404
});
@@ -10599,18 +10671,6 @@ export class Deparser implements DeparserVisitor {
1059910671
return output.join(' ');
1060010672
}
1060110673

10602-
version(node: any, context: any): string {
10603-
// Handle version node - typically just return the version number
10604-
if (typeof node === 'number') {
10605-
return node.toString();
10606-
}
10607-
if (typeof node === 'string') {
10608-
return node;
10609-
}
10610-
if (node && typeof node === 'object' && node.version) {
10611-
return node.version.toString();
10612-
}
10613-
return '';
10614-
}
10674+
1061510675

1061610676
}

0 commit comments

Comments
 (0)