diff --git a/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap index fcb5d85d..be9ba3a1 100644 --- a/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap +++ b/packages/deparser/__tests__/pretty/__snapshots__/constraints-pretty.test.ts.snap @@ -87,7 +87,7 @@ exports[`pretty: pretty/constraints-9.sql 1`] = ` ADD CONSTRAINT tax_rate_range CHECK ( tax_rate >= 0 - AND tax_rate <= 1 + AND tax_rate <= 1 )" `; @@ -151,7 +151,7 @@ exports[`pretty: pretty/constraints-16.sql 1`] = ` ADD CONSTRAINT chk_enrollment_date CHECK ( enrollment_date <= CURRENT_DATE - AND status IN ('active', 'completed', 'withdrawn') + AND status IN ('active', 'completed', 'withdrawn') )" `; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/misc-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/misc-pretty.test.ts.snap index 8811ecb9..d3f62e7e 100644 --- a/packages/deparser/__tests__/pretty/__snapshots__/misc-pretty.test.ts.snap +++ b/packages/deparser/__tests__/pretty/__snapshots__/misc-pretty.test.ts.snap @@ -95,8 +95,8 @@ CASE WHEN EXISTS (SELECT 1 FROM reviews AS r WHERE - r.product_id = p.id - AND r.rating >= 4) THEN 'Popular' + r.product_id = p.id + AND r.rating >= 4) THEN 'Popular' ELSE 'Unrated' END AS status FROM products AS p @@ -165,8 +165,8 @@ WHERE AND EXISTS (SELECT 1 FROM order_notes AS n WHERE - (n.order_id = o.id - AND n.note ILIKE '%expedite%')))) + (n.order_id = o.id + AND n.note ILIKE '%expedite%')))) ORDER BY o.created_at DESC" `; @@ -229,8 +229,8 @@ CASE WHEN EXISTS (SELECT 1 FROM logins WHERE - logins.user_id = users.user_id - AND success = false) THEN 'risky' + logins.user_id = users.user_id + AND success = false) THEN 'risky' ELSE 'safe' END AS risk_status FROM users" @@ -261,12 +261,12 @@ exports[`pretty: pretty/misc-12.sql 1`] = ` id, (SELECT CASE - WHEN count(*) > 5 THEN 'frequent' - ELSE 'occasional' + WHEN count(*) > 5 THEN 'frequent' + ELSE 'occasional' END FROM purchases AS p WHERE - p.user_id = u.id) AS purchase_freq + p.user_id = u.id) AS purchase_freq FROM users AS u" `; diff --git a/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap b/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap index c51727b2..fade802e 100644 --- a/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap +++ b/packages/deparser/__tests__/pretty/__snapshots__/select-pretty.test.ts.snap @@ -117,7 +117,7 @@ WHERE id IN (SELECT user_id FROM orders WHERE - total > 100)" + total > 100)" `; exports[`pretty: pretty/selects-14.sql 1`] = ` diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 11472383..9fd6ab4f 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -1,6 +1,6 @@ import { Node } from '@pgsql/types'; -import { SqlFormatter } from './utils/sql-formatter'; import { DeparserContext, DeparserVisitor } from './visitors/base'; +import { SqlFormatter } from './utils/sql-formatter'; import { QuoteUtils } from './utils/quote-utils'; import { ListUtils } from './utils/list-utils'; import * as t from '@pgsql/types'; @@ -89,7 +89,7 @@ function isParseResult(obj: any): obj is t.ParseResult { // IMPORTANT: ParseResult.stmts is "repeated RawStmt" in protobuf, meaning // the array contains RawStmt objects inline (not wrapped as { RawStmt: ... }) // Example: { version: 170004, stmts: [{ stmt: {...}, stmt_len: 32 }] } - return obj && typeof obj === 'object' && + return obj && typeof obj === 'object' && !Array.isArray(obj) && !('ParseResult' in obj) && !('RawStmt' in obj) && @@ -103,44 +103,42 @@ function isWrappedParseResult(obj: any): obj is { ParseResult: t.ParseResult } { /** * Deparser - Converts PostgreSQL AST nodes back to SQL strings - * + * * Entry Points: * 1. ParseResult (from libpg-query) - The complete parse result * Structure: { version: number, stmts: RawStmt[] } - * Note: stmts is "repeated RawStmt" in protobuf, so array contains RawStmt + * Note: stmts is "repeated RawStmt" in protobuf, so array contains RawStmt * objects inline (not wrapped as { RawStmt: ... } nodes) * Example: { version: 170004, stmts: [{ stmt: {...}, stmt_len: 32 }] } - * + * * 2. Wrapped ParseResult - When explicitly wrapped as a Node * Structure: { ParseResult: { version: number, stmts: RawStmt[] } } - * + * * 3. Wrapped RawStmt - When explicitly wrapped as a Node * Structure: { RawStmt: { stmt: Node, stmt_len?: number } } - * + * * 4. Array of Nodes - Multiple statements to deparse * Can be: Node[] (e.g., SelectStmt, InsertStmt, etc.) - * + * * 5. Single Node - Individual statement node * Example: { SelectStmt: {...} }, { InsertStmt: {...} }, etc. - * + * * The deparser automatically detects bare ParseResult objects for backward * compatibility and wraps them internally for consistent processing. */ export class Deparser implements DeparserVisitor { - private formatter: SqlFormatter; private tree: Node[]; private options: DeparserOptions; constructor(tree: Node | Node[] | t.ParseResult, opts: DeparserOptions = {}) { - this.formatter = new SqlFormatter(opts.newline, opts.tab, opts.pretty); - + // Set default options this.options = { functionDelimiter: '$$', functionDelimiterFallback: '$EOFCODE$', ...opts }; - + // Handle different input types if (isParseResult(tree)) { // Duck-typed ParseResult (backward compatibility) @@ -171,15 +169,17 @@ export class Deparser implements DeparserVisitor { } deparseQuery(): string { + const formatter = new SqlFormatter(this.options.newline, this.options.tab, this.options.pretty); + const context = new DeparserContext({ formatter, prettyMode: this.options.pretty }); return this.tree .map(node => { // All nodes should go through the standard deparse method // which will route to the appropriate handler - const result = this.deparse(node); + const result = this.deparse(node, context); return result || ''; }) .filter(result => result !== '') - .join(this.formatter.newline() + this.formatter.newline()); + .join(context.newline() + context.newline()); } /** @@ -195,11 +195,16 @@ export class Deparser implements DeparserVisitor { return delimiter; } - deparse(node: Node, context: DeparserContext = { parentNodeTypes: [] }): string | null { + deparse(node: Node, context?: DeparserContext): string | null { if (node == null) { return null; } + if (!context) { + const formatter = new SqlFormatter(this.options.newline, this.options.tab, this.options.pretty); + context = new DeparserContext({ formatter, prettyMode: this.options.pretty }); + } + if (typeof node === 'number' || node instanceof Number) { return node.toString(); } @@ -212,27 +217,28 @@ export class Deparser implements DeparserVisitor { } } - visit(node: Node, context: DeparserContext = { parentNodeTypes: [] }): string { + visit(node: Node, context?: DeparserContext): string { + if (!context) { + const formatter = new SqlFormatter(this.options.newline, this.options.tab, this.options.pretty); + context = new DeparserContext({ formatter, prettyMode: this.options.pretty }); + } + const nodeType = this.getNodeType(node); - + // Handle empty objects if (!nodeType) { return ''; } - + const nodeData = this.getNodeData(node); const methodName = nodeType as keyof this; if (typeof this[methodName] === 'function') { - const childContext: DeparserContext = { - ...context, - parentNodeTypes: [...context.parentNodeTypes, nodeType] - }; - const result = (this[methodName] as any)(nodeData, childContext); - + const result = (this[methodName] as any)(nodeData, context); + return result; } - + throw new Error(`Deparser does not handle node type: ${nodeType}`); } @@ -252,7 +258,7 @@ export class Deparser implements DeparserVisitor { if (!node.stmts || node.stmts.length === 0) { return ''; } - + // Deparse each RawStmt in the ParseResult // Note: node.stmts is "repeated RawStmt" so contains RawStmt objects inline // Each element has structure: { stmt: Node, stmt_len?: number, stmt_location?: number } @@ -260,16 +266,16 @@ export class Deparser implements DeparserVisitor { .filter((rawStmt: t.RawStmt) => rawStmt != null) .map((rawStmt: t.RawStmt) => this.RawStmt(rawStmt, context)) .filter((result: string) => result !== '') - .join(this.formatter.newline() + this.formatter.newline()); + .join(context.newline() + context.newline()); } RawStmt(node: t.RawStmt, context: DeparserContext): string { if (!node.stmt) { return ''; } - + const deparsedStmt = this.deparse(node.stmt, context); - + // Add semicolon if stmt_len is provided (indicates it had one in original) if (node.stmt_len) { return deparsedStmt + ';'; @@ -286,14 +292,14 @@ export class Deparser implements DeparserVisitor { if (!node.op || node.op === 'SETOP_NONE') { if (node.valuesLists == null) { - if (!this.formatter.isPretty() || !node.targetList) { + if (!context.isPretty() || !node.targetList) { output.push('SELECT'); } } }else { const leftStmt = this.SelectStmt(node.larg as t.SelectStmt, context); const rightStmt = this.SelectStmt(node.rarg as t.SelectStmt, context); - + // Add parentheses if the operand is a set operation OR has ORDER BY/LIMIT clauses OR has WITH clause const leftNeedsParens = node.larg && ( ((node.larg as t.SelectStmt).op && (node.larg as t.SelectStmt).op !== 'SETOP_NONE') || @@ -309,9 +315,9 @@ export class Deparser implements DeparserVisitor { (node.rarg as t.SelectStmt).limitOffset || (node.rarg as t.SelectStmt).withClause ); - + if (leftNeedsParens) { - output.push(this.formatter.parens(leftStmt)); + output.push(context.parens(leftStmt)); } else { output.push(leftStmt); } @@ -335,7 +341,7 @@ export class Deparser implements DeparserVisitor { } if (rightNeedsParens) { - output.push(this.formatter.parens(rightStmt)); + output.push(context.parens(rightStmt)); } else { output.push(rightStmt); } @@ -347,20 +353,20 @@ export class Deparser implements DeparserVisitor { const distinctClause = ListUtils.unwrapList(node.distinctClause); if (distinctClause.length > 0 && Object.keys(distinctClause[0]).length > 0) { const clause = distinctClause - .map(e => this.visit(e as Node, { ...context, select: true })) + .map(e => this.visit(e as Node, context.spawn('SelectStmt', { select: true }))) .join(', '); - distinctPart = ' DISTINCT ON ' + this.formatter.parens(clause); + distinctPart = ' DISTINCT ON ' + context.parens(clause); } else { distinctPart = ' DISTINCT'; } - - if (!this.formatter.isPretty()) { + + if (!context.isPretty()) { if (distinctClause.length > 0 && Object.keys(distinctClause[0]).length > 0) { output.push('DISTINCT ON'); const clause = distinctClause - .map(e => this.visit(e as Node, { ...context, select: true })) + .map(e => this.visit(e as Node, context.spawn('SelectStmt', { select: true }))) .join(', '); - output.push(this.formatter.parens(clause)); + output.push(context.parens(clause)); } else { output.push('DISTINCT'); } @@ -369,18 +375,18 @@ export class Deparser implements DeparserVisitor { if (node.targetList) { const targetList = ListUtils.unwrapList(node.targetList); - if (this.formatter.isPretty()) { + if (context.isPretty()) { if (targetList.length === 1) { const targetNode = targetList[0] as Node; - const target = this.visit(targetNode, { ...context, select: true }); - + const target = this.visit(targetNode, context.spawn('SelectStmt', { select: true })); + // Check if single target is complex - if so, use multiline format if (this.isComplexSelectTarget(targetNode)) { output.push('SELECT' + distinctPart); if (this.containsMultilineStringLiteral(target)) { output.push(target); } else { - output.push(this.formatter.indent(target)); + output.push(context.indent(target)); } } else { output.push('SELECT' + distinctPart + ' ' + target); @@ -388,19 +394,19 @@ export class Deparser implements DeparserVisitor { } else { const targetStrings = targetList .map(e => { - const targetStr = this.visit(e as Node, { ...context, select: true }); + const targetStr = this.visit(e as Node, context.spawn('SelectStmt', { select: true })); if (this.containsMultilineStringLiteral(targetStr)) { return targetStr; } - return this.formatter.indent(targetStr); + return context.indent(targetStr); }); - const formattedTargets = targetStrings.join(',' + this.formatter.newline()); + const formattedTargets = targetStrings.join(',' + context.newline()); output.push('SELECT' + distinctPart); output.push(formattedTargets); } } else { const targets = targetList - .map(e => this.visit(e as Node, { ...context, select: true })) + .map(e => this.visit(e as Node, context.spawn('SelectStmt', { select: true }))) .join(', '); output.push(targets); } @@ -414,23 +420,23 @@ export class Deparser implements DeparserVisitor { if (node.fromClause) { const fromList = ListUtils.unwrapList(node.fromClause); const fromItems = fromList - .map(e => this.deparse(e as Node, { ...context, from: true })) + .map(e => this.deparse(e as Node, context.spawn('SelectStmt', { from: true }))) .join(', '); output.push('FROM ' + fromItems.trim()); } if (node.whereClause) { - if (this.formatter.isPretty()) { + if (context.isPretty()) { output.push('WHERE'); - const whereExpr = this.visit(node.whereClause as Node, context); - const lines = whereExpr.split(this.formatter.newline()); + const whereExpr = this.visit(node.whereClause as Node, context.spawn('SelectStmt')); + const lines = whereExpr.split(context.newline()); const indentedLines = lines.map((line, index) => { if (index === 0) { - return this.formatter.indent(line); + return context.indent(line); } return line; }); - output.push(indentedLines.join(this.formatter.newline())); + output.push(indentedLines.join(context.newline())); } else { output.push('WHERE'); output.push(this.visit(node.whereClause as Node, context)); @@ -438,24 +444,24 @@ export class Deparser implements DeparserVisitor { } if (node.valuesLists) { - if (this.formatter.isPretty()) { + if (context.isPretty()) { output.push('VALUES'); const lists = ListUtils.unwrapList(node.valuesLists).map(list => { const values = ListUtils.unwrapList(list).map(val => this.visit(val as Node, context)); - return this.formatter.parens(values.join(', ')); + return context.parens(values.join(', ')); }); const indentedTuples = lists.map(tuple => { if (this.containsMultilineStringLiteral(tuple)) { return tuple; } - return this.formatter.indent(tuple); + return context.indent(tuple); }); output.push(indentedTuples.join(',\n')); } else { output.push('VALUES'); const lists = ListUtils.unwrapList(node.valuesLists).map(list => { const values = ListUtils.unwrapList(list).map(val => this.visit(val as Node, context)); - return this.formatter.parens(values.join(', ')); + return context.parens(values.join(', ')); }); output.push(lists.join(', ')); } @@ -463,35 +469,35 @@ export class Deparser implements DeparserVisitor { if (node.groupClause) { const groupList = ListUtils.unwrapList(node.groupClause); - if (this.formatter.isPretty()) { + if (context.isPretty()) { const groupItems = groupList .map(e => { - const groupStr = this.visit(e as Node, { ...context, group: true }); + const groupStr = this.visit(e as Node, context.spawn('SelectStmt', { group: true })); if (this.containsMultilineStringLiteral(groupStr)) { return groupStr; } - return this.formatter.indent(groupStr); + return context.indent(groupStr); }) - .join(',' + this.formatter.newline()); + .join(',' + context.newline()); output.push('GROUP BY'); output.push(groupItems); } else { output.push('GROUP BY'); const groupItems = groupList - .map(e => this.visit(e as Node, { ...context, group: true })) + .map(e => this.visit(e as Node, context.spawn('SelectStmt', { group: true }))) .join(', '); output.push(groupItems); } } if (node.havingClause) { - if (this.formatter.isPretty()) { + if (context.isPretty()) { output.push('HAVING'); const havingStr = this.visit(node.havingClause as Node, context); if (this.containsMultilineStringLiteral(havingStr)) { output.push(havingStr); } else { - output.push(this.formatter.indent(havingStr)); + output.push(context.indent(havingStr)); } } else { output.push('HAVING'); @@ -510,22 +516,22 @@ export class Deparser implements DeparserVisitor { if (node.sortClause) { const sortList = ListUtils.unwrapList(node.sortClause); - if (this.formatter.isPretty()) { + if (context.isPretty()) { const sortItems = sortList .map(e => { - const sortStr = this.visit(e as Node, { ...context, sort: true }); + const sortStr = this.visit(e as Node, context.spawn('SelectStmt', { sort: true })); if (this.containsMultilineStringLiteral(sortStr)) { return sortStr; } - return this.formatter.indent(sortStr); + return context.indent(sortStr); }) - .join(',' + this.formatter.newline()); + .join(',' + context.newline()); output.push('ORDER BY'); output.push(sortItems); } else { output.push('ORDER BY'); const sortItems = sortList - .map(e => this.visit(e as Node, { ...context, sort: true })) + .map(e => this.visit(e as Node, context.spawn('SelectStmt', { sort: true }))) .join(', '); output.push(sortItems); } @@ -547,9 +553,9 @@ export class Deparser implements DeparserVisitor { output.push(lockingClauses); } - if (this.formatter.isPretty()) { + if (context.isPretty()) { const filteredOutput = output.filter(item => item.trim() !== ''); - return filteredOutput.join(this.formatter.newline()); + return filteredOutput.join(context.newline()); } return output.join(' '); } @@ -563,14 +569,14 @@ export class Deparser implements DeparserVisitor { switch (kind) { case 'AEXPR_OP': if (lexpr && rexpr) { - const operator = this.deparseOperatorName(name); + const operator = this.deparseOperatorName(name, context); let leftExpr = this.visit(lexpr, context); let rightExpr = this.visit(rexpr, context); - + // Check if left expression needs parentheses let leftNeedsParens = false; if (lexpr && 'A_Expr' in lexpr && lexpr.A_Expr?.kind === 'AEXPR_OP') { - const leftOp = this.deparseOperatorName(ListUtils.unwrapList(lexpr.A_Expr.name)); + const leftOp = this.deparseOperatorName(ListUtils.unwrapList(lexpr.A_Expr.name), context); if (this.needsParentheses(leftOp, operator, 'left')) { leftNeedsParens = true; } @@ -579,13 +585,13 @@ export class Deparser implements DeparserVisitor { leftNeedsParens = true; } if (leftNeedsParens) { - leftExpr = this.formatter.parens(leftExpr); + leftExpr = context.parens(leftExpr); } - + // Check if right expression needs parentheses let rightNeedsParens = false; if (rexpr && 'A_Expr' in rexpr && rexpr.A_Expr?.kind === 'AEXPR_OP') { - const rightOp = this.deparseOperatorName(ListUtils.unwrapList(rexpr.A_Expr.name)); + const rightOp = this.deparseOperatorName(ListUtils.unwrapList(rexpr.A_Expr.name), context); if (this.needsParentheses(rightOp, operator, 'right')) { rightNeedsParens = true; } @@ -594,44 +600,44 @@ export class Deparser implements DeparserVisitor { rightNeedsParens = true; } if (rightNeedsParens) { - rightExpr = this.formatter.parens(rightExpr); + rightExpr = context.parens(rightExpr); } - - return this.formatter.format([leftExpr, operator, rightExpr]); + + return context.format([leftExpr, operator, rightExpr]); }else if (rexpr) { - return this.formatter.format([ - this.deparseOperatorName(name), + return context.format([ + this.deparseOperatorName(name, context), this.visit(rexpr, context) ]); } break; case 'AEXPR_OP_ANY': - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), - this.deparseOperatorName(name), + this.deparseOperatorName(name, context), 'ANY', - this.formatter.parens(this.visit(rexpr, context)) + context.parens(this.visit(rexpr, context)) ]); case 'AEXPR_OP_ALL': - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), - this.deparseOperatorName(name), + this.deparseOperatorName(name, context), 'ALL', - this.formatter.parens(this.visit(rexpr, context)) + context.parens(this.visit(rexpr, context)) ]); case 'AEXPR_DISTINCT': { let leftExpr = this.visit(lexpr, context); let rightExpr = this.visit(rexpr, context); - + // Add parentheses for complex expressions if (lexpr && this.isComplexExpression(lexpr)) { - leftExpr = this.formatter.parens(leftExpr); + leftExpr = context.parens(leftExpr); } if (rexpr && this.isComplexExpression(rexpr)) { - rightExpr = this.formatter.parens(rightExpr); + rightExpr = context.parens(rightExpr); } - - return this.formatter.format([ + + return context.format([ leftExpr, 'IS DISTINCT FROM', rightExpr @@ -640,79 +646,79 @@ export class Deparser implements DeparserVisitor { case 'AEXPR_NOT_DISTINCT': { let leftExpr = this.visit(lexpr, context); let rightExpr = this.visit(rexpr, context); - + // Add parentheses for complex expressions if (lexpr && this.isComplexExpression(lexpr)) { - leftExpr = this.formatter.parens(leftExpr); + leftExpr = context.parens(leftExpr); } if (rexpr && this.isComplexExpression(rexpr)) { - rightExpr = this.formatter.parens(rightExpr); + rightExpr = context.parens(rightExpr); } - - return this.formatter.format([ + + return context.format([ leftExpr, 'IS NOT DISTINCT FROM', rightExpr ]); } case 'AEXPR_NULLIF': - return this.formatter.format([ + return context.format([ 'NULLIF', - this.formatter.parens([ + context.parens([ this.visit(lexpr, context), this.visit(rexpr, context) ].join(', ')) ]); case 'AEXPR_IN': - const inOperator = this.deparseOperatorName(name); + const inOperator = this.deparseOperatorName(name, context); if (inOperator === '<>' || inOperator === '!=') { - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT IN', - this.formatter.parens(this.visit(rexpr, context)) + context.parens(this.visit(rexpr, context)) ]); } else { - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'IN', - this.formatter.parens(this.visit(rexpr, context)) + context.parens(this.visit(rexpr, context)) ]); } case 'AEXPR_LIKE': - const likeOp = this.deparseOperatorName(name); + const likeOp = this.deparseOperatorName(name, context); if (likeOp === '!~~') { - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT LIKE', this.visit(rexpr, context) ]); } else { - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'LIKE', this.visit(rexpr, context) ]); } case 'AEXPR_ILIKE': - const ilikeOp = this.deparseOperatorName(name); + const ilikeOp = this.deparseOperatorName(name, context); if (ilikeOp === '!~~*') { - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT ILIKE', this.visit(rexpr, context) ]); } else { - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'ILIKE', this.visit(rexpr, context) ]); } case 'AEXPR_SIMILAR': - const similarOp = this.deparseOperatorName(name); + const similarOp = this.deparseOperatorName(name, context); let rightExpr: string; - - if (rexpr && 'FuncCall' in rexpr && + + if (rexpr && 'FuncCall' in rexpr && rexpr.FuncCall?.funcname?.length === 2 && (rexpr.FuncCall.funcname[0] as any)?.String?.sval === 'pg_catalog' && (rexpr.FuncCall.funcname[1] as any)?.String?.sval === 'similar_to_escape') { @@ -724,40 +730,40 @@ export class Deparser implements DeparserVisitor { } else { rightExpr = this.visit(rexpr, context); } - + if (similarOp === '!~') { - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT SIMILAR TO', rightExpr ]); } else { - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'SIMILAR TO', rightExpr ]); } case 'AEXPR_BETWEEN': - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'BETWEEN', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_NOT_BETWEEN': - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT BETWEEN', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_BETWEEN_SYM': - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'BETWEEN SYMMETRIC', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_NOT_BETWEEN_SYM': - return this.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT BETWEEN SYMMETRIC', this.visitBetweenRange(rexpr, context) @@ -767,22 +773,22 @@ export class Deparser implements DeparserVisitor { throw new Error(`Unhandled A_Expr kind: ${kind}`); } - deparseOperatorName(name: t.Node[]): string { + deparseOperatorName(name: t.Node[], context: DeparserContext): string { if (!name || name.length === 0) { return ''; } - + const parts = name.map((n: any) => { if (n.String) { return n.String.sval || n.String.str; } - return this.visit(n, { parentNodeTypes: [] }); + return this.visit(n, context); }); - + if (parts.length > 1) { return `OPERATOR(${parts.join('.')})`; } - + return parts.join('.'); } @@ -818,24 +824,24 @@ export class Deparser implements DeparserVisitor { '<<': 10, '>>': 10 }; - + return precedence[operator] || 0; } private needsParentheses(childOp: string, parentOp: string, position: 'left' | 'right'): boolean { const childPrec = this.getOperatorPrecedence(childOp); const parentPrec = this.getOperatorPrecedence(parentOp); - + if (childPrec < parentPrec) { return true; } - + if (childPrec === parentPrec && position === 'right') { if (parentOp === '-' || parentOp === '/') { return true; } } - + return false; } @@ -860,37 +866,37 @@ export class Deparser implements DeparserVisitor { // Always complex: CASE expressions if (node.CaseExpr) return true; - + // Always complex: Subqueries and subselects if (node.SubLink) return true; - + // Always complex: Boolean tests and expressions if (node.NullTest || node.BooleanTest || node.BoolExpr) return true; - + // COALESCE and similar functions - complex if multiple arguments if (node.CoalesceExpr) { const args = node.CoalesceExpr.args; if (args && Array.isArray(args) && args.length > 1) return true; } - + // Function calls - complex if multiple args or has clauses if (node.FuncCall) { const funcCall = node.FuncCall; const args = funcCall.args ? (Array.isArray(funcCall.args) ? funcCall.args : [funcCall.args]) : []; - + // Complex if has window clause, filter, order by, etc. if (funcCall.over || funcCall.agg_filter || funcCall.agg_order || funcCall.agg_distinct) { return true; } - + // Complex if multiple arguments if (args.length > 1) return true; - + if (args.length === 1) { return this.isComplexSelectTarget(args[0]); } } - + if (node.A_Expr) { const expr = node.A_Expr; // Check if operands are complex @@ -898,21 +904,21 @@ export class Deparser implements DeparserVisitor { if (expr.rexpr && this.isComplexSelectTarget(expr.rexpr)) return true; return false; } - + if (node.TypeCast) { return this.isComplexSelectTarget(node.TypeCast.arg); } - + if (node.A_ArrayExpr) return true; - + if (node.A_Indirection) { return this.isComplexSelectTarget(node.A_Indirection.arg); } - + if (node.A_Const || node.ColumnRef || node.ParamRef || node.A_Star) { return false; } - + return false; } @@ -938,15 +944,15 @@ export class Deparser implements DeparserVisitor { if (node.cols) { const cols = ListUtils.unwrapList(node.cols); - const insertContext = { ...context, insertColumns: true }; + const insertContext = context.spawn('InsertStmt', { insertColumns: true }); const columnNames = cols.map(col => this.visit(col as Node, insertContext)); - - if (this.formatter.isPretty()) { + + if (context.isPretty()) { // Always format columns in multiline parentheses for pretty printing - const indentedColumns = columnNames.map(col => this.formatter.indent(col)); + const indentedColumns = columnNames.map(col => context.indent(col)); output.push('(\n' + indentedColumns.join(',\n') + '\n)'); } else { - output.push(this.formatter.parens(columnNames.join(', '))); + output.push(context.parens(columnNames.join(', '))); } } @@ -961,7 +967,7 @@ export class Deparser implements DeparserVisitor { output.push('ON CONFLICT'); if (node.onConflictClause.infer) { const infer = node.onConflictClause.infer; - + // Handle ON CONSTRAINT clause if (infer.conname) { output.push('ON CONSTRAINT'); @@ -969,36 +975,36 @@ export class Deparser implements DeparserVisitor { } else if (infer.indexElems) { const elems = ListUtils.unwrapList(infer.indexElems); const indexElems = elems.map(elem => this.visit(elem as Node, context)); - output.push(this.formatter.parens(indexElems.join(', '))); + output.push(context.parens(indexElems.join(', '))); } - + // Handle WHERE clause for conflict detection if (infer.whereClause) { output.push('WHERE'); output.push(this.visit(infer.whereClause as Node, context)); } } - + if (node.onConflictClause.action === 'ONCONFLICT_UPDATE') { output.push('DO UPDATE SET'); const targetList = ListUtils.unwrapList(node.onConflictClause.targetList); - + if (targetList && targetList.length) { const firstTarget = targetList[0]; - + if (firstTarget.ResTarget?.val?.MultiAssignRef && targetList.every(target => target.ResTarget?.val?.MultiAssignRef)) { const sortedTargets = targetList.sort((a, b) => a.ResTarget.val.MultiAssignRef.colno - b.ResTarget.val.MultiAssignRef.colno); const names = sortedTargets.map(target => target.ResTarget.name); - output.push(this.formatter.parens(names.join(', '))); + output.push(context.parens(names.join(', '))); output.push('='); output.push(this.visit(firstTarget.ResTarget.val.MultiAssignRef.source, context)); } else { - const updateContext = { ...context, update: true }; + const updateContext = context.spawn('UpdateStmt', { update: true }); const targets = targetList.map(target => this.visit(target as Node, updateContext)); output.push(targets.join(', ')); } } - + if (node.onConflictClause.whereClause) { output.push('WHERE'); output.push(this.visit(node.onConflictClause.whereClause as Node, context)); @@ -1034,39 +1040,39 @@ export class Deparser implements DeparserVisitor { const targetList = ListUtils.unwrapList(node.targetList); if (targetList && targetList.length) { const firstTarget = targetList[0]; - + const processedTargets = new Set(); const assignmentParts = []; - + for (let i = 0; i < targetList.length; i++) { if (processedTargets.has(i)) continue; - + const target = targetList[i]; const multiAssignRef = target.ResTarget?.val?.MultiAssignRef; - + if (multiAssignRef) { const relatedTargets = []; for (let j = i; j < targetList.length; j++) { const otherTarget = targetList[j]; const otherMultiAssignRef = otherTarget.ResTarget?.val?.MultiAssignRef; - - if (otherMultiAssignRef && + + if (otherMultiAssignRef && JSON.stringify(otherMultiAssignRef.source) === JSON.stringify(multiAssignRef.source)) { relatedTargets.push(otherTarget); processedTargets.add(j); } } - + const names = relatedTargets.map(t => t.ResTarget.name); - const multiAssignment = `${this.formatter.parens(names.join(', '))} = ${this.visit(multiAssignRef.source, context)}`; + const multiAssignment = `${context.parens(names.join(', '))} = ${this.visit(multiAssignRef.source, context)}`; assignmentParts.push(multiAssignment); } else { // Handle regular single-column assignment - assignmentParts.push(this.visit(target, { ...context, update: true })); + assignmentParts.push(this.visit(target, context.spawn('UpdateStmt', { update: true }))); processedTargets.add(i); } } - + output.push(assignmentParts.join(',')); } @@ -1105,7 +1111,7 @@ export class Deparser implements DeparserVisitor { output.push('DELETE'); output.push('FROM'); - + if (!node.relation) { throw new Error('DeleteStmt requires a relation'); } @@ -1158,21 +1164,21 @@ export class Deparser implements DeparserVisitor { WithClause(node: t.WithClause, context: DeparserContext): string { const output: string[] = ['WITH']; - + if (node.recursive) { output.push('RECURSIVE'); } - + if (node.ctes && node.ctes.length > 0) { const ctes = ListUtils.unwrapList(node.ctes); - if (this.formatter.isPretty()) { + if (context.isPretty()) { const cteStrings = ctes.map((cte, index) => { - const cteStr = this.visit(cte, context); - const prefix = index === 0 ? this.formatter.newline() : ',' + this.formatter.newline(); + const cteStr = this.visit(cte, context.spawn('WithClause')); + const prefix = index === 0 ? context.newline() : ',' + context.newline(); if (this.containsMultilineStringLiteral(cteStr)) { return prefix + cteStr; } - return prefix + this.formatter.indent(cteStr); + return prefix + context.indent(cteStr); }); output.push(cteStrings.join('')); } else { @@ -1180,7 +1186,7 @@ export class Deparser implements DeparserVisitor { output.push(cteStrings.join(', ')); } } - + return output.join(' '); } @@ -1188,10 +1194,10 @@ export class Deparser implements DeparserVisitor { ResTarget(node: t.ResTarget, context: DeparserContext): string { const output: string[] = []; - + if (context.update && node.name) { output.push(QuoteUtils.quote(node.name)); - + // Handle indirection (array indexing, field access, etc.) if (node.indirection && node.indirection.length > 0) { const indirectionStrs = ListUtils.unwrapList(node.indirection).map(item => { @@ -1202,14 +1208,14 @@ export class Deparser implements DeparserVisitor { }); output.push(indirectionStrs.join('')); } - + output.push('='); if (node.val) { output.push(this.deparse(node.val, context)); } } else if (context.insertColumns && node.name) { output.push(QuoteUtils.quote(node.name)); - + // Handle indirection for INSERT column lists (e.g., q.c1.r) if (node.indirection && node.indirection.length > 0) { const indirectionStrs = ListUtils.unwrapList(node.indirection).map(item => { @@ -1224,13 +1230,13 @@ export class Deparser implements DeparserVisitor { if (node.val) { output.push(this.deparse(node.val, context)); } - + if (node.name) { output.push('AS'); output.push(QuoteUtils.quote(node.name)); } } - + return output.join(' '); } @@ -1261,13 +1267,13 @@ export class Deparser implements DeparserVisitor { BoolExpr(node: t.BoolExpr, context: DeparserContext): string { const boolop = node.boolop as string; const args = ListUtils.unwrapList(node.args); - + let formatStr = '%s'; if (context.bool) { formatStr = '(%s)'; } - - const boolContext = { ...context, bool: true }; + + const boolContext = context.spawn('BoolExpr', { bool: true }); // explanation of our syntax/fix below: // return formatStr.replace('%s', andArgs); // ❌ Interprets $ as special syntax @@ -1275,16 +1281,16 @@ export class Deparser implements DeparserVisitor { switch (boolop) { case 'AND_EXPR': - if (this.formatter.isPretty() && args.length > 1) { - const andArgs = args.map(arg => this.visit(arg, boolContext)).join(this.formatter.newline() + ' AND '); + if (context.isPretty() && args.length > 1) { + const andArgs = args.map(arg => this.visit(arg, boolContext)).join(context.newline() + context.indent('AND ')); return formatStr.replace('%s', () => andArgs); } else { const andArgs = args.map(arg => this.visit(arg, boolContext)).join(' AND '); return formatStr.replace('%s', () => andArgs); } case 'OR_EXPR': - if (this.formatter.isPretty() && args.length > 1) { - const orArgs = args.map(arg => this.visit(arg, boolContext)).join(this.formatter.newline() + ' OR '); + if (context.isPretty() && args.length > 1) { + const orArgs = args.map(arg => this.visit(arg, boolContext)).join(context.newline() + context.indent('OR ')); return formatStr.replace('%s', () => orArgs); } else { const orArgs = args.map(arg => this.visit(arg, boolContext)).join(' OR '); @@ -1308,24 +1314,24 @@ export class Deparser implements DeparserVisitor { const xmlDoc = this.visit(args[1], context); return `xmlexists (${xpath} PASSING ${xmlDoc})`; } - + // Handle EXTRACT function with SQL syntax if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.extract' && args.length >= 2) { const field = this.visit(args[0], context); const source = this.visit(args[1], context); return `EXTRACT(${field} FROM ${source})`; } - + // Handle TRIM function with SQL syntax (TRIM TRAILING/LEADING/BOTH) if (node.funcformat === 'COERCE_SQL_SYNTAX' && (name === 'pg_catalog.rtrim' || name === 'pg_catalog.ltrim' || name === 'pg_catalog.btrim') && args.length >= 1) { const source = this.visit(args[0], context); let trimChar = ''; - + // Handle optional trim character (second argument) if (args.length >= 2) { trimChar = ` ${this.visit(args[1], context)}`; } - + if (name === 'pg_catalog.rtrim') { return `TRIM(TRAILING${trimChar} FROM ${source})`; } else if (name === 'pg_catalog.ltrim') { @@ -1334,13 +1340,13 @@ export class Deparser implements DeparserVisitor { return `TRIM(BOTH${trimChar} FROM ${source})`; } } - + // Handle COLLATION FOR function - use SQL syntax instead of function call if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.pg_collation_for') { const argStrs = args.map(arg => this.visit(arg, context)); return `COLLATION FOR (${argStrs.join(', ')})`; } - + // Handle SUBSTRING function with FROM ... FOR ... syntax if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.substring') { const source = this.visit(args[0], context); @@ -1353,7 +1359,7 @@ export class Deparser implements DeparserVisitor { return `SUBSTRING(${source} FROM ${start})`; } } - + // Handle POSITION function with IN syntax if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.position') { if (args.length === 2) { @@ -1362,7 +1368,7 @@ export class Deparser implements DeparserVisitor { return `POSITION(${substring} IN ${string})`; } } - + // Handle OVERLAY function with PLACING ... FROM ... FOR ... syntax if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.overlay') { if (args.length === 4) { @@ -1378,7 +1384,7 @@ export class Deparser implements DeparserVisitor { return `OVERLAY(${string} PLACING ${substring} FROM ${start})`; } } - + // Handle IS NORMALIZED function with SQL syntax if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.is_normalized') { const string = this.visit(args[0], context); @@ -1390,7 +1396,7 @@ export class Deparser implements DeparserVisitor { return `${string} IS NORMALIZED`; } } - + // Handle NORMALIZE function with SQL syntax if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.normalize') { const string = this.visit(args[0], context); @@ -1402,12 +1408,12 @@ export class Deparser implements DeparserVisitor { return `normalize(${string})`; } } - + // Handle SYSTEM_USER function with SQL syntax (no parentheses) if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.system_user' && args.length === 0) { return 'SYSTEM_USER'; } - + // Handle OVERLAPS operator with special infix syntax if (name === 'pg_catalog.overlaps' && args.length === 4) { const left1 = this.visit(args[0], context); @@ -1421,20 +1427,20 @@ export class Deparser implements DeparserVisitor { if (name === 'pg_catalog.timezone' && args.length === 2) { let timestamp = this.visit(args[1], context); const timezone = this.visit(args[0], context); - + // Add parentheses around timestamp if it contains arithmetic operations if (args[1] && 'A_Expr' in args[1] && args[1].A_Expr?.kind === 'AEXPR_OP') { - const op = this.deparseOperatorName(ListUtils.unwrapList(args[1].A_Expr.name)); + const op = this.deparseOperatorName(ListUtils.unwrapList(args[1].A_Expr.name), context); if (op === '+' || op === '-' || op === '*' || op === '/') { - timestamp = this.formatter.parens(timestamp); + timestamp = context.parens(timestamp); } } - + return `${timestamp} AT TIME ZONE ${timezone}`; } const params: string[] = []; - + if (node.agg_star) { if (node.agg_distinct) { params.push('DISTINCT *'); @@ -1447,7 +1453,7 @@ export class Deparser implements DeparserVisitor { const lastIndex = argStrs.length - 1; argStrs[lastIndex] = `VARIADIC ${argStrs[lastIndex]}`; } - + if (node.agg_distinct && argStrs.length > 0) { params.push('DISTINCT ' + argStrs.join(', ')); } else { @@ -1487,29 +1493,29 @@ export class Deparser implements DeparserVisitor { result += ` OVER ${node.over.name}`; } else { const windowParts: string[] = []; - + if (node.over.partitionClause) { const partitions = ListUtils.unwrapList(node.over.partitionClause); const partitionStrs = partitions.map(p => this.visit(p, context)); windowParts.push(`PARTITION BY ${partitionStrs.join(', ')}`); } - + if (node.over.orderClause) { const orders = ListUtils.unwrapList(node.over.orderClause); const orderStrs = orders.map(o => this.visit(o, context)); windowParts.push(`ORDER BY ${orderStrs.join(', ')}`); } - + // Handle window frame specifications using the dedicated formatWindowFrame method - const frameClause = this.formatWindowFrame(node.over); + const frameClause = this.formatWindowFrame(node.over, context.spawn('FuncCall')); if (frameClause) { windowParts.push(frameClause); } - + if (windowParts.length > 0) { - if (this.formatter.isPretty() && windowParts.length > 1) { - const formattedParts = windowParts.map(part => this.formatter.indent(part)); - result += ` OVER (${this.formatter.newline()}${formattedParts.join(this.formatter.newline())}${this.formatter.newline()})`; + if (context.isPretty() && windowParts.length > 1) { + const formattedParts = windowParts.map(part => context.indent(part)); + result += ` OVER (${context.newline()}${formattedParts.join(context.newline())}${context.newline()})`; } else { result += ` OVER (${windowParts.join(' ')})`; } @@ -1530,7 +1536,7 @@ export class Deparser implements DeparserVisitor { A_Const(node: t.A_Const, context: DeparserContext): string { const nodeAny = node as any; - + if (nodeAny.ival !== undefined) { if (typeof nodeAny.ival === 'object' && nodeAny.ival !== null) { if (nodeAny.ival.ival !== undefined) { @@ -1620,7 +1626,7 @@ export class Deparser implements DeparserVisitor { return `b'${bsval}'`; } } - + if (nodeAny.val) { if (nodeAny.val.Integer?.ival !== undefined) { return nodeAny.val.Integer.ival.toString(); @@ -1634,11 +1640,11 @@ export class Deparser implements DeparserVisitor { return nodeAny.val.BitString.bsval; } } - + if (nodeAny.isnull === true) { return 'NULL'; } - + if (typeof nodeAny === 'object' && nodeAny !== null) { if (nodeAny.Boolean !== undefined) { return nodeAny.Boolean ? 'true' : 'false'; @@ -1661,15 +1667,15 @@ export class Deparser implements DeparserVisitor { } return QuoteUtils.escape(nodeAny.String); } - + if (Object.keys(nodeAny).length === 0) { return 'NULL'; } - + console.warn('A_Const: Unhandled object structure:', JSON.stringify(nodeAny, null, 2)); return 'NULL'; } - + return 'NULL'; } @@ -1691,7 +1697,7 @@ export class Deparser implements DeparserVisitor { } const output: string[] = []; - + // Handle SETOF keyword if (node.setof) { output.push('SETOF'); @@ -1714,7 +1720,7 @@ export class Deparser implements DeparserVisitor { const nameStr = typeof name === 'string' ? name : (name.String?.sval || name.String?.str); return nameStr === 'interval'; }); - + if (isInterval) { args = this.formatIntervalTypeMods(node.typmods, context); // For interval types, we'll handle the formatting differently to avoid parentheses @@ -1752,7 +1758,7 @@ export class Deparser implements DeparserVisitor { if (names.length === 1) { const typeName = names[0]; - + if (typeName === 'char') { if (context.parentNodeTypes.includes('TypeCast') && args === '1') { output.push('"char"'); @@ -1761,29 +1767,29 @@ export class Deparser implements DeparserVisitor { } return output.join(' '); } - + const quotedTypeName = QuoteUtils.quote(typeName); let result = mods(quotedTypeName, args); - + if (node.arrayBounds && node.arrayBounds.length > 0) { result += formatArrayBounds(node.arrayBounds); } - + output.push(result); return output.join(' '); } if (names.length === 2) { const [catalog, type] = names; - + if (catalog === 'pg_catalog' && type === 'char') { output.push(mods('pg_catalog.char', args)); return output.join(' '); } - + if (catalog === 'pg_catalog') { let typeName = `${catalog}.${type}`; - + if (type === 'bpchar' && args) { typeName = 'char'; } else if (type === 'varchar') { @@ -1825,13 +1831,13 @@ export class Deparser implements DeparserVisitor { typeName = 'time with time zone'; } } - + let result = mods(typeName, args); - + if (node.arrayBounds && node.arrayBounds.length > 0) { result += formatArrayBounds(node.arrayBounds); } - + output.push(result); return output.join(' '); } @@ -1839,11 +1845,11 @@ export class Deparser implements DeparserVisitor { const quotedNames = names.map((name: string) => QuoteUtils.quote(name)); let result = mods(quotedNames.join('.'), args); - + if (node.arrayBounds && node.arrayBounds.length > 0) { result += formatArrayBounds(node.arrayBounds); } - + output.push(result); return output.join(' '); } @@ -1862,7 +1868,7 @@ export class Deparser implements DeparserVisitor { } return this.quoteIfNeeded(colStr); }); - output.push('AS', this.quoteIfNeeded(name) + this.formatter.parens(quotedColnames.join(', '))); + output.push('AS', this.quoteIfNeeded(name) + context.parens(quotedColnames.join(', '))); } else { output.push('AS', this.quoteIfNeeded(name)); } @@ -1873,13 +1879,13 @@ export class Deparser implements DeparserVisitor { RangeVar(node: t.RangeVar, context: DeparserContext): string { const output: string[] = []; // Handle ONLY keyword for inheritance control (but not for type definitions, ALTER TYPE, or CREATE FOREIGN TABLE) - if (node && (!('inh' in node) || node.inh === undefined) && - !context.parentNodeTypes.includes('CompositeTypeStmt') && - !context.parentNodeTypes.includes('AlterTypeStmt') && + if (node && (!('inh' in node) || node.inh === undefined) && + !context.parentNodeTypes.includes('CompositeTypeStmt') && + (!context.parentNodeTypes.includes('AlterTypeStmt') && context.objtype !== 'OBJECT_TYPE') && !context.parentNodeTypes.includes('CreateForeignTableStmt')) { output.push('ONLY'); } - + let tableName = ''; if (node.catalogname) { tableName = QuoteUtils.quote(node.catalogname); @@ -1900,7 +1906,7 @@ export class Deparser implements DeparserVisitor { } const result = output.join(' '); - + return result; } @@ -1948,20 +1954,20 @@ export class Deparser implements DeparserVisitor { if (mods.length === 2) { const firstMod = mods[0]; const secondMod = mods[1]; - + if (firstMod && typeof firstMod === 'object') { const firstConst = (firstMod as any).A_Const; if (firstConst && firstConst.ival !== undefined) { const firstValue = typeof firstConst.ival === 'object' ? firstConst.ival.ival : firstConst.ival; - + // Check if second mod is precision (empty ival object or specific precision value) if (secondMod && typeof secondMod === 'object') { const secondConst = (secondMod as any).A_Const; if (secondConst && secondConst.ival !== undefined) { - const secondValue = typeof secondConst.ival === 'object' ? - (secondConst.ival.ival !== undefined ? secondConst.ival.ival : 0) : + const secondValue = typeof secondConst.ival === 'object' ? + (secondConst.ival.ival !== undefined ? secondConst.ival.ival : 0) : secondConst.ival; - + if (firstValue === 32767 && secondValue >= 0) { return `(${secondValue})`; } @@ -2010,18 +2016,18 @@ export class Deparser implements DeparserVisitor { } return true; }); - + if (filteredMods.length === 0) { return null; } - + return filteredMods.map(mod => { return this.deparse(mod, context); }).join(', '); } formatSingleTypeMod(typemod: number, typeName: string): string | null { - + switch (typeName) { case 'varchar': case 'bpchar': @@ -2053,7 +2059,7 @@ export class Deparser implements DeparserVisitor { } break; } - + return null; } @@ -2093,17 +2099,17 @@ export class Deparser implements DeparserVisitor { isPgCatalogType(typeName: string): boolean { const cleanTypeName = typeName.replace(/^pg_catalog\./, ''); - + if (pgCatalogTypes.includes(cleanTypeName)) { return true; } - + for (const [realType, aliases] of pgCatalogTypeAliases) { if (aliases.includes(cleanTypeName)) { return true; } } - + return false; } @@ -2135,15 +2141,15 @@ export class Deparser implements DeparserVisitor { A_Indirection(node: t.A_Indirection, context: DeparserContext): string { let argStr = this.visit(node.arg, context); - + const argType = this.getNodeType(node.arg); if (argType === 'TypeCast' || argType === 'SubLink' || argType === 'A_Expr' || argType === 'FuncCall' || argType === 'A_Indirection' || argType === 'ColumnRef' || argType === 'RowExpr') { argStr = `(${argStr})`; } - + const output = [argStr]; const indirection = ListUtils.unwrapList(node.indirection); - + for (const subnode of indirection) { if (subnode.String || subnode.A_Star) { const value = subnode.A_Star ? '*' : QuoteUtils.quote(subnode.String.sval || subnode.String.str); @@ -2152,12 +2158,12 @@ export class Deparser implements DeparserVisitor { output.push(this.visit(subnode, context)); } } - + return output.join(''); } - A_Star(node: t.A_Star, context: DeparserContext): string { - return '*'; + A_Star(node: t.A_Star, context: DeparserContext): string { + return '*'; } CaseExpr(node: t.CaseExpr, context: DeparserContext): string { @@ -2168,29 +2174,29 @@ export class Deparser implements DeparserVisitor { } const args = ListUtils.unwrapList(node.args); - - if (this.formatter.isPretty() && args.length > 0) { + + if (context.isPretty() && args.length > 0) { for (const arg of args) { - const whenClause = this.visit(arg, context); + const whenClause = this.visit(arg, context.spawn('CaseExpr')); if (this.containsMultilineStringLiteral(whenClause)) { - output.push(this.formatter.newline() + whenClause); + output.push(context.newline() + whenClause); } else { - output.push(this.formatter.newline() + this.formatter.indent(whenClause)); + output.push(context.newline() + context.indent(whenClause)); } } if (node.defresult) { - const elseResult = this.visit(node.defresult, context); + const elseResult = this.visit(node.defresult, context.spawn('CaseExpr')); if (this.containsMultilineStringLiteral(elseResult)) { - output.push(this.formatter.newline() + 'ELSE ' + elseResult); + output.push(context.newline() + 'ELSE ' + elseResult); } else { - output.push(this.formatter.newline() + this.formatter.indent('ELSE ' + elseResult)); + output.push(context.newline() + context.indent('ELSE ' + elseResult)); } } - output.push(this.formatter.newline() + 'END'); + output.push(context.newline() + 'END'); return output.join(' '); - } else { + }else { for (const arg of args) { output.push(this.visit(arg, context)); } @@ -2214,25 +2220,25 @@ export class Deparser implements DeparserVisitor { TypeCast(node: t.TypeCast, context: DeparserContext): string { const arg = this.visit(node.arg, context); const typeName = this.TypeName(node.typeName, context); - + // Check if this is a bpchar typecast that should preserve original syntax for AST consistency if (typeName === 'bpchar' || typeName === 'pg_catalog.bpchar') { const names = node.typeName?.names; - const isQualifiedBpchar = names && names.length === 2 && - (names[0] as any)?.String?.sval === 'pg_catalog' && + const isQualifiedBpchar = names && names.length === 2 && + (names[0] as any)?.String?.sval === 'pg_catalog' && (names[1] as any)?.String?.sval === 'bpchar'; - + if (isQualifiedBpchar) { return `CAST(${arg} AS ${typeName})`; } } - + if (this.isPgCatalogType(typeName)) { const argType = this.getNodeType(node.arg); - + const isSimpleArgument = argType === 'A_Const' || argType === 'ColumnRef'; const isFunctionCall = argType === 'FuncCall'; - + if (isSimpleArgument || isFunctionCall) { // For simple arguments, avoid :: syntax if they have complex structure if (isSimpleArgument && (arg.includes('(') || arg.startsWith('-'))) { @@ -2242,40 +2248,40 @@ export class Deparser implements DeparserVisitor { } } } - + return `CAST(${arg} AS ${typeName})`; } CollateClause(node: t.CollateClause, context: DeparserContext): string { const output: string[] = []; - + if (node.arg) { let argStr = this.visit(node.arg, context); - + const argType = this.getNodeType(node.arg); if (argType === 'A_Expr' || argType === 'FuncCall' || argType === 'SubLink') { argStr = `(${argStr})`; } - + output.push(argStr); } - + output.push('COLLATE'); - + if (node.collname) { const collname = ListUtils.unwrapList(node.collname); output.push(collname.map(n => this.visit(n, context)).join('.')); } - + return output.join(' '); } BooleanTest(node: t.BooleanTest, context: DeparserContext): string { const output: string[] = []; - const boolContext = { ...context, bool: true }; - + const boolContext = context.spawn('BooleanTest', { bool: true }); + output.push(this.visit(node.arg, boolContext)); - + switch (node.booltesttype as string) { case 'IS_TRUE': output.push('IS TRUE'); @@ -2296,15 +2302,15 @@ export class Deparser implements DeparserVisitor { output.push('IS NOT UNKNOWN'); break; } - + return output.join(' '); } NullTest(node: t.NullTest, context: DeparserContext): string { const output: string[] = []; - + output.push(this.visit(node.arg, context)); - + switch (node.nulltesttype as string) { case 'IS_NULL': output.push('IS NULL'); @@ -2313,7 +2319,7 @@ export class Deparser implements DeparserVisitor { output.push('IS NOT NULL'); break; } - + return output.join(' '); } @@ -2332,12 +2338,12 @@ export class Deparser implements DeparserVisitor { private static needsQuotes(value: string): boolean { if (!value) return false; - + const needsQuotesRegex = /[a-z]+[\W\w]*[A-Z]+|[A-Z]+[\W\w]*[a-z]+|\W/; - + const isAllUppercase = /^[A-Z]+$/.test(value); - - return needsQuotesRegex.test(value) || + + return needsQuotesRegex.test(value) || Deparser.RESERVED_WORDS.has(value.toLowerCase()) || isAllUppercase; } @@ -2352,7 +2358,7 @@ export class Deparser implements DeparserVisitor { preserveOperatorDefElemCase(defName: string): string { const caseMap: { [key: string]: string } = { 'leftarg': 'Leftarg', - 'rightarg': 'Rightarg', + 'rightarg': 'Rightarg', 'procedure': 'Procedure', 'function': 'Function', 'commutator': 'Commutator', @@ -2362,7 +2368,7 @@ export class Deparser implements DeparserVisitor { 'hashes': 'Hashes', 'merges': 'Merges' }; - + return caseMap[defName.toLowerCase()] || defName; } @@ -2372,14 +2378,14 @@ export class Deparser implements DeparserVisitor { if (context.isStringLiteral || context.isEnumValue) { return QuoteUtils.formatEString(node.sval || ''); } - + const value = node.sval || ''; - - if (context.parentNodeTypes.includes('DefElem') || + + if (context.parentNodeTypes.includes('DefElem') || context.parentNodeTypes.includes('CreateOpClassItem')) { return value; } - + if (context.parentNodeTypes.includes('ObjectWithArgs')) { // Check if this is a pure operator symbol (only operator characters, no alphanumeric) const pureOperatorRegex = /^[+\-*/<>=~!@#%^&|`?]+$/; @@ -2387,23 +2393,23 @@ export class Deparser implements DeparserVisitor { return value; // Don't quote pure operator symbols like "=" or "-" } } - + return Deparser.needsQuotes(value) ? `"${value}"` : value; } - - Integer(node: t.Integer, context: DeparserContext): string { - return node.ival?.toString() || '0'; + + Integer(node: t.Integer, context: DeparserContext): string { + return node.ival?.toString() || '0'; } - - Float(node: t.Float, context: DeparserContext): string { - return node.fval || '0.0'; + + Float(node: t.Float, context: DeparserContext): string { + return node.fval || '0.0'; } - - Boolean(node: t.Boolean, context: DeparserContext): string { - return node.boolval ? 'true' : 'false'; + + Boolean(node: t.Boolean, context: DeparserContext): string { + return node.boolval ? 'true' : 'false'; } - - BitString(node: t.BitString, context: DeparserContext): string { + + BitString(node: t.BitString, context: DeparserContext): string { // Check if this is a hexadecimal bit string (starts with x) if (node.bsval.startsWith('x')) { return `x'${node.bsval.substring(1)}'`; @@ -2412,11 +2418,11 @@ export class Deparser implements DeparserVisitor { return `b'${node.bsval.substring(1)}'`; } // Fallback for raw values without prefix - return `b'${node.bsval}'`; + return `b'${node.bsval}'`; } - - Null(node: t.Node, context: DeparserContext): string { - return 'NULL'; + + Null(node: t.Node, context: DeparserContext): string { + return 'NULL'; } List(node: t.List, context: DeparserContext): string { @@ -2447,36 +2453,36 @@ export class Deparser implements DeparserVisitor { if (node.ofTypename) { output.push('OF'); output.push(this.TypeName(node.ofTypename, context)); - + // Handle additional constraints for typed tables if (node.tableElts) { const elements = ListUtils.unwrapList(node.tableElts); const elementStrs = elements.map(el => { return this.deparse(el, context); }); - output.push(this.formatter.parens(elementStrs.join(', '))); + output.push(context.parens(elementStrs.join(', '))); } } else if (node.tableElts) { const elements = ListUtils.unwrapList(node.tableElts); const elementStrs = elements.map(el => { - return this.deparse(el, context); + return this.deparse(el, context.spawn('CreateStmt')); }); - - if (this.formatter.isPretty()) { + + if (context.isPretty()) { const formattedElements = elementStrs.map(el => { const trimmedEl = el.trim(); // Remove leading newlines from constraint elements to avoid extra blank lines if (trimmedEl.startsWith('\n')) { - return this.formatter.indent(trimmedEl.substring(1)); + return context.indentToCurrentLevel(trimmedEl.substring(1)); } - return this.formatter.indent(trimmedEl); - }).join(',' + this.formatter.newline()); - output.push('(' + this.formatter.newline() + formattedElements + this.formatter.newline() + ')'); + return context.indent(trimmedEl); + }).join(',' + context.newline()); + output.push('(' + context.newline() + formattedElements + context.newline() + ')'); } else { - output.push(this.formatter.parens(elementStrs.join(', '))); + output.push(context.parens(elementStrs.join(', '))); } }else if (!node.partbound) { - output.push(this.formatter.parens('')); + output.push(context.parens('')); } if (node.partbound && node.inhRelations && node.inhRelations.length > 0) { @@ -2484,7 +2490,7 @@ export class Deparser implements DeparserVisitor { const inherits = ListUtils.unwrapList(node.inhRelations); const inheritStrs = inherits.map(rel => this.visit(rel, context)); output.push(inheritStrs[0]); - + if (node.partbound.strategy === 'l' && node.partbound.listdatums) { output.push('FOR VALUES IN'); const listValues = ListUtils.unwrapList(node.partbound.listdatums) @@ -2517,7 +2523,7 @@ export class Deparser implements DeparserVisitor { output.push('INHERITS'); const inherits = ListUtils.unwrapList(node.inhRelations); const inheritStrs = inherits.map(rel => this.visit(rel, context)); - output.push(this.formatter.parens(inheritStrs.join(', '))); + output.push(context.parens(inheritStrs.join(', '))); } if (node.partspec) { @@ -2558,7 +2564,7 @@ export class Deparser implements DeparserVisitor { // Handle table options like WITH (fillfactor=10) if (node.options && node.options.length > 0) { - const createStmtContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateStmt'] }; + const createStmtContext = context.spawn('CreateStmt'); const optionStrs = node.options.map((option: any) => { return this.deparse(option, createStmtContext); }); @@ -2587,7 +2593,7 @@ export class Deparser implements DeparserVisitor { if (node.fdwoptions && node.fdwoptions.length > 0) { output.push('OPTIONS'); - const columnContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'ColumnDef'] }; + const columnContext = context.spawn('ColumnDef'); const options = ListUtils.unwrapList(node.fdwoptions).map(opt => this.visit(opt, columnContext)); output.push(`(${options.join(', ')})`); } @@ -2599,7 +2605,7 @@ export class Deparser implements DeparserVisitor { if (node.constraints) { const constraints = ListUtils.unwrapList(node.constraints); const constraintStrs = constraints.map(constraint => { - const columnConstraintContext = { ...context, isColumnConstraint: true }; + const columnConstraintContext = context.spawn('ColumnDef', { isColumnConstraint: true }); return this.visit(constraint, columnConstraintContext); }); output.push(...constraintStrs); @@ -2646,21 +2652,21 @@ export class Deparser implements DeparserVisitor { } break; case 'CONSTR_CHECK': - if (this.formatter.isPretty() && !context.isColumnConstraint) { - output.push('\n' + this.formatter.indent('CHECK')); + if (context.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.indent('CHECK')); } else { output.push('CHECK'); } if (node.raw_expr) { - if (this.formatter.isPretty()) { - const checkExpr = this.visit(node.raw_expr, context); + if (context.isPretty()) { + const checkExpr = this.visit(node.raw_expr, context.spawn('Constraint', { indentLevel: context.indentLevel + 1 })); if (checkExpr.includes('\n')) { - output.push('(\n' + this.formatter.indent(checkExpr) + '\n)'); + output.push('(\n' + context.indent(checkExpr) + '\n)'); } else { output.push(`(${checkExpr})`); } } else { - output.push(this.formatter.parens(this.visit(node.raw_expr, context))); + output.push(context.parens(this.visit(node.raw_expr, context))); } } // Handle NOT VALID for check constraints @@ -2681,7 +2687,7 @@ export class Deparser implements DeparserVisitor { } output.push('AS'); if (node.raw_expr) { - output.push(this.formatter.parens(this.visit(node.raw_expr, context))); + output.push(context.parens(this.visit(node.raw_expr, context))); } output.push('STORED'); break; @@ -2733,8 +2739,8 @@ export class Deparser implements DeparserVisitor { } break; case 'CONSTR_UNIQUE': - if (this.formatter.isPretty() && !context.isColumnConstraint) { - output.push('\n' + this.formatter.indent('UNIQUE')); + if (context.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.indent('UNIQUE')); } else { output.push('UNIQUE'); } @@ -2755,15 +2761,15 @@ export class Deparser implements DeparserVisitor { case 'CONSTR_FOREIGN': // Only add "FOREIGN KEY" for table-level constraints, not column-level constraints if (!context.isColumnConstraint) { - if (this.formatter.isPretty()) { - output.push('\n' + this.formatter.indent('FOREIGN KEY')); + if (context.isPretty()) { + output.push('\n' + context.indent('FOREIGN KEY')); if (node.fk_attrs && node.fk_attrs.length > 0) { const fkAttrs = ListUtils.unwrapList(node.fk_attrs) .map(attr => this.visit(attr, context)) .join(', '); output.push(`(${fkAttrs})`); } - output.push('\n' + this.formatter.indent('REFERENCES')); + output.push('\n' + context.indent('REFERENCES')); } else { output.push('FOREIGN KEY'); if (node.fk_attrs && node.fk_attrs.length > 0) { @@ -2778,7 +2784,7 @@ export class Deparser implements DeparserVisitor { output.push('REFERENCES'); } if (node.pktable) { - if (this.formatter.isPretty() && !context.isColumnConstraint) { + if (context.isPretty() && !context.isColumnConstraint) { const lastIndex = output.length - 1; if (lastIndex >= 0 && output[lastIndex].includes('REFERENCES')) { output[lastIndex] += ' ' + this.RangeVar(node.pktable, context); @@ -2793,7 +2799,7 @@ export class Deparser implements DeparserVisitor { const pkAttrs = ListUtils.unwrapList(node.pk_attrs) .map(attr => this.visit(attr, context)) .join(', '); - if (this.formatter.isPretty() && !context.isColumnConstraint) { + if (context.isPretty() && !context.isColumnConstraint) { const lastIndex = output.length - 1; if (lastIndex >= 0) { output[lastIndex] += ` (${pkAttrs})`; @@ -2814,8 +2820,8 @@ export class Deparser implements DeparserVisitor { matchClause = 'MATCH PARTIAL'; break; } - if (this.formatter.isPretty() && !context.isColumnConstraint) { - output.push('\n' + this.formatter.indent(matchClause)); + if (context.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.indent(matchClause)); } else { output.push(matchClause); } @@ -2836,8 +2842,8 @@ export class Deparser implements DeparserVisitor { updateClause += 'SET DEFAULT'; break; } - if (this.formatter.isPretty()) { - output.push('\n' + this.formatter.indent(updateClause)); + if (context.isPretty()) { + output.push('\n' + context.indent(updateClause)); } else { output.push('ON UPDATE'); output.push(updateClause.replace('ON UPDATE ', '')); @@ -2859,8 +2865,8 @@ export class Deparser implements DeparserVisitor { deleteClause += 'SET DEFAULT'; break; } - if (this.formatter.isPretty()) { - output.push('\n' + this.formatter.indent(deleteClause)); + if (context.isPretty()) { + output.push('\n' + context.indent(deleteClause)); } else { output.push('ON DELETE'); output.push(deleteClause.replace('ON DELETE ', '')); @@ -2868,8 +2874,8 @@ export class Deparser implements DeparserVisitor { } // Handle NOT VALID for foreign key constraints - only for table constraints, not domain constraints if (node.skip_validation && !context.isDomainConstraint) { - if (this.formatter.isPretty() && !context.isColumnConstraint) { - output.push('\n' + this.formatter.indent('NOT VALID')); + if (context.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.indent('NOT VALID')); } else { output.push('NOT VALID'); } @@ -2925,12 +2931,12 @@ export class Deparser implements DeparserVisitor { // Handle deferrable constraints for all constraint types that support it if (node.contype === 'CONSTR_PRIMARY' || node.contype === 'CONSTR_UNIQUE' || node.contype === 'CONSTR_FOREIGN') { if (node.deferrable) { - if (this.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') { - output.push('\n' + this.formatter.indent('DEFERRABLE')); + if (context.isPretty() && node.contype === 'CONSTR_FOREIGN') { + output.push('\n' + context.indent('DEFERRABLE')); if (node.initdeferred === true) { - output.push('\n' + this.formatter.indent('INITIALLY DEFERRED')); + output.push('\n' + context.indent('INITIALLY DEFERRED')); } else if (node.initdeferred === false) { - output.push('\n' + this.formatter.indent('INITIALLY IMMEDIATE')); + output.push('\n' + context.indent('INITIALLY IMMEDIATE')); } } else { output.push('DEFERRABLE'); @@ -2941,15 +2947,15 @@ export class Deparser implements DeparserVisitor { } } } else if (node.deferrable === false) { - if (this.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') { - output.push('\n' + this.formatter.indent('NOT DEFERRABLE')); + if (context.isPretty() && node.contype === 'CONSTR_FOREIGN') { + output.push('\n' + context.indent('NOT DEFERRABLE')); } else { output.push('NOT DEFERRABLE'); } } } - if (this.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') { + if (context.isPretty() && node.contype === 'CONSTR_FOREIGN') { let result = ''; for (let i = 0; i < output.length; i++) { if (output[i].startsWith('\n')) { @@ -2967,13 +2973,14 @@ export class Deparser implements DeparserVisitor { } SubLink(node: t.SubLink, context: DeparserContext): string { - const subselect = this.formatter.parens(this.visit(node.subselect, context)); - + const sublinkContext = context.spawn('SubLink', { indentLevel: context.indentLevel + 1 }); + const subselect = context.parens(this.visit(node.subselect, sublinkContext)); + switch (node.subLinkType) { case 'ANY_SUBLINK': if (node.testexpr && node.operName) { const testExpr = this.visit(node.testexpr, context); - const operator = this.deparseOperatorName(node.operName); + const operator = this.deparseOperatorName(node.operName, context); return `${testExpr} ${operator} ANY ${subselect}`; } else if (node.testexpr) { const testExpr = this.visit(node.testexpr, context); @@ -2983,7 +2990,7 @@ export class Deparser implements DeparserVisitor { case 'ALL_SUBLINK': if (node.testexpr && node.operName) { const testExpr = this.visit(node.testexpr, context); - const operator = this.deparseOperatorName(node.operName); + const operator = this.deparseOperatorName(node.operName, context); return `${testExpr} ${operator} ALL ${subselect}`; } return subselect; @@ -2999,49 +3006,49 @@ export class Deparser implements DeparserVisitor { CaseWhen(node: t.CaseWhen, context: DeparserContext): string { const output: string[] = ['WHEN']; - + if (node.expr) { output.push(this.visit(node.expr, context)); } - + output.push('THEN'); - + if (node.result) { output.push(this.visit(node.result, context)); } - + return output.join(' '); } WindowDef(node: t.WindowDef, context: DeparserContext): string { const output: string[] = []; - + if (node.name) { output.push(node.name); } - + const windowParts: string[] = []; - + if (node.partitionClause) { const partitions = ListUtils.unwrapList(node.partitionClause); const partitionStrs = partitions.map(p => this.visit(p, context)); windowParts.push(`PARTITION BY ${partitionStrs.join(', ')}`); } - + if (node.orderClause) { const orders = ListUtils.unwrapList(node.orderClause); const orderStrs = orders.map(o => this.visit(o, context)); windowParts.push(`ORDER BY ${orderStrs.join(', ')}`); } - + // Only add frame clause if frameOptions indicates non-default framing if (node.frameOptions && node.frameOptions !== 1058) { - const frameClause = this.formatWindowFrame(node); + const frameClause = this.formatWindowFrame(node, context.spawn('WindowDef')); if (frameClause) { windowParts.push(frameClause); } } - + if (windowParts.length > 0) { if (node.name) { output.push('AS'); @@ -3053,30 +3060,30 @@ export class Deparser implements DeparserVisitor { } else if (output.length === 0) { output.push('()'); } - + return output.join(' '); } - formatWindowFrame(node: any): string | null { + formatWindowFrame(node: any, context: DeparserContext): string | null { if (!node.frameOptions) return null; - + const frameOptions = node.frameOptions; const frameParts: string[] = []; - + if (frameOptions & 0x01) { // FRAMEOPTION_NONDEFAULT if (frameOptions & 0x02) { // FRAMEOPTION_RANGE frameParts.push('RANGE'); - } else if (frameOptions & 0x04) { // FRAMEOPTION_ROWS + } else if (frameOptions & 0x04) { // FRAMEOPTION_ROWS frameParts.push('ROWS'); } else if (frameOptions & 0x08) { // FRAMEOPTION_GROUPS frameParts.push('GROUPS'); } } - + if (frameParts.length === 0) return null; - + const boundsParts: string[] = []; - + // Handle specific frameOptions values that have known mappings if (frameOptions === 789) { boundsParts.push('CURRENT ROW'); @@ -3086,8 +3093,8 @@ export class Deparser implements DeparserVisitor { boundsParts.push('AND CURRENT ROW'); } else if (frameOptions === 18453) { if (node.startOffset && node.endOffset) { - boundsParts.push(`${this.visit(node.startOffset, { parentNodeTypes: [] })} PRECEDING`); - boundsParts.push(`AND ${this.visit(node.endOffset, { parentNodeTypes: [] })} FOLLOWING`); + boundsParts.push(`${this.visit(node.startOffset, context)} PRECEDING`); + boundsParts.push(`AND ${this.visit(node.endOffset, context)} FOLLOWING`); } } else if (frameOptions === 1557) { boundsParts.push('CURRENT ROW'); @@ -3095,7 +3102,7 @@ export class Deparser implements DeparserVisitor { } else if (frameOptions === 16917) { boundsParts.push('CURRENT ROW'); if (node.endOffset) { - boundsParts.push(`AND ${this.visit(node.endOffset, { parentNodeTypes: [] })} FOLLOWING`); + boundsParts.push(`AND ${this.visit(node.endOffset, context)} FOLLOWING`); } } else if (frameOptions === 1058) { return null; @@ -3103,27 +3110,27 @@ export class Deparser implements DeparserVisitor { // Handle start bound - prioritize explicit offset values over bit flags if (node.startOffset) { if (frameOptions & 0x400) { // FRAMEOPTION_START_VALUE_PRECEDING - boundsParts.push(`${this.visit(node.startOffset, { parentNodeTypes: [] })} PRECEDING`); + boundsParts.push(`${this.visit(node.startOffset, context)} PRECEDING`); } else if (frameOptions & 0x800) { // FRAMEOPTION_START_VALUE_FOLLOWING - boundsParts.push(`${this.visit(node.startOffset, { parentNodeTypes: [] })} FOLLOWING`); + boundsParts.push(`${this.visit(node.startOffset, context)} FOLLOWING`); } else { - boundsParts.push(`${this.visit(node.startOffset, { parentNodeTypes: [] })} PRECEDING`); + boundsParts.push(`${this.visit(node.startOffset, context)} PRECEDING`); } } else if (frameOptions & 0x10) { // FRAMEOPTION_START_UNBOUNDED_PRECEDING boundsParts.push('UNBOUNDED PRECEDING'); } else if (frameOptions & 0x20) { // FRAMEOPTION_START_CURRENT_ROW boundsParts.push('CURRENT ROW'); } - + // Handle end bound - prioritize explicit offset values over bit flags if (node.endOffset) { if (boundsParts.length > 0) { if (frameOptions & 0x1000) { // FRAMEOPTION_END_VALUE_PRECEDING - boundsParts.push(`AND ${this.visit(node.endOffset, { parentNodeTypes: [] })} PRECEDING`); + boundsParts.push(`AND ${this.visit(node.endOffset, context)} PRECEDING`); } else if (frameOptions & 0x2000) { // FRAMEOPTION_END_VALUE_FOLLOWING - boundsParts.push(`AND ${this.visit(node.endOffset, { parentNodeTypes: [] })} FOLLOWING`); + boundsParts.push(`AND ${this.visit(node.endOffset, context)} FOLLOWING`); } else { - boundsParts.push(`AND ${this.visit(node.endOffset, { parentNodeTypes: [] })} FOLLOWING`); + boundsParts.push(`AND ${this.visit(node.endOffset, context)} FOLLOWING`); } } } else if (frameOptions & 0x80) { // FRAMEOPTION_END_UNBOUNDED_FOLLOWING @@ -3138,22 +3145,22 @@ export class Deparser implements DeparserVisitor { boundsParts.push('AND CURRENT ROW'); } } - + if (boundsParts.length > 0) { frameParts.push('BETWEEN'); frameParts.push(boundsParts.join(' ')); } - + return frameParts.join(' '); } SortBy(node: t.SortBy, context: DeparserContext): string { const output: string[] = []; - + if (node.node) { output.push(this.visit(node.node, context)); } - + if (node.sortby_dir === 'SORTBY_USING' && node.useOp) { output.push('USING'); const useOp = ListUtils.unwrapList(node.useOp); @@ -3168,13 +3175,13 @@ export class Deparser implements DeparserVisitor { } else if (node.sortby_dir === 'SORTBY_DESC') { output.push('DESC'); } - + if (node.sortby_nulls === 'SORTBY_NULLS_FIRST') { output.push('NULLS FIRST'); } else if (node.sortby_nulls === 'SORTBY_NULLS_LAST') { output.push('NULLS LAST'); } - + return output.join(' '); } @@ -3204,31 +3211,31 @@ export class Deparser implements DeparserVisitor { CommonTableExpr(node: t.CommonTableExpr, context: DeparserContext): string { const output: string[] = []; - + if (node.ctename) { output.push(node.ctename); } - + if (node.aliascolnames) { const colnames = ListUtils.unwrapList(node.aliascolnames); const colnameStrs = colnames.map(col => this.visit(col, context)); // Don't add space before column list parentheses to match original formatting - output[output.length - 1] += this.formatter.parens(colnameStrs.join(', ')); + output[output.length - 1] += context.parens(colnameStrs.join(', ')); } - + output.push('AS'); - + // Handle materialization clauses if (node.ctematerialized === 'CTEMaterializeNever') { output.push('NOT MATERIALIZED'); } else if (node.ctematerialized === 'CTEMaterializeAlways') { output.push('MATERIALIZED'); } - + if (node.ctequery) { - output.push(this.formatter.parens(this.visit(node.ctequery, context))); + output.push(context.parens(this.visit(node.ctequery, context))); } - + return output.join(' '); } @@ -3238,7 +3245,7 @@ export class Deparser implements DeparserVisitor { LockingClause(node: any, context: DeparserContext): string { const output: string[] = []; - + switch (node.strength) { case 'LCS_FORUPDATE': output.push('FOR UPDATE'); @@ -3255,7 +3262,7 @@ export class Deparser implements DeparserVisitor { default: throw new Error(`Unsupported locking strength: ${node.strength}`); } - + if (node.lockedRels && node.lockedRels.length > 0) { output.push('OF'); const relations = ListUtils.unwrapList(node.lockedRels) @@ -3263,20 +3270,20 @@ export class Deparser implements DeparserVisitor { .join(', '); output.push(relations); } - + if (node.waitPolicy === 'LockWaitSkip') { output.push('SKIP LOCKED'); } else if (node.waitPolicy === 'LockWaitError') { output.push('NOWAIT'); } - + return output.join(' '); } MinMaxExpr(node: t.MinMaxExpr, context: DeparserContext): string { const args = ListUtils.unwrapList(node.args); const argStrs = args.map(arg => this.visit(arg, context)); - + if (node.op === 'IS_GREATEST') { return `GREATEST(${argStrs.join(', ')})`; } else { @@ -3287,11 +3294,11 @@ export class Deparser implements DeparserVisitor { RowExpr(node: t.RowExpr, context: DeparserContext): string { const args = ListUtils.unwrapList(node.args); const argStrs = args.map(arg => this.visit(arg, context)); - + if (node.row_format === 'COERCE_IMPLICIT_CAST') { return `(${argStrs.join(', ')})`; } - + return `ROW(${argStrs.join(', ')})`; } @@ -3313,7 +3320,7 @@ export class Deparser implements DeparserVisitor { DistinctExpr(node: t.DistinctExpr, context: DeparserContext): string { const args = ListUtils.unwrapList(node.args); if (args.length === 2) { - const literalContext = { ...context, isStringLiteral: true }; + const literalContext = context.spawn('DistinctExpr', { isStringLiteral: true }); const left = this.visit(args[0], literalContext); const right = this.visit(args[1], literalContext); return `${left} IS DISTINCT FROM ${right}`; @@ -3324,7 +3331,7 @@ export class Deparser implements DeparserVisitor { NullIfExpr(node: t.NullIfExpr, context: DeparserContext): string { const args = ListUtils.unwrapList(node.args); if (args.length === 2) { - const literalContext = { ...context, isStringLiteral: true }; + const literalContext = context.spawn('NullIfExpr', { isStringLiteral: true }); const left = this.visit(args[0], literalContext); const right = this.visit(args[1], literalContext); return `NULLIF(${left}, ${right})`; @@ -3349,11 +3356,11 @@ export class Deparser implements DeparserVisitor { let result = funcName + '('; const hasDistinct = node.aggdistinct && node.aggdistinct.length > 0; - + if (node.args && node.args.length > 0) { const args = ListUtils.unwrapList(node.args); const argStrs = args.map(arg => this.visit(arg, context)); - + if (hasDistinct) { result += 'DISTINCT ' + argStrs.join(', '); } else { @@ -3406,7 +3413,7 @@ export class Deparser implements DeparserVisitor { FieldSelect(node: t.FieldSelect, context: DeparserContext): string { const output: string[] = []; - + if (node.arg) { output.push(this.visit(node.arg, context)); } @@ -3420,7 +3427,7 @@ export class Deparser implements DeparserVisitor { RelabelType(node: t.RelabelType, context: DeparserContext): string { if (node.arg) { - const literalContext = { ...context, isStringLiteral: true }; + const literalContext = context.spawn('RelabelType', { isStringLiteral: true }); return this.visit(node.arg, literalContext); } return ''; @@ -3442,7 +3449,7 @@ export class Deparser implements DeparserVisitor { ConvertRowtypeExpr(node: t.ConvertRowtypeExpr, context: DeparserContext): string { if (node.arg) { - const literalContext = { ...context, isStringLiteral: true }; + const literalContext = context.spawn('ConvertRowtypeExpr', { isStringLiteral: true }); return this.visit(node.arg, literalContext); } return ''; @@ -3450,56 +3457,56 @@ export class Deparser implements DeparserVisitor { NamedArgExpr(node: t.NamedArgExpr, context: DeparserContext): string { const output: string[] = []; - + if (node.name) { output.push(node.name); output.push('=>'); } - + if (node.arg) { output.push(this.visit(node.arg, context)); } - + return output.join(' '); } ViewStmt(node: t.ViewStmt, context: DeparserContext): string { const output: string[] = []; - + output.push('CREATE'); - + if (node.replace) { output.push('OR REPLACE'); } - + if (node.view && node.view.relpersistence === 't') { output.push('TEMPORARY'); } - + output.push('VIEW'); - + if (node.view) { output.push(this.RangeVar(node.view, context)); } - + if (node.aliases && node.aliases.length > 0) { const aliasStrs = ListUtils.unwrapList(node.aliases).map(alias => this.visit(alias, context)); - output.push(this.formatter.parens(aliasStrs.join(', '))); + output.push(context.parens(aliasStrs.join(', '))); } - + if (node.options && node.options.length > 0) { - const viewContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'ViewStmt'] }; + const viewContext = context.spawn('ViewStmt'); const optionStrs = ListUtils.unwrapList(node.options) .map(option => this.visit(option, viewContext)); output.push(`WITH (${optionStrs.join(', ')})`); } - + output.push('AS'); - + if (node.query) { output.push(this.visit(node.query, context)); } - + if (node.withCheckOption) { switch (node.withCheckOption) { case 'CASCADED_CHECK_OPTION': @@ -3510,98 +3517,98 @@ export class Deparser implements DeparserVisitor { break; } } - + return output.join(' '); } IndexStmt(node: t.IndexStmt, context: DeparserContext): string { const output: string[] = []; - + output.push('CREATE'); - + if (node.unique) { output.push('UNIQUE'); } - + output.push('INDEX'); - + if (node.concurrent) { output.push('CONCURRENTLY'); } - + if (node.if_not_exists) { output.push('IF NOT EXISTS'); } - + if (node.idxname) { output.push(QuoteUtils.quote(node.idxname)); } - + output.push('ON'); - + if (node.relation) { output.push(this.RangeVar(node.relation, context)); } - + if (node.accessMethod && node.accessMethod !== 'btree') { output.push('USING'); output.push(node.accessMethod); } - + if (node.indexParams && node.indexParams.length > 0) { const paramStrs = ListUtils.unwrapList(node.indexParams).map(param => this.visit(param, context)); - output.push(this.formatter.parens(paramStrs.join(', '))); + output.push(context.parens(paramStrs.join(', '))); } - + if (node.indexIncludingParams && node.indexIncludingParams.length > 0) { const includeStrs = ListUtils.unwrapList(node.indexIncludingParams).map(param => this.visit(param, context)); output.push('INCLUDE'); - output.push(this.formatter.parens(includeStrs.join(', '))); + output.push(context.parens(includeStrs.join(', '))); } - + if (node.whereClause) { output.push('WHERE'); output.push(this.visit(node.whereClause, context)); } - + if (node.options && node.options.length > 0) { - const indexContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'IndexStmt'] }; + const indexContext = context.spawn('IndexStmt'); const optionStrs = ListUtils.unwrapList(node.options).map(option => this.visit(option, indexContext)); output.push('WITH'); - output.push(this.formatter.parens(optionStrs.join(', '))); + output.push(context.parens(optionStrs.join(', '))); } - + if (node.nulls_not_distinct) { output.push('NULLS NOT DISTINCT'); } - + if (node.tableSpace) { output.push('TABLESPACE'); output.push(QuoteUtils.quote(node.tableSpace)); } - + return output.join(' '); } IndexElem(node: t.IndexElem, context: DeparserContext): string { const output: string[] = []; - + if (node.name) { output.push(QuoteUtils.quote(node.name)); } else if (node.expr) { - output.push(this.formatter.parens(this.visit(node.expr, context))); + output.push(context.parens(this.visit(node.expr, context))); } - + if (node.collation && node.collation.length > 0) { const collationStrs = ListUtils.unwrapList(node.collation).map(coll => this.visit(coll, context)); output.push('COLLATE'); output.push(collationStrs.join('.')); } - + if (node.opclass && node.opclass.length > 0) { const opclassStrs = ListUtils.unwrapList(node.opclass).map(op => this.visit(op, context)); let opclassStr = opclassStrs.join('.'); - + // Handle operator class parameters (opclassopts) if (node.opclassopts && node.opclassopts.length > 0) { const opclassOpts = ListUtils.unwrapList(node.opclassopts).map(opt => { @@ -3609,14 +3616,14 @@ export class Deparser implements DeparserVisitor { const stringData = this.getNodeData(opt.DefElem.arg); return `${opt.DefElem.defname}='${stringData.sval}'`; } - return this.visit(opt, context); + return this.visit(opt, context.spawn('IndexElem')); }); opclassStr += `(${opclassOpts.join(', ')})`; } - + output.push(opclassStr); } - + if (node.ordering) { switch (node.ordering) { case 'SORTBY_ASC': @@ -3627,7 +3634,7 @@ export class Deparser implements DeparserVisitor { break; } } - + if (node.nulls_ordering) { switch (node.nulls_ordering) { case 'SORTBY_NULLS_FIRST': @@ -3638,7 +3645,7 @@ export class Deparser implements DeparserVisitor { break; } } - + return output.join(' '); } @@ -3648,7 +3655,7 @@ export class Deparser implements DeparserVisitor { if (node.name) { output.push(QuoteUtils.quote(node.name)); } else if (node.expr) { - output.push(this.formatter.parens(this.visit(node.expr, context))); + output.push(context.parens(this.visit(node.expr, context))); } if (node.collation && node.collation.length > 0) { @@ -3667,15 +3674,15 @@ export class Deparser implements DeparserVisitor { PartitionCmd(node: t.PartitionCmd, context: DeparserContext): string { const output: string[] = []; - + if (node.concurrent) { output.push('CONCURRENTLY'); } - + if (node.name) { output.push(this.visit(node.name as any, context)); } - + if (node.bound) { if (node.bound.strategy === 'l' && node.bound.listdatums) { output.push('FOR VALUES IN'); @@ -3705,7 +3712,7 @@ export class Deparser implements DeparserVisitor { output.push('DEFAULT'); } } - + return output.join(' '); } @@ -3721,7 +3728,7 @@ export class Deparser implements DeparserVisitor { 2107: 'array_agg', 2108: 'string_agg' }; - + return commonAggFunctions[aggfnoid || 0] || 'unknown_agg'; } @@ -3738,7 +3745,7 @@ export class Deparser implements DeparserVisitor { 3108: 'first_value', 3109: 'last_value' }; - + return commonWindowFunctions[winfnoid || 0] || 'unknown_window_func'; } @@ -3766,22 +3773,22 @@ export class Deparser implements DeparserVisitor { 61: '>', // int4gt 62: '>=', // int4ge }; - + return commonOperators[opno || 0] || '='; } JoinExpr(node: t.JoinExpr, context: DeparserContext): string { const output: string[] = []; - + if (node.larg) { output.push(this.visit(node.larg, context)); } - + let joinStr = ''; if (node.isNatural) { joinStr = 'NATURAL '; } - + switch (node.jointype) { case 'JOIN_INNER': if (node.isNatural) { @@ -3805,44 +3812,44 @@ export class Deparser implements DeparserVisitor { default: joinStr += 'JOIN'; } - + if (node.rarg) { let rargStr = this.visit(node.rarg, context); - + if (node.rarg && 'JoinExpr' in node.rarg && !node.rarg.JoinExpr.alias) { rargStr = `(${rargStr})`; } - - if (this.formatter.isPretty()) { - output.push(this.formatter.newline() + joinStr + ' ' + rargStr); + + if (context.isPretty()) { + output.push(context.newline() + joinStr + ' ' + rargStr); } else { output.push(joinStr + ' ' + rargStr); } } else { - if (this.formatter.isPretty()) { - output.push(this.formatter.newline() + joinStr); + if (context.isPretty()) { + output.push(context.newline() + joinStr); } else { output.push(joinStr); } } - + if (node.usingClause && node.usingClause.length > 0) { const usingList = ListUtils.unwrapList(node.usingClause); const columnNames = usingList.map(col => this.visit(col, context)); - if (this.formatter.isPretty()) { + if (context.isPretty()) { output.push(` USING (${columnNames.join(', ')})`); } else { output.push(`USING (${columnNames.join(', ')})`); } } else if (node.quals) { const qualsStr = this.visit(node.quals, context); - if (this.formatter.isPretty()) { + if (context.isPretty()) { // For complex JOIN conditions, format with proper indentation if (qualsStr.includes('AND') || qualsStr.includes('OR') || qualsStr.length > 50) { if (this.containsMultilineStringLiteral(qualsStr)) { output.push(` ON ${qualsStr}`); } else { - output.push(` ON${this.formatter.newline()}${this.formatter.indent(qualsStr)}`); + output.push(` ON${context.newline()}${context.indent(qualsStr)}`); } } else { output.push(` ON ${qualsStr}`); @@ -3851,14 +3858,14 @@ export class Deparser implements DeparserVisitor { output.push(`ON ${qualsStr}`); } } - + let result; - if (this.formatter.isPretty()) { + if (context.isPretty()) { result = output.join(''); } else { result = output.join(' '); } - + if (node.join_using_alias && node.join_using_alias.aliasname) { let aliasStr = node.join_using_alias.aliasname; if (node.join_using_alias.colnames && node.join_using_alias.colnames.length > 0) { @@ -3868,7 +3875,7 @@ export class Deparser implements DeparserVisitor { } result += ` AS ${aliasStr}`; } - + if (node.alias && node.alias.aliasname) { let aliasStr = node.alias.aliasname; if (node.alias.colnames && node.alias.colnames.length > 0) { @@ -3878,26 +3885,26 @@ export class Deparser implements DeparserVisitor { } result = `(${result}) ${aliasStr}`; } - + return result; } FromExpr(node: t.FromExpr, context: DeparserContext): string { const fromlist = ListUtils.unwrapList(node.fromlist); const fromStrs = fromlist.map(item => this.visit(item, context)); - + let result = fromStrs.join(', '); - + if (node.quals) { result += ` WHERE ${this.visit(node.quals, context)}`; } - + return result; } TransactionStmt(node: t.TransactionStmt, context: DeparserContext): string { const output: string[] = []; - + switch (node.kind) { case 'TRANS_STMT_BEGIN': output.push('BEGIN'); @@ -3950,7 +3957,7 @@ export class Deparser implements DeparserVisitor { default: throw new Error(`Unsupported TransactionStmt kind: ${node.kind}`); } - + // Handle transaction options (e.g., READ ONLY, ISOLATION LEVEL) if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options).map(option => { @@ -3998,12 +4005,12 @@ export class Deparser implements DeparserVisitor { } return this.visit(option, context); }).filter(Boolean); - + if (options.length > 0) { output.push(options.join(', ')); } } - + return output.join(' '); } @@ -4022,7 +4029,7 @@ export class Deparser implements DeparserVisitor { } return this.visit(arg, context); }).join(', ') : ''; - + // Handle args - always include TO clause if args exist (even if empty string) const paramName = node.name && (node.name.includes('.') || node.name.includes('-') || /[A-Z]/.test(node.name)) ? `"${node.name}"` : node.name; if (!node.args || node.args.length === 0) { @@ -4147,7 +4154,7 @@ export class Deparser implements DeparserVisitor { if (node.rolename) { return this.quoteIfNeeded(node.rolename); } - + switch (node.roletype) { case 'ROLESPEC_PUBLIC': return 'PUBLIC'; @@ -4348,7 +4355,7 @@ export class Deparser implements DeparserVisitor { } return this.visit(item, context); }); - + if (items.length === 3) { const [schemaName, tableName, policyName] = items; output.push(`${policyName} ON ${schemaName}.${tableName}`); @@ -4366,14 +4373,14 @@ export class Deparser implements DeparserVisitor { const items = objList.List.items.map((item: any) => { return this.visit(item, context); }).filter((name: string) => name && name.trim()); - + if (items.length === 2) { const [sourceType, targetType] = items; return `(${sourceType} AS ${targetType})`; } return items.join('.'); } - + const objName = this.visit(objList, context); return objName; }).filter((name: string) => name && name.trim()).join(', '); @@ -4389,7 +4396,7 @@ export class Deparser implements DeparserVisitor { } return this.visit(item, context); }).filter((name: string) => name && name.trim()); - + if (items.length === 2) { const [tableName, triggerName] = items; return `${triggerName} ON ${tableName}`; @@ -4399,7 +4406,7 @@ export class Deparser implements DeparserVisitor { } return items.join('.'); } - + const objName = this.visit(objList, context); return objName; }).filter((name: string) => name && name.trim()).join(', '); @@ -4416,7 +4423,7 @@ export class Deparser implements DeparserVisitor { } return this.visit(item, context); }).filter((name: string) => name && name.trim()); - + if (items.length === 2) { const accessMethod = items[0]; const objectName = items[1]; @@ -4429,7 +4436,7 @@ export class Deparser implements DeparserVisitor { } return items.join('.'); } - + const objName = this.visit(objList, context); return objName; }).filter((name: string) => name && name.trim()).join(', '); @@ -4446,14 +4453,14 @@ export class Deparser implements DeparserVisitor { } return this.visit(item, context); }).filter((name: string) => name && name.trim()); - + if (items.length === 2) { const [typeName, languageName] = items; return `FOR ${typeName} LANGUAGE ${languageName}`; } return items.join('.'); } - + const objName = this.visit(objList, context); return objName; }).filter((name: string) => name && name.trim()).join(', '); @@ -4466,7 +4473,7 @@ export class Deparser implements DeparserVisitor { const objName = objList.map(obj => this.visit(obj, context)).filter(name => name && name.trim()).join('.'); return objName; } - + if (objList && objList.List && objList.List.items) { const items = objList.List.items.map((item: any) => { if (item.String && item.String.sval) { @@ -4476,8 +4483,8 @@ export class Deparser implements DeparserVisitor { }).filter((name: string) => name && name.trim()); return items.join('.'); } - - const objContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DropStmt'], objtype: node.removeType }; + + const objContext = context.spawn('DropStmt', { objtype: node.removeType }); const objName = this.visit(objList, objContext); return objName; }).filter((name: string) => name && name.trim()).join(', '); @@ -4597,7 +4604,7 @@ export class Deparser implements DeparserVisitor { if (node.options && node.options.length > 0) { output.push('WITH'); const optionsStr = ListUtils.unwrapList(node.options) - .map(opt => this.visit(opt, context)) + .map(opt => this.visit(opt, context.spawn('CopyStmt'))) .join(', '); output.push(`(${optionsStr})`); } @@ -4648,9 +4655,7 @@ export class Deparser implements DeparserVisitor { output.push('IF EXISTS'); } - const alterContext = node.objtype === 'OBJECT_TYPE' - ? { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterTypeStmt'] } - : context; + const alterContext = context.spawn('AlterTableStmt', { objtype: node.objtype }); if (node.relation) { const relationStr = this.RangeVar(node.relation, alterContext); @@ -4659,12 +4664,12 @@ export class Deparser implements DeparserVisitor { if (node.cmds && node.cmds.length > 0) { const commands = ListUtils.unwrapList(node.cmds); - if (this.formatter.isPretty()) { + if (context.isPretty()) { const commandsStr = commands .map(cmd => { const cmdStr = this.visit(cmd, alterContext); if (cmdStr.startsWith('ADD CONSTRAINT') || cmdStr.startsWith('ADD ')) { - return this.formatter.newline() + this.formatter.indent(cmdStr); + return context.newline() + context.indent(cmdStr); } return cmdStr; }) @@ -4687,7 +4692,7 @@ export class Deparser implements DeparserVisitor { if (node.subtype) { switch (node.subtype) { case 'AT_AddColumn': - if (context.parentNodeTypes.includes('AlterTypeStmt')) { + if (context.objtype === 'OBJECT_TYPE') { output.push('ADD ATTRIBUTE'); } else { output.push('ADD COLUMN'); @@ -4698,40 +4703,40 @@ export class Deparser implements DeparserVisitor { if (node.def) { const colDefData = this.getNodeData(node.def); const parts: string[] = []; - + if (colDefData.colname) { parts.push(QuoteUtils.quote(colDefData.colname)); } - + if (colDefData.typeName) { parts.push(this.TypeName(colDefData.typeName, context)); } - + if (colDefData.fdwoptions && colDefData.fdwoptions.length > 0) { parts.push('OPTIONS'); - const columnContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'ColumnDef'] }; + const columnContext = context.spawn('ColumnDef'); const options = ListUtils.unwrapList(colDefData.fdwoptions).map(opt => this.visit(opt, columnContext)); parts.push(`(${options.join(', ')})`); } - + if (colDefData.constraints) { const constraints = ListUtils.unwrapList(colDefData.constraints); const constraintStrs = constraints.map(constraint => { - const columnConstraintContext = { ...context, isColumnConstraint: true }; + const columnConstraintContext = context.spawn('ColumnDef', { isColumnConstraint: true }); return this.visit(constraint, columnConstraintContext); }); parts.push(...constraintStrs); } - + if (colDefData.raw_default) { parts.push('DEFAULT'); parts.push(this.visit(colDefData.raw_default, context)); } - + if (colDefData.is_not_null) { parts.push('NOT NULL'); } - + output.push(parts.join(' ')); } if (node.behavior === 'DROP_CASCADE') { @@ -4740,13 +4745,13 @@ export class Deparser implements DeparserVisitor { break; case 'AT_DropColumn': if (node.missing_ok) { - if (context.parentNodeTypes.includes('AlterTypeStmt')) { + if (context.objtype === 'OBJECT_TYPE') { output.push('DROP ATTRIBUTE IF EXISTS'); } else { output.push('DROP COLUMN IF EXISTS'); } } else { - if (context.parentNodeTypes.includes('AlterTypeStmt')) { + if (context.objtype === 'OBJECT_TYPE') { output.push('DROP ATTRIBUTE'); } else { output.push('DROP COLUMN'); @@ -4762,7 +4767,7 @@ export class Deparser implements DeparserVisitor { } break; case 'AT_AlterColumnType': - if (context.parentNodeTypes.includes('AlterTypeStmt')) { + if (context.objtype === 'OBJECT_TYPE') { output.push('ALTER ATTRIBUTE'); } else { output.push('ALTER COLUMN'); @@ -4822,7 +4827,7 @@ export class Deparser implements DeparserVisitor { case 'AT_SetRelOptions': output.push('SET'); if (node.def) { - const alterTableContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterTableCmd'], subtype: 'AT_SetRelOptions' }; + const alterTableContext = context.spawn('AlterTableCmd', { subtype: 'AT_SetRelOptions' }); const options = ListUtils.unwrapList(node.def) .map(option => this.visit(option, alterTableContext)) .join(', '); @@ -4834,7 +4839,7 @@ export class Deparser implements DeparserVisitor { case 'AT_ResetRelOptions': output.push('RESET'); if (node.def) { - const alterTableContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterTableCmd'], subtype: 'AT_ResetRelOptions' }; + const alterTableContext = context.spawn('AlterTableCmd', { subtype: 'AT_ResetRelOptions' }); const options = ListUtils.unwrapList(node.def) .map(option => this.visit(option, alterTableContext)) .join(', '); @@ -4926,7 +4931,7 @@ export class Deparser implements DeparserVisitor { } output.push('SET'); if (node.def) { - const alterTableContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterTableCmd'], subtype: 'AT_SetOptions' }; + const alterTableContext = context.spawn('AlterTableCmd', { subtype: 'AT_SetOptions' }); const options = ListUtils.unwrapList(node.def) .map(option => this.visit(option, alterTableContext)) .join(', '); @@ -4942,7 +4947,7 @@ export class Deparser implements DeparserVisitor { } output.push('RESET'); if (node.def) { - const alterTableContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterTableCmd'], subtype: 'AT_ResetOptions' }; + const alterTableContext = context.spawn('AlterTableCmd', { subtype: 'AT_ResetOptions' }); const options = ListUtils.unwrapList(node.def) .map(option => this.visit(option, alterTableContext)) .join(', '); @@ -5179,7 +5184,7 @@ export class Deparser implements DeparserVisitor { } output.push('OPTIONS'); if (node.def) { - const alterColumnContext = { ...context, alterColumnOptions: true }; + const alterColumnContext = context.spawn('AlterTableCmd', { alterColumnOptions: true }); const options = ListUtils.unwrapList(node.def) .map(option => this.visit(option, alterColumnContext)) .join(', '); @@ -5218,7 +5223,7 @@ export class Deparser implements DeparserVisitor { case 'AT_GenericOptions': output.push('OPTIONS'); if (node.def) { - const alterTableContext = { ...context, alterTableOptions: true }; + const alterTableContext = context.spawn('AlterTableCmd', { alterTableOptions: true }); const options = ListUtils.unwrapList(node.def) .map(option => this.visit(option, alterTableContext)) .join(', '); @@ -5274,20 +5279,20 @@ export class Deparser implements DeparserVisitor { CreateFunctionStmt(node: t.CreateFunctionStmt, context: DeparserContext): string { const output: string[] = ['CREATE']; - + if (node.replace) { output.push('OR REPLACE'); } - + if (node.is_procedure) { output.push('PROCEDURE'); } else { output.push('FUNCTION'); } - + if (node.funcname && node.funcname.length > 0) { const funcName = node.funcname.map((name: any) => this.visit(name, context)).join('.'); - + if (node.parameters && node.parameters.length > 0) { const params = node.parameters .filter((param: any) => { @@ -5295,7 +5300,7 @@ export class Deparser implements DeparserVisitor { return paramData.mode !== 'FUNC_PARAM_TABLE'; }) .map((param: any) => this.visit(param, context)); - + if (params.length > 0) { output.push(funcName + '(' + params.join(', ') + ')'); } else { @@ -5305,12 +5310,12 @@ export class Deparser implements DeparserVisitor { output.push(funcName + '()'); } } - + const hasTableParams = node.parameters && node.parameters.some((param: any) => { const paramData = this.getNodeData(param); return paramData.mode === 'FUNC_PARAM_TABLE'; }); - + if (hasTableParams) { output.push('RETURNS TABLE ('); const tableParams = node.parameters @@ -5325,20 +5330,20 @@ export class Deparser implements DeparserVisitor { output.push('RETURNS'); output.push(this.TypeName(node.returnType as any, context)); } - + if (node.options && node.options.length > 0) { - const funcContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateFunctionStmt'] }; + const funcContext = context.spawn('CreateFunctionStmt'); const options = node.options.map((opt: any) => this.visit(opt, funcContext)); output.push(...options); } - + if (node.sql_body) { const bodyType = this.getNodeType(node.sql_body); if (bodyType === 'ReturnStmt') { output.push(this.visit(node.sql_body, context)); } else { output.push('BEGIN ATOMIC'); - + // Handle List of statements in sql_body if (bodyType === 'List') { const statements = ListUtils.unwrapList(node.sql_body); @@ -5349,7 +5354,7 @@ export class Deparser implements DeparserVisitor { if (statements.length === 1 && statements[0].List) { actualStatements = ListUtils.unwrapList(statements[0]); } - + const stmtStrings = actualStatements .filter(stmt => stmt && Object.keys(stmt).length > 0) // Filter out empty objects .map(stmt => { @@ -5368,17 +5373,17 @@ export class Deparser implements DeparserVisitor { output.push(bodyStmt); } } - + output.push('END'); } } - + return output.join(' '); } FunctionParameter(node: t.FunctionParameter, context: DeparserContext): string { const output: string[] = []; - + if (node.mode) { switch (node.mode) { case 'FUNC_PARAM_IN': @@ -5395,37 +5400,37 @@ export class Deparser implements DeparserVisitor { break; } } - + if (node.name) { output.push(QuoteUtils.quote(node.name)); } - + if (node.argType) { output.push(this.TypeName(node.argType as any, context)); } - + if (node.defexpr) { output.push('DEFAULT'); output.push(this.visit(node.defexpr, context)); } - + return output.join(' '); } CreateEnumStmt(node: t.CreateEnumStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'TYPE']; - + if (node.typeName) { const typeName = ListUtils.unwrapList(node.typeName) .map(name => this.visit(name, context)) .join('.'); output.push(typeName); } - + output.push('AS', 'ENUM'); - + if (node.vals && node.vals.length > 0) { - const enumContext = { ...context, isEnumValue: true }; + const enumContext = context.spawn('CreateEnumStmt', { isEnumValue: true }); const values = ListUtils.unwrapList(node.vals) .map(val => this.visit(val, enumContext)) .join(', '); @@ -5433,29 +5438,29 @@ export class Deparser implements DeparserVisitor { } else { output.push('()'); } - + return output.join(' '); } CreateDomainStmt(node: t.CreateDomainStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'DOMAIN']; - + if (node.domainname) { const domainName = ListUtils.unwrapList(node.domainname) .map(name => this.visit(name, context)) .join('.'); output.push(domainName); } - + if (node.typeName) { output.push('AS'); output.push(this.TypeName(node.typeName, context)); } - + if (node.collClause) { output.push(this.CollateClause(node.collClause, context)); } - + if (node.constraints) { const constraints = ListUtils.unwrapList(node.constraints) .map(constraint => this.visit(constraint, context)) @@ -5464,13 +5469,13 @@ export class Deparser implements DeparserVisitor { output.push(constraints); } } - + return output.join(' '); } CreateRoleStmt(node: t.CreateRoleStmt, context: DeparserContext): string { const output: string[] = ['CREATE']; - + if (node.stmt_type === 'ROLESTMT_ROLE') { output.push('ROLE'); } else if (node.stmt_type === 'ROLESTMT_USER') { @@ -5480,23 +5485,22 @@ export class Deparser implements DeparserVisitor { } else { output.push('ROLE'); } - + if (node.role) { const roleName = Deparser.needsQuotes(node.role) ? `"${node.role}"` : node.role; output.push(roleName); } - + if (node.options) { - const roleContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateRoleStmt'] }; const options = ListUtils.unwrapList(node.options) - .map(option => this.visit(option, roleContext)) + .map(option => this.visit(option, context.spawn('CreateRoleStmt'))) .join(' '); if (options) { output.push('WITH'); output.push(options); } } - + return output.join(' '); } @@ -5504,9 +5508,9 @@ export class Deparser implements DeparserVisitor { if (!node.defname) { return ''; } - + // Handle CREATE OPERATOR commutator/negator - MUST be first to prevent quoting - if (context.parentNodeTypes.includes('DefineStmt') && + if (context.parentNodeTypes.includes('DefineStmt') && ['commutator', 'negator'].includes(node.defname.toLowerCase())) { if (node.arg && this.getNodeType(node.arg) === 'List') { const listData = this.getNodeData(node.arg); @@ -5517,49 +5521,49 @@ export class Deparser implements DeparserVisitor { } } } - + // Handle IndexElem opclassopts - MUST be first to preserve string types if (context.parentNodeTypes.includes('IndexElem')) { if (node.arg && this.getNodeType(node.arg) === 'String') { const stringData = this.getNodeData(node.arg); return `${node.defname}='${stringData.sval}'`; } - return `${node.defname}=${this.visit(node.arg, { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DefElem'] })}`; + return `${node.defname}=${this.visit(node.arg, context.spawn('DefElem'))}`; } - + // Handle CREATE OPERATOR boolean flags - MUST be first to preserve case - if (context.parentNodeTypes.includes('DefineStmt') && + if (context.parentNodeTypes.includes('DefineStmt') && ['hashes', 'merges'].includes(node.defname.toLowerCase()) && !node.arg) { if (node.defname !== node.defname.toLowerCase() && node.defname !== node.defname.toUpperCase()) { return `"${node.defname}"`; } return node.defname.charAt(0).toUpperCase() + node.defname.slice(1).toLowerCase(); } - + // Handle FDW-related statements and ALTER OPTIONS that use space format for options if (context.parentNodeTypes.includes('AlterFdwStmt') || context.parentNodeTypes.includes('CreateFdwStmt') || context.parentNodeTypes.includes('CreateForeignServerStmt') || context.parentNodeTypes.includes('AlterForeignServerStmt') || context.parentNodeTypes.includes('CreateUserMappingStmt') || context.parentNodeTypes.includes('AlterUserMappingStmt') || context.parentNodeTypes.includes('ColumnDef') || context.parentNodeTypes.includes('CreateForeignTableStmt') || context.parentNodeTypes.includes('ImportForeignSchemaStmt') || context.alterColumnOptions || context.alterTableOptions) { if (['handler', 'validator'].includes(node.defname)) { if (!node.arg) { return `NO ${node.defname.toUpperCase()}`; } - const defElemContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DefElem'] }; + const defElemContext = context.spawn('DefElem'); const argValue = this.visit(node.arg, defElemContext); return `${node.defname.toUpperCase()} ${argValue}`; } // Handle OPTIONS clause - use space format, not equals format if (node.arg) { - const defElemContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DefElem'] }; + const defElemContext = context.spawn('DefElem'); const argValue = this.visit(node.arg, defElemContext); - + if (context.parentNodeTypes.includes('CreateFdwStmt') || context.parentNodeTypes.includes('AlterFdwStmt')) { - const finalValue = typeof argValue === 'string' && !argValue.startsWith("'") - ? `'${argValue}'` + const finalValue = typeof argValue === 'string' && !argValue.startsWith("'") + ? `'${argValue}'` : argValue; - + const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') || Deparser.needsQuotes(node.defname) - ? `"${node.defname}"` + ? `"${node.defname}"` : node.defname; - + if (node.defaction === 'DEFELEM_ADD') { return `ADD ${quotedDefname} ${finalValue}`; } else if (node.defaction === 'DEFELEM_DROP') { @@ -5567,14 +5571,14 @@ export class Deparser implements DeparserVisitor { } else if (node.defaction === 'DEFELEM_SET') { return `SET ${quotedDefname} ${finalValue}`; } - + return `${quotedDefname} ${finalValue}`; } - - const quotedValue = typeof argValue === 'string' && !argValue.startsWith("'") - ? `'${argValue}'` + + const quotedValue = typeof argValue === 'string' && !argValue.startsWith("'") + ? `'${argValue}'` : argValue; - + if (node.defaction === 'DEFELEM_ADD') { return `ADD ${node.defname} ${quotedValue}`; } else if (node.defaction === 'DEFELEM_DROP') { @@ -5582,9 +5586,9 @@ export class Deparser implements DeparserVisitor { } else if (node.defaction === 'DEFELEM_SET') { return `SET ${node.defname} ${quotedValue}`; } - - const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') - ? `"${node.defname}"` + + const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') + ? `"${node.defname}"` : node.defname; return `${quotedDefname} ${quotedValue}`; } else if (node.defaction === 'DEFELEM_DROP') { @@ -5592,13 +5596,13 @@ export class Deparser implements DeparserVisitor { return `DROP ${node.defname}`; } } - + // Handle sequence options that can have NO prefix when no argument (before checking node.arg) - if ((context.parentNodeTypes.includes('CreateSeqStmt') || context.parentNodeTypes.includes('AlterSeqStmt')) && + if ((context.parentNodeTypes.includes('CreateSeqStmt') || context.parentNodeTypes.includes('AlterSeqStmt')) && (node.defname === 'minvalue' || node.defname === 'maxvalue') && !node.arg) { return `NO ${node.defname.toUpperCase()}`; } - + // Handle CREATE ROLE / ALTER ROLE password options BEFORE checking node.arg if (context.parentNodeTypes.includes('CreateRoleStmt') || context.parentNodeTypes.includes('AlterRoleStmt')) { if (node.defname === 'password') { @@ -5606,19 +5610,19 @@ export class Deparser implements DeparserVisitor { if (!node.arg) { return 'PASSWORD NULL'; } - const defElemContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DefElem'] }; + const defElemContext = context.spawn('DefElem'); const argValue = this.visit(node.arg, defElemContext); - const quotedValue = typeof argValue === 'string' && !argValue.startsWith("'") - ? `'${argValue}'` + const quotedValue = typeof argValue === 'string' && !argValue.startsWith("'") + ? `'${argValue}'` : argValue; return `PASSWORD ${quotedValue}`; } } - + if (node.arg) { - const defElemContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DefElem'] }; + const defElemContext = context.spawn('DefElem'); const argValue = this.visit(node.arg, defElemContext); - + if (context.parentNodeTypes.includes('AlterOperatorStmt')) { if (node.arg && this.getNodeType(node.arg) === 'TypeName') { const typeNameData = this.getNodeData(node.arg); @@ -5629,7 +5633,7 @@ export class Deparser implements DeparserVisitor { } } } - + if (node.arg && this.getNodeType(node.arg) === 'List') { const listData = this.getNodeData(node.arg); const listItems = ListUtils.unwrapList(listData.items); @@ -5638,27 +5642,27 @@ export class Deparser implements DeparserVisitor { } } } - + if (context.parentNodeTypes.includes('CreatedbStmt') || context.parentNodeTypes.includes('DropdbStmt')) { - const quotedValue = typeof argValue === 'string' - ? QuoteUtils.escape(argValue) + const quotedValue = typeof argValue === 'string' + ? QuoteUtils.escape(argValue) : argValue; return `${node.defname} = ${quotedValue}`; } - + // CreateForeignServerStmt and AlterForeignServerStmt use space format like CreateFdwStmt if (context.parentNodeTypes.includes('CreateForeignServerStmt') || context.parentNodeTypes.includes('AlterForeignServerStmt')) { - const quotedValue = typeof argValue === 'string' - ? QuoteUtils.escape(argValue) + const quotedValue = typeof argValue === 'string' + ? QuoteUtils.escape(argValue) : argValue; - const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') - ? `"${node.defname}"` + const quotedDefname = node.defname.includes(' ') || node.defname.includes('-') + ? `"${node.defname}"` : node.defname; return `${quotedDefname} ${quotedValue}`; } - - + + if (context.parentNodeTypes.includes('CreateRoleStmt') || context.parentNodeTypes.includes('AlterRoleStmt')) { if (node.defname === 'rolemembers') { // Handle List of RoleSpec nodes for GROUP statements @@ -5666,7 +5670,7 @@ export class Deparser implements DeparserVisitor { const listData = this.getNodeData(node.arg); const listItems = ListUtils.unwrapList(listData.items); const roleNames = listItems.map(item => this.visit(item, context)); - + if (context.parentNodeTypes.includes('CreateRoleStmt')) { return `ROLE ${roleNames.join(', ')}`; } else { @@ -5684,27 +5688,27 @@ export class Deparser implements DeparserVisitor { return `IN ROLE ${roleNames.join(', ')}`; } } - + if (node.defname === 'validUntil') { - const quotedValue = typeof argValue === 'string' && !argValue.startsWith("'") - ? `'${argValue}'` + const quotedValue = typeof argValue === 'string' && !argValue.startsWith("'") + ? `'${argValue}'` : argValue; return `VALID UNTIL ${quotedValue}`; } - + if (node.defname === 'adminmembers') { return `ADMIN ${argValue}`; } - + if (node.defname === 'connectionlimit') { return `CONNECTION LIMIT ${argValue}`; } - + if (node.defname === 'sysid') { return `SYSID ${argValue}`; } - - if (argValue === 'true') { + + if (String(argValue) === 'true') { // Handle special cases where the positive form has a different name if (node.defname === 'isreplication') { return 'REPLICATION'; @@ -5713,7 +5717,7 @@ export class Deparser implements DeparserVisitor { return 'LOGIN'; } return node.defname.toUpperCase(); - } else if (argValue === 'false') { + } else if (String(argValue) === 'false') { // Handle special cases where the negative form has a different name if (node.defname === 'canlogin') { return 'NOLOGIN'; @@ -5724,7 +5728,7 @@ export class Deparser implements DeparserVisitor { return `NO${node.defname.toUpperCase()}`; } } - + if (context.parentNodeTypes.includes('CreateSeqStmt') || context.parentNodeTypes.includes('AlterSeqStmt')) { if (node.defname === 'owned_by') { // Handle List node for table.column reference @@ -5748,7 +5752,7 @@ export class Deparser implements DeparserVisitor { return `OWNED BY ${argValue}`; } } - + // Handle boolean sequence options if (node.defname === 'cycle') { const boolValue = String(argValue).toLowerCase(); @@ -5758,19 +5762,19 @@ export class Deparser implements DeparserVisitor { return 'NO CYCLE'; } } - + // Handle sequence options that can have NO prefix when no argument if ((node.defname === 'minvalue' || node.defname === 'maxvalue') && !node.arg) { return `NO ${node.defname.toUpperCase()}`; } - + return `${node.defname.toUpperCase()} ${argValue}`; } - + if (context.parentNodeTypes.includes('CreateTableSpaceStmt') || context.parentNodeTypes.includes('AlterTableSpaceOptionsStmt')) { return `${node.defname.toUpperCase()} ${argValue}`; } - + if (context.parentNodeTypes.includes('ExplainStmt')) { if (argValue) { return `${node.defname.toUpperCase()} ${argValue.toUpperCase()}`; @@ -5778,12 +5782,12 @@ export class Deparser implements DeparserVisitor { return node.defname.toUpperCase(); } } - + if (context.parentNodeTypes.includes('DoStmt')) { if (node.defname === 'as') { - const defElemContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DefElem'] }; + const defElemContext = context.spawn('DefElem'); const argValue = node.arg ? this.visit(node.arg, defElemContext) : ''; - + if (Array.isArray(argValue)) { const bodyParts = argValue; const body = bodyParts.join(''); @@ -5796,7 +5800,7 @@ export class Deparser implements DeparserVisitor { } return ''; } - + if (context.parentNodeTypes.includes('CreateFunctionStmt') || context.parentNodeTypes.includes('AlterFunctionStmt')) { if (node.defname === 'as') { // Handle List type (multiple function body strings) @@ -5809,7 +5813,7 @@ export class Deparser implements DeparserVisitor { } return this.visit(item, context); }); - + if (bodyParts.length === 1) { const body = bodyParts[0]; const delimiter = this.getFunctionDelimiter(body); @@ -5834,7 +5838,7 @@ export class Deparser implements DeparserVisitor { return `${delimiter}${part}${delimiter}`; }).join(', ')}`; } - } + } // Handle String type (single function body) else { const delimiter = this.getFunctionDelimiter(argValue); @@ -5870,7 +5874,7 @@ export class Deparser implements DeparserVisitor { } return `${node.defname.toUpperCase()} ${argValue}`; } - + if (context.parentNodeTypes.includes('CreateExtensionStmt') || context.parentNodeTypes.includes('AlterExtensionStmt') || context.parentNodeTypes.includes('CreateFdwStmt') || context.parentNodeTypes.includes('AlterFdwStmt')) { // AlterExtensionStmt specific cases if (context.parentNodeTypes.includes('AlterExtensionStmt')) { @@ -5881,34 +5885,34 @@ export class Deparser implements DeparserVisitor { return `SCHEMA ${argValue}`; } } - + // CreateFdwStmt specific cases if (context.parentNodeTypes.includes('CreateFdwStmt')) { if (['handler', 'validator'].includes(node.defname)) { return `${node.defname.toUpperCase()} ${argValue}`; } - const quotedValue = typeof argValue === 'string' - ? QuoteUtils.escape(argValue) + const quotedValue = typeof argValue === 'string' + ? QuoteUtils.escape(argValue) : argValue; return `${node.defname} ${quotedValue}`; } - - + + // CreateExtensionStmt cases (schema, version, etc.) if (node.defname === 'cascade') { return argValue === 'true' ? 'CASCADE' : ''; } return `${node.defname.toUpperCase()} ${argValue}`; } - + // Handle IndexStmt WITH clause options - no quotes, compact formatting if (context.parentNodeTypes.includes('IndexStmt')) { return `${node.defname}=${argValue}`; } - + // Handle IndexElem opclassopts - preserve string values as strings if (context.parentNodeTypes.includes('IndexElem')) { if (node.arg && this.getNodeType(node.arg) === 'String') { @@ -5917,7 +5921,7 @@ export class Deparser implements DeparserVisitor { } return `${node.defname}=${argValue}`; } - + // Handle CreateStmt table options - no quotes, compact formatting if (context.parentNodeTypes.includes('CreateStmt')) { // For numeric values, use the raw value without quotes @@ -5927,7 +5931,7 @@ export class Deparser implements DeparserVisitor { } return `${node.defname}=${argValue}`; } - + // Handle CreateEventTrigStmt WHEN clause - use IN syntax for List arguments if (context.parentNodeTypes.includes('CreateEventTrigStmt')) { if (node.arg && this.getNodeType(node.arg) === 'List') { @@ -5944,9 +5948,9 @@ export class Deparser implements DeparserVisitor { } return `${node.defname} = ${argValue}`; } - + // Handle AT_SetRelOptions context - don't quote values that should be type names - if ((context.parentNodeTypes.includes('AlterTableCmd') || context.parentNodeTypes.includes('AlterTableStmt')) && + if ((context.parentNodeTypes.includes('AlterTableCmd') || context.parentNodeTypes.includes('AlterTableStmt')) && !context.parentNodeTypes.includes('ColumnDef')) { const optionName = node.defnamespace ? `${node.defnamespace}.${node.defname}` : node.defname; if (node.arg && this.getNodeType(node.arg) === 'TypeName') { @@ -5954,18 +5958,18 @@ export class Deparser implements DeparserVisitor { } return `${optionName} = ${argValue}`; } - + // Handle ViewStmt WITH options - don't quote numeric values if (context.parentNodeTypes.includes('ViewStmt')) { if (typeof argValue === 'string' && /^\d+$/.test(argValue)) { return `${node.defname}=${argValue}`; } - const quotedValue = typeof argValue === 'string' - ? QuoteUtils.escape(argValue) + const quotedValue = typeof argValue === 'string' + ? QuoteUtils.escape(argValue) : argValue; return `${node.defname} = ${quotedValue}`; } - + // Handle CopyStmt WITH clause options - uppercase format without quotes if (context.parentNodeTypes.includes('CopyStmt')) { if (node.defname === 'format' && node.arg && this.getNodeType(node.arg) === 'String') { @@ -5982,7 +5986,7 @@ export class Deparser implements DeparserVisitor { // Handle CREATE OPERATOR and CREATE TYPE context if (context.parentNodeTypes.includes('DefineStmt')) { const preservedName = this.preserveOperatorDefElemCase(node.defname); - + // Handle operator arguments that should be Lists vs TypeNames if (['commutator', 'negator'].includes(node.defname.toLowerCase())) { if (node.arg) { @@ -5999,7 +6003,7 @@ export class Deparser implements DeparserVisitor { } return preservedName; } - + // Handle boolean flags (no arguments) - preserve quoted case if (['hashes', 'merges'].includes(node.defname.toLowerCase())) { if (node.defname !== node.defname.toLowerCase() && node.defname !== node.defname.toUpperCase()) { @@ -6007,7 +6011,7 @@ export class Deparser implements DeparserVisitor { } return preservedName.toUpperCase(); } - + // Handle CREATE AGGREGATE quoted identifiers - preserve quotes when needed if (Deparser.needsQuotes(node.defname)) { const quotedDefname = `"${node.defname}"`; @@ -6025,7 +6029,7 @@ export class Deparser implements DeparserVisitor { } return quotedDefname; } - + // Handle other operator parameters with preserved case if (preservedName !== node.defname) { if (node.arg) { @@ -6033,7 +6037,7 @@ export class Deparser implements DeparserVisitor { } return preservedName; } - + // CREATE TYPE context - preserve string literals with single quotes, handle boolean strings if (node.arg && this.getNodeType(node.arg) === 'String') { const stringData = this.getNodeData(node.arg); @@ -6063,13 +6067,13 @@ export class Deparser implements DeparserVisitor { return `${node.defname} = ${argValue}`; } } - - const quotedValue = typeof argValue === 'string' - ? QuoteUtils.escape(argValue) + + const quotedValue = typeof argValue === 'string' + ? QuoteUtils.escape(argValue) : argValue; return `${node.defname} = ${quotedValue}`; } - + // Handle CREATE TYPE boolean flags - preserve quoted case for attributes like "Passedbyvalue" if (context.parentNodeTypes.includes('DefineStmt') && !node.arg) { // Check if the original defname appears to be quoted (mixed case that's not all upper/lower) @@ -6077,151 +6081,151 @@ export class Deparser implements DeparserVisitor { return `"${node.defname}"`; } } - + return node.defname.toUpperCase(); } CreateTableSpaceStmt(node: t.CreateTableSpaceStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'TABLESPACE']; - + if (node.tablespacename) { output.push(node.tablespacename); } - + if (node.owner) { output.push('OWNER'); output.push(this.RoleSpec(node.owner, context)); } - + if (node.location) { output.push('LOCATION'); output.push(`'${node.location}'`); } - + if (node.options && node.options.length > 0) { output.push('WITH'); - const tsContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateTableSpaceStmt'] }; + const tsContext = context.spawn('CreateTableSpaceStmt'); const options = ListUtils.unwrapList(node.options) .map(option => this.visit(option, tsContext)) .join(', '); output.push(`(${options})`); } - + return output.join(' '); } DropTableSpaceStmt(node: t.DropTableSpaceStmt, context: DeparserContext): string { const output: string[] = ['DROP', 'TABLESPACE']; - + if (node.missing_ok) { output.push('IF', 'EXISTS'); } - + if (node.tablespacename) { output.push(node.tablespacename); } - + return output.join(' '); } AlterTableSpaceOptionsStmt(node: t.AlterTableSpaceOptionsStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'TABLESPACE']; - + if (node.tablespacename) { output.push(node.tablespacename); } - + if (node.isReset) { output.push('RESET'); } else { output.push('SET'); } - + if (node.options && node.options.length > 0) { - const tablespaceContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterTableSpaceOptionsStmt'] }; + const tablespaceContext = context.spawn('AlterTableSpaceOptionsStmt'); const options = ListUtils.unwrapList(node.options) .map(option => this.visit(option, tablespaceContext)) .join(', '); output.push(`(${options})`); } - + return output.join(' '); } CreateExtensionStmt(node: t.CreateExtensionStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'EXTENSION']; - + if (node.if_not_exists) { output.push('IF', 'NOT', 'EXISTS'); } - + if (node.extname) { output.push(this.quoteIfNeeded(node.extname)); } - + if (node.options && node.options.length > 0) { - const extContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateExtensionStmt'] }; + const extContext = context.spawn('CreateExtensionStmt'); const options = ListUtils.unwrapList(node.options) .map(option => this.visit(option, extContext)) .join(' '); output.push(options); } - + return output.join(' '); } AlterExtensionStmt(node: t.AlterExtensionStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'EXTENSION']; - + if (node.extname) { output.push(this.quoteIfNeeded(node.extname)); } - + if (node.options && node.options.length > 0) { - const extContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterExtensionStmt'] }; + const extContext = context.spawn('AlterExtensionStmt'); const options = ListUtils.unwrapList(node.options) .map(option => this.visit(option, extContext)) .join(' '); output.push(options); } - + return output.join(' '); } CreateFdwStmt(node: t.CreateFdwStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'FOREIGN', 'DATA', 'WRAPPER']; - + if (node.fdwname) { output.push(node.fdwname); } - + if (node.func_options && node.func_options.length > 0) { - const fdwContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateFdwStmt'] }; + const fdwContext = context.spawn('CreateFdwStmt'); const funcOptions = ListUtils.unwrapList(node.func_options) .map(option => this.visit(option, fdwContext)) .join(' '); output.push(funcOptions); } - + if (node.options && node.options.length > 0) { output.push('OPTIONS'); - const fdwContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateFdwStmt'] }; + const fdwContext = context.spawn('CreateFdwStmt'); const options = ListUtils.unwrapList(node.options) .map(option => this.visit(option, fdwContext)) .join(', '); output.push(`(${options})`); } - + return output.join(' '); } SetOperationStmt(node: t.SetOperationStmt, context: DeparserContext): string { const output: string[] = []; - + if (node.larg) { output.push(this.visit(node.larg, context)); } - + if (node.op) { switch (node.op) { case 'SETOP_UNION': @@ -6237,17 +6241,17 @@ export class Deparser implements DeparserVisitor { throw new Error(`Unsupported SetOperation: ${node.op}`); } } - + if (node.rarg) { output.push(this.visit(node.rarg, context)); } - + return output.join(' '); } ReplicaIdentityStmt(node: t.ReplicaIdentityStmt, context: DeparserContext): string { const output: string[] = []; - + if (node.identity_type) { switch (node.identity_type) { case 'd': @@ -6273,35 +6277,35 @@ export class Deparser implements DeparserVisitor { throw new Error(`Unsupported replica identity type: ${node.identity_type}`); } } - + return output.join(' '); } AlterCollationStmt(node: t.AlterCollationStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'COLLATION']; - + if (node.collname && node.collname.length > 0) { const collationName = ListUtils.unwrapList(node.collname) .map(name => this.visit(name, context)) .join('.'); output.push(collationName); } - + output.push('REFRESH', 'VERSION'); - + return output.join(' '); } AlterDomainStmt(node: t.AlterDomainStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'DOMAIN']; - + if (node.typeName && node.typeName.length > 0) { const domainName = ListUtils.unwrapList(node.typeName) .map(name => this.visit(name, context)) .join('.'); output.push(domainName); } - + if (node.subtype) { switch (node.subtype) { case 'AT_SetNotNull': @@ -6323,7 +6327,7 @@ export class Deparser implements DeparserVisitor { output.push('ADD'); if (node.def) { // Pass domain context to avoid adding constraint names for domain constraints - const domainContext = { ...context, isDomainConstraint: true }; + const domainContext = context.spawn('CreateDomainStmt', { isDomainConstraint: true }); output.push(this.visit(node.def, domainContext)); } break; @@ -6349,7 +6353,7 @@ export class Deparser implements DeparserVisitor { output.push('ADD'); if (node.def) { // Pass domain context to avoid adding constraint names for domain constraints - const domainContext = { ...context, isDomainConstraint: true }; + const domainContext = context.spawn('CreateDomainStmt', { isDomainConstraint: true }); output.push(this.visit(node.def, domainContext)); } break; @@ -6389,96 +6393,96 @@ export class Deparser implements DeparserVisitor { throw new Error(`Unsupported AlterDomainStmt subtype: ${node.subtype}`); } } - + return output.join(' '); } PrepareStmt(node: t.PrepareStmt, context: DeparserContext): string { const output: string[] = ['PREPARE']; - + if (node.name) { output.push(node.name); } - + if (node.argtypes && node.argtypes.length > 0) { const argTypes = ListUtils.unwrapList(node.argtypes) .map(argType => this.visit(argType, context)) .join(', '); output.push(`(${argTypes})`); } - + output.push('AS'); - + if (node.query) { output.push(this.visit(node.query, context)); } - + return output.join(' '); } ExecuteStmt(node: t.ExecuteStmt, context: DeparserContext): string { const output: string[] = ['EXECUTE']; - + if (node.name) { output.push(node.name); } - + if (node.params && node.params.length > 0) { const params = ListUtils.unwrapList(node.params) .map(param => this.visit(param, context)) .join(', '); output.push(`(${params})`); } - + return output.join(' '); } DeallocateStmt(node: t.DeallocateStmt, context: DeparserContext): string { const output: string[] = ['DEALLOCATE']; - + if (node.isall) { output.push('ALL'); } else if (node.name) { output.push(node.name); } - + return output.join(' '); } NotifyStmt(node: t.NotifyStmt, context: DeparserContext): string { const output: string[] = ['NOTIFY']; - + if (node.conditionname) { output.push(node.conditionname); } - + if (node.payload !== null && node.payload !== undefined) { output.push(','); output.push(`'${node.payload}'`); } - + return output.join(' '); } ListenStmt(node: t.ListenStmt, context: DeparserContext): string { const output: string[] = ['LISTEN']; - + if (node.conditionname) { output.push(node.conditionname); } - + return output.join(' '); } UnlistenStmt(node: t.UnlistenStmt, context: DeparserContext): string { const output: string[] = ['UNLISTEN']; - + if (node.conditionname) { output.push(node.conditionname); } else { output.push('*'); } - + return output.join(' '); } @@ -6510,7 +6514,7 @@ export class Deparser implements DeparserVisitor { CommentStmt(node: t.CommentStmt, context: DeparserContext): string { const output: string[] = ['COMMENT ON']; - + if (node.objtype) { switch (node.objtype) { case 'OBJECT_TABLE': @@ -6592,7 +6596,7 @@ export class Deparser implements DeparserVisitor { output.push(node.objtype.replace('OBJECT_', '')); } } - + if (node.object) { // Handle object names specially for CommentStmt if (node.object && typeof node.object === 'object' && 'List' in node.object) { @@ -6600,7 +6604,7 @@ export class Deparser implements DeparserVisitor { if (list.items && list.items.length > 0) { const objectParts = ListUtils.unwrapList(list.items) .map(item => this.visit(item, context)); - + if (node.objtype === 'OBJECT_TABCONSTRAINT') { if (objectParts.length === 3) { const [schema, table, constraint] = objectParts; @@ -6638,13 +6642,13 @@ export class Deparser implements DeparserVisitor { // For operators, we need to handle ObjectWithArgs structure if (node.object && (node.object as any).ObjectWithArgs) { const objWithArgs = (node.object as any).ObjectWithArgs; - let operatorName = objWithArgs.objname && objWithArgs.objname[0] && objWithArgs.objname[0].String + let operatorName = objWithArgs.objname && objWithArgs.objname[0] && objWithArgs.objname[0].String ? objWithArgs.objname[0].String.sval : 'unknown'; - + if (operatorName.startsWith('"') && operatorName.endsWith('"')) { operatorName = operatorName.slice(1, -1); } - + const args: string[] = []; if (objWithArgs.objargs) { objWithArgs.objargs.forEach((arg: any) => { @@ -6658,7 +6662,7 @@ export class Deparser implements DeparserVisitor { } }); } - + output.push(`${operatorName} (${args.join(', ')})`); } else { output.push(objectParts.join('.')); @@ -6700,14 +6704,14 @@ export class Deparser implements DeparserVisitor { } else if (node.objtype === 'OBJECT_OPERATOR' && node.object && (node.object as any).ObjectWithArgs) { // Handle direct ObjectWithArgs for OPERATOR syntax: COMMENT ON OPERATOR -(NONE, integer) IS 'comment' const objWithArgs = (node.object as any).ObjectWithArgs; - let operatorName = objWithArgs.objname && objWithArgs.objname[0] && objWithArgs.objname[0].String + let operatorName = objWithArgs.objname && objWithArgs.objname[0] && objWithArgs.objname[0].String ? objWithArgs.objname[0].String.sval : 'unknown'; - + // Remove quotes from operator name if present if (operatorName.startsWith('"') && operatorName.endsWith('"')) { operatorName = operatorName.slice(1, -1); } - + const args: string[] = []; if (objWithArgs.objargs) { objWithArgs.objargs.forEach((arg: any) => { @@ -6721,35 +6725,35 @@ export class Deparser implements DeparserVisitor { } }); } - + output.push(`${operatorName}(${args.join(', ')})`); } else { - const objContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CommentStmt'], objtype: node.objtype }; + const objContext = context.spawn('CommentStmt', { objtype: node.objtype }); output.push(this.visit(node.object, objContext)); } } - + output.push('IS'); - + if (node.comment === null || node.comment === undefined) { output.push('NULL'); } else if (node.comment) { output.push(QuoteUtils.formatEString(node.comment)); } - + return output.join(' '); } LockStmt(node: t.LockStmt, context: DeparserContext): string { const output: string[] = ['LOCK', 'TABLE']; - + if (node.relations && node.relations.length > 0) { const relations = ListUtils.unwrapList(node.relations) .map(rel => this.visit(rel, context)) .join(', '); output.push(relations); } - + if (node.mode !== undefined) { const lockModes = [ '', // mode 0 (unused) @@ -6762,78 +6766,78 @@ export class Deparser implements DeparserVisitor { 'EXCLUSIVE', // mode 7 'ACCESS EXCLUSIVE' // mode 8 ]; - + if (node.mode >= 1 && node.mode < lockModes.length) { output.push('IN', lockModes[node.mode], 'MODE'); } } - + if (node.nowait) { output.push('NOWAIT'); } - + return output.join(' '); } CreatePolicyStmt(node: t.CreatePolicyStmt, context: DeparserContext): string { const output: string[] = []; - + const initialParts = ['CREATE', 'POLICY']; if (node.policy_name) { initialParts.push(QuoteUtils.quote(node.policy_name)); } - + output.push(initialParts.join(' ')); - + // Add ON clause on new line in pretty mode if (node.table) { - if (this.formatter.isPretty()) { - output.push(this.formatter.newline() + this.formatter.indent(`ON ${this.RangeVar(node.table, context)}`)); + if (context.isPretty()) { + output.push(context.newline() + context.indent(`ON ${this.RangeVar(node.table, context)}`)); } else { output.push('ON'); output.push(this.RangeVar(node.table, context)); } } - + // Handle AS RESTRICTIVE/PERMISSIVE clause if (node.permissive === undefined) { - if (this.formatter.isPretty()) { - output.push(this.formatter.newline() + this.formatter.indent('AS RESTRICTIVE')); + if (context.isPretty()) { + output.push(context.newline() + context.indent('AS RESTRICTIVE')); } else { output.push('AS', 'RESTRICTIVE'); } } else if (node.permissive === true) { - if (this.formatter.isPretty()) { - output.push(this.formatter.newline() + this.formatter.indent('AS PERMISSIVE')); + if (context.isPretty()) { + output.push(context.newline() + context.indent('AS PERMISSIVE')); } else { output.push('AS', 'PERMISSIVE'); } } - + if (node.cmd_name) { - if (this.formatter.isPretty()) { - output.push(this.formatter.newline() + this.formatter.indent(`FOR ${node.cmd_name.toUpperCase()}`)); + if (context.isPretty()) { + output.push(context.newline() + context.indent(`FOR ${node.cmd_name.toUpperCase()}`)); } else { output.push('FOR', node.cmd_name.toUpperCase()); } } - + if (node.roles && node.roles.length > 0) { const roles = ListUtils.unwrapList(node.roles).map(role => this.visit(role, context)); - if (this.formatter.isPretty()) { - output.push(this.formatter.newline() + this.formatter.indent(`TO ${roles.join(', ')}`)); + if (context.isPretty()) { + output.push(context.newline() + context.indent(`TO ${roles.join(', ')}`)); } else { output.push('TO'); output.push(roles.join(', ')); } } - + if (node.qual) { - if (this.formatter.isPretty()) { + if (context.isPretty()) { const qualExpr = this.visit(node.qual, context); - output.push(this.formatter.newline() + this.formatter.indent('USING (')); - output.push(this.formatter.newline() + this.formatter.indent(this.formatter.indent(qualExpr))); - output.push(this.formatter.newline() + this.formatter.indent(')')); + output.push(context.newline() + context.indent('USING (')); + output.push(context.newline() + context.indent(context.indent(qualExpr))); + output.push(context.newline() + context.indent(')')); } else { output.push('USING'); output.push(`(${this.visit(node.qual, context)})`); @@ -6841,116 +6845,116 @@ export class Deparser implements DeparserVisitor { } if (node.with_check) { - if (this.formatter.isPretty()) { + if (context.isPretty()) { const checkExpr = this.visit(node.with_check, context); - output.push(this.formatter.newline() + this.formatter.indent('WITH CHECK (')); - output.push(this.formatter.newline() + this.formatter.indent(this.formatter.indent(checkExpr))); - output.push(this.formatter.newline() + this.formatter.indent(')')); + output.push(context.newline() + context.indent('WITH CHECK (')); + output.push(context.newline() + context.indent(context.indent(checkExpr))); + output.push(context.newline() + context.indent(')')); } else { output.push('WITH CHECK'); output.push(`(${this.visit(node.with_check, context)})`); } } - - return this.formatter.isPretty() ? output.join('') : output.join(' '); + + return context.isPretty() ? output.join('') : output.join(' '); } AlterPolicyStmt(node: t.AlterPolicyStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'POLICY']; - + if (node.policy_name) { output.push(QuoteUtils.quote(node.policy_name)); } - + if (node.table) { output.push('ON'); output.push(this.RangeVar(node.table, context)); } - + if (node.roles && node.roles.length > 0) { output.push('TO'); const roles = ListUtils.unwrapList(node.roles).map(role => this.visit(role, context)); output.push(roles.join(', ')); } - + if (node.qual) { output.push('USING'); output.push(`(${this.visit(node.qual, context)})`); } - + if (node.with_check) { output.push('WITH CHECK'); output.push(`(${this.visit(node.with_check, context)})`); } - + return output.join(' '); } CreateUserMappingStmt(node: t.CreateUserMappingStmt, context: DeparserContext): string { const output: string[] = ['CREATE']; - + if (node.if_not_exists) { output.push('IF', 'NOT', 'EXISTS'); } - + output.push('USER', 'MAPPING'); - + output.push('FOR'); - + if (node.user) { output.push(this.RoleSpec(node.user, context)); } else { output.push('CURRENT_USER'); } - + output.push('SERVER'); - + if (node.servername) { output.push(`"${node.servername}"`); } - + if (node.options && node.options.length > 0) { output.push('OPTIONS'); - const userMappingContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateUserMappingStmt'] }; + const userMappingContext = context.spawn('CreateUserMappingStmt'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, userMappingContext)); output.push(`(${options.join(', ')})`); } - + return output.join(' '); } CreateStatsStmt(node: t.CreateStatsStmt, context: DeparserContext): string { const output: string[] = ['CREATE']; - + if (node.if_not_exists) { output.push('IF', 'NOT', 'EXISTS'); } - + output.push('STATISTICS'); - + if (node.defnames && node.defnames.length > 0) { const names = ListUtils.unwrapList(node.defnames).map(name => this.visit(name, context)); output.push(names.join('.')); } - + if (node.stat_types && node.stat_types.length > 0) { const types = ListUtils.unwrapList(node.stat_types).map(type => this.visit(type, context)); output.push(`(${types.join(', ')})`); } - + output.push('ON'); - + if (node.exprs && node.exprs.length > 0) { const exprs = ListUtils.unwrapList(node.exprs).map(expr => this.visit(expr, context)); output.push(exprs.join(', ')); } - + if (node.relations && node.relations.length > 0) { output.push('FROM'); const relations = ListUtils.unwrapList(node.relations).map(rel => this.visit(rel, context)); output.push(relations.join(', ')); } - + return output.join(' '); } @@ -6965,11 +6969,11 @@ export class Deparser implements DeparserVisitor { CreatePublicationStmt(node: t.CreatePublicationStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'PUBLICATION']; - + if (node.pubname) { output.push(`"${node.pubname}"`); } - + if (node.pubobjects && node.pubobjects.length > 0) { output.push('FOR', 'TABLE'); const tables = ListUtils.unwrapList(node.pubobjects).map(table => this.visit(table, context)); @@ -6977,52 +6981,52 @@ export class Deparser implements DeparserVisitor { } else if (node.for_all_tables) { output.push('FOR', 'ALL', 'TABLES'); } - + if (node.options && node.options.length > 0) { output.push('WITH'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, context)); output.push(`(${options.join(', ')})`); } - + return output.join(' '); } CreateSubscriptionStmt(node: t.CreateSubscriptionStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'SUBSCRIPTION']; - + if (node.subname) { output.push(`"${node.subname}"`); } - + output.push('CONNECTION'); - + if (node.conninfo) { output.push(`'${node.conninfo}'`); } - + output.push('PUBLICATION'); - + if (node.publication && node.publication.length > 0) { const publications = ListUtils.unwrapList(node.publication).map(pub => this.visit(pub, context)); output.push(publications.join(', ')); } - + if (node.options && node.options.length > 0) { output.push('WITH'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, context)); output.push(`(${options.join(', ')})`); } - + return output.join(' '); } AlterPublicationStmt(node: t.AlterPublicationStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'PUBLICATION']; - + if (node.pubname) { output.push(`"${node.pubname}"`); } - + if (node.action) { switch (node.action) { case 'AP_AddObjects': @@ -7038,7 +7042,7 @@ export class Deparser implements DeparserVisitor { throw new Error(`Unsupported AlterPublicationStmt action: ${node.action}`); } } - + if (node.for_all_tables) { output.push('FOR ALL TABLES'); } else if (node.pubobjects && node.pubobjects.length > 0) { @@ -7046,23 +7050,23 @@ export class Deparser implements DeparserVisitor { const objects = ListUtils.unwrapList(node.pubobjects).map(obj => this.visit(obj, context)); output.push(objects.join(', ')); } - + if (node.options && node.options.length > 0) { output.push('WITH'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, context)); output.push(`(${options.join(', ')})`); } - + return output.join(' '); } AlterSubscriptionStmt(node: t.AlterSubscriptionStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'SUBSCRIPTION']; - + if (node.subname) { output.push(`"${node.subname}"`); } - + if (node.kind) { switch (node.kind) { case 'ALTER_SUBSCRIPTION_OPTIONS': @@ -7094,27 +7098,27 @@ export class Deparser implements DeparserVisitor { throw new Error(`Unsupported AlterSubscriptionStmt kind: ${node.kind}`); } } - + if (node.options && node.options.length > 0) { output.push('WITH'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, context)); output.push(`(${options.join(', ')})`); } - + return output.join(' '); } DropSubscriptionStmt(node: t.DropSubscriptionStmt, context: DeparserContext): string { const output: string[] = ['DROP', 'SUBSCRIPTION']; - + if (node.missing_ok) { output.push('IF EXISTS'); } - + if (node.subname) { output.push(`"${node.subname}"`); } - + if (node.behavior) { switch (node.behavior) { case 'DROP_CASCADE': @@ -7125,19 +7129,19 @@ export class Deparser implements DeparserVisitor { break; } } - + return output.join(' '); } DoStmt(node: t.DoStmt, context: DeparserContext): string { const output: string[] = ['DO']; - + if (node.args && node.args.length > 0) { - const doContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DoStmt'] }; + const doContext = context.spawn('DoStmt'); const args = ListUtils.unwrapList(node.args); - + const processedArgs: string[] = []; - + for (const arg of args) { const nodeType = this.getNodeType(arg); if (nodeType === 'DefElem') { @@ -7158,10 +7162,10 @@ export class Deparser implements DeparserVisitor { } } } - + output.push(...processedArgs); } - + return output.join(' '); } @@ -7169,26 +7173,26 @@ export class Deparser implements DeparserVisitor { // Check if content contains nested dollar quotes const dollarQuotePattern = /\$[a-zA-Z0-9_]*\$/g; const matches = content.match(dollarQuotePattern) || []; - + if (matches.length === 0) { return '$$'; } - + const existingTags = new Set(matches); - + // Check if $$ is already used if (existingTags.has('$$')) { let counter = 1; let tag = `$do${counter}$`; - + while (existingTags.has(tag)) { counter++; tag = `$do${counter}$`; } - + return tag; } - + return '$$'; } @@ -7210,56 +7214,56 @@ export class Deparser implements DeparserVisitor { ConstraintsSetStmt(node: t.ConstraintsSetStmt, context: DeparserContext): string { const output: string[] = ['SET', 'CONSTRAINTS']; - + if (node.constraints && node.constraints.length > 0) { const constraints = ListUtils.unwrapList(node.constraints).map(constraint => this.visit(constraint, context)); output.push(constraints.join(', ')); } else { output.push('ALL'); } - + output.push(node.deferred ? 'DEFERRED' : 'IMMEDIATE'); - + return output.join(' '); } AlterSystemStmt(node: t.AlterSystemStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'SYSTEM']; - + if (node.setstmt) { const setStmt = this.VariableSetStmt(node.setstmt, context); const setStmtWithoutPrefix = setStmt.replace(/^SET\s+/, ''); output.push('SET', setStmtWithoutPrefix); } - + return output.join(' '); } VacuumRelation(node: t.VacuumRelation, context: DeparserContext): string { const output: string[] = []; - + if (node.relation) { output.push(this.RangeVar(node.relation, context)); } - + if (node.va_cols && node.va_cols.length > 0) { output.push('('); const columns = ListUtils.unwrapList(node.va_cols).map(col => this.visit(col, context)); output.push(columns.join(', ')); output.push(')'); } - + return output.join(' '); } DropOwnedStmt(node: t.DropOwnedStmt, context: DeparserContext): string { const output: string[] = ['DROP', 'OWNED', 'BY']; - + if (node.roles && node.roles.length > 0) { const roles = ListUtils.unwrapList(node.roles).map(role => this.visit(role, context)); output.push(roles.join(', ')); } - + if (node.behavior) { switch (node.behavior) { case 'DROP_CASCADE': @@ -7270,53 +7274,53 @@ export class Deparser implements DeparserVisitor { break; } } - + return output.join(' '); } ReassignOwnedStmt(node: t.ReassignOwnedStmt, context: DeparserContext): string { const output: string[] = ['REASSIGN', 'OWNED', 'BY']; - + if (node.roles && node.roles.length > 0) { const roles = ListUtils.unwrapList(node.roles).map(role => this.visit(role, context)); output.push(roles.join(', ')); } - + output.push('TO'); - + if (node.newrole) { output.push(this.RoleSpec(node.newrole, context)); } - + return output.join(' '); } AlterTSDictionaryStmt(node: t.AlterTSDictionaryStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'TEXT', 'SEARCH', 'DICTIONARY']; - + if (node.dictname && node.dictname.length > 0) { const dictName = ListUtils.unwrapList(node.dictname).map(name => this.visit(name, context)); output.push(dictName.join('.')); } - + if (node.options && node.options.length > 0) { output.push('('); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, context)); output.push(options.join(', ')); output.push(')'); } - + return output.join(' '); } AlterTSConfigurationStmt(node: t.AlterTSConfigurationStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'TEXT', 'SEARCH', 'CONFIGURATION']; - + if (node.cfgname && node.cfgname.length > 0) { const cfgName = ListUtils.unwrapList(node.cfgname).map(name => this.visit(name, context)); output.push(cfgName.join('.')); } - + if (node.kind) { switch (node.kind) { case 'ALTER_TSCONFIG_ADD_MAPPING': @@ -7393,28 +7397,28 @@ export class Deparser implements DeparserVisitor { throw new Error(`Unsupported AlterTSConfigurationStmt kind: ${node.kind}`); } } - + return output.join(' '); } ClosePortalStmt(node: t.ClosePortalStmt, context: DeparserContext): string { const output: string[] = ['CLOSE']; - + if (node.portalname) { output.push(QuoteUtils.quote(node.portalname)); } else { output.push('ALL'); } - + return output.join(' '); } FetchStmt(node: t.FetchStmt, context: DeparserContext): string { const output: string[] = [node.ismove ? 'MOVE' : 'FETCH']; - + // Check if howMany represents "ALL" (PostgreSQL uses LONG_MAX as sentinel) const isAll = (node.howMany as any) === 9223372036854776000; - + // Handle direction first, then check for ALL within each direction if (node.direction) { switch (node.direction) { @@ -7455,40 +7459,40 @@ export class Deparser implements DeparserVisitor { // Handle plain "ALL" without direction output.push('ALL'); } - + if (node.portalname) { output.push(QuoteUtils.quote(node.portalname)); } - + return output.join(' '); } AlterStatsStmt(node: t.AlterStatsStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'STATISTICS']; - + if (node.defnames && node.defnames.length > 0) { const names = ListUtils.unwrapList(node.defnames).map(name => this.visit(name, context)); output.push(names.join('.')); } - + output.push('SET', 'STATISTICS'); - + if (node.stxstattarget) { output.push(this.visit(node.stxstattarget, context)); } - + return output.join(' '); } ObjectWithArgs(node: t.ObjectWithArgs, context: DeparserContext): string { let result = ''; - + if (node.objname && node.objname.length > 0) { - const objContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'ObjectWithArgs'] }; + const objContext = context.spawn('ObjectWithArgs'); const names = ListUtils.unwrapList(node.objname).map(name => this.visit(name, objContext)); result = names.join('.'); } - + if (node.objfuncargs && node.objfuncargs.length > 0) { const funcArgs = ListUtils.unwrapList(node.objfuncargs).map(arg => this.visit(arg, context)); result += `(${funcArgs.join(', ')})`; @@ -7504,7 +7508,7 @@ export class Deparser implements DeparserVisitor { } else if (node.args_unspecified) { // For functions with unspecified args, don't add parentheses } else { - if ((context.parentNodeTypes.includes('CommentStmt') || context.parentNodeTypes.includes('DropStmt')) && + if ((context.parentNodeTypes.includes('CommentStmt') || context.parentNodeTypes.includes('DropStmt')) && context.objtype === 'OBJECT_AGGREGATE') { result += '(*)'; } else if (context.parentNodeTypes.includes('CreateOpClassItem')) { @@ -7513,165 +7517,165 @@ export class Deparser implements DeparserVisitor { result += '()'; } } - + return result; } AlterOperatorStmt(node: t.AlterOperatorStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'OPERATOR']; - + if (node.opername) { output.push(this.ObjectWithArgs(node.opername, context)); } - + output.push('SET'); - + if (node.options && node.options.length > 0) { - const alterOpContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterOperatorStmt'] }; + const alterOpContext = context.spawn('AlterOperatorStmt'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, alterOpContext)); output.push(`(${options.join(', ')})`); } - + return output.join(' '); } AlterFdwStmt(node: t.AlterFdwStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'FOREIGN', 'DATA', 'WRAPPER']; - + if (node.fdwname) { output.push(QuoteUtils.quote(node.fdwname)); } - + if (node.func_options && node.func_options.length > 0) { - const fdwContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterFdwStmt'] }; + const fdwContext = context.spawn('AlterFdwStmt'); const funcOptions = ListUtils.unwrapList(node.func_options).map(opt => this.visit(opt, fdwContext)); output.push(funcOptions.join(' ')); } - + if (node.options && node.options.length > 0) { output.push('OPTIONS'); - const fdwContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterFdwStmt'] }; + const fdwContext = context.spawn('AlterFdwStmt'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, fdwContext)); output.push(`(${options.join(', ')})`); } - + return output.join(' '); } CreateForeignServerStmt(node: t.CreateForeignServerStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'SERVER']; - + if (node.if_not_exists) { output.push('IF', 'NOT', 'EXISTS'); } - + if (node.servername) { output.push(QuoteUtils.quote(node.servername)); } - + if (node.servertype) { output.push('TYPE', QuoteUtils.escape(node.servertype)); } - + if (node.version) { output.push('VERSION', QuoteUtils.escape(node.version)); } - + if (node.fdwname) { output.push('FOREIGN', 'DATA', 'WRAPPER', QuoteUtils.quote(node.fdwname)); } - + if (node.options && node.options.length > 0) { output.push('OPTIONS'); output.push('('); - const optionsContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateForeignServerStmt'] }; + const optionsContext = context.spawn('CreateForeignServerStmt'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, optionsContext)); output.push(options.join(', ')); output.push(')'); } - + return output.join(' '); } AlterForeignServerStmt(node: t.AlterForeignServerStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'SERVER']; - + if (node.servername) { output.push(QuoteUtils.quote(node.servername)); } - + if (node.version) { output.push('VERSION', QuoteUtils.escape(node.version)); } - + if (node.options && node.options.length > 0) { output.push('OPTIONS'); output.push('('); - const optionsContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterForeignServerStmt'] }; + const optionsContext = context.spawn('AlterForeignServerStmt'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, optionsContext)); output.push(options.join(', ')); output.push(')'); } - + return output.join(' '); } AlterUserMappingStmt(node: t.AlterUserMappingStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'USER', 'MAPPING', 'FOR']; - + if (node.user) { output.push(this.RoleSpec(node.user, context)); } else { output.push('CURRENT_USER'); } - + output.push('SERVER'); - + if (node.servername) { output.push(QuoteUtils.quote(node.servername)); } - + if (node.options && node.options.length > 0) { output.push('OPTIONS'); - const userMappingContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterUserMappingStmt'] }; + const userMappingContext = context.spawn('AlterUserMappingStmt'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, userMappingContext)); output.push(`(${options.join(', ')})`); } - + return output.join(' '); } DropUserMappingStmt(node: t.DropUserMappingStmt, context: DeparserContext): string { const output: string[] = ['DROP', 'USER', 'MAPPING']; - + if (node.missing_ok) { output.push('IF', 'EXISTS'); } - + output.push('FOR'); - + if (node.user) { output.push(this.RoleSpec(node.user, context)); } else { output.push('CURRENT_USER'); } - + output.push('SERVER'); - + if (node.servername) { output.push(QuoteUtils.quote(node.servername)); } - + return output.join(' '); } ImportForeignSchemaStmt(node: t.ImportForeignSchemaStmt, context: DeparserContext): string { const output: string[] = ['IMPORT', 'FOREIGN', 'SCHEMA']; - + if (node.remote_schema) { output.push(QuoteUtils.quote(node.remote_schema)); } - + if (node.list_type) { switch (node.list_type) { case 'FDW_IMPORT_SCHEMA_ALL': @@ -7694,87 +7698,87 @@ export class Deparser implements DeparserVisitor { throw new Error(`Unsupported ImportForeignSchemaStmt list_type: ${node.list_type}`); } } - + output.push('FROM', 'SERVER'); - + if (node.server_name) { output.push(QuoteUtils.quote(node.server_name)); } - + output.push('INTO'); - + if (node.local_schema) { output.push(QuoteUtils.quote(node.local_schema)); } - + if (node.options && node.options.length > 0) { - const importSchemaContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'ImportForeignSchemaStmt'] }; + const importSchemaContext = context.spawn('ImportForeignSchemaStmt'); const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, importSchemaContext)); output.push(`OPTIONS (${options.join(', ')})`); } - + return output.join(' '); } ClusterStmt(node: t.ClusterStmt, context: DeparserContext): string { const output: string[] = ['CLUSTER']; - + if (node.relation) { output.push(this.RangeVar(node.relation, context)); - + if (node.indexname) { output.push('USING', `"${node.indexname}"`); } } - + if (node.params && node.params.length > 0) { const params = ListUtils.unwrapList(node.params).map(param => this.visit(param, context)); output.push(`(${params.join(', ')})`); } - + return output.join(' '); } VacuumStmt(node: t.VacuumStmt, context: DeparserContext): string { const output: string[] = [node.is_vacuumcmd ? 'VACUUM' : 'ANALYZE']; - + if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options).map(option => this.visit(option, context)); output.push(`(${options.join(', ')})`); } - + if (node.rels && node.rels.length > 0) { const relations = ListUtils.unwrapList(node.rels).map(rel => this.visit(rel, context)); output.push(relations.join(', ')); } - + return output.join(' '); } ExplainStmt(node: t.ExplainStmt, context: DeparserContext): string { const output: string[] = ['EXPLAIN']; - + if (node.options && node.options.length > 0) { - const explainContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'ExplainStmt'] }; + const explainContext = context.spawn('ExplainStmt'); const options = ListUtils.unwrapList(node.options).map(option => this.visit(option, explainContext)); output.push(`(${options.join(', ')})`); } - + if (node.query) { output.push(this.visit(node.query, context)); } - + return output.join(' '); } ReindexStmt(node: t.ReindexStmt, context: DeparserContext): string { const output: string[] = ['REINDEX']; - + if (node.params && node.params.length > 0) { const params = ListUtils.unwrapList(node.params).map(param => this.visit(param, context)); output.push(`(${params.join(', ')})`); } - + if (node.kind) { switch (node.kind) { case 'REINDEX_OBJECT_INDEX': @@ -7796,24 +7800,24 @@ export class Deparser implements DeparserVisitor { throw new Error(`Unsupported ReindexStmt kind: ${node.kind}`); } } - + if (node.relation) { output.push(this.RangeVar(node.relation, context)); } - + if (node.name) { output.push(`"${node.name}"`); } - + return output.join(' '); } CallStmt(node: t.CallStmt, context: DeparserContext): string { const output: string[] = ['CALL']; - + if (node.funccall) { const funcCall = node.funccall as any; - + if (funcCall.funcname && funcCall.funcname.length > 0) { const funcNameParts = funcCall.funcname.map((nameNode: any) => { if (nameNode.String) { @@ -7822,7 +7826,7 @@ export class Deparser implements DeparserVisitor { return this.visit(nameNode, context); }); const funcName = funcNameParts.join('.'); - + let argsStr = ''; if (funcCall.args && funcCall.args.length > 0) { const argStrs = funcCall.args.map((arg: any) => this.visit(arg, context)); @@ -7830,7 +7834,7 @@ export class Deparser implements DeparserVisitor { } else { argsStr = '()'; } - + output.push(`${funcName}${argsStr}`); } } else if (node.funcexpr) { @@ -7838,59 +7842,59 @@ export class Deparser implements DeparserVisitor { } else { throw new Error('CallStmt requires either funccall or funcexpr'); } - + return output.join(' '); } CreatedbStmt(node: t.CreatedbStmt, context: DeparserContext): string { const output: string[] = ['CREATE DATABASE']; - + if (!node.dbname) { throw new Error('CreatedbStmt requires dbname'); } - + output.push(`"${node.dbname}"`); - + if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options) .map(option => this.visit(option, context)) .join(' '); output.push('WITH', options); } - + return output.join(' '); } DropdbStmt(node: t.DropdbStmt, context: DeparserContext): string { const output: string[] = ['DROP DATABASE']; - + if (node.missing_ok) { output.push('IF EXISTS'); } - + if (!node.dbname) { throw new Error('DropdbStmt requires dbname'); } - + output.push(`"${node.dbname}"`); - + if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options) .map(option => this.visit(option, context)) .join(' '); output.push('WITH', options); } - + return output.join(' '); } RenameStmt(node: t.RenameStmt, context: DeparserContext): string { const output: string[] = ['ALTER']; - + if (!node.renameType) { throw new Error('RenameStmt requires renameType'); } - + switch (node.renameType) { case 'OBJECT_TABLE': output.push('TABLE'); @@ -8025,26 +8029,24 @@ export class Deparser implements DeparserVisitor { default: throw new Error(`Unsupported RenameStmt renameType: ${node.renameType}`); } - + if (node.missing_ok) { output.push('IF EXISTS'); } - + // Handle OBJECT_RULE special case: rule_name ON table_name format if (node.renameType === 'OBJECT_RULE' && node.subname && node.relation) { output.push(QuoteUtils.quote(node.subname)); output.push('ON'); output.push(this.RangeVar(node.relation, context)); } else if (node.relation) { - const rangeVarContext = node.relationType === 'OBJECT_TYPE' - ? { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterTypeStmt'] } - : context; - + const rangeVarContext = context.spawn('RenameStmt', { objtype: node.relationType }); + // Add ON keyword for policy operations if (node.renameType === 'OBJECT_POLICY') { output.push('ON'); } - + output.push(this.RangeVar(node.relation, rangeVarContext)); } else if (node.object) { // Handle operator family and operator class objects specially to format name USING access_method correctly @@ -8069,7 +8071,7 @@ export class Deparser implements DeparserVisitor { output.push(this.visit(node.object, context)); } } - + if (node.renameType === 'OBJECT_COLUMN' && node.subname) { output.push('RENAME COLUMN', `"${node.subname}"`, 'TO'); } else if (node.renameType === 'OBJECT_DOMCONSTRAINT' && node.subname) { @@ -8087,28 +8089,28 @@ export class Deparser implements DeparserVisitor { } else { output.push('RENAME TO'); } - + if (!node.newname) { throw new Error('RenameStmt requires newname'); } - + output.push(QuoteUtils.quote(node.newname)); - + // Handle CASCADE/RESTRICT behavior for RENAME operations if (node.behavior === 'DROP_CASCADE') { output.push('CASCADE'); } - + return output.join(' '); } AlterOwnerStmt(node: t.AlterOwnerStmt, context: DeparserContext): string { const output: string[] = ['ALTER']; - + if (!node.objectType) { throw new Error('AlterOwnerStmt requires objectType'); } - + switch (node.objectType) { case 'OBJECT_TABLE': output.push('TABLE'); @@ -8179,7 +8181,7 @@ export class Deparser implements DeparserVisitor { default: throw new Error(`Unsupported AlterOwnerStmt objectType: ${node.objectType}`); } - + if (node.relation) { output.push(this.RangeVar(node.relation, context)); } else if (node.object) { @@ -8197,21 +8199,21 @@ export class Deparser implements DeparserVisitor { output.push(this.visit(node.object, context)); } } - + output.push('OWNER TO'); - + if (!node.newowner) { throw new Error('AlterOwnerStmt requires newowner'); } - + output.push(this.RoleSpec(node.newowner, context)); - + return output.join(' '); } GrantStmt(node: t.GrantStmt, context: DeparserContext): string { const output: string[] = []; - + if (node.is_grant) { output.push('GRANT'); } else { @@ -8222,7 +8224,7 @@ export class Deparser implements DeparserVisitor { } if (node.privileges && node.privileges.length > 0) { - const privilegeContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'GrantStmt'] }; + const privilegeContext = context.spawn('GrantStmt'); const privileges = ListUtils.unwrapList(node.privileges) .map(priv => this.visit(priv, privilegeContext)) .join(', '); @@ -8364,7 +8366,7 @@ export class Deparser implements DeparserVisitor { GrantRoleStmt(node: t.GrantRoleStmt, context: DeparserContext): string { const output: string[] = []; - + // Check for inherit, admin, and set options first to place them correctly let hasInheritOption = false; let hasAdminOption = false; @@ -8372,46 +8374,46 @@ export class Deparser implements DeparserVisitor { let inheritValue: boolean | undefined; let adminValue: boolean | undefined; let setValue: boolean | undefined; - + if (node.opt && node.opt.length > 0) { const options = ListUtils.unwrapList(node.opt); - - const inheritOption = options.find(opt => + + const inheritOption = options.find(opt => opt.DefElem && opt.DefElem.defname === 'inherit' ); - - const adminOption = options.find(opt => + + const adminOption = options.find(opt => (opt.String && opt.String.sval === 'admin') || (opt.DefElem && opt.DefElem.defname === 'admin') ); - - const setOption = options.find(opt => + + const setOption = options.find(opt => opt.DefElem && opt.DefElem.defname === 'set' ); - + if (inheritOption && inheritOption.DefElem) { hasInheritOption = true; inheritValue = inheritOption.DefElem.arg?.Boolean?.boolval; } - + if (adminOption) { hasAdminOption = true; if (adminOption.DefElem && adminOption.DefElem.arg) { adminValue = adminOption.DefElem.arg.Boolean?.boolval; } } - + if (setOption && setOption.DefElem) { hasSetOption = true; setValue = setOption.DefElem.arg?.Boolean?.boolval; } } - + if (node.is_grant) { output.push('GRANT'); } else { output.push('REVOKE'); - + if (hasInheritOption) { output.push('INHERIT OPTION FOR'); } else if (hasAdminOption) { @@ -8441,7 +8443,7 @@ export class Deparser implements DeparserVisitor { if (node.is_grant) { const withOptions: string[] = []; - + if (hasAdminOption) { if (adminValue === true) { withOptions.push('ADMIN OPTION'); @@ -8451,7 +8453,7 @@ export class Deparser implements DeparserVisitor { withOptions.push('ADMIN OPTION'); } } - + if (hasInheritOption) { if (inheritValue === true) { withOptions.push('INHERIT OPTION'); @@ -8459,7 +8461,7 @@ export class Deparser implements DeparserVisitor { withOptions.push('INHERIT FALSE'); } } - + if (hasSetOption) { if (setValue === true) { withOptions.push('SET TRUE'); @@ -8467,7 +8469,7 @@ export class Deparser implements DeparserVisitor { withOptions.push('SET FALSE'); } } - + if (withOptions.length > 0) { output.push('WITH', withOptions.join(', ')); } @@ -8554,8 +8556,8 @@ export class Deparser implements DeparserVisitor { if (node.action) { const actionStr = this.GrantStmt(node.action, context); - if (this.formatter.isPretty()) { - return output.join(' ') + this.formatter.newline() + this.formatter.indent(actionStr); + if (context.isPretty()) { + return output.join(' ') + context.newline() + context.indent(actionStr); } else { output.push(actionStr); } @@ -8742,14 +8744,14 @@ export class Deparser implements DeparserVisitor { output.push(QuoteUtils.quote(node.trigname)); } - if (this.formatter.isPretty()) { + if (context.isPretty()) { const components: string[] = []; - + const timing: string[] = []; if (node.timing & 2) timing.push('BEFORE'); else if (node.timing & 64) timing.push('INSTEAD OF'); else timing.push('AFTER'); - + const events: string[] = []; if (node.events & 4) events.push('INSERT'); if (node.events & 8) events.push('DELETE'); @@ -8764,39 +8766,39 @@ export class Deparser implements DeparserVisitor { events.push(updateStr); } if (node.events & 32) events.push('TRUNCATE'); - - components.push(this.formatter.indent(timing.join(' ') + ' ' + events.join(' OR '))); - + + components.push(context.indent(timing.join(' ') + ' ' + events.join(' OR '))); + if (node.relation) { - components.push(this.formatter.indent('ON ' + this.RangeVar(node.relation, context))); + components.push(context.indent('ON ' + this.RangeVar(node.relation, context))); } - + if (node.transitionRels && node.transitionRels.length > 0) { const transitionClauses = ListUtils.unwrapList(node.transitionRels) .map(rel => this.visit(rel, context)) .join(' '); - components.push(this.formatter.indent('REFERENCING ' + transitionClauses)); + components.push(context.indent('REFERENCING ' + transitionClauses)); } - + if (node.deferrable) { - components.push(this.formatter.indent('DEFERRABLE')); + components.push(context.indent('DEFERRABLE')); } - + if (node.initdeferred) { - components.push(this.formatter.indent('INITIALLY DEFERRED')); + components.push(context.indent('INITIALLY DEFERRED')); } - + if (node.row) { - components.push(this.formatter.indent('FOR EACH ROW')); + components.push(context.indent('FOR EACH ROW')); } else { - components.push(this.formatter.indent('FOR EACH STATEMENT')); + components.push(context.indent('FOR EACH STATEMENT')); } - + if (node.whenClause) { const whenStr = 'WHEN (' + this.visit(node.whenClause, context) + ')'; - components.push(this.formatter.indent(whenStr)); + components.push(context.indent(whenStr)); } - + let executeStr = 'EXECUTE'; if (node.funcname && node.funcname.length > 0) { const funcName = ListUtils.unwrapList(node.funcname) @@ -8804,9 +8806,9 @@ export class Deparser implements DeparserVisitor { .join('.'); executeStr += ' PROCEDURE ' + funcName; } - + if (node.args && node.args.length > 0) { - const argContext = { ...context, isStringLiteral: true }; + const argContext = context.spawn('CreateTrigStmt', { isStringLiteral: true }); const args = ListUtils.unwrapList(node.args) .map(arg => this.visit(arg, argContext)) .join(', '); @@ -8814,10 +8816,10 @@ export class Deparser implements DeparserVisitor { } else { executeStr += '()'; } - - components.push(this.formatter.indent(executeStr)); - - return output.join(' ') + this.formatter.newline() + components.join(this.formatter.newline()); + + components.push(context.indent(executeStr)); + + return output.join(' ') + context.newline() + components.join(context.newline()); } else { const timing: string[] = []; if (node.timing & 2) timing.push('BEFORE'); @@ -8889,7 +8891,7 @@ export class Deparser implements DeparserVisitor { if (node.args && node.args.length > 0) { output.push('('); - const argContext = { ...context, isStringLiteral: true }; + const argContext = context.spawn('CreateTrigStmt', { isStringLiteral: true }); const args = ListUtils.unwrapList(node.args) .map(arg => this.visit(arg, argContext)) .join(', '); @@ -8905,17 +8907,17 @@ export class Deparser implements DeparserVisitor { TriggerTransition(node: t.TriggerTransition, context: DeparserContext): string { const output: string[] = []; - + if (node.isNew) { output.push('NEW TABLE AS'); } else { output.push('OLD TABLE AS'); } - + if (node.name) { output.push(QuoteUtils.quote(node.name)); } - + return output.join(' '); } @@ -8934,7 +8936,7 @@ export class Deparser implements DeparserVisitor { if (node.whenclause && node.whenclause.length > 0) { output.push('WHEN'); - const eventTriggerContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateEventTrigStmt'] }; + const eventTriggerContext = context.spawn('CreateEventTrigStmt'); const conditions = ListUtils.unwrapList(node.whenclause) .map(condition => this.visit(condition, eventTriggerContext)) .join(' AND '); @@ -9081,40 +9083,40 @@ export class Deparser implements DeparserVisitor { MergeStmt(node: t.MergeStmt, context: DeparserContext): string { const output: string[] = []; - + if (node.withClause) { output.push(this.WithClause(node.withClause, context)); } - + output.push('MERGE INTO'); - + if (node.relation) { output.push(this.RangeVar(node.relation, context)); } - + if (node.sourceRelation) { output.push('USING'); output.push(this.visit(node.sourceRelation, context)); } - + if (node.joinCondition) { output.push('ON'); output.push(this.visit(node.joinCondition, context)); } - + if (node.mergeWhenClauses && node.mergeWhenClauses.length > 0) { const whenClauses = ListUtils.unwrapList(node.mergeWhenClauses) .map(clause => this.visit(clause, context)) .join(' '); output.push(whenClauses); } - + return output.join(' '); } AlterTableMoveAllStmt(node: t.AlterTableMoveAllStmt, context: DeparserContext): string { const output: string[] = ['ALTER']; - + if (node.objtype === 'OBJECT_TABLE') { output.push('TABLE'); } else if (node.objtype === 'OBJECT_INDEX') { @@ -9122,29 +9124,29 @@ export class Deparser implements DeparserVisitor { } else { output.push('TABLE'); } - + output.push('ALL', 'IN', 'TABLESPACE'); - + if (node.orig_tablespacename) { output.push(QuoteUtils.quote(node.orig_tablespacename)); } - + output.push('SET', 'TABLESPACE'); - + if (node.new_tablespacename) { output.push(QuoteUtils.quote(node.new_tablespacename)); } - + if (node.nowait) { output.push('NOWAIT'); } - + return output.join(' '); } CreateSeqStmt(node: t.CreateSeqStmt, context: DeparserContext): string { const output: string[] = ['CREATE']; - + // Check if this is a temporary sequence if (node.sequence) { const seq = node.sequence as any; @@ -9152,13 +9154,13 @@ export class Deparser implements DeparserVisitor { output.push('TEMPORARY'); } } - + output.push('SEQUENCE'); - + if (node.if_not_exists) { output.push('IF NOT EXISTS'); } - + if (node.sequence) { const sequenceName: string[] = []; const seq = node.sequence as any; @@ -9170,9 +9172,9 @@ export class Deparser implements DeparserVisitor { } output.push(sequenceName.join('.')); } - + if (node.options && node.options.length > 0) { - const seqContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateSeqStmt'] }; + const seqContext = context.spawn('CreateSeqStmt'); const optionStrs = ListUtils.unwrapList(node.options) .filter(option => option != null && this.getNodeType(option) !== 'undefined') .map(option => { @@ -9189,17 +9191,17 @@ export class Deparser implements DeparserVisitor { output.push(optionStrs); } } - + return output.join(' '); } AlterSeqStmt(node: t.AlterSeqStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'SEQUENCE']; - + if (node.missing_ok) { output.push('IF EXISTS'); } - + if (node.sequence) { const sequenceName: string[] = []; const seq = node.sequence as any; @@ -9211,9 +9213,9 @@ export class Deparser implements DeparserVisitor { } output.push(sequenceName.join('.')); } - + if (node.options && node.options.length > 0) { - const seqContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterSeqStmt'] }; + const seqContext = context.spawn('AlterSeqStmt'); const optionStrs = ListUtils.unwrapList(node.options) .filter(option => option && option !== undefined) .map(option => { @@ -9233,24 +9235,24 @@ export class Deparser implements DeparserVisitor { output.push(optionStrs); } } - + if (node.for_identity) { output.push('FOR IDENTITY'); } - + return output.join(' '); } CompositeTypeStmt(node: t.CompositeTypeStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'TYPE']; - + if (node.typevar) { - const typeContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CompositeTypeStmt'] }; + const typeContext = context.spawn('CompositeTypeStmt'); output.push(this.RangeVar(node.typevar, typeContext)); } - + output.push('AS'); - + if (node.coldeflist && node.coldeflist.length > 0) { const colDefs = ListUtils.unwrapList(node.coldeflist) .map(colDef => this.visit(colDef, context)) @@ -9260,22 +9262,22 @@ export class Deparser implements DeparserVisitor { // Handle empty composite types - still need parentheses output.push('()'); } - + return output.join(' '); } CreateRangeStmt(node: t.CreateRangeStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'TYPE']; - + if (node.typeName && node.typeName.length > 0) { const typeNameStr = ListUtils.unwrapList(node.typeName) .map(name => this.visit(name, context)) .join('.'); output.push(typeNameStr); } - + output.push('AS', 'RANGE'); - + if (node.params && node.params.length > 0) { const paramStrs = ListUtils.unwrapList(node.params).map(param => { const paramData = this.getNodeData(param); @@ -9287,20 +9289,20 @@ export class Deparser implements DeparserVisitor { }); output.push(`(${paramStrs.join(', ')})`); } - + return output.join(' '); } AlterEnumStmt(node: t.AlterEnumStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'TYPE']; - + if (node.typeName && node.typeName.length > 0) { const typeNameStr = ListUtils.unwrapList(node.typeName) .map(name => this.visit(name, context)) .join('.'); output.push(typeNameStr); } - + if (node.oldVal && node.newVal) { const escapedOldVal = node.oldVal.replace(/'/g, "''"); const escapedNewVal = node.newVal.replace(/'/g, "''"); @@ -9321,22 +9323,22 @@ export class Deparser implements DeparserVisitor { } } } - + return output.join(' '); } AlterTypeStmt(node: t.AlterTypeStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'TYPE']; - + if (node.typeName && node.typeName.length > 0) { const typeNameStr = ListUtils.unwrapList(node.typeName) .map(name => this.visit(name, context)) .join('.'); output.push(typeNameStr); } - + output.push('SET'); - + if (node.options && node.options.length > 0) { const optionStrs = ListUtils.unwrapList(node.options).map(option => { const optionData = this.getNodeData(option); @@ -9348,36 +9350,36 @@ export class Deparser implements DeparserVisitor { }); output.push(`(${optionStrs.join(', ')})`); } - + return output.join(' '); } AlterRoleStmt(node: t.AlterRoleStmt, context: DeparserContext): string { // Check if this is an ALTER GROUP statement by looking for rolemembers DefElem - const isGroupStatement = node.options && - ListUtils.unwrapList(node.options).some(option => + const isGroupStatement = node.options && + ListUtils.unwrapList(node.options).some(option => option.DefElem && option.DefElem.defname === 'rolemembers' ); - + const output: string[] = ['ALTER', isGroupStatement ? 'GROUP' : 'ROLE']; - + if (node.role) { output.push(this.RoleSpec(node.role, context)); } - + if (node.options) { - const roleContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterRoleStmt'] }; - + const roleContext = context.spawn('AlterRoleStmt'); + // Handle GROUP operations specially based on action value if (isGroupStatement) { - const roleMembersOption = ListUtils.unwrapList(node.options).find(option => + const roleMembersOption = ListUtils.unwrapList(node.options).find(option => option.DefElem && option.DefElem.defname === 'rolemembers' ); - + if (roleMembersOption && roleMembersOption.DefElem) { const operation = node.action === 1 ? 'ADD' : 'DROP'; output.push(operation, 'USER'); - + if (roleMembersOption.DefElem.arg && roleMembersOption.DefElem.arg.List) { const users = ListUtils.unwrapList(roleMembersOption.DefElem.arg.List.items) .map(user => this.visit(user, roleContext)) @@ -9394,24 +9396,24 @@ export class Deparser implements DeparserVisitor { } } } - + return output.join(' '); } DropRoleStmt(node: t.DropRoleStmt, context: DeparserContext): string { const output: string[] = ['DROP', 'ROLE']; - + if (node.missing_ok) { output.push('IF EXISTS'); } - + if (node.roles) { const roleNames = ListUtils.unwrapList(node.roles) .map(role => this.visit(role, context)) .join(', '); output.push(roleNames); } - + return output.join(' '); } @@ -9419,28 +9421,28 @@ export class Deparser implements DeparserVisitor { if (!node || !Array.isArray(node)) { return ''; } - + return node.map((target: any) => this.visit(target, context)).join(', '); } CreateAggregateStmt(node: t.DefineStmt, context: DeparserContext): string { const output: string[] = ['CREATE']; - + if (node.replace) { output.push('OR REPLACE'); } - + output.push('AGGREGATE'); - + if (node.defnames && node.defnames.length > 0) { const aggName = ListUtils.unwrapList(node.defnames) .map(name => this.visit(name, context)) .join('.'); output.push(aggName); } - + output.push('('); - + // Handle aggregate arguments/parameters if (node.args && node.args.length > 0) { const args = ListUtils.unwrapList(node.args) @@ -9450,12 +9452,12 @@ export class Deparser implements DeparserVisitor { } else { output.push('*'); } - + output.push(')'); output.push('('); - + const options: string[] = []; - + if (node.definition && node.definition.length > 0) { const optionStrs = ListUtils.unwrapList(node.definition) .map(option => { @@ -9492,16 +9494,16 @@ export class Deparser implements DeparserVisitor { }); options.push(...optionStrs); } - + output.push(options.join(', ')); output.push(')'); - + return output.join(' '); } CreateTableAsStmt(node: t.CreateTableAsStmt, context: DeparserContext): string { const output: string[] = ['CREATE']; - + if (node.objtype === 'OBJECT_MATVIEW') { output.push('MATERIALIZED VIEW'); } else { @@ -9511,15 +9513,15 @@ export class Deparser implements DeparserVisitor { output.push('TABLE'); } } - + if (node.if_not_exists) { output.push('IF NOT EXISTS'); } - + if (node.into && node.into.rel) { output.push(this.RangeVar(node.into.rel, context)); } - + if (node.into && node.into.colNames && node.into.colNames.length > 0) { output.push('('); const colNames = ListUtils.unwrapList(node.into.colNames) @@ -9528,12 +9530,12 @@ export class Deparser implements DeparserVisitor { output.push(colNames); output.push(')'); } - + if (node.into && node.into.accessMethod) { output.push('USING'); output.push(node.into.accessMethod); } - + if (node.into && node.into.onCommit && node.into.onCommit !== 'ONCOMMIT_NOOP') { output.push('ON COMMIT'); switch (node.into.onCommit) { @@ -9548,13 +9550,13 @@ export class Deparser implements DeparserVisitor { break; } } - + output.push('AS'); - + if (node.query) { output.push(this.visit(node.query as any, context)); } - + if (node.into && node.into.options && node.into.options.length > 0) { output.push('WITH'); const options = ListUtils.unwrapList(node.into.options) @@ -9562,29 +9564,29 @@ export class Deparser implements DeparserVisitor { .join(', '); output.push(`(${options})`); } - + if (node.into && node.into.skipData) { output.push('WITH NO DATA'); } - + return output.join(' '); } RefreshMatViewStmt(node: t.RefreshMatViewStmt, context: DeparserContext): string { const output: string[] = ['REFRESH', 'MATERIALIZED', 'VIEW']; - + if (node.concurrent) { output.push('CONCURRENTLY'); } - + if (node.relation) { output.push(this.visit(node.relation as any, context)); } - + if (node.skipData) { output.push('WITH NO DATA'); } - + return output.join(' '); } @@ -9592,21 +9594,21 @@ export class Deparser implements DeparserVisitor { AccessPriv(node: t.AccessPriv, context: DeparserContext): string { const output: string[] = []; - + if (node.priv_name) { output.push(node.priv_name.toUpperCase()); } else { output.push('ALL'); } - + if (node.cols && node.cols.length > 0) { output.push('('); - const colContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AccessPriv'] }; + const colContext = context.spawn('AccessPriv'); const columns = ListUtils.unwrapList(node.cols).map(col => this.visit(col, colContext)); output.push(columns.join(', ')); output.push(')'); } - + return output.join(' '); } @@ -9619,15 +9621,15 @@ export class Deparser implements DeparserVisitor { DefineStmt(node: t.DefineStmt, context: DeparserContext): string { const output: string[] = []; - + if (!node.kind) { throw new Error('DefineStmt requires kind property'); } - + switch (node.kind) { case 'OBJECT_OPERATOR': output.push('CREATE OPERATOR'); - + if (node.defnames && node.defnames.length > 0) { const names = ListUtils.unwrapList(node.defnames).map((name, index) => { if (index === node.defnames.length - 1) { @@ -9640,7 +9642,7 @@ export class Deparser implements DeparserVisitor { }); output.push(names.join('.')); } - + if (node.definition && node.definition.length > 0) { output.push('('); const definitions = ListUtils.unwrapList(node.definition).map(def => { @@ -9648,7 +9650,7 @@ export class Deparser implements DeparserVisitor { const defElem = def.DefElem; const defName = defElem.defname; const defValue = defElem.arg; - + if (defName && defValue) { let preservedDefName; if (Deparser.needsQuotes(defName)) { @@ -9656,7 +9658,7 @@ export class Deparser implements DeparserVisitor { } else { preservedDefName = this.preserveOperatorDefElemCase(defName); } - + if ((defName.toLowerCase() === 'commutator' || defName.toLowerCase() === 'negator') && defValue.List) { const listItems = ListUtils.unwrapList(defValue.List.items); if (listItems.length === 1 && listItems[0].String) { @@ -9682,50 +9684,50 @@ export class Deparser implements DeparserVisitor { output.push(')'); } break; - + case 'OBJECT_TYPE': output.push('CREATE TYPE'); - + if (node.defnames && node.defnames.length > 0) { output.push(ListUtils.unwrapList(node.defnames).map(name => this.visit(name, context)).join('.')); } - + if (node.definition && node.definition.length > 0) { - const defineStmtContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'DefineStmt'] }; + const defineStmtContext = context.spawn('DefineStmt'); const definitions = ListUtils.unwrapList(node.definition).map(def => { return this.visit(def, defineStmtContext); }); output.push(`(${definitions.join(', ')})`); } break; - + case 'OBJECT_AGGREGATE': output.push('CREATE'); if (node.replace) { output.push('OR REPLACE'); } output.push('AGGREGATE'); - + if (node.defnames && node.defnames.length > 0) { const nameStrs = ListUtils.unwrapList(node.defnames).map(name => this.visit(name, context)); output.push(nameStrs.join('.')); } - + if (node.args && node.args.length > 0) { const args = ListUtils.unwrapList(node.args); - + // Check if this is an ordered-set aggregate (indicated by Integer(1) or empty Integer after List with FunctionParameter FUNC_PARAM_DEFAULT) const hasOrderedSetIndicator = args.some(arg => arg.Integer && arg.Integer.ival === 1); - + // Check for ORDER BY pattern: List with FunctionParameter FUNC_PARAM_DEFAULT followed by empty Integer - const hasOrderByPattern = args.length >= 2 && - args[0].List && - args[0].List.items && + const hasOrderByPattern = args.length >= 2 && + args[0].List && + args[0].List.items && args[0].List.items.length === 1 && - args[0].List.items[0].FunctionParameter && + args[0].List.items[0].FunctionParameter && args[0].List.items[0].FunctionParameter.mode === 'FUNC_PARAM_DEFAULT' && args[1].Integer && Object.keys(args[1].Integer).length === 0; - + const filteredArgs = args.filter(arg => { if (arg.Integer && (arg.Integer.ival === -1 || arg.Integer.ival === 1)) { return false; @@ -9735,7 +9737,7 @@ export class Deparser implements DeparserVisitor { } return true; }); - + if (filteredArgs.length > 0) { if (hasOrderByPattern) { // Handle ORDER BY syntax for aggregates like myavg (ORDER BY numeric) @@ -9770,7 +9772,7 @@ export class Deparser implements DeparserVisitor { const items = ListUtils.unwrapList(listArg.items); const firstItem = this.visit(items[0], context); const remainingItems = items.slice(1).map(item => this.visit(item, context)); - + output.push(`(${firstItem} ORDER BY ${remainingItems.join(', ')})`); } else if (listArg.items && listArg.items.length === 1) { // Handle single VARIADIC parameter in ordered-set context @@ -9805,14 +9807,14 @@ export class Deparser implements DeparserVisitor { } } } - + if (node.definition && node.definition.length > 0) { const definitions = ListUtils.unwrapList(node.definition).map(def => { if (def.DefElem) { const defElem = def.DefElem; const defName = defElem.defname; const defValue = defElem.arg; - + if (defName && defValue) { let preservedDefName; if (Deparser.needsQuotes(defName)) { @@ -9820,7 +9822,7 @@ export class Deparser implements DeparserVisitor { } else { preservedDefName = defName; } - + // Handle String arguments with single quotes for string literals if (defValue.String) { return `${preservedDefName} = '${defValue.String.sval}'`; @@ -9833,14 +9835,14 @@ export class Deparser implements DeparserVisitor { output.push(`(${definitions.join(', ')})`); } break; - + case 'OBJECT_TSDICTIONARY': output.push('CREATE TEXT SEARCH DICTIONARY'); - + if (node.defnames && node.defnames.length > 0) { output.push(ListUtils.unwrapList(node.defnames).map(name => this.visit(name, context)).join('.')); } - + if (node.definition && node.definition.length > 0) { output.push('('); const definitions = ListUtils.unwrapList(node.definition).map(def => { @@ -9848,7 +9850,7 @@ export class Deparser implements DeparserVisitor { const defElem = def.DefElem; const defName = defElem.defname; const defValue = defElem.arg; - + if (defName && defValue) { return `${defName} = ${this.visit(defValue, context)}`; } @@ -9859,14 +9861,14 @@ export class Deparser implements DeparserVisitor { output.push(')'); } break; - + case 'OBJECT_TSCONFIGURATION': output.push('CREATE TEXT SEARCH CONFIGURATION'); - + if (node.defnames && node.defnames.length > 0) { output.push(ListUtils.unwrapList(node.defnames).map(name => this.visit(name, context)).join('.')); } - + if (node.definition && node.definition.length > 0) { output.push('('); const definitions = ListUtils.unwrapList(node.definition).map(def => { @@ -9874,7 +9876,7 @@ export class Deparser implements DeparserVisitor { const defElem = def.DefElem; const defName = defElem.defname; const defValue = defElem.arg; - + if (defName && defValue) { return `${defName} = ${this.visit(defValue, context)}`; } @@ -9885,17 +9887,17 @@ export class Deparser implements DeparserVisitor { output.push(')'); } break; - + case 'OBJECT_TSPARSER': output.push('CREATE TEXT SEARCH PARSER'); - + if (node.defnames && node.defnames.length > 0) { const names = ListUtils.unwrapList(node.defnames) .map(name => this.visit(name, context)) .join('.'); output.push(names); } - + if (node.definition && node.definition.length > 0) { output.push('('); const definitions = ListUtils.unwrapList(node.definition).map(def => { @@ -9903,7 +9905,7 @@ export class Deparser implements DeparserVisitor { const defElem = def.DefElem; const defName = defElem.defname; const defValue = defElem.arg; - + if (defName && defValue) { return `${defName} = ${this.visit(defValue, context)}`; } @@ -9914,17 +9916,17 @@ export class Deparser implements DeparserVisitor { output.push(')'); } break; - + case 'OBJECT_TSTEMPLATE': output.push('CREATE TEXT SEARCH TEMPLATE'); - + if (node.defnames && node.defnames.length > 0) { const names = ListUtils.unwrapList(node.defnames) .map(name => this.visit(name, context)) .join('.'); output.push(names); } - + if (node.definition && node.definition.length > 0) { output.push('('); const definitions = ListUtils.unwrapList(node.definition).map(def => { @@ -9932,7 +9934,7 @@ export class Deparser implements DeparserVisitor { const defElem = def.DefElem; const defName = defElem.defname; const defValue = defElem.arg; - + if (defName && defValue) { return `${defName} = ${this.visit(defValue, context)}`; } @@ -9943,27 +9945,27 @@ export class Deparser implements DeparserVisitor { output.push(')'); } break; - + case 'OBJECT_COLLATION': output.push('CREATE COLLATION'); - + if (node.defnames && node.defnames.length > 0) { output.push(ListUtils.unwrapList(node.defnames).map(name => this.visit(name, context)).join('.')); } - + if (node.definition && node.definition.length > 0) { const definitions = ListUtils.unwrapList(node.definition).map(def => { if (def.DefElem) { const defElem = def.DefElem; const defName = defElem.defname; const defValue = defElem.arg; - + if (defName && defValue) { // Handle FROM clause for collation definitions if (defName === 'from') { return `FROM ${this.visit(defValue, context)}`; } - + // For CREATE COLLATION, ensure String nodes are quoted as string literals let valueStr; if (defValue.String) { @@ -9976,7 +9978,7 @@ export class Deparser implements DeparserVisitor { } return this.visit(def, context); }); - + // Check if we have FROM clause or parameter definitions const hasFromClause = definitions.some(def => def.startsWith('FROM ')); if (hasFromClause) { @@ -9987,101 +9989,101 @@ export class Deparser implements DeparserVisitor { } } break; - + default: throw new Error(`Unsupported DefineStmt kind: ${node.kind}`); } - + return output.join(' '); } AlterDatabaseStmt(node: t.AlterDatabaseStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'DATABASE']; - + if (node.dbname) { output.push(QuoteUtils.quote(node.dbname)); } - + if (node.options && node.options.length > 0) { const options = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, context)); output.push(options.join(' ')); } - + return output.join(' '); } AlterDatabaseRefreshCollStmt(node: t.AlterDatabaseRefreshCollStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'DATABASE']; - + if (node.dbname) { output.push(QuoteUtils.quote(node.dbname)); } - + output.push('REFRESH', 'COLLATION', 'VERSION'); - + return output.join(' '); } AlterDatabaseSetStmt(node: t.AlterDatabaseSetStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'DATABASE']; - + if (node.dbname) { output.push(QuoteUtils.quote(node.dbname)); } - + if (node.setstmt) { const setClause = this.VariableSetStmt(node.setstmt, context); output.push(setClause); } - + return output.join(' '); } DeclareCursorStmt(node: t.DeclareCursorStmt, context: DeparserContext): string { const output: string[] = ['DECLARE']; - + if (node.portalname) { output.push(QuoteUtils.quote(node.portalname)); } - + // Handle cursor options before CURSOR keyword const cursorOptions: string[] = []; if (node.options) { - + if (node.options & 2) { cursorOptions.push('SCROLL'); } else if (node.options & 4) { cursorOptions.push('NO SCROLL'); } - + // Handle other cursor options if (node.options & 1) cursorOptions.push('BINARY'); if (node.options & 8) cursorOptions.push('INSENSITIVE'); } - + if (cursorOptions.length > 0) { output.push(...cursorOptions); } - + output.push('CURSOR'); - + // Handle WITH HOLD after CURSOR keyword (0x0020 = 32) if (node.options && (node.options & 32)) { output.push('WITH HOLD'); } - + output.push('FOR'); - + if (node.query) { output.push(this.visit(node.query, context)); } - + return output.join(' '); } PublicationObjSpec(node: t.PublicationObjSpec, context: DeparserContext): string { const output: string[] = []; - + if (node.pubobjtype === 'PUBLICATIONOBJ_TABLE') { output.push('TABLE'); if (node.pubtable) { @@ -10095,39 +10097,39 @@ export class Deparser implements DeparserVisitor { } else if (node.pubobjtype === 'PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA') { output.push('TABLES IN SCHEMA CURRENT_SCHEMA'); } - + return output.join(' '); } PublicationTable(node: t.PublicationTable, context: DeparserContext): string { const output: string[] = []; - + if (node.relation) { output.push(this.RangeVar(node.relation, context)); } - + if (node.columns && node.columns.length > 0) { const columns = ListUtils.unwrapList(node.columns).map(col => this.visit(col, context)); output.push(`(${columns.join(', ')})`); } - + if (node.whereClause) { output.push('WHERE'); output.push(this.visit(node.whereClause, context)); } - + return output.join(' '); } CreateAmStmt(node: t.CreateAmStmt, context: DeparserContext): string { const output: string[] = ['CREATE', 'ACCESS', 'METHOD']; - + if (node.amname) { output.push(QuoteUtils.quote(node.amname)); } - + output.push('TYPE'); - + switch (node.amtype) { case 'i': output.push('INDEX'); @@ -10140,7 +10142,7 @@ export class Deparser implements DeparserVisitor { output.push(node.amtype || ''); break; } - + if (node.handler_name && node.handler_name.length > 0) { output.push('HANDLER'); const handlerName = ListUtils.unwrapList(node.handler_name) @@ -10148,28 +10150,28 @@ export class Deparser implements DeparserVisitor { .join('.'); output.push(handlerName); } - + return output.join(' '); } IntoClause(node: t.IntoClause, context: DeparserContext): string { const output: string[] = []; - + if (node.rel) { output.push(this.RangeVar(node.rel, context)); } - + if (node.colNames && node.colNames.length > 0) { const columns = ListUtils.unwrapList(node.colNames) .map(col => this.visit(col, context)) .join(', '); output.push(`(${columns})`); } - + if (node.accessMethod) { output.push('USING', node.accessMethod); } - + if (node.options && node.options.length > 0) { output.push('WITH'); const options = ListUtils.unwrapList(node.options) @@ -10177,7 +10179,7 @@ export class Deparser implements DeparserVisitor { .join(', '); output.push(`(${options})`); } - + if (node.onCommit && node.onCommit !== 'ONCOMMIT_NOOP') { output.push('ON COMMIT'); switch (node.onCommit) { @@ -10192,29 +10194,29 @@ export class Deparser implements DeparserVisitor { break; } } - + if (node.tableSpaceName) { output.push('TABLESPACE', QuoteUtils.quote(node.tableSpaceName)); } - + return output.join(' '); } OnConflictExpr(node: t.OnConflictExpr, context: DeparserContext): string { const output: string[] = ['ON CONFLICT']; - + if (node.arbiterElems && node.arbiterElems.length > 0) { const arbiters = ListUtils.unwrapList(node.arbiterElems) .map(elem => this.visit(elem, context)) .join(', '); output.push(`(${arbiters})`); } - + if (node.arbiterWhere) { output.push('WHERE'); output.push(this.visit(node.arbiterWhere, context)); } - + if (node.action === 'ONCONFLICT_NOTHING') { output.push('DO NOTHING'); } else if (node.action === 'ONCONFLICT_UPDATE') { @@ -10225,13 +10227,13 @@ export class Deparser implements DeparserVisitor { .join(', '); output.push(updates); } - + if (node.onConflictWhere) { output.push('WHERE'); output.push(this.visit(node.onConflictWhere, context)); } } - + return output.join(' '); } @@ -10241,14 +10243,14 @@ export class Deparser implements DeparserVisitor { CreateOpClassItem(node: t.CreateOpClassItem, context: DeparserContext): string { const output: string[] = []; - + if (node.itemtype === 1) { output.push('OPERATOR'); // For operators, always include the number (default to 0 if undefined) const operatorNumber = node.number !== undefined ? node.number : 0; output.push(operatorNumber.toString()); if (node.name) { - const opClassContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateOpClassItem'] }; + const opClassContext = context.spawn('CreateOpClassItem'); output.push(this.ObjectWithArgs(node.name, opClassContext)); } } else if (node.itemtype === 2) { @@ -10257,7 +10259,7 @@ export class Deparser implements DeparserVisitor { const functionNumber = node.number !== undefined ? node.number : 0; output.push(functionNumber.toString()); if (node.name) { - const opClassContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateOpClassItem'] }; + const opClassContext = context.spawn('CreateOpClassItem'); output.push(this.ObjectWithArgs(node.name, opClassContext)); } }else if (node.itemtype === 3) { @@ -10266,7 +10268,7 @@ export class Deparser implements DeparserVisitor { output.push(this.TypeName(node.storedtype, context)); } } - + if (node.order_family && node.order_family.length > 0) { output.push('FOR ORDER BY'); const orderFamily = ListUtils.unwrapList(node.order_family) @@ -10274,14 +10276,14 @@ export class Deparser implements DeparserVisitor { .join('.'); output.push(orderFamily); } - + if (node.class_args && node.class_args.length > 0) { const classArgs = ListUtils.unwrapList(node.class_args) .map(arg => this.visit(arg, context)) .join(', '); output.push(`(${classArgs})`); } - + return output.join(' '); } @@ -10294,10 +10296,10 @@ export class Deparser implements DeparserVisitor { TableFunc(node: t.TableFunc, context: DeparserContext): string { const output: string[] = []; - + if (node.functype === 'TFT_XMLTABLE') { output.push('XMLTABLE'); - + if (node.ns_names && node.ns_names.length > 0) { output.push('XMLNAMESPACES'); const namespaces = ListUtils.unwrapList(node.ns_names) @@ -10305,16 +10307,16 @@ export class Deparser implements DeparserVisitor { .join(', '); output.push(`(${namespaces})`); } - + if (node.rowexpr) { output.push(`(${this.visit(node.rowexpr, context)})`); } - + if (node.docexpr) { output.push('PASSING'); output.push(this.visit(node.docexpr, context)); } - + if (node.colexprs && node.colexprs.length > 0) { output.push('COLUMNS'); const columns = ListUtils.unwrapList(node.colexprs) @@ -10324,16 +10326,16 @@ export class Deparser implements DeparserVisitor { } } else if (node.functype === 'TFT_JSON_TABLE') { output.push('JSON_TABLE'); - + if (node.docexpr) { output.push(`(${this.visit(node.docexpr, context)})`); } - + if (node.rowexpr) { output.push(','); output.push(`'${this.visit(node.rowexpr, context)}'`); } - + if (node.colexprs && node.colexprs.length > 0) { output.push('COLUMNS'); const columns = ListUtils.unwrapList(node.colexprs) @@ -10342,26 +10344,26 @@ export class Deparser implements DeparserVisitor { output.push(`(${columns})`); } } - + return output.join(' '); } RangeTableFunc(node: t.RangeTableFunc, context: DeparserContext): string { const output: string[] = []; - + if (node.lateral) { output.push('LATERAL'); } - + if (node.docexpr) { output.push(this.visit(node.docexpr, context)); } - + if (node.rowexpr) { output.push('PASSING'); output.push(this.visit(node.rowexpr, context)); } - + if (node.columns && node.columns.length > 0) { output.push('COLUMNS'); const columns = ListUtils.unwrapList(node.columns) @@ -10369,74 +10371,74 @@ export class Deparser implements DeparserVisitor { .join(', '); output.push(`(${columns})`); } - + if (node.alias) { output.push(this.Alias(node.alias, context)); } - + return output.join(' '); } RangeTableFuncCol(node: t.RangeTableFuncCol, context: DeparserContext): string { const output: string[] = []; - + if (node.colname) { output.push(QuoteUtils.quote(node.colname)); } - + if (node.for_ordinality) { output.push('FOR ORDINALITY'); } else if (node.typeName) { output.push(this.TypeName(node.typeName, context)); } - + if (node.colexpr) { output.push('PATH'); output.push(`'${this.visit(node.colexpr, context)}'`); } - + if (node.coldefexpr) { output.push('DEFAULT'); output.push(this.visit(node.coldefexpr, context)); } - + return output.join(' '); } JsonArrayQueryConstructor(node: t.JsonArrayQueryConstructor, context: DeparserContext): string { const output: string[] = ['JSON_ARRAYAGG']; - + if (node.query) { output.push(`(${this.visit(node.query, context)})`); } - + if (node.format) { output.push('FORMAT JSON'); } - + if (node.output) { output.push('RETURNING TEXT'); } - + if (node.absent_on_null) { output.push('ABSENT ON NULL'); } else { output.push('NULL ON NULL'); } - + return output.join(' '); } RangeFunction(node: t.RangeFunction, context: DeparserContext): string { const output: string[] = []; - + if (node.lateral) { output.push('LATERAL'); } - + if (node.is_rowsfrom) { output.push('ROWS FROM'); - + if (node.functions && node.functions.length > 0) { const functionStrs = ListUtils.unwrapList(node.functions) .filter(func => func != null) @@ -10472,7 +10474,7 @@ export class Deparser implements DeparserVisitor { } }) .filter(str => str && str.trim()); - + if (functionStrs.length > 0) { output.push(`(${functionStrs.join(', ')})`); } @@ -10506,11 +10508,11 @@ export class Deparser implements DeparserVisitor { } } } - + if (node.ordinality) { output.push('WITH ORDINALITY'); } - + // Handle alias and column definitions together for proper PostgreSQL syntax if (node.alias) { if (node.coldeflist && node.coldeflist.length > 0) { @@ -10529,7 +10531,7 @@ export class Deparser implements DeparserVisitor { .filter(str => str && str.trim()); output.push(`AS (${coldefs.join(', ')})`); } - + return output.join(' '); } @@ -10545,9 +10547,9 @@ export class Deparser implements DeparserVisitor { return 'XMLPI()'; } } - + const output: string[] = []; - + switch (node.op) { case 'IS_XMLCONCAT': output.push('XMLCONCAT'); @@ -10598,11 +10600,11 @@ export class Deparser implements DeparserVisitor { if (node.args && node.args.length > 0) { const args = ListUtils.unwrapList(node.args); const rootParts: string[] = []; - + if (args[0]) { rootParts.push(this.visit(args[0], context)); } - + if (args[1]) { const versionArg = args[1]; if (versionArg.A_Const && versionArg.A_Const.isnull) { @@ -10611,7 +10613,7 @@ export class Deparser implements DeparserVisitor { rootParts.push(`version ${this.visit(versionArg, context)}`); } } - + if (args[2]) { const standaloneArg = args[2]; if (standaloneArg.A_Const && standaloneArg.A_Const.ival !== undefined) { @@ -10629,7 +10631,7 @@ export class Deparser implements DeparserVisitor { rootParts.push(`STANDALONE ${this.visit(standaloneArg, context)}`); } } - + if (rootParts.length > 0) { output.push(`(${rootParts.join(', ')})`); } @@ -10649,20 +10651,20 @@ export class Deparser implements DeparserVisitor { default: throw new Error(`Unsupported XmlExpr op: ${node.op}`); } - + // Handle name and args for operations that don't have special handling if (node.op !== 'IS_XMLELEMENT' && node.op !== 'IS_XMLPARSE' && node.op !== 'IS_XMLROOT' && node.op !== 'IS_DOCUMENT') { if (node.name) { const quotedName = QuoteUtils.quote(node.name); output.push(`NAME ${quotedName}`); } - + if (node.args && node.args.length > 0) { const argStrs = ListUtils.unwrapList(node.args).map(arg => this.visit(arg, context)); output.push(`(${argStrs.join(', ')})`); } } - + if (node.named_args && node.named_args.length > 0 && node.op !== 'IS_XMLELEMENT') { const namedArgStrs = ListUtils.unwrapList(node.named_args).map(arg => this.visit(arg, context)); if (node.op === 'IS_XMLFOREST') { @@ -10671,7 +10673,7 @@ export class Deparser implements DeparserVisitor { output.push(`XMLATTRIBUTES(${namedArgStrs.join(', ')})`); } } - + return output.join(' '); } @@ -10705,50 +10707,50 @@ export class Deparser implements DeparserVisitor { RangeTableSample(node: t.RangeTableSample, context: DeparserContext): string { const output: string[] = []; - + if (node.relation) { output.push(this.visit(node.relation as any, context)); } - + output.push('TABLESAMPLE'); - + if (node.method && node.method.length > 0) { const methodParts = node.method.map((m: any) => this.visit(m, context)); output.push(methodParts.join('.')); } - + if (node.args && node.args.length > 0) { const argStrs = node.args.map((arg: any) => this.visit(arg, context)); output.push(`(${argStrs.join(', ')})`); } - + if (node.repeatable) { output.push('REPEATABLE'); output.push(`(${this.visit(node.repeatable as any, context)})`); } - + return output.join(' '); } XmlSerialize(node: t.XmlSerialize, context: DeparserContext): string { const output: string[] = ['XMLSERIALIZE']; - + output.push('('); - + if (node.typeName) { if (node.xmloption === 'XMLOPTION_DOCUMENT') { output.push('DOCUMENT'); } else { output.push('CONTENT'); } - + output.push(this.visit(node.expr as any, context)); output.push('AS'); output.push(this.TypeName(node.typeName, context)); } - + output.push(')'); - + return output.join(' '); } @@ -10756,35 +10758,35 @@ export class Deparser implements DeparserVisitor { if (!node || !Array.isArray(node)) { return ''; } - + const output: string[] = ['WITH']; - + // Check if any CTE is recursive by examining the first CTE's structure if (node.length > 0 && node[0] && node[0].CommonTableExpr && node[0].CommonTableExpr.recursive) { output.push('RECURSIVE'); } - + const cteStrs = node.map(cte => this.visit(cte, context)); output.push(cteStrs.join(', ')); - + return output.join(' '); } RuleStmt(node: t.RuleStmt, context: DeparserContext): string { const output: string[] = ['CREATE']; - + if (node.replace) { output.push('OR REPLACE'); } - + output.push('RULE'); - + if (node.rulename) { output.push(QuoteUtils.quote(node.rulename)); } - + output.push('AS ON'); - + if (node.event) { switch (node.event) { case 'CMD_SELECT': @@ -10803,25 +10805,25 @@ export class Deparser implements DeparserVisitor { output.push(node.event.toString()); } } - + output.push('TO'); - + if (node.relation) { // Handle relation node directly as RangeVar since it contains the RangeVar properties output.push(this.RangeVar(node.relation as any, context)); } - + if (node.whereClause) { output.push('WHERE'); output.push(this.visit(node.whereClause, context)); } - + output.push('DO'); - + if (node.instead) { output.push('INSTEAD'); } - + if (node.actions && node.actions.length > 0) { if (node.actions.length === 1) { output.push(this.visit(node.actions[0], context)); @@ -10834,27 +10836,27 @@ export class Deparser implements DeparserVisitor { } else { output.push('NOTHING'); } - + return output.join(' '); } RangeSubselect(node: t.RangeSubselect, context: DeparserContext): string { const output: string[] = []; - + if (node.lateral) { output.push('LATERAL'); } - + if (node.subquery) { output.push('('); output.push(this.visit(node.subquery, context)); output.push(')'); } - + if (node.alias) { output.push(this.Alias(node.alias, context)); } - + return output.join(' '); } @@ -10945,28 +10947,28 @@ export class Deparser implements DeparserVisitor { GroupingFunc(node: t.GroupingFunc, context: DeparserContext): string { const output: string[] = ['GROUPING']; - + if (node.args && node.args.length > 0) { const argStrs = ListUtils.unwrapList(node.args).map(arg => this.visit(arg, context)); output.push(`(${argStrs.join(', ')})`); } else { output.push('()'); } - + return output.join(''); } MultiAssignRef(node: t.MultiAssignRef, context: DeparserContext): string { const output: string[] = []; - + if (node.source) { output.push(this.visit(node.source, context)); } - + if (node.colno > 0) { output.push(`[${node.colno}]`); } - + return output.join(''); } @@ -10976,32 +10978,32 @@ export class Deparser implements DeparserVisitor { CurrentOfExpr(node: t.CurrentOfExpr, context: DeparserContext): string { const output: string[] = ['CURRENT OF']; - + if (node.cursor_name) { output.push(QuoteUtils.quote(node.cursor_name)); } - + if (node.cursor_param > 0) { output.push(`$${node.cursor_param}`); } - + return output.join(' '); } TableLikeClause(node: t.TableLikeClause, context: DeparserContext): string { const output: string[] = ['LIKE']; - + if (node.relation) { output.push(this.visit(node.relation as any, context)); } - + if (node.options && typeof node.options === 'number') { // Handle special case for INCLUDING ALL (all bits set) if (node.options === 2147483647 || node.options === 0x7FFFFFFF) { output.push('INCLUDING ALL'); } else { const optionStrs: string[] = []; - + // Handle bitfield options for CREATE TABLE LIKE if (node.options & 0x01) optionStrs.push('INCLUDING COMMENTS'); if (node.options & 0x04) optionStrs.push('INCLUDING CONSTRAINTS'); @@ -11011,41 +11013,41 @@ export class Deparser implements DeparserVisitor { if (node.options & 0x40) optionStrs.push('INCLUDING INDEXES'); if (node.options & 0x80) optionStrs.push('INCLUDING STATISTICS'); if (node.options & 0x100) optionStrs.push('INCLUDING STORAGE'); - + if (optionStrs.length > 0) { output.push(optionStrs.join(' ')); } } } - + return output.join(' '); } AlterFunctionStmt(node: t.AlterFunctionStmt, context: DeparserContext): string { const output: string[] = ['ALTER']; - + if (node.objtype === 'OBJECT_PROCEDURE') { output.push('PROCEDURE'); } else { output.push('FUNCTION'); } - + if (node.func) { output.push(this.ObjectWithArgs(node.func, context)); } - + if (node.actions && node.actions.length > 0) { - const alterFunctionContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterFunctionStmt'] }; + const alterFunctionContext = context.spawn('AlterFunctionStmt'); const actionStrs = ListUtils.unwrapList(node.actions).map(action => this.visit(action, alterFunctionContext)); output.push(actionStrs.join(' ')); } - + return output.join(' '); } AlterObjectSchemaStmt(node: t.AlterObjectSchemaStmt, context: DeparserContext): string { const output: string[] = ['ALTER']; - + switch (node.objectType) { case 'OBJECT_TABLE': output.push('TABLE'); @@ -11107,11 +11109,11 @@ export class Deparser implements DeparserVisitor { default: output.push(node.objectType.toString()); } - + if (node.missing_ok) { output.push('IF EXISTS'); } - + if (node.relation && (node.objectType === 'OBJECT_TABLE' || node.objectType === 'OBJECT_FOREIGN_TABLE' || node.objectType === 'OBJECT_MATVIEW')) { output.push(this.RangeVar(node.relation, context)); } else if (node.object) { @@ -11219,30 +11221,30 @@ export class Deparser implements DeparserVisitor { output.push(this.visit(node.object as any, context)); } } - + output.push('SET SCHEMA'); - + if (node.newschema) { output.push(QuoteUtils.quote(node.newschema)); } - + return output.join(' '); } AlterRoleSetStmt(node: t.AlterRoleSetStmt, context: DeparserContext): string { const output: string[] = ['ALTER', 'ROLE']; - + if (node.role) { output.push(this.RoleSpec(node.role, context)); } else { output.push('ALL'); } - + if (node.database) { output.push('IN DATABASE'); output.push(this.quoteIfNeeded(node.database)); } - + if (node.setstmt) { if (node.setstmt.kind === 'VAR_RESET') { output.push('RESET'); @@ -11254,7 +11256,7 @@ export class Deparser implements DeparserVisitor { if (node.setstmt.name) { output.push(node.setstmt.name); } - + if (node.setstmt.args && node.setstmt.args.length > 0) { output.push('TO'); const args = ListUtils.unwrapList(node.setstmt.args) @@ -11264,19 +11266,19 @@ export class Deparser implements DeparserVisitor { } } } - + return output.join(' '); } CreateForeignTableStmt(node: t.CreateForeignTableStmt, context: DeparserContext): string { const output: string[] = ['CREATE FOREIGN TABLE']; - + if (node.base && node.base.relation) { - const relationContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateForeignTableStmt'] }; + const relationContext = context.spawn('CreateForeignTableStmt'); // Handle relation node directly as RangeVar since it contains the RangeVar properties output.push(this.RangeVar(node.base.relation as any, relationContext)); } - + if (node.base && node.base.tableElts) { const elementStrs = ListUtils.unwrapList(node.base.tableElts).map(el => this.visit(el, context)); output.push(`(${elementStrs.join(', ')})`); @@ -11284,7 +11286,7 @@ export class Deparser implements DeparserVisitor { // Only add empty parentheses if this is not a partition table output.push('()'); } - + if (node.base && node.base.inhRelations && node.base.inhRelations.length > 0) { if (node.base.partbound) { const inheritStrs = ListUtils.unwrapList(node.base.inhRelations).map(rel => this.visit(rel, context)); @@ -11297,18 +11299,18 @@ export class Deparser implements DeparserVisitor { output.push(`INHERITS (${inheritStrs.join(', ')})`); } } - + if (node.servername) { output.push('SERVER'); output.push(QuoteUtils.quote(node.servername)); } - + if (node.options && node.options.length > 0) { - const foreignTableContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, 'CreateForeignTableStmt'] }; + const foreignTableContext = context.spawn('CreateForeignTableStmt'); const optionStrs = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, foreignTableContext)); output.push(`OPTIONS (${optionStrs.join(', ')})`); } - + return output.join(' '); } diff --git a/packages/deparser/src/visitors/base.ts b/packages/deparser/src/visitors/base.ts index 55e8cf6e..a140b3a0 100644 --- a/packages/deparser/src/visitors/base.ts +++ b/packages/deparser/src/visitors/base.ts @@ -1,12 +1,153 @@ import { Node } from '@pgsql/types'; +import { SqlFormatter } from '../utils/sql-formatter'; -export interface DeparserContext { +export interface DeparserContextOptions { isStringLiteral?: boolean; - parentNodeTypes: string[]; + parentNodeTypes?: string[]; indentLevel?: number; + prettyMode?: boolean; + formatter?: SqlFormatter; + select?: boolean; + from?: boolean; + group?: boolean; + sort?: boolean; + insertColumns?: boolean; + update?: boolean; + bool?: boolean; + isColumnConstraint?: boolean; + isDomainConstraint?: boolean; + alterColumnOptions?: boolean; + alterTableOptions?: boolean; + isEnumValue?: boolean; + objtype?: string; + subtype?: string; [key: string]: any; } +export class DeparserContext { + readonly indentLevel: number; + readonly prettyMode: boolean; + readonly isStringLiteral?: boolean; + readonly parentNodeTypes: string[]; + private readonly formatter: SqlFormatter; + readonly select?: boolean; + readonly from?: boolean; + readonly group?: boolean; + readonly sort?: boolean; + readonly insertColumns?: boolean; + readonly update?: boolean; + readonly bool?: boolean; + readonly isColumnConstraint?: boolean; + readonly isDomainConstraint?: boolean; + readonly alterColumnOptions?: boolean; + readonly alterTableOptions?: boolean; + readonly isEnumValue?: boolean; + readonly objtype?: string; + readonly subtype?: string; + readonly [key: string]: any; + + constructor({ + indentLevel = 0, + prettyMode = false, + isStringLiteral, + parentNodeTypes = [], + formatter, + select, + from, + group, + sort, + insertColumns, + update, + bool, + isColumnConstraint, + isDomainConstraint, + alterColumnOptions, + alterTableOptions, + isEnumValue, + objtype, + subtype, + ...rest + }: DeparserContextOptions = {}) { + this.indentLevel = indentLevel; + this.prettyMode = prettyMode; + this.isStringLiteral = isStringLiteral; + this.parentNodeTypes = parentNodeTypes; + this.formatter = formatter || new SqlFormatter('\n', ' ', prettyMode); + this.select = select; + this.from = from; + this.group = group; + this.sort = sort; + this.insertColumns = insertColumns; + this.update = update; + this.bool = bool; + this.isColumnConstraint = isColumnConstraint; + this.isDomainConstraint = isDomainConstraint; + this.alterColumnOptions = alterColumnOptions; + this.alterTableOptions = alterTableOptions; + this.isEnumValue = isEnumValue; + this.objtype = objtype; + this.subtype = subtype; + + Object.assign(this, rest); + } + + spawn(nodeType: string, overrides: Partial = {}): DeparserContext { + return new DeparserContext({ + indentLevel: this.indentLevel, + prettyMode: this.prettyMode, + isStringLiteral: this.isStringLiteral, + parentNodeTypes: [...this.parentNodeTypes, nodeType], + formatter: this.formatter, + select: this.select, + from: this.from, + group: this.group, + sort: this.sort, + insertColumns: this.insertColumns, + update: this.update, + bool: this.bool, + isColumnConstraint: this.isColumnConstraint, + isDomainConstraint: this.isDomainConstraint, + alterColumnOptions: this.alterColumnOptions, + alterTableOptions: this.alterTableOptions, + isEnumValue: this.isEnumValue, + objtype: this.objtype, + subtype: this.subtype, + ...overrides, + }); + } + + indent(text: string, count?: number): string { + if (!this.prettyMode) { + return text; + } + const indentCount = count !== undefined ? count : this.indentLevel + 1; + return this.formatter.indent(text, indentCount); + } + + indentToCurrentLevel(text: string): string { + if (!this.prettyMode) { + return text; + } + return this.formatter.indent(text, this.indentLevel); + } + + newline(): string { + return this.formatter.newline(); + } + + parens(content: string): string { + return this.formatter.parens(content); + } + + format(parts: string[], separator?: string): string { + return this.formatter.format(parts, separator); + } + + isPretty(): boolean { + return this.formatter.isPretty(); + } +} + export interface DeparserVisitor { visit(node: Node, context?: DeparserContext): string; }