pgsql-deparser
Version:
PostgreSQL AST Deparser
1,186 lines • 451 kB
JavaScript
"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 >