pgsql-deparser 17.1.0 → 17.4.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/deparser.d.ts +48 -4
- package/deparser.js +145 -71
- package/esm/deparser.js +145 -71
- package/index.d.ts +2 -2
- package/package.json +3 -3
package/deparser.d.ts
CHANGED
|
@@ -4,19 +4,64 @@ import * as t from '@pgsql/types';
|
|
|
4
4
|
export interface DeparserOptions {
|
|
5
5
|
newline?: string;
|
|
6
6
|
tab?: string;
|
|
7
|
+
functionDelimiter?: string;
|
|
8
|
+
functionDelimiterFallback?: string;
|
|
7
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Deparser - Converts PostgreSQL AST nodes back to SQL strings
|
|
12
|
+
*
|
|
13
|
+
* Entry Points:
|
|
14
|
+
* 1. ParseResult (from libpg-query) - The complete parse result
|
|
15
|
+
* Structure: { version: number, stmts: RawStmt[] }
|
|
16
|
+
* Note: stmts is "repeated RawStmt" in protobuf, so array contains RawStmt
|
|
17
|
+
* objects inline (not wrapped as { RawStmt: ... } nodes)
|
|
18
|
+
* Example: { version: 170004, stmts: [{ stmt: {...}, stmt_len: 32 }] }
|
|
19
|
+
*
|
|
20
|
+
* 2. Wrapped ParseResult - When explicitly wrapped as a Node
|
|
21
|
+
* Structure: { ParseResult: { version: number, stmts: RawStmt[] } }
|
|
22
|
+
*
|
|
23
|
+
* 3. Wrapped RawStmt - When explicitly wrapped as a Node
|
|
24
|
+
* Structure: { RawStmt: { stmt: Node, stmt_len?: number } }
|
|
25
|
+
*
|
|
26
|
+
* 4. Array of Nodes - Multiple statements to deparse
|
|
27
|
+
* Can be: Node[] (e.g., SelectStmt, InsertStmt, etc.)
|
|
28
|
+
*
|
|
29
|
+
* 5. Single Node - Individual statement node
|
|
30
|
+
* Example: { SelectStmt: {...} }, { InsertStmt: {...} }, etc.
|
|
31
|
+
*
|
|
32
|
+
* The deparser automatically detects bare ParseResult objects for backward
|
|
33
|
+
* compatibility and wraps them internally for consistent processing.
|
|
34
|
+
*/
|
|
8
35
|
export declare class Deparser implements DeparserVisitor {
|
|
9
36
|
private formatter;
|
|
10
37
|
private tree;
|
|
11
|
-
|
|
12
|
-
|
|
38
|
+
private options;
|
|
39
|
+
constructor(tree: Node | Node[] | t.ParseResult, opts?: DeparserOptions);
|
|
40
|
+
/**
|
|
41
|
+
* Static method to deparse PostgreSQL AST nodes to SQL
|
|
42
|
+
* @param query - Can be:
|
|
43
|
+
* - ParseResult from libpg-query (e.g., { version: 170004, stmts: [...] })
|
|
44
|
+
* - Wrapped ParseResult node (e.g., { ParseResult: {...} })
|
|
45
|
+
* - Wrapped RawStmt node (e.g., { RawStmt: {...} })
|
|
46
|
+
* - Array of Nodes
|
|
47
|
+
* - Single Node (e.g., { SelectStmt: {...} })
|
|
48
|
+
* @param opts - Deparser options for formatting
|
|
49
|
+
* @returns The deparsed SQL string
|
|
50
|
+
*/
|
|
51
|
+
static deparse(query: Node | Node[] | t.ParseResult, opts?: DeparserOptions): string;
|
|
13
52
|
deparseQuery(): string;
|
|
53
|
+
/**
|
|
54
|
+
* Get the appropriate function delimiter based on the body content
|
|
55
|
+
* @param body The function body to check
|
|
56
|
+
* @returns The delimiter to use
|
|
57
|
+
*/
|
|
58
|
+
private getFunctionDelimiter;
|
|
14
59
|
deparse(node: Node, context?: DeparserContext): string | null;
|
|
15
60
|
visit(node: Node, context?: DeparserContext): string;
|
|
16
61
|
getNodeType(node: Node): string;
|
|
17
62
|
getNodeData(node: Node): any;
|
|
63
|
+
ParseResult(node: t.ParseResult, context: DeparserContext): string;
|
|
18
64
|
RawStmt(node: t.RawStmt, context: DeparserContext): string;
|
|
19
|
-
stmt(node: any, context?: DeparserContext): string;
|
|
20
65
|
SelectStmt(node: t.SelectStmt, context: DeparserContext): string;
|
|
21
66
|
A_Expr(node: t.A_Expr, context: DeparserContext): string;
|
|
22
67
|
deparseOperatorName(name: t.Node[]): string;
|
|
@@ -249,5 +294,4 @@ export declare class Deparser implements DeparserVisitor {
|
|
|
249
294
|
AlterObjectSchemaStmt(node: t.AlterObjectSchemaStmt, context: DeparserContext): string;
|
|
250
295
|
AlterRoleSetStmt(node: t.AlterRoleSetStmt, context: DeparserContext): string;
|
|
251
296
|
CreateForeignTableStmt(node: t.CreateForeignTableStmt, context: DeparserContext): string;
|
|
252
|
-
version(node: any, context: any): string;
|
|
253
297
|
}
|
package/deparser.js
CHANGED
|
@@ -4,27 +4,112 @@ exports.Deparser = void 0;
|
|
|
4
4
|
const sql_formatter_1 = require("./utils/sql-formatter");
|
|
5
5
|
const quote_utils_1 = require("./utils/quote-utils");
|
|
6
6
|
const list_utils_1 = require("./utils/list-utils");
|
|
7
|
+
// Type guards for better type safety
|
|
8
|
+
function isParseResult(obj) {
|
|
9
|
+
// A ParseResult is an object that could have stmts (but not required)
|
|
10
|
+
// and is not already wrapped as a Node
|
|
11
|
+
// IMPORTANT: ParseResult.stmts is "repeated RawStmt" in protobuf, meaning
|
|
12
|
+
// the array contains RawStmt objects inline (not wrapped as { RawStmt: ... })
|
|
13
|
+
// Example: { version: 170004, stmts: [{ stmt: {...}, stmt_len: 32 }] }
|
|
14
|
+
return obj && typeof obj === 'object' &&
|
|
15
|
+
!Array.isArray(obj) &&
|
|
16
|
+
!('ParseResult' in obj) &&
|
|
17
|
+
!('RawStmt' in obj) &&
|
|
18
|
+
// Check if it looks like a ParseResult (has stmts or version)
|
|
19
|
+
('stmts' in obj || 'version' in obj);
|
|
20
|
+
}
|
|
21
|
+
function isWrappedParseResult(obj) {
|
|
22
|
+
return obj && typeof obj === 'object' && 'ParseResult' in obj;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Deparser - Converts PostgreSQL AST nodes back to SQL strings
|
|
26
|
+
*
|
|
27
|
+
* Entry Points:
|
|
28
|
+
* 1. ParseResult (from libpg-query) - The complete parse result
|
|
29
|
+
* Structure: { version: number, stmts: RawStmt[] }
|
|
30
|
+
* Note: stmts is "repeated RawStmt" in protobuf, so array contains RawStmt
|
|
31
|
+
* objects inline (not wrapped as { RawStmt: ... } nodes)
|
|
32
|
+
* Example: { version: 170004, stmts: [{ stmt: {...}, stmt_len: 32 }] }
|
|
33
|
+
*
|
|
34
|
+
* 2. Wrapped ParseResult - When explicitly wrapped as a Node
|
|
35
|
+
* Structure: { ParseResult: { version: number, stmts: RawStmt[] } }
|
|
36
|
+
*
|
|
37
|
+
* 3. Wrapped RawStmt - When explicitly wrapped as a Node
|
|
38
|
+
* Structure: { RawStmt: { stmt: Node, stmt_len?: number } }
|
|
39
|
+
*
|
|
40
|
+
* 4. Array of Nodes - Multiple statements to deparse
|
|
41
|
+
* Can be: Node[] (e.g., SelectStmt, InsertStmt, etc.)
|
|
42
|
+
*
|
|
43
|
+
* 5. Single Node - Individual statement node
|
|
44
|
+
* Example: { SelectStmt: {...} }, { InsertStmt: {...} }, etc.
|
|
45
|
+
*
|
|
46
|
+
* The deparser automatically detects bare ParseResult objects for backward
|
|
47
|
+
* compatibility and wraps them internally for consistent processing.
|
|
48
|
+
*/
|
|
7
49
|
class Deparser {
|
|
8
50
|
formatter;
|
|
9
51
|
tree;
|
|
52
|
+
options;
|
|
10
53
|
constructor(tree, opts = {}) {
|
|
11
54
|
this.formatter = new sql_formatter_1.SqlFormatter(opts.newline, opts.tab);
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
55
|
+
// Set default options
|
|
56
|
+
this.options = {
|
|
57
|
+
functionDelimiter: '$$',
|
|
58
|
+
functionDelimiterFallback: '$EOFCODE$',
|
|
59
|
+
...opts
|
|
60
|
+
};
|
|
61
|
+
// Handle different input types
|
|
62
|
+
if (isParseResult(tree)) {
|
|
63
|
+
// Duck-typed ParseResult (backward compatibility)
|
|
64
|
+
// Wrap it as a proper Node for consistent handling
|
|
65
|
+
this.tree = [{ ParseResult: tree }];
|
|
15
66
|
}
|
|
16
|
-
else {
|
|
17
|
-
|
|
67
|
+
else if (Array.isArray(tree)) {
|
|
68
|
+
// Array of Nodes
|
|
69
|
+
this.tree = tree;
|
|
18
70
|
}
|
|
19
|
-
|
|
71
|
+
else {
|
|
72
|
+
// Single Node (including wrapped ParseResult)
|
|
73
|
+
this.tree = [tree];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Static method to deparse PostgreSQL AST nodes to SQL
|
|
78
|
+
* @param query - Can be:
|
|
79
|
+
* - ParseResult from libpg-query (e.g., { version: 170004, stmts: [...] })
|
|
80
|
+
* - Wrapped ParseResult node (e.g., { ParseResult: {...} })
|
|
81
|
+
* - Wrapped RawStmt node (e.g., { RawStmt: {...} })
|
|
82
|
+
* - Array of Nodes
|
|
83
|
+
* - Single Node (e.g., { SelectStmt: {...} })
|
|
84
|
+
* @param opts - Deparser options for formatting
|
|
85
|
+
* @returns The deparsed SQL string
|
|
86
|
+
*/
|
|
20
87
|
static deparse(query, opts = {}) {
|
|
21
88
|
return new Deparser(query, opts).deparseQuery();
|
|
22
89
|
}
|
|
23
90
|
deparseQuery() {
|
|
24
91
|
return this.tree
|
|
25
|
-
.map(node =>
|
|
92
|
+
.map(node => {
|
|
93
|
+
// All nodes should go through the standard deparse method
|
|
94
|
+
// which will route to the appropriate handler
|
|
95
|
+
const result = this.deparse(node);
|
|
96
|
+
return result || '';
|
|
97
|
+
})
|
|
98
|
+
.filter(result => result !== '')
|
|
26
99
|
.join(this.formatter.newline() + this.formatter.newline());
|
|
27
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Get the appropriate function delimiter based on the body content
|
|
103
|
+
* @param body The function body to check
|
|
104
|
+
* @returns The delimiter to use
|
|
105
|
+
*/
|
|
106
|
+
getFunctionDelimiter(body) {
|
|
107
|
+
const delimiter = this.options.functionDelimiter || '$$';
|
|
108
|
+
if (body.includes(delimiter)) {
|
|
109
|
+
return this.options.functionDelimiterFallback || '$EOFCODE$';
|
|
110
|
+
}
|
|
111
|
+
return delimiter;
|
|
112
|
+
}
|
|
28
113
|
deparse(node, context = { parentNodeTypes: [] }) {
|
|
29
114
|
if (node == null) {
|
|
30
115
|
return null;
|
|
@@ -42,6 +127,10 @@ class Deparser {
|
|
|
42
127
|
}
|
|
43
128
|
visit(node, context = { parentNodeTypes: [] }) {
|
|
44
129
|
const nodeType = this.getNodeType(node);
|
|
130
|
+
// Handle empty objects
|
|
131
|
+
if (!nodeType) {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
45
134
|
const nodeData = this.getNodeData(node);
|
|
46
135
|
const methodName = nodeType;
|
|
47
136
|
if (typeof this[methodName] === 'function') {
|
|
@@ -64,24 +153,29 @@ class Deparser {
|
|
|
64
153
|
}
|
|
65
154
|
return node;
|
|
66
155
|
}
|
|
67
|
-
|
|
68
|
-
if (node.
|
|
69
|
-
return
|
|
156
|
+
ParseResult(node, context) {
|
|
157
|
+
if (!node.stmts || node.stmts.length === 0) {
|
|
158
|
+
return '';
|
|
70
159
|
}
|
|
71
|
-
|
|
160
|
+
// Deparse each RawStmt in the ParseResult
|
|
161
|
+
// Note: node.stmts is "repeated RawStmt" so contains RawStmt objects inline
|
|
162
|
+
// Each element has structure: { stmt: Node, stmt_len?: number, stmt_location?: number }
|
|
163
|
+
return node.stmts
|
|
164
|
+
.filter((rawStmt) => rawStmt != null)
|
|
165
|
+
.map((rawStmt) => this.RawStmt(rawStmt, context))
|
|
166
|
+
.filter((result) => result !== '')
|
|
167
|
+
.join(this.formatter.newline() + this.formatter.newline());
|
|
72
168
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (keys.length === 1) {
|
|
77
|
-
const statementType = keys[0];
|
|
78
|
-
const methodName = statementType;
|
|
79
|
-
if (typeof this[methodName] === 'function') {
|
|
80
|
-
return this[methodName](node[statementType], context);
|
|
81
|
-
}
|
|
82
|
-
throw new Error(`Deparser does not handle statement type: ${statementType}`);
|
|
169
|
+
RawStmt(node, context) {
|
|
170
|
+
if (!node.stmt) {
|
|
171
|
+
return '';
|
|
83
172
|
}
|
|
84
|
-
|
|
173
|
+
const deparsedStmt = this.deparse(node.stmt, context);
|
|
174
|
+
// Add semicolon if stmt_len is provided (indicates it had one in original)
|
|
175
|
+
if (node.stmt_len) {
|
|
176
|
+
return deparsedStmt + ';';
|
|
177
|
+
}
|
|
178
|
+
return deparsedStmt;
|
|
85
179
|
}
|
|
86
180
|
SelectStmt(node, context) {
|
|
87
181
|
const output = [];
|
|
@@ -1193,7 +1287,7 @@ class Deparser {
|
|
|
1193
1287
|
}
|
|
1194
1288
|
let args = null;
|
|
1195
1289
|
if (node.typmods) {
|
|
1196
|
-
const isInterval = names.some(name => {
|
|
1290
|
+
const isInterval = names.some((name) => {
|
|
1197
1291
|
const nameStr = typeof name === 'string' ? name : (name.String?.sval || name.String?.str);
|
|
1198
1292
|
return nameStr === 'interval';
|
|
1199
1293
|
});
|
|
@@ -4789,17 +4883,17 @@ class Deparser {
|
|
|
4789
4883
|
}
|
|
4790
4884
|
if (context.parentNodeTypes.includes('DoStmt')) {
|
|
4791
4885
|
if (node.defname === 'as') {
|
|
4886
|
+
const defElemContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DefElem'] };
|
|
4887
|
+
const argValue = node.arg ? this.visit(node.arg, defElemContext) : '';
|
|
4792
4888
|
if (Array.isArray(argValue)) {
|
|
4793
4889
|
const bodyParts = argValue;
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
}
|
|
4797
|
-
else {
|
|
4798
|
-
return `$$${bodyParts.join('')}$$`;
|
|
4799
|
-
}
|
|
4890
|
+
const body = bodyParts.join('');
|
|
4891
|
+
const delimiter = this.getFunctionDelimiter(body);
|
|
4892
|
+
return `${delimiter}${body}${delimiter}`;
|
|
4800
4893
|
}
|
|
4801
4894
|
else {
|
|
4802
|
-
|
|
4895
|
+
const delimiter = this.getFunctionDelimiter(argValue);
|
|
4896
|
+
return `${delimiter}${argValue}${delimiter}`;
|
|
4803
4897
|
}
|
|
4804
4898
|
}
|
|
4805
4899
|
return '';
|
|
@@ -4818,16 +4912,14 @@ class Deparser {
|
|
|
4818
4912
|
});
|
|
4819
4913
|
if (bodyParts.length === 1) {
|
|
4820
4914
|
const body = bodyParts[0];
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
return `AS '${body.replace(/'/g, "''")}'`;
|
|
4824
|
-
}
|
|
4825
|
-
else {
|
|
4826
|
-
return `AS $$${body}$$`;
|
|
4827
|
-
}
|
|
4915
|
+
const delimiter = this.getFunctionDelimiter(body);
|
|
4916
|
+
return `AS ${delimiter}${body}${delimiter}`;
|
|
4828
4917
|
}
|
|
4829
4918
|
else {
|
|
4830
|
-
return `AS ${bodyParts.map((part) =>
|
|
4919
|
+
return `AS ${bodyParts.map((part) => {
|
|
4920
|
+
const delimiter = this.getFunctionDelimiter(part);
|
|
4921
|
+
return `${delimiter}${part}${delimiter}`;
|
|
4922
|
+
}).join(', ')}`;
|
|
4831
4923
|
}
|
|
4832
4924
|
}
|
|
4833
4925
|
// Handle Array type (legacy support)
|
|
@@ -4835,27 +4927,20 @@ class Deparser {
|
|
|
4835
4927
|
const bodyParts = argValue;
|
|
4836
4928
|
if (bodyParts.length === 1) {
|
|
4837
4929
|
const body = bodyParts[0];
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
return `AS '${body.replace(/'/g, "''")}'`;
|
|
4841
|
-
}
|
|
4842
|
-
else {
|
|
4843
|
-
return `AS $$${body}$$`;
|
|
4844
|
-
}
|
|
4930
|
+
const delimiter = this.getFunctionDelimiter(body);
|
|
4931
|
+
return `AS ${delimiter}${body}${delimiter}`;
|
|
4845
4932
|
}
|
|
4846
4933
|
else {
|
|
4847
|
-
return `AS ${bodyParts.map(part =>
|
|
4934
|
+
return `AS ${bodyParts.map(part => {
|
|
4935
|
+
const delimiter = this.getFunctionDelimiter(part);
|
|
4936
|
+
return `${delimiter}${part}${delimiter}`;
|
|
4937
|
+
}).join(', ')}`;
|
|
4848
4938
|
}
|
|
4849
4939
|
}
|
|
4850
4940
|
// Handle String type (single function body)
|
|
4851
4941
|
else {
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
return `AS '${argValue.replace(/'/g, "''")}'`;
|
|
4855
|
-
}
|
|
4856
|
-
else {
|
|
4857
|
-
return `AS $$${argValue}$$`;
|
|
4858
|
-
}
|
|
4942
|
+
const delimiter = this.getFunctionDelimiter(argValue);
|
|
4943
|
+
return `AS ${delimiter}${argValue}${delimiter}`;
|
|
4859
4944
|
}
|
|
4860
4945
|
}
|
|
4861
4946
|
if (node.defname === 'language') {
|
|
@@ -5965,12 +6050,12 @@ class Deparser {
|
|
|
5965
6050
|
processedArgs.push(`LANGUAGE ${langValue}`);
|
|
5966
6051
|
}
|
|
5967
6052
|
else if (defElem.defname === 'as') {
|
|
5968
|
-
// Handle code block with
|
|
6053
|
+
// Handle code block with configurable delimiter
|
|
5969
6054
|
const argNodeType = this.getNodeType(defElem.arg);
|
|
5970
6055
|
if (argNodeType === 'String') {
|
|
5971
6056
|
const stringNode = this.getNodeData(defElem.arg);
|
|
5972
|
-
const
|
|
5973
|
-
processedArgs.push(`${
|
|
6057
|
+
const delimiter = this.getFunctionDelimiter(stringNode.sval);
|
|
6058
|
+
processedArgs.push(`${delimiter}${stringNode.sval}${delimiter}`);
|
|
5974
6059
|
}
|
|
5975
6060
|
else {
|
|
5976
6061
|
processedArgs.push(this.visit(defElem.arg, doContext));
|
|
@@ -6004,9 +6089,11 @@ class Deparser {
|
|
|
6004
6089
|
}
|
|
6005
6090
|
InlineCodeBlock(node, context) {
|
|
6006
6091
|
if (node.source_text) {
|
|
6007
|
-
|
|
6092
|
+
const delimiter = this.getFunctionDelimiter(node.source_text);
|
|
6093
|
+
return `${delimiter}${node.source_text}${delimiter}`;
|
|
6008
6094
|
}
|
|
6009
|
-
|
|
6095
|
+
const delimiter = this.options.functionDelimiter || '$$';
|
|
6096
|
+
return `${delimiter}${delimiter}`;
|
|
6010
6097
|
}
|
|
6011
6098
|
CallContext(node, context) {
|
|
6012
6099
|
if (node.atomic !== undefined) {
|
|
@@ -9589,18 +9676,5 @@ class Deparser {
|
|
|
9589
9676
|
}
|
|
9590
9677
|
return output.join(' ');
|
|
9591
9678
|
}
|
|
9592
|
-
version(node, context) {
|
|
9593
|
-
// Handle version node - typically just return the version number
|
|
9594
|
-
if (typeof node === 'number') {
|
|
9595
|
-
return node.toString();
|
|
9596
|
-
}
|
|
9597
|
-
if (typeof node === 'string') {
|
|
9598
|
-
return node;
|
|
9599
|
-
}
|
|
9600
|
-
if (node && typeof node === 'object' && node.version) {
|
|
9601
|
-
return node.version.toString();
|
|
9602
|
-
}
|
|
9603
|
-
return '';
|
|
9604
|
-
}
|
|
9605
9679
|
}
|
|
9606
9680
|
exports.Deparser = Deparser;
|
package/esm/deparser.js
CHANGED
|
@@ -1,27 +1,112 @@
|
|
|
1
1
|
import { SqlFormatter } from './utils/sql-formatter';
|
|
2
2
|
import { QuoteUtils } from './utils/quote-utils';
|
|
3
3
|
import { ListUtils } from './utils/list-utils';
|
|
4
|
+
// Type guards for better type safety
|
|
5
|
+
function isParseResult(obj) {
|
|
6
|
+
// A ParseResult is an object that could have stmts (but not required)
|
|
7
|
+
// and is not already wrapped as a Node
|
|
8
|
+
// IMPORTANT: ParseResult.stmts is "repeated RawStmt" in protobuf, meaning
|
|
9
|
+
// the array contains RawStmt objects inline (not wrapped as { RawStmt: ... })
|
|
10
|
+
// Example: { version: 170004, stmts: [{ stmt: {...}, stmt_len: 32 }] }
|
|
11
|
+
return obj && typeof obj === 'object' &&
|
|
12
|
+
!Array.isArray(obj) &&
|
|
13
|
+
!('ParseResult' in obj) &&
|
|
14
|
+
!('RawStmt' in obj) &&
|
|
15
|
+
// Check if it looks like a ParseResult (has stmts or version)
|
|
16
|
+
('stmts' in obj || 'version' in obj);
|
|
17
|
+
}
|
|
18
|
+
function isWrappedParseResult(obj) {
|
|
19
|
+
return obj && typeof obj === 'object' && 'ParseResult' in obj;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Deparser - Converts PostgreSQL AST nodes back to SQL strings
|
|
23
|
+
*
|
|
24
|
+
* Entry Points:
|
|
25
|
+
* 1. ParseResult (from libpg-query) - The complete parse result
|
|
26
|
+
* Structure: { version: number, stmts: RawStmt[] }
|
|
27
|
+
* Note: stmts is "repeated RawStmt" in protobuf, so array contains RawStmt
|
|
28
|
+
* objects inline (not wrapped as { RawStmt: ... } nodes)
|
|
29
|
+
* Example: { version: 170004, stmts: [{ stmt: {...}, stmt_len: 32 }] }
|
|
30
|
+
*
|
|
31
|
+
* 2. Wrapped ParseResult - When explicitly wrapped as a Node
|
|
32
|
+
* Structure: { ParseResult: { version: number, stmts: RawStmt[] } }
|
|
33
|
+
*
|
|
34
|
+
* 3. Wrapped RawStmt - When explicitly wrapped as a Node
|
|
35
|
+
* Structure: { RawStmt: { stmt: Node, stmt_len?: number } }
|
|
36
|
+
*
|
|
37
|
+
* 4. Array of Nodes - Multiple statements to deparse
|
|
38
|
+
* Can be: Node[] (e.g., SelectStmt, InsertStmt, etc.)
|
|
39
|
+
*
|
|
40
|
+
* 5. Single Node - Individual statement node
|
|
41
|
+
* Example: { SelectStmt: {...} }, { InsertStmt: {...} }, etc.
|
|
42
|
+
*
|
|
43
|
+
* The deparser automatically detects bare ParseResult objects for backward
|
|
44
|
+
* compatibility and wraps them internally for consistent processing.
|
|
45
|
+
*/
|
|
4
46
|
export class Deparser {
|
|
5
47
|
formatter;
|
|
6
48
|
tree;
|
|
49
|
+
options;
|
|
7
50
|
constructor(tree, opts = {}) {
|
|
8
51
|
this.formatter = new SqlFormatter(opts.newline, opts.tab);
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
52
|
+
// Set default options
|
|
53
|
+
this.options = {
|
|
54
|
+
functionDelimiter: '$$',
|
|
55
|
+
functionDelimiterFallback: '$EOFCODE$',
|
|
56
|
+
...opts
|
|
57
|
+
};
|
|
58
|
+
// Handle different input types
|
|
59
|
+
if (isParseResult(tree)) {
|
|
60
|
+
// Duck-typed ParseResult (backward compatibility)
|
|
61
|
+
// Wrap it as a proper Node for consistent handling
|
|
62
|
+
this.tree = [{ ParseResult: tree }];
|
|
12
63
|
}
|
|
13
|
-
else {
|
|
14
|
-
|
|
64
|
+
else if (Array.isArray(tree)) {
|
|
65
|
+
// Array of Nodes
|
|
66
|
+
this.tree = tree;
|
|
15
67
|
}
|
|
16
|
-
|
|
68
|
+
else {
|
|
69
|
+
// Single Node (including wrapped ParseResult)
|
|
70
|
+
this.tree = [tree];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Static method to deparse PostgreSQL AST nodes to SQL
|
|
75
|
+
* @param query - Can be:
|
|
76
|
+
* - ParseResult from libpg-query (e.g., { version: 170004, stmts: [...] })
|
|
77
|
+
* - Wrapped ParseResult node (e.g., { ParseResult: {...} })
|
|
78
|
+
* - Wrapped RawStmt node (e.g., { RawStmt: {...} })
|
|
79
|
+
* - Array of Nodes
|
|
80
|
+
* - Single Node (e.g., { SelectStmt: {...} })
|
|
81
|
+
* @param opts - Deparser options for formatting
|
|
82
|
+
* @returns The deparsed SQL string
|
|
83
|
+
*/
|
|
17
84
|
static deparse(query, opts = {}) {
|
|
18
85
|
return new Deparser(query, opts).deparseQuery();
|
|
19
86
|
}
|
|
20
87
|
deparseQuery() {
|
|
21
88
|
return this.tree
|
|
22
|
-
.map(node =>
|
|
89
|
+
.map(node => {
|
|
90
|
+
// All nodes should go through the standard deparse method
|
|
91
|
+
// which will route to the appropriate handler
|
|
92
|
+
const result = this.deparse(node);
|
|
93
|
+
return result || '';
|
|
94
|
+
})
|
|
95
|
+
.filter(result => result !== '')
|
|
23
96
|
.join(this.formatter.newline() + this.formatter.newline());
|
|
24
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Get the appropriate function delimiter based on the body content
|
|
100
|
+
* @param body The function body to check
|
|
101
|
+
* @returns The delimiter to use
|
|
102
|
+
*/
|
|
103
|
+
getFunctionDelimiter(body) {
|
|
104
|
+
const delimiter = this.options.functionDelimiter || '$$';
|
|
105
|
+
if (body.includes(delimiter)) {
|
|
106
|
+
return this.options.functionDelimiterFallback || '$EOFCODE$';
|
|
107
|
+
}
|
|
108
|
+
return delimiter;
|
|
109
|
+
}
|
|
25
110
|
deparse(node, context = { parentNodeTypes: [] }) {
|
|
26
111
|
if (node == null) {
|
|
27
112
|
return null;
|
|
@@ -39,6 +124,10 @@ export class Deparser {
|
|
|
39
124
|
}
|
|
40
125
|
visit(node, context = { parentNodeTypes: [] }) {
|
|
41
126
|
const nodeType = this.getNodeType(node);
|
|
127
|
+
// Handle empty objects
|
|
128
|
+
if (!nodeType) {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
42
131
|
const nodeData = this.getNodeData(node);
|
|
43
132
|
const methodName = nodeType;
|
|
44
133
|
if (typeof this[methodName] === 'function') {
|
|
@@ -61,24 +150,29 @@ export class Deparser {
|
|
|
61
150
|
}
|
|
62
151
|
return node;
|
|
63
152
|
}
|
|
64
|
-
|
|
65
|
-
if (node.
|
|
66
|
-
return
|
|
153
|
+
ParseResult(node, context) {
|
|
154
|
+
if (!node.stmts || node.stmts.length === 0) {
|
|
155
|
+
return '';
|
|
67
156
|
}
|
|
68
|
-
|
|
157
|
+
// Deparse each RawStmt in the ParseResult
|
|
158
|
+
// Note: node.stmts is "repeated RawStmt" so contains RawStmt objects inline
|
|
159
|
+
// Each element has structure: { stmt: Node, stmt_len?: number, stmt_location?: number }
|
|
160
|
+
return node.stmts
|
|
161
|
+
.filter((rawStmt) => rawStmt != null)
|
|
162
|
+
.map((rawStmt) => this.RawStmt(rawStmt, context))
|
|
163
|
+
.filter((result) => result !== '')
|
|
164
|
+
.join(this.formatter.newline() + this.formatter.newline());
|
|
69
165
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (keys.length === 1) {
|
|
74
|
-
const statementType = keys[0];
|
|
75
|
-
const methodName = statementType;
|
|
76
|
-
if (typeof this[methodName] === 'function') {
|
|
77
|
-
return this[methodName](node[statementType], context);
|
|
78
|
-
}
|
|
79
|
-
throw new Error(`Deparser does not handle statement type: ${statementType}`);
|
|
166
|
+
RawStmt(node, context) {
|
|
167
|
+
if (!node.stmt) {
|
|
168
|
+
return '';
|
|
80
169
|
}
|
|
81
|
-
|
|
170
|
+
const deparsedStmt = this.deparse(node.stmt, context);
|
|
171
|
+
// Add semicolon if stmt_len is provided (indicates it had one in original)
|
|
172
|
+
if (node.stmt_len) {
|
|
173
|
+
return deparsedStmt + ';';
|
|
174
|
+
}
|
|
175
|
+
return deparsedStmt;
|
|
82
176
|
}
|
|
83
177
|
SelectStmt(node, context) {
|
|
84
178
|
const output = [];
|
|
@@ -1190,7 +1284,7 @@ export class Deparser {
|
|
|
1190
1284
|
}
|
|
1191
1285
|
let args = null;
|
|
1192
1286
|
if (node.typmods) {
|
|
1193
|
-
const isInterval = names.some(name => {
|
|
1287
|
+
const isInterval = names.some((name) => {
|
|
1194
1288
|
const nameStr = typeof name === 'string' ? name : (name.String?.sval || name.String?.str);
|
|
1195
1289
|
return nameStr === 'interval';
|
|
1196
1290
|
});
|
|
@@ -4786,17 +4880,17 @@ export class Deparser {
|
|
|
4786
4880
|
}
|
|
4787
4881
|
if (context.parentNodeTypes.includes('DoStmt')) {
|
|
4788
4882
|
if (node.defname === 'as') {
|
|
4883
|
+
const defElemContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DefElem'] };
|
|
4884
|
+
const argValue = node.arg ? this.visit(node.arg, defElemContext) : '';
|
|
4789
4885
|
if (Array.isArray(argValue)) {
|
|
4790
4886
|
const bodyParts = argValue;
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
}
|
|
4794
|
-
else {
|
|
4795
|
-
return `$$${bodyParts.join('')}$$`;
|
|
4796
|
-
}
|
|
4887
|
+
const body = bodyParts.join('');
|
|
4888
|
+
const delimiter = this.getFunctionDelimiter(body);
|
|
4889
|
+
return `${delimiter}${body}${delimiter}`;
|
|
4797
4890
|
}
|
|
4798
4891
|
else {
|
|
4799
|
-
|
|
4892
|
+
const delimiter = this.getFunctionDelimiter(argValue);
|
|
4893
|
+
return `${delimiter}${argValue}${delimiter}`;
|
|
4800
4894
|
}
|
|
4801
4895
|
}
|
|
4802
4896
|
return '';
|
|
@@ -4815,16 +4909,14 @@ export class Deparser {
|
|
|
4815
4909
|
});
|
|
4816
4910
|
if (bodyParts.length === 1) {
|
|
4817
4911
|
const body = bodyParts[0];
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
return `AS '${body.replace(/'/g, "''")}'`;
|
|
4821
|
-
}
|
|
4822
|
-
else {
|
|
4823
|
-
return `AS $$${body}$$`;
|
|
4824
|
-
}
|
|
4912
|
+
const delimiter = this.getFunctionDelimiter(body);
|
|
4913
|
+
return `AS ${delimiter}${body}${delimiter}`;
|
|
4825
4914
|
}
|
|
4826
4915
|
else {
|
|
4827
|
-
return `AS ${bodyParts.map((part) =>
|
|
4916
|
+
return `AS ${bodyParts.map((part) => {
|
|
4917
|
+
const delimiter = this.getFunctionDelimiter(part);
|
|
4918
|
+
return `${delimiter}${part}${delimiter}`;
|
|
4919
|
+
}).join(', ')}`;
|
|
4828
4920
|
}
|
|
4829
4921
|
}
|
|
4830
4922
|
// Handle Array type (legacy support)
|
|
@@ -4832,27 +4924,20 @@ export class Deparser {
|
|
|
4832
4924
|
const bodyParts = argValue;
|
|
4833
4925
|
if (bodyParts.length === 1) {
|
|
4834
4926
|
const body = bodyParts[0];
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
return `AS '${body.replace(/'/g, "''")}'`;
|
|
4838
|
-
}
|
|
4839
|
-
else {
|
|
4840
|
-
return `AS $$${body}$$`;
|
|
4841
|
-
}
|
|
4927
|
+
const delimiter = this.getFunctionDelimiter(body);
|
|
4928
|
+
return `AS ${delimiter}${body}${delimiter}`;
|
|
4842
4929
|
}
|
|
4843
4930
|
else {
|
|
4844
|
-
return `AS ${bodyParts.map(part =>
|
|
4931
|
+
return `AS ${bodyParts.map(part => {
|
|
4932
|
+
const delimiter = this.getFunctionDelimiter(part);
|
|
4933
|
+
return `${delimiter}${part}${delimiter}`;
|
|
4934
|
+
}).join(', ')}`;
|
|
4845
4935
|
}
|
|
4846
4936
|
}
|
|
4847
4937
|
// Handle String type (single function body)
|
|
4848
4938
|
else {
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
return `AS '${argValue.replace(/'/g, "''")}'`;
|
|
4852
|
-
}
|
|
4853
|
-
else {
|
|
4854
|
-
return `AS $$${argValue}$$`;
|
|
4855
|
-
}
|
|
4939
|
+
const delimiter = this.getFunctionDelimiter(argValue);
|
|
4940
|
+
return `AS ${delimiter}${argValue}${delimiter}`;
|
|
4856
4941
|
}
|
|
4857
4942
|
}
|
|
4858
4943
|
if (node.defname === 'language') {
|
|
@@ -5962,12 +6047,12 @@ export class Deparser {
|
|
|
5962
6047
|
processedArgs.push(`LANGUAGE ${langValue}`);
|
|
5963
6048
|
}
|
|
5964
6049
|
else if (defElem.defname === 'as') {
|
|
5965
|
-
// Handle code block with
|
|
6050
|
+
// Handle code block with configurable delimiter
|
|
5966
6051
|
const argNodeType = this.getNodeType(defElem.arg);
|
|
5967
6052
|
if (argNodeType === 'String') {
|
|
5968
6053
|
const stringNode = this.getNodeData(defElem.arg);
|
|
5969
|
-
const
|
|
5970
|
-
processedArgs.push(`${
|
|
6054
|
+
const delimiter = this.getFunctionDelimiter(stringNode.sval);
|
|
6055
|
+
processedArgs.push(`${delimiter}${stringNode.sval}${delimiter}`);
|
|
5971
6056
|
}
|
|
5972
6057
|
else {
|
|
5973
6058
|
processedArgs.push(this.visit(defElem.arg, doContext));
|
|
@@ -6001,9 +6086,11 @@ export class Deparser {
|
|
|
6001
6086
|
}
|
|
6002
6087
|
InlineCodeBlock(node, context) {
|
|
6003
6088
|
if (node.source_text) {
|
|
6004
|
-
|
|
6089
|
+
const delimiter = this.getFunctionDelimiter(node.source_text);
|
|
6090
|
+
return `${delimiter}${node.source_text}${delimiter}`;
|
|
6005
6091
|
}
|
|
6006
|
-
|
|
6092
|
+
const delimiter = this.options.functionDelimiter || '$$';
|
|
6093
|
+
return `${delimiter}${delimiter}`;
|
|
6007
6094
|
}
|
|
6008
6095
|
CallContext(node, context) {
|
|
6009
6096
|
if (node.atomic !== undefined) {
|
|
@@ -9586,17 +9673,4 @@ export class Deparser {
|
|
|
9586
9673
|
}
|
|
9587
9674
|
return output.join(' ');
|
|
9588
9675
|
}
|
|
9589
|
-
version(node, context) {
|
|
9590
|
-
// Handle version node - typically just return the version number
|
|
9591
|
-
if (typeof node === 'number') {
|
|
9592
|
-
return node.toString();
|
|
9593
|
-
}
|
|
9594
|
-
if (typeof node === 'string') {
|
|
9595
|
-
return node;
|
|
9596
|
-
}
|
|
9597
|
-
if (node && typeof node === 'object' && node.version) {
|
|
9598
|
-
return node.version.toString();
|
|
9599
|
-
}
|
|
9600
|
-
return '';
|
|
9601
|
-
}
|
|
9602
9676
|
}
|
package/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { Deparser } from "./deparser";
|
|
1
|
+
import { Deparser, DeparserOptions } from "./deparser";
|
|
2
2
|
declare const deparse: typeof Deparser.deparse;
|
|
3
|
-
export { deparse, Deparser };
|
|
3
|
+
export { deparse, Deparser, DeparserOptions };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgsql-deparser",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.4.0",
|
|
4
4
|
"author": "Dan Lynch <pyramation@gmail.com>",
|
|
5
5
|
"description": "PostgreSQL AST Deparser",
|
|
6
6
|
"main": "index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"libpg-query": "17.3.3"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@pgsql/types": "^17.
|
|
51
|
+
"@pgsql/types": "^17.4.0"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "3725e93239a76bdc02d2d9159209e01d0142e364"
|
|
54
54
|
}
|