From a89d46592ba4d113f57a890b63ef123d8470db45 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Fri, 5 May 2023 20:40:38 +0200
Subject: [PATCH] add ruleutils.c support for MERGE

---
 src/backend/utils/adt/ruleutils.c   | 119 ++++++++++++++++++++++++++++
 src/test/regress/expected/merge.out |  68 ++++++++++++++++
 src/test/regress/sql/merge.sql      |  38 +++++++++
 3 files changed, 225 insertions(+)

diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 461735e84f0..851488dd6d3 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -411,6 +411,8 @@ static void get_update_query_targetlist_def(Query *query, List *targetList,
 											RangeTblEntry *rte);
 static void get_delete_query_def(Query *query, deparse_context *context,
 								 bool colNamesVisible);
+static void get_merge_query_def(Query *query, deparse_context *context,
+								bool colNamesVisible);
 static void get_utility_query_def(Query *query, deparse_context *context);
 static void get_basic_select_query(Query *query, deparse_context *context,
 								   TupleDesc resultDesc, bool colNamesVisible);
@@ -5448,6 +5450,10 @@ get_query_def(Query *query, StringInfo buf, List *parentnamespace,
 			get_delete_query_def(query, &context, colNamesVisible);
 			break;
 
+		case CMD_MERGE:
+			get_merge_query_def(query, &context, colNamesVisible);
+			break;
+
 		case CMD_NOTHING:
 			appendStringInfoString(buf, "NOTHING");
 			break;
@@ -7043,6 +7049,119 @@ get_delete_query_def(Query *query, deparse_context *context,
 	}
 }
 
+/* ----------
+ * get_merge_query_def				- Parse back a MERGE parsetree
+ * ----------
+ */
+static void
+get_merge_query_def(Query *query, deparse_context *context,
+					bool colNamesVisible)
+{
+
+	StringInfo	buf = context->buf;
+	RangeTblEntry *rte;
+	ListCell   *lc;
+
+	/* Insert the WITH clause if given */
+	get_with_clause(query, context);
+
+	/*
+	 * Start the query with MERGE INTO relname
+	 */
+	rte = rt_fetch(query->resultRelation, query->rtable);
+	Assert(rte->rtekind == RTE_RELATION);
+	if (PRETTY_INDENT(context))
+	{
+		appendStringInfoChar(buf, ' ');
+		context->indentLevel += PRETTYINDENT_STD;
+	}
+	appendStringInfo(buf, "MERGE INTO %s%s",
+					 only_marker(rte),
+					 generate_relation_name(rte->relid, NIL));
+
+	/* Print the relation alias, if needed */
+	get_rte_alias(rte, query->resultRelation, false, context);
+
+	get_from_clause(query, " USING ", context);
+	appendContextKeyword(context, " ON ",
+						 -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+	get_rule_expr(query->jointree->quals, context, false);
+
+	appendStringInfoChar(buf, '\n');	/* FIXME proper way to do this? */
+
+	/* FIXME missing: OVERRIDING clause */
+	foreach(lc, query->mergeActionList)
+	{
+		MergeAction *action = lfirst_node(MergeAction, lc);
+
+		appendStringInfo(buf, "WHEN %sMATCHED", action->matched ? "" : "NOT ");
+
+		if (action->qual)
+		{
+			appendContextKeyword(context, " AND ",
+								 -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+			get_rule_expr(action->qual, context, false);
+		}
+		appendContextKeyword(context, " THEN ", -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+
+		if (action->commandType == CMD_INSERT)
+		{
+			ListCell   *lc2;
+			char	   *sep = "";
+			List	   *strippedexprs = NIL;
+
+			appendStringInfoString(buf, "INSERT ");
+
+			/*
+			 * This matches what get_insert_query_def does for the VALUES
+			 * clause
+			 */
+			if (action->targetList)
+				appendStringInfoChar(buf, '(');
+			foreach(lc2, action->targetList)
+			{
+				TargetEntry *tle = (TargetEntry *) lfirst(lc2);
+
+				Assert(!tle->resjunk);
+
+				appendStringInfoString(buf, sep);
+				sep = ", ";
+
+				appendStringInfoString(buf, quote_identifier(get_attname(rte->relid,
+																		 tle->resno,
+																		 false)));
+				strippedexprs = lappend(strippedexprs,
+										processIndirection((Node *) tle->expr,
+														   context));
+			}
+			if (action->targetList)
+				appendStringInfoString(buf, ") ");
+			if (strippedexprs)
+			{
+				appendContextKeyword(context, "VALUES (",
+									 -PRETTYINDENT_STD, PRETTYINDENT_STD, 2);
+				get_rule_list_toplevel(strippedexprs, context, false);
+				appendStringInfoChar(buf, ')');
+			}
+			else
+				appendStringInfoString(buf, "DEFAULT VALUES");
+		}
+		else if (action->commandType == CMD_UPDATE)
+		{
+			appendStringInfoString(buf, "UPDATE SET ");
+			get_update_query_targetlist_def(query, action->targetList, context, rte);
+		}
+		else if (action->commandType == CMD_DELETE)
+			appendStringInfoString(buf, "DELETE ");
+		else if (action->commandType == CMD_NOTHING)
+			appendStringInfoString(buf, "DO NOTHING ");
+
+		appendStringInfoChar(buf, '\n');
+	}
+
+	/* No RETURNING support yet */
+	Assert(query->returningList == NIL);
+}
 
 /* ----------
  * get_utility_query_def			- Parse back a UTILITY parsetree
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index 133d42117c0..2e71dda4228 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -1319,6 +1319,74 @@ ERROR:  syntax error at or near "RETURNING"
 LINE 10: RETURNING *;
          ^
 ROLLBACK;
+-- Verify that ruleutils can properly deparse
+CREATE TABLE sf_target(id int, data text, filling int[]);
+CREATE TABLE sf_source(id int, data text);
+CREATE OR REPLACE FUNCTION public.merge_sf_test()
+ RETURNS void
+ LANGUAGE sql
+BEGIN ATOMIC
+ MERGE INTO sf_target
+    USING sf_source s
+   ON (s.id = sf_target.id)
+WHEN MATCHED
+   AND ((s.id + sf_target.id) = 42)
+   THEN UPDATE SET data = (repeat(sf_target.data, s.id) || s.data), id = length(s.data)
+WHEN NOT MATCHED
+   AND (s.data IS NOT NULL)
+   THEN INSERT (data, id)
+   VALUES (s.data, s.id)
+WHEN MATCHED
+   AND (length((s.data || sf_target.data)) > 10)
+   THEN UPDATE SET data = s.data
+WHEN MATCHED
+   THEN UPDATE SET filling[s.id] = sf_target.id
+WHEN NOT MATCHED
+   AND (s.id > 200)
+   THEN INSERT DEFAULT VALUES
+WHEN NOT MATCHED
+   AND (s.id > 100)
+   THEN INSERT (id, data)
+   VALUES (s.id, DEFAULT)
+WHEN NOT MATCHED
+   THEN INSERT (filling[1], id)
+   VALUES (s.id, s.id)
+;
+END;
+\sf+ merge_sf_test
+        CREATE OR REPLACE FUNCTION public.merge_sf_test()
+         RETURNS void
+         LANGUAGE sql
+1       BEGIN ATOMIC
+2        MERGE INTO sf_target
+3           USING sf_source s
+4          ON (s.id = sf_target.id)
+5       WHEN MATCHED
+6          AND ((s.id + sf_target.id) = 42)
+7          THEN UPDATE SET data = (repeat(sf_target.data, s.id) || s.data), id = length(s.data)
+8       WHEN NOT MATCHED
+9          AND (s.data IS NOT NULL)
+10         THEN INSERT (data, id)
+11         VALUES (s.data, s.id)
+12      WHEN MATCHED
+13         AND (length((s.data || sf_target.data)) > 10)
+14         THEN UPDATE SET data = s.data
+15      WHEN MATCHED
+16         THEN UPDATE SET filling[s.id] = sf_target.id
+17      WHEN NOT MATCHED
+18         AND (s.id > 200)
+19         THEN INSERT DEFAULT VALUES
+20      WHEN NOT MATCHED
+21         AND (s.id > 100)
+22         THEN INSERT (id, data)
+23         VALUES (s.id, DEFAULT)
+24      WHEN NOT MATCHED
+25         THEN INSERT (filling[1], id)
+26         VALUES (s.id, s.id)
+27      ;
+28      END
+DROP FUNCTION merge_sf_test;
+DROP TABLE sf_target, sf_source;
 -- EXPLAIN
 CREATE TABLE ex_mtarget (a int, b int)
   WITH (autovacuum_enabled=off);
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
index 4cf6db908b5..41662089905 100644
--- a/src/test/regress/sql/merge.sql
+++ b/src/test/regress/sql/merge.sql
@@ -870,6 +870,44 @@ WHEN MATCHED AND tid < 2 THEN
 RETURNING *;
 ROLLBACK;
 
+-- Verify that ruleutils can properly deparse
+CREATE TABLE sf_target(id int, data text, filling int[]);
+CREATE TABLE sf_source(id int, data text);
+CREATE OR REPLACE FUNCTION public.merge_sf_test()
+ RETURNS void
+ LANGUAGE sql
+BEGIN ATOMIC
+ MERGE INTO sf_target
+    USING sf_source s
+   ON (s.id = sf_target.id)
+WHEN MATCHED
+   AND ((s.id + sf_target.id) = 42)
+   THEN UPDATE SET data = (repeat(sf_target.data, s.id) || s.data), id = length(s.data)
+WHEN NOT MATCHED
+   AND (s.data IS NOT NULL)
+   THEN INSERT (data, id)
+   VALUES (s.data, s.id)
+WHEN MATCHED
+   AND (length((s.data || sf_target.data)) > 10)
+   THEN UPDATE SET data = s.data
+WHEN MATCHED
+   THEN UPDATE SET filling[s.id] = sf_target.id
+WHEN NOT MATCHED
+   AND (s.id > 200)
+   THEN INSERT DEFAULT VALUES
+WHEN NOT MATCHED
+   AND (s.id > 100)
+   THEN INSERT (id, data)
+   VALUES (s.id, DEFAULT)
+WHEN NOT MATCHED
+   THEN INSERT (filling[1], id)
+   VALUES (s.id, s.id)
+;
+END;
+\sf+ merge_sf_test
+DROP FUNCTION merge_sf_test;
+DROP TABLE sf_target, sf_source;
+
 -- EXPLAIN
 CREATE TABLE ex_mtarget (a int, b int)
   WITH (autovacuum_enabled=off);
-- 
2.39.2

