Skip to content

Commit 1815fff

Browse files
committed
feat(deparser): add ParseResult support, array handling, and configurable function delimiters
- Add support for ParseResult objects as input to deparser - Add support for arrays of Node objects - Add configurable function delimiter options (functionDelimiter and functionDelimiterFallback) - Update DoStmt, CreateFunctionStmt, and InlineCodeBlock to use configurable delimiters - Export DeparserOptions interface from index.ts - Automatic fallback when delimiter found in function body
1 parent 0c9b9fb commit 1815fff

File tree

2 files changed

+74
-42
lines changed

2 files changed

+74
-42
lines changed

packages/deparser/src/deparser.ts

Lines changed: 72 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,45 @@ import * as t from '@pgsql/types';
88
export interface DeparserOptions {
99
newline?: string;
1010
tab?: string;
11+
// Function body delimiter options
12+
functionDelimiter?: string; // Default: '$$'
13+
// Alternative delimiter when the default is found in the body
14+
functionDelimiterFallback?: string; // Default: '$EOFCODE$'
1115
}
1216

1317
export class Deparser implements DeparserVisitor {
1418
private formatter: SqlFormatter;
1519
private tree: Node[];
20+
private options: DeparserOptions;
1621

17-
constructor(tree: Node | Node[], opts: DeparserOptions = {}) {
22+
constructor(tree: Node | Node[] | t.ParseResult, opts: DeparserOptions = {}) {
1823
this.formatter = new SqlFormatter(opts.newline, opts.tab);
1924

20-
// Handle parsed query objects that contain both version and stmts
25+
// Set default options
26+
this.options = {
27+
functionDelimiter: '$$',
28+
functionDelimiterFallback: '$EOFCODE$',
29+
...opts
30+
};
31+
32+
// Handle ParseResult objects
2133
if (tree && typeof tree === 'object' && !Array.isArray(tree) && 'stmts' in tree) {
22-
this.tree = (tree as any).stmts;
23-
} else {
24-
this.tree = Array.isArray(tree) ? tree : [tree];
25-
}
26-
}
27-
28-
static deparse(query: Node | Node[], opts: DeparserOptions = {}): string {
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)) {
41+
this.tree = tree;
42+
}
43+
// Handle single Node
44+
else {
45+
this.tree = [tree as Node];
46+
}
47+
}
48+
49+
static deparse(query: Node | Node[] | t.ParseResult, opts: DeparserOptions = {}): string {
2950
return new Deparser(query, opts).deparseQuery();
3051
}
3152

@@ -35,6 +56,19 @@ export class Deparser implements DeparserVisitor {
3556
.join(this.formatter.newline() + this.formatter.newline());
3657
}
3758

59+
/**
60+
* Get the appropriate function delimiter based on the body content
61+
* @param body The function body to check
62+
* @returns The delimiter to use
63+
*/
64+
private getFunctionDelimiter(body: string): string {
65+
const delimiter = this.options.functionDelimiter || '$$';
66+
if (body.includes(delimiter)) {
67+
return this.options.functionDelimiterFallback || '$EOFCODE$';
68+
}
69+
return delimiter;
70+
}
71+
3872
deparse(node: Node, context: DeparserContext = { parentNodeTypes: [] }): string | null {
3973
if (node == null) {
4074
return null;
@@ -5165,15 +5199,17 @@ export class Deparser implements DeparserVisitor {
51655199

51665200
if (context.parentNodeTypes.includes('DoStmt')) {
51675201
if (node.defname === 'as') {
5202+
const defElemContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DefElem'] };
5203+
const argValue = node.arg ? this.visit(node.arg, defElemContext) : '';
5204+
51685205
if (Array.isArray(argValue)) {
51695206
const bodyParts = argValue;
5170-
if (bodyParts.length === 1) {
5171-
return `$$${bodyParts[0]}$$`;
5172-
} else {
5173-
return `$$${bodyParts.join('')}$$`;
5174-
}
5207+
const body = bodyParts.join('');
5208+
const delimiter = this.getFunctionDelimiter(body);
5209+
return `${delimiter}${body}${delimiter}`;
51755210
} else {
5176-
return `$$${argValue}$$`;
5211+
const delimiter = this.getFunctionDelimiter(argValue);
5212+
return `${delimiter}${argValue}${delimiter}`;
51775213
}
51785214
}
51795215
return '';
@@ -5194,39 +5230,33 @@ export class Deparser implements DeparserVisitor {
51945230

51955231
if (bodyParts.length === 1) {
51965232
const body = bodyParts[0];
5197-
// Check if body contains $$ to avoid conflicts
5198-
if (body.includes('$$')) {
5199-
return `AS '${body.replace(/'/g, "''")}'`;
5200-
} else {
5201-
return `AS $$${body}$$`;
5202-
}
5233+
const delimiter = this.getFunctionDelimiter(body);
5234+
return `AS ${delimiter}${body}${delimiter}`;
52035235
} else {
5204-
return `AS ${bodyParts.map((part: string) => `$$${part}$$`).join(', ')}`;
5236+
return `AS ${bodyParts.map((part: string) => {
5237+
const delimiter = this.getFunctionDelimiter(part);
5238+
return `${delimiter}${part}${delimiter}`;
5239+
}).join(', ')}`;
52055240
}
52065241
}
52075242
// Handle Array type (legacy support)
52085243
else if (Array.isArray(argValue)) {
52095244
const bodyParts = argValue;
52105245
if (bodyParts.length === 1) {
52115246
const body = bodyParts[0];
5212-
// Check if body contains $$ to avoid conflicts
5213-
if (body.includes('$$')) {
5214-
return `AS '${body.replace(/'/g, "''")}'`;
5215-
} else {
5216-
return `AS $$${body}$$`;
5217-
}
5247+
const delimiter = this.getFunctionDelimiter(body);
5248+
return `AS ${delimiter}${body}${delimiter}`;
52185249
} else {
5219-
return `AS ${bodyParts.map(part => `$$${part}$$`).join(', ')}`;
5250+
return `AS ${bodyParts.map(part => {
5251+
const delimiter = this.getFunctionDelimiter(part);
5252+
return `${delimiter}${part}${delimiter}`;
5253+
}).join(', ')}`;
52205254
}
52215255
}
52225256
// Handle String type (single function body)
52235257
else {
5224-
// Check if argValue contains $$ to avoid conflicts
5225-
if (argValue.includes('$$')) {
5226-
return `AS '${argValue.replace(/'/g, "''")}'`;
5227-
} else {
5228-
return `AS $$${argValue}$$`;
5229-
}
5258+
const delimiter = this.getFunctionDelimiter(argValue);
5259+
return `AS ${delimiter}${argValue}${delimiter}`;
52305260
}
52315261
}
52325262
if (node.defname === 'language') {
@@ -6485,12 +6515,12 @@ export class Deparser implements DeparserVisitor {
64856515
const langValue = this.visit(defElem.arg, doContext);
64866516
processedArgs.push(`LANGUAGE ${langValue}`);
64876517
} else if (defElem.defname === 'as') {
6488-
// Handle code block with dollar quoting
6518+
// Handle code block with configurable delimiter
64896519
const argNodeType = this.getNodeType(defElem.arg);
64906520
if (argNodeType === 'String') {
64916521
const stringNode = this.getNodeData(defElem.arg) as any;
6492-
const dollarTag = this.generateUniqueDollarTag(stringNode.sval);
6493-
processedArgs.push(`${dollarTag}${stringNode.sval}${dollarTag}`);
6522+
const delimiter = this.getFunctionDelimiter(stringNode.sval);
6523+
processedArgs.push(`${delimiter}${stringNode.sval}${delimiter}`);
64946524
} else {
64956525
processedArgs.push(this.visit(defElem.arg, doContext));
64966526
}
@@ -6533,9 +6563,11 @@ export class Deparser implements DeparserVisitor {
65336563

65346564
InlineCodeBlock(node: t.InlineCodeBlock, context: DeparserContext): string {
65356565
if (node.source_text) {
6536-
return `$$${node.source_text}$$`;
6566+
const delimiter = this.getFunctionDelimiter(node.source_text);
6567+
return `${delimiter}${node.source_text}${delimiter}`;
65376568
}
6538-
return '$$$$';
6569+
const delimiter = this.options.functionDelimiter || '$$';
6570+
return `${delimiter}${delimiter}`;
65396571
}
65406572

65416573
CallContext(node: t.CallContext, context: DeparserContext): string {

packages/deparser/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Deparser } from "./deparser";
1+
import { Deparser, DeparserOptions } from "./deparser";
22

33
const deparse = Deparser.deparse;
4-
export { deparse, Deparser };
4+
export { deparse, Deparser, DeparserOptions };

0 commit comments

Comments
 (0)