UNPKG

pgsql-deparser

Version:
1,186 lines 451 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Deparser = void 0; const base_1 = require("./visitors/base"); const sql_formatter_1 = require("./utils/sql-formatter"); const quote_utils_1 = require("./utils/quote-utils"); const list_utils_1 = require("./utils/list-utils"); /** * List of real PostgreSQL built-in types as they appear in pg_catalog.pg_type.typname. * These are stored in lowercase in PostgreSQL system catalogs. * Use these for lookups, validations, or introspection logic. */ const pgCatalogTypes = [ // Integers 'int2', // smallint 'int4', // integer 'int8', // bigint // Floating-point & numeric 'float4', // real 'float8', // double precision 'numeric', // arbitrary precision (aka "decimal") // Text & string 'varchar', // variable-length string 'char', // internal one-byte type (used in special cases) 'bpchar', // blank-padded char(n) 'text', // unlimited string 'bool', // boolean // Dates & times 'date', // calendar date 'time', // time without time zone 'timetz', // time with time zone 'timestamp', // timestamp without time zone 'timestamptz', // timestamp with time zone 'interval', // duration // Binary & structured 'bytea', // binary data 'uuid', // universally unique identifier // JSON & XML 'json', // textual JSON 'jsonb', // binary JSON 'xml', // XML format // Money & bitstrings 'money', // currency value 'bit', // fixed-length bit string 'varbit', // variable-length bit string // Network types 'inet', // IPv4 or IPv6 address 'cidr', // network address 'macaddr', // MAC address (6 bytes) 'macaddr8' // MAC address (8 bytes) ]; /** * Parser-level type aliases accepted by PostgreSQL SQL syntax, * but not present in pg_catalog.pg_type. These are resolved to * real types during parsing and never appear in introspection. */ const pgCatalogTypeAliases = [ ['numeric', ['decimal', 'dec']], ['int4', ['int', 'integer']], ['float8', ['float']], ['bpchar', ['character']], ['varchar', ['character varying']] ]; // 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 { tree; options; constructor(tree, opts = {}) { // 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() { const formatter = new sql_formatter_1.SqlFormatter(this.options.newline, this.options.tab, this.options.pretty); const context = new base_1.DeparserContext({ formatter, prettyMode: this.options.pretty }); return this.tree .map(node => { // All nodes should go through the standard deparse method // which will route to the appropriate handler const result = this.deparse(node, context); return result || ''; }) .filter(result => result !== '') .join(context.newline() + context.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) { if (node == null) { return null; } if (!context) { const formatter = new sql_formatter_1.SqlFormatter(this.options.newline, this.options.tab, this.options.pretty); context = new base_1.DeparserContext({ formatter, prettyMode: this.options.pretty }); } 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) { if (!context) { const formatter = new sql_formatter_1.SqlFormatter(this.options.newline, this.options.tab, this.options.pretty); context = new base_1.DeparserContext({ formatter, prettyMode: this.options.pretty }); } const nodeType = this.getNodeType(node); // Handle empty objects if (!nodeType) { return ''; } const nodeData = this.getNodeData(node); const methodName = nodeType; if (typeof this[methodName] === 'function') { const result = this[methodName](nodeData, context); 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(context.newline() + context.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 (!context.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(context.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(context.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.spawn('SelectStmt', { select: true }))) .join(', '); distinctPart = ' DISTINCT ON ' + context.parens(clause); } else { distinctPart = ' DISTINCT'; } 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, context.spawn('SelectStmt', { select: true }))) .join(', '); output.push(context.parens(clause)); } else { output.push('DISTINCT'); } } } if (node.targetList) { const targetList = list_utils_1.ListUtils.unwrapList(node.targetList); if (context.isPretty()) { if (targetList.length === 1) { const targetNode = targetList[0]; const target = this.visit(targetNode, context.spawn('SelectStmt', { select: true })); // Check if single target is complex - if so, use multiline format if (this.isComplexSelectTarget(targetNode)) { output.push('SELECT' + distinctPart); if (this.containsMultilineStringLiteral(target)) { output.push(target); } else { output.push(context.indent(target)); } } else { output.push('SELECT' + distinctPart + ' ' + target); } } else { const targetStrings = targetList .map(e => { const targetStr = this.visit(e, context.spawn('SelectStmt', { select: true })); if (this.containsMultilineStringLiteral(targetStr)) { return targetStr; } return context.indent(targetStr); }); const formattedTargets = targetStrings.join(',' + context.newline()); output.push('SELECT' + distinctPart); output.push(formattedTargets); } } else { const targets = targetList .map(e => this.visit(e, context.spawn('SelectStmt', { 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.spawn('SelectStmt', { from: true }))) .join(', '); output.push('FROM ' + fromItems.trim()); } if (node.whereClause) { if (context.isPretty()) { output.push('WHERE'); const whereExpr = this.visit(node.whereClause, context); const lines = whereExpr.split(context.newline()); const indentedLines = lines.map((line, index) => { if (index === 0) { return context.indent(line); } return line; }); output.push(indentedLines.join(context.newline())); } else { output.push('WHERE'); output.push(this.visit(node.whereClause, context)); } } if (node.valuesLists) { if (context.isPretty()) { 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 context.parens(values.join(', ')); }); const indentedTuples = lists.map(tuple => { if (this.containsMultilineStringLiteral(tuple)) { return tuple; } return context.indent(tuple); }); output.push(indentedTuples.join(',\n')); } else { 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 context.parens(values.join(', ')); }); output.push(lists.join(', ')); } } if (node.groupClause) { const groupList = list_utils_1.ListUtils.unwrapList(node.groupClause); if (context.isPretty()) { const groupItems = groupList .map(e => { const groupStr = this.visit(e, context.spawn('SelectStmt', { group: true, indentLevel: context.indentLevel + 1 })); if (this.containsMultilineStringLiteral(groupStr)) { return groupStr; } return context.indent(groupStr); }) .join(',' + context.newline()); output.push('GROUP BY'); output.push(groupItems); } else { output.push('GROUP BY'); const groupItems = groupList .map(e => this.visit(e, context.spawn('SelectStmt', { group: true }))) .join(', '); output.push(groupItems); } } if (node.havingClause) { if (context.isPretty()) { output.push('HAVING'); const havingStr = this.visit(node.havingClause, context); if (this.containsMultilineStringLiteral(havingStr)) { output.push(havingStr); } else { output.push(context.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 (context.isPretty()) { const sortItems = sortList .map(e => { const sortStr = this.visit(e, context.spawn('SelectStmt', { sort: true, indentLevel: context.indentLevel + 1 })); if (this.containsMultilineStringLiteral(sortStr)) { return sortStr; } return context.indent(sortStr); }) .join(',' + context.newline()); output.push('ORDER BY'); output.push(sortItems); } else { output.push('ORDER BY'); const sortItems = sortList .map(e => this.visit(e, context.spawn('SelectStmt', { 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 (context.isPretty()) { const filteredOutput = output.filter(item => item.trim() !== ''); return filteredOutput.join(context.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, 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(list_utils_1.ListUtils.unwrapList(lexpr.A_Expr.name), context); if (this.needsParentheses(leftOp, operator, 'left')) { leftNeedsParens = true; } } if (lexpr && this.isComplexExpression(lexpr)) { leftNeedsParens = true; } if (leftNeedsParens) { leftExpr = context.parens(leftExpr); } // Check if right expression needs parentheses let rightNeedsParens = false; if (rexpr && 'A_Expr' in rexpr && rexpr.A_Expr?.kind === 'AEXPR_OP') { const rightOp = this.deparseOperatorName(list_utils_1.ListUtils.unwrapList(rexpr.A_Expr.name), context); if (this.needsParentheses(rightOp, operator, 'right')) { rightNeedsParens = true; } } if (rexpr && this.isComplexExpression(rexpr)) { rightNeedsParens = true; } if (rightNeedsParens) { rightExpr = context.parens(rightExpr); } return context.format([leftExpr, operator, rightExpr]); } else if (rexpr) { return context.format([ this.deparseOperatorName(name, context), this.visit(rexpr, context) ]); } break; case 'AEXPR_OP_ANY': return context.format([ this.visit(lexpr, context), this.deparseOperatorName(name, context), 'ANY', context.parens(this.visit(rexpr, context)) ]); case 'AEXPR_OP_ALL': return context.format([ this.visit(lexpr, context), this.deparseOperatorName(name, context), 'ALL', context.parens(this.visit(rexpr, context)) ]); case 'AEXPR_DISTINCT': { let leftExpr = this.visit(lexpr, context); let rightExpr = this.visit(rexpr, context); // Add parentheses for complex expressions if (lexpr && this.isComplexExpression(lexpr)) { leftExpr = context.parens(leftExpr); } if (rexpr && this.isComplexExpression(rexpr)) { rightExpr = context.parens(rightExpr); } return context.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 = context.parens(leftExpr); } if (rexpr && this.isComplexExpression(rexpr)) { rightExpr = context.parens(rightExpr); } return context.format([ leftExpr, 'IS NOT DISTINCT FROM', rightExpr ]); } case 'AEXPR_NULLIF': return context.format([ 'NULLIF', context.parens([ this.visit(lexpr, context), this.visit(rexpr, context) ].join(', ')) ]); case 'AEXPR_IN': const inOperator = this.deparseOperatorName(name, context); if (inOperator === '<>' || inOperator === '!=') { return context.format([ this.visit(lexpr, context), 'NOT IN', context.parens(this.visit(rexpr, context)) ]); } else { return context.format([ this.visit(lexpr, context), 'IN', context.parens(this.visit(rexpr, context)) ]); } case 'AEXPR_LIKE': const likeOp = this.deparseOperatorName(name, context); if (likeOp === '!~~') { return context.format([ this.visit(lexpr, context), 'NOT LIKE', this.visit(rexpr, context) ]); } else { return context.format([ this.visit(lexpr, context), 'LIKE', this.visit(rexpr, context) ]); } case 'AEXPR_ILIKE': const ilikeOp = this.deparseOperatorName(name, context); if (ilikeOp === '!~~*') { return context.format([ this.visit(lexpr, context), 'NOT ILIKE', this.visit(rexpr, context) ]); } else { return context.format([ this.visit(lexpr, context), 'ILIKE', this.visit(rexpr, context) ]); } case 'AEXPR_SIMILAR': const similarOp = this.deparseOperatorName(name, context); 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 context.format([ this.visit(lexpr, context), 'NOT SIMILAR TO', rightExpr ]); } else { return context.format([ this.visit(lexpr, context), 'SIMILAR TO', rightExpr ]); } case 'AEXPR_BETWEEN': return context.format([ this.visit(lexpr, context), 'BETWEEN', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_NOT_BETWEEN': return context.format([ this.visit(lexpr, context), 'NOT BETWEEN', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_BETWEEN_SYM': return context.format([ this.visit(lexpr, context), 'BETWEEN SYMMETRIC', this.visitBetweenRange(rexpr, context) ]); case 'AEXPR_NOT_BETWEEN_SYM': return context.format([ this.visit(lexpr, context), 'NOT BETWEEN SYMMETRIC', this.visitBetweenRange(rexpr, context) ]); } throw new Error(`Unhandled A_Expr kind: ${kind}`); } deparseOperatorName(name, context) { 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, context); }); 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); } isComplexSelectTarget(node) { if (!node) return false; if (node.ResTarget?.val) { return this.isComplexExpression(node.ResTarget.val); } // 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 if (expr.lexpr && this.isComplexSelectTarget(expr.lexpr)) return true; 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; } 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.spawn('InsertStmt', { insertColumns: true }); const columnNames = cols.map(col => this.visit(col, insertContext)); if (context.isPretty()) { // Always format columns in multiline parentheses for pretty printing const indentedColumns = columnNames.map(col => context.indent(col)); output.push('(\n' + indentedColumns.join(',\n') + '\n)'); } else { output.push(context.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(context.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(context.parens(names.join(', '))); output.push('='); output.push(this.visit(firstTarget.ResTarget.val.MultiAssignRef.source, context)); } else { const updateContext = context.spawn('UpdateStmt', { 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 = `${context.parens(names.join(', '))} = ${this.visit(multiAssignRef.source, context)}`; assignmentParts.push(multiAssignment); } else { // Handle regular single-column assignment assignmentParts.push(this.visit(target, context.spawn('UpdateStmt', { 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 (context.isPretty()) { const cteStrings = ctes.map((cte, index) => { const cteStr = this.visit(cte, context); const prefix = index === 0 ? context.newline() : ',' + context.newline(); if (this.containsMultilineStringLiteral(cteStr)) { return prefix + cteStr; } return prefix + context.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.spawn('BoolExpr', { 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 (context.isPretty() && args.length > 1) { const andArgs = args.map(arg => this.visit(arg, boolContext)).join(context.newline() + context.indent('AND ')); return formatStr.replace('%s', () => andArgs); } else { const andArgs = args.map(arg => this.visit(arg, boolContext)).join(' AND '); return formatStr.replace('%s', () => andArgs); } case 'OR_EXPR': if (context.isPretty() && args.length > 1) { const orArgs = args.map(arg => this.visit(arg, boolContext)).join(context.newline() + context.indent('OR ')); return formatStr.replace('%s', () => orArgs); } else { const orArgs = args.map(arg => this.visit(arg, boolContext)).join(' OR '); 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 >