UNPKG

pgsql-deparser

Version:
1,156 lines 430 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Deparser = void 0; const sql_formatter_1 = require("./utils/sql-formatter"); const quote_utils_1 = require("./utils/quote-utils"); const list_utils_1 = require("./utils/list-utils"); // Type guards for better type safety function isParseResult(obj) { // A ParseResult is an object that could have stmts (but not required) // and is not already wrapped as a Node // 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' && !Array.isArray(obj) && !('ParseResult' in obj) && !('RawStmt' in obj) && // Check if it looks like a ParseResult (has stmts or version) ('stmts' in obj || 'version' in obj); } function isWrappedParseResult(obj) { return obj && typeof obj === 'object' && 'ParseResult' in obj; } /** * 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 * 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. */ class Deparser { formatter; tree; options; constructor(tree, opts = {}) { this.formatter = new sql_formatter_1.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) // Wrap it as a proper Node for consistent handling this.tree = [{ ParseResult: tree }]; } else if (Array.isArray(tree)) { // Array of Nodes this.tree = tree; } else { // Single Node (including wrapped ParseResult) this.tree = [tree]; } } /** * Static method to deparse PostgreSQL AST nodes to SQL * @param query - Can be: * - ParseResult from libpg-query (e.g., { version: 170004, stmts: [...] }) * - Wrapped ParseResult node (e.g., { ParseResult: {...} }) * - Wrapped RawStmt node (e.g., { RawStmt: {...} }) * - Array of Nodes * - Single Node (e.g., { SelectStmt: {...} }) * @param opts - Deparser options for formatting * @returns The deparsed SQL string */ static deparse(query, opts = {}) { return new Deparser(query, opts).deparseQuery(); } deparseQuery() { 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); return result || ''; }) .filter(result => result !== '') .join(this.formatter.newline() + this.formatter.newline()); } /** * Get the appropriate function delimiter based on the body content * @param body The function body to check * @returns The delimiter to use */ getFunctionDelimiter(body) { const delimiter = this.options.functionDelimiter || '$$'; if (body.includes(delimiter)) { return this.options.functionDelimiterFallback || '$EOFCODE$'; } return delimiter; } deparse(node, context = { parentNodeTypes: [] }) { if (node == null) { return null; } if (typeof node === 'number' || node instanceof Number) { return node.toString(); } try { return this.visit(node, context); } catch (error) { const nodeType = Object.keys(node)[0]; throw new Error(`Error deparsing ${nodeType}: ${error.message}`); } } visit(node, context = { parentNodeTypes: [] }) { const nodeType = this.getNodeType(node); // Handle empty objects if (!nodeType) { return ''; } const nodeData = this.getNodeData(node); const methodName = nodeType; if (typeof this[methodName] === 'function') { const childContext = { ...context, parentNodeTypes: [...context.parentNodeTypes, nodeType] }; const result = this[methodName](nodeData, childContext); return result; } throw new Error(`Deparser does not handle node type: ${nodeType}`); } getNodeType(node) { return Object.keys(node)[0]; } getNodeData(node) { const keys = Object.keys(node); if (keys.length === 1 && typeof node[keys[0]] === 'object') { return node[keys[0]]; } return node; } ParseResult(node, context) { 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 } return node.stmts .filter((rawStmt) => rawStmt != null) .map((rawStmt) => this.RawStmt(rawStmt, context)) .filter((result) => result !== '') .join(this.formatter.newline() + this.formatter.newline()); } RawStmt(node, context) { 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 + ';'; } return deparsedStmt; } SelectStmt(node, context) { const output = []; if (node.withClause) { output.push(this.WithClause(node.withClause, context)); } if (!node.op || node.op === 'SETOP_NONE') { if (node.valuesLists == null) { if (!this.formatter.isPretty() || !node.targetList) { output.push('SELECT'); } } } else { const leftStmt = this.SelectStmt(node.larg, context); const rightStmt = this.SelectStmt(node.rarg, 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.op && node.larg.op !== 'SETOP_NONE') || node.larg.sortClause || node.larg.limitCount || node.larg.limitOffset || node.larg.withClause); const rightNeedsParens = node.rarg && ((node.rarg.op && node.rarg.op !== 'SETOP_NONE') || node.rarg.sortClause || node.rarg.limitCount || node.rarg.limitOffset || node.rarg.withClause); if (leftNeedsParens) { output.push(this.formatter.parens(leftStmt)); } else { output.push(leftStmt); } switch (node.op) { case 'SETOP_UNION': output.push('UNION'); break; case 'SETOP_INTERSECT': output.push('INTERSECT'); break; case 'SETOP_EXCEPT': output.push('EXCEPT'); break; default: throw new Error(`Bad SelectStmt op: ${node.op}`); } if (node.all) { output.push('ALL'); } if (rightNeedsParens) { output.push(this.formatter.parens(rightStmt)); } else { output.push(rightStmt); } } // Handle DISTINCT clause - in pretty mode, we'll include it in the SELECT clause let distinctPart = ''; if (node.distinctClause) { const distinctClause = list_utils_1.ListUtils.unwrapList(node.distinctClause); if (distinctClause.length > 0 && Object.keys(distinctClause[0]).length > 0) { const clause = distinctClause .map(e => this.visit(e, { ...context, select: true })) .join(', '); distinctPart = ' DISTINCT ON ' + this.formatter.parens(clause); } else { distinctPart = ' DISTINCT'; } if (!this.formatter.isPretty()) { if (distinctClause.length > 0 && Object.keys(distinctClause[0]).length > 0) { output.push('DISTINCT ON'); const clause = distinctClause .map(e => this.visit(e, { ...context, select: true })) .join(', '); output.push(this.formatter.parens(clause)); } else { output.push('DISTINCT'); } } } if (node.targetList) { const targetList = list_utils_1.ListUtils.unwrapList(node.targetList); if (this.formatter.isPretty()) { const targetStrings = targetList .map(e => { const targetStr = this.visit(e, { ...context, select: true }); if (this.containsMultilineStringLiteral(targetStr)) { return targetStr; } return this.formatter.indent(targetStr); }); const formattedTargets = targetStrings.join(',' + this.formatter.newline()); output.push('SELECT' + distinctPart); output.push(formattedTargets); } else { const targets = targetList .map(e => this.visit(e, { ...context, select: true })) .join(', '); output.push(targets); } } if (node.intoClause) { output.push('INTO'); output.push(this.IntoClause(node.intoClause, context)); } if (node.fromClause) { const fromList = list_utils_1.ListUtils.unwrapList(node.fromClause); const fromItems = fromList .map(e => this.deparse(e, { ...context, from: true })) .join(', '); output.push('FROM ' + fromItems.trim()); } if (node.whereClause) { if (this.formatter.isPretty()) { output.push('WHERE'); const whereExpr = this.visit(node.whereClause, context); const lines = whereExpr.split(this.formatter.newline()); const indentedLines = lines.map((line, index) => { if (index === 0) { return this.formatter.indent(line); } return line; }); output.push(indentedLines.join(this.formatter.newline())); } else { output.push('WHERE'); output.push(this.visit(node.whereClause, context)); } } if (node.valuesLists) { output.push('VALUES'); const lists = list_utils_1.ListUtils.unwrapList(node.valuesLists).map(list => { const values = list_utils_1.ListUtils.unwrapList(list).map(val => this.visit(val, context)); return this.formatter.parens(values.join(', ')); }); output.push(lists.join(', ')); } if (node.groupClause) { const groupList = list_utils_1.ListUtils.unwrapList(node.groupClause); if (this.formatter.isPretty()) { const groupItems = groupList .map(e => { const groupStr = this.visit(e, { ...context, group: true }); if (this.containsMultilineStringLiteral(groupStr)) { return groupStr; } return this.formatter.indent(groupStr); }) .join(',' + this.formatter.newline()); output.push('GROUP BY'); output.push(groupItems); } else { output.push('GROUP BY'); const groupItems = groupList .map(e => this.visit(e, { ...context, group: true })) .join(', '); output.push(groupItems); } } if (node.havingClause) { if (this.formatter.isPretty()) { output.push('HAVING'); const havingStr = this.visit(node.havingClause, context); if (this.containsMultilineStringLiteral(havingStr)) { output.push(havingStr); } else { output.push(this.formatter.indent(havingStr)); } } else { output.push('HAVING'); output.push(this.visit(node.havingClause, context)); } } if (node.windowClause) { output.push('WINDOW'); const windowList = list_utils_1.ListUtils.unwrapList(node.windowClause); const windowClauses = windowList .map(e => this.visit(e, context)) .join(', '); output.push(windowClauses); } if (node.sortClause) { const sortList = list_utils_1.ListUtils.unwrapList(node.sortClause); if (this.formatter.isPretty()) { const sortItems = sortList .map(e => { const sortStr = this.visit(e, { ...context, sort: true }); if (this.containsMultilineStringLiteral(sortStr)) { return sortStr; } return this.formatter.indent(sortStr); }) .join(',' + this.formatter.newline()); output.push('ORDER BY'); output.push(sortItems); } else { output.push('ORDER BY'); const sortItems = sortList .map(e => this.visit(e, { ...context, sort: true })) .join(', '); output.push(sortItems); } } if (node.limitCount) { output.push('LIMIT ' + this.visit(node.limitCount, context)); } if (node.limitOffset) { output.push('OFFSET ' + this.visit(node.limitOffset, context)); } if (node.lockingClause) { const lockingList = list_utils_1.ListUtils.unwrapList(node.lockingClause); const lockingClauses = lockingList .map(e => this.visit(e, context)) .join(' '); output.push(lockingClauses); } if (this.formatter.isPretty()) { const filteredOutput = output.filter(item => item.trim() !== ''); return filteredOutput.join(this.formatter.newline()); } return output.join(' '); } A_Expr(node, context) { const kind = node.kind; const name = list_utils_1.ListUtils.unwrapList(node.name); const lexpr = node.lexpr; const rexpr = node.rexpr; switch (kind) { case 'AEXPR_OP': if (lexpr && rexpr) { 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') { const leftOp = this.deparseOperatorName(list_utils_1.ListUtils.unwrapList(lexpr.A_Expr.name)); if (this.needsParentheses(leftOp, operator, 'left')) { leftNeedsParens = true; } } if (lexpr && this.isComplexExpression(lexpr)) { leftNeedsParens = true; } 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') { const rightOp = this.deparseOperatorName(list_utils_1.ListUtils.unwrapList(rexpr.A_Expr.name)); if (this.needsParentheses(rightOp, operator, 'right')) { rightNeedsParens = true; } } if (rexpr && this.isComplexExpression(rexpr)) { rightNeedsParens = true; } if (rightNeedsParens) { rightExpr = this.formatter.parens(rightExpr); } return this.formatter.format([leftExpr, operator, rightExpr]); } else if (rexpr) { return this.formatter.format([ this.deparseOperatorName(name), this.visit(rexpr, context) ]); } break; case 'AEXPR_OP_ANY': return this.formatter.format([ this.visit(lexpr, context), this.deparseOperatorName(name), 'ANY', this.formatter.parens(this.visit(rexpr, context)) ]); case 'AEXPR_OP_ALL': return this.formatter.format([ this.visit(lexpr, context), this.deparseOperatorName(name), 'ALL', this.formatter.parens(this.visit(rexpr, context)) ]); case 'AEXPR_DISTINCT': { let leftExpr = this.visit(lexpr, context); let rightExpr = this.visit(rexpr, context); // Add parentheses for complex expressions if (lexpr && this.isComplexExpression(lexpr)) { leftExpr = this.formatter.parens(leftExpr); } if (rexpr && this.isComplexExpression(rexpr)) { rightExpr = this.formatter.parens(rightExpr); } return this.formatter.format([ leftExpr, 'IS DISTINCT FROM', rightExpr ]); } 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); } if (rexpr && this.isComplexExpression(rexpr)) { rightExpr = this.formatter.parens(rightExpr); } return this.formatter.format([ leftExpr, 'IS NOT DISTINCT FROM', rightExpr ]); } case 'AEXPR_NULLIF': return this.formatter.format([ 'NULLIF', this.formatter.parens([ this.visit(lexpr, context), this.visit(rexpr, context) ].join(', ')) ]); case 'AEXPR_IN': const inOperator = this.deparseOperatorName(name); if (inOperator === '<>' || inOperator === '!=') { return this.formatter.format([ this.visit(lexpr, context), 'NOT IN', this.formatter.parens(this.visit(rexpr, context)) ]); } else { return this.formatter.format([ this.visit(lexpr, context), 'IN', this.formatter.parens(this.visit(rexpr, context)) ]); } case 'AEXPR_LIKE': const likeOp = this.deparseOperatorName(name); if (likeOp === '!~~') { return this.formatter.format([ this.visit(lexpr, context), 'NOT LIKE', this.visit(rexpr, context) ]); } else { return this.formatter.format([ this.visit(lexpr, context), 'LIKE', this.visit(rexpr, context) ]); } case 'AEXPR_ILIKE': const ilikeOp = this.deparseOperatorName(name); if (ilikeOp === '!~~*') { return this.formatter.format([ this.visit(lexpr, context), 'NOT ILIKE', this.visit(rexpr, context) ]); } else { return this.formatter.format([ this.visit(lexpr, context), 'ILIKE', this.visit(rexpr, context) ]); } case 'AEXPR_SIMILAR': const similarOp = this.deparseOperatorName(name); let rightExpr; if (rexpr && 'FuncCall' in rexpr && rexpr.FuncCall?.funcname?.length === 2 && rexpr.FuncCall.funcname[0]?.String?.sval === 'pg_catalog' && rexpr.FuncCall.funcname[1]?.String?.sval === 'similar_to_escape') { const args = rexpr.FuncCall.args || []; rightExpr = this.visit(args[0], context); if (args.length > 1) { rightExpr += ` ESCAPE ${this.visit(args[1], context)}`; } } else { rightExpr = this.visit(rexpr, context); } if (similarOp === '!~') { return this.formatter.format([ this.visit(lexpr, context), 'NOT SIMILAR TO', rightExpr ]); } else { return this.formatter.format([ this.visit(lexpr, context), 'SIMILAR TO', rightExpr ]); } case 'AEXPR_BETWEEN': return this.formatter.format([ this.visit(lexpr, context), 'BETWEEN', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_NOT_BETWEEN': return this.formatter.format([ this.visit(lexpr, context), 'NOT BETWEEN', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_BETWEEN_SYM': return this.formatter.format([ this.visit(lexpr, context), 'BETWEEN SYMMETRIC', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_NOT_BETWEEN_SYM': return this.formatter.format([ this.visit(lexpr, context), 'NOT BETWEEN SYMMETRIC', this.visitBetweenRange(rexpr, context) ]); } throw new Error(`Unhandled A_Expr kind: ${kind}`); } deparseOperatorName(name) { if (!name || name.length === 0) { return ''; } const parts = name.map((n) => { if (n.String) { return n.String.sval || n.String.str; } return this.visit(n, { parentNodeTypes: [] }); }); if (parts.length > 1) { return `OPERATOR(${parts.join('.')})`; } return parts.join('.'); } getOperatorPrecedence(operator) { const precedence = { '||': 1, // string concatenation 'OR': 2, // logical OR 'AND': 3, // logical AND 'NOT': 4, // logical NOT 'IS': 5, // IS NULL, IS NOT NULL, etc. 'IN': 5, // IN, NOT IN 'BETWEEN': 5, // BETWEEN, NOT BETWEEN 'LIKE': 5, // LIKE, ILIKE, SIMILAR TO 'ILIKE': 5, 'SIMILAR': 5, '<': 6, // comparison operators '<=': 6, '>': 6, '>=': 6, '=': 6, '<>': 6, '!=': 6, '+': 7, // addition, subtraction '-': 7, '*': 8, // multiplication, division, modulo '/': 8, '%': 8, '^': 9, // exponentiation '~': 10, // bitwise operators '&': 10, '|': 10, '#': 10, '<<': 10, '>>': 10 }; return precedence[operator] || 0; } needsParentheses(childOp, parentOp, position) { 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; } isComplexExpression(node) { return !!(node.NullTest || node.BooleanTest || node.BoolExpr || node.CaseExpr || node.CoalesceExpr || node.SubLink || node.A_Expr); } visitBetweenRange(rexpr, context) { if (rexpr && 'List' in rexpr && rexpr.List?.items) { const items = rexpr.List.items.map((item) => this.visit(item, context)); return items.join(' AND '); } return this.visit(rexpr, context); } InsertStmt(node, context) { const output = []; if (node.withClause) { output.push(this.WithClause(node.withClause, context)); } output.push('INSERT INTO'); output.push(this.RangeVar(node.relation, context)); if (node.cols) { const cols = list_utils_1.ListUtils.unwrapList(node.cols); const insertContext = { ...context, insertColumns: true }; const columnNames = cols.map(col => this.visit(col, insertContext)); output.push(this.formatter.parens(columnNames.join(', '))); } if (node.selectStmt) { output.push(this.visit(node.selectStmt, context)); } else if (!node.cols || (node.cols && list_utils_1.ListUtils.unwrapList(node.cols).length === 0)) { // Handle DEFAULT VALUES case when no columns and no selectStmt output.push('DEFAULT VALUES'); } if (node.onConflictClause) { output.push('ON CONFLICT'); if (node.onConflictClause.infer) { const infer = node.onConflictClause.infer; // Handle ON CONSTRAINT clause if (infer.conname) { output.push('ON CONSTRAINT'); output.push(infer.conname); } else if (infer.indexElems) { const elems = list_utils_1.ListUtils.unwrapList(infer.indexElems); const indexElems = elems.map(elem => this.visit(elem, 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, context)); } } if (node.onConflictClause.action === 'ONCONFLICT_UPDATE') { output.push('DO UPDATE SET'); const targetList = list_utils_1.ListUtils.unwrapList(node.onConflictClause.targetList); if (targetList && targetList.length) { const firstTarget = targetList[0]; if (firstTarget.ResTarget?.val?.MultiAssignRef && targetList.every(target => target.ResTarget?.val?.MultiAssignRef)) { const sortedTargets = targetList.sort((a, b) => a.ResTarget.val.MultiAssignRef.colno - b.ResTarget.val.MultiAssignRef.colno); const names = sortedTargets.map(target => target.ResTarget.name); output.push(this.formatter.parens(names.join(', '))); output.push('='); output.push(this.visit(firstTarget.ResTarget.val.MultiAssignRef.source, context)); } else { const updateContext = { ...context, update: true }; const targets = targetList.map(target => this.visit(target, updateContext)); output.push(targets.join(', ')); } } if (node.onConflictClause.whereClause) { output.push('WHERE'); output.push(this.visit(node.onConflictClause.whereClause, context)); } } else if (node.onConflictClause.action === 'ONCONFLICT_NOTHING') { output.push('DO NOTHING'); } } if (node.returningList) { output.push('RETURNING'); const returningList = list_utils_1.ListUtils.unwrapList(node.returningList); const returns = returningList.map(ret => this.visit(ret, context)); output.push(returns.join(', ')); } return output.join(' '); } UpdateStmt(node, context) { const output = []; if (node.withClause) { output.push(this.WithClause(node.withClause, context)); } output.push('UPDATE'); if (node.relation) { output.push(this.RangeVar(node.relation, context)); } output.push('SET'); const targetList = list_utils_1.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 && 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); } else { // Handle regular single-column assignment assignmentParts.push(this.visit(target, { ...context, update: true })); processedTargets.add(i); } } output.push(assignmentParts.join(',')); } if (node.fromClause) { output.push('FROM'); const fromList = list_utils_1.ListUtils.unwrapList(node.fromClause); const fromItems = fromList.map(item => this.visit(item, context)); output.push(fromItems.join(', ')); } if (node.whereClause) { output.push('WHERE'); output.push(this.visit(node.whereClause, context)); } if (node.returningList) { output.push('RETURNING'); output.push(this.deparseReturningList(node.returningList, context)); } return output.join(' '); } DeleteStmt(node, context) { try { const output = []; if (node.withClause) { try { output.push(this.WithClause(node.withClause, context)); } catch (error) { console.warn(`Error processing withClause in DeleteStmt: ${error instanceof Error ? error.message : String(error)}`); throw new Error(`Error deparsing DeleteStmt: ${error instanceof Error ? error.message : String(error)}`); } } output.push('DELETE'); output.push('FROM'); if (!node.relation) { throw new Error('DeleteStmt requires a relation'); } output.push(this.RangeVar(node.relation, context)); if (node.usingClause) { output.push('USING'); const usingList = list_utils_1.ListUtils.unwrapList(node.usingClause); const usingItems = usingList .filter(item => item != null && this.getNodeType(item) !== 'undefined') .map(item => { try { return this.visit(item, context); } catch (error) { console.warn(`Error processing usingClause item in DeleteStmt: ${error instanceof Error ? error.message : String(error)}`); return ''; } }) .filter(item => item && item.trim()); if (usingItems.length > 0) { output.push(usingItems.join(', ')); } } if (node.whereClause) { output.push('WHERE'); try { output.push(this.visit(node.whereClause, context)); } catch (error) { console.warn(`Error processing whereClause in DeleteStmt: ${error instanceof Error ? error.message : String(error)}`); throw new Error(`Error deparsing DeleteStmt: ${error instanceof Error ? error.message : String(error)}`); } } if (node.returningList) { output.push('RETURNING'); try { output.push(this.deparseReturningList(node.returningList, context)); } catch (error) { console.warn(`Error processing returningList in DeleteStmt: ${error instanceof Error ? error.message : String(error)}`); throw new Error(`Error deparsing DeleteStmt: ${error instanceof Error ? error.message : String(error)}`); } } return output.join(' '); } catch (error) { throw new Error(`Error deparsing DeleteStmt: ${error instanceof Error ? error.message : String(error)}`); } } WithClause(node, context) { const output = ['WITH']; if (node.recursive) { output.push('RECURSIVE'); } if (node.ctes && node.ctes.length > 0) { const ctes = list_utils_1.ListUtils.unwrapList(node.ctes); if (this.formatter.isPretty()) { const cteStrings = ctes.map(cte => { const cteStr = this.visit(cte, context); if (this.containsMultilineStringLiteral(cteStr)) { return this.formatter.newline() + cteStr; } return this.formatter.newline() + this.formatter.indent(cteStr); }); output.push(cteStrings.join(',')); } else { const cteStrings = ctes.map(cte => this.visit(cte, context)); output.push(cteStrings.join(', ')); } } return output.join(' '); } ResTarget(node, context) { const output = []; if (context.update && node.name) { output.push(quote_utils_1.QuoteUtils.quote(node.name)); // Handle indirection (array indexing, field access, etc.) if (node.indirection && node.indirection.length > 0) { const indirectionStrs = list_utils_1.ListUtils.unwrapList(node.indirection).map(item => { if (item.String) { return `.${quote_utils_1.QuoteUtils.quote(item.String.sval || item.String.str)}`; } return this.visit(item, context); }); output.push(indirectionStrs.join('')); } output.push('='); if (node.val) { output.push(this.deparse(node.val, context)); } } else if (context.insertColumns && node.name) { output.push(quote_utils_1.QuoteUtils.quote(node.name)); // Handle indirection for INSERT column lists (e.g., q.c1.r) if (node.indirection && node.indirection.length > 0) { const indirectionStrs = list_utils_1.ListUtils.unwrapList(node.indirection).map(item => { if (item.String) { return `.${quote_utils_1.QuoteUtils.quote(item.String.sval || item.String.str)}`; } return this.visit(item, context); }); output.push(indirectionStrs.join('')); } } else { if (node.val) { output.push(this.deparse(node.val, context)); } if (node.name) { output.push('AS'); output.push(quote_utils_1.QuoteUtils.quote(node.name)); } } return output.join(' '); } deparseReturningList(list, context) { return list_utils_1.ListUtils.unwrapList(list) .filter(item => item != null && this.getNodeType(item) !== 'undefined') .map(item => { try { // Handle ResTarget wrapper if (this.getNodeType(item) === 'ResTarget') { const resTarget = this.getNodeData(item); const val = resTarget.val ? this.visit(resTarget.val, context) : ''; const alias = resTarget.name ? ` AS ${quote_utils_1.QuoteUtils.quote(resTarget.name)}` : ''; return val + alias; } else { const val = this.visit(item, context); return val; } } catch (error) { console.warn(`Error processing returning item: ${error instanceof Error ? error.message : String(error)}`); return ''; } }) .filter(item => item && item.trim()) .join(', '); } BoolExpr(node, context) { const boolop = node.boolop; const args = list_utils_1.ListUtils.unwrapList(node.args); let formatStr = '%s'; if (context.bool) { formatStr = '(%s)'; } const boolContext = { ...context, bool: true }; // explanation of our syntax/fix below: // return formatStr.replace('%s', andArgs); // ❌ Interprets $ as special syntax // return formatStr.replace('%s', () => andArgs); // ✅ Function callback prevents interpretation 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 '); 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 '); return formatStr.replace('%s', () => orArgs); } else { const orArgs = args.map(arg => this.visit(arg, boolContext)).join(' OR '); return formatStr.replace('%s', () => orArgs); } case 'NOT_EXPR': return `NOT (${this.visit(args[0], context)})`; default: throw new Error(`Unhandled BoolExpr boolop: ${boolop}`); } } FuncCall(node, context) { const funcname = list_utils_1.ListUtils.unwrapList(node.funcname); const args = list_utils_1.ListUtils.unwrapList(node.args); const name = funcname.map(n => this.visit(n, context)).join('.'); // Handle special SQL syntax functions like XMLEXISTS and EXTRACT if (node.funcformat === 'COERCE_SQL_SYNTAX' && name === 'pg_catalog.xmlexists' && args.length >= 2) { const xpath = this.visit(args[0], context); 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') { return `TRIM(LEADING${trimChar} FROM ${source})`; } else if (name === 'pg_catalog.btrim') { 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); if (args.length === 3) { const start = this.visit(args[1], context); const length = this.visit(args[2], context); return `SUBSTRING(${source} FROM ${start} FOR ${length})`; } else if (args.length === 2) { const start = this.visit(args[1], context); 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) { const string = this.visit(args[0], context); const substring = this.visit(args[1], context); 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) { const string = this.visit(args[0], context); const substring = this.visit(args[1], context); const start = this.visit(args[2], context); const length = this.visit(args[3], context); return `OVERLAY(${string} PLACING ${substring} FROM ${start} FOR ${length})`; } else if (args.length === 3) { const string = this.visit(args[0], context); const substring = this.visit(args[1], context); const start = this.visit(args[2], context); 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); if (args.length === 2) { const form = this.visit(args[1], context); const formValue = form.replace(/'/g, ''); return `${string} IS ${formValue} NORMALIZED`; } else { 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); if (args.length === 2) { const form = this.visit(args[1], context); const formValue = form.replace(/'/g, ''); return `normalize(${string}, ${formValue})`; } else { 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); const left2 = this.visit(args[1], context); const right1 = this.visit(args[2], context); const right2 = this.visit(args[3], context); return `(${left1}, ${left2}) OVERLAPS (${right1}, ${right2})`; } // Handle AT TIME ZONE operator with special infix syntax 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(list_utils_1.ListUtils.unwrapList(args[1].A_Expr.name)); if (op === '+' || op === '-' || op === '*' || op === '/') { timestamp = this.formatter.parens(timestamp); } } return `${timestamp} AT TIME ZONE ${timezone}`; } const params = []; if (node.agg_star) { if (node.agg_distinct) { params.push('DISTINCT *'); } else { params.push('*'); } } else { const argStrs = args.map(arg => this.visit(arg, context)); if (node.func_variadic && argStrs.length > 0) { const lastIndex = argStrs.length -