From 80d4e853fe6197228612e4312d690f66c1f818c4 Mon Sep 17 00:00:00 2001
From: Onder KALACI <onderkalaci@gmail.com>
Date: Fri, 6 Dec 2024 11:07:26 +0300
Subject: [PATCH] Allow FDW extensions to support MERGE command via CustomScan

Currently, it is not possible for any fdw extension
to support Merge command, as that's prohibited in the
parser.

In this commit, we allow extensions to support Merge command
via `CustomScan` node by moving the Merge support check
from parser to planner.

For existing fdw, they don't have to change anyting. We change
postgres_fdw mostly for documentation purposes.
---
 contrib/postgres_fdw/postgres_fdw.c     | 16 ++++++++++++++++
 doc/src/sgml/fdwhandler.sgml            | 25 +++++++++++++++++++++++++
 src/backend/optimizer/plan/createplan.c | 16 ++++++++++------
 src/backend/parser/parse_merge.c        | 12 ++++++++++--
 src/include/foreign/fdwapi.h            |  5 +++++
 src/tools/pgindent/typedefs.list        |  1 +
 6 files changed, 67 insertions(+), 8 deletions(-)

diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index c0810fbd7c..541a57575c 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -376,6 +376,7 @@ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
 static void postgresEndForeignInsert(EState *estate,
 									 ResultRelInfo *resultRelInfo);
 static int	postgresIsForeignRelUpdatable(Relation rel);
+static bool postgresIsForeignServerMergeCapable(void);
 static bool postgresPlanDirectModify(PlannerInfo *root,
 									 ModifyTable *plan,
 									 Index resultRelation,
@@ -574,6 +575,7 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 	routine->BeginForeignInsert = postgresBeginForeignInsert;
 	routine->EndForeignInsert = postgresEndForeignInsert;
 	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
+	routine->IsForeignServerMergeCapable = postgresIsForeignServerMergeCapable;
 	routine->PlanDirectModify = postgresPlanDirectModify;
 	routine->BeginDirectModify = postgresBeginDirectModify;
 	routine->IterateDirectModify = postgresIterateDirectModify;
@@ -2348,6 +2350,20 @@ postgresIsForeignRelUpdatable(Relation rel)
 		(1 << CMD_INSERT) | (1 << CMD_UPDATE) | (1 << CMD_DELETE) : 0;
 }
 
+
+/*
+ * postgresIsForeignServerMergeCapable
+ *		Determine whether the foreign server is capable of merge. Core code (ExecMerge())
+ *		doesn't support merge on foreign tables, so we always return false. Some FDWs
+ *		may support merge via CustomScan nodes, in which case they should return true.
+ */
+static bool
+postgresIsForeignServerMergeCapable(void)
+{
+	/* postgres_fdw does not support CMD_MERGE */
+	return false;
+}
+
 /*
  * postgresRecheckForeignScan
  *		Execute a local join execution plan for a foreign join
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index b80320504d..818ae93e7e 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -1288,6 +1288,31 @@ RecheckForeignScan(ForeignScanState *node,
     </para>
    </sect2>
 
+   <sect2 id="fdw-callbacks-merge">
+    <title>FDW Routines for <command>MERGE</command></title>
+
+    <para>
+<programlisting>
+bool
+IsForeignServerMergeCapable();
+</programlisting>
+
+     Postgres doesn't support <command>MERGE</command> on foreign tables,
+     see <function>ExecMerge</function>. Still, extensions may provide
+     custom scan nodes to support <command>MERGE</command> on foreign
+     tables. If your extension provides such custom scan node, this
+     function should return true.
+    </para>
+
+    <para>
+     If the <function>IsForeignServerMergeCapable</function> pointer is set to
+     <literal>NULL</literal> or returns <literal>false</literal>,
+     <command>MERGE</command> fails on the planning phase with a proper
+     error message.
+    </para>
+
+   </sect2>
+
    <sect2 id="fdw-callbacks-explain">
     <title>FDW Routines for <command>EXPLAIN</command></title>
 
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 178c572b02..de6418ac0b 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -7242,13 +7242,17 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
 		 */
 		if (operation == CMD_MERGE && fdwroutine != NULL)
 		{
-			RangeTblEntry *rte = planner_rt_fetch(rti, root);
+			/* Check if the foreign table is mergeable */
+			if (!fdwroutine->IsForeignServerMergeCapable || !fdwroutine->IsForeignServerMergeCapable())
+			{
+				RangeTblEntry *rte = planner_rt_fetch(rti, root);
 
-			ereport(ERROR,
-					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					errmsg("cannot execute MERGE on relation \"%s\"",
-						   get_rel_name(rte->relid)),
-					errdetail_relkind_not_supported(rte->relkind));
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("cannot execute MERGE on relation \"%s\"",
+							   get_rel_name(rte->relid)),
+						errdetail_relkind_not_supported(rte->relkind));
+			}
 		}
 
 		/*
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
index 87df79027d..97db9f11f4 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -194,10 +194,18 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
 										 false, targetPerms);
 	qry->mergeTargetRelation = qry->resultRelation;
 
-	/* The target relation must be a table or a view */
+	/*
+	 * The target relation must be a table or a view.
+	 *
+	 * Although the Merge command on foreign tables are allowed in the
+	 * grammar, it is not natively supported for foreign tables. We allow the
+	 * parser so that we give extensions a chance to support it via custom
+	 * scan nodes.
+	 */
 	if (pstate->p_target_relation->rd_rel->relkind != RELKIND_RELATION &&
 		pstate->p_target_relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE &&
-		pstate->p_target_relation->rd_rel->relkind != RELKIND_VIEW)
+		pstate->p_target_relation->rd_rel->relkind != RELKIND_VIEW &&
+		pstate->p_target_relation->rd_rel->relkind != RELKIND_FOREIGN_TABLE)
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot execute MERGE on relation \"%s\"",
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index fcde3876b2..13a4fc0e25 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -115,6 +115,8 @@ typedef void (*EndForeignInsert_function) (EState *estate,
 
 typedef int (*IsForeignRelUpdatable_function) (Relation rel);
 
+typedef bool (*IsForeignServerMergeCapable_function) (void);
+
 typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
 										   ModifyTable *plan,
 										   Index resultRelation,
@@ -248,6 +250,9 @@ typedef struct FdwRoutine
 	RefetchForeignRow_function RefetchForeignRow;
 	RecheckForeignScan_function RecheckForeignScan;
 
+	/* Support functions for MERGE */
+	IsForeignServerMergeCapable_function IsForeignServerMergeCapable;
+
 	/* Support functions for EXPLAIN */
 	ExplainForeignScan_function ExplainForeignScan;
 	ExplainForeignModify_function ExplainForeignModify;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index ce33e55bf1..3c38e9f98c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1268,6 +1268,7 @@ IpcSemaphoreId
 IpcSemaphoreKey
 IsForeignPathAsyncCapable_function
 IsForeignRelUpdatable_function
+IsForeignServerMergeCapable_function
 IsForeignScanParallelSafe_function
 IsoConnInfo
 IspellDict
-- 
2.43.0

