From d8e1041d8328956c25424a876c5344210f863659 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Jun 2025 09:21:25 +0000 Subject: [PATCH 01/10] refactor: replace parentNodeTypes usage with DeparserContext.spawn() method - Replace parentNodeTypes=[] with new DeparserContext() - Replace parentNodeTypes=[...context.parentNodeTypes, nodeType] with context.spawn(nodeType) - Replace {...context, property: true} patterns with context.spawn(methodName, {property: true}) - Fix context propagation issues in IndexElem and CopyStmt methods - Ensure proper context spawning for GIN index parameters and COPY statement WITH clauses - All 279 test suites continue to pass with the new context system Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 197 ++++++++++++------------- packages/deparser/src/visitors/base.ts | 108 +++++++++++++- 2 files changed, 202 insertions(+), 103 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 11472383..5e4d2181 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -195,7 +195,7 @@ export class Deparser implements DeparserVisitor { return delimiter; } - deparse(node: Node, context: DeparserContext = { parentNodeTypes: [] }): string | null { + deparse(node: Node, context: DeparserContext = new DeparserContext({})): string | null { if (node == null) { return null; } @@ -212,7 +212,7 @@ export class Deparser implements DeparserVisitor { } } - visit(node: Node, context: DeparserContext = { parentNodeTypes: [] }): string { + visit(node: Node, context: DeparserContext = new DeparserContext({})): string { const nodeType = this.getNodeType(node); // Handle empty objects @@ -224,11 +224,7 @@ export class Deparser implements DeparserVisitor { 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; } @@ -347,7 +343,7 @@ 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); } else { @@ -358,7 +354,7 @@ export class Deparser implements DeparserVisitor { 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)); } else { @@ -372,7 +368,7 @@ export class Deparser implements DeparserVisitor { if (this.formatter.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)) { @@ -388,7 +384,7 @@ 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; } @@ -400,7 +396,7 @@ export class Deparser implements DeparserVisitor { } } 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,7 +410,7 @@ 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()); } @@ -466,7 +462,7 @@ export class Deparser implements DeparserVisitor { if (this.formatter.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; } @@ -478,7 +474,7 @@ export class Deparser implements DeparserVisitor { } 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); } @@ -513,7 +509,7 @@ export class Deparser implements DeparserVisitor { if (this.formatter.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; } @@ -525,7 +521,7 @@ export class Deparser implements DeparserVisitor { } 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); } @@ -776,7 +772,7 @@ export class Deparser implements DeparserVisitor { if (n.String) { return n.String.sval || n.String.str; } - return this.visit(n, { parentNodeTypes: [] }); + return this.visit(n, new DeparserContext({})); }); if (parts.length > 1) { @@ -938,7 +934,7 @@ 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()) { @@ -993,7 +989,7 @@ export class Deparser implements DeparserVisitor { 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(', ')); } @@ -1062,7 +1058,7 @@ export class Deparser implements DeparserVisitor { 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); } } @@ -1267,7 +1263,7 @@ export class Deparser implements DeparserVisitor { 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 @@ -2272,7 +2268,7 @@ export class Deparser implements DeparserVisitor { 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)); @@ -2558,7 +2554,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 +2583,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 +2595,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); @@ -3086,8 +3082,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, new DeparserContext({}))} PRECEDING`); + boundsParts.push(`AND ${this.visit(node.endOffset, new DeparserContext({}))} FOLLOWING`); } } else if (frameOptions === 1557) { boundsParts.push('CURRENT ROW'); @@ -3095,7 +3091,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, new DeparserContext({}))} FOLLOWING`); } } else if (frameOptions === 1058) { return null; @@ -3103,11 +3099,11 @@ 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, new DeparserContext({}))} PRECEDING`); } else if (frameOptions & 0x800) { // FRAMEOPTION_START_VALUE_FOLLOWING - boundsParts.push(`${this.visit(node.startOffset, { parentNodeTypes: [] })} FOLLOWING`); + boundsParts.push(`${this.visit(node.startOffset, new DeparserContext({}))} FOLLOWING`); } else { - boundsParts.push(`${this.visit(node.startOffset, { parentNodeTypes: [] })} PRECEDING`); + boundsParts.push(`${this.visit(node.startOffset, new DeparserContext({}))} PRECEDING`); } } else if (frameOptions & 0x10) { // FRAMEOPTION_START_UNBOUNDED_PRECEDING boundsParts.push('UNBOUNDED PRECEDING'); @@ -3119,11 +3115,11 @@ export class Deparser implements DeparserVisitor { 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, new DeparserContext({}))} 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, new DeparserContext({}))} FOLLOWING`); } else { - boundsParts.push(`AND ${this.visit(node.endOffset, { parentNodeTypes: [] })} FOLLOWING`); + boundsParts.push(`AND ${this.visit(node.endOffset, new DeparserContext({}))} FOLLOWING`); } } } else if (frameOptions & 0x80) { // FRAMEOPTION_END_UNBOUNDED_FOLLOWING @@ -3313,7 +3309,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 +3320,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})`; @@ -3420,7 +3416,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 +3438,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 ''; @@ -3488,7 +3484,7 @@ export class Deparser implements DeparserVisitor { } 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(', ')})`); @@ -3565,7 +3561,7 @@ export class Deparser implements DeparserVisitor { } 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(', '))); @@ -3609,7 +3605,7 @@ 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(', ')})`; } @@ -4477,7 +4473,7 @@ export class Deparser implements DeparserVisitor { 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 +4593,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})`); } @@ -4649,7 +4645,7 @@ export class Deparser implements DeparserVisitor { } const alterContext = node.objtype === 'OBJECT_TYPE' - ? { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterTypeStmt'] } + ? context.spawn('AlterTypeStmt') : context; if (node.relation) { @@ -4709,7 +4705,7 @@ export class Deparser implements DeparserVisitor { 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(', ')})`); } @@ -4717,7 +4713,7 @@ export class Deparser implements DeparserVisitor { 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); @@ -4822,7 +4818,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 +4830,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 +4922,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 +4938,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 +5175,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 +5214,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(', '); @@ -5327,7 +5323,7 @@ export class Deparser implements DeparserVisitor { } 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); } @@ -5425,7 +5421,7 @@ export class Deparser implements DeparserVisitor { 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(', '); @@ -5487,9 +5483,8 @@ export class Deparser implements DeparserVisitor { } 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'); @@ -5524,7 +5519,7 @@ export class Deparser implements DeparserVisitor { 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 @@ -5542,13 +5537,13 @@ export class Deparser implements DeparserVisitor { 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')) { @@ -5606,7 +5601,7 @@ 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}'` @@ -5616,7 +5611,7 @@ export class Deparser implements DeparserVisitor { } 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')) { @@ -5704,7 +5699,7 @@ export class Deparser implements DeparserVisitor { 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 +5708,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'; @@ -5781,7 +5776,7 @@ export class Deparser implements DeparserVisitor { 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)) { @@ -6100,7 +6095,7 @@ export class Deparser implements DeparserVisitor { 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(', '); @@ -6138,7 +6133,7 @@ export class Deparser implements DeparserVisitor { } 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(', '); @@ -6160,7 +6155,7 @@ export class Deparser implements DeparserVisitor { } 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(' '); @@ -6178,7 +6173,7 @@ export class Deparser implements DeparserVisitor { } 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(' '); @@ -6196,7 +6191,7 @@ export class Deparser implements DeparserVisitor { } 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(' '); @@ -6205,7 +6200,7 @@ export class Deparser implements DeparserVisitor { 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(', '); @@ -6323,7 +6318,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 +6344,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; @@ -6724,7 +6719,7 @@ 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)); } } @@ -6911,7 +6906,7 @@ export class Deparser implements DeparserVisitor { 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(', ')})`); } @@ -7133,7 +7128,7 @@ export class Deparser implements DeparserVisitor { 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[] = []; @@ -7484,7 +7479,7 @@ export class Deparser implements DeparserVisitor { 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('.'); } @@ -7527,7 +7522,7 @@ export class Deparser implements DeparserVisitor { 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(', ')})`); } @@ -7543,14 +7538,14 @@ export class Deparser implements DeparserVisitor { } 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(', ')})`); } @@ -7584,7 +7579,7 @@ export class Deparser implements DeparserVisitor { 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(')'); @@ -7607,7 +7602,7 @@ export class Deparser implements DeparserVisitor { 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(')'); @@ -7633,7 +7628,7 @@ export class Deparser implements DeparserVisitor { 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(', ')})`); } @@ -7708,7 +7703,7 @@ export class Deparser implements DeparserVisitor { } 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(', ')})`); } @@ -7755,7 +7750,7 @@ export class Deparser implements DeparserVisitor { 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(', ')})`); } @@ -8037,7 +8032,7 @@ export class Deparser implements DeparserVisitor { output.push(this.RangeVar(node.relation, context)); } else if (node.relation) { const rangeVarContext = node.relationType === 'OBJECT_TYPE' - ? { ...context, parentNodeTypes: [...context.parentNodeTypes, 'AlterTypeStmt'] } + ? context.spawn('AlterTypeStmt') : context; // Add ON keyword for policy operations @@ -8222,7 +8217,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(', '); @@ -8806,7 +8801,7 @@ export class Deparser implements DeparserVisitor { } 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(', '); @@ -8889,7 +8884,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(', '); @@ -8934,7 +8929,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 '); @@ -9172,7 +9167,7 @@ export class Deparser implements DeparserVisitor { } 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 => { @@ -9213,7 +9208,7 @@ export class Deparser implements DeparserVisitor { } 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 => { @@ -9245,7 +9240,7 @@ export class Deparser implements DeparserVisitor { 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)); } @@ -9366,7 +9361,7 @@ export class Deparser implements DeparserVisitor { } 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) { @@ -9601,7 +9596,7 @@ export class Deparser implements DeparserVisitor { 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(')'); @@ -9691,7 +9686,7 @@ export class Deparser implements DeparserVisitor { } 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); }); @@ -10248,7 +10243,7 @@ export class Deparser implements DeparserVisitor { 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 +10252,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) { @@ -11035,7 +11030,7 @@ export class Deparser implements DeparserVisitor { } 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(' ')); } @@ -11272,7 +11267,7 @@ export class Deparser implements DeparserVisitor { 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)); } @@ -11304,7 +11299,7 @@ export class Deparser implements DeparserVisitor { } 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(', ')})`); } diff --git a/packages/deparser/src/visitors/base.ts b/packages/deparser/src/visitors/base.ts index 55e8cf6e..bfb450da 100644 --- a/packages/deparser/src/visitors/base.ts +++ b/packages/deparser/src/visitors/base.ts @@ -1,12 +1,116 @@ import { Node } from '@pgsql/types'; -export interface DeparserContext { +export interface DeparserContextOptions { isStringLiteral?: boolean; - parentNodeTypes: string[]; + parentNodeTypes?: string[]; indentLevel?: number; + prettyMode?: boolean; + 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[]; + 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 = [], + 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.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], + 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, + }); + } +} + export interface DeparserVisitor { visit(node: Node, context?: DeparserContext): string; } From 3a97f5c1655571c2d144894725e2af1c59fb0aa9 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Jun 2025 09:31:33 +0000 Subject: [PATCH 02/10] refactor: update visit method to spawn context for every node type - Change visit method to use context.spawn(nodeType) when calling node methods - Ensures all methods receive properly contextualized calls with nodeType in parentNodeTypes - Makes context spawning more consistent throughout the entire deparser - All 279 test suites continue to pass with this more thorough approach Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 5e4d2181..49ff5f2e 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -224,7 +224,7 @@ export class Deparser implements DeparserVisitor { const methodName = nodeType as keyof this; if (typeof this[methodName] === 'function') { - const result = (this[methodName] as any)(nodeData, context); + const result = (this[methodName] as any)(nodeData, context.spawn(nodeType)); return result; } From 14326c3c79587fdab1be6103fe8c054a15ca315f Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Jun 2025 09:51:16 +0000 Subject: [PATCH 03/10] fix: update RenameStmt to pass objtype when spawning AlterTypeStmt context - Ensures RangeVar receives correct objtype context to prevent 'ONLY' keyword - Fixes ALTER TYPE RENAME ATTRIBUTE statements - All 279 test suites now passing Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 49ff5f2e..381bd087 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -1871,7 +1871,7 @@ export class Deparser implements DeparserVisitor { // 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') && + context.objtype !== 'OBJECT_TYPE' && !context.parentNodeTypes.includes('CreateForeignTableStmt')) { output.push('ONLY'); } @@ -4644,9 +4644,7 @@ export class Deparser implements DeparserVisitor { output.push('IF EXISTS'); } - const alterContext = node.objtype === 'OBJECT_TYPE' - ? context.spawn('AlterTypeStmt') - : context; + const alterContext = context.spawn('AlterTableStmt', { objtype: node.objtype }); if (node.relation) { const relationStr = this.RangeVar(node.relation, alterContext); @@ -4683,7 +4681,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'); @@ -4736,13 +4734,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'); @@ -4758,7 +4756,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'); @@ -8032,7 +8030,7 @@ export class Deparser implements DeparserVisitor { output.push(this.RangeVar(node.relation, context)); } else if (node.relation) { const rangeVarContext = node.relationType === 'OBJECT_TYPE' - ? context.spawn('AlterTypeStmt') + ? context.spawn('AlterTypeStmt', { objtype: 'OBJECT_TYPE' }) : context; // Add ON keyword for policy operations From 8c1f29c7e0318d0833f58d396c4fbe091714cc65 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Jun 2025 09:57:19 +0000 Subject: [PATCH 04/10] refactor: group AlterTypeStmt and objtype conditions in RangeVar for better readability - Group (!context.parentNodeTypes.includes('AlterTypeStmt') && context.objtype !== 'OBJECT_TYPE') conditions - Improves code semantics by making it clear both conditions relate to preventing ONLY keyword for ALTER TYPE operations - Remove ternary operator from RenameStmt to maintain universal spawn() flow - All 279 test suites passing Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 2010 ++++++++++++++--------------- 1 file changed, 1004 insertions(+), 1006 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 381bd087..9db2566e 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -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,26 +103,26 @@ 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. */ @@ -133,14 +133,14 @@ export class Deparser implements DeparserVisitor { 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) @@ -214,21 +214,21 @@ export class Deparser implements DeparserVisitor { visit(node: Node, context: DeparserContext = new DeparserContext({})): string { 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 result = (this[methodName] as any)(nodeData, context.spawn(nodeType)); - + return result; } - + throw new Error(`Deparser does not handle node type: ${nodeType}`); } @@ -248,7 +248,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 } @@ -263,9 +263,9 @@ export class Deparser implements DeparserVisitor { 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 + ';'; @@ -289,7 +289,7 @@ export class Deparser implements DeparserVisitor { }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') || @@ -305,7 +305,7 @@ 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)); } else { @@ -349,7 +349,7 @@ export class Deparser implements DeparserVisitor { } else { distinctPart = ' DISTINCT'; } - + if (!this.formatter.isPretty()) { if (distinctClause.length > 0 && Object.keys(distinctClause[0]).length > 0) { output.push('DISTINCT ON'); @@ -369,7 +369,7 @@ export class Deparser implements DeparserVisitor { if (targetList.length === 1) { const targetNode = targetList[0] as Node; 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); @@ -562,7 +562,7 @@ export class Deparser implements DeparserVisitor { const operator = this.deparseOperatorName(name); 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') { @@ -577,7 +577,7 @@ export class Deparser implements DeparserVisitor { if (leftNeedsParens) { leftExpr = this.formatter.parens(leftExpr); } - + // Check if right expression needs parentheses let rightNeedsParens = false; if (rexpr && 'A_Expr' in rexpr && rexpr.A_Expr?.kind === 'AEXPR_OP') { @@ -592,7 +592,7 @@ export class Deparser implements DeparserVisitor { if (rightNeedsParens) { rightExpr = this.formatter.parens(rightExpr); } - + return this.formatter.format([leftExpr, operator, rightExpr]); }else if (rexpr) { return this.formatter.format([ @@ -618,7 +618,7 @@ export class Deparser implements DeparserVisitor { 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); @@ -626,7 +626,7 @@ export class Deparser implements DeparserVisitor { if (rexpr && this.isComplexExpression(rexpr)) { rightExpr = this.formatter.parens(rightExpr); } - + return this.formatter.format([ leftExpr, 'IS DISTINCT FROM', @@ -636,7 +636,7 @@ 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); @@ -644,7 +644,7 @@ export class Deparser implements DeparserVisitor { if (rexpr && this.isComplexExpression(rexpr)) { rightExpr = this.formatter.parens(rightExpr); } - + return this.formatter.format([ leftExpr, 'IS NOT DISTINCT FROM', @@ -707,8 +707,8 @@ export class Deparser implements DeparserVisitor { case 'AEXPR_SIMILAR': const similarOp = this.deparseOperatorName(name); 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') { @@ -720,7 +720,7 @@ export class Deparser implements DeparserVisitor { } else { rightExpr = this.visit(rexpr, context); } - + if (similarOp === '!~') { return this.formatter.format([ this.visit(lexpr, context), @@ -767,18 +767,18 @@ export class Deparser implements DeparserVisitor { 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, new DeparserContext({})); }); - + if (parts.length > 1) { return `OPERATOR(${parts.join('.')})`; } - + return parts.join('.'); } @@ -814,24 +814,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; } @@ -856,37 +856,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 @@ -894,21 +894,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; } @@ -936,7 +936,7 @@ export class Deparser implements DeparserVisitor { const cols = ListUtils.unwrapList(node.cols); const insertContext = context.spawn('InsertStmt', { insertColumns: true }); const columnNames = cols.map(col => this.visit(col as Node, insertContext)); - + if (this.formatter.isPretty()) { // Always format columns in multiline parentheses for pretty printing const indentedColumns = columnNames.map(col => this.formatter.indent(col)); @@ -957,7 +957,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'); @@ -967,21 +967,21 @@ export class Deparser implements DeparserVisitor { const indexElems = elems.map(elem => this.visit(elem as Node, context)); output.push(this.formatter.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); @@ -994,7 +994,7 @@ export class Deparser implements DeparserVisitor { output.push(targets.join(', ')); } } - + if (node.onConflictClause.whereClause) { output.push('WHERE'); output.push(this.visit(node.onConflictClause.whereClause as Node, context)); @@ -1030,29 +1030,29 @@ 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)}`; assignmentParts.push(multiAssignment); @@ -1062,7 +1062,7 @@ export class Deparser implements DeparserVisitor { processedTargets.add(i); } } - + output.push(assignmentParts.join(',')); } @@ -1101,7 +1101,7 @@ export class Deparser implements DeparserVisitor { output.push('DELETE'); output.push('FROM'); - + if (!node.relation) { throw new Error('DeleteStmt requires a relation'); } @@ -1154,11 +1154,11 @@ 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()) { @@ -1176,7 +1176,7 @@ export class Deparser implements DeparserVisitor { output.push(cteStrings.join(', ')); } } - + return output.join(' '); } @@ -1184,10 +1184,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 => { @@ -1198,14 +1198,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 => { @@ -1220,13 +1220,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(' '); } @@ -1257,12 +1257,12 @@ 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.spawn('BoolExpr', { bool: true }); // explanation of our syntax/fix below: @@ -1304,24 +1304,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') { @@ -1330,13 +1330,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); @@ -1349,7 +1349,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) { @@ -1358,7 +1358,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) { @@ -1374,7 +1374,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); @@ -1386,7 +1386,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); @@ -1398,12 +1398,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); @@ -1417,7 +1417,7 @@ 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)); @@ -1425,12 +1425,12 @@ export class Deparser implements DeparserVisitor { timestamp = this.formatter.parens(timestamp); } } - + return `${timestamp} AT TIME ZONE ${timezone}`; } const params: string[] = []; - + if (node.agg_star) { if (node.agg_distinct) { params.push('DISTINCT *'); @@ -1443,7 +1443,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 { @@ -1483,25 +1483,25 @@ 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); 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)); @@ -1526,7 +1526,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) { @@ -1616,7 +1616,7 @@ export class Deparser implements DeparserVisitor { return `b'${bsval}'`; } } - + if (nodeAny.val) { if (nodeAny.val.Integer?.ival !== undefined) { return nodeAny.val.Integer.ival.toString(); @@ -1630,11 +1630,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'; @@ -1657,15 +1657,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'; } @@ -1687,7 +1687,7 @@ export class Deparser implements DeparserVisitor { } const output: string[] = []; - + // Handle SETOF keyword if (node.setof) { output.push('SETOF'); @@ -1710,7 +1710,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 @@ -1748,7 +1748,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"'); @@ -1757,29 +1757,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') { @@ -1821,13 +1821,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(' '); } @@ -1835,11 +1835,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(' '); } @@ -1869,13 +1869,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.objtype !== 'OBJECT_TYPE' && + 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); @@ -1896,7 +1896,7 @@ export class Deparser implements DeparserVisitor { } const result = output.join(' '); - + return result; } @@ -1944,20 +1944,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})`; } @@ -2006,18 +2006,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': @@ -2049,7 +2049,7 @@ export class Deparser implements DeparserVisitor { } break; } - + return null; } @@ -2089,17 +2089,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; } @@ -2131,15 +2131,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); @@ -2148,12 +2148,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 { @@ -2164,7 +2164,7 @@ export class Deparser implements DeparserVisitor { } const args = ListUtils.unwrapList(node.args); - + if (this.formatter.isPretty() && args.length > 0) { for (const arg of args) { const whenClause = this.visit(arg, context); @@ -2210,25 +2210,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('-'))) { @@ -2238,40 +2238,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.spawn('BooleanTest', { bool: true }); - + output.push(this.visit(node.arg, boolContext)); - + switch (node.booltesttype as string) { case 'IS_TRUE': output.push('IS TRUE'); @@ -2292,15 +2292,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'); @@ -2309,7 +2309,7 @@ export class Deparser implements DeparserVisitor { output.push('IS NOT NULL'); break; } - + return output.join(' '); } @@ -2328,12 +2328,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; } @@ -2348,7 +2348,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', @@ -2358,7 +2358,7 @@ export class Deparser implements DeparserVisitor { 'hashes': 'Hashes', 'merges': 'Merges' }; - + return caseMap[defName.toLowerCase()] || defName; } @@ -2368,14 +2368,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 = /^[+\-*/<>=~!@#%^&|`?]+$/; @@ -2383,23 +2383,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)}'`; @@ -2408,11 +2408,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 { @@ -2443,7 +2443,7 @@ 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); @@ -2457,7 +2457,7 @@ export class Deparser implements DeparserVisitor { const elementStrs = elements.map(el => { return this.deparse(el, context); }); - + if (this.formatter.isPretty()) { const formattedElements = elementStrs.map(el => { const trimmedEl = el.trim(); @@ -2480,7 +2480,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) @@ -2964,7 +2964,7 @@ export class Deparser implements DeparserVisitor { SubLink(node: t.SubLink, context: DeparserContext): string { const subselect = this.formatter.parens(this.visit(node.subselect, context)); - + switch (node.subLinkType) { case 'ANY_SUBLINK': if (node.testexpr && node.operName) { @@ -2995,41 +2995,41 @@ 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); @@ -3037,7 +3037,7 @@ export class Deparser implements DeparserVisitor { windowParts.push(frameClause); } } - + if (windowParts.length > 0) { if (node.name) { output.push('AS'); @@ -3049,30 +3049,30 @@ export class Deparser implements DeparserVisitor { } else if (output.length === 0) { output.push('()'); } - + return output.join(' '); } formatWindowFrame(node: any): 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'); @@ -3110,7 +3110,7 @@ export class Deparser implements DeparserVisitor { } 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) { @@ -3134,22 +3134,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); @@ -3164,13 +3164,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(' '); } @@ -3200,31 +3200,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.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))); } - + return output.join(' '); } @@ -3234,7 +3234,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'); @@ -3251,7 +3251,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) @@ -3259,20 +3259,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 { @@ -3283,11 +3283,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(', ')})`; } @@ -3345,11 +3345,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 { @@ -3402,7 +3402,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)); } @@ -3446,56 +3446,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(', '))); } - + if (node.options && node.options.length > 0) { 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': @@ -3506,98 +3506,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(', '))); } - + 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(', '))); } - + if (node.whereClause) { output.push('WHERE'); output.push(this.visit(node.whereClause, context)); } - + if (node.options && node.options.length > 0) { 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(', '))); } - + 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))); } - + 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,10 +3609,10 @@ export class Deparser implements DeparserVisitor { }); opclassStr += `(${opclassOpts.join(', ')})`; } - + output.push(opclassStr); } - + if (node.ordering) { switch (node.ordering) { case 'SORTBY_ASC': @@ -3623,7 +3623,7 @@ export class Deparser implements DeparserVisitor { break; } } - + if (node.nulls_ordering) { switch (node.nulls_ordering) { case 'SORTBY_NULLS_FIRST': @@ -3634,7 +3634,7 @@ export class Deparser implements DeparserVisitor { break; } } - + return output.join(' '); } @@ -3663,15 +3663,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'); @@ -3701,7 +3701,7 @@ export class Deparser implements DeparserVisitor { output.push('DEFAULT'); } } - + return output.join(' '); } @@ -3717,7 +3717,7 @@ export class Deparser implements DeparserVisitor { 2107: 'array_agg', 2108: 'string_agg' }; - + return commonAggFunctions[aggfnoid || 0] || 'unknown_agg'; } @@ -3734,7 +3734,7 @@ export class Deparser implements DeparserVisitor { 3108: 'first_value', 3109: 'last_value' }; - + return commonWindowFunctions[winfnoid || 0] || 'unknown_window_func'; } @@ -3762,22 +3762,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) { @@ -3801,14 +3801,14 @@ 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); } else { @@ -3821,7 +3821,7 @@ export class Deparser implements DeparserVisitor { 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)); @@ -3847,14 +3847,14 @@ export class Deparser implements DeparserVisitor { output.push(`ON ${qualsStr}`); } } - + let result; if (this.formatter.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) { @@ -3864,7 +3864,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) { @@ -3874,26 +3874,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'); @@ -3946,7 +3946,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 => { @@ -3994,12 +3994,12 @@ export class Deparser implements DeparserVisitor { } return this.visit(option, context); }).filter(Boolean); - + if (options.length > 0) { output.push(options.join(', ')); } } - + return output.join(' '); } @@ -4018,7 +4018,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) { @@ -4143,7 +4143,7 @@ export class Deparser implements DeparserVisitor { if (node.rolename) { return this.quoteIfNeeded(node.rolename); } - + switch (node.roletype) { case 'ROLESPEC_PUBLIC': return 'PUBLIC'; @@ -4344,7 +4344,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}`); @@ -4362,14 +4362,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(', '); @@ -4385,7 +4385,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}`; @@ -4395,7 +4395,7 @@ export class Deparser implements DeparserVisitor { } return items.join('.'); } - + const objName = this.visit(objList, context); return objName; }).filter((name: string) => name && name.trim()).join(', '); @@ -4412,7 +4412,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]; @@ -4425,7 +4425,7 @@ export class Deparser implements DeparserVisitor { } return items.join('.'); } - + const objName = this.visit(objList, context); return objName; }).filter((name: string) => name && name.trim()).join(', '); @@ -4442,14 +4442,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(', '); @@ -4462,7 +4462,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) { @@ -4472,7 +4472,7 @@ export class Deparser implements DeparserVisitor { }).filter((name: string) => name && name.trim()); return items.join('.'); } - + const objContext = context.spawn('DropStmt', { objtype: node.removeType }); const objName = this.visit(objList, objContext); return objName; @@ -4692,22 +4692,22 @@ 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.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 => { @@ -4716,16 +4716,16 @@ export class Deparser implements DeparserVisitor { }); 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') { @@ -5268,20 +5268,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) => { @@ -5289,7 +5289,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 { @@ -5299,12 +5299,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 @@ -5319,20 +5319,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.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); @@ -5343,7 +5343,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 => { @@ -5362,17 +5362,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': @@ -5389,35 +5389,35 @@ 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.spawn('CreateEnumStmt', { isEnumValue: true }); const values = ListUtils.unwrapList(node.vals) @@ -5427,29 +5427,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)) @@ -5458,13 +5458,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') { @@ -5474,12 +5474,12 @@ 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 options = ListUtils.unwrapList(node.options) .map(option => this.visit(option, context.spawn('CreateRoleStmt'))) @@ -5489,7 +5489,7 @@ export class Deparser implements DeparserVisitor { output.push(options); } } - + return output.join(' '); } @@ -5497,9 +5497,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); @@ -5510,7 +5510,7 @@ 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') { @@ -5519,16 +5519,16 @@ export class Deparser implements DeparserVisitor { } 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)) { @@ -5543,16 +5543,16 @@ export class Deparser implements DeparserVisitor { if (node.arg) { 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') { @@ -5560,14 +5560,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') { @@ -5575,9 +5575,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') { @@ -5585,13 +5585,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') { @@ -5601,17 +5601,17 @@ export class Deparser implements DeparserVisitor { } 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.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); @@ -5622,7 +5622,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); @@ -5631,27 +5631,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 @@ -5659,7 +5659,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 { @@ -5677,26 +5677,26 @@ 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 (String(argValue) === 'true') { // Handle special cases where the positive form has a different name if (node.defname === 'isreplication') { @@ -5717,7 +5717,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 @@ -5741,7 +5741,7 @@ export class Deparser implements DeparserVisitor { return `OWNED BY ${argValue}`; } } - + // Handle boolean sequence options if (node.defname === 'cycle') { const boolValue = String(argValue).toLowerCase(); @@ -5751,19 +5751,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()}`; @@ -5771,12 +5771,12 @@ export class Deparser implements DeparserVisitor { return node.defname.toUpperCase(); } } - + if (context.parentNodeTypes.includes('DoStmt')) { if (node.defname === 'as') { 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(''); @@ -5789,7 +5789,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) @@ -5802,7 +5802,7 @@ export class Deparser implements DeparserVisitor { } return this.visit(item, context); }); - + if (bodyParts.length === 1) { const body = bodyParts[0]; const delimiter = this.getFunctionDelimiter(body); @@ -5827,7 +5827,7 @@ export class Deparser implements DeparserVisitor { return `${delimiter}${part}${delimiter}`; }).join(', ')}`; } - } + } // Handle String type (single function body) else { const delimiter = this.getFunctionDelimiter(argValue); @@ -5863,7 +5863,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')) { @@ -5874,34 +5874,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') { @@ -5910,7 +5910,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 @@ -5920,7 +5920,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') { @@ -5937,9 +5937,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') { @@ -5947,18 +5947,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') { @@ -5975,7 +5975,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) { @@ -5992,7 +5992,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()) { @@ -6000,7 +6000,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}"`; @@ -6018,7 +6018,7 @@ export class Deparser implements DeparserVisitor { } return quotedDefname; } - + // Handle other operator parameters with preserved case if (preservedName !== node.defname) { if (node.arg) { @@ -6026,7 +6026,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); @@ -6056,13 +6056,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) @@ -6070,27 +6070,27 @@ 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.spawn('CreateTableSpaceStmt'); @@ -6099,37 +6099,37 @@ export class Deparser implements DeparserVisitor { .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.spawn('AlterTableSpaceOptionsStmt'); const options = ListUtils.unwrapList(node.options) @@ -6137,21 +6137,21 @@ export class Deparser implements DeparserVisitor { .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.spawn('CreateExtensionStmt'); const options = ListUtils.unwrapList(node.options) @@ -6159,17 +6159,17 @@ export class Deparser implements DeparserVisitor { .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.spawn('AlterExtensionStmt'); const options = ListUtils.unwrapList(node.options) @@ -6177,17 +6177,17 @@ export class Deparser implements DeparserVisitor { .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.spawn('CreateFdwStmt'); const funcOptions = ListUtils.unwrapList(node.func_options) @@ -6195,7 +6195,7 @@ export class Deparser implements DeparserVisitor { .join(' '); output.push(funcOptions); } - + if (node.options && node.options.length > 0) { output.push('OPTIONS'); const fdwContext = context.spawn('CreateFdwStmt'); @@ -6204,17 +6204,17 @@ export class Deparser implements DeparserVisitor { .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': @@ -6230,17 +6230,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': @@ -6266,35 +6266,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': @@ -6382,96 +6382,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(' '); } @@ -6503,7 +6503,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': @@ -6585,7 +6585,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) { @@ -6593,7 +6593,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; @@ -6631,13 +6631,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) => { @@ -6651,7 +6651,7 @@ export class Deparser implements DeparserVisitor { } }); } - + output.push(`${operatorName} (${args.join(', ')})`); } else { output.push(objectParts.join('.')); @@ -6693,14 +6693,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) => { @@ -6714,35 +6714,35 @@ export class Deparser implements DeparserVisitor { } }); } - + output.push(`${operatorName}(${args.join(', ')})`); } else { 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) @@ -6755,29 +6755,29 @@ 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()) { @@ -6787,7 +6787,7 @@ export class Deparser implements DeparserVisitor { output.push(this.RangeVar(node.table, context)); } } - + // Handle AS RESTRICTIVE/PERMISSIVE clause if (node.permissive === undefined) { if (this.formatter.isPretty()) { @@ -6802,7 +6802,7 @@ export class Deparser implements DeparserVisitor { 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()}`)); @@ -6810,7 +6810,7 @@ export class Deparser implements DeparserVisitor { 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()) { @@ -6820,7 +6820,7 @@ export class Deparser implements DeparserVisitor { output.push(roles.join(', ')); } } - + if (node.qual) { if (this.formatter.isPretty()) { const qualExpr = this.visit(node.qual, context); @@ -6844,106 +6844,106 @@ export class Deparser implements DeparserVisitor { output.push(`(${this.visit(node.with_check, context)})`); } } - + return this.formatter.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.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(' '); } @@ -6958,11 +6958,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)); @@ -6970,52 +6970,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': @@ -7031,7 +7031,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) { @@ -7039,23 +7039,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': @@ -7087,27 +7087,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': @@ -7118,19 +7118,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.spawn('DoStmt'); const args = ListUtils.unwrapList(node.args); - + const processedArgs: string[] = []; - + for (const arg of args) { const nodeType = this.getNodeType(arg); if (nodeType === 'DefElem') { @@ -7151,10 +7151,10 @@ export class Deparser implements DeparserVisitor { } } } - + output.push(...processedArgs); } - + return output.join(' '); } @@ -7162,26 +7162,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 '$$'; } @@ -7203,56 +7203,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': @@ -7263,53 +7263,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': @@ -7386,28 +7386,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) { @@ -7448,40 +7448,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.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(', ')})`; @@ -7497,7 +7497,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')) { @@ -7506,74 +7506,74 @@ 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.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.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.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('('); @@ -7582,21 +7582,21 @@ export class Deparser implements DeparserVisitor { 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('('); @@ -7605,66 +7605,66 @@ export class Deparser implements DeparserVisitor { 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.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': @@ -7687,87 +7687,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.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.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': @@ -7789,24 +7789,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) { @@ -7815,7 +7815,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)); @@ -7823,7 +7823,7 @@ export class Deparser implements DeparserVisitor { } else { argsStr = '()'; } - + output.push(`${funcName}${argsStr}`); } } else if (node.funcexpr) { @@ -7831,59 +7831,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'); @@ -8018,26 +8018,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.spawn('AlterTypeStmt', { objtype: 'OBJECT_TYPE' }) - : 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 @@ -8062,7 +8060,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) { @@ -8080,28 +8078,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'); @@ -8172,7 +8170,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) { @@ -8190,21 +8188,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 { @@ -8357,7 +8355,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; @@ -8365,46 +8363,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) { @@ -8434,7 +8432,7 @@ export class Deparser implements DeparserVisitor { if (node.is_grant) { const withOptions: string[] = []; - + if (hasAdminOption) { if (adminValue === true) { withOptions.push('ADMIN OPTION'); @@ -8444,7 +8442,7 @@ export class Deparser implements DeparserVisitor { withOptions.push('ADMIN OPTION'); } } - + if (hasInheritOption) { if (inheritValue === true) { withOptions.push('INHERIT OPTION'); @@ -8452,7 +8450,7 @@ export class Deparser implements DeparserVisitor { withOptions.push('INHERIT FALSE'); } } - + if (hasSetOption) { if (setValue === true) { withOptions.push('SET TRUE'); @@ -8460,7 +8458,7 @@ export class Deparser implements DeparserVisitor { withOptions.push('SET FALSE'); } } - + if (withOptions.length > 0) { output.push('WITH', withOptions.join(', ')); } @@ -8737,12 +8735,12 @@ export class Deparser implements DeparserVisitor { if (this.formatter.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'); @@ -8757,39 +8755,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 '))); - + if (node.relation) { components.push(this.formatter.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)); } - + if (node.deferrable) { components.push(this.formatter.indent('DEFERRABLE')); } - + if (node.initdeferred) { components.push(this.formatter.indent('INITIALLY DEFERRED')); } - + if (node.row) { components.push(this.formatter.indent('FOR EACH ROW')); } else { components.push(this.formatter.indent('FOR EACH STATEMENT')); } - + if (node.whenClause) { const whenStr = 'WHEN (' + this.visit(node.whenClause, context) + ')'; components.push(this.formatter.indent(whenStr)); } - + let executeStr = 'EXECUTE'; if (node.funcname && node.funcname.length > 0) { const funcName = ListUtils.unwrapList(node.funcname) @@ -8797,7 +8795,7 @@ export class Deparser implements DeparserVisitor { .join('.'); executeStr += ' PROCEDURE ' + funcName; } - + if (node.args && node.args.length > 0) { const argContext = context.spawn('CreateTrigStmt', { isStringLiteral: true }); const args = ListUtils.unwrapList(node.args) @@ -8807,9 +8805,9 @@ export class Deparser implements DeparserVisitor { } else { executeStr += '()'; } - + components.push(this.formatter.indent(executeStr)); - + return output.join(' ') + this.formatter.newline() + components.join(this.formatter.newline()); } else { const timing: string[] = []; @@ -8898,17 +8896,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(' '); } @@ -9074,40 +9072,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') { @@ -9115,29 +9113,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; @@ -9145,13 +9143,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; @@ -9163,7 +9161,7 @@ export class Deparser implements DeparserVisitor { } output.push(sequenceName.join('.')); } - + if (node.options && node.options.length > 0) { const seqContext = context.spawn('CreateSeqStmt'); const optionStrs = ListUtils.unwrapList(node.options) @@ -9182,17 +9180,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; @@ -9204,7 +9202,7 @@ export class Deparser implements DeparserVisitor { } output.push(sequenceName.join('.')); } - + if (node.options && node.options.length > 0) { const seqContext = context.spawn('AlterSeqStmt'); const optionStrs = ListUtils.unwrapList(node.options) @@ -9226,24 +9224,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.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)) @@ -9253,22 +9251,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); @@ -9280,20 +9278,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, "''"); @@ -9314,22 +9312,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); @@ -9341,36 +9339,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.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)) @@ -9387,24 +9385,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(' '); } @@ -9412,28 +9410,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) @@ -9443,12 +9441,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 => { @@ -9485,16 +9483,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 { @@ -9504,15 +9502,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) @@ -9521,12 +9519,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) { @@ -9541,13 +9539,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) @@ -9555,29 +9553,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(' '); } @@ -9585,13 +9583,13 @@ 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.spawn('AccessPriv'); @@ -9599,7 +9597,7 @@ export class Deparser implements DeparserVisitor { output.push(columns.join(', ')); output.push(')'); } - + return output.join(' '); } @@ -9612,15 +9610,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) { @@ -9633,7 +9631,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 => { @@ -9641,7 +9639,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)) { @@ -9649,7 +9647,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) { @@ -9675,14 +9673,14 @@ 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.spawn('DefineStmt'); const definitions = ListUtils.unwrapList(node.definition).map(def => { @@ -9691,34 +9689,34 @@ export class Deparser implements DeparserVisitor { 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; @@ -9728,7 +9726,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) @@ -9763,7 +9761,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 @@ -9798,14 +9796,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)) { @@ -9813,7 +9811,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}'`; @@ -9826,14 +9824,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 => { @@ -9841,7 +9839,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)}`; } @@ -9852,14 +9850,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 => { @@ -9867,7 +9865,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)}`; } @@ -9878,17 +9876,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 => { @@ -9896,7 +9894,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)}`; } @@ -9907,17 +9905,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 => { @@ -9925,7 +9923,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)}`; } @@ -9936,27 +9934,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) { @@ -9969,7 +9967,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) { @@ -9980,101 +9978,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) { @@ -10088,39 +10086,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'); @@ -10133,7 +10131,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) @@ -10141,28 +10139,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) @@ -10170,7 +10168,7 @@ export class Deparser implements DeparserVisitor { .join(', '); output.push(`(${options})`); } - + if (node.onCommit && node.onCommit !== 'ONCOMMIT_NOOP') { output.push('ON COMMIT'); switch (node.onCommit) { @@ -10185,29 +10183,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') { @@ -10218,13 +10216,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(' '); } @@ -10234,7 +10232,7 @@ 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) @@ -10259,7 +10257,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) @@ -10267,14 +10265,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(' '); } @@ -10287,10 +10285,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) @@ -10298,16 +10296,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) @@ -10317,16 +10315,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) @@ -10335,26 +10333,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) @@ -10362,74 +10360,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) @@ -10465,7 +10463,7 @@ export class Deparser implements DeparserVisitor { } }) .filter(str => str && str.trim()); - + if (functionStrs.length > 0) { output.push(`(${functionStrs.join(', ')})`); } @@ -10499,11 +10497,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) { @@ -10522,7 +10520,7 @@ export class Deparser implements DeparserVisitor { .filter(str => str && str.trim()); output.push(`AS (${coldefs.join(', ')})`); } - + return output.join(' '); } @@ -10538,9 +10536,9 @@ export class Deparser implements DeparserVisitor { return 'XMLPI()'; } } - + const output: string[] = []; - + switch (node.op) { case 'IS_XMLCONCAT': output.push('XMLCONCAT'); @@ -10591,11 +10589,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) { @@ -10604,7 +10602,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) { @@ -10622,7 +10620,7 @@ export class Deparser implements DeparserVisitor { rootParts.push(`STANDALONE ${this.visit(standaloneArg, context)}`); } } - + if (rootParts.length > 0) { output.push(`(${rootParts.join(', ')})`); } @@ -10642,20 +10640,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') { @@ -10664,7 +10662,7 @@ export class Deparser implements DeparserVisitor { output.push(`XMLATTRIBUTES(${namedArgStrs.join(', ')})`); } } - + return output.join(' '); } @@ -10698,50 +10696,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(' '); } @@ -10749,35 +10747,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': @@ -10796,25 +10794,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)); @@ -10827,27 +10825,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(' '); } @@ -10938,28 +10936,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(''); } @@ -10969,32 +10967,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'); @@ -11004,41 +11002,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.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'); @@ -11100,11 +11098,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) { @@ -11212,30 +11210,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'); @@ -11247,7 +11245,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) @@ -11257,19 +11255,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.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(', ')})`); @@ -11277,7 +11275,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)); @@ -11290,18 +11288,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.spawn('CreateForeignTableStmt'); const optionStrs = ListUtils.unwrapList(node.options).map(opt => this.visit(opt, foreignTableContext)); output.push(`OPTIONS (${optionStrs.join(', ')})`); } - + return output.join(' '); } From 18236c0c071080084862997eb1d66bda67d40c66 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Jun 2025 10:05:35 +0000 Subject: [PATCH 05/10] fix: update formatWindowFrame and deparseOperatorName to accept context parameters - Update formatWindowFrame method signature to accept DeparserContext parameter - Update deparseOperatorName method signature to accept DeparserContext parameter - Replace all empty context creation with passed context in both methods - Update all call sites to pass properly spawned context instead of creating empty contexts - Ensures consistent context propagation in helper methods that are not actual node types Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 54 +++++++++++++++---------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 9db2566e..ac94c44d 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -559,14 +559,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; } @@ -581,7 +581,7 @@ export class Deparser implements DeparserVisitor { // 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; } @@ -596,7 +596,7 @@ export class Deparser implements DeparserVisitor { return this.formatter.format([leftExpr, operator, rightExpr]); }else if (rexpr) { return this.formatter.format([ - this.deparseOperatorName(name), + this.deparseOperatorName(name, context), this.visit(rexpr, context) ]); } @@ -604,14 +604,14 @@ export class Deparser implements DeparserVisitor { case 'AEXPR_OP_ANY': return this.formatter.format([ this.visit(lexpr, context), - this.deparseOperatorName(name), + this.deparseOperatorName(name, context), 'ANY', this.formatter.parens(this.visit(rexpr, context)) ]); case 'AEXPR_OP_ALL': return this.formatter.format([ this.visit(lexpr, context), - this.deparseOperatorName(name), + this.deparseOperatorName(name, context), 'ALL', this.formatter.parens(this.visit(rexpr, context)) ]); @@ -660,7 +660,7 @@ export class Deparser implements DeparserVisitor { ].join(', ')) ]); case 'AEXPR_IN': - const inOperator = this.deparseOperatorName(name); + const inOperator = this.deparseOperatorName(name, context); if (inOperator === '<>' || inOperator === '!=') { return this.formatter.format([ this.visit(lexpr, context), @@ -675,7 +675,7 @@ export class Deparser implements DeparserVisitor { ]); } case 'AEXPR_LIKE': - const likeOp = this.deparseOperatorName(name); + const likeOp = this.deparseOperatorName(name, context); if (likeOp === '!~~') { return this.formatter.format([ this.visit(lexpr, context), @@ -690,7 +690,7 @@ export class Deparser implements DeparserVisitor { ]); } case 'AEXPR_ILIKE': - const ilikeOp = this.deparseOperatorName(name); + const ilikeOp = this.deparseOperatorName(name, context); if (ilikeOp === '!~~*') { return this.formatter.format([ this.visit(lexpr, context), @@ -705,7 +705,7 @@ export class Deparser implements DeparserVisitor { ]); } case 'AEXPR_SIMILAR': - const similarOp = this.deparseOperatorName(name); + const similarOp = this.deparseOperatorName(name, context); let rightExpr: string; if (rexpr && 'FuncCall' in rexpr && @@ -763,7 +763,7 @@ 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 ''; } @@ -772,7 +772,7 @@ export class Deparser implements DeparserVisitor { if (n.String) { return n.String.sval || n.String.str; } - return this.visit(n, new DeparserContext({})); + return this.visit(n, context); }); if (parts.length > 1) { @@ -1420,7 +1420,7 @@ export class Deparser implements DeparserVisitor { // 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); } @@ -1497,7 +1497,7 @@ export class Deparser implements DeparserVisitor { } // 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); } @@ -2969,7 +2969,7 @@ export class Deparser implements DeparserVisitor { 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); @@ -2979,7 +2979,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; @@ -3032,7 +3032,7 @@ export class Deparser implements DeparserVisitor { // 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); } @@ -3053,7 +3053,7 @@ export class Deparser implements DeparserVisitor { return output.join(' '); } - formatWindowFrame(node: any): string | null { + formatWindowFrame(node: any, context: DeparserContext): string | null { if (!node.frameOptions) return null; const frameOptions = node.frameOptions; @@ -3082,8 +3082,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, new DeparserContext({}))} PRECEDING`); - boundsParts.push(`AND ${this.visit(node.endOffset, new DeparserContext({}))} 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'); @@ -3091,7 +3091,7 @@ export class Deparser implements DeparserVisitor { } else if (frameOptions === 16917) { boundsParts.push('CURRENT ROW'); if (node.endOffset) { - boundsParts.push(`AND ${this.visit(node.endOffset, new DeparserContext({}))} FOLLOWING`); + boundsParts.push(`AND ${this.visit(node.endOffset, context)} FOLLOWING`); } } else if (frameOptions === 1058) { return null; @@ -3099,11 +3099,11 @@ 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, new DeparserContext({}))} PRECEDING`); + boundsParts.push(`${this.visit(node.startOffset, context)} PRECEDING`); } else if (frameOptions & 0x800) { // FRAMEOPTION_START_VALUE_FOLLOWING - boundsParts.push(`${this.visit(node.startOffset, new DeparserContext({}))} FOLLOWING`); + boundsParts.push(`${this.visit(node.startOffset, context)} FOLLOWING`); } else { - boundsParts.push(`${this.visit(node.startOffset, new DeparserContext({}))} PRECEDING`); + boundsParts.push(`${this.visit(node.startOffset, context)} PRECEDING`); } } else if (frameOptions & 0x10) { // FRAMEOPTION_START_UNBOUNDED_PRECEDING boundsParts.push('UNBOUNDED PRECEDING'); @@ -3115,11 +3115,11 @@ export class Deparser implements DeparserVisitor { if (node.endOffset) { if (boundsParts.length > 0) { if (frameOptions & 0x1000) { // FRAMEOPTION_END_VALUE_PRECEDING - boundsParts.push(`AND ${this.visit(node.endOffset, new DeparserContext({}))} 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, new DeparserContext({}))} FOLLOWING`); + boundsParts.push(`AND ${this.visit(node.endOffset, context)} FOLLOWING`); } else { - boundsParts.push(`AND ${this.visit(node.endOffset, new DeparserContext({}))} FOLLOWING`); + boundsParts.push(`AND ${this.visit(node.endOffset, context)} FOLLOWING`); } } } else if (frameOptions & 0x80) { // FRAMEOPTION_END_UNBOUNDED_FOLLOWING From 04d1885590cbf4c08abe5a3768975e2d15cf9b97 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Jun 2025 10:14:48 +0000 Subject: [PATCH 06/10] refactor: remove automatic context spawning from visit() method - Change visit() to pass original context instead of spawning new context - Prevents premature context spawning and gives node methods control - All tests continue to pass (279/279 test suites) - Helper methods already properly use passed context parameters Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index ac94c44d..9d2f3c08 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -214,21 +214,21 @@ export class Deparser implements DeparserVisitor { visit(node: Node, context: DeparserContext = new DeparserContext({})): string { 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 result = (this[methodName] as any)(nodeData, context.spawn(nodeType)); - + const result = (this[methodName] as any)(nodeData, context); + return result; } - + throw new Error(`Deparser does not handle node type: ${nodeType}`); } From c7b881e5f82f151fbc891a8e25bedba730855056 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Jun 2025 10:34:57 +0000 Subject: [PATCH 07/10] refactor: move SqlFormatter from direct import to DeparserContext property - Add SqlFormatter as a property of DeparserContext with default instantiation - Replace all this.formatter references with context.formatter throughout deparser - Remove direct SqlFormatter import and private formatter property from Deparser class - Update deparse() and visit() methods to create context with formatter when not provided - Maintain modular design allowing external formatter instantiation and context passing - Preserve existing formatting logic while improving context-aware formatting - All 279 test suites continue to pass Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 366 +++++++++++++------------ packages/deparser/src/visitors/base.ts | 6 + 2 files changed, 194 insertions(+), 178 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 9d2f3c08..0124baee 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'; @@ -127,12 +127,10 @@ function isWrappedParseResult(obj: any): obj is { ParseResult: t.ParseResult } { * 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 = { @@ -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 }); 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.formatter.newline() + context.formatter.newline()); } /** @@ -195,10 +195,15 @@ export class Deparser implements DeparserVisitor { return delimiter; } - deparse(node: Node, context: DeparserContext = new DeparserContext({})): 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 }); + } if (typeof node === 'number' || node instanceof Number) { return node.toString(); @@ -212,7 +217,12 @@ export class Deparser implements DeparserVisitor { } } - visit(node: Node, context: DeparserContext = new DeparserContext({})): 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 }); + } + const nodeType = this.getNodeType(node); // Handle empty objects @@ -256,7 +266,7 @@ 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.formatter.newline() + context.formatter.newline()); } RawStmt(node: t.RawStmt, context: DeparserContext): string { @@ -282,7 +292,7 @@ export class Deparser implements DeparserVisitor { if (!node.op || node.op === 'SETOP_NONE') { if (node.valuesLists == null) { - if (!this.formatter.isPretty() || !node.targetList) { + if (!context.formatter.isPretty() || !node.targetList) { output.push('SELECT'); } } @@ -307,7 +317,7 @@ export class Deparser implements DeparserVisitor { ); if (leftNeedsParens) { - output.push(this.formatter.parens(leftStmt)); + output.push(context.formatter.parens(leftStmt)); } else { output.push(leftStmt); } @@ -331,7 +341,7 @@ export class Deparser implements DeparserVisitor { } if (rightNeedsParens) { - output.push(this.formatter.parens(rightStmt)); + output.push(context.formatter.parens(rightStmt)); } else { output.push(rightStmt); } @@ -345,18 +355,18 @@ export class Deparser implements DeparserVisitor { const clause = distinctClause .map(e => this.visit(e as Node, context.spawn('SelectStmt', { select: true }))) .join(', '); - distinctPart = ' DISTINCT ON ' + this.formatter.parens(clause); + distinctPart = ' DISTINCT ON ' + context.formatter.parens(clause); } else { distinctPart = ' DISTINCT'; } - if (!this.formatter.isPretty()) { + if (!context.formatter.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.spawn('SelectStmt', { select: true }))) .join(', '); - output.push(this.formatter.parens(clause)); + output.push(context.formatter.parens(clause)); } else { output.push('DISTINCT'); } @@ -365,7 +375,7 @@ export class Deparser implements DeparserVisitor { if (node.targetList) { const targetList = ListUtils.unwrapList(node.targetList); - if (this.formatter.isPretty()) { + if (context.formatter.isPretty()) { if (targetList.length === 1) { const targetNode = targetList[0] as Node; const target = this.visit(targetNode, context.spawn('SelectStmt', { select: true })); @@ -376,7 +386,7 @@ export class Deparser implements DeparserVisitor { if (this.containsMultilineStringLiteral(target)) { output.push(target); } else { - output.push(this.formatter.indent(target)); + output.push(context.formatter.indent(target)); } } else { output.push('SELECT' + distinctPart + ' ' + target); @@ -388,9 +398,9 @@ export class Deparser implements DeparserVisitor { if (this.containsMultilineStringLiteral(targetStr)) { return targetStr; } - return this.formatter.indent(targetStr); + return context.formatter.indent(targetStr); }); - const formattedTargets = targetStrings.join(',' + this.formatter.newline()); + const formattedTargets = targetStrings.join(',' + context.formatter.newline()); output.push('SELECT' + distinctPart); output.push(formattedTargets); } @@ -416,17 +426,17 @@ export class Deparser implements DeparserVisitor { } if (node.whereClause) { - if (this.formatter.isPretty()) { + if (context.formatter.isPretty()) { output.push('WHERE'); const whereExpr = this.visit(node.whereClause as Node, context); - const lines = whereExpr.split(this.formatter.newline()); + const lines = whereExpr.split(context.formatter.newline()); const indentedLines = lines.map((line, index) => { if (index === 0) { - return this.formatter.indent(line); + return context.formatter.indent(line); } return line; }); - output.push(indentedLines.join(this.formatter.newline())); + output.push(indentedLines.join(context.formatter.newline())); } else { output.push('WHERE'); output.push(this.visit(node.whereClause as Node, context)); @@ -434,24 +444,24 @@ export class Deparser implements DeparserVisitor { } if (node.valuesLists) { - if (this.formatter.isPretty()) { + if (context.formatter.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.formatter.parens(values.join(', ')); }); const indentedTuples = lists.map(tuple => { if (this.containsMultilineStringLiteral(tuple)) { return tuple; } - return this.formatter.indent(tuple); + return context.formatter.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.formatter.parens(values.join(', ')); }); output.push(lists.join(', ')); } @@ -459,16 +469,16 @@ export class Deparser implements DeparserVisitor { if (node.groupClause) { const groupList = ListUtils.unwrapList(node.groupClause); - if (this.formatter.isPretty()) { + if (context.formatter.isPretty()) { const groupItems = groupList .map(e => { 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.formatter.indent(groupStr); }) - .join(',' + this.formatter.newline()); + .join(',' + context.formatter.newline()); output.push('GROUP BY'); output.push(groupItems); } else { @@ -481,13 +491,13 @@ export class Deparser implements DeparserVisitor { } if (node.havingClause) { - if (this.formatter.isPretty()) { + if (context.formatter.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.formatter.indent(havingStr)); } } else { output.push('HAVING'); @@ -506,16 +516,16 @@ export class Deparser implements DeparserVisitor { if (node.sortClause) { const sortList = ListUtils.unwrapList(node.sortClause); - if (this.formatter.isPretty()) { + if (context.formatter.isPretty()) { const sortItems = sortList .map(e => { 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.formatter.indent(sortStr); }) - .join(',' + this.formatter.newline()); + .join(',' + context.formatter.newline()); output.push('ORDER BY'); output.push(sortItems); } else { @@ -543,9 +553,9 @@ export class Deparser implements DeparserVisitor { output.push(lockingClauses); } - if (this.formatter.isPretty()) { + if (context.formatter.isPretty()) { const filteredOutput = output.filter(item => item.trim() !== ''); - return filteredOutput.join(this.formatter.newline()); + return filteredOutput.join(context.formatter.newline()); } return output.join(' '); } @@ -575,7 +585,7 @@ export class Deparser implements DeparserVisitor { leftNeedsParens = true; } if (leftNeedsParens) { - leftExpr = this.formatter.parens(leftExpr); + leftExpr = context.formatter.parens(leftExpr); } // Check if right expression needs parentheses @@ -590,30 +600,30 @@ export class Deparser implements DeparserVisitor { rightNeedsParens = true; } if (rightNeedsParens) { - rightExpr = this.formatter.parens(rightExpr); + rightExpr = context.formatter.parens(rightExpr); } - return this.formatter.format([leftExpr, operator, rightExpr]); + return context.formatter.format([leftExpr, operator, rightExpr]); }else if (rexpr) { - return this.formatter.format([ + return context.formatter.format([ this.deparseOperatorName(name, context), this.visit(rexpr, context) ]); } break; case 'AEXPR_OP_ANY': - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), this.deparseOperatorName(name, context), 'ANY', - this.formatter.parens(this.visit(rexpr, context)) + context.formatter.parens(this.visit(rexpr, context)) ]); case 'AEXPR_OP_ALL': - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), this.deparseOperatorName(name, context), 'ALL', - this.formatter.parens(this.visit(rexpr, context)) + context.formatter.parens(this.visit(rexpr, context)) ]); case 'AEXPR_DISTINCT': { let leftExpr = this.visit(lexpr, context); @@ -621,13 +631,13 @@ export class Deparser implements DeparserVisitor { // Add parentheses for complex expressions if (lexpr && this.isComplexExpression(lexpr)) { - leftExpr = this.formatter.parens(leftExpr); + leftExpr = context.formatter.parens(leftExpr); } if (rexpr && this.isComplexExpression(rexpr)) { - rightExpr = this.formatter.parens(rightExpr); + rightExpr = context.formatter.parens(rightExpr); } - return this.formatter.format([ + return context.formatter.format([ leftExpr, 'IS DISTINCT FROM', rightExpr @@ -639,22 +649,22 @@ export class Deparser implements DeparserVisitor { // Add parentheses for complex expressions if (lexpr && this.isComplexExpression(lexpr)) { - leftExpr = this.formatter.parens(leftExpr); + leftExpr = context.formatter.parens(leftExpr); } if (rexpr && this.isComplexExpression(rexpr)) { - rightExpr = this.formatter.parens(rightExpr); + rightExpr = context.formatter.parens(rightExpr); } - return this.formatter.format([ + return context.formatter.format([ leftExpr, 'IS NOT DISTINCT FROM', rightExpr ]); } case 'AEXPR_NULLIF': - return this.formatter.format([ + return context.formatter.format([ 'NULLIF', - this.formatter.parens([ + context.formatter.parens([ this.visit(lexpr, context), this.visit(rexpr, context) ].join(', ')) @@ -662,28 +672,28 @@ export class Deparser implements DeparserVisitor { case 'AEXPR_IN': const inOperator = this.deparseOperatorName(name, context); if (inOperator === '<>' || inOperator === '!=') { - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'NOT IN', - this.formatter.parens(this.visit(rexpr, context)) + context.formatter.parens(this.visit(rexpr, context)) ]); } else { - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'IN', - this.formatter.parens(this.visit(rexpr, context)) + context.formatter.parens(this.visit(rexpr, context)) ]); } case 'AEXPR_LIKE': const likeOp = this.deparseOperatorName(name, context); if (likeOp === '!~~') { - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'NOT LIKE', this.visit(rexpr, context) ]); } else { - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'LIKE', this.visit(rexpr, context) @@ -692,13 +702,13 @@ export class Deparser implements DeparserVisitor { case 'AEXPR_ILIKE': const ilikeOp = this.deparseOperatorName(name, context); if (ilikeOp === '!~~*') { - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'NOT ILIKE', this.visit(rexpr, context) ]); } else { - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'ILIKE', this.visit(rexpr, context) @@ -722,38 +732,38 @@ export class Deparser implements DeparserVisitor { } if (similarOp === '!~') { - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'NOT SIMILAR TO', rightExpr ]); } else { - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'SIMILAR TO', rightExpr ]); } case 'AEXPR_BETWEEN': - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'BETWEEN', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_NOT_BETWEEN': - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'NOT BETWEEN', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_BETWEEN_SYM': - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'BETWEEN SYMMETRIC', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_NOT_BETWEEN_SYM': - return this.formatter.format([ + return context.formatter.format([ this.visit(lexpr, context), 'NOT BETWEEN SYMMETRIC', this.visitBetweenRange(rexpr, context) @@ -937,12 +947,12 @@ export class Deparser implements DeparserVisitor { const insertContext = context.spawn('InsertStmt', { insertColumns: true }); const columnNames = cols.map(col => this.visit(col as Node, insertContext)); - if (this.formatter.isPretty()) { + if (context.formatter.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.formatter.indent(col)); output.push('(\n' + indentedColumns.join(',\n') + '\n)'); } else { - output.push(this.formatter.parens(columnNames.join(', '))); + output.push(context.formatter.parens(columnNames.join(', '))); } } @@ -965,7 +975,7 @@ 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.formatter.parens(indexElems.join(', '))); } // Handle WHERE clause for conflict detection @@ -985,7 +995,7 @@ export class Deparser implements DeparserVisitor { 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.formatter.parens(names.join(', '))); output.push('='); output.push(this.visit(firstTarget.ResTarget.val.MultiAssignRef.source, context)); } else { @@ -1054,7 +1064,7 @@ export class Deparser implements DeparserVisitor { } const names = relatedTargets.map(t => t.ResTarget.name); - const multiAssignment = `${this.formatter.parens(names.join(', '))} = ${this.visit(multiAssignRef.source, context)}`; + const multiAssignment = `${context.formatter.parens(names.join(', '))} = ${this.visit(multiAssignRef.source, context)}`; assignmentParts.push(multiAssignment); } else { // Handle regular single-column assignment @@ -1161,14 +1171,14 @@ export class Deparser implements DeparserVisitor { if (node.ctes && node.ctes.length > 0) { const ctes = ListUtils.unwrapList(node.ctes); - if (this.formatter.isPretty()) { + if (context.formatter.isPretty()) { const cteStrings = ctes.map((cte, index) => { const cteStr = this.visit(cte, context); - const prefix = index === 0 ? this.formatter.newline() : ',' + this.formatter.newline(); + const prefix = index === 0 ? context.formatter.newline() : ',' + context.formatter.newline(); if (this.containsMultilineStringLiteral(cteStr)) { return prefix + cteStr; } - return prefix + this.formatter.indent(cteStr); + return prefix + context.formatter.indent(cteStr); }); output.push(cteStrings.join('')); } else { @@ -1271,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.formatter.isPretty() && args.length > 1) { + const andArgs = args.map(arg => this.visit(arg, boolContext)).join(context.formatter.newline() + ' 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.formatter.isPretty() && args.length > 1) { + const orArgs = args.map(arg => this.visit(arg, boolContext)).join(context.formatter.newline() + ' OR '); return formatStr.replace('%s', () => orArgs); } else { const orArgs = args.map(arg => this.visit(arg, boolContext)).join(' OR '); @@ -1422,7 +1432,7 @@ export class Deparser implements DeparserVisitor { 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), context); if (op === '+' || op === '-' || op === '*' || op === '/') { - timestamp = this.formatter.parens(timestamp); + timestamp = context.formatter.parens(timestamp); } } @@ -1503,9 +1513,9 @@ export class Deparser implements DeparserVisitor { } 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.formatter.isPretty() && windowParts.length > 1) { + const formattedParts = windowParts.map(part => context.formatter.indent(part)); + result += ` OVER (${context.formatter.newline()}${formattedParts.join(context.formatter.newline())}${context.formatter.newline()})`; } else { result += ` OVER (${windowParts.join(' ')})`; } @@ -1858,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.formatter.parens(quotedColnames.join(', '))); } else { output.push('AS', this.quoteIfNeeded(name)); } @@ -2165,26 +2175,26 @@ export class Deparser implements DeparserVisitor { const args = ListUtils.unwrapList(node.args); - if (this.formatter.isPretty() && args.length > 0) { + if (context.formatter.isPretty() && args.length > 0) { for (const arg of args) { const whenClause = this.visit(arg, context); if (this.containsMultilineStringLiteral(whenClause)) { - output.push(this.formatter.newline() + whenClause); + output.push(context.formatter.newline() + whenClause); } else { - output.push(this.formatter.newline() + this.formatter.indent(whenClause)); + output.push(context.formatter.newline() + context.formatter.indent(whenClause)); } } if (node.defresult) { const elseResult = this.visit(node.defresult, context); if (this.containsMultilineStringLiteral(elseResult)) { - output.push(this.formatter.newline() + 'ELSE ' + elseResult); + output.push(context.formatter.newline() + 'ELSE ' + elseResult); } else { - output.push(this.formatter.newline() + this.formatter.indent('ELSE ' + elseResult)); + output.push(context.formatter.newline() + context.formatter.indent('ELSE ' + elseResult)); } } - output.push(this.formatter.newline() + 'END'); + output.push(context.formatter.newline() + 'END'); return output.join(' '); } else { for (const arg of args) { @@ -2450,7 +2460,7 @@ export class Deparser implements DeparserVisitor { const elementStrs = elements.map(el => { return this.deparse(el, context); }); - output.push(this.formatter.parens(elementStrs.join(', '))); + output.push(context.formatter.parens(elementStrs.join(', '))); } } else if (node.tableElts) { const elements = ListUtils.unwrapList(node.tableElts); @@ -2458,21 +2468,21 @@ export class Deparser implements DeparserVisitor { return this.deparse(el, context); }); - if (this.formatter.isPretty()) { + if (context.formatter.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.formatter.indent(trimmedEl.substring(1)); } - return this.formatter.indent(trimmedEl); - }).join(',' + this.formatter.newline()); - output.push('(' + this.formatter.newline() + formattedElements + this.formatter.newline() + ')'); + return context.formatter.indent(trimmedEl); + }).join(',' + context.formatter.newline()); + output.push('(' + context.formatter.newline() + formattedElements + context.formatter.newline() + ')'); } else { - output.push(this.formatter.parens(elementStrs.join(', '))); + output.push(context.formatter.parens(elementStrs.join(', '))); } }else if (!node.partbound) { - output.push(this.formatter.parens('')); + output.push(context.formatter.parens('')); } if (node.partbound && node.inhRelations && node.inhRelations.length > 0) { @@ -2513,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.formatter.parens(inheritStrs.join(', '))); } if (node.partspec) { @@ -2642,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.formatter.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.formatter.indent('CHECK')); } else { output.push('CHECK'); } if (node.raw_expr) { - if (this.formatter.isPretty()) { + if (context.formatter.isPretty()) { const checkExpr = this.visit(node.raw_expr, context); if (checkExpr.includes('\n')) { - output.push('(\n' + this.formatter.indent(checkExpr) + '\n)'); + output.push('(\n' + context.formatter.indent(checkExpr) + '\n)'); } else { output.push(`(${checkExpr})`); } } else { - output.push(this.formatter.parens(this.visit(node.raw_expr, context))); + output.push(context.formatter.parens(this.visit(node.raw_expr, context))); } } // Handle NOT VALID for check constraints @@ -2677,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.formatter.parens(this.visit(node.raw_expr, context))); } output.push('STORED'); break; @@ -2729,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.formatter.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.formatter.indent('UNIQUE')); } else { output.push('UNIQUE'); } @@ -2751,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.formatter.isPretty()) { + output.push('\n' + context.formatter.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.formatter.indent('REFERENCES')); } else { output.push('FOREIGN KEY'); if (node.fk_attrs && node.fk_attrs.length > 0) { @@ -2774,7 +2784,7 @@ export class Deparser implements DeparserVisitor { output.push('REFERENCES'); } if (node.pktable) { - if (this.formatter.isPretty() && !context.isColumnConstraint) { + if (context.formatter.isPretty() && !context.isColumnConstraint) { const lastIndex = output.length - 1; if (lastIndex >= 0 && output[lastIndex].includes('REFERENCES')) { output[lastIndex] += ' ' + this.RangeVar(node.pktable, context); @@ -2789,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.formatter.isPretty() && !context.isColumnConstraint) { const lastIndex = output.length - 1; if (lastIndex >= 0) { output[lastIndex] += ` (${pkAttrs})`; @@ -2810,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.formatter.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.formatter.indent(matchClause)); } else { output.push(matchClause); } @@ -2832,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.formatter.isPretty()) { + output.push('\n' + context.formatter.indent(updateClause)); } else { output.push('ON UPDATE'); output.push(updateClause.replace('ON UPDATE ', '')); @@ -2855,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.formatter.isPretty()) { + output.push('\n' + context.formatter.indent(deleteClause)); } else { output.push('ON DELETE'); output.push(deleteClause.replace('ON DELETE ', '')); @@ -2864,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.formatter.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.formatter.indent('NOT VALID')); } else { output.push('NOT VALID'); } @@ -2921,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.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') { + output.push('\n' + context.formatter.indent('DEFERRABLE')); if (node.initdeferred === true) { - output.push('\n' + this.formatter.indent('INITIALLY DEFERRED')); + output.push('\n' + context.formatter.indent('INITIALLY DEFERRED')); } else if (node.initdeferred === false) { - output.push('\n' + this.formatter.indent('INITIALLY IMMEDIATE')); + output.push('\n' + context.formatter.indent('INITIALLY IMMEDIATE')); } } else { output.push('DEFERRABLE'); @@ -2937,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.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') { + output.push('\n' + context.formatter.indent('NOT DEFERRABLE')); } else { output.push('NOT DEFERRABLE'); } } } - if (this.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') { + if (context.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') { let result = ''; for (let i = 0; i < output.length; i++) { if (output[i].startsWith('\n')) { @@ -2963,7 +2973,7 @@ export class Deparser implements DeparserVisitor { } SubLink(node: t.SubLink, context: DeparserContext): string { - const subselect = this.formatter.parens(this.visit(node.subselect, context)); + const subselect = context.formatter.parens(this.visit(node.subselect, context)); switch (node.subLinkType) { case 'ANY_SUBLINK': @@ -3209,7 +3219,7 @@ export class Deparser implements DeparserVisitor { 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.formatter.parens(colnameStrs.join(', ')); } output.push('AS'); @@ -3222,7 +3232,7 @@ export class Deparser implements DeparserVisitor { } if (node.ctequery) { - output.push(this.formatter.parens(this.visit(node.ctequery, context))); + output.push(context.formatter.parens(this.visit(node.ctequery, context))); } return output.join(' '); @@ -3480,7 +3490,7 @@ export class Deparser implements DeparserVisitor { 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.formatter.parens(aliasStrs.join(', '))); } if (node.options && node.options.length > 0) { @@ -3546,13 +3556,13 @@ export class Deparser implements DeparserVisitor { 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.formatter.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.formatter.parens(includeStrs.join(', '))); } if (node.whereClause) { @@ -3564,7 +3574,7 @@ export class Deparser implements DeparserVisitor { 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.formatter.parens(optionStrs.join(', '))); } if (node.nulls_not_distinct) { @@ -3585,7 +3595,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.formatter.parens(this.visit(node.expr, context))); } if (node.collation && node.collation.length > 0) { @@ -3644,7 +3654,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.formatter.parens(this.visit(node.expr, context))); } if (node.collation && node.collation.length > 0) { @@ -3809,14 +3819,14 @@ export class Deparser implements DeparserVisitor { rargStr = `(${rargStr})`; } - if (this.formatter.isPretty()) { - output.push(this.formatter.newline() + joinStr + ' ' + rargStr); + if (context.formatter.isPretty()) { + output.push(context.formatter.newline() + joinStr + ' ' + rargStr); } else { output.push(joinStr + ' ' + rargStr); } } else { - if (this.formatter.isPretty()) { - output.push(this.formatter.newline() + joinStr); + if (context.formatter.isPretty()) { + output.push(context.formatter.newline() + joinStr); } else { output.push(joinStr); } @@ -3825,20 +3835,20 @@ export class Deparser implements DeparserVisitor { 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.formatter.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.formatter.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.formatter.newline()}${context.formatter.indent(qualsStr)}`); } } else { output.push(` ON ${qualsStr}`); @@ -3849,7 +3859,7 @@ export class Deparser implements DeparserVisitor { } let result; - if (this.formatter.isPretty()) { + if (context.formatter.isPretty()) { result = output.join(''); } else { result = output.join(' '); @@ -4653,12 +4663,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.formatter.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.formatter.newline() + context.formatter.indent(cmdStr); } return cmdStr; }) @@ -6780,8 +6790,8 @@ export class Deparser implements DeparserVisitor { // 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.formatter.isPretty()) { + output.push(context.formatter.newline() + context.formatter.indent(`ON ${this.RangeVar(node.table, context)}`)); } else { output.push('ON'); output.push(this.RangeVar(node.table, context)); @@ -6790,22 +6800,22 @@ export class Deparser implements DeparserVisitor { // 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.formatter.isPretty()) { + output.push(context.formatter.newline() + context.formatter.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.formatter.isPretty()) { + output.push(context.formatter.newline() + context.formatter.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.formatter.isPretty()) { + output.push(context.formatter.newline() + context.formatter.indent(`FOR ${node.cmd_name.toUpperCase()}`)); } else { output.push('FOR', node.cmd_name.toUpperCase()); } @@ -6813,8 +6823,8 @@ export class Deparser implements DeparserVisitor { 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.formatter.isPretty()) { + output.push(context.formatter.newline() + context.formatter.indent(`TO ${roles.join(', ')}`)); } else { output.push('TO'); output.push(roles.join(', ')); @@ -6822,11 +6832,11 @@ export class Deparser implements DeparserVisitor { } if (node.qual) { - if (this.formatter.isPretty()) { + if (context.formatter.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.formatter.newline() + context.formatter.indent('USING (')); + output.push(context.formatter.newline() + context.formatter.indent(context.formatter.indent(qualExpr))); + output.push(context.formatter.newline() + context.formatter.indent(')')); } else { output.push('USING'); output.push(`(${this.visit(node.qual, context)})`); @@ -6834,18 +6844,18 @@ export class Deparser implements DeparserVisitor { } if (node.with_check) { - if (this.formatter.isPretty()) { + if (context.formatter.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.formatter.newline() + context.formatter.indent('WITH CHECK (')); + output.push(context.formatter.newline() + context.formatter.indent(context.formatter.indent(checkExpr))); + output.push(context.formatter.newline() + context.formatter.indent(')')); } else { output.push('WITH CHECK'); output.push(`(${this.visit(node.with_check, context)})`); } } - return this.formatter.isPretty() ? output.join('') : output.join(' '); + return context.formatter.isPretty() ? output.join('') : output.join(' '); } AlterPolicyStmt(node: t.AlterPolicyStmt, context: DeparserContext): string { @@ -8545,8 +8555,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.formatter.isPretty()) { + return output.join(' ') + context.formatter.newline() + context.formatter.indent(actionStr); } else { output.push(actionStr); } @@ -8733,7 +8743,7 @@ export class Deparser implements DeparserVisitor { output.push(QuoteUtils.quote(node.trigname)); } - if (this.formatter.isPretty()) { + if (context.formatter.isPretty()) { const components: string[] = []; const timing: string[] = []; @@ -8756,36 +8766,36 @@ export class Deparser implements DeparserVisitor { } if (node.events & 32) events.push('TRUNCATE'); - components.push(this.formatter.indent(timing.join(' ') + ' ' + events.join(' OR '))); + components.push(context.formatter.indent(timing.join(' ') + ' ' + events.join(' OR '))); if (node.relation) { - components.push(this.formatter.indent('ON ' + this.RangeVar(node.relation, context))); + components.push(context.formatter.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.formatter.indent('REFERENCING ' + transitionClauses)); } if (node.deferrable) { - components.push(this.formatter.indent('DEFERRABLE')); + components.push(context.formatter.indent('DEFERRABLE')); } if (node.initdeferred) { - components.push(this.formatter.indent('INITIALLY DEFERRED')); + components.push(context.formatter.indent('INITIALLY DEFERRED')); } if (node.row) { - components.push(this.formatter.indent('FOR EACH ROW')); + components.push(context.formatter.indent('FOR EACH ROW')); } else { - components.push(this.formatter.indent('FOR EACH STATEMENT')); + components.push(context.formatter.indent('FOR EACH STATEMENT')); } if (node.whenClause) { const whenStr = 'WHEN (' + this.visit(node.whenClause, context) + ')'; - components.push(this.formatter.indent(whenStr)); + components.push(context.formatter.indent(whenStr)); } let executeStr = 'EXECUTE'; @@ -8806,9 +8816,9 @@ export class Deparser implements DeparserVisitor { executeStr += '()'; } - components.push(this.formatter.indent(executeStr)); + components.push(context.formatter.indent(executeStr)); - return output.join(' ') + this.formatter.newline() + components.join(this.formatter.newline()); + return output.join(' ') + context.formatter.newline() + components.join(context.formatter.newline()); } else { const timing: string[] = []; if (node.timing & 2) timing.push('BEFORE'); diff --git a/packages/deparser/src/visitors/base.ts b/packages/deparser/src/visitors/base.ts index bfb450da..df13b0e8 100644 --- a/packages/deparser/src/visitors/base.ts +++ b/packages/deparser/src/visitors/base.ts @@ -1,10 +1,12 @@ import { Node } from '@pgsql/types'; +import { SqlFormatter } from '../utils/sql-formatter'; export interface DeparserContextOptions { isStringLiteral?: boolean; parentNodeTypes?: string[]; indentLevel?: number; prettyMode?: boolean; + formatter?: SqlFormatter; select?: boolean; from?: boolean; group?: boolean; @@ -27,6 +29,7 @@ export class DeparserContext { readonly prettyMode: boolean; readonly isStringLiteral?: boolean; readonly parentNodeTypes: string[]; + readonly formatter: SqlFormatter; readonly select?: boolean; readonly from?: boolean; readonly group?: boolean; @@ -48,6 +51,7 @@ export class DeparserContext { prettyMode = false, isStringLiteral, parentNodeTypes = [], + formatter, select, from, group, @@ -68,6 +72,7 @@ export class DeparserContext { 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; @@ -92,6 +97,7 @@ export class DeparserContext { prettyMode: this.prettyMode, isStringLiteral: this.isStringLiteral, parentNodeTypes: [...this.parentNodeTypes, nodeType], + formatter: this.formatter, select: this.select, from: this.from, group: this.group, From 2a76b3d3f7e63837d9a3d3bc5a04d16a519b9278 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Jun 2025 15:57:49 +0000 Subject: [PATCH 08/10] feat: make SqlFormatter private and add context-aware indentation - Make SqlFormatter private property on DeparserContext - Add public methods: indent(), newline(), parens(), format(), isPretty() - Replace all context.formatter.* references with direct context methods - Fix CREATE TABLE constraint indentation using context.indentLevel - UNIQUE and FOREIGN KEY constraints now properly indented relative to table structure - Context spawning with increased indent levels for table elements - All 279 test suites continue to pass Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 353 +++++++++++++------------ packages/deparser/src/visitors/base.ts | 26 +- 2 files changed, 202 insertions(+), 177 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 0124baee..027eb67a 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -170,7 +170,7 @@ 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 }); + const context = new DeparserContext({ formatter, prettyMode: this.options.pretty }); return this.tree .map(node => { // All nodes should go through the standard deparse method @@ -179,7 +179,7 @@ export class Deparser implements DeparserVisitor { return result || ''; }) .filter(result => result !== '') - .join(context.formatter.newline() + context.formatter.newline()); + .join(context.newline() + context.newline()); } /** @@ -202,7 +202,7 @@ export class Deparser implements DeparserVisitor { if (!context) { const formatter = new SqlFormatter(this.options.newline, this.options.tab, this.options.pretty); - context = new DeparserContext({ formatter }); + context = new DeparserContext({ formatter, prettyMode: this.options.pretty }); } if (typeof node === 'number' || node instanceof Number) { @@ -220,7 +220,7 @@ export class Deparser implements DeparserVisitor { 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 }); + context = new DeparserContext({ formatter, prettyMode: this.options.pretty }); } const nodeType = this.getNodeType(node); @@ -266,7 +266,7 @@ export class Deparser implements DeparserVisitor { .filter((rawStmt: t.RawStmt) => rawStmt != null) .map((rawStmt: t.RawStmt) => this.RawStmt(rawStmt, context)) .filter((result: string) => result !== '') - .join(context.formatter.newline() + context.formatter.newline()); + .join(context.newline() + context.newline()); } RawStmt(node: t.RawStmt, context: DeparserContext): string { @@ -292,7 +292,7 @@ export class Deparser implements DeparserVisitor { if (!node.op || node.op === 'SETOP_NONE') { if (node.valuesLists == null) { - if (!context.formatter.isPretty() || !node.targetList) { + if (!context.isPretty() || !node.targetList) { output.push('SELECT'); } } @@ -317,7 +317,7 @@ export class Deparser implements DeparserVisitor { ); if (leftNeedsParens) { - output.push(context.formatter.parens(leftStmt)); + output.push(context.parens(leftStmt)); } else { output.push(leftStmt); } @@ -341,7 +341,7 @@ export class Deparser implements DeparserVisitor { } if (rightNeedsParens) { - output.push(context.formatter.parens(rightStmt)); + output.push(context.parens(rightStmt)); } else { output.push(rightStmt); } @@ -355,18 +355,18 @@ export class Deparser implements DeparserVisitor { const clause = distinctClause .map(e => this.visit(e as Node, context.spawn('SelectStmt', { select: true }))) .join(', '); - distinctPart = ' DISTINCT ON ' + context.formatter.parens(clause); + distinctPart = ' DISTINCT ON ' + context.parens(clause); } else { distinctPart = ' DISTINCT'; } - if (!context.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.spawn('SelectStmt', { select: true }))) .join(', '); - output.push(context.formatter.parens(clause)); + output.push(context.parens(clause)); } else { output.push('DISTINCT'); } @@ -375,7 +375,7 @@ export class Deparser implements DeparserVisitor { if (node.targetList) { const targetList = ListUtils.unwrapList(node.targetList); - if (context.formatter.isPretty()) { + if (context.isPretty()) { if (targetList.length === 1) { const targetNode = targetList[0] as Node; const target = this.visit(targetNode, context.spawn('SelectStmt', { select: true })); @@ -386,7 +386,7 @@ export class Deparser implements DeparserVisitor { if (this.containsMultilineStringLiteral(target)) { output.push(target); } else { - output.push(context.formatter.indent(target)); + output.push(context.indent(target)); } } else { output.push('SELECT' + distinctPart + ' ' + target); @@ -398,9 +398,9 @@ export class Deparser implements DeparserVisitor { if (this.containsMultilineStringLiteral(targetStr)) { return targetStr; } - return context.formatter.indent(targetStr); + return context.indent(targetStr); }); - const formattedTargets = targetStrings.join(',' + context.formatter.newline()); + const formattedTargets = targetStrings.join(',' + context.newline()); output.push('SELECT' + distinctPart); output.push(formattedTargets); } @@ -426,17 +426,17 @@ export class Deparser implements DeparserVisitor { } if (node.whereClause) { - if (context.formatter.isPretty()) { + if (context.isPretty()) { output.push('WHERE'); const whereExpr = this.visit(node.whereClause as Node, context); - const lines = whereExpr.split(context.formatter.newline()); + const lines = whereExpr.split(context.newline()); const indentedLines = lines.map((line, index) => { if (index === 0) { - return context.formatter.indent(line); + return context.indent(line); } return line; }); - output.push(indentedLines.join(context.formatter.newline())); + output.push(indentedLines.join(context.newline())); } else { output.push('WHERE'); output.push(this.visit(node.whereClause as Node, context)); @@ -444,24 +444,24 @@ export class Deparser implements DeparserVisitor { } if (node.valuesLists) { - if (context.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 context.formatter.parens(values.join(', ')); + return context.parens(values.join(', ')); }); const indentedTuples = lists.map(tuple => { if (this.containsMultilineStringLiteral(tuple)) { return tuple; } - return context.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 context.formatter.parens(values.join(', ')); + return context.parens(values.join(', ')); }); output.push(lists.join(', ')); } @@ -469,16 +469,16 @@ export class Deparser implements DeparserVisitor { if (node.groupClause) { const groupList = ListUtils.unwrapList(node.groupClause); - if (context.formatter.isPretty()) { + if (context.isPretty()) { const groupItems = groupList .map(e => { const groupStr = this.visit(e as Node, context.spawn('SelectStmt', { group: true })); if (this.containsMultilineStringLiteral(groupStr)) { return groupStr; } - return context.formatter.indent(groupStr); + return context.indent(groupStr); }) - .join(',' + context.formatter.newline()); + .join(',' + context.newline()); output.push('GROUP BY'); output.push(groupItems); } else { @@ -491,13 +491,13 @@ export class Deparser implements DeparserVisitor { } if (node.havingClause) { - if (context.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(context.formatter.indent(havingStr)); + output.push(context.indent(havingStr)); } } else { output.push('HAVING'); @@ -516,16 +516,16 @@ export class Deparser implements DeparserVisitor { if (node.sortClause) { const sortList = ListUtils.unwrapList(node.sortClause); - if (context.formatter.isPretty()) { + if (context.isPretty()) { const sortItems = sortList .map(e => { const sortStr = this.visit(e as Node, context.spawn('SelectStmt', { sort: true })); if (this.containsMultilineStringLiteral(sortStr)) { return sortStr; } - return context.formatter.indent(sortStr); + return context.indent(sortStr); }) - .join(',' + context.formatter.newline()); + .join(',' + context.newline()); output.push('ORDER BY'); output.push(sortItems); } else { @@ -553,9 +553,9 @@ export class Deparser implements DeparserVisitor { output.push(lockingClauses); } - if (context.formatter.isPretty()) { + if (context.isPretty()) { const filteredOutput = output.filter(item => item.trim() !== ''); - return filteredOutput.join(context.formatter.newline()); + return filteredOutput.join(context.newline()); } return output.join(' '); } @@ -585,7 +585,7 @@ export class Deparser implements DeparserVisitor { leftNeedsParens = true; } if (leftNeedsParens) { - leftExpr = context.formatter.parens(leftExpr); + leftExpr = context.parens(leftExpr); } // Check if right expression needs parentheses @@ -600,30 +600,30 @@ export class Deparser implements DeparserVisitor { rightNeedsParens = true; } if (rightNeedsParens) { - rightExpr = context.formatter.parens(rightExpr); + rightExpr = context.parens(rightExpr); } - return context.formatter.format([leftExpr, operator, rightExpr]); + return context.format([leftExpr, operator, rightExpr]); }else if (rexpr) { - return context.formatter.format([ + return context.format([ this.deparseOperatorName(name, context), this.visit(rexpr, context) ]); } break; case 'AEXPR_OP_ANY': - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), this.deparseOperatorName(name, context), 'ANY', - context.formatter.parens(this.visit(rexpr, context)) + context.parens(this.visit(rexpr, context)) ]); case 'AEXPR_OP_ALL': - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), this.deparseOperatorName(name, context), 'ALL', - context.formatter.parens(this.visit(rexpr, context)) + context.parens(this.visit(rexpr, context)) ]); case 'AEXPR_DISTINCT': { let leftExpr = this.visit(lexpr, context); @@ -631,13 +631,13 @@ export class Deparser implements DeparserVisitor { // Add parentheses for complex expressions if (lexpr && this.isComplexExpression(lexpr)) { - leftExpr = context.formatter.parens(leftExpr); + leftExpr = context.parens(leftExpr); } if (rexpr && this.isComplexExpression(rexpr)) { - rightExpr = context.formatter.parens(rightExpr); + rightExpr = context.parens(rightExpr); } - return context.formatter.format([ + return context.format([ leftExpr, 'IS DISTINCT FROM', rightExpr @@ -649,22 +649,22 @@ export class Deparser implements DeparserVisitor { // Add parentheses for complex expressions if (lexpr && this.isComplexExpression(lexpr)) { - leftExpr = context.formatter.parens(leftExpr); + leftExpr = context.parens(leftExpr); } if (rexpr && this.isComplexExpression(rexpr)) { - rightExpr = context.formatter.parens(rightExpr); + rightExpr = context.parens(rightExpr); } - return context.formatter.format([ + return context.format([ leftExpr, 'IS NOT DISTINCT FROM', rightExpr ]); } case 'AEXPR_NULLIF': - return context.formatter.format([ + return context.format([ 'NULLIF', - context.formatter.parens([ + context.parens([ this.visit(lexpr, context), this.visit(rexpr, context) ].join(', ')) @@ -672,28 +672,28 @@ export class Deparser implements DeparserVisitor { case 'AEXPR_IN': const inOperator = this.deparseOperatorName(name, context); if (inOperator === '<>' || inOperator === '!=') { - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT IN', - context.formatter.parens(this.visit(rexpr, context)) + context.parens(this.visit(rexpr, context)) ]); } else { - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'IN', - context.formatter.parens(this.visit(rexpr, context)) + context.parens(this.visit(rexpr, context)) ]); } case 'AEXPR_LIKE': const likeOp = this.deparseOperatorName(name, context); if (likeOp === '!~~') { - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT LIKE', this.visit(rexpr, context) ]); } else { - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'LIKE', this.visit(rexpr, context) @@ -702,13 +702,13 @@ export class Deparser implements DeparserVisitor { case 'AEXPR_ILIKE': const ilikeOp = this.deparseOperatorName(name, context); if (ilikeOp === '!~~*') { - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT ILIKE', this.visit(rexpr, context) ]); } else { - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'ILIKE', this.visit(rexpr, context) @@ -732,38 +732,38 @@ export class Deparser implements DeparserVisitor { } if (similarOp === '!~') { - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT SIMILAR TO', rightExpr ]); } else { - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'SIMILAR TO', rightExpr ]); } case 'AEXPR_BETWEEN': - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'BETWEEN', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_NOT_BETWEEN': - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT BETWEEN', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_BETWEEN_SYM': - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'BETWEEN SYMMETRIC', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_NOT_BETWEEN_SYM': - return context.formatter.format([ + return context.format([ this.visit(lexpr, context), 'NOT BETWEEN SYMMETRIC', this.visitBetweenRange(rexpr, context) @@ -947,12 +947,12 @@ export class Deparser implements DeparserVisitor { const insertContext = context.spawn('InsertStmt', { insertColumns: true }); const columnNames = cols.map(col => this.visit(col as Node, insertContext)); - if (context.formatter.isPretty()) { + if (context.isPretty()) { // Always format columns in multiline parentheses for pretty printing - const indentedColumns = columnNames.map(col => context.formatter.indent(col)); + const indentedColumns = columnNames.map(col => context.indent(col)); output.push('(\n' + indentedColumns.join(',\n') + '\n)'); } else { - output.push(context.formatter.parens(columnNames.join(', '))); + output.push(context.parens(columnNames.join(', '))); } } @@ -975,7 +975,7 @@ 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(context.formatter.parens(indexElems.join(', '))); + output.push(context.parens(indexElems.join(', '))); } // Handle WHERE clause for conflict detection @@ -995,7 +995,7 @@ export class Deparser implements DeparserVisitor { 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(context.formatter.parens(names.join(', '))); + output.push(context.parens(names.join(', '))); output.push('='); output.push(this.visit(firstTarget.ResTarget.val.MultiAssignRef.source, context)); } else { @@ -1064,7 +1064,7 @@ export class Deparser implements DeparserVisitor { } const names = relatedTargets.map(t => t.ResTarget.name); - const multiAssignment = `${context.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 @@ -1171,14 +1171,14 @@ export class Deparser implements DeparserVisitor { if (node.ctes && node.ctes.length > 0) { const ctes = ListUtils.unwrapList(node.ctes); - if (context.formatter.isPretty()) { + if (context.isPretty()) { const cteStrings = ctes.map((cte, index) => { const cteStr = this.visit(cte, context); - const prefix = index === 0 ? context.formatter.newline() : ',' + context.formatter.newline(); + const prefix = index === 0 ? context.newline() : ',' + context.newline(); if (this.containsMultilineStringLiteral(cteStr)) { return prefix + cteStr; } - return prefix + context.formatter.indent(cteStr); + return prefix + context.indent(cteStr); }); output.push(cteStrings.join('')); } else { @@ -1281,16 +1281,16 @@ export class Deparser implements DeparserVisitor { switch (boolop) { case 'AND_EXPR': - if (context.formatter.isPretty() && args.length > 1) { - const andArgs = args.map(arg => this.visit(arg, boolContext)).join(context.formatter.newline() + ' AND '); + if (context.isPretty() && args.length > 1) { + const andArgs = args.map(arg => this.visit(arg, boolContext)).join(context.newline() + ' 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 (context.formatter.isPretty() && args.length > 1) { - const orArgs = args.map(arg => this.visit(arg, boolContext)).join(context.formatter.newline() + ' OR '); + if (context.isPretty() && args.length > 1) { + const orArgs = args.map(arg => this.visit(arg, boolContext)).join(context.newline() + ' OR '); return formatStr.replace('%s', () => orArgs); } else { const orArgs = args.map(arg => this.visit(arg, boolContext)).join(' OR '); @@ -1432,7 +1432,7 @@ export class Deparser implements DeparserVisitor { 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), context); if (op === '+' || op === '-' || op === '*' || op === '/') { - timestamp = context.formatter.parens(timestamp); + timestamp = context.parens(timestamp); } } @@ -1513,9 +1513,9 @@ export class Deparser implements DeparserVisitor { } if (windowParts.length > 0) { - if (context.formatter.isPretty() && windowParts.length > 1) { - const formattedParts = windowParts.map(part => context.formatter.indent(part)); - result += ` OVER (${context.formatter.newline()}${formattedParts.join(context.formatter.newline())}${context.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(' ')})`; } @@ -1868,7 +1868,7 @@ export class Deparser implements DeparserVisitor { } return this.quoteIfNeeded(colStr); }); - output.push('AS', this.quoteIfNeeded(name) + context.formatter.parens(quotedColnames.join(', '))); + output.push('AS', this.quoteIfNeeded(name) + context.parens(quotedColnames.join(', '))); } else { output.push('AS', this.quoteIfNeeded(name)); } @@ -2175,26 +2175,26 @@ export class Deparser implements DeparserVisitor { const args = ListUtils.unwrapList(node.args); - if (context.formatter.isPretty() && args.length > 0) { + if (context.isPretty() && args.length > 0) { for (const arg of args) { const whenClause = this.visit(arg, context); if (this.containsMultilineStringLiteral(whenClause)) { - output.push(context.formatter.newline() + whenClause); + output.push(context.newline() + whenClause); } else { - output.push(context.formatter.newline() + context.formatter.indent(whenClause)); + output.push(context.newline() + context.indent(whenClause)); } } if (node.defresult) { const elseResult = this.visit(node.defresult, context); if (this.containsMultilineStringLiteral(elseResult)) { - output.push(context.formatter.newline() + 'ELSE ' + elseResult); + output.push(context.newline() + 'ELSE ' + elseResult); } else { - output.push(context.formatter.newline() + context.formatter.indent('ELSE ' + elseResult)); + output.push(context.newline() + context.indent('ELSE ' + elseResult)); } } - output.push(context.formatter.newline() + 'END'); + output.push(context.newline() + 'END'); return output.join(' '); } else { for (const arg of args) { @@ -2460,29 +2460,30 @@ export class Deparser implements DeparserVisitor { const elementStrs = elements.map(el => { return this.deparse(el, context); }); - output.push(context.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); + const tableElementContext = context.spawn('CreateStmt', { indentLevel: context.indentLevel + 1 }); + return this.deparse(el, tableElementContext); }); - if (context.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 context.formatter.indent(trimmedEl.substring(1)); + return ' ' + trimmedEl.substring(1); } - return context.formatter.indent(trimmedEl); - }).join(',' + context.formatter.newline()); - output.push('(' + context.formatter.newline() + formattedElements + context.formatter.newline() + ')'); + return ' ' + trimmedEl; + }).join(',' + context.newline()); + output.push('(' + context.newline() + formattedElements + context.newline() + ')'); } else { - output.push(context.formatter.parens(elementStrs.join(', '))); + output.push(context.parens(elementStrs.join(', '))); } }else if (!node.partbound) { - output.push(context.formatter.parens('')); + output.push(context.parens('')); } if (node.partbound && node.inhRelations && node.inhRelations.length > 0) { @@ -2523,7 +2524,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(context.formatter.parens(inheritStrs.join(', '))); + output.push(context.parens(inheritStrs.join(', '))); } if (node.partspec) { @@ -2652,21 +2653,21 @@ export class Deparser implements DeparserVisitor { } break; case 'CONSTR_CHECK': - if (context.formatter.isPretty() && !context.isColumnConstraint) { - output.push('\n' + context.formatter.indent('CHECK')); + if (context.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.indent('CHECK')); } else { output.push('CHECK'); } if (node.raw_expr) { - if (context.formatter.isPretty()) { + if (context.isPretty()) { const checkExpr = this.visit(node.raw_expr, context); if (checkExpr.includes('\n')) { - output.push('(\n' + context.formatter.indent(checkExpr) + '\n)'); + output.push('(\n' + context.indent(checkExpr) + '\n)'); } else { output.push(`(${checkExpr})`); } } else { - output.push(context.formatter.parens(this.visit(node.raw_expr, context))); + output.push(context.parens(this.visit(node.raw_expr, context))); } } // Handle NOT VALID for check constraints @@ -2687,7 +2688,7 @@ export class Deparser implements DeparserVisitor { } output.push('AS'); if (node.raw_expr) { - output.push(context.formatter.parens(this.visit(node.raw_expr, context))); + output.push(context.parens(this.visit(node.raw_expr, context))); } output.push('STORED'); break; @@ -2739,8 +2740,8 @@ export class Deparser implements DeparserVisitor { } break; case 'CONSTR_UNIQUE': - if (context.formatter.isPretty() && !context.isColumnConstraint) { - output.push('\n' + context.formatter.indent('UNIQUE')); + if (context.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.indent('UNIQUE')); } else { output.push('UNIQUE'); } @@ -2761,15 +2762,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 (context.formatter.isPretty()) { - output.push('\n' + context.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' + context.formatter.indent('REFERENCES')); + output.push('\n' + context.indent('REFERENCES')); } else { output.push('FOREIGN KEY'); if (node.fk_attrs && node.fk_attrs.length > 0) { @@ -2784,7 +2785,7 @@ export class Deparser implements DeparserVisitor { output.push('REFERENCES'); } if (node.pktable) { - if (context.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); @@ -2799,7 +2800,7 @@ export class Deparser implements DeparserVisitor { const pkAttrs = ListUtils.unwrapList(node.pk_attrs) .map(attr => this.visit(attr, context)) .join(', '); - if (context.formatter.isPretty() && !context.isColumnConstraint) { + if (context.isPretty() && !context.isColumnConstraint) { const lastIndex = output.length - 1; if (lastIndex >= 0) { output[lastIndex] += ` (${pkAttrs})`; @@ -2820,8 +2821,8 @@ export class Deparser implements DeparserVisitor { matchClause = 'MATCH PARTIAL'; break; } - if (context.formatter.isPretty() && !context.isColumnConstraint) { - output.push('\n' + context.formatter.indent(matchClause)); + if (context.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.indent(matchClause)); } else { output.push(matchClause); } @@ -2842,8 +2843,8 @@ export class Deparser implements DeparserVisitor { updateClause += 'SET DEFAULT'; break; } - if (context.formatter.isPretty()) { - output.push('\n' + context.formatter.indent(updateClause)); + if (context.isPretty()) { + output.push('\n' + context.indent(updateClause)); } else { output.push('ON UPDATE'); output.push(updateClause.replace('ON UPDATE ', '')); @@ -2865,8 +2866,8 @@ export class Deparser implements DeparserVisitor { deleteClause += 'SET DEFAULT'; break; } - if (context.formatter.isPretty()) { - output.push('\n' + context.formatter.indent(deleteClause)); + if (context.isPretty()) { + output.push('\n' + context.indent(deleteClause)); } else { output.push('ON DELETE'); output.push(deleteClause.replace('ON DELETE ', '')); @@ -2874,8 +2875,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 (context.formatter.isPretty() && !context.isColumnConstraint) { - output.push('\n' + context.formatter.indent('NOT VALID')); + if (context.isPretty() && !context.isColumnConstraint) { + output.push('\n' + context.indent('NOT VALID')); } else { output.push('NOT VALID'); } @@ -2931,12 +2932,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 (context.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') { - output.push('\n' + context.formatter.indent('DEFERRABLE')); + if (context.isPretty() && node.contype === 'CONSTR_FOREIGN') { + output.push('\n' + context.indent('DEFERRABLE')); if (node.initdeferred === true) { - output.push('\n' + context.formatter.indent('INITIALLY DEFERRED')); + output.push('\n' + context.indent('INITIALLY DEFERRED')); } else if (node.initdeferred === false) { - output.push('\n' + context.formatter.indent('INITIALLY IMMEDIATE')); + output.push('\n' + context.indent('INITIALLY IMMEDIATE')); } } else { output.push('DEFERRABLE'); @@ -2947,15 +2948,15 @@ export class Deparser implements DeparserVisitor { } } } else if (node.deferrable === false) { - if (context.formatter.isPretty() && node.contype === 'CONSTR_FOREIGN') { - output.push('\n' + context.formatter.indent('NOT DEFERRABLE')); + if (context.isPretty() && node.contype === 'CONSTR_FOREIGN') { + output.push('\n' + context.indent('NOT DEFERRABLE')); } else { output.push('NOT DEFERRABLE'); } } } - if (context.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')) { @@ -2973,7 +2974,7 @@ export class Deparser implements DeparserVisitor { } SubLink(node: t.SubLink, context: DeparserContext): string { - const subselect = context.formatter.parens(this.visit(node.subselect, context)); + const subselect = context.parens(this.visit(node.subselect, context)); switch (node.subLinkType) { case 'ANY_SUBLINK': @@ -3219,7 +3220,7 @@ export class Deparser implements DeparserVisitor { 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] += context.formatter.parens(colnameStrs.join(', ')); + output[output.length - 1] += context.parens(colnameStrs.join(', ')); } output.push('AS'); @@ -3232,7 +3233,7 @@ export class Deparser implements DeparserVisitor { } if (node.ctequery) { - output.push(context.formatter.parens(this.visit(node.ctequery, context))); + output.push(context.parens(this.visit(node.ctequery, context))); } return output.join(' '); @@ -3490,7 +3491,7 @@ export class Deparser implements DeparserVisitor { if (node.aliases && node.aliases.length > 0) { const aliasStrs = ListUtils.unwrapList(node.aliases).map(alias => this.visit(alias, context)); - output.push(context.formatter.parens(aliasStrs.join(', '))); + output.push(context.parens(aliasStrs.join(', '))); } if (node.options && node.options.length > 0) { @@ -3556,13 +3557,13 @@ export class Deparser implements DeparserVisitor { if (node.indexParams && node.indexParams.length > 0) { const paramStrs = ListUtils.unwrapList(node.indexParams).map(param => this.visit(param, context)); - output.push(context.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(context.formatter.parens(includeStrs.join(', '))); + output.push(context.parens(includeStrs.join(', '))); } if (node.whereClause) { @@ -3574,7 +3575,7 @@ export class Deparser implements DeparserVisitor { const indexContext = context.spawn('IndexStmt'); const optionStrs = ListUtils.unwrapList(node.options).map(option => this.visit(option, indexContext)); output.push('WITH'); - output.push(context.formatter.parens(optionStrs.join(', '))); + output.push(context.parens(optionStrs.join(', '))); } if (node.nulls_not_distinct) { @@ -3595,7 +3596,7 @@ export class Deparser implements DeparserVisitor { if (node.name) { output.push(QuoteUtils.quote(node.name)); } else if (node.expr) { - output.push(context.formatter.parens(this.visit(node.expr, context))); + output.push(context.parens(this.visit(node.expr, context))); } if (node.collation && node.collation.length > 0) { @@ -3654,7 +3655,7 @@ export class Deparser implements DeparserVisitor { if (node.name) { output.push(QuoteUtils.quote(node.name)); } else if (node.expr) { - output.push(context.formatter.parens(this.visit(node.expr, context))); + output.push(context.parens(this.visit(node.expr, context))); } if (node.collation && node.collation.length > 0) { @@ -3819,14 +3820,14 @@ export class Deparser implements DeparserVisitor { rargStr = `(${rargStr})`; } - if (context.formatter.isPretty()) { - output.push(context.formatter.newline() + joinStr + ' ' + rargStr); + if (context.isPretty()) { + output.push(context.newline() + joinStr + ' ' + rargStr); } else { output.push(joinStr + ' ' + rargStr); } } else { - if (context.formatter.isPretty()) { - output.push(context.formatter.newline() + joinStr); + if (context.isPretty()) { + output.push(context.newline() + joinStr); } else { output.push(joinStr); } @@ -3835,20 +3836,20 @@ export class Deparser implements DeparserVisitor { if (node.usingClause && node.usingClause.length > 0) { const usingList = ListUtils.unwrapList(node.usingClause); const columnNames = usingList.map(col => this.visit(col, context)); - if (context.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 (context.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${context.formatter.newline()}${context.formatter.indent(qualsStr)}`); + output.push(` ON${context.newline()}${context.indent(qualsStr)}`); } } else { output.push(` ON ${qualsStr}`); @@ -3859,7 +3860,7 @@ export class Deparser implements DeparserVisitor { } let result; - if (context.formatter.isPretty()) { + if (context.isPretty()) { result = output.join(''); } else { result = output.join(' '); @@ -4663,12 +4664,12 @@ export class Deparser implements DeparserVisitor { if (node.cmds && node.cmds.length > 0) { const commands = ListUtils.unwrapList(node.cmds); - if (context.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 context.formatter.newline() + context.formatter.indent(cmdStr); + return context.newline() + context.indent(cmdStr); } return cmdStr; }) @@ -6790,8 +6791,8 @@ export class Deparser implements DeparserVisitor { // Add ON clause on new line in pretty mode if (node.table) { - if (context.formatter.isPretty()) { - output.push(context.formatter.newline() + context.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)); @@ -6800,22 +6801,22 @@ export class Deparser implements DeparserVisitor { // Handle AS RESTRICTIVE/PERMISSIVE clause if (node.permissive === undefined) { - if (context.formatter.isPretty()) { - output.push(context.formatter.newline() + context.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 (context.formatter.isPretty()) { - output.push(context.formatter.newline() + context.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 (context.formatter.isPretty()) { - output.push(context.formatter.newline() + context.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()); } @@ -6823,8 +6824,8 @@ export class Deparser implements DeparserVisitor { if (node.roles && node.roles.length > 0) { const roles = ListUtils.unwrapList(node.roles).map(role => this.visit(role, context)); - if (context.formatter.isPretty()) { - output.push(context.formatter.newline() + context.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(', ')); @@ -6832,11 +6833,11 @@ export class Deparser implements DeparserVisitor { } if (node.qual) { - if (context.formatter.isPretty()) { + if (context.isPretty()) { const qualExpr = this.visit(node.qual, context); - output.push(context.formatter.newline() + context.formatter.indent('USING (')); - output.push(context.formatter.newline() + context.formatter.indent(context.formatter.indent(qualExpr))); - output.push(context.formatter.newline() + context.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)})`); @@ -6844,18 +6845,18 @@ export class Deparser implements DeparserVisitor { } if (node.with_check) { - if (context.formatter.isPretty()) { + if (context.isPretty()) { const checkExpr = this.visit(node.with_check, context); - output.push(context.formatter.newline() + context.formatter.indent('WITH CHECK (')); - output.push(context.formatter.newline() + context.formatter.indent(context.formatter.indent(checkExpr))); - output.push(context.formatter.newline() + context.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 context.formatter.isPretty() ? output.join('') : output.join(' '); + return context.isPretty() ? output.join('') : output.join(' '); } AlterPolicyStmt(node: t.AlterPolicyStmt, context: DeparserContext): string { @@ -8555,8 +8556,8 @@ export class Deparser implements DeparserVisitor { if (node.action) { const actionStr = this.GrantStmt(node.action, context); - if (context.formatter.isPretty()) { - return output.join(' ') + context.formatter.newline() + context.formatter.indent(actionStr); + if (context.isPretty()) { + return output.join(' ') + context.newline() + context.indent(actionStr); } else { output.push(actionStr); } @@ -8743,7 +8744,7 @@ export class Deparser implements DeparserVisitor { output.push(QuoteUtils.quote(node.trigname)); } - if (context.formatter.isPretty()) { + if (context.isPretty()) { const components: string[] = []; const timing: string[] = []; @@ -8766,36 +8767,36 @@ export class Deparser implements DeparserVisitor { } if (node.events & 32) events.push('TRUNCATE'); - components.push(context.formatter.indent(timing.join(' ') + ' ' + events.join(' OR '))); + components.push(context.indent(timing.join(' ') + ' ' + events.join(' OR '))); if (node.relation) { - components.push(context.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(context.formatter.indent('REFERENCING ' + transitionClauses)); + components.push(context.indent('REFERENCING ' + transitionClauses)); } if (node.deferrable) { - components.push(context.formatter.indent('DEFERRABLE')); + components.push(context.indent('DEFERRABLE')); } if (node.initdeferred) { - components.push(context.formatter.indent('INITIALLY DEFERRED')); + components.push(context.indent('INITIALLY DEFERRED')); } if (node.row) { - components.push(context.formatter.indent('FOR EACH ROW')); + components.push(context.indent('FOR EACH ROW')); } else { - components.push(context.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(context.formatter.indent(whenStr)); + components.push(context.indent(whenStr)); } let executeStr = 'EXECUTE'; @@ -8816,9 +8817,9 @@ export class Deparser implements DeparserVisitor { executeStr += '()'; } - components.push(context.formatter.indent(executeStr)); + components.push(context.indent(executeStr)); - return output.join(' ') + context.formatter.newline() + components.join(context.formatter.newline()); + return output.join(' ') + context.newline() + components.join(context.newline()); } else { const timing: string[] = []; if (node.timing & 2) timing.push('BEFORE'); diff --git a/packages/deparser/src/visitors/base.ts b/packages/deparser/src/visitors/base.ts index df13b0e8..2803a260 100644 --- a/packages/deparser/src/visitors/base.ts +++ b/packages/deparser/src/visitors/base.ts @@ -29,7 +29,7 @@ export class DeparserContext { readonly prettyMode: boolean; readonly isStringLiteral?: boolean; readonly parentNodeTypes: string[]; - readonly formatter: SqlFormatter; + private readonly formatter: SqlFormatter; readonly select?: boolean; readonly from?: boolean; readonly group?: boolean; @@ -115,6 +115,30 @@ export class DeparserContext { ...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); + } + + 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 { From 22d5915933313a2f5dfca998dbde7a2cb59e2a2e Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Jun 2025 16:37:49 +0000 Subject: [PATCH 09/10] fix: improve CREATE TABLE constraint indentation using proper context indentLevel - Remove explicit indentLevel increment when spawning table element contexts - Let context.indent() method handle indentation automatically using indentLevel + 1 - UNIQUE and FOREIGN KEY constraints now properly indented relative to columns - Constraints are visually distinct with proper logical grouping indentation - All tests passing including CREATE TABLE pretty printing snapshots Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 027eb67a..600dc295 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -2465,8 +2465,7 @@ export class Deparser implements DeparserVisitor { } else if (node.tableElts) { const elements = ListUtils.unwrapList(node.tableElts); const elementStrs = elements.map(el => { - const tableElementContext = context.spawn('CreateStmt', { indentLevel: context.indentLevel + 1 }); - return this.deparse(el, tableElementContext); + return this.deparse(el, context.spawn('CreateStmt')); }); if (context.isPretty()) { @@ -2474,9 +2473,9 @@ export class Deparser implements DeparserVisitor { const trimmedEl = el.trim(); // Remove leading newlines from constraint elements to avoid extra blank lines if (trimmedEl.startsWith('\n')) { - return ' ' + trimmedEl.substring(1); + return context.indent(trimmedEl.substring(1)); } - return ' ' + trimmedEl; + return context.indent(trimmedEl); }).join(',' + context.newline()); output.push('(' + context.newline() + formattedElements + context.newline() + ')'); } else { From 6cc2f4e4566541c671d5ffc42625d4c47ea76aa5 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Jun 2025 16:41:31 +0000 Subject: [PATCH 10/10] feat: improve indentation for boolean expressions and SELECT clauses - Replace hardcoded ' AND ' and ' OR ' spacing with context.indent() in BoolExpr - Add proper indentLevel context spawning for GROUP BY and ORDER BY clauses - Boolean expressions now use context-aware indentation for better logical grouping - SELECT clause elements properly track indent levels through context spawning - Consistent indentation management across nested statements and expressions Co-Authored-By: Dan Lynch --- packages/deparser/src/deparser.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/deparser/src/deparser.ts b/packages/deparser/src/deparser.ts index 600dc295..50602cc2 100644 --- a/packages/deparser/src/deparser.ts +++ b/packages/deparser/src/deparser.ts @@ -472,7 +472,7 @@ export class Deparser implements DeparserVisitor { if (context.isPretty()) { const groupItems = groupList .map(e => { - const groupStr = this.visit(e as Node, context.spawn('SelectStmt', { group: true })); + const groupStr = this.visit(e as Node, context.spawn('SelectStmt', { group: true, indentLevel: context.indentLevel + 1 })); if (this.containsMultilineStringLiteral(groupStr)) { return groupStr; } @@ -519,7 +519,7 @@ export class Deparser implements DeparserVisitor { if (context.isPretty()) { const sortItems = sortList .map(e => { - const sortStr = this.visit(e as Node, context.spawn('SelectStmt', { sort: true })); + const sortStr = this.visit(e as Node, context.spawn('SelectStmt', { sort: true, indentLevel: context.indentLevel + 1 })); if (this.containsMultilineStringLiteral(sortStr)) { return sortStr; } @@ -1282,7 +1282,7 @@ export class Deparser implements DeparserVisitor { switch (boolop) { case 'AND_EXPR': if (context.isPretty() && args.length > 1) { - const andArgs = args.map(arg => this.visit(arg, boolContext)).join(context.newline() + ' AND '); + 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 '); @@ -1290,7 +1290,7 @@ export class Deparser implements DeparserVisitor { } case 'OR_EXPR': if (context.isPretty() && args.length > 1) { - const orArgs = args.map(arg => this.visit(arg, boolContext)).join(context.newline() + ' OR '); + 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 ');