On Wed, 1 Nov 2023 at 17:49, Jeff Davis <pg...@j-davis.com> wrote:
>
> Most of my concern is that parts of the implementation feel like a
> hack, which makes me concerned that we're approaching it the wrong way.
>

OK, that's a fair point. Attached is a new version, replacing those
parts of the implementation with a new MergingFunc node. It doesn't
add that much more complexity, and I think the new code is much
neater.

Also, I think this makes it easier / more natural to add additional
returning options, like Merlin's suggestion to return a user-defined
label value, though I haven't implemented that.

I have gone with the name originally suggested by Vik -- MERGING(),
which means that that has to be a new col-name keyword. I'm not
especially wedded to that name, but I think that it's not a bad
choice, and I think going with that is preferable to making MERGE a
col-name keyword.

So (quoting the example from the docs), the new syntax looks like this:

MERGE INTO products p
  USING stock s ON p.product_id = s.product_id
  WHEN MATCHED AND s.quantity > 0 THEN
    UPDATE SET in_stock = true, quantity = s.quantity
  WHEN MATCHED THEN
    UPDATE SET in_stock = false, quantity = 0
  WHEN NOT MATCHED THEN
    INSERT (product_id, in_stock, quantity)
      VALUES (s.product_id, true, s.quantity)
  RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), p.*;

 action | clause_number | product_id | in_stock | quantity
--------+---------------+------------+----------+----------
 UPDATE |             1 |       1001 | t        |       50
 UPDATE |             2 |       1002 | f        |        0
 INSERT |             3 |       1003 | t        |       10

By default, the returned column names are automatically taken from the
argument to the MERGING() function (which isn't actually a function
anymore).

There's one bug that I know about, to do with cross-partition updates,
but since that's a pre-existing bug, I'll start a new thread for it.

Regards,
Dean
diff --git a/doc/src/sgml/dml.sgml b/doc/src/sgml/dml.sgml
new file mode 100644
index cbbc5e2..3d95bdb
--- a/doc/src/sgml/dml.sgml
+++ b/doc/src/sgml/dml.sgml
@@ -283,10 +283,15 @@ DELETE FROM products;
    <secondary>RETURNING</secondary>
   </indexterm>
 
+  <indexterm zone="dml-returning">
+   <primary>MERGE</primary>
+   <secondary>RETURNING</secondary>
+  </indexterm>
+
   <para>
    Sometimes it is useful to obtain data from modified rows while they are
    being manipulated.  The <command>INSERT</command>, <command>UPDATE</command>,
-   and <command>DELETE</command> commands all have an
+   <command>DELETE</command>, and <command>MERGE</command> commands all have an
    optional <literal>RETURNING</literal> clause that supports this.  Use
    of <literal>RETURNING</literal> avoids performing an extra database query to
    collect the data, and is especially valuable when it would otherwise be
@@ -339,6 +344,21 @@ DELETE FROM products
 </programlisting>
   </para>
 
+  <para>
+   In a <command>MERGE</command>, the data available to <literal>RETURNING</literal> is
+   the content of the source row plus the content of the inserted, updated, or
+   deleted target row.  Since it is quite common for the source and target to
+   have many of the same columns, specifying <literal>RETURNING *</literal>
+   can lead to a lot of duplicated columns, so it is often more useful to
+   qualify it so as to return just the source or target row.  For example:
+<programlisting>
+MERGE INTO products p USING new_products n ON p.product_no = n.product_no
+  WHEN NOT MATCHED THEN INSERT VALUES (n.product_no, n.name, n.price)
+  WHEN MATCHED THEN UPDATE SET name = n.name, price = n.price
+  RETURNING p.*;
+</programlisting>
+  </para>
+
   <para>
    If there are triggers (<xref linkend="triggers"/>) on the target table,
    the data available to <literal>RETURNING</literal> is the row as modified by
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
new file mode 100644
index a6fcac0..06d5fe8
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21960,6 +21960,84 @@ SELECT count(*) FROM sometable;
 
  </sect1>
 
+ <sect1 id="functions-merge-support">
+  <title>Merge Support Functions</title>
+
+  <indexterm>
+   <primary>MERGE</primary>
+   <secondary>RETURNING</secondary>
+  </indexterm>
+
+  <indexterm>
+   <primary>MERGING</primary>
+  </indexterm>
+
+  <para>
+   <productname>PostgreSQL</productname> includes one merge support function
+   that may be used in the <literal>RETURNING</literal> list of a
+   <xref linkend="sql-merge"/> command to return additional information about
+   the action taken for each row:
+<synopsis>
+<function id="function-merging">MERGING</function> ( <replaceable>property</replaceable> )
+</synopsis>
+   The following are valid property values specifying what to return:
+
+   <variablelist>
+    <varlistentry>
+     <term><literal>ACTION</literal></term>
+     <listitem>
+      <para>
+       The merge action command executed for the current row
+       (<literal>'INSERT'</literal>, <literal>'UPDATE'</literal>, or
+       <literal>'DELETE'</literal>).
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>CLAUSE_NUMBER</literal></term>
+     <listitem>
+      <para>
+       The 1-based index of the <literal>WHEN</literal> clause executed for
+       the current row.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+  </para>
+
+  <para>
+   The following example illustrates how this may be used to determine
+   which actions were performed for each row affected by the
+   <command>MERGE</command> command:
+<screen><![CDATA[
+MERGE INTO products p
+  USING stock s ON p.product_id = s.product_id
+  WHEN MATCHED AND s.quantity > 0 THEN
+    UPDATE SET in_stock = true, quantity = s.quantity
+  WHEN MATCHED THEN
+    UPDATE SET in_stock = false, quantity = 0
+  WHEN NOT MATCHED THEN
+    INSERT (product_id, in_stock, quantity)
+      VALUES (s.product_id, true, s.quantity)
+  RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), p.*;
+
+ action | clause_number | product_id | in_stock | quantity 
+--------+---------------+------------+----------+----------
+ UPDATE |             1 |       1001 | t        |       50
+ UPDATE |             2 |       1002 | f        |        0
+ INSERT |             3 |       1003 | t        |       10
+]]></screen>
+  </para>
+
+  <para>
+   Note that this function can only be used in the <literal>RETURNING</literal>
+   list of a <command>MERGE</command> command.  It is an error to use it in any
+   other part of a query.
+  </para>
+
+ </sect1>
+
  <sect1 id="functions-subquery">
   <title>Subquery Expressions</title>
 
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
new file mode 100644
index fe8def4..51a15ca
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -1387,9 +1387,9 @@
      to a <glossterm linkend="glossary-client">client</glossterm> upon the
      completion of an <acronym>SQL</acronym> command, usually a
      <command>SELECT</command> but it can be an
-     <command>INSERT</command>, <command>UPDATE</command>, or
-     <command>DELETE</command> command if the <literal>RETURNING</literal>
-     clause is specified.
+     <command>INSERT</command>, <command>UPDATE</command>,
+     <command>DELETE</command>, or <command>MERGE</command> command if the
+     <literal>RETURNING</literal> clause is specified.
     </para>
     <para>
      The fact that a result set is a relation means that a query can be used
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
new file mode 100644
index 5977534..4ad9894
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1020,8 +1020,8 @@ INSERT INTO mytable VALUES (1,'one'), (2
     </para>
 
     <para>
-     If the command does return rows (for example <command>SELECT</command>,
-     or <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>
+     If the command does return rows (for example <command>SELECT</command>, or
+     <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
      with <literal>RETURNING</literal>), there are two ways to proceed.
      When the command will return at most one row, or you only care about
      the first row of output, write the command as usual but add
@@ -1149,6 +1149,7 @@ SELECT <replaceable>select_expressions</
 INSERT ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
 UPDATE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
 DELETE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
+MERGE ... RETURNING <replaceable>expressions</replaceable> INTO <optional>STRICT</optional> <replaceable>target</replaceable>;
 </synopsis>
 
      where <replaceable>target</replaceable> can be a record variable, a row
@@ -1159,8 +1160,8 @@ DELETE ... RETURNING <replaceable>expres
      <literal>INTO</literal> clause) just as described above,
      and the plan is cached in the same way.
      This works for <command>SELECT</command>,
-     <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
-     <literal>RETURNING</literal>, and certain utility commands
+     <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>
+     with <literal>RETURNING</literal>, and certain utility commands
      that return row sets, such as <command>EXPLAIN</command>.
      Except for the <literal>INTO</literal> clause, the SQL command is the same
      as it would be written outside <application>PL/pgSQL</application>.
@@ -1236,7 +1237,7 @@ END;
     </para>
 
     <para>
-     For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command> with
+     For <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command> with
      <literal>RETURNING</literal>, <application>PL/pgSQL</application> reports
      an error for more than one returned row, even when
      <literal>STRICT</literal> is not specified.  This is because there
diff --git a/doc/src/sgml/queries.sgml b/doc/src/sgml/queries.sgml
new file mode 100644
index 8a4674e..ae1f738
--- a/doc/src/sgml/queries.sgml
+++ b/doc/src/sgml/queries.sgml
@@ -2062,9 +2062,10 @@ SELECT <replaceable>select_list</replace
    Table Expressions or <acronym>CTE</acronym>s, can be thought of as defining
    temporary tables that exist just for one query.  Each auxiliary statement
    in a <literal>WITH</literal> clause can be a <command>SELECT</command>,
-   <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>; and the
+   <command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
+   or <command>MERGE</command>; and the
    <literal>WITH</literal> clause itself is attached to a primary statement that can
-   be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
+   also be a <command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>,
    <command>DELETE</command>, or <command>MERGE</command>.
   </para>
 
@@ -2598,8 +2599,8 @@ SELECT * FROM w AS w1 JOIN w AS w2 ON w1
    <title>Data-Modifying Statements in <literal>WITH</literal></title>
 
    <para>
-    You can use most data-modifying statements (<command>INSERT</command>,
-    <command>UPDATE</command>, or <command>DELETE</command>, but not
+    You can use data-modifying statements (<command>INSERT</command>,
+    <command>UPDATE</command>, <command>DELETE</command>, or
     <command>MERGE</command>) in <literal>WITH</literal>.  This
     allows you to perform several different operations in the same query.
     An example is:
diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
new file mode 100644
index d12ba96..9b9536b
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -122,13 +122,16 @@ COPY { <replaceable class="parameter">ta
       A <link linkend="sql-select"><command>SELECT</command></link>,
       <link linkend="sql-values"><command>VALUES</command></link>,
       <link linkend="sql-insert"><command>INSERT</command></link>,
-      <link linkend="sql-update"><command>UPDATE</command></link>, or
-      <link linkend="sql-delete"><command>DELETE</command></link> command whose results are to be
-      copied.  Note that parentheses are required around the query.
+      <link linkend="sql-update"><command>UPDATE</command></link>,
+      <link linkend="sql-delete"><command>DELETE</command></link>, or
+      <link linkend="sql-merge"><command>MERGE</command></link> command
+      whose results are to be copied.  Note that parentheses are required
+      around the query.
      </para>
      <para>
-      For <command>INSERT</command>, <command>UPDATE</command> and
-      <command>DELETE</command> queries a RETURNING clause must be provided,
+      For <command>INSERT</command>, <command>UPDATE</command>,
+      <command>DELETE</command>, and <command>MERGE</command> queries a
+      RETURNING clause must be provided,
       and the target relation must not have a conditional rule, nor
       an <literal>ALSO</literal> rule, nor an <literal>INSTEAD</literal> rule
       that expands to multiple statements.
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0f61d47..1ef29ab
--- a/doc/src/sgml/ref/merge.sgml
+++ b/doc/src/sgml/ref/merge.sgml
@@ -25,6 +25,7 @@ PostgreSQL documentation
 MERGE INTO [ ONLY ] <replaceable class="parameter">target_table_name</replaceable> [ * ] [ [ AS ] <replaceable class="parameter">target_alias</replaceable> ]
 USING <replaceable class="parameter">data_source</replaceable> ON <replaceable class="parameter">join_condition</replaceable>
 <replaceable class="parameter">when_clause</replaceable> [...]
+[ RETURNING * | <replaceable class="parameter">output_expression</replaceable> [ [ AS ] <replaceable class="parameter">output_name</replaceable> ] [, ...] ]
 
 <phrase>where <replaceable class="parameter">data_source</replaceable> is:</phrase>
 
@@ -95,6 +96,18 @@ DELETE
   </para>
 
   <para>
+   The optional <literal>RETURNING</literal> clause causes <command>MERGE</command>
+   to compute and return value(s) based on each row inserted, updated, or
+   deleted.  Any expression using the source or target table's columns, or
+   the <xref linkend="function-merging"/> function can be computed.  When an
+   <command>INSERT</command> or <command>UPDATE</command> action is performed,
+   the new values of the target table's columns are used.  When a
+   <command>DELETE</command> is performed, the old values of the target table's
+   columns are used.  The syntax of the <literal>RETURNING</literal> list is
+   identical to that of the output list of <command>SELECT</command>.
+  </para>
+
+  <para>
    There is no separate <literal>MERGE</literal> privilege.
    If you specify an update action, you must have the
    <literal>UPDATE</literal> privilege on the column(s)
@@ -125,6 +138,18 @@ DELETE
 
   <variablelist>
    <varlistentry>
+    <term><replaceable class="parameter">with_query</replaceable></term>
+    <listitem>
+     <para>
+      The <literal>WITH</literal> clause allows you to specify one or more
+      subqueries that can be referenced by name in the <command>MERGE</command>
+      query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
+      for details.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
     <term><replaceable class="parameter">target_table_name</replaceable></term>
     <listitem>
      <para>
@@ -399,13 +424,30 @@ DELETE
    </varlistentry>
 
    <varlistentry>
-    <term><replaceable class="parameter">with_query</replaceable></term>
+    <term><replaceable class="parameter">output_expression</replaceable></term>
     <listitem>
      <para>
-      The <literal>WITH</literal> clause allows you to specify one or more
-      subqueries that can be referenced by name in the <command>MERGE</command>
-      query. See <xref linkend="queries-with"/> and <xref linkend="sql-select"/>
-      for details.
+      An expression to be computed and returned by the <command>MERGE</command>
+      command after each row is merged.  The expression can use any columns of
+      the source or target tables, or the <xref linkend="function-merging"/>
+      function to return additional information about the action executed.
+     </para>
+     <para>
+      Writing <literal>*</literal> will return all columns from the source
+      table, followed by all columns from the target table.  Often this will
+      lead to a lot of duplication, since it is common for the source and
+      target tables to have a lot of the same columns.  This can be avoided by
+      qualifying the <literal>*</literal> with the name or alias of the source
+      or target table.
+     </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term><replaceable class="parameter">output_name</replaceable></term>
+    <listitem>
+     <para>
+      A name to use for a returned column.
      </para>
     </listitem>
    </varlistentry>
@@ -428,6 +470,13 @@ MERGE <replaceable class="parameter">tot
    were changed in any way.
   </para>
 
+  <para>
+   If the <command>MERGE</command> command contains a <literal>RETURNING</literal>
+   clause, the result will be similar to that of a <command>SELECT</command>
+   statement containing the columns and values defined in the
+   <literal>RETURNING</literal> list, computed over the row(s) inserted, updated,
+   or deleted by the command.
+  </para>
  </refsect1>
 
  <refsect1>
@@ -544,13 +593,6 @@ MERGE <replaceable class="parameter">tot
   </para>
 
   <para>
-   There is no <literal>RETURNING</literal> clause with
-   <command>MERGE</command>.  Actions of <command>INSERT</command>,
-   <command>UPDATE</command> and <command>DELETE</command> cannot contain
-   <literal>RETURNING</literal> or <literal>WITH</literal> clauses.
-  </para>
-
-  <para>
    When <command>MERGE</command> is run concurrently with other commands
    that modify the target table, the usual transaction isolation rules
    apply; see <xref linkend="transaction-iso"/> for an explanation
@@ -602,7 +644,8 @@ WHEN NOT MATCHED THEN
   <para>
    Attempt to insert a new stock item along with the quantity of stock. If
    the item already exists, instead update the stock count of the existing
-   item. Don't allow entries that have zero stock.
+   item. Don't allow entries that have zero stock. Return details of all
+   changes made.
 <programlisting>
 MERGE INTO wines w
 USING wine_stock_changes s
@@ -612,7 +655,8 @@ WHEN NOT MATCHED AND s.stock_delta > 0 T
 WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
   UPDATE SET stock = w.stock + s.stock_delta
 WHEN MATCHED THEN
-  DELETE;
+  DELETE
+RETURNING MERGING(ACTION), w.*;
 </programlisting>
 
    The <literal>wine_stock_changes</literal> table might be, for example, a
@@ -627,7 +671,8 @@ WHEN MATCHED THEN
     This command conforms to the <acronym>SQL</acronym> standard.
   </para>
    <para>
-    The WITH clause and <literal>DO NOTHING</literal> action are extensions to
+    The WITH clause, <literal>DO NOTHING</literal> action, and
+    <literal>RETURNING</literal> clause are extensions to
     the <acronym>SQL</acronym> standard.
   </para>
  </refsect1>
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
new file mode 100644
index 42d7891..8513ca6
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -74,7 +74,7 @@ SELECT [ ALL | DISTINCT [ ON ( <replacea
 
 <phrase>and <replaceable class="parameter">with_query</replaceable> is:</phrase>
 
-    <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> )
+    <replaceable class="parameter">with_query_name</replaceable> [ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) ] AS [ [ NOT ] MATERIALIZED ] ( <replaceable class="parameter">select</replaceable> | <replaceable class="parameter">values</replaceable> | <replaceable class="parameter">insert</replaceable> | <replaceable class="parameter">update</replaceable> | <replaceable class="parameter">delete</replaceable> | <replaceable class="parameter">merge</replaceable> )
         [ SEARCH { BREADTH | DEPTH } FIRST BY <replaceable>column_name</replaceable> [, ...] SET <replaceable>search_seq_col_name</replaceable> ]
         [ CYCLE <replaceable>column_name</replaceable> [, ...] SET <replaceable>cycle_mark_col_name</replaceable> [ TO <replaceable>cycle_mark_value</replaceable> DEFAULT <replaceable>cycle_mark_default</replaceable> ] USING <replaceable>cycle_path_col_name</replaceable> ]
 
@@ -230,10 +230,10 @@ TABLE [ ONLY ] <replaceable class="param
     The subqueries effectively act as temporary tables or views
     for the duration of the primary query.
     Each subquery can be a <command>SELECT</command>, <command>TABLE</command>, <command>VALUES</command>,
-    <command>INSERT</command>, <command>UPDATE</command> or
-    <command>DELETE</command> statement.
+    <command>INSERT</command>, <command>UPDATE</command>,
+    <command>DELETE</command>, or <command>MERGE</command> statement.
     When writing a data-modifying statement (<command>INSERT</command>,
-    <command>UPDATE</command> or <command>DELETE</command>) in
+    <command>UPDATE</command>, <command>DELETE</command>, or <command>MERGE</command>) in
     <literal>WITH</literal>, it is usual to include a <literal>RETURNING</literal> clause.
     It is the output of <literal>RETURNING</literal>, <emphasis>not</emphasis> the underlying
     table that the statement modifies, that forms the temporary table that is
@@ -2182,7 +2182,8 @@ SELECT 2+2;
 
    <para>
     <productname>PostgreSQL</productname> allows <command>INSERT</command>,
-    <command>UPDATE</command>, and <command>DELETE</command> to be used as <literal>WITH</literal>
+    <command>UPDATE</command>, <command>DELETE</command>, and
+    <command>MERGE</command> to be used as <literal>WITH</literal>
     queries.  This is not found in the SQL standard.
    </para>
   </refsect2>
diff --git a/doc/src/sgml/rowtypes.sgml b/doc/src/sgml/rowtypes.sgml
new file mode 100644
index 4d86f97..bbeac84
--- a/doc/src/sgml/rowtypes.sgml
+++ b/doc/src/sgml/rowtypes.sgml
@@ -348,7 +348,7 @@ SELECT m.* FROM some_table, LATERAL myfu
    column expansion of this kind when it appears at the top level of
    a <link linkend="queries-select-lists"><command>SELECT</command> output
    list</link>, a <link linkend="dml-returning"><literal>RETURNING</literal>
-   list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>,
+   list</link> in <command>INSERT</command>/<command>UPDATE</command>/<command>DELETE</command>/<command>MERGE</command>,
    a <link linkend="queries-values"><literal>VALUES</literal> clause</link>, or
    a <link linkend="sql-syntax-row-constructors">row constructor</link>.
    In all other contexts (including when nested inside one of those
diff --git a/doc/src/sgml/spi.sgml b/doc/src/sgml/spi.sgml
new file mode 100644
index 47f4b5b..bb37786
--- a/doc/src/sgml/spi.sgml
+++ b/doc/src/sgml/spi.sgml
@@ -301,8 +301,9 @@ SPI_execute("INSERT INTO foo SELECT * FR
    is returned in the global variable <varname>SPI_processed</varname>.
    If the return value of the function is <symbol>SPI_OK_SELECT</symbol>,
    <symbol>SPI_OK_INSERT_RETURNING</symbol>,
-   <symbol>SPI_OK_DELETE_RETURNING</symbol>, or
-   <symbol>SPI_OK_UPDATE_RETURNING</symbol>,
+   <symbol>SPI_OK_DELETE_RETURNING</symbol>,
+   <symbol>SPI_OK_UPDATE_RETURNING</symbol>, or
+   <symbol>SPI_OK_MERGE_RETURNING</symbol>,
    then you can use the
    global pointer <literal>SPITupleTable *SPI_tuptable</literal> to
    access the result rows.  Some utility commands (such as
@@ -472,6 +473,15 @@ typedef struct SPITupleTable
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><symbol>SPI_OK_MERGE_RETURNING</symbol></term>
+     <listitem>
+      <para>
+       if a <command>MERGE RETURNING</command> was executed
+      </para>
+     </listitem>
+    </varlistentry>
 
     <varlistentry>
      <term><symbol>SPI_OK_UTILITY</symbol></term>
diff --git a/doc/src/sgml/xfunc.sgml b/doc/src/sgml/xfunc.sgml
new file mode 100644
index 89116ae..cb0aa55
--- a/doc/src/sgml/xfunc.sgml
+++ b/doc/src/sgml/xfunc.sgml
@@ -177,7 +177,8 @@
     statements separated by semicolons.  A semicolon after the last
     statement is optional.  Unless the function is declared to return
     <type>void</type>, the last statement must be a <command>SELECT</command>,
-    or an <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command>
+    or an <command>INSERT</command>, <command>UPDATE</command>,
+    <command>DELETE</command>, or <command>MERGE</command>
     that has a <literal>RETURNING</literal> clause.
    </para>
 
@@ -1236,8 +1237,9 @@ SELECT x, CASE WHEN x &gt; 0 THEN genera
 
     <note>
      <para>
-      If a function's last command is <command>INSERT</command>, <command>UPDATE</command>,
-      or <command>DELETE</command> with <literal>RETURNING</literal>, that command will
+      If a function's last command is <command>INSERT</command>,
+      <command>UPDATE</command>, <command>DELETE</command>, or
+      <command>MERGE</command> with <literal>RETURNING</literal>, that command will
       always be executed to completion, even if the function is not declared
       with <literal>SETOF</literal> or the calling query does not fetch all the
       result rows.  Any extra rows produced by the <literal>RETURNING</literal>
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
new file mode 100644
index c5d7d78..7977873
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -283,12 +283,6 @@ DoCopy(ParseState *pstate, const CopyStm
 	{
 		Assert(stmt->query);
 
-		/* MERGE is allowed by parser, but unimplemented. Reject for now */
-		if (IsA(stmt->query, MergeStmt))
-			ereport(ERROR,
-					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					errmsg("MERGE not supported in COPY"));
-
 		query = makeNode(RawStmt);
 		query->stmt = stmt->query;
 		query->stmt_location = stmt_location;
diff --git a/src/backend/commands/copyto.c b/src/backend/commands/copyto.c
new file mode 100644
index c66a047..1145aaf
--- a/src/backend/commands/copyto.c
+++ b/src/backend/commands/copyto.c
@@ -510,7 +510,8 @@ BeginCopyTo(ParseState *pstate,
 		{
 			Assert(query->commandType == CMD_INSERT ||
 				   query->commandType == CMD_UPDATE ||
-				   query->commandType == CMD_DELETE);
+				   query->commandType == CMD_DELETE ||
+				   query->commandType == CMD_MERGE);
 
 			ereport(ERROR,
 					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
new file mode 100644
index 2c62b0c..6414c0d
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1111,6 +1111,21 @@ ExecInitExprRec(Expr *node, ExprState *s
 				break;
 			}
 
+		case T_MergingFunc:
+			{
+				MergingFunc *mf_node = (MergingFunc *) node;
+
+				if (!state->parent ||
+					!IsA(state->parent, ModifyTableState) ||
+					((ModifyTableState *) state->parent)->operation != CMD_MERGE)
+					elog(ERROR, "MergingFunc found in non-merge plan node");
+
+				scratch.opcode = EEOP_MERGING_FUNC;
+				scratch.d.merging_func.mfop = mf_node->mfop;
+				ExprEvalPushStep(state, &scratch);
+				break;
+			}
+
 		case T_SubscriptingRef:
 			{
 				SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
new file mode 100644
index 24c2b60..b176ab6
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -483,6 +483,7 @@ ExecInterpExpr(ExprState *state, ExprCon
 		&&CASE_EEOP_AGGREF,
 		&&CASE_EEOP_GROUPING_FUNC,
 		&&CASE_EEOP_WINDOW_FUNC,
+		&&CASE_EEOP_MERGING_FUNC,
 		&&CASE_EEOP_SUBPLAN,
 		&&CASE_EEOP_AGG_STRICT_DESERIALIZE,
 		&&CASE_EEOP_AGG_DESERIALIZE,
@@ -1582,6 +1583,14 @@ ExecInterpExpr(ExprState *state, ExprCon
 			EEO_NEXT();
 		}
 
+		EEO_CASE(EEOP_MERGING_FUNC)
+		{
+			/* too complex/uncommon for an inline implementation */
+			ExecEvalMergingFunc(state, op, econtext);
+
+			EEO_NEXT();
+		}
+
 		EEO_CASE(EEOP_SUBPLAN)
 		{
 			/* too complex for an inline implementation */
@@ -4171,6 +4180,59 @@ ExecEvalGroupingFunc(ExprState *state, E
 }
 
 /*
+ * ExecEvalMergingFunc
+ *
+ * Returns information about the current MERGE action for its RETURNING list.
+ */
+void
+ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+	MergingFuncOp mfop = op->d.merging_func.mfop;
+	ModifyTableState *mtstate = castNode(ModifyTableState, state->parent);
+	MergeActionState *relaction = mtstate->mt_merge_action;
+
+	if (!relaction)
+		elog(ERROR, "no merge action in progress");
+
+	switch (mfop)
+	{
+		case MERGING_ACTION:
+			/* Return the MERGE action ("INSERT", "UPDATE", or "DELETE") */
+			switch (relaction->mas_action->commandType)
+			{
+				case CMD_INSERT:
+					*op->resvalue = PointerGetDatum(cstring_to_text_with_len("INSERT", 6));
+					*op->resnull = false;
+					break;
+				case CMD_UPDATE:
+					*op->resvalue = PointerGetDatum(cstring_to_text_with_len("UPDATE", 6));
+					*op->resnull = false;
+					break;
+				case CMD_DELETE:
+					*op->resvalue = PointerGetDatum(cstring_to_text_with_len("DELETE", 6));
+					*op->resnull = false;
+					break;
+				case CMD_NOTHING:
+					elog(ERROR, "unexpected merge action: DO NOTHING");
+					break;
+				default:
+					elog(ERROR, "unrecognized commandType: %d",
+						 (int) relaction->mas_action->commandType);
+			}
+			break;
+
+		case MERGING_CLAUSE_NUMBER:
+			/* Return the 1-based index of the MERGE action */
+			*op->resvalue = Int32GetDatum(relaction->mas_action->index);
+			*op->resnull = false;
+			break;
+
+		default:
+			elog(ERROR, "unrecognized MergingFuncOp: %d", (int) mfop);
+	}
+}
+
+/*
  * Hand off evaluation of a subplan to nodeSubplan.c
  */
 void
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
new file mode 100644
index f6c3432..34d8231
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -612,8 +612,8 @@ ExecInitPartitionInfo(ModifyTableState *
 	 * Build the RETURNING projection for the partition.  Note that we didn't
 	 * build the returningList for partitions within the planner, but simple
 	 * translation of varattnos will suffice.  This only occurs for the INSERT
-	 * case or in the case of UPDATE tuple routing where we didn't find a
-	 * result rel to reuse.
+	 * case or in the case of UPDATE/MERGE tuple routing where we didn't find
+	 * a result rel to reuse.
 	 */
 	if (node && node->returningLists != NIL)
 	{
@@ -622,12 +622,14 @@ ExecInitPartitionInfo(ModifyTableState *
 		List	   *returningList;
 
 		/* See the comment above for WCO lists. */
-		/* (except no RETURNING support for MERGE yet) */
 		Assert((node->operation == CMD_INSERT &&
 				list_length(node->returningLists) == 1 &&
 				list_length(node->resultRelations) == 1) ||
 			   (node->operation == CMD_UPDATE &&
 				list_length(node->returningLists) ==
+				list_length(node->resultRelations)) ||
+			   (node->operation == CMD_MERGE &&
+				list_length(node->returningLists) ==
 				list_length(node->resultRelations)));
 
 		/*
diff --git a/src/backend/executor/functions.c b/src/backend/executor/functions.c
new file mode 100644
index bace252..68cb4fa
--- a/src/backend/executor/functions.c
+++ b/src/backend/executor/functions.c
@@ -1660,8 +1660,8 @@ check_sql_fn_retval(List *queryTreeLists
 
 	/*
 	 * If it's a plain SELECT, it returns whatever the targetlist says.
-	 * Otherwise, if it's INSERT/UPDATE/DELETE with RETURNING, it returns
-	 * that. Otherwise, the function return type must be VOID.
+	 * Otherwise, if it's INSERT/UPDATE/DELETE/MERGE with RETURNING, it
+	 * returns that. Otherwise, the function return type must be VOID.
 	 *
 	 * Note: eventually replace this test with QueryReturnsTuples?	We'd need
 	 * a more general method of determining the output type, though.  Also, it
@@ -1679,7 +1679,8 @@ check_sql_fn_retval(List *queryTreeLists
 	else if (parse &&
 			 (parse->commandType == CMD_INSERT ||
 			  parse->commandType == CMD_UPDATE ||
-			  parse->commandType == CMD_DELETE) &&
+			  parse->commandType == CMD_DELETE ||
+			  parse->commandType == CMD_MERGE) &&
 			 parse->returningList)
 	{
 		tlist = parse->returningList;
@@ -1693,7 +1694,7 @@ check_sql_fn_retval(List *queryTreeLists
 				(errcode(ERRCODE_INVALID_FUNCTION_DEFINITION),
 				 errmsg("return type mismatch in function declared to return %s",
 						format_type_be(rettype)),
-				 errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE RETURNING.")));
+				 errdetail("Function's final statement must be SELECT or INSERT/UPDATE/DELETE/MERGE RETURNING.")));
 		return false;			/* keep compiler quiet */
 	}
 
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index 299c2c7..7745d86
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -36,8 +36,7 @@
  *		RETURNING tuple after completing each row insert, update, or delete.
  *		It must be called again to continue the operation.  Without RETURNING,
  *		we just loop within the node until all the work is done, then
- *		return NULL.  This avoids useless call/return overhead.  (MERGE does
- *		not support RETURNING.)
+ *		return NULL.  This avoids useless call/return overhead.
  */
 
 #include "postgres.h"
@@ -88,9 +87,6 @@ typedef struct ModifyTableContext
 	 */
 	TupleTableSlot *planSlot;
 
-	/* MERGE specific */
-	MergeActionState *relaction;	/* MERGE action in progress */
-
 	/*
 	 * Information about the changes that were made concurrently to a tuple
 	 * being updated or deleted
@@ -153,13 +149,14 @@ static TupleTableSlot *ExecMerge(ModifyT
 								 ItemPointer tupleid,
 								 bool canSetTag);
 static void ExecInitMerge(ModifyTableState *mtstate, EState *estate);
-static bool ExecMergeMatched(ModifyTableContext *context,
-							 ResultRelInfo *resultRelInfo,
-							 ItemPointer tupleid,
-							 bool canSetTag);
-static void ExecMergeNotMatched(ModifyTableContext *context,
-								ResultRelInfo *resultRelInfo,
-								bool canSetTag);
+static TupleTableSlot *ExecMergeMatched(ModifyTableContext *context,
+										ResultRelInfo *resultRelInfo,
+										ItemPointer tupleid,
+										bool canSetTag,
+										bool *matched);
+static TupleTableSlot *ExecMergeNotMatched(ModifyTableContext *context,
+										   ResultRelInfo *resultRelInfo,
+										   bool canSetTag);
 
 
 /*
@@ -979,7 +976,7 @@ ExecInsert(ModifyTableContext *context,
 		if (mtstate->operation == CMD_UPDATE)
 			wco_kind = WCO_RLS_UPDATE_CHECK;
 		else if (mtstate->operation == CMD_MERGE)
-			wco_kind = (context->relaction->mas_action->commandType == CMD_UPDATE) ?
+			wco_kind = (mtstate->mt_merge_action->mas_action->commandType == CMD_UPDATE) ?
 				WCO_RLS_UPDATE_CHECK : WCO_RLS_INSERT_CHECK;
 		else
 			wco_kind = WCO_RLS_INSERT_CHECK;
@@ -1828,7 +1825,7 @@ ExecCrossPartitionUpdate(ModifyTableCont
 		 * additional rechecking, and might end up executing a different
 		 * action entirely).
 		 */
-		if (context->relaction != NULL)
+		if (mtstate->mt_merge_action != NULL)
 			return false;
 		else if (TupIsNull(epqslot))
 			return true;
@@ -2069,7 +2066,7 @@ lreplace:
 		 * No luck, a retry is needed.  If running MERGE, we do not do so
 		 * here; instead let it handle that on its own rules.
 		 */
-		if (context->relaction != NULL)
+		if (context->mtstate->mt_merge_action != NULL)
 			return TM_Updated;
 
 		/*
@@ -2712,6 +2709,7 @@ static TupleTableSlot *
 ExecMerge(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 		  ItemPointer tupleid, bool canSetTag)
 {
+	TupleTableSlot *rslot = NULL;
 	bool		matched;
 
 	/*-----
@@ -2759,18 +2757,18 @@ ExecMerge(ModifyTableContext *context, R
 	 */
 	matched = tupleid != NULL;
 	if (matched)
-		matched = ExecMergeMatched(context, resultRelInfo, tupleid, canSetTag);
+		rslot = ExecMergeMatched(context, resultRelInfo, tupleid, canSetTag,
+								 &matched);
 
 	/*
-	 * Either we were dealing with a NOT MATCHED tuple or ExecMergeMatched()
-	 * returned "false", indicating the previously MATCHED tuple no longer
-	 * matches.
+	 * Deal with the NOT MATCHED case (either a NOT MATCHED tuple from the
+	 * join, or a previously MATCHED tuple for which ExecMergeMatched() set
+	 * "matched" to false, indicating that it no longer matches).
 	 */
 	if (!matched)
-		ExecMergeNotMatched(context, resultRelInfo, canSetTag);
+		rslot = ExecMergeNotMatched(context, resultRelInfo, canSetTag);
 
-	/* No RETURNING support yet */
-	return NULL;
+	return rslot;
 }
 
 /*
@@ -2780,8 +2778,8 @@ ExecMerge(ModifyTableContext *context, R
  * We start from the first WHEN MATCHED action and check if the WHEN quals
  * pass, if any. If the WHEN quals for the first action do not pass, we
  * check the second, then the third and so on. If we reach to the end, no
- * action is taken and we return true, indicating that no further action is
- * required for this tuple.
+ * action is taken and "matched" is set to true, indicating that no further
+ * action is required for this tuple.
  *
  * If we do find a qualifying action, then we attempt to execute the action.
  *
@@ -2790,16 +2788,17 @@ ExecMerge(ModifyTableContext *context, R
  * with individual actions are evaluated by this routine via ExecQual, while
  * EvalPlanQual checks for the join quals. If EvalPlanQual tells us that the
  * updated tuple still passes the join quals, then we restart from the first
- * action to look for a qualifying action. Otherwise, we return false --
- * meaning that a NOT MATCHED action must now be executed for the current
- * source tuple.
+ * action to look for a qualifying action. Otherwise, "matched" is set to
+ * false -- meaning that a NOT MATCHED action must now be executed for the
+ * current source tuple.
  */
-static bool
+static TupleTableSlot *
 ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
-				 ItemPointer tupleid, bool canSetTag)
+				 ItemPointer tupleid, bool canSetTag, bool *matched)
 {
 	ModifyTableState *mtstate = context->mtstate;
-	TupleTableSlot *newslot;
+	TupleTableSlot *rslot = NULL;
+	TupleTableSlot *newslot = NULL;
 	EState	   *estate = context->estate;
 	ExprContext *econtext = mtstate->ps.ps_ExprContext;
 	bool		isNull;
@@ -2810,7 +2809,10 @@ ExecMergeMatched(ModifyTableContext *con
 	 * If there are no WHEN MATCHED actions, we are done.
 	 */
 	if (resultRelInfo->ri_matchedMergeAction == NIL)
-		return true;
+	{
+		*matched = true;
+		return NULL;
+	}
 
 	/*
 	 * Make tuple and any needed join variables available to ExecQual and
@@ -2889,12 +2891,15 @@ lmerge_matched:
 				 */
 				newslot = ExecProject(relaction->mas_proj);
 
-				context->relaction = relaction;
+				mtstate->mt_merge_action = relaction;
 				if (!ExecUpdatePrologue(context, resultRelInfo,
 										tupleid, NULL, newslot, &result))
 				{
 					if (result == TM_Ok)
-						return true;	/* "do nothing" */
+					{
+						*matched = true;
+						return NULL;	/* "do nothing" */
+					}
 					break;		/* concurrent update/delete */
 				}
 				result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2908,12 +2913,15 @@ lmerge_matched:
 				break;
 
 			case CMD_DELETE:
-				context->relaction = relaction;
+				mtstate->mt_merge_action = relaction;
 				if (!ExecDeletePrologue(context, resultRelInfo, tupleid,
 										NULL, NULL, &result))
 				{
 					if (result == TM_Ok)
-						return true;	/* "do nothing" */
+					{
+						*matched = true;
+						return NULL;	/* "do nothing" */
+					}
 					break;		/* concurrent update/delete */
 				}
 				result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -2969,7 +2977,8 @@ lmerge_matched:
 				 * If the tuple was already deleted, return to let caller
 				 * handle it under NOT MATCHED clauses.
 				 */
-				return false;
+				*matched = false;
+				return NULL;
 
 			case TM_Updated:
 				{
@@ -3015,13 +3024,19 @@ lmerge_matched:
 							 * NOT MATCHED actions.
 							 */
 							if (TupIsNull(epqslot))
-								return false;
+							{
+								*matched = false;
+								return NULL;
+							}
 
 							(void) ExecGetJunkAttribute(epqslot,
 														resultRelInfo->ri_RowIdAttNo,
 														&isNull);
 							if (isNull)
-								return false;
+							{
+								*matched = false;
+								return NULL;
+							}
 
 							/*
 							 * When a tuple was updated and migrated to
@@ -3056,7 +3071,8 @@ lmerge_matched:
 							 * tuple already deleted; tell caller to run NOT
 							 * MATCHED actions
 							 */
-							return false;
+							*matched = false;
+							return NULL;
 
 						case TM_SelfModified:
 
@@ -3076,13 +3092,14 @@ lmerge_matched:
 										(errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION),
 										 errmsg("tuple to be updated or deleted was already modified by an operation triggered by the current command"),
 										 errhint("Consider using an AFTER trigger instead of a BEFORE trigger to propagate changes to other rows.")));
-							return false;
+							*matched = false;
+							return NULL;
 
 						default:
 							/* see table_tuple_lock call in ExecDelete() */
 							elog(ERROR, "unexpected table_tuple_lock status: %u",
 								 result);
-							return false;
+							return NULL;
 					}
 				}
 
@@ -3094,6 +3111,31 @@ lmerge_matched:
 				break;
 		}
 
+		/* Process RETURNING if present */
+		if (resultRelInfo->ri_projectReturning)
+		{
+			switch (commandType)
+			{
+				case CMD_UPDATE:
+					rslot = ExecProcessReturning(resultRelInfo, newslot,
+												 context->planSlot);
+					break;
+
+				case CMD_DELETE:
+					rslot = ExecProcessReturning(resultRelInfo,
+												 resultRelInfo->ri_oldTupleSlot,
+												 context->planSlot);
+					break;
+
+				case CMD_NOTHING:
+					break;
+
+				default:
+					elog(ERROR, "unrecognized commandType: %d",
+						 (int) commandType);
+			}
+		}
+
 		/*
 		 * We've activated one of the WHEN clauses, so we don't search
 		 * further. This is required behaviour, not an optimization.
@@ -3104,19 +3146,22 @@ lmerge_matched:
 	/*
 	 * Successfully executed an action or no qualifying action was found.
 	 */
-	return true;
+	*matched = true;
+
+	return rslot;
 }
 
 /*
  * Execute the first qualifying NOT MATCHED action.
  */
-static void
+static TupleTableSlot *
 ExecMergeNotMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 					bool canSetTag)
 {
 	ModifyTableState *mtstate = context->mtstate;
 	ExprContext *econtext = mtstate->ps.ps_ExprContext;
 	List	   *actionStates = NIL;
+	TupleTableSlot *rslot = NULL;
 	ListCell   *l;
 
 	/*
@@ -3166,10 +3211,10 @@ ExecMergeNotMatched(ModifyTableContext *
 				 * so we don't need to map the tuple here.
 				 */
 				newslot = ExecProject(action->mas_proj);
-				context->relaction = action;
+				mtstate->mt_merge_action = action;
 
-				(void) ExecInsert(context, mtstate->rootResultRelInfo, newslot,
-								  canSetTag, NULL, NULL);
+				rslot = ExecInsert(context, mtstate->rootResultRelInfo,
+								   newslot, canSetTag, NULL, NULL);
 				mtstate->mt_merge_inserted += 1;
 				break;
 			case CMD_NOTHING:
@@ -3185,6 +3230,8 @@ ExecMergeNotMatched(ModifyTableContext *
 		 */
 		break;
 	}
+
+	return rslot;
 }
 
 /*
@@ -3647,8 +3694,17 @@ ExecModifyTable(PlanState *pstate)
 				{
 					EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
 
-					ExecMerge(&context, node->resultRelInfo, NULL, node->canSetTag);
-					continue;	/* no RETURNING support yet */
+					slot = ExecMerge(&context, node->resultRelInfo, NULL,
+									 node->canSetTag);
+
+					/*
+					 * If we got a RETURNING result, return it to the caller.
+					 * We'll continue the work on next call.
+					 */
+					if (slot)
+						return slot;
+
+					continue;	/* continue with the next tuple */
 				}
 
 				elog(ERROR, "tableoid is NULL");
@@ -3725,8 +3781,17 @@ ExecModifyTable(PlanState *pstate)
 					{
 						EvalPlanQualSetSlot(&node->mt_epqstate, context.planSlot);
 
-						ExecMerge(&context, node->resultRelInfo, NULL, node->canSetTag);
-						continue;	/* no RETURNING support yet */
+						slot = ExecMerge(&context, node->resultRelInfo, NULL,
+										 node->canSetTag);
+
+						/*
+						 * If we got a RETURNING result, return it to the
+						 * caller.  We'll continue the work on next call.
+						 */
+						if (slot)
+							return slot;
+
+						continue;	/* continue with the next tuple */
 					}
 
 					elog(ERROR, "ctid is NULL");
@@ -3818,7 +3883,7 @@ ExecModifyTable(PlanState *pstate)
 				}
 				slot = ExecGetUpdateNewTuple(resultRelInfo, context.planSlot,
 											 oldSlot);
-				context.relaction = NULL;
+				node->mt_merge_action = NULL;
 
 				/* Now apply the update. */
 				slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
new file mode 100644
index 3397568..b9f443c
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2033,6 +2033,8 @@ SPI_result_code_string(int code)
 			return "SPI_OK_TD_REGISTER";
 		case SPI_OK_MERGE:
 			return "SPI_OK_MERGE";
+		case SPI_OK_MERGE_RETURNING:
+			return "SPI_OK_MERGE_RETURNING";
 	}
 	/* Unrecognized code ... return something useful ... */
 	sprintf(buf, "Unrecognized SPI code %d", code);
@@ -2886,7 +2888,10 @@ _SPI_pquery(QueryDesc *queryDesc, bool f
 				res = SPI_OK_UPDATE;
 			break;
 		case CMD_MERGE:
-			res = SPI_OK_MERGE;
+			if (queryDesc->plannedstmt->hasReturning)
+				res = SPI_OK_MERGE_RETURNING;
+			else
+				res = SPI_OK_MERGE;
 			break;
 		default:
 			return SPI_ERROR_OPUNKNOWN;
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
new file mode 100644
index a3a0876..e966037
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -1980,6 +1980,12 @@ llvm_compile_expr(ExprState *state)
 					break;
 				}
 
+			case EEOP_MERGING_FUNC:
+				build_EvalXFunc(b, mod, "ExecEvalMergingFunc",
+								v_state, op, v_econtext);
+				LLVMBuildBr(b, opblocks[opno + 1]);
+				break;
+
 			case EEOP_SUBPLAN:
 				build_EvalXFunc(b, mod, "ExecEvalSubPlan",
 								v_state, op, v_econtext);
diff --git a/src/backend/jit/llvm/llvmjit_types.c b/src/backend/jit/llvm/llvmjit_types.c
new file mode 100644
index 791902f..65db8f2
--- a/src/backend/jit/llvm/llvmjit_types.c
+++ b/src/backend/jit/llvm/llvmjit_types.c
@@ -155,6 +155,7 @@ void	   *referenced_functions[] =
 	ExecEvalFuncExprFusage,
 	ExecEvalFuncExprStrictFusage,
 	ExecEvalGroupingFunc,
+	ExecEvalMergingFunc,
 	ExecEvalMinMax,
 	ExecEvalNextValueExpr,
 	ExecEvalParamExec,
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
new file mode 100644
index c03f4f2..6cf7895
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -67,6 +67,9 @@ exprType(const Node *expr)
 		case T_WindowFunc:
 			type = ((const WindowFunc *) expr)->wintype;
 			break;
+		case T_MergingFunc:
+			type = ((const MergingFunc *) expr)->mftype;
+			break;
 		case T_SubscriptingRef:
 			type = ((const SubscriptingRef *) expr)->refrestype;
 			break;
@@ -810,6 +813,9 @@ exprCollation(const Node *expr)
 		case T_WindowFunc:
 			coll = ((const WindowFunc *) expr)->wincollid;
 			break;
+		case T_MergingFunc:
+			coll = ((const MergingFunc *) expr)->mfcollid;
+			break;
 		case T_SubscriptingRef:
 			coll = ((const SubscriptingRef *) expr)->refcollid;
 			break;
@@ -1085,6 +1091,9 @@ exprSetCollation(Node *expr, Oid collati
 		case T_WindowFunc:
 			((WindowFunc *) expr)->wincollid = collation;
 			break;
+		case T_MergingFunc:
+			((MergingFunc *) expr)->mfcollid = collation;
+			break;
 		case T_SubscriptingRef:
 			((SubscriptingRef *) expr)->refcollid = collation;
 			break;
@@ -1343,6 +1352,9 @@ exprLocation(const Node *expr)
 			/* function name should always be the first thing */
 			loc = ((const WindowFunc *) expr)->location;
 			break;
+		case T_MergingFunc:
+			loc = ((const MergingFunc *) expr)->location;
+			break;
 		case T_SubscriptingRef:
 			/* just use container argument's location */
 			loc = exprLocation((Node *) ((const SubscriptingRef *) expr)->refexpr);
@@ -2035,6 +2047,7 @@ expression_tree_walker_impl(Node *node,
 		case T_RangeTblRef:
 		case T_SortGroupClause:
 		case T_CTESearchClause:
+		case T_MergingFunc:
 			/* primitive node types with no expression subnodes */
 			break;
 		case T_WithCheckOption:
@@ -2865,6 +2878,7 @@ expression_tree_mutator_impl(Node *node,
 		case T_RangeTblRef:
 		case T_SortGroupClause:
 		case T_CTESearchClause:
+		case T_MergingFunc:
 			return (Node *) copyObject(node);
 		case T_WithCheckOption:
 			{
@@ -3827,6 +3841,7 @@ raw_expression_tree_walker_impl(Node *no
 		case T_ParamRef:
 		case T_A_Const:
 		case T_A_Star:
+		case T_MergingFunc:
 			/* primitive node types with no subnodes */
 			break;
 		case T_Alias:
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
new file mode 100644
index 7a9fe88..262adf5
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -1846,7 +1846,8 @@ convert_EXISTS_to_ANY(PlannerInfo *root,
 /*
  * Replace correlation vars (uplevel vars) with Params.
  *
- * Uplevel PlaceHolderVars and aggregates are replaced, too.
+ * Uplevel PlaceHolderVars, aggregates, GROUPING() expressions, and MERGING()
+ * expressions are replaced, too.
  *
  * Note: it is critical that this runs immediately after SS_process_sublinks.
  * Since we do not recurse into the arguments of uplevel PHVs and aggregates,
@@ -1900,6 +1901,11 @@ replace_correlation_vars_mutator(Node *n
 		if (((GroupingFunc *) node)->agglevelsup > 0)
 			return (Node *) replace_outer_grouping(root, (GroupingFunc *) node);
 	}
+	if (IsA(node, MergingFunc))
+	{
+		if (root->parse->commandType != CMD_MERGE)
+			return (Node *) replace_outer_merging(root, (MergingFunc *) node);
+	}
 	return expression_tree_mutator(node,
 								   replace_correlation_vars_mutator,
 								   (void *) root);
diff --git a/src/backend/optimizer/util/paramassign.c b/src/backend/optimizer/util/paramassign.c
new file mode 100644
index d6a923b..b3b4da6
--- a/src/backend/optimizer/util/paramassign.c
+++ b/src/backend/optimizer/util/paramassign.c
@@ -307,6 +307,56 @@ replace_outer_grouping(PlannerInfo *root
 }
 
 /*
+ * Generate a Param node to replace the given MergingFunc expression which is
+ * expected to be in the RETURNING list of an upper-level MERGE query.  Record
+ * the need for the MergingFunc in the proper upper-level root->plan_params.
+ */
+Param *
+replace_outer_merging(PlannerInfo *root, MergingFunc *mf)
+{
+	Param	   *retval;
+	PlannerParamItem *pitem;
+	Oid			ptype = exprType((Node *) mf);
+
+	Assert(root->parse->commandType != CMD_MERGE);
+
+	/*
+	 * The parser should have ensured that the MergingFunc is in the RETURNING
+	 * list of an upper-level MERGE query, so find that query.
+	 */
+	do
+	{
+		root = root->parent_root;
+		if (root == NULL)
+			elog(ERROR, "MergingFunc found outside MERGE");
+	} while (root->parse->commandType != CMD_MERGE);
+
+	/*
+	 * It does not seem worthwhile to try to de-duplicate references to outer
+	 * MergingFunc expressions.  Just make a new slot every time.
+	 */
+	mf = copyObject(mf);
+
+	pitem = makeNode(PlannerParamItem);
+	pitem->item = (Node *) mf;
+	pitem->paramId = list_length(root->glob->paramExecTypes);
+	root->glob->paramExecTypes = lappend_oid(root->glob->paramExecTypes,
+											 ptype);
+
+	root->plan_params = lappend(root->plan_params, pitem);
+
+	retval = makeNode(Param);
+	retval->paramkind = PARAM_EXEC;
+	retval->paramid = pitem->paramId;
+	retval->paramtype = ptype;
+	retval->paramtypmod = -1;
+	retval->paramcollid = InvalidOid;
+	retval->location = mf->location;
+
+	return retval;
+}
+
+/*
  * Generate a Param node to replace the given Var,
  * which is expected to come from some upper NestLoop plan node.
  * Record the need for the Var in root->curOuterParams.
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
new file mode 100644
index 7a1dfb6..5f882b4
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -74,7 +74,6 @@ static void determineRecursiveColTypes(P
 									   Node *larg, List *nrtargetlist);
 static Query *transformReturnStmt(ParseState *pstate, ReturnStmt *stmt);
 static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt);
-static List *transformReturningList(ParseState *pstate, List *returningList);
 static Query *transformPLAssignStmt(ParseState *pstate,
 									PLAssignStmt *stmt);
 static Query *transformDeclareCursorStmt(ParseState *pstate,
@@ -553,7 +552,8 @@ transformDeleteStmt(ParseState *pstate,
 	qual = transformWhereClause(pstate, stmt->whereClause,
 								EXPR_KIND_WHERE, "WHERE");
 
-	qry->returningList = transformReturningList(pstate, stmt->returningList);
+	qry->returningList = transformReturningList(pstate, stmt->returningList,
+												EXPR_KIND_RETURNING);
 
 	/* done building the range table and jointree */
 	qry->rtable = pstate->p_rtable;
@@ -980,7 +980,8 @@ transformInsertStmt(ParseState *pstate,
 	/* Process RETURNING, if any. */
 	if (stmt->returningList)
 		qry->returningList = transformReturningList(pstate,
-													stmt->returningList);
+													stmt->returningList,
+													EXPR_KIND_RETURNING);
 
 	/* done building the range table and jointree */
 	qry->rtable = pstate->p_rtable;
@@ -2445,7 +2446,8 @@ transformUpdateStmt(ParseState *pstate,
 	qual = transformWhereClause(pstate, stmt->whereClause,
 								EXPR_KIND_WHERE, "WHERE");
 
-	qry->returningList = transformReturningList(pstate, stmt->returningList);
+	qry->returningList = transformReturningList(pstate, stmt->returningList,
+												EXPR_KIND_RETURNING);
 
 	/*
 	 * Now we are done with SELECT-like processing, and can get on with
@@ -2539,10 +2541,11 @@ transformUpdateTargetList(ParseState *ps
 
 /*
  * transformReturningList -
- *	handle a RETURNING clause in INSERT/UPDATE/DELETE
+ *	handle a RETURNING clause in INSERT/UPDATE/DELETE/MERGE
  */
-static List *
-transformReturningList(ParseState *pstate, List *returningList)
+List *
+transformReturningList(ParseState *pstate, List *returningList,
+					   ParseExprKind exprKind)
 {
 	List	   *rlist;
 	int			save_next_resno;
@@ -2559,7 +2562,7 @@ transformReturningList(ParseState *pstat
 	pstate->p_next_resno = 1;
 
 	/* transform RETURNING identically to a SELECT targetlist */
-	rlist = transformTargetList(pstate, returningList, EXPR_KIND_RETURNING);
+	rlist = transformTargetList(pstate, returningList, exprKind);
 
 	/*
 	 * Complain if the nonempty tlist expanded to nothing (which is possible
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
new file mode 100644
index c224df4..c366c5b
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -693,7 +693,7 @@ static Node *makeRecursiveViewSelect(cha
 	BOOLEAN_P BOTH BREADTH BY
 
 	CACHE CALL CALLED CASCADE CASCADED CASE CAST CATALOG_P CHAIN CHAR_P
-	CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLOSE
+	CHARACTER CHARACTERISTICS CHECK CHECKPOINT CLASS CLAUSE_NUMBER CLOSE
 	CLUSTER COALESCE COLLATE COLLATION COLUMN COLUMNS COMMENT COMMENTS COMMIT
 	COMMITTED COMPRESSION CONCURRENTLY CONFIGURATION CONFLICT
 	CONNECTION CONSTRAINT CONSTRAINTS CONTENT_P CONTINUE_P CONVERSION_P COPY
@@ -731,7 +731,7 @@ static Node *makeRecursiveViewSelect(cha
 	LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
 	LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
-	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+	MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE MERGING METHOD
 	MINUTE_P MINVALUE MODE MONTH_P MOVE
 
 	NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -12332,6 +12332,7 @@ MergeStmt:
 			USING table_ref
 			ON a_expr
 			merge_when_list
+			returning_clause
 				{
 					MergeStmt  *m = makeNode(MergeStmt);
 
@@ -12340,6 +12341,7 @@ MergeStmt:
 					m->sourceRelation = $6;
 					m->joinCondition = $8;
 					m->mergeWhenClauses = $9;
+					m->returningList = $10;
 
 					$$ = (Node *) m;
 				}
@@ -15755,6 +15757,24 @@ func_expr_common_subexpr:
 					n->location = @1;
 					$$ = (Node *) n;
 				}
+			| MERGING '(' ACTION ')'
+				{
+					MergingFunc *m = makeNode(MergingFunc);
+
+					m->mfop = MERGING_ACTION;
+					m->mftype = TEXTOID;
+					m->location = @1;
+					$$ = (Node *) m;
+				}
+			| MERGING '(' CLAUSE_NUMBER ')'
+				{
+					MergingFunc *m = makeNode(MergingFunc);
+
+					m->mfop = MERGING_CLAUSE_NUMBER;
+					m->mftype = INT4OID;
+					m->location = @1;
+					$$ = (Node *) m;
+				}
 			;
 
 
@@ -17100,6 +17120,7 @@ unreserved_keyword:
 			| CHARACTERISTICS
 			| CHECKPOINT
 			| CLASS
+			| CLAUSE_NUMBER
 			| CLOSE
 			| CLUSTER
 			| COLUMNS
@@ -17419,6 +17440,7 @@ col_name_keyword:
 			| JSON_SCALAR
 			| JSON_SERIALIZE
 			| LEAST
+			| MERGING
 			| NATIONAL
 			| NCHAR
 			| NONE
@@ -17637,6 +17659,7 @@ bare_label_keyword:
 			| CHECK
 			| CHECKPOINT
 			| CLASS
+			| CLAUSE_NUMBER
 			| CLOSE
 			| CLUSTER
 			| COALESCE
@@ -17808,6 +17831,7 @@ bare_label_keyword:
 			| MATERIALIZED
 			| MAXVALUE
 			| MERGE
+			| MERGING
 			| METHOD
 			| MINVALUE
 			| MODE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
new file mode 100644
index 85cd47b..e72c37b
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -456,6 +456,7 @@ check_agglevels_and_constraints(ParseSta
 			errkind = true;
 			break;
 		case EXPR_KIND_RETURNING:
+		case EXPR_KIND_MERGE_RETURNING:
 			errkind = true;
 			break;
 		case EXPR_KIND_VALUES:
@@ -903,6 +904,7 @@ transformWindowFuncCall(ParseState *psta
 			errkind = true;
 			break;
 		case EXPR_KIND_RETURNING:
+		case EXPR_KIND_MERGE_RETURNING:
 			errkind = true;
 			break;
 		case EXPR_KIND_VALUES:
diff --git a/src/backend/parser/parse_cte.c b/src/backend/parser/parse_cte.c
new file mode 100644
index 6992a78..5cad6d8
--- a/src/backend/parser/parse_cte.c
+++ b/src/backend/parser/parse_cte.c
@@ -126,13 +126,6 @@ transformWithClause(ParseState *pstate,
 		CommonTableExpr *cte = (CommonTableExpr *) lfirst(lc);
 		ListCell   *rest;
 
-		/* MERGE is allowed by parser, but unimplemented. Reject for now */
-		if (IsA(cte->ctequery, MergeStmt))
-			ereport(ERROR,
-					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-					errmsg("MERGE not supported in WITH query"),
-					parser_errposition(pstate, cte->location));
-
 		for_each_cell(rest, withClause->ctes, lnext(withClause->ctes, lc))
 		{
 			CommonTableExpr *cte2 = (CommonTableExpr *) lfirst(rest);
@@ -153,7 +146,8 @@ transformWithClause(ParseState *pstate,
 			/* must be a data-modifying statement */
 			Assert(IsA(cte->ctequery, InsertStmt) ||
 				   IsA(cte->ctequery, UpdateStmt) ||
-				   IsA(cte->ctequery, DeleteStmt));
+				   IsA(cte->ctequery, DeleteStmt) ||
+				   IsA(cte->ctequery, MergeStmt));
 
 			pstate->p_hasModifyingCTE = true;
 		}
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
new file mode 100644
index 64c582c..ad0717e
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ static Node *transformAExprDistinct(Pars
 static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
 static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
 static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformMergingFunc(ParseState *pstate, MergingFunc *f);
 static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
 static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
 static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
@@ -227,6 +228,10 @@ transformExprRecurse(ParseState *pstate,
 			result = transformGroupingFunc(pstate, (GroupingFunc *) expr);
 			break;
 
+		case T_MergingFunc:
+			result = transformMergingFunc(pstate, (MergingFunc *) expr);
+			break;
+
 		case T_NamedArgExpr:
 			{
 				NamedArgExpr *na = (NamedArgExpr *) expr;
@@ -541,6 +546,7 @@ transformColumnRef(ParseState *pstate, C
 		case EXPR_KIND_LIMIT:
 		case EXPR_KIND_OFFSET:
 		case EXPR_KIND_RETURNING:
+		case EXPR_KIND_MERGE_RETURNING:
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
 		case EXPR_KIND_CHECK_CONSTRAINT:
@@ -1354,6 +1360,31 @@ transformAExprBetween(ParseState *pstate
 }
 
 static Node *
+transformMergingFunc(ParseState *pstate, MergingFunc *f)
+{
+	/*
+	 * Check that we're in the RETURNING list of a MERGE command.
+	 */
+	if (pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+	{
+		ParseState *parent_pstate = pstate->parentParseState;
+
+		while (parent_pstate &&
+			   parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+			parent_pstate = parent_pstate->parentParseState;
+
+		if (!parent_pstate ||
+			parent_pstate->p_expr_kind != EXPR_KIND_MERGE_RETURNING)
+			ereport(ERROR,
+					errcode(ERRCODE_WRONG_OBJECT_TYPE),
+					errmsg("MERGING() can only be used in the RETURNING list of a MERGE command"),
+					parser_errposition(pstate, f->location));
+	}
+
+	return (Node *) f;
+}
+
+static Node *
 transformBoolExpr(ParseState *pstate, BoolExpr *a)
 {
 	List	   *args = NIL;
@@ -1767,6 +1798,7 @@ transformSubLink(ParseState *pstate, Sub
 		case EXPR_KIND_LIMIT:
 		case EXPR_KIND_OFFSET:
 		case EXPR_KIND_RETURNING:
+		case EXPR_KIND_MERGE_RETURNING:
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
 		case EXPR_KIND_CYCLE_MARK:
@@ -3115,6 +3147,7 @@ ParseExprKindName(ParseExprKind exprKind
 		case EXPR_KIND_OFFSET:
 			return "OFFSET";
 		case EXPR_KIND_RETURNING:
+		case EXPR_KIND_MERGE_RETURNING:
 			return "RETURNING";
 		case EXPR_KIND_VALUES:
 		case EXPR_KIND_VALUES_SINGLE:
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
new file mode 100644
index 6c29471..1a0f983
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2599,6 +2599,7 @@ check_srf_call_placement(ParseState *pst
 			errkind = true;
 			break;
 		case EXPR_KIND_RETURNING:
+		case EXPR_KIND_MERGE_RETURNING:
 			errkind = true;
 			break;
 		case EXPR_KIND_VALUES:
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
new file mode 100644
index 91b1156..33852e9
--- a/src/backend/parser/parse_merge.c
+++ b/src/backend/parser/parse_merge.c
@@ -233,6 +233,10 @@ transformMergeStmt(ParseState *pstate, M
 	 */
 	qry->jointree = makeFromExpr(pstate->p_joinlist, joinExpr);
 
+	/* Transform the RETURNING list, if any */
+	qry->returningList = transformReturningList(pstate, stmt->returningList,
+												EXPR_KIND_MERGE_RETURNING);
+
 	/*
 	 * We now have a good query shape, so now look at the WHEN conditions and
 	 * action targetlists.
@@ -254,6 +258,7 @@ transformMergeStmt(ParseState *pstate, M
 		MergeAction *action;
 
 		action = makeNode(MergeAction);
+		action->index = foreach_current_index(l) + 1;
 		action->commandType = mergeWhenClause->commandType;
 		action->matched = mergeWhenClause->matched;
 
@@ -390,9 +395,6 @@ transformMergeStmt(ParseState *pstate, M
 
 	qry->mergeActionList = mergeActionList;
 
-	/* RETURNING could potentially be added in the future, but not in SQL std */
-	qry->returningList = NULL;
-
 	qry->hasTargetSRFs = false;
 	qry->hasSubLinks = pstate->p_hasSubLinks;
 
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
new file mode 100644
index 864ea9b..cc19041
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -2346,9 +2346,10 @@ addRangeTableEntryForCTE(ParseState *pst
 		cte->cterefcount++;
 
 	/*
-	 * We throw error if the CTE is INSERT/UPDATE/DELETE without RETURNING.
-	 * This won't get checked in case of a self-reference, but that's OK
-	 * because data-modifying CTEs aren't allowed to be recursive anyhow.
+	 * We throw error if the CTE is INSERT/UPDATE/DELETE/MERGE without
+	 * RETURNING.  This won't get checked in case of a self-reference, but
+	 * that's OK because data-modifying CTEs aren't allowed to be recursive
+	 * anyhow.
 	 */
 	if (IsA(cte->ctequery, Query))
 	{
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
new file mode 100644
index 3bc62ac..eaa2a2b
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -1807,6 +1807,20 @@ FigureColnameInternal(Node *node, char *
 			/* make GROUPING() act like a regular function */
 			*name = "grouping";
 			return 2;
+		case T_MergingFunc:
+			switch (((MergingFunc *) node)->mfop)
+			{
+				case MERGING_ACTION:
+					*name = "action";
+					return 2;
+				case MERGING_CLAUSE_NUMBER:
+					*name = "clause_number";
+					return 2;
+				default:
+					elog(ERROR, "unrecognized MergingFuncOp: %d",
+						 (int) ((MergingFunc *) node)->mfop);
+			}
+			break;
 		case T_SubLink:
 			switch (((SubLink *) node)->subLinkType)
 			{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 41a3623..3786a7c
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -3615,9 +3615,9 @@ RewriteQuery(Query *parsetree, List *rew
 	ListCell   *lc1;
 
 	/*
-	 * First, recursively process any insert/update/delete statements in WITH
-	 * clauses.  (We have to do this first because the WITH clauses may get
-	 * copied into rule actions below.)
+	 * First, recursively process any insert/update/delete/merge statements in
+	 * WITH clauses.  (We have to do this first because the WITH clauses may
+	 * get copied into rule actions below.)
 	 */
 	foreach(lc1, parsetree->cteList)
 	{
@@ -3642,7 +3642,8 @@ RewriteQuery(Query *parsetree, List *rew
 			if (!(ctequery->commandType == CMD_SELECT ||
 				  ctequery->commandType == CMD_UPDATE ||
 				  ctequery->commandType == CMD_INSERT ||
-				  ctequery->commandType == CMD_DELETE))
+				  ctequery->commandType == CMD_DELETE ||
+				  ctequery->commandType == CMD_MERGE))
 			{
 				/*
 				 * Currently it could only be NOTIFY; this error message will
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index b1620e4..966dcf0
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -395,10 +395,10 @@ get_row_security_policies(Query *root, R
 	 * on the final action we take.
 	 *
 	 * We already fetched the SELECT policies above, to check existing rows,
-	 * but we must also check that new rows created by UPDATE actions are
-	 * visible, if SELECT rights are required for this relation. We don't do
-	 * this for INSERT actions, since an INSERT command would only do this
-	 * check if it had a RETURNING list, and MERGE does not support RETURNING.
+	 * but we must also check that new rows created by INSERT/UPDATE actions
+	 * are visible, if SELECT rights are required. For INSERT actions, we only
+	 * do this if RETURNING is specified, to be consistent with a plain INSERT
+	 * command, which can only require SELECT rights when RETURNING is used.
 	 *
 	 * We don't push the UPDATE/DELETE USING quals to the RTE because we don't
 	 * really want to apply them while scanning the relation since we don't
@@ -420,6 +420,8 @@ get_row_security_policies(Query *root, R
 		List	   *merge_delete_restrictive_policies;
 		List	   *merge_insert_permissive_policies;
 		List	   *merge_insert_restrictive_policies;
+		List	   *merge_select_permissive_policies = NIL;
+		List	   *merge_select_restrictive_policies = NIL;
 
 		/*
 		 * Fetch the UPDATE policies and set them up to execute on the
@@ -457,9 +459,6 @@ get_row_security_policies(Query *root, R
 		 */
 		if (perminfo->requiredPerms & ACL_SELECT)
 		{
-			List	   *merge_select_permissive_policies;
-			List	   *merge_select_restrictive_policies;
-
 			get_policies_for_relation(rel, CMD_SELECT, user_id,
 									  &merge_select_permissive_policies,
 									  &merge_select_restrictive_policies);
@@ -508,6 +507,21 @@ get_row_security_policies(Query *root, R
 							   withCheckOptions,
 							   hasSubLinks,
 							   false);
+
+		/*
+		 * Add ALL/SELECT policies as WCO_RLS_INSERT_CHECK WCOs, to ensure
+		 * that the inserted row is visible when executing an INSERT action,
+		 * if RETURNING is specified and SELECT rights are required for this
+		 * relation.
+		 */
+		if (perminfo->requiredPerms & ACL_SELECT && root->returningList)
+			add_with_check_options(rel, rt_index,
+								   WCO_RLS_INSERT_CHECK,
+								   merge_select_permissive_policies,
+								   merge_select_restrictive_policies,
+								   withCheckOptions,
+								   hasSubLinks,
+								   true);
 	}
 
 	table_close(rel, NoLock);
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
new file mode 100644
index e3ccf6c..6f3dc89
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -2142,11 +2142,10 @@ QueryReturnsTuples(Query *parsetree)
 		case CMD_SELECT:
 			/* returns tuples */
 			return true;
-		case CMD_MERGE:
-			return false;
 		case CMD_INSERT:
 		case CMD_UPDATE:
 		case CMD_DELETE:
+		case CMD_MERGE:
 			/* the forms with RETURNING return tuples */
 			if (parsetree->returningList)
 				return true;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
new file mode 100644
index ed7f40f..aca2217
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7197,8 +7197,13 @@ get_merge_query_def(Query *query, depars
 			appendStringInfoString(buf, "DO NOTHING");
 	}
 
-	/* No RETURNING support in MERGE yet */
-	Assert(query->returningList == NIL);
+	/* Add RETURNING if present */
+	if (query->returningList)
+	{
+		appendContextKeyword(context, " RETURNING",
+							 -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+		get_target_list(query->returningList, context, NULL, colNamesVisible);
+	}
 }
 
 
@@ -8298,6 +8303,7 @@ isSimpleNode(Node *node, Node *parentNod
 		case T_Aggref:
 		case T_GroupingFunc:
 		case T_WindowFunc:
+		case T_MergingFunc:
 		case T_FuncExpr:
 		case T_JsonConstructorExpr:
 			/* function-like: name(..) or name[..] */
@@ -8652,6 +8658,27 @@ get_rule_expr(Node *node, deparse_contex
 			get_windowfunc_expr((WindowFunc *) node, context);
 			break;
 
+		case T_MergingFunc:
+			{
+				MergingFunc *mexpr = (MergingFunc *) node;
+
+				appendStringInfoString(buf, "MERGING(");
+				switch (mexpr->mfop)
+				{
+					case MERGING_ACTION:
+						appendStringInfoString(buf, "ACTION");
+						break;
+					case MERGING_CLAUSE_NUMBER:
+						appendStringInfoString(buf, "CLAUSE_NUMBER");
+						break;
+					default:
+						elog(ERROR, "unrecognized MergingFuncOp: %d",
+							 (int) mexpr->mfop);
+				}
+				appendStringInfoChar(buf, ')');
+			}
+			break;
+
 		case T_SubscriptingRef:
 			{
 				SubscriptingRef *sbsref = (SubscriptingRef *) node;
diff --git a/src/bin/psql/common.c b/src/bin/psql/common.c
new file mode 100644
index daabf6f..6b63214
--- a/src/bin/psql/common.c
+++ b/src/bin/psql/common.c
@@ -983,13 +983,17 @@ PrintQueryResult(PGresult *result, bool
 			else
 				success = true;
 
-			/* if it's INSERT/UPDATE/DELETE RETURNING, also print status */
+			/*
+			 * If it's INSERT/UPDATE/DELETE/MERGE RETURNING, also print
+			 * status.
+			 */
 			if (last || pset.show_all_results)
 			{
 				cmdstatus = PQcmdStatus(result);
 				if (strncmp(cmdstatus, "INSERT", 6) == 0 ||
 					strncmp(cmdstatus, "UPDATE", 6) == 0 ||
-					strncmp(cmdstatus, "DELETE", 6) == 0)
+					strncmp(cmdstatus, "DELETE", 6) == 0 ||
+					strncmp(cmdstatus, "MERGE", 5) == 0)
 					PrintQueryStatus(result, printStatusFout);
 			}
 
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
new file mode 100644
index 048573c..7580ab9
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -241,6 +241,7 @@ typedef enum ExprEvalOp
 	EEOP_AGGREF,
 	EEOP_GROUPING_FUNC,
 	EEOP_WINDOW_FUNC,
+	EEOP_MERGING_FUNC,
 	EEOP_SUBPLAN,
 
 	/* aggregation related nodes */
@@ -623,6 +624,12 @@ typedef struct ExprEvalStep
 			WindowFuncExprState *wfstate;
 		}			window_func;
 
+		/* for EEOP_MERGING_FUNC */
+		struct
+		{
+			MergingFuncOp mfop; /* the MERGING() operation to perform */
+		}			merging_func;
+
 		/* for EEOP_SUBPLAN */
 		struct
 		{
@@ -806,6 +813,8 @@ extern void ExecEvalJsonConstructor(Expr
 									ExprContext *econtext);
 extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op);
 extern void ExecEvalGroupingFunc(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalMergingFunc(ExprState *state, ExprEvalStep *op,
+								ExprContext *econtext);
 extern void ExecEvalSubPlan(ExprState *state, ExprEvalStep *op,
 							ExprContext *econtext);
 extern void ExecEvalWholeRowVar(ExprState *state, ExprEvalStep *op,
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
new file mode 100644
index d1de139..ed8787d
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -97,6 +97,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
 #define SPI_OK_REL_UNREGISTER	16
 #define SPI_OK_TD_REGISTER		17
 #define SPI_OK_MERGE			18
+#define SPI_OK_MERGE_RETURNING	19
 
 #define SPI_OPT_NONATOMIC		(1 << 0)
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 5d7f17d..08bc9ee
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1318,6 +1318,9 @@ typedef struct ModifyTableState
 	/* Flags showing which subcommands are present INS/UPD/DEL/DO NOTHING */
 	int			mt_merge_subcommands;
 
+	/* For MERGE, the action currently being executed */
+	MergeActionState *mt_merge_action;
+
 	/* tuple counters for MERGE */
 	double		mt_merge_inserted;
 	double		mt_merge_updated;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index cf7e790..0dd872a
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1688,6 +1688,7 @@ typedef struct MergeWhenClause
 typedef struct MergeAction
 {
 	NodeTag		type;
+	int			index;			/* 1-based index of the clause */
 	bool		matched;		/* true=MATCHED, false=NOT MATCHED */
 	CmdType		commandType;	/* INSERT/UPDATE/DELETE/DO NOTHING */
 	/* OVERRIDING clause */
@@ -1951,6 +1952,7 @@ typedef struct MergeStmt
 	Node	   *sourceRelation; /* source relation */
 	Node	   *joinCondition;	/* join condition between source and target */
 	List	   *mergeWhenClauses;	/* list of MergeWhenClause(es) */
+	List	   *returningList;	/* list of expressions to return */
 	WithClause *withClause;		/* WITH clause */
 } MergeStmt;
 
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
new file mode 100644
index ab6d7fd..7a2fe8e
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -564,6 +564,39 @@ typedef struct WindowFunc
 } WindowFunc;
 
 /*
+ * MergingFunc
+ *
+ * A MergingFunc is a MERGING(...) expression that can only appear in the
+ * RETURNING list of a MERGE command.  It returns information about the
+ * currently executing merge action.  Possible operations are:
+ *
+ *	MERGING_ACTION:
+ *		Return the command string of the current merge action ("INSERT",
+ *		"UPDATE" or "DELETE").
+ *
+ *	MERGING_CLAUSE_NUMBER:
+ *		Return the 1-based index of the current merge WHEN clause.
+ */
+typedef enum MergingFuncOp
+{
+	MERGING_ACTION,
+	MERGING_CLAUSE_NUMBER,
+} MergingFuncOp;
+
+typedef struct MergingFunc
+{
+	Expr		xpr;
+	/* operation to perform */
+	MergingFuncOp mfop;
+	/* type Oid of result */
+	Oid			mftype pg_node_attr(query_jumble_ignore);
+	/* OID of collation, or InvalidOid if none */
+	Oid			mfcollid pg_node_attr(query_jumble_ignore);
+	/* token location, or -1 if unknown */
+	int			location;
+} MergingFunc;
+
+/*
  * SubscriptingRef: describes a subscripting operation over a container
  * (array, etc).
  *
diff --git a/src/include/optimizer/paramassign.h b/src/include/optimizer/paramassign.h
new file mode 100644
index 55c27a6..cd0e629
--- a/src/include/optimizer/paramassign.h
+++ b/src/include/optimizer/paramassign.h
@@ -20,6 +20,7 @@ extern Param *replace_outer_placeholderv
 										   PlaceHolderVar *phv);
 extern Param *replace_outer_agg(PlannerInfo *root, Aggref *agg);
 extern Param *replace_outer_grouping(PlannerInfo *root, GroupingFunc *grp);
+extern Param *replace_outer_merging(PlannerInfo *root, MergingFunc *mf);
 extern Param *replace_nestloop_param_var(PlannerInfo *root, Var *var);
 extern Param *replace_nestloop_param_placeholdervar(PlannerInfo *root,
 													PlaceHolderVar *phv);
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
new file mode 100644
index c96483a..47d988b
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -44,6 +44,8 @@ extern List *transformInsertRow(ParseSta
 								bool strip_indirection);
 extern List *transformUpdateTargetList(ParseState *pstate,
 									   List *origTlist);
+extern List *transformReturningList(ParseState *pstate, List *returningList,
+									ParseExprKind exprKind);
 extern Query *transformTopLevelStmt(ParseState *pstate, RawStmt *parseTree);
 extern Query *transformStmt(ParseState *pstate, Node *parseTree);
 
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
new file mode 100644
index 5984dcf..c283a3f
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -80,6 +80,7 @@ PG_KEYWORD("characteristics", CHARACTERI
 PG_KEYWORD("check", CHECK, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("checkpoint", CHECKPOINT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("class", CLASS, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("clause_number", CLAUSE_NUMBER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("close", CLOSE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("cluster", CLUSTER, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("coalesce", COALESCE, COL_NAME_KEYWORD, BARE_LABEL)
@@ -266,6 +267,7 @@ PG_KEYWORD("matched", MATCHED, UNRESERVE
 PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("merging", MERGING, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
new file mode 100644
index f589112..836819d
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -61,7 +61,8 @@ typedef enum ParseExprKind
 	EXPR_KIND_DISTINCT_ON,		/* DISTINCT ON */
 	EXPR_KIND_LIMIT,			/* LIMIT */
 	EXPR_KIND_OFFSET,			/* OFFSET */
-	EXPR_KIND_RETURNING,		/* RETURNING */
+	EXPR_KIND_RETURNING,		/* RETURNING in INSERT/UPDATE/DELETE */
+	EXPR_KIND_MERGE_RETURNING,	/* RETURNING in MERGE */
 	EXPR_KIND_VALUES,			/* VALUES */
 	EXPR_KIND_VALUES_SINGLE,	/* single-row VALUES (in INSERT only) */
 	EXPR_KIND_CHECK_CONSTRAINT, /* CHECK constraint for a table */
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
new file mode 100644
index 4b76f76..fb6289c
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -4267,9 +4267,9 @@ exec_stmt_execsql(PLpgSQL_execstate *est
 	/*
 	 * If we have INTO, then we only need one row back ... but if we have INTO
 	 * STRICT or extra check too_many_rows, ask for two rows, so that we can
-	 * verify the statement returns only one.  INSERT/UPDATE/DELETE are always
-	 * treated strictly. Without INTO, just run the statement to completion
-	 * (tcount = 0).
+	 * verify the statement returns only one.  INSERT/UPDATE/DELETE/MERGE are
+	 * always treated strictly. Without INTO, just run the statement to
+	 * completion (tcount = 0).
 	 *
 	 * We could just ask for two rows always when using INTO, but there are
 	 * some cases where demanding the extra row costs significant time, eg by
@@ -4307,10 +4307,11 @@ exec_stmt_execsql(PLpgSQL_execstate *est
 		case SPI_OK_INSERT:
 		case SPI_OK_UPDATE:
 		case SPI_OK_DELETE:
+		case SPI_OK_MERGE:
 		case SPI_OK_INSERT_RETURNING:
 		case SPI_OK_UPDATE_RETURNING:
 		case SPI_OK_DELETE_RETURNING:
-		case SPI_OK_MERGE:
+		case SPI_OK_MERGE_RETURNING:
 			Assert(stmt->mod_stmt);
 			exec_set_found(estate, (SPI_processed != 0));
 			break;
@@ -4489,10 +4490,11 @@ exec_stmt_dynexecute(PLpgSQL_execstate *
 		case SPI_OK_INSERT:
 		case SPI_OK_UPDATE:
 		case SPI_OK_DELETE:
+		case SPI_OK_MERGE:
 		case SPI_OK_INSERT_RETURNING:
 		case SPI_OK_UPDATE_RETURNING:
 		case SPI_OK_DELETE_RETURNING:
-		case SPI_OK_MERGE:
+		case SPI_OK_MERGE_RETURNING:
 		case SPI_OK_UTILITY:
 		case SPI_OK_REWRITTEN:
 			break;
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
new file mode 100644
index e8f9d7b..0c9f81e
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -2456,6 +2456,7 @@ pltcl_process_SPI_result(Tcl_Interp *int
 		case SPI_OK_INSERT_RETURNING:
 		case SPI_OK_DELETE_RETURNING:
 		case SPI_OK_UPDATE_RETURNING:
+		case SPI_OK_MERGE_RETURNING:
 
 			/*
 			 * Process the tuples we got
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 28a6d0b..183d160
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -123,20 +123,20 @@ ON tid = tid
 WHEN MATCHED THEN DO NOTHING;
 ERROR:  name "target" specified more than once
 DETAIL:  The name is used both as MERGE target table and data source.
--- used in a CTE
+-- used in a CTE without RETURNING
 WITH foo AS (
   MERGE INTO target USING source ON (true)
   WHEN MATCHED THEN DELETE
 ) SELECT * FROM foo;
-ERROR:  MERGE not supported in WITH query
-LINE 1: WITH foo AS (
-             ^
--- used in COPY
+ERROR:  WITH query "foo" does not have a RETURNING clause
+LINE 4: ) SELECT * FROM foo;
+                        ^
+-- used in COPY without RETURNING
 COPY (
   MERGE INTO target USING source ON (true)
   WHEN MATCHED THEN DELETE
 ) TO stdout;
-ERROR:  MERGE not supported in COPY
+ERROR:  COPY query must have a RETURNING clause
 -- unsupported relation types
 -- view
 CREATE VIEW tv AS SELECT * FROM target;
@@ -1256,7 +1256,7 @@ BEGIN;
 MERGE INTO sq_target
 USING v
 ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
     UPDATE SET balance = balance + delta
 WHEN NOT MATCHED THEN
 	INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1271,7 +1271,7 @@ INSERT INTO sq_source (sid, balance, del
 MERGE INTO sq_target t
 USING v
 ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
     UPDATE SET balance = t.balance + delta
 WHEN NOT MATCHED THEN
 	INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1295,7 +1295,7 @@ WITH targq AS (
 MERGE INTO sq_target t
 USING v
 ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
     UPDATE SET balance = t.balance + delta
 WHEN NOT MATCHED THEN
 	INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -1304,20 +1304,191 @@ WHEN MATCHED AND tid < 2 THEN
 ROLLBACK;
 -- RETURNING
 BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
 MERGE INTO sq_target t
-USING v
+USING sq_source s
 ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
     UPDATE SET balance = t.balance + delta
 WHEN NOT MATCHED THEN
-	INSERT (balance, tid) VALUES (balance + delta, sid)
+    INSERT (balance, tid) VALUES (balance + delta, sid)
 WHEN MATCHED AND tid < 2 THEN
-	DELETE
-RETURNING *;
-ERROR:  syntax error at or near "RETURNING"
-LINE 10: RETURNING *;
-         ^
+    DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+          (SELECT abbrev FROM merge_actions
+            WHERE action = MERGING(ACTION)) AS action,
+          t.*,
+          CASE MERGING(ACTION)
+              WHEN 'INSERT' THEN 'Inserted '||t
+              WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+              WHEN 'DELETE' THEN 'Removed '||t
+          END AS description;
+ clause | action | tid | balance |     description     
+--------+--------+-----+---------+---------------------
+      3 | del    |   1 |     100 | Removed (1,100)
+      1 | upd    |   2 |     220 | Added 20 to balance
+      2 | ins    |   4 |      40 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- error when using MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+ERROR:  MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: SELECT MERGING(ACTION) FROM sq_target;
+               ^
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+ERROR:  MERGING() can only be used in the RETURNING list of a MERGE command
+LINE 1: ...ATE sq_target SET balance = balance + 1 RETURNING MERGING(CL...
+                                                             ^
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+    MERGE INTO sq_target t
+    USING sq_source s
+    ON tid = sid
+    WHEN MATCHED AND tid >= 2 THEN
+        UPDATE SET balance = t.balance + delta
+    WHEN NOT MATCHED THEN
+        INSERT (balance, tid) VALUES (balance + delta, sid)
+    WHEN MATCHED AND tid < 2 THEN
+        DELETE
+    RETURNING MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+              CASE MERGING(ACTION)
+                  WHEN 'INSERT' THEN 'Inserted '||t
+                  WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+                  WHEN 'DELETE' THEN 'Removed '||t
+              END AS description
+), m2 AS (
+    MERGE INTO sq_target_merge_log l
+    USING m
+    ON l.tid = m.tid
+    WHEN MATCHED THEN
+        UPDATE SET last_change = description
+    WHEN NOT MATCHED THEN
+        INSERT VALUES (m.tid, description)
+    RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+ action | log_action | tid |     last_change     
+--------+------------+-----+---------------------
+ DELETE | UPDATE     |   1 | Removed (1,100)
+ UPDATE | INSERT     |   2 | Added 20 to balance
+ INSERT | INSERT     |   4 | Inserted (4,40)
+(3 rows)
+
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ tid |     last_change     
+-----+---------------------
+   1 | Removed (1,100)
+   2 | Added 20 to balance
+   4 | Inserted (4,40)
+(3 rows)
+
+ROLLBACK;
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+    MERGE INTO sq_target t
+    USING sq_source s
+    ON tid = sid
+    WHEN MATCHED AND tid >= 2 THEN
+        UPDATE SET balance = t.balance + delta
+    WHEN NOT MATCHED THEN
+        INSERT (balance, tid) VALUES (balance + delta, sid)
+    WHEN MATCHED AND tid < 2 THEN
+        DELETE
+    RETURNING MERGING(ACTION), t.*
+) TO stdout;
+DELETE	1	100
+UPDATE	2	220
+INSERT	4	40
+ROLLBACK;
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+                                     OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+    MERGE INTO sq_target t
+    USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+    ON tid = v.sid
+    WHEN MATCHED AND tid >= 2 THEN
+        UPDATE SET balance = t.balance + v.delta
+    WHEN NOT MATCHED THEN
+        INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+    WHEN MATCHED AND tid < 2 THEN
+        DELETE
+    RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ action | tid | new_balance 
+--------+-----+-------------
+ DELETE |   1 |         100
+ UPDATE |   3 |         320
+ INSERT |   4 |         110
+(3 rows)
+
+ROLLBACK;
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+    MERGE INTO sq_target t
+    USING sq_source s
+    ON tid = sid
+    WHEN MATCHED AND tid >= 2 THEN
+        UPDATE SET balance = t.balance + delta
+    WHEN NOT MATCHED THEN
+        INSERT (balance, tid) VALUES (balance + delta, sid)
+    WHEN MATCHED AND tid < 2 THEN
+        DELETE
+    RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ action | tid | balance 
+--------+-----+---------
+ DELETE |   1 |     100
+ UPDATE |   2 |     220
+ INSERT |   4 |      40
+(3 rows)
+
+ROLLBACK;
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+                                     OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+    MERGE INTO sq_target t
+    USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+    ON tid = v.sid
+    WHEN MATCHED AND tid >= 2 THEN
+        UPDATE SET balance = t.balance + v.delta
+    WHEN NOT MATCHED THEN
+        INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+    WHEN MATCHED AND tid < 2 THEN
+        DELETE
+    RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ r_action | r_tid | r_balance 
+----------+-------+-----------
+ DELETE   |     1 |       100
+ UPDATE   |     3 |       320
+ INSERT   |     4 |       110
+(3 rows)
+
 ROLLBACK;
 -- EXPLAIN
 CREATE TABLE ex_mtarget (a int, b int)
@@ -1514,7 +1685,7 @@ SELECT * FROM sq_target WHERE tid = 1;
 (1 row)
 
 ROLLBACK;
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
 NOTICE:  drop cascades to view v
 CREATE TABLE pa_target (tid integer, balance float, val text)
 	PARTITION BY LIST (tid);
@@ -1640,6 +1811,32 @@ SELECT * FROM pa_target ORDER BY tid;
 (14 rows)
 
 ROLLBACK;
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+  USING pa_source s
+  ON t.tid = s.sid AND t.tid = 1
+  WHEN MATCHED THEN
+    UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+  RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | tid | balance |           val            
+--------+---------------+-----+---------+--------------------------
+ UPDATE |             1 |   2 |     110 | initial updated by merge
+(1 row)
+
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance |           val            
+-----+---------+--------------------------
+   2 |     110 | initial updated by merge
+   3 |     300 | initial
+   5 |     500 | initial
+   7 |     700 | initial
+   9 |     900 | initial
+  11 |    1100 | initial
+  13 |    1300 | initial
+(7 rows)
+
+ROLLBACK;
 DROP TABLE pa_target CASCADE;
 -- The target table is partitioned in the same way, but this time by attaching
 -- partitions which have columns in different order, dropped columns etc.
@@ -1731,7 +1928,26 @@ MERGE INTO pa_target t
   WHEN MATCHED THEN
     UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
   WHEN NOT MATCHED THEN
-    INSERT VALUES (sid, delta, 'inserted by merge');
+    INSERT VALUES (sid, delta, 'inserted by merge')
+  RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number | tid | balance |           val            
+--------+---------------+-----+---------+--------------------------
+ UPDATE |             1 |   2 |     110 | initial updated by merge
+ INSERT |             2 |   2 |      20 | inserted by merge
+ UPDATE |             1 |   4 |     330 | initial updated by merge
+ INSERT |             2 |   4 |      40 | inserted by merge
+ UPDATE |             1 |   6 |     550 | initial updated by merge
+ INSERT |             2 |   6 |      60 | inserted by merge
+ UPDATE |             1 |   8 |     770 | initial updated by merge
+ INSERT |             2 |   8 |      80 | inserted by merge
+ UPDATE |             1 |  10 |     990 | initial updated by merge
+ INSERT |             2 |  10 |     100 | inserted by merge
+ UPDATE |             1 |  12 |    1210 | initial updated by merge
+ INSERT |             2 |  12 |     120 | inserted by merge
+ UPDATE |             1 |  14 |    1430 | initial updated by merge
+ INSERT |             2 |  14 |     140 | inserted by merge
+(14 rows)
+
 SELECT * FROM pa_target ORDER BY tid;
  tid | balance |           val            
 -----+---------+--------------------------
@@ -1798,7 +2014,21 @@ MERGE INTO pa_target t
   WHEN MATCHED THEN
     UPDATE SET balance = balance + delta, val = val || ' updated by merge'
   WHEN NOT MATCHED THEN
-    INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+    INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+  RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+ action | clause_number |          logts           | tid | balance |           val            
+--------+---------------+--------------------------+-----+---------+--------------------------
+ UPDATE |             1 | Tue Jan 31 00:00:00 2017 |   1 |     110 | initial updated by merge
+ UPDATE |             1 | Tue Feb 28 00:00:00 2017 |   2 |     220 | initial updated by merge
+ INSERT |             2 | Sun Jan 15 00:00:00 2017 |   3 |      30 | inserted by merge
+ UPDATE |             1 | Tue Jan 31 00:00:00 2017 |   4 |     440 | initial updated by merge
+ UPDATE |             1 | Tue Feb 28 00:00:00 2017 |   5 |     550 | initial updated by merge
+ INSERT |             2 | Sun Jan 15 00:00:00 2017 |   6 |      60 | inserted by merge
+ UPDATE |             1 | Tue Jan 31 00:00:00 2017 |   7 |     770 | initial updated by merge
+ UPDATE |             1 | Tue Feb 28 00:00:00 2017 |   8 |     880 | initial updated by merge
+ INSERT |             2 | Sun Jan 15 00:00:00 2017 |   9 |      90 | inserted by merge
+(9 rows)
+
 SELECT * FROM pa_target ORDER BY tid;
           logts           | tid | balance |           val            
 --------------------------+-----+---------+--------------------------
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 6988128..3fd4fab
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2302,6 +2302,35 @@ WHEN MATCHED THEN
 	UPDATE SET dnotes = dnotes || ' notes added by merge8 '
 WHEN NOT MATCHED THEN
 	INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+ did | cid | dlevel | dauthor | dtitle | dnotes 
+-----+-----+--------+---------+--------+--------
+(0 rows)
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+	UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+	INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+ERROR:  new row violates row-level security policy for table "document"
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+	UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+	INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
+ sdid | did | cid | dlevel |     dauthor     |  dtitle   | dnotes 
+------+-----+-----+--------+-----------------+-----------+--------
+   14 |  14 |  11 |      1 | regress_rls_bob | new novel | 
+(1 row)
+
 RESET SESSION AUTHORIZATION;
 -- drop the restrictive SELECT policy so that we can look at the
 -- final state of the table
@@ -2325,7 +2354,8 @@ SELECT * FROM document;
   12 |  11 |      1 | regress_rls_bob   | another novel                    | 
    1 |  11 |      1 | regress_rls_bob   | my first novel                   |  notes added by merge2  notes added by merge3  notes added by merge4  notes added by merge7 
   13 |  44 |      1 | regress_rls_bob   | new manga                        | 
-(15 rows)
+  14 |  11 |      1 | regress_rls_bob   | new novel                        | 
+(16 rows)
 
 --
 -- ROLE/GROUP
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
new file mode 100644
index 1442c43..f187dec
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3596,7 +3596,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
 -- test deparsing
 CREATE TABLE sf_target(id int, data text, filling int[]);
 CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+               a int, b text, id int, data text, filling int[])
  LANGUAGE sql
 BEGIN ATOMIC
  MERGE INTO sf_target t
@@ -3633,11 +3634,13 @@ WHEN NOT MATCHED
    VALUES (s.a, s.b, DEFAULT)
 WHEN NOT MATCHED
    THEN INSERT (filling[1], id)
-   VALUES (s.a, s.a);
+   VALUES (s.a, s.a)
+RETURNING
+   merging(action), merging(clause_number) AS clause_number, *;
 END;
 \sf merge_sf_test
 CREATE OR REPLACE FUNCTION public.merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number integer, a integer, b text, id integer, data text, filling integer[])
  LANGUAGE sql
 BEGIN ATOMIC
  MERGE INTO sf_target t
@@ -3674,7 +3677,14 @@ BEGIN ATOMIC
       VALUES (s.a, s.b, DEFAULT)
     WHEN NOT MATCHED
      THEN INSERT (filling[1], id)
-      VALUES (s.a, s.a);
+      VALUES (s.a, s.a)
+   RETURNING MERGING(ACTION) AS action,
+     MERGING(CLAUSE_NUMBER) AS clause_number,
+     s.a,
+     s.b,
+     t.id,
+     t.data,
+     t.filling;
 END
 DROP FUNCTION merge_sf_test;
 DROP TABLE sf_target;
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 4cf6db9..0eb2f97
--- a/src/test/regress/sql/merge.sql
+++ b/src/test/regress/sql/merge.sql
@@ -86,12 +86,12 @@ MERGE INTO target
 USING target
 ON tid = tid
 WHEN MATCHED THEN DO NOTHING;
--- used in a CTE
+-- used in a CTE without RETURNING
 WITH foo AS (
   MERGE INTO target USING source ON (true)
   WHEN MATCHED THEN DELETE
 ) SELECT * FROM foo;
--- used in COPY
+-- used in COPY without RETURNING
 COPY (
   MERGE INTO target USING source ON (true)
   WHEN MATCHED THEN DELETE
@@ -816,7 +816,7 @@ BEGIN;
 MERGE INTO sq_target
 USING v
 ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
     UPDATE SET balance = balance + delta
 WHEN NOT MATCHED THEN
 	INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -829,7 +829,7 @@ INSERT INTO sq_source (sid, balance, del
 MERGE INTO sq_target t
 USING v
 ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
     UPDATE SET balance = t.balance + delta
 WHEN NOT MATCHED THEN
 	INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -847,7 +847,7 @@ WITH targq AS (
 MERGE INTO sq_target t
 USING v
 ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
     UPDATE SET balance = t.balance + delta
 WHEN NOT MATCHED THEN
 	INSERT (balance, tid) VALUES (balance + delta, sid)
@@ -857,17 +857,146 @@ ROLLBACK;
 
 -- RETURNING
 BEGIN;
-INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+CREATE TABLE merge_actions(action text, abbrev text);
+INSERT INTO merge_actions VALUES ('INSERT', 'ins'), ('UPDATE', 'upd'), ('DELETE', 'del');
 MERGE INTO sq_target t
-USING v
+USING sq_source s
 ON tid = sid
-WHEN MATCHED AND tid > 2 THEN
+WHEN MATCHED AND tid >= 2 THEN
     UPDATE SET balance = t.balance + delta
 WHEN NOT MATCHED THEN
-	INSERT (balance, tid) VALUES (balance + delta, sid)
+    INSERT (balance, tid) VALUES (balance + delta, sid)
 WHEN MATCHED AND tid < 2 THEN
-	DELETE
-RETURNING *;
+    DELETE
+RETURNING MERGING(CLAUSE_NUMBER) AS clause,
+          (SELECT abbrev FROM merge_actions
+            WHERE action = MERGING(ACTION)) AS action,
+          t.*,
+          CASE MERGING(ACTION)
+              WHEN 'INSERT' THEN 'Inserted '||t
+              WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+              WHEN 'DELETE' THEN 'Removed '||t
+          END AS description;
+ROLLBACK;
+
+-- error when using MERGING() outside MERGE
+SELECT MERGING(ACTION) FROM sq_target;
+UPDATE sq_target SET balance = balance + 1 RETURNING MERGING(CLAUSE_NUMBER);
+
+-- RETURNING in CTEs
+CREATE TABLE sq_target_merge_log (tid integer NOT NULL, last_change text);
+INSERT INTO sq_target_merge_log VALUES (1, 'Original value');
+BEGIN;
+WITH m AS (
+    MERGE INTO sq_target t
+    USING sq_source s
+    ON tid = sid
+    WHEN MATCHED AND tid >= 2 THEN
+        UPDATE SET balance = t.balance + delta
+    WHEN NOT MATCHED THEN
+        INSERT (balance, tid) VALUES (balance + delta, sid)
+    WHEN MATCHED AND tid < 2 THEN
+        DELETE
+    RETURNING MERGING(CLAUSE_NUMBER), MERGING(ACTION), t.*,
+              CASE MERGING(ACTION)
+                  WHEN 'INSERT' THEN 'Inserted '||t
+                  WHEN 'UPDATE' THEN 'Added '||delta||' to balance'
+                  WHEN 'DELETE' THEN 'Removed '||t
+              END AS description
+), m2 AS (
+    MERGE INTO sq_target_merge_log l
+    USING m
+    ON l.tid = m.tid
+    WHEN MATCHED THEN
+        UPDATE SET last_change = description
+    WHEN NOT MATCHED THEN
+        INSERT VALUES (m.tid, description)
+    RETURNING action, MERGING(ACTION) AS log_action, l.*
+)
+SELECT * FROM m2;
+SELECT * FROM sq_target_merge_log ORDER BY tid;
+ROLLBACK;
+
+-- COPY (MERGE ... RETURNING) TO ...
+BEGIN;
+COPY (
+    MERGE INTO sq_target t
+    USING sq_source s
+    ON tid = sid
+    WHEN MATCHED AND tid >= 2 THEN
+        UPDATE SET balance = t.balance + delta
+    WHEN NOT MATCHED THEN
+        INSERT (balance, tid) VALUES (balance + delta, sid)
+    WHEN MATCHED AND tid < 2 THEN
+        DELETE
+    RETURNING MERGING(ACTION), t.*
+) TO stdout;
+ROLLBACK;
+
+-- SQL function with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+                                     OUT action text, OUT tid int, OUT new_balance int)
+LANGUAGE sql AS
+$$
+    MERGE INTO sq_target t
+    USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+    ON tid = v.sid
+    WHEN MATCHED AND tid >= 2 THEN
+        UPDATE SET balance = t.balance + v.delta
+    WHEN NOT MATCHED THEN
+        INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+    WHEN MATCHED AND tid < 2 THEN
+        DELETE
+    RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT action, tid, new_balance FROM merge_into_sq_target(sid, balance, delta)) m;
+ROLLBACK;
+
+-- SQL SRF with MERGE ... RETURNING
+BEGIN;
+CREATE FUNCTION merge_sq_source_into_sq_target()
+RETURNS TABLE (action text, tid int, balance int)
+LANGUAGE sql AS
+$$
+    MERGE INTO sq_target t
+    USING sq_source s
+    ON tid = sid
+    WHEN MATCHED AND tid >= 2 THEN
+        UPDATE SET balance = t.balance + delta
+    WHEN NOT MATCHED THEN
+        INSERT (balance, tid) VALUES (balance + delta, sid)
+    WHEN MATCHED AND tid < 2 THEN
+        DELETE
+    RETURNING MERGING(ACTION), t.*;
+$$;
+SELECT * FROM merge_sq_source_into_sq_target();
+ROLLBACK;
+
+-- PL/pgSQL function with MERGE ... RETURNING ... INTO
+BEGIN;
+CREATE FUNCTION merge_into_sq_target(sid int, balance int, delta int,
+                                     OUT r_action text, OUT r_tid int, OUT r_balance int)
+LANGUAGE plpgsql AS
+$$
+BEGIN
+    MERGE INTO sq_target t
+    USING (VALUES ($1, $2, $3)) AS v(sid, balance, delta)
+    ON tid = v.sid
+    WHEN MATCHED AND tid >= 2 THEN
+        UPDATE SET balance = t.balance + v.delta
+    WHEN NOT MATCHED THEN
+        INSERT (balance, tid) VALUES (v.balance + v.delta, v.sid)
+    WHEN MATCHED AND tid < 2 THEN
+        DELETE
+    RETURNING MERGING(ACTION), t.* INTO r_action, r_tid, r_balance;
+END;
+$$;
+SELECT m.*
+FROM (VALUES (1, 0, 0), (3, 0, 20), (4, 100, 10)) AS v(sid, balance, delta),
+LATERAL (SELECT r_action, r_tid, r_balance FROM merge_into_sq_target(sid, balance, delta)) m;
 ROLLBACK;
 
 -- EXPLAIN
@@ -966,7 +1095,7 @@ WHEN MATCHED THEN
 SELECT * FROM sq_target WHERE tid = 1;
 ROLLBACK;
 
-DROP TABLE sq_target, sq_source CASCADE;
+DROP TABLE sq_target, sq_target_merge_log, sq_source CASCADE;
 
 CREATE TABLE pa_target (tid integer, balance float, val text)
 	PARTITION BY LIST (tid);
@@ -1033,6 +1162,17 @@ SELECT merge_func();
 SELECT * FROM pa_target ORDER BY tid;
 ROLLBACK;
 
+-- update partition key to partition not initially scanned
+BEGIN;
+MERGE INTO pa_target t
+  USING pa_source s
+  ON t.tid = s.sid AND t.tid = 1
+  WHEN MATCHED THEN
+    UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
+  RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
 DROP TABLE pa_target CASCADE;
 
 -- The target table is partitioned in the same way, but this time by attaching
@@ -1091,7 +1231,8 @@ MERGE INTO pa_target t
   WHEN MATCHED THEN
     UPDATE SET tid = tid + 1, balance = balance + delta, val = val || ' updated by merge'
   WHEN NOT MATCHED THEN
-    INSERT VALUES (sid, delta, 'inserted by merge');
+    INSERT VALUES (sid, delta, 'inserted by merge')
+  RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
 SELECT * FROM pa_target ORDER BY tid;
 ROLLBACK;
 
@@ -1145,7 +1286,8 @@ MERGE INTO pa_target t
   WHEN MATCHED THEN
     UPDATE SET balance = balance + delta, val = val || ' updated by merge'
   WHEN NOT MATCHED THEN
-    INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge');
+    INSERT VALUES (slogts::timestamp, sid, delta, 'inserted by merge')
+  RETURNING MERGING(ACTION), MERGING(CLAUSE_NUMBER), t.*;
 SELECT * FROM pa_target ORDER BY tid;
 ROLLBACK;
 
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index dec7340..1d5ed0a
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -992,6 +992,27 @@ WHEN MATCHED THEN
 	UPDATE SET dnotes = dnotes || ' notes added by merge8 '
 WHEN NOT MATCHED THEN
 	INSERT VALUES (13, 44, 1, 'regress_rls_bob', 'new manga');
+SELECT * FROM document WHERE did = 13;
+
+-- but not OK if RETURNING is used
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+	UPDATE SET dnotes = dnotes || ' notes added by merge9 '
+WHEN NOT MATCHED THEN
+	INSERT VALUES (14, 44, 1, 'regress_rls_bob', 'new manga')
+RETURNING *;
+
+-- but OK if new row is visible
+MERGE INTO document d
+USING (SELECT 14 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+	UPDATE SET dnotes = dnotes || ' notes added by merge10 '
+WHEN NOT MATCHED THEN
+	INSERT VALUES (14, 11, 1, 'regress_rls_bob', 'new novel')
+RETURNING *;
 
 RESET SESSION AUTHORIZATION;
 -- drop the restrictive SELECT policy so that we can look at the
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
new file mode 100644
index 8b7e255..d820b9b
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1281,7 +1281,8 @@ MERGE INTO rule_merge2 t USING (SELECT 1
 CREATE TABLE sf_target(id int, data text, filling int[]);
 
 CREATE FUNCTION merge_sf_test()
- RETURNS void
+ RETURNS TABLE(action text, clause_number int,
+               a int, b text, id int, data text, filling int[])
  LANGUAGE sql
 BEGIN ATOMIC
  MERGE INTO sf_target t
@@ -1318,7 +1319,9 @@ WHEN NOT MATCHED
    VALUES (s.a, s.b, DEFAULT)
 WHEN NOT MATCHED
    THEN INSERT (filling[1], id)
-   VALUES (s.a, s.a);
+   VALUES (s.a, s.a)
+RETURNING
+   merging(action), merging(clause_number) AS clause_number, *;
 END;
 
 \sf merge_sf_test
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
new file mode 100644
index 87c1aee..31a552e
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1559,6 +1559,8 @@ MergePath
 MergeScanSelCache
 MergeStmt
 MergeWhenClause
+MergingFunc
+MergingFuncOp
 MetaCommand
 MinMaxAggInfo
 MinMaxAggPath

Reply via email to