@@ -8,24 +8,45 @@ import * as t from '@pgsql/types';
88export 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
1317export 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 {
0 commit comments