From 77c426152e1fbcaf4bff2bd7a32765a3b568cbb4 Mon Sep 17 00:00:00 2001
From: Pavan Deolasee <pavan.deolasee@gmail.com>
Date: Tue, 27 Mar 2018 09:20:33 +0530
Subject: [PATCH v27 3/5] Fix EXPLAIN ANALYZE output to report counts
 correctly.

Fix some typos in passing

Address Simon's review comments

Run ExecCheckPlanOutput() for MERGE action's targetlists

Fetch tableoid only for partitioned tables

Some other misc fixes
---
 doc/src/sgml/ref/create_policy.sgml    |   7 ++
 doc/src/sgml/ref/merge.sgml            |   7 ++
 doc/src/sgml/trigger.sgml              |   6 +-
 src/backend/commands/explain.c         |   9 ++-
 src/backend/commands/trigger.c         |   2 +-
 src/backend/executor/nodeMerge.c       |  29 ++++---
 src/backend/executor/nodeModifyTable.c |  11 +--
 src/backend/optimizer/util/relnode.c   |   1 +
 src/backend/parser/parse_clause.c      |   6 ++
 src/backend/parser/parse_merge.c       |  23 ++----
 src/backend/rewrite/rewriteHandler.c   |  32 ++++----
 src/include/executor/instrument.h      |   7 +-
 src/include/nodes/execnodes.h          |  11 ++-
 src/test/regress/expected/merge.out    | 138 ++++++++++++++++++++++++++++++++-
 src/test/regress/expected/with.out     |  16 ++--
 src/test/regress/sql/merge.sql         |  44 +++++++++++
 16 files changed, 277 insertions(+), 72 deletions(-)

diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml
index 0e35b0ef43..32f39a48ba 100644
--- a/doc/src/sgml/ref/create_policy.sgml
+++ b/doc/src/sgml/ref/create_policy.sgml
@@ -94,6 +94,13 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
    exist, a <quote>default deny</quote> policy is assumed, so that no rows will
    be visible or updatable.
   </para>
+
+  <para>
+   No separate policy exists for <command>MERGE</command>. Instead policies
+   defined for <literal>SELECT</literal>, <literal>INSERT</literal>,
+   <literal>UPDATE</literal> and <literal>DELETE</literal> are applied
+   while executing MERGE, depending on the actions that are activated.
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
index 405a4cee29..539e512ced 100644
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -115,6 +115,13 @@ DELETE
    of the <replaceable class="parameter">target_table_name</replaceable>
    referred to in a <literal>condition</literal>.
   </para>
+
+  <para>
+   MERGE is not supported if the <replaceable
+   class="parameter">target_table_name</replaceable> has
+   <literal>RULES</literal> defined on it.
+   See <xref linkend="rules"/> for more information about <literal>RULES</literal>.
+  </para>
  </refsect1>
 
  <refsect1>
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index ac662bc64d..cce58fbf1d 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -186,15 +186,15 @@
     No separate triggers are defined for <command>MERGE</command>. Instead,
     statement-level or row-level <command>UPDATE</command>,
     <command>DELETE</command> and <command>INSERT</command> triggers are fired
-    depedning on what actions are specified in the <command>MERGE</command> query
+    depending on what actions are specified in the <command>MERGE</command> query
     and what actions are activated.
    </para>
 
    <para>
     While running a <command>MERGE</command> command, statement-level
     <literal>BEFORE</literal> and <literal>AFTER</literal> triggers are fired for
-    specific actions specified in the <command>MERGE</command>, irrespective of
-    whether the action is finally activated or not. This is same as
+    events specified in the actions of the <command>MERGE</command> command,
+    irrespective of whether the action is finally activated or not. This is same as
     an <command>UPDATE</command> statement that updates no rows, yet
     statement-level triggers are fired. The row-level triggers are fired only
     when a row is actually updated, inserted or deleted. So it's perfectly legal
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index dc2f727d21..ca486872ac 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3086,18 +3086,21 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 			double		insert_path;
 			double		update_path;
 			double		delete_path;
+			double		skipped_path;
 
 			InstrEndLoop(mtstate->mt_plans[0]->instrument);
 
 			/* count the number of source rows */
 			total = mtstate->mt_plans[0]->instrument->ntuples;
-			update_path = mtstate->ps.instrument->nfiltered1;
-			delete_path = mtstate->ps.instrument->nfiltered2;
-			insert_path = total - update_path - delete_path;
+			insert_path = mtstate->ps.instrument->nfiltered1;
+			update_path = mtstate->ps.instrument->nfiltered2;
+			delete_path = mtstate->ps.instrument->nfiltered3;
+			skipped_path = total - insert_path - update_path - delete_path;
 
 			ExplainPropertyFloat("Tuples Inserted", NULL, insert_path, 0, es);
 			ExplainPropertyFloat("Tuples Updated", NULL, update_path, 0, es);
 			ExplainPropertyFloat("Tuples Deleted", NULL, delete_path, 0, es);
+			ExplainPropertyFloat("Tuples Skipped", NULL, skipped_path, 0, es);
 		}
 	}
 
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 331c37ad67..1617706376 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -3317,7 +3317,7 @@ ltrmark:;
 					 * If we're running MERGE then we must install the
 					 * new tuple in the slot of the underlying join query and
 					 * not the result relation itself. If the join does not
-					 * yeild any tuple, the caller will take the necessary
+					 * yield any tuple, the caller will take the necessary
 					 * action.
 					 */
 					epqslot = EvalPlanQual(estate,
diff --git a/src/backend/executor/nodeMerge.c b/src/backend/executor/nodeMerge.c
index e633af7704..ee41ed2eb2 100644
--- a/src/backend/executor/nodeMerge.c
+++ b/src/backend/executor/nodeMerge.c
@@ -59,8 +59,6 @@ ExecMergeMatched(ModifyTableState *mtstate, EState *estate,
 {
 	ExprContext *econtext = mtstate->ps.ps_ExprContext;
 	bool		isNull;
-	Datum		datum;
-	Oid			tableoid = InvalidOid;
 	List	   *mergeMatchedActionStates = NIL;
 	HeapUpdateFailureData hufd;
 	bool		tuple_updated,
@@ -73,22 +71,22 @@ ExecMergeMatched(ModifyTableState *mtstate, EState *estate,
 	ListCell   *l;
 	TupleTableSlot *saved_slot = slot;
 
-
-	/*
-	 * We always fetch the tableoid while performing MATCHED MERGE action.
-	 * This is strictly not required if the target table is not a partitioned
-	 * table. But we are not yet optimising for that case.
-	 */
-	datum = ExecGetJunkAttribute(slot, junkfilter->jf_otherJunkAttNo,
-								 &isNull);
-	Assert(!isNull);
-	tableoid = DatumGetObjectId(datum);
-
 	if (mtstate->mt_partition_tuple_routing)
 	{
+		Datum		datum;
+		Oid			tableoid = InvalidOid;
 		int         leaf_part_index;
 		PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
 
+		/*
+		 * In case of partitioned table, we fetch the tableoid while performing
+		 * MATCHED MERGE action.
+		 */
+		datum = ExecGetJunkAttribute(slot, junkfilter->jf_otherJunkAttNo,
+				&isNull);
+		Assert(!isNull);
+		tableoid = DatumGetObjectId(datum);
+
 		/*
 		 * If we're dealing with a MATCHED tuple, then tableoid must have been
 		 * set correctly. In case of partitioned table, we must now fetch the
@@ -361,9 +359,9 @@ lmerge_matched:;
 		}
 
 		if (action->commandType == CMD_UPDATE && tuple_updated)
-			InstrCountFiltered1(&mtstate->ps, 1);
-		if (action->commandType == CMD_DELETE && tuple_deleted)
 			InstrCountFiltered2(&mtstate->ps, 1);
+		if (action->commandType == CMD_DELETE && tuple_deleted)
+			InstrCountFiltered3(&mtstate->ps, 1);
 
 		/*
 		 * We've activated one of the WHEN clauses, so we don't search
@@ -466,6 +464,7 @@ ExecMergeNotMatched(ModifyTableState *mtstate, EState *estate,
 				/* Revert ExecPrepareTupleRouting's state change. */
 				if (proute)
 					estate->es_result_relation_info = resultRelInfo;
+				InstrCountFiltered1(&mtstate->ps, 1);
 				break;
 			case CMD_NOTHING:
 				/* Do Nothing */
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index def7eec223..08f812fc6d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -2733,9 +2733,13 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 			switch (action->commandType)
 			{
 				case CMD_INSERT:
+					ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
+										action->targetList);
 					mtstate->mt_merge_subcommands |= MERGE_INSERT;
 					break;
 				case CMD_UPDATE:
+					ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
+										action->targetList);
 					mtstate->mt_merge_subcommands |= MERGE_UPDATE;
 					break;
 				case CMD_DELETE:
@@ -2814,10 +2818,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 				subplan = mtstate->mt_plans[i]->plan;
 
-				/*
-				 * XXX we probably need to check plan output for CMD_MERGE
-				 * also
-				 */
 				if (operation == CMD_INSERT || operation == CMD_UPDATE)
 					ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
 										subplan->targetlist);
@@ -2842,7 +2842,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 						if (!AttributeNumberIsValid(j->jf_junkAttNo))
 							elog(ERROR, "could not find junk ctid column");
 
-						if (operation == CMD_MERGE)
+						if (operation == CMD_MERGE &&
+							relkind == RELKIND_PARTITIONED_TABLE)
 						{
 							j->jf_otherJunkAttNo = ExecFindJunkAttribute(j, "tableoid");
 							if (!AttributeNumberIsValid(j->jf_otherJunkAttNo))
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 901cf24e20..da8f0f93fc 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -1237,6 +1237,7 @@ find_childrel_parents(PlannerInfo *root, RelOptInfo *rel)
 	return result;
 }
 
+
 /*
  * get_baserel_parampathinfo
  *		Get the ParamPathInfo for a parameterized path for a base relation,
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 9df9828df8..857fe058c3 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -1094,6 +1094,12 @@ getRTEForSpecialRelationTypes(ParseState *pstate, RangeVar *rv)
  *
  * *top_rti: receives the rangetable index of top_rte.  (Ditto.)
  *
+ * *right_rte: receives the RTE corresponding to the right side of the
+ * jointree. Only MERGE really needs to know about this and only MERGE passes a
+ * non-NULL pointer.
+ *
+ * *right_rti: receives the rangetable index of the right_rte.
+ *
  * *namespace: receives a List of ParseNamespaceItems for the RTEs exposed
  * as table/column names by this item.  (The lateral_only flags in these items
  * are indeterminate and should be explicitly set by the caller before use.)
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
index 26a6692231..6df63439bb 100644
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -279,10 +279,13 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
 	}
 
 	/*
-	 * Construct a query of the form SELECT relation.ctid	--junk attribute
-	 * ,relation.tableoid	--junk attribute ,source_relation.<somecols>
-	 * ,relation.<somecols> FROM relation RIGHT JOIN source_relation ON
-	 * join_condition -- no WHERE clause - all conditions are applied in
+	 * Construct a query of the form
+	 * 	SELECT relation.ctid	--junk attribute
+	 *		  ,relation.tableoid	--junk attribute
+	 * 		  ,source_relation.<somecols>
+	 * 		  ,relation.<somecols>
+	 *  FROM relation RIGHT JOIN source_relation
+	 *  ON  join_condition; -- no WHERE clause - all conditions are applied in
 	 * executor
 	 *
 	 * stmt->relation is the target relation, given as a RangeVar
@@ -353,18 +356,6 @@ transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
 	 * If there are no INSERT actions we won't be using the non-matching
 	 * candidate rows for anything, so no need for an outer join. We do still
 	 * need an inner join for UPDATE and DELETE actions.
-	 *
-	 * Possible additional simplifications...
-	 *
-	 * XXX if we have a constant ON clause, we can skip join altogether
-	 *
-	 * XXX if we have a constant subquery, we can also skip join
-	 *
-	 * XXX if we were really keen we could look through the actionList and
-	 * pull out common conditions, if there were no terminal clauses and put
-	 * them into the main query as an early row filter but that seems like an
-	 * atypical case and so checking for it would be likely to just be wasted
-	 * effort.
 	 */
 	if (targetPerms & ACL_INSERT)
 		joinexpr->jointype = JOIN_RIGHT;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 8e5661df35..74b2829ffa 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1407,22 +1407,26 @@ rewriteTargetListMerge(Query *parsetree, Relation target_relation)
 	parsetree->targetList = lappend(parsetree->targetList, tle);
 
 	/*
-	 * Emit TABLEOID so that executor can find the row to update or delete.
+	 * If we are dealing with partitioned table, then emit TABLEOID so that
+	 * executor can find the partition the row belongs to.
 	 */
-	var = makeVar(parsetree->mergeTarget_relation,
-				  TableOidAttributeNumber,
-				  OIDOID,
-				  -1,
-				  InvalidOid,
-				  0);
-
-	attrname = "tableoid";
-	tle = makeTargetEntry((Expr *) var,
-						  list_length(parsetree->targetList) + 1,
-						  pstrdup(attrname),
-						  true);
+	if (target_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+	{
+		var = makeVar(parsetree->mergeTarget_relation,
+				TableOidAttributeNumber,
+				OIDOID,
+				-1,
+				InvalidOid,
+				0);
+
+		attrname = "tableoid";
+		tle = makeTargetEntry((Expr *) var,
+				list_length(parsetree->targetList) + 1,
+				pstrdup(attrname),
+				true);
 
-	parsetree->targetList = lappend(parsetree->targetList, tle);
+		parsetree->targetList = lappend(parsetree->targetList, tle);
+	}
 }
 
 /*
diff --git a/src/include/executor/instrument.h b/src/include/executor/instrument.h
index b72f91898a..28eb0093d4 100644
--- a/src/include/executor/instrument.h
+++ b/src/include/executor/instrument.h
@@ -58,8 +58,11 @@ typedef struct Instrumentation
 	double		total;			/* Total total time (in seconds) */
 	double		ntuples;		/* Total tuples produced */
 	double		nloops;			/* # of run cycles for this node */
-	double		nfiltered1;		/* # tuples removed by scanqual or joinqual */
-	double		nfiltered2;		/* # tuples removed by "other" quals */
+	double		nfiltered1;		/* # tuples removed by scanqual or joinqual OR
+								 * # tuples inserted by MERGE */
+	double		nfiltered2;		/* # tuples removed by "other" quals OR
+								 * # tuples updated by MERGE */
+	double		nfiltered3;		/* # tuples deleted by MERGE */
 	BufferUsage bufusage;		/* Total buffer usage */
 } Instrumentation;
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 0bf1d7aeb6..a839d53334 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -467,12 +467,12 @@ typedef struct ResultRelInfo
 	MergeState *ri_mergeState;
 
 	/*
-	 * While executing MERGE, the target relation is processed twice; once to
-	 * as a result relation and to run a join between the target and the
+	 * While executing MERGE, the target relation is processed twice; once
+	 * as a target relation and once to run a join between the target and the
 	 * source. We generate two different RTEs for these two purposes, one with
 	 * rte->inh set to false and other with rte->inh set to true.
 	 *
-	 * Since the plan re-evaluated by EvalPlanQual uses the second RTE, we must
+	 * Since the plan re-evaluated by EvalPlanQual uses the join RTE, we must
 	 * install the updated tuple in the scan corresponding to that RTE. The
 	 * following member tracks the index of the second RTE for EvalPlanQual
 	 * purposes. ri_mergeTargetRTI is non-zero only when MERGE is in-progress.
@@ -1005,6 +1005,11 @@ typedef struct PlanState
 		if (((PlanState *)(node))->instrument) \
 			((PlanState *)(node))->instrument->nfiltered2 += (delta); \
 	} while(0)
+#define InstrCountFiltered3(node, delta) \
+	do { \
+		if (((PlanState *)(node))->instrument) \
+			((PlanState *)(node))->instrument->nfiltered3 += (delta); \
+	} while(0)
 
 /*
  * EPQState is state for executing an EvalPlanQual recheck on a candidate
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index 411ccd0e66..90f3177743 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -822,6 +822,7 @@ NOTICE:  AFTER INSERT STATEMENT trigger
    Tuples Inserted: 1
    Tuples Updated: 1
    Tuples Deleted: 1
+   Tuples Skipped: 0
    ->  Hash Left Join (actual rows=3 loops=1)
          Hash Cond: (s.sid = t_1.tid)
          ->  Seq Scan on source s (actual rows=3 loops=1)
@@ -840,7 +841,7 @@ NOTICE:  AFTER INSERT STATEMENT trigger
  Trigger merge_bsd: calls=1
  Trigger merge_bsi: calls=1
  Trigger merge_bsu: calls=1
-(22 rows)
+(23 rows)
 
 SELECT * FROM target ORDER BY tid;
  tid | balance 
@@ -1085,6 +1086,7 @@ NOTICE:  AFTER UPDATE STATEMENT trigger
    Tuples Inserted: 0
    Tuples Updated: 1
    Tuples Deleted: 0
+   Tuples Skipped: 0
    ->  Seq Scan on target t_1 (actual rows=1 loops=1)
          Filter: (tid = 1)
          Rows Removed by Filter: 2
@@ -1092,7 +1094,7 @@ NOTICE:  AFTER UPDATE STATEMENT trigger
  Trigger merge_asu: calls=1
  Trigger merge_bru: calls=1
  Trigger merge_bsu: calls=1
-(11 rows)
+(12 rows)
 
 SELECT * FROM target ORDER BY tid;
  tid | balance 
@@ -1210,6 +1212,138 @@ ERROR:  syntax error at or near "RETURNING"
 LINE 10: RETURNING *
          ^
 ROLLBACK;
+-- EXPLAIN
+CREATE TABLE ex_mtarget (a int, b int);
+CREATE TABLE ex_msource (a int, b int);
+INSERT INTO ex_mtarget SELECT i, i*10 FROM generate_series(1,100,2) i;
+INSERT INTO ex_msource SELECT i, i*10 FROM generate_series(1,100,1) i;
+-- only updates
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
+WHEN MATCHED THEN
+	UPDATE SET b = t.b + 1;
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0 loops=1)
+   Tuples Inserted: 0
+   Tuples Updated: 50
+   Tuples Deleted: 0
+   Tuples Skipped: 0
+   ->  Merge Join (actual rows=50 loops=1)
+         Merge Cond: (t_1.a = s.a)
+         ->  Sort (actual rows=50 loops=1)
+               Sort Key: t_1.a
+               Sort Method: quicksort  Memory: 27kB
+               ->  Seq Scan on ex_mtarget t_1 (actual rows=50 loops=1)
+         ->  Sort (actual rows=100 loops=1)
+               Sort Key: s.a
+               Sort Method: quicksort  Memory: 33kB
+               ->  Seq Scan on ex_msource s (actual rows=100 loops=1)
+(15 rows)
+
+-- only updates to selected tuples
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
+WHEN MATCHED AND t.a < 10 THEN
+	UPDATE SET b = t.b + 1;
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0 loops=1)
+   Tuples Inserted: 0
+   Tuples Updated: 5
+   Tuples Deleted: 0
+   Tuples Skipped: 45
+   ->  Merge Join (actual rows=50 loops=1)
+         Merge Cond: (t_1.a = s.a)
+         ->  Sort (actual rows=50 loops=1)
+               Sort Key: t_1.a
+               Sort Method: quicksort  Memory: 27kB
+               ->  Seq Scan on ex_mtarget t_1 (actual rows=50 loops=1)
+         ->  Sort (actual rows=100 loops=1)
+               Sort Key: s.a
+               Sort Method: quicksort  Memory: 33kB
+               ->  Seq Scan on ex_msource s (actual rows=100 loops=1)
+(15 rows)
+
+-- updates + deletes
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
+WHEN MATCHED AND t.a < 10 THEN
+	UPDATE SET b = t.b + 1
+WHEN MATCHED AND t.a >= 10 AND t.a <= 20 THEN
+	DELETE;
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0 loops=1)
+   Tuples Inserted: 0
+   Tuples Updated: 5
+   Tuples Deleted: 5
+   Tuples Skipped: 40
+   ->  Merge Join (actual rows=50 loops=1)
+         Merge Cond: (t_1.a = s.a)
+         ->  Sort (actual rows=50 loops=1)
+               Sort Key: t_1.a
+               Sort Method: quicksort  Memory: 27kB
+               ->  Seq Scan on ex_mtarget t_1 (actual rows=50 loops=1)
+         ->  Sort (actual rows=100 loops=1)
+               Sort Key: s.a
+               Sort Method: quicksort  Memory: 33kB
+               ->  Seq Scan on ex_msource s (actual rows=100 loops=1)
+(15 rows)
+
+-- only inserts
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
+WHEN NOT MATCHED AND s.a < 10 THEN
+	INSERT VALUES (a, b);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0 loops=1)
+   Tuples Inserted: 4
+   Tuples Updated: 0
+   Tuples Deleted: 0
+   Tuples Skipped: 96
+   ->  Merge Left Join (actual rows=100 loops=1)
+         Merge Cond: (s.a = t_1.a)
+         ->  Sort (actual rows=100 loops=1)
+               Sort Key: s.a
+               Sort Method: quicksort  Memory: 33kB
+               ->  Seq Scan on ex_msource s (actual rows=100 loops=1)
+         ->  Sort (actual rows=45 loops=1)
+               Sort Key: t_1.a
+               Sort Method: quicksort  Memory: 27kB
+               ->  Seq Scan on ex_mtarget t_1 (actual rows=45 loops=1)
+(15 rows)
+
+-- all three
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
+WHEN MATCHED AND t.a < 10 THEN
+	UPDATE SET b = t.b + 1
+WHEN MATCHED AND t.a >= 30 AND t.a <= 40 THEN
+	DELETE
+WHEN NOT MATCHED AND s.a < 20 THEN
+	INSERT VALUES (a, b);
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ Merge on ex_mtarget t (actual rows=0 loops=1)
+   Tuples Inserted: 10
+   Tuples Updated: 9
+   Tuples Deleted: 5
+   Tuples Skipped: 76
+   ->  Merge Left Join (actual rows=100 loops=1)
+         Merge Cond: (s.a = t_1.a)
+         ->  Sort (actual rows=100 loops=1)
+               Sort Key: s.a
+               Sort Method: quicksort  Memory: 33kB
+               ->  Seq Scan on ex_msource s (actual rows=100 loops=1)
+         ->  Sort (actual rows=49 loops=1)
+               Sort Key: t_1.a
+               Sort Method: quicksort  Memory: 27kB
+               ->  Seq Scan on ex_mtarget t_1 (actual rows=49 loops=1)
+(15 rows)
+
+DROP TABLE ex_msource, ex_mtarget;
 -- Subqueries
 BEGIN;
 MERGE INTO sq_target t
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 543ca4f272..ba2c937bca 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -1932,10 +1932,10 @@ WHEN NOT MATCHED THEN INSERT VALUES(o.k, o.v);
      ->  Result
            Output: 1, 'cte_basic val'::text
    ->  Hash Right Join
-         Output: o.k, o.v, o.*, m_1.ctid, m_1.tableoid
+         Output: o.k, o.v, o.*, m_1.ctid
          Hash Cond: (m_1.k = o.k)
          ->  Seq Scan on public.m m_1
-               Output: m_1.ctid, m_1.tableoid, m_1.k
+               Output: m_1.ctid, m_1.k
          ->  Hash
                Output: o.k, o.v, o.*
                ->  Subquery Scan on o
@@ -1981,10 +1981,10 @@ WHEN NOT MATCHED THEN INSERT VALUES(o.k, o.v);
                  Output: (cte_init.b || ' merge update'::text)
                  Filter: (cte_init.a = 1)
    ->  Hash Right Join
-         Output: o.k, o.v, o.*, m_1.ctid, m_1.tableoid
+         Output: o.k, o.v, o.*, m_1.ctid
          Hash Cond: (m_1.k = o.k)
          ->  Seq Scan on public.m m_1
-               Output: m_1.ctid, m_1.tableoid, m_1.k
+               Output: m_1.ctid, m_1.k
          ->  Hash
                Output: o.k, o.v, o.*
                ->  Subquery Scan on o
@@ -2011,8 +2011,8 @@ WITH merge_source_cte AS (SELECT 15 a, 'merge_source_cte val' b)
 MERGE INTO m USING (select * from merge_source_cte) o ON m.k=o.a
 WHEN MATCHED THEN UPDATE SET v = (SELECT b || merge_source_cte.*::text || ' merge update' FROM merge_source_cte WHERE a = 15)
 WHEN NOT MATCHED THEN INSERT VALUES(o.a, o.b || (SELECT merge_source_cte.*::text || ' merge insert' FROM merge_source_cte));
-                                                         QUERY PLAN                                                          
------------------------------------------------------------------------------------------------------------------------------
+                                                  QUERY PLAN                                                   
+---------------------------------------------------------------------------------------------------------------
  Merge on public.m
    CTE merge_source_cte
      ->  Result
@@ -2025,10 +2025,10 @@ WHEN NOT MATCHED THEN INSERT VALUES(o.a, o.b || (SELECT merge_source_cte.*::text
      ->  CTE Scan on merge_source_cte merge_source_cte_2
            Output: ((merge_source_cte_2.*)::text || ' merge insert'::text)
    ->  Hash Right Join
-         Output: merge_source_cte.a, merge_source_cte.b, ROW(merge_source_cte.a, merge_source_cte.b), m_1.ctid, m_1.tableoid
+         Output: merge_source_cte.a, merge_source_cte.b, ROW(merge_source_cte.a, merge_source_cte.b), m_1.ctid
          Hash Cond: (m_1.k = merge_source_cte.a)
          ->  Seq Scan on public.m m_1
-               Output: m_1.ctid, m_1.tableoid, m_1.k
+               Output: m_1.ctid, m_1.k
          ->  Hash
                Output: merge_source_cte.a, merge_source_cte.b
                ->  CTE Scan on merge_source_cte
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
index 8b5244fc63..cd6144bb5f 100644
--- a/src/test/regress/sql/merge.sql
+++ b/src/test/regress/sql/merge.sql
@@ -790,6 +790,50 @@ RETURNING *
 ;
 ROLLBACK;
 
+-- EXPLAIN
+CREATE TABLE ex_mtarget (a int, b int);
+CREATE TABLE ex_msource (a int, b int);
+INSERT INTO ex_mtarget SELECT i, i*10 FROM generate_series(1,100,2) i;
+INSERT INTO ex_msource SELECT i, i*10 FROM generate_series(1,100,1) i;
+
+-- only updates
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
+WHEN MATCHED THEN
+	UPDATE SET b = t.b + 1;
+
+-- only updates to selected tuples
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
+WHEN MATCHED AND t.a < 10 THEN
+	UPDATE SET b = t.b + 1;
+
+-- updates + deletes
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
+WHEN MATCHED AND t.a < 10 THEN
+	UPDATE SET b = t.b + 1
+WHEN MATCHED AND t.a >= 10 AND t.a <= 20 THEN
+	DELETE;
+
+-- only inserts
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
+WHEN NOT MATCHED AND s.a < 10 THEN
+	INSERT VALUES (a, b);
+
+-- all three
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
+WHEN MATCHED AND t.a < 10 THEN
+	UPDATE SET b = t.b + 1
+WHEN MATCHED AND t.a >= 30 AND t.a <= 40 THEN
+	DELETE
+WHEN NOT MATCHED AND s.a < 20 THEN
+	INSERT VALUES (a, b);
+
+DROP TABLE ex_msource, ex_mtarget;
+
 -- Subqueries
 BEGIN;
 MERGE INTO sq_target t
-- 
2.14.3 (Apple Git-98)

