0002_merge_v23e_main.patch
application/octet-stream
Filename: 0002_merge_v23e_main.patch
Type: application/octet-stream
Part: 0
Patch
Format: unified
| File | + | − |
|---|---|---|
| contrib/test_decoding/expected/ddl.out | 46 | 0 |
| contrib/test_decoding/sql/ddl.sql | 16 | 0 |
| doc/src/sgml/libpq.sgml | 5 | 3 |
| doc/src/sgml/mvcc.sgml | 27 | 1 |
| doc/src/sgml/plpgsql.sgml | 2 | 1 |
| doc/src/sgml/ref/allfiles.sgml | 1 | 0 |
| doc/src/sgml/reference.sgml | 1 | 0 |
| doc/src/sgml/ref/insert.sgml | 10 | 1 |
| doc/src/sgml/ref/merge.sgml | 599 | 0 |
| doc/src/sgml/trigger.sgml | 20 | 0 |
| src/backend/access/heap/heapam.c | 3 | 0 |
| src/backend/catalog/sql_features.txt | 3 | 3 |
| src/backend/commands/explain.c | 30 | 0 |
| src/backend/commands/prepare.c | 1 | 0 |
| src/backend/commands/trigger.c | 126 | 35 |
| src/backend/executor/execMain.c | 16 | 0 |
| src/backend/executor/execPartition.c | 126 | 0 |
| src/backend/executor/execReplication.c | 2 | 2 |
| src/backend/executor/Makefile | 1 | 1 |
| src/backend/executor/nodeMerge.c | 565 | 0 |
| src/backend/executor/nodeModifyTable.c | 302 | 35 |
| src/backend/executor/README | 10 | 0 |
| src/backend/executor/spi.c | 3 | 0 |
| src/backend/nodes/copyfuncs.c | 40 | 0 |
| src/backend/nodes/equalfuncs.c | 32 | 0 |
| src/backend/nodes/nodeFuncs.c | 45 | 3 |
| src/backend/nodes/outfuncs.c | 25 | 0 |
| src/backend/nodes/readfuncs.c | 6 | 0 |
| src/backend/optimizer/plan/createplan.c | 18 | 4 |
| src/backend/optimizer/plan/planner.c | 27 | 2 |
| src/backend/optimizer/plan/setrefs.c | 53 | 0 |
| src/backend/optimizer/prep/preptlist.c | 39 | 1 |
| src/backend/optimizer/util/pathnode.c | 9 | 2 |
| src/backend/optimizer/util/plancat.c | 4 | 0 |
| src/backend/optimizer/util/relnode.c | 0 | 1 |
| src/backend/parser/analyze.c | 10 | 8 |
| src/backend/parser/gram.y | 157 | 1 |
| src/backend/parser/Makefile | 1 | 1 |
| src/backend/parser/parse_agg.c | 10 | 0 |
| src/backend/parser/parse_clause.c | 34 | 5 |
| src/backend/parser/parse_collate.c | 1 | 0 |
| src/backend/parser/parse_expr.c | 3 | 0 |
| src/backend/parser/parse_func.c | 3 | 0 |
| src/backend/parser/parse_merge.c | 580 | 0 |
| src/backend/parser/parse_relation.c | 10 | 0 |
| src/backend/rewrite/rewriteHandler.c | 105 | 7 |
| src/backend/rewrite/rowsecurity.c | 97 | 0 |
| src/backend/tcop/pquery.c | 5 | 0 |
| src/backend/tcop/utility.c | 16 | 0 |
| src/include/access/heapam.h | 1 | 0 |
| src/include/commands/trigger.h | 4 | 2 |
| src/include/executor/execPartition.h | 1 | 0 |
| src/include/executor/nodeMerge.h | 22 | 0 |
| src/include/executor/nodeModifyTable.h | 21 | 0 |
| src/include/executor/spi.h | 1 | 0 |
| src/include/miscadmin.h | 1 | 0 |
| src/include/nodes/execnodes.h | 39 | 1 |
| src/include/nodes/nodes.h | 5 | 1 |
| src/include/nodes/parsenodes.h | 35 | 4 |
| src/include/nodes/plannodes.h | 6 | 2 |
| src/include/nodes/relation.h | 5 | 2 |
| src/include/optimizer/pathnode.h | 5 | 2 |
| src/include/parser/analyze.h | 5 | 0 |
| src/include/parser/kwlist.h | 2 | 0 |
| src/include/parser/parse_clause.h | 4 | 1 |
| src/include/parser/parse_merge.h | 19 | 0 |
| src/include/parser/parse_node.h | 4 | 2 |
| src/include/rewrite/rewriteHandler.h | 1 | 0 |
| src/interfaces/libpq/fe-exec.c | 5 | 4 |
| src/pl/plpgsql/src/pl_exec.c | 4 | 1 |
| src/pl/plpgsql/src/pl_gram.y | 8 | 0 |
| src/pl/plpgsql/src/plpgsql.h | 2 | 2 |
| src/pl/plpgsql/src/pl_scanner.c | 1 | 0 |
| src/test/isolation/expected/merge-delete.out | 97 | 0 |
| src/test/isolation/expected/merge-insert-update.out | 84 | 0 |
| src/test/isolation/expected/merge-match-recheck.out | 106 | 0 |
| src/test/isolation/expected/merge-update.out | 213 | 0 |
| src/test/isolation/isolation_schedule | 4 | 0 |
| src/test/isolation/specs/merge-delete.spec | 51 | 0 |
| src/test/isolation/specs/merge-insert-update.spec | 52 | 0 |
| src/test/isolation/specs/merge-match-recheck.spec | 79 | 0 |
| src/test/isolation/specs/merge-update.spec | 132 | 0 |
| src/test/regress/expected/identity.out | 55 | 0 |
| src/test/regress/expected/merge.out | 1599 | 0 |
| src/test/regress/expected/rowsecurity.out | 153 | 0 |
| src/test/regress/expected/rules.out | 31 | 0 |
| src/test/regress/expected/triggers.out | 48 | 0 |
| src/test/regress/parallel_schedule | 1 | 1 |
| src/test/regress/serial_schedule | 1 | 0 |
| src/test/regress/sql/identity.sql | 45 | 0 |
| src/test/regress/sql/merge.sql | 1068 | 0 |
| src/test/regress/sql/rowsecurity.sql | 124 | 0 |
| src/test/regress/sql/rules.sql | 33 | 0 |
| src/test/regress/sql/triggers.sql | 47 | 0 |
| src/tools/pgindent/typedefs.list | 3 | 0 |
diff --git a/contrib/test_decoding/expected/ddl.out b/contrib/test_decoding/expected/ddl.out
index b7c76469fc..79c359d6e3 100644
--- a/contrib/test_decoding/expected/ddl.out
+++ b/contrib/test_decoding/expected/ddl.out
@@ -192,6 +192,52 @@ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc
COMMIT
(33 rows)
+-- MERGE support
+BEGIN;
+MERGE INTO replication_example t
+ USING (SELECT i as id, i as data, i as num FROM generate_series(-20, 5) i) s
+ ON t.id = s.id
+ WHEN MATCHED AND t.id < 0 THEN
+ UPDATE SET somenum = somenum + 1
+ WHEN MATCHED AND t.id >= 0 THEN
+ DELETE
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.*);
+COMMIT;
+/* display results */
+SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+ data
+--------------------------------------------------------------------------------------------------------------------------------------------------
+ BEGIN
+ table public.replication_example: INSERT: id[integer]:-20 somedata[integer]:-20 somenum[integer]:-20 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: INSERT: id[integer]:-19 somedata[integer]:-19 somenum[integer]:-19 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: INSERT: id[integer]:-18 somedata[integer]:-18 somenum[integer]:-18 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: INSERT: id[integer]:-17 somedata[integer]:-17 somenum[integer]:-17 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: INSERT: id[integer]:-16 somedata[integer]:-16 somenum[integer]:-16 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-15 somedata[integer]:-15 somenum[integer]:-14 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-14 somedata[integer]:-14 somenum[integer]:-13 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-13 somedata[integer]:-13 somenum[integer]:-12 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-12 somedata[integer]:-12 somenum[integer]:-11 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-11 somedata[integer]:-11 somenum[integer]:-10 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-10 somedata[integer]:-10 somenum[integer]:-9 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-9 somedata[integer]:-9 somenum[integer]:-8 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-8 somedata[integer]:-8 somenum[integer]:-7 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-7 somedata[integer]:-7 somenum[integer]:-6 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-6 somedata[integer]:-6 somenum[integer]:-5 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-5 somedata[integer]:-5 somenum[integer]:-4 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-4 somedata[integer]:-4 somenum[integer]:-3 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-3 somedata[integer]:-3 somenum[integer]:-2 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-2 somedata[integer]:-2 somenum[integer]:-1 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: UPDATE: id[integer]:-1 somedata[integer]:-1 somenum[integer]:0 zaphod1[integer]:null zaphod2[integer]:null
+ table public.replication_example: DELETE: id[integer]:0
+ table public.replication_example: DELETE: id[integer]:1
+ table public.replication_example: DELETE: id[integer]:2
+ table public.replication_example: DELETE: id[integer]:3
+ table public.replication_example: DELETE: id[integer]:4
+ table public.replication_example: DELETE: id[integer]:5
+ COMMIT
+(28 rows)
+
CREATE TABLE tr_unique(id2 serial unique NOT NULL, data int);
INSERT INTO tr_unique(data) VALUES(10);
ALTER TABLE tr_unique RENAME TO tr_pkey;
diff --git a/contrib/test_decoding/sql/ddl.sql b/contrib/test_decoding/sql/ddl.sql
index c4b10a4cf9..0e608b252f 100644
--- a/contrib/test_decoding/sql/ddl.sql
+++ b/contrib/test_decoding/sql/ddl.sql
@@ -93,6 +93,22 @@ COMMIT;
/* display results */
SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+-- MERGE support
+BEGIN;
+MERGE INTO replication_example t
+ USING (SELECT i as id, i as data, i as num FROM generate_series(-20, 5) i) s
+ ON t.id = s.id
+ WHEN MATCHED AND t.id < 0 THEN
+ UPDATE SET somenum = somenum + 1
+ WHEN MATCHED AND t.id >= 0 THEN
+ DELETE
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.*);
+COMMIT;
+
+/* display results */
+SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'include-xids', '0', 'skip-empty-xacts', '1');
+
CREATE TABLE tr_unique(id2 serial unique NOT NULL, data int);
INSERT INTO tr_unique(data) VALUES(10);
ALTER TABLE tr_unique RENAME TO tr_pkey;
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 1fd5dd9fca..de03046f5c 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -3873,9 +3873,11 @@ char *PQcmdTuples(PGresult *res);
<structname>PGresult</structname>. This function can only be used following
the execution of a <command>SELECT</command>, <command>CREATE TABLE AS</command>,
<command>INSERT</command>, <command>UPDATE</command>, <command>DELETE</command>,
- <command>MOVE</command>, <command>FETCH</command>, or <command>COPY</command> statement,
- or an <command>EXECUTE</command> of a prepared query that contains an
- <command>INSERT</command>, <command>UPDATE</command>, or <command>DELETE</command> statement.
+ <command>MERGE</command>, <command>MOVE</command>, <command>FETCH</command>,
+ or <command>COPY</command> statement, or an <command>EXECUTE</command> of a
+ prepared query that contains an <command>INSERT</command>,
+ <command>UPDATE</command>, <command>DELETE</command>
+ or <command>MERGE</command> statement.
If the command that generated the <structname>PGresult</structname> was anything
else, <function>PQcmdTuples</function> returns an empty string. The caller
should not free the return value directly. It will be freed when
diff --git a/doc/src/sgml/mvcc.sgml b/doc/src/sgml/mvcc.sgml
index 24613e3c75..0e3e89af56 100644
--- a/doc/src/sgml/mvcc.sgml
+++ b/doc/src/sgml/mvcc.sgml
@@ -422,6 +422,31 @@ COMMIT;
<literal>11</literal>, which no longer matches the criteria.
</para>
+ <para>
+ The <command>MERGE</command> allows the user to specify various combinations
+ of <command>INSERT</command>, <command>UPDATE</command> or
+ <command>DELETE</command> subcommands. A <command>MERGE</command> command
+ with both <command>INSERT</command> and <command>UPDATE</command>
+ subcommands looks similar to <command>INSERT</command> with an
+ <literal>ON CONFLICT DO UPDATE</literal> clause but does not guarantee
+ that either <command>INSERT</command> and <command>UPDATE</command> will occur.
+
+ If MERGE attempts an UPDATE or DELETE and the row is concurrently updated
+ but the join condition still passes for the current target and the current
+ source tuple, then MERGE will behave the same as the UPDATE or DELETE commands
+ and perform its action on the latest version of the row, using standard
+ EvalPlanQual. MERGE actions can be conditional, so conditions must be
+ re-evaluated on the latest row, starting from the first action.
+
+ On the other hand, if the row is concurrently updated or deleted so that
+ the join condition fails, then MERGE will execute a NOT MATCHED action, if it
+ exists and the AND WHEN qual evaluates to true.
+
+ If MERGE attempts an INSERT and a unique index is present and a duplicate
+ row is concurrently inserted then a uniqueness violation is raised. MERGE
+ does not attempt to avoid the ERROR by attempting an UPDATE.
+ </para>
+
<para>
Because Read Committed mode starts each command with a new snapshot
that includes all transactions committed up to that instant,
@@ -900,7 +925,8 @@ ERROR: could not serialize access due to read/write dependencies among transact
<para>
The commands <command>UPDATE</command>,
- <command>DELETE</command>, and <command>INSERT</command>
+ <command>DELETE</command>, <command>INSERT</command> and
+ <command>MERGE</command>
acquire this lock mode on the target table (in addition to
<literal>ACCESS SHARE</literal> locks on any other referenced
tables). In general, this lock mode will be acquired by any
diff --git a/doc/src/sgml/plpgsql.sgml b/doc/src/sgml/plpgsql.sgml
index 7ed926fd51..67b22a0d04 100644
--- a/doc/src/sgml/plpgsql.sgml
+++ b/doc/src/sgml/plpgsql.sgml
@@ -1246,7 +1246,7 @@ EXECUTE format('SELECT count(*) FROM %I '
</programlisting>
Another restriction on parameter symbols is that they only work in
<command>SELECT</command>, <command>INSERT</command>, <command>UPDATE</command>, and
- <command>DELETE</command> commands. In other statement
+ <command>DELETE</command> and <command>MERGE</command> commands. In other statement
types (generically called utility statements), you must insert
values textually even if they are just data values.
</para>
@@ -1529,6 +1529,7 @@ GET DIAGNOSTICS integer_var = ROW_COUNT;
<listitem>
<para>
<command>UPDATE</command>, <command>INSERT</command>, and <command>DELETE</command>
+ and <command>MERGE</command>
statements set <literal>FOUND</literal> true if at least one
row is affected, false if no row is affected.
</para>
diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml
index 22e6893211..4e01e5641c 100644
--- a/doc/src/sgml/ref/allfiles.sgml
+++ b/doc/src/sgml/ref/allfiles.sgml
@@ -159,6 +159,7 @@ Complete list of usable sgml source files in this directory.
<!ENTITY load SYSTEM "load.sgml">
<!ENTITY lock SYSTEM "lock.sgml">
<!ENTITY move SYSTEM "move.sgml">
+<!ENTITY merge SYSTEM "merge.sgml">
<!ENTITY notify SYSTEM "notify.sgml">
<!ENTITY prepare SYSTEM "prepare.sgml">
<!ENTITY prepareTransaction SYSTEM "prepare_transaction.sgml">
diff --git a/doc/src/sgml/ref/insert.sgml b/doc/src/sgml/ref/insert.sgml
index 134092fa9c..77dc11091e 100644
--- a/doc/src/sgml/ref/insert.sgml
+++ b/doc/src/sgml/ref/insert.sgml
@@ -571,6 +571,13 @@ INSERT <replaceable>oid</replaceable> <replaceable class="parameter">count</repl
is a partition, an error will occur if one of the input rows violates
the partition constraint.
</para>
+
+ <para>
+ You may also wish to consider using <command>MERGE</command>, since that
+ allows mixed <command>INSERT</command>, <command>UPDATE</command> and
+ <command>DELETE</command> within a single statement.
+ See <xref linkend="sql-merge"/>.
+ </para>
</refsect1>
<refsect1>
@@ -741,7 +748,9 @@ INSERT INTO distributors (did, dname) VALUES (10, 'Conrad International')
Also, the case in
which a column name list is omitted, but not all the columns are
filled from the <literal>VALUES</literal> clause or <replaceable>query</replaceable>,
- is disallowed by the standard.
+ is disallowed by the standard. If you prefer a more SQL Standard
+ conforming statement than <literal>ON CONFLICT</literal>, see
+ <xref linkend="sql-merge"/>.
</para>
<para>
diff --git a/doc/src/sgml/ref/merge.sgml b/doc/src/sgml/ref/merge.sgml
new file mode 100644
index 0000000000..405a4cee29
--- /dev/null
+++ b/doc/src/sgml/ref/merge.sgml
@@ -0,0 +1,599 @@
+<!--
+doc/src/sgml/ref/merge.sgml
+PostgreSQL documentation
+-->
+
+<refentry id="sql-merge">
+
+ <refmeta>
+ <refentrytitle>MERGE</refentrytitle>
+ <manvolnum>7</manvolnum>
+ <refmiscinfo>SQL - Language Statements</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>MERGE</refname>
+ <refpurpose>insert, update, or delete rows of a table based upon source data</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+<synopsis>
+MERGE INTO <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> [...]
+
+where <replaceable class="parameter">data_source</replaceable> is
+
+{ <replaceable class="parameter">source_table_name</replaceable> |
+ ( source_query )
+}
+[ [ AS ] <replaceable class="parameter">source_alias</replaceable> ]
+
+and <replaceable class="parameter">when_clause</replaceable> is
+
+{ WHEN MATCHED [ AND <replaceable class="parameter">condition</replaceable> ] THEN { <replaceable class="parameter">merge_update</replaceable> | <replaceable class="parameter">merge_delete</replaceable> } |
+ WHEN NOT MATCHED [ AND <replaceable class="parameter">condition</replaceable> ] THEN { <replaceable class="parameter">merge_insert</replaceable> | DO NOTHING }
+}
+
+and <replaceable class="parameter">merge_update</replaceable> is
+
+UPDATE SET { <replaceable class="parameter">column_name</replaceable> = { <replaceable class="parameter">expression</replaceable> | DEFAULT } |
+ ( <replaceable class="parameter">column_name</replaceable> [, ...] ) = ( { <replaceable class="parameter">expression</replaceable> | DEFAULT } [, ...] )
+ } [, ...]
+
+and <replaceable class="parameter">merge_insert</replaceable> is
+
+INSERT [( <replaceable class="parameter">column_name</replaceable> [, ...] )]
+[ OVERRIDING { SYSTEM | USER } VALUE ]
+{ VALUES ( { <replaceable class="parameter">expression</replaceable> | DEFAULT } [, ...] ) | DEFAULT VALUES }
+
+and <replaceable class="parameter">merge_delete</replaceable> is
+
+DELETE
+</synopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para>
+ <command>MERGE</command> performs actions that modify rows in the
+ <replaceable class="parameter">target_table_name</replaceable>,
+ using the <replaceable class="parameter">data_source</replaceable>.
+ <command>MERGE</command> provides a single <acronym>SQL</acronym>
+ statement that can conditionally <command>INSERT</command>,
+ <command>UPDATE</command> or <command>DELETE</command> rows, a task
+ that would otherwise require multiple procedural language statements.
+ </para>
+
+ <para>
+ First, the <command>MERGE</command> command performs a join
+ from <replaceable class="parameter">data_source</replaceable> to
+ <replaceable class="parameter">target_table_name</replaceable>
+ producing zero or more candidate change rows. For each candidate change
+ row the status of <literal>MATCHED</literal> or <literal>NOT MATCHED</literal> is set
+ just once, after which <literal>WHEN</literal> clauses are evaluated
+ in the order specified. If one of them is activated, the specified
+ action occurs. No more than one <literal>WHEN</literal> clause can be
+ activated for any candidate change row.
+ </para>
+
+ <para>
+ <command>MERGE</command> actions have the same effect as
+ regular <command>UPDATE</command>, <command>INSERT</command>, or
+ <command>DELETE</command> commands of the same names. The syntax of
+ those commands is different, notably that there is no <literal>WHERE</literal>
+ clause and no tablename is specified. All actions refer to the
+ <replaceable class="parameter">target_table_name</replaceable>,
+ though modifications to other tables may be made using triggers.
+ </para>
+
+ <para>
+ When <literal>DO NOTHING</literal> action is specified, the source row is
+ skipped. Since actions are evaluated in the given order, <literal>DO
+ NOTHING</literal> can be handy to skip non-interesting source rows before
+ more fine-grained handling.
+ </para>
+
+ <para>
+ There is no MERGE privilege.
+ You must have the <literal>UPDATE</literal> privilege on the column(s)
+ of the <replaceable class="parameter">target_table_name</replaceable>
+ referred to in the <literal>SET</literal> clause
+ if you specify an update action, the <literal>INSERT</literal> privilege
+ on the <replaceable class="parameter">target_table_name</replaceable>
+ if you specify an insert action and/or the <literal>DELETE</literal>
+ privilege on the <replaceable class="parameter">target_table_name</replaceable>
+ if you specify a delete action on the
+ <replaceable class="parameter">target_table_name</replaceable>.
+ Privileges are tested once at statement start and are checked
+ whether or not particular <literal>WHEN</literal> clauses are activated
+ during the subsequent execution.
+ You will require the <literal>SELECT</literal> privilege on the
+ <replaceable class="parameter">data_source</replaceable> and any column(s)
+ of the <replaceable class="parameter">target_table_name</replaceable>
+ referred to in a <literal>condition</literal>.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>Parameters</title>
+
+ <variablelist>
+ <varlistentry>
+ <term><replaceable class="parameter">target_table_name</replaceable></term>
+ <listitem>
+ <para>
+ The name (optionally schema-qualified) of the target table or materialized
+ view to merge into.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">target_alias</replaceable></term>
+ <listitem>
+ <para>
+ A substitute name for the target table. When an alias is
+ provided, it completely hides the actual name of the table. For
+ example, given <literal>MERGE foo AS f</literal>, the remainder of the
+ <command>MERGE</command> statement must refer to this table as
+ <literal>f</literal> not <literal>foo</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">source_table_name</replaceable></term>
+ <listitem>
+ <para>
+ The name (optionally schema-qualified) of the source table, view or
+ transition table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">source_query</replaceable></term>
+ <listitem>
+ <para>
+ A query (<command>SELECT</command> statement or <command>VALUES</command>
+ statement) that supplies the rows to be merged into the
+ <replaceable class="parameter">target_table_name</replaceable>.
+ Refer to the <xref linkend="sql-select"/>
+ statement or <xref linkend="sql-values"/>
+ statement for a description of the syntax.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">source_alias</replaceable></term>
+ <listitem>
+ <para>
+ A substitute name for the data source. When an alias is
+ provided, it completely hides whether table or query was specified.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">join_condition</replaceable></term>
+ <listitem>
+ <para>
+ <replaceable class="parameter">join_condition</replaceable> is
+ an expression resulting in a value of type
+ <type>boolean</type> (similar to a <literal>WHERE</literal>
+ clause) that specifies which rows in the
+ <replaceable class="parameter">data_source</replaceable>
+ match rows in the
+ <replaceable class="parameter">target_table_name</replaceable>.
+ </para>
+ <warning>
+ <para>
+ Only columns from <replaceable class="parameter">target_table_name</replaceable>
+ that attempt to match <replaceable class="parameter">data_source</replaceable>
+ rows should appear in <replaceable class="parameter">join_condition</replaceable>.
+ <replaceable class="parameter">join_condition</replaceable> subexpressions that
+ only reference <replaceable class="parameter">target_table_name</replaceable>
+ columns can only affect which action is taken, often in surprising ways.
+ </para>
+ </warning>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">when_clause</replaceable></term>
+ <listitem>
+ <para>
+ At least one <literal>WHEN</literal> clause is required.
+ </para>
+ <para>
+ If the <literal>WHEN</literal> clause specifies <literal>WHEN MATCHED</literal>
+ and the candidate change row matches a row in the
+ <replaceable class="parameter">target_table_name</replaceable>
+ the <literal>WHEN</literal> clause is activated if the
+ <replaceable class="parameter">condition</replaceable> is
+ absent or is present and evaluates to <literal>true</literal>.
+ If the <literal>WHEN</literal> clause specifies <literal>WHEN NOT MATCHED</literal>
+ and the candidate change row does not match a row in the
+ <replaceable class="parameter">target_table_name</replaceable>
+ the <literal>WHEN</literal> clause is activated if the
+ <replaceable class="parameter">condition</replaceable> is
+ absent or is present and evaluates to <literal>true</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">condition</replaceable></term>
+ <listitem>
+ <para>
+ An expression that returns a value of type <type>boolean</type>.
+ If this expression returns <literal>true</literal> then the <literal>WHEN</literal>
+ clause will be activated and the corresponding action will occur for
+ that row. The expression may not contain functions that possibly performs
+ writes to the database.
+ </para>
+ <para>
+ A condition on a <literal>WHEN MATCHED</literal> clause can refer to columns
+ in both the source and the target relation. A condition on a
+ <literal>WHEN NOT MATCHED</literal> clause can only refer to columns from
+ the source relation, since by definition there is no matching target row.
+ Only the system attributes from the target table are accessible.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">merge_insert</replaceable></term>
+ <listitem>
+ <para>
+ The specification of an <literal>INSERT</literal> action that inserts
+ one row into the target table.
+ The target column names can be listed in any order. If no list of
+ column names is given at all, the default is all the columns of the
+ table in their declared order.
+ </para>
+ <para>
+ Each column not present in the explicit or implicit column list will be
+ filled with a default value, either its declared default value
+ or null if there is none.
+ </para>
+ <para>
+ If the expression for any column is not of the correct data type,
+ automatic type conversion will be attempted.
+ </para>
+ <para>
+ If <replaceable class="parameter">target_table_name</replaceable>
+ is a partitioned table, each row is routed to the appropriate partition
+ and inserted into it.
+ If <replaceable class="parameter">target_table_name</replaceable>
+ is a partition, an error will occur if one of the input rows violates
+ the partition constraint.
+ </para>
+ <para>
+ Column names may not be specified more than once.
+ <command>INSERT</command> actions cannot contain sub-selects.
+ </para>
+ <para>
+ The <literal>VALUES</literal> clause can only refer to columns from
+ the source relation, since by definition there is no matching target row.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">merge_update</replaceable></term>
+ <listitem>
+ <para>
+ The specification of an <literal>UPDATE</literal> action that updates
+ the current row of the <replaceable
+ class="parameter">target_table_name</replaceable>.
+ Column names may not be specified more than once.
+ </para>
+ <para>
+ Do not include the table name, as you would normally do with an
+ <xref linkend="sql-update"/> command.
+ For example, <literal>UPDATE tab SET col = 1</literal> is invalid. Also,
+ do not include a <literal>WHERE</literal> clause, since only the current
+ row can be updated. For example,
+ <literal>UPDATE SET col = 1 WHERE key = 57</literal> is invalid.
+ <command>UPDATE</command> actions cannot contain sub-selects in the
+ <literal>SET</literal> clause.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">merge_delete</replaceable></term>
+ <listitem>
+ <para>
+ Specifies a <literal>DELETE</literal> action that deletes the current row
+ of the <replaceable class="parameter">target_table_name</replaceable>.
+ Do not include the tablename or any other clauses, as you would normally
+ do with an <xref linkend="sql-delete"/> command.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">column_name</replaceable></term>
+ <listitem>
+ <para>
+ The name of a column in the <replaceable
+ class="parameter">target_table_name</replaceable>. The column name
+ can be qualified with a subfield name or array subscript, if
+ needed. (Inserting into only some fields of a composite
+ column leaves the other fields null.) When referencing a
+ column, do not include the table's name in the specification
+ of a target column.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>OVERRIDING SYSTEM VALUE</literal></term>
+ <listitem>
+ <para>
+ Without this clause, it is an error to specify an explicit value
+ (other than <literal>DEFAULT</literal>) for an identity column defined
+ as <literal>GENERATED ALWAYS</literal>. This clause overrides that
+ restriction.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>OVERRIDING USER VALUE</literal></term>
+ <listitem>
+ <para>
+ If this clause is specified, then any values supplied for identity
+ columns defined as <literal>GENERATED BY DEFAULT</literal> are ignored
+ and the default sequence-generated values are applied.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>DEFAULT VALUES</literal></term>
+ <listitem>
+ <para>
+ All columns will be filled with their default values.
+ (An <literal>OVERRIDING</literal> clause is not permitted in this
+ form.)
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><replaceable class="parameter">expression</replaceable></term>
+ <listitem>
+ <para>
+ An expression to assign to the column. The expression can use the
+ old values of this and other columns in the table.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><literal>DEFAULT</literal></term>
+ <listitem>
+ <para>
+ Set the column to its default value (which will be NULL if no
+ specific default expression has been assigned to it).
+ </para>
+ </listitem>
+ </varlistentry>
+
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>Outputs</title>
+
+ <para>
+ On successful completion, a <command>MERGE</command> command returns a command
+ tag of the form
+<screen>
+MERGE <replaceable class="parameter">total-count</replaceable>
+</screen>
+ The <replaceable class="parameter">total-count</replaceable> is the total
+ number of rows changed (whether updated, inserted or deleted).
+ If <replaceable class="parameter">total-count</replaceable> is 0, no rows
+ were changed in any way.
+ </para>
+
+ </refsect1>
+
+ <refsect1>
+ <title>Execution</title>
+
+ <para>
+ The following steps take place during the execution of
+ <command>MERGE</command>.
+ <orderedlist>
+ <listitem>
+ <para>
+ Perform any BEFORE STATEMENT triggers for all actions specified, whether or
+ not their <literal>WHEN</literal> clauses are activated during execution.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Perform a join from source to target table.
+ The resulting query will be optimized normally and will produce
+ a set of candidate change row. For each candidate change row
+ <orderedlist>
+ <listitem>
+ <para>
+ Evaluate whether each row is MATCHED or NOT MATCHED.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Test each WHEN condition in the order specified until one activates.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ When activated, perform the following actions
+ <orderedlist>
+ <listitem>
+ <para>
+ Perform any BEFORE ROW triggers that fire for the action's event type.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Apply the action specified, invoking any check constraints on the
+ target table.
+ However, it will not invoke rules.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Perform any AFTER ROW triggers that fire for the action's event type.
+ </para>
+ </listitem>
+ </orderedlist>
+ </para>
+ </listitem>
+ </orderedlist>
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Perform any AFTER STATEMENT triggers for actions specified, whether or
+ not they actually occur. This is similar to the behavior of an
+ <command>UPDATE</command> statement that modifies no rows.
+ </para>
+ </listitem>
+ </orderedlist>
+ In summary, statement triggers for an event type (say, INSERT) will
+ be fired whenever we <emphasis>specify</emphasis> an action of that kind. Row-level
+ triggers will fire only for the one event type <emphasis>activated</emphasis>.
+ So a <command>MERGE</command> might fire statement triggers for both
+ <command>UPDATE</command> and <command>INSERT</command>, even though only
+ <command>UPDATE</command> row triggers were fired.
+ </para>
+
+ <para>
+ You should ensure that the join produces at most one candidate change row
+ for each target row. In other words, a target row shouldn't join to more
+ than one data source row. If it does, then only one of the candidate change
+ rows will be used to modify the target row, later attempts to modify will
+ cause an error. This can also occur if row triggers make changes to the
+ target table which are then subsequently modified by <command>MERGE</command>.
+ If the repeated action is an <command>INSERT</command> this will
+ cause a uniqueness violation while a repeated <command>UPDATE</command> or
+ <command>DELETE</command> will cause a cardinality violation; the latter behavior
+ is required by the <acronym>SQL</acronym> Standard. This differs from
+ historical <productname>PostgreSQL</productname> behavior of joins in
+ <command>UPDATE</command> and <command>DELETE</command> statements where second and
+ subsequent attempts to modify are simply ignored.
+ </para>
+
+ <para>
+ If a <literal>WHEN</literal> clause omits an <literal>AND</literal> clause it becomes
+ the final reachable clause of that kind (<literal>MATCHED</literal> or
+ <literal>NOT MATCHED</literal>). If a later <literal>WHEN</literal> clause of that kind
+ is specified it would be provably unreachable and an error is raised.
+ If a final reachable clause is omitted it is possible that no action
+ will be taken for a candidate change row.
+ </para>
+
+ </refsect1>
+ <refsect1>
+ <title>Notes</title>
+
+ <para>
+ The order in which rows are generated from the data source is indeterminate
+ by default. A <replaceable class="parameter">source_query</replaceable>
+ can be used to specify a consistent ordering, if required, which might be
+ needed to avoid deadlocks between concurrent transactions.
+ </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>
+
+ <tip>
+ <para>
+ You may also wish to consider using <command>INSERT ... ON CONFLICT</command> as an
+ alternative statement which offers the ability to run an <command>UPDATE</command>
+ if a concurrent <command>INSERT</command> occurs. There are a variety of
+ differences and restrictions between the two statement types and they are not
+ interchangeable.
+ </para>
+ </tip>
+ </refsect1>
+
+ <refsect1>
+ <title>Examples</title>
+
+ <para>
+ Perform maintenance on CustomerAccounts based upon new Transactions.
+
+<programlisting>
+MERGE CustomerAccount CA
+USING RecentTransactions T
+ON T.CustomerId = CA.CustomerId
+WHEN MATCHED THEN
+ UPDATE SET Balance = Balance + TransactionValue
+WHEN NOT MATCHED THEN
+ INSERT (CustomerId, Balance)
+ VALUES (T.CustomerId, T.TransactionValue);
+</programlisting>
+
+ notice that this would be exactly equivalent to the following
+ statement because the <literal>MATCHED</literal> result does not change
+ during execution
+
+<programlisting>
+MERGE CustomerAccount CA
+USING (Select CustomerId, TransactionValue From RecentTransactions) AS T
+ON CA.CustomerId = T.CustomerId
+WHEN NOT MATCHED THEN
+ INSERT (CustomerId, Balance)
+ VALUES (T.CustomerId, T.TransactionValue)
+WHEN MATCHED THEN
+ UPDATE SET Balance = Balance + TransactionValue;
+</programlisting>
+ </para>
+
+ <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.
+<programlisting>
+MERGE INTO wines w
+USING wine_stock_changes s
+ON s.winename = w.winename
+WHEN NOT MATCHED AND s.stock_delta > 0 THEN
+ INSERT VALUES(s.winename, s.stock_delta)
+WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN
+ UPDATE SET stock = w.stock + s.stock_delta;
+WHEN MATCHED THEN
+ DELETE;
+</programlisting>
+
+ The wine_stock_changes table might be, for example, a temporary table
+ recently loaded into the database.
+ </para>
+
+ </refsect1>
+
+ <refsect1>
+ <title>Compatibility</title>
+ <para>
+ This command conforms to the <acronym>SQL</acronym> standard.
+ </para>
+ <para>
+ The DO NOTHING action is an extension to the <acronym>SQL</acronym> standard.
+ </para>
+ </refsect1>
+</refentry>
diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml
index d27fb414f7..ef2270c467 100644
--- a/doc/src/sgml/reference.sgml
+++ b/doc/src/sgml/reference.sgml
@@ -186,6 +186,7 @@
&listen;
&load;
&lock;
+ &merge;
&move;
¬ify;
&prepare;
diff --git a/doc/src/sgml/trigger.sgml b/doc/src/sgml/trigger.sgml
index c43dbc9786..ac662bc64d 100644
--- a/doc/src/sgml/trigger.sgml
+++ b/doc/src/sgml/trigger.sgml
@@ -182,6 +182,26 @@
will be fired.
</para>
+ <para>
+ No separate triggers are defined for <command>MERGE</command>. Instead,
+ statement-level or row-level <command>UPDATE</command>,
+ <command>DELETE</command> and <command>INSERT</command> triggers are fired
+ depedning on what actions are specified in the <command>MERGE</command> query
+ and what actions are activated.
+ </para>
+
+ <para>
+ While running a <command>MERGE</command> command, statement-level
+ <literal>BEFORE</literal> and <literal>AFTER</literal> triggers are fired for
+ specific actions specified in the <command>MERGE</command>, irrespective of
+ whether the action is finally activated or not. This is same as
+ an <command>UPDATE</command> statement that updates no rows, yet
+ statement-level triggers are fired. The row-level triggers are fired only
+ when a row is actually updated, inserted or deleted. So it's perfectly legal
+ that while statement-level triggers are fired for certain type of action, no
+ row-level triggers are fired for the same kind of action.
+ </para>
+
<para>
Trigger functions invoked by per-statement triggers should always
return <symbol>NULL</symbol>. Trigger functions invoked by per-row
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index c08ab14c02..dec97a7328 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -3241,6 +3241,7 @@ l1:
result == HeapTupleUpdated ||
result == HeapTupleBeingUpdated);
Assert(!(tp.t_data->t_infomask & HEAP_XMAX_INVALID));
+ hufd->result = result;
hufd->ctid = tp.t_data->t_ctid;
hufd->xmax = HeapTupleHeaderGetUpdateXid(tp.t_data);
if (result == HeapTupleSelfUpdated)
@@ -3882,6 +3883,7 @@ l2:
result == HeapTupleUpdated ||
result == HeapTupleBeingUpdated);
Assert(!(oldtup.t_data->t_infomask & HEAP_XMAX_INVALID));
+ hufd->result = result;
hufd->ctid = oldtup.t_data->t_ctid;
hufd->xmax = HeapTupleHeaderGetUpdateXid(oldtup.t_data);
if (result == HeapTupleSelfUpdated)
@@ -5086,6 +5088,7 @@ failed:
Assert(result == HeapTupleSelfUpdated || result == HeapTupleUpdated ||
result == HeapTupleWouldBlock);
Assert(!(tuple->t_data->t_infomask & HEAP_XMAX_INVALID));
+ hufd->result = result;
hufd->ctid = tuple->t_data->t_ctid;
hufd->xmax = HeapTupleHeaderGetUpdateXid(tuple->t_data);
if (result == HeapTupleSelfUpdated)
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 20d61f3780..9612c135da 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -229,9 +229,9 @@ F311 Schema definition statement 02 CREATE TABLE for persistent base tables YES
F311 Schema definition statement 03 CREATE VIEW YES
F311 Schema definition statement 04 CREATE VIEW: WITH CHECK OPTION YES
F311 Schema definition statement 05 GRANT statement YES
-F312 MERGE statement NO consider INSERT ... ON CONFLICT DO UPDATE
-F313 Enhanced MERGE statement NO
-F314 MERGE statement with DELETE branch NO
+F312 MERGE statement YES consider INSERT ... ON CONFLICT DO UPDATE
+F313 Enhanced MERGE statement YES
+F314 MERGE statement with DELETE branch YES
F321 User authorization YES
F341 Usage tables NO no ROUTINE_*_USAGE tables
F361 Subprogram support YES
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c38d178cd9..dc2f727d21 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -887,6 +887,9 @@ ExplainNode(PlanState *planstate, List *ancestors,
case CMD_DELETE:
pname = operation = "Delete";
break;
+ case CMD_MERGE:
+ pname = operation = "Merge";
+ break;
default:
pname = "???";
break;
@@ -2948,6 +2951,10 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
operation = "Delete";
foperation = "Foreign Delete";
break;
+ case CMD_MERGE:
+ operation = "Merge";
+ foperation = "Foreign Merge";
+ break;
default:
operation = "???";
foperation = "Foreign ???";
@@ -3070,6 +3077,29 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
other_path, 0, es);
}
}
+ else if (node->operation == CMD_MERGE)
+ {
+ /* EXPLAIN ANALYZE display of actual outcome for each tuple proposed */
+ if (es->analyze && mtstate->ps.instrument)
+ {
+ double total;
+ double insert_path;
+ double update_path;
+ double delete_path;
+
+ InstrEndLoop(mtstate->mt_plans[0]->instrument);
+
+ /* count the number of source rows */
+ total = mtstate->mt_plans[0]->instrument->ntuples;
+ update_path = mtstate->ps.instrument->nfiltered1;
+ delete_path = mtstate->ps.instrument->nfiltered2;
+ insert_path = total - update_path - delete_path;
+
+ ExplainPropertyFloat("Tuples Inserted", NULL, insert_path, 0, es);
+ ExplainPropertyFloat("Tuples Updated", NULL, update_path, 0, es);
+ ExplainPropertyFloat("Tuples Deleted", NULL, delete_path, 0, es);
+ }
+ }
if (labeltargets)
ExplainCloseGroup("Target Tables", "Target Tables", false, es);
diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index b945b1556a..c3610b1874 100644
--- a/src/backend/commands/prepare.c
+++ b/src/backend/commands/prepare.c
@@ -151,6 +151,7 @@ PrepareQuery(PrepareStmt *stmt, const char *queryString,
case CMD_INSERT:
case CMD_UPDATE:
case CMD_DELETE:
+ case CMD_MERGE:
/* OK */
break;
default:
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index fbd176b5d0..b2977bed61 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -84,7 +84,8 @@ static HeapTuple GetTupleForTrigger(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tid,
LockTupleMode lockmode,
- TupleTableSlot **newSlot);
+ TupleTableSlot **newSlot,
+ HeapUpdateFailureData *hufdp);
static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
Trigger *trigger, TriggerEvent event,
Bitmapset *modifiedCols,
@@ -2503,7 +2504,8 @@ bool
ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
- HeapTuple fdw_trigtuple)
+ HeapTuple fdw_trigtuple,
+ HeapUpdateFailureData *hufdp)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
bool result = true;
@@ -2517,7 +2519,7 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
if (fdw_trigtuple == NULL)
{
trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- LockTupleExclusive, &newSlot);
+ LockTupleExclusive, &newSlot, hufdp);
if (trigtuple == NULL)
return false;
}
@@ -2588,6 +2590,7 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
relinfo,
tupleid,
LockTupleExclusive,
+ NULL,
NULL);
else
trigtuple = fdw_trigtuple;
@@ -2725,7 +2728,8 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TupleTableSlot *slot)
+ TupleTableSlot *slot,
+ HeapUpdateFailureData *hufdp)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
HeapTuple slottuple = ExecMaterializeSlot(slot);
@@ -2746,7 +2750,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
{
/* get a copy of the on-disk tuple we are planning to update */
trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
- lockmode, &newSlot);
+ lockmode, &newSlot, hufdp);
if (trigtuple == NULL)
return NULL; /* cancel the update action */
}
@@ -2866,6 +2870,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
relinfo,
tupleid,
LockTupleExclusive,
+ NULL,
NULL);
else
trigtuple = fdw_trigtuple;
@@ -3014,7 +3019,8 @@ GetTupleForTrigger(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tid,
LockTupleMode lockmode,
- TupleTableSlot **newSlot)
+ TupleTableSlot **newSlot,
+ HeapUpdateFailureData *hufdp)
{
Relation relation = relinfo->ri_RelationDesc;
HeapTupleData tuple;
@@ -3040,6 +3046,11 @@ ltrmark:;
estate->es_output_cid,
lockmode, LockWaitBlock,
false, &buffer, &hufd);
+
+ /* Let the caller know about failure reason, if any. */
+ if (hufdp)
+ *hufdp = hufd;
+
switch (test)
{
case HeapTupleSelfUpdated:
@@ -3075,11 +3086,23 @@ ltrmark:;
{
/* it was updated, so look at the updated version */
TupleTableSlot *epqslot;
+ Index rti;
+
+ /*
+ * If we're running MERGE then we must install the
+ * new tuple in the slot of the underlying join query and
+ * not the result relation itself. If the join does not
+ * yeild any tuple, the caller will take the necessary
+ * action.
+ */
+ rti = relinfo->ri_mergeTargetRTI > 0 ?
+ relinfo->ri_mergeTargetRTI :
+ relinfo->ri_RangeTableIndex;
epqslot = EvalPlanQual(estate,
epqstate,
relation,
- relinfo->ri_RangeTableIndex,
+ rti,
lockmode,
&hufd.ctid,
hufd.xmax);
@@ -3602,8 +3625,14 @@ struct AfterTriggersTableData
bool before_trig_done; /* did we already queue BS triggers? */
bool after_trig_done; /* did we already queue AS triggers? */
AfterTriggerEventList after_trig_events; /* if so, saved list pointer */
- Tuplestorestate *old_tuplestore; /* "old" transition table, if any */
- Tuplestorestate *new_tuplestore; /* "new" transition table, if any */
+ /* "old" transition table for UPDATE, if any */
+ Tuplestorestate *old_upd_tuplestore;
+ /* "new" transition table for UPDATE, if any */
+ Tuplestorestate *new_upd_tuplestore;
+ /* "old" transition table for DELETE, if any */
+ Tuplestorestate *old_del_tuplestore;
+ /* "new" transition table INSERT, if any */
+ Tuplestorestate *new_ins_tuplestore;
};
static AfterTriggersData afterTriggers;
@@ -4070,13 +4099,19 @@ AfterTriggerExecute(AfterTriggerEvent event,
{
if (LocTriggerData.tg_trigger->tgoldtable)
{
- LocTriggerData.tg_oldtable = evtshared->ats_table->old_tuplestore;
+ if (TRIGGER_FIRED_BY_UPDATE(evtshared->ats_event))
+ LocTriggerData.tg_oldtable = evtshared->ats_table->old_upd_tuplestore;
+ else
+ LocTriggerData.tg_oldtable = evtshared->ats_table->old_del_tuplestore;
evtshared->ats_table->closed = true;
}
if (LocTriggerData.tg_trigger->tgnewtable)
{
- LocTriggerData.tg_newtable = evtshared->ats_table->new_tuplestore;
+ if (TRIGGER_FIRED_BY_INSERT(evtshared->ats_event))
+ LocTriggerData.tg_newtable = evtshared->ats_table->new_ins_tuplestore;
+ else
+ LocTriggerData.tg_newtable = evtshared->ats_table->new_upd_tuplestore;
evtshared->ats_table->closed = true;
}
}
@@ -4411,8 +4446,10 @@ TransitionCaptureState *
MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType)
{
TransitionCaptureState *state;
- bool need_old,
- need_new;
+ bool need_old_upd,
+ need_new_upd,
+ need_old_del,
+ need_new_ins;
AfterTriggersTableData *table;
MemoryContext oldcxt;
ResourceOwner saveResourceOwner;
@@ -4424,23 +4461,31 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType)
switch (cmdType)
{
case CMD_INSERT:
- need_old = false;
- need_new = trigdesc->trig_insert_new_table;
+ need_old_upd = need_old_del = need_new_upd = false;
+ need_new_ins = trigdesc->trig_insert_new_table;
break;
case CMD_UPDATE:
- need_old = trigdesc->trig_update_old_table;
- need_new = trigdesc->trig_update_new_table;
+ need_old_upd = trigdesc->trig_update_old_table;
+ need_new_upd = trigdesc->trig_update_new_table;
+ need_old_del = need_new_ins = false;
break;
case CMD_DELETE:
- need_old = trigdesc->trig_delete_old_table;
- need_new = false;
+ need_old_del = trigdesc->trig_delete_old_table;
+ need_old_upd = need_new_upd = need_new_ins = false;
+ break;
+ case CMD_MERGE:
+ need_old_upd = trigdesc->trig_update_old_table;
+ need_new_upd = trigdesc->trig_update_new_table;
+ need_old_del = trigdesc->trig_delete_old_table;
+ need_new_ins = trigdesc->trig_insert_new_table;
break;
default:
elog(ERROR, "unexpected CmdType: %d", (int) cmdType);
- need_old = need_new = false; /* keep compiler quiet */
+ /* keep compiler quiet */
+ need_old_upd = need_new_upd = need_old_del = need_new_ins = false;
break;
}
- if (!need_old && !need_new)
+ if (!need_old_upd && !need_new_upd && !need_new_ins && !need_old_del)
return NULL;
/* Check state, like AfterTriggerSaveEvent. */
@@ -4470,10 +4515,14 @@ MakeTransitionCaptureState(TriggerDesc *trigdesc, Oid relid, CmdType cmdType)
saveResourceOwner = CurrentResourceOwner;
CurrentResourceOwner = CurTransactionResourceOwner;
- if (need_old && table->old_tuplestore == NULL)
- table->old_tuplestore = tuplestore_begin_heap(false, false, work_mem);
- if (need_new && table->new_tuplestore == NULL)
- table->new_tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ if (need_old_upd && table->old_upd_tuplestore == NULL)
+ table->old_upd_tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ if (need_new_upd && table->new_upd_tuplestore == NULL)
+ table->new_upd_tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ if (need_old_del && table->old_del_tuplestore == NULL)
+ table->old_del_tuplestore = tuplestore_begin_heap(false, false, work_mem);
+ if (need_new_ins && table->new_ins_tuplestore == NULL)
+ table->new_ins_tuplestore = tuplestore_begin_heap(false, false, work_mem);
CurrentResourceOwner = saveResourceOwner;
MemoryContextSwitchTo(oldcxt);
@@ -4662,12 +4711,20 @@ AfterTriggerFreeQuery(AfterTriggersQueryData *qs)
{
AfterTriggersTableData *table = (AfterTriggersTableData *) lfirst(lc);
- ts = table->old_tuplestore;
- table->old_tuplestore = NULL;
+ ts = table->old_upd_tuplestore;
+ table->old_upd_tuplestore = NULL;
if (ts)
tuplestore_end(ts);
- ts = table->new_tuplestore;
- table->new_tuplestore = NULL;
+ ts = table->new_upd_tuplestore;
+ table->new_upd_tuplestore = NULL;
+ if (ts)
+ tuplestore_end(ts);
+ ts = table->old_del_tuplestore;
+ table->old_del_tuplestore = NULL;
+ if (ts)
+ tuplestore_end(ts);
+ ts = table->new_ins_tuplestore;
+ table->new_ins_tuplestore = NULL;
if (ts)
tuplestore_end(ts);
}
@@ -5489,12 +5546,11 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
newtup == NULL));
if (oldtup != NULL &&
- ((event == TRIGGER_EVENT_DELETE && delete_old_table) ||
- (event == TRIGGER_EVENT_UPDATE && update_old_table)))
+ (event == TRIGGER_EVENT_DELETE && delete_old_table))
{
Tuplestorestate *old_tuplestore;
- old_tuplestore = transition_capture->tcs_private->old_tuplestore;
+ old_tuplestore = transition_capture->tcs_private->old_del_tuplestore;
if (map != NULL)
{
@@ -5506,13 +5562,48 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
else
tuplestore_puttuple(old_tuplestore, oldtup);
}
+ if (oldtup != NULL &&
+ (event == TRIGGER_EVENT_UPDATE && update_old_table))
+ {
+ Tuplestorestate *old_tuplestore;
+
+ old_tuplestore = transition_capture->tcs_private->old_upd_tuplestore;
+
+ if (map != NULL)
+ {
+ HeapTuple converted = do_convert_tuple(oldtup, map);
+
+ tuplestore_puttuple(old_tuplestore, converted);
+ pfree(converted);
+ }
+ else
+ tuplestore_puttuple(old_tuplestore, oldtup);
+ }
+ if (newtup != NULL &&
+ (event == TRIGGER_EVENT_INSERT && insert_new_table))
+ {
+ Tuplestorestate *new_tuplestore;
+
+ new_tuplestore = transition_capture->tcs_private->new_ins_tuplestore;
+
+ if (original_insert_tuple != NULL)
+ tuplestore_puttuple(new_tuplestore, original_insert_tuple);
+ else if (map != NULL)
+ {
+ HeapTuple converted = do_convert_tuple(newtup, map);
+
+ tuplestore_puttuple(new_tuplestore, converted);
+ pfree(converted);
+ }
+ else
+ tuplestore_puttuple(new_tuplestore, newtup);
+ }
if (newtup != NULL &&
- ((event == TRIGGER_EVENT_INSERT && insert_new_table) ||
- (event == TRIGGER_EVENT_UPDATE && update_new_table)))
+ (event == TRIGGER_EVENT_UPDATE && update_new_table))
{
Tuplestorestate *new_tuplestore;
- new_tuplestore = transition_capture->tcs_private->new_tuplestore;
+ new_tuplestore = transition_capture->tcs_private->new_upd_tuplestore;
if (original_insert_tuple != NULL)
tuplestore_puttuple(new_tuplestore, original_insert_tuple);
diff --git a/src/backend/executor/Makefile b/src/backend/executor/Makefile
index cc09895fa5..68675f9796 100644
--- a/src/backend/executor/Makefile
+++ b/src/backend/executor/Makefile
@@ -22,7 +22,7 @@ OBJS = execAmi.o execCurrent.o execExpr.o execExprInterp.o \
nodeCustom.o nodeFunctionscan.o nodeGather.o \
nodeHash.o nodeHashjoin.o nodeIndexscan.o nodeIndexonlyscan.o \
nodeLimit.o nodeLockRows.o nodeGatherMerge.o \
- nodeMaterial.o nodeMergeAppend.o nodeMergejoin.o nodeModifyTable.o \
+ nodeMaterial.o nodeMergeAppend.o nodeMergejoin.o nodeMerge.o nodeModifyTable.o \
nodeNestloop.o nodeProjectSet.o nodeRecursiveunion.o nodeResult.o \
nodeSamplescan.o nodeSeqscan.o nodeSetOp.o nodeSort.o nodeUnique.o \
nodeValuesscan.o \
diff --git a/src/backend/executor/README b/src/backend/executor/README
index 0d7cd552eb..05769772b7 100644
--- a/src/backend/executor/README
+++ b/src/backend/executor/README
@@ -37,6 +37,16 @@ the plan tree returns the computed tuples to be updated, plus a "junk"
one. For DELETE, the plan tree need only deliver a CTID column, and the
ModifyTable node visits each of those rows and marks the row deleted.
+MERGE runs one generic plan that returns candidate target rows. Each row
+consists of a super-row that contains all the columns needed by any of the
+individual actions, plus a CTID and a TABLEOID junk columns. The CTID column is
+required to know if a matching target row was found or not and the TABLEOID
+column is needed to find the underlying target partition, in case when the
+target table is a partition table. If the CTID column is set we attempt to
+activate WHEN MATCHED actions, or if it is NULL then we will attempt to
+activate WHEN NOT MATCHED actions. Once we know which action is activated we
+form the final result row and apply only those changes.
+
XXX a great deal more documentation needs to be written here...
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 91ba939bdc..3c67a802e4 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -232,6 +232,7 @@ standard_ExecutorStart(QueryDesc *queryDesc, int eflags)
case CMD_INSERT:
case CMD_DELETE:
case CMD_UPDATE:
+ case CMD_MERGE:
estate->es_output_cid = GetCurrentCommandId(true);
break;
@@ -1347,6 +1348,8 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
resultRelInfo->ri_junkFilter = NULL;
resultRelInfo->ri_projectReturning = NULL;
+ resultRelInfo->ri_mergeState = (MergeState *) palloc0(sizeof (MergeState));
+
/*
* Partition constraint, which also includes the partition constraint of
* all the ancestors that are partitions. Note that it will be checked
@@ -2195,6 +2198,19 @@ ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
errmsg("new row violates row-level security policy for table \"%s\"",
wco->relname)));
break;
+ case WCO_RLS_MERGE_UPDATE_CHECK:
+ case WCO_RLS_MERGE_DELETE_CHECK:
+ if (wco->polname != NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("target row violates row-level security policy \"%s\" (USING expression) for table \"%s\"",
+ wco->polname, wco->relname)));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("target row violates row-level security policy (USING expression) for table \"%s\"",
+ wco->relname)));
+ break;
case WCO_RLS_CONFLICT_CHECK:
if (wco->polname != NULL)
ereport(ERROR,
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 4147121575..706236cc73 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -64,6 +64,8 @@ ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel)
ResultRelInfo *update_rri = NULL;
int num_update_rri = 0,
update_rri_index = 0;
+ bool is_update = false;
+ bool is_merge = false;
PartitionTupleRouting *proute;
int nparts;
@@ -85,6 +87,11 @@ ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel)
/* Set up details specific to the type of tuple routing we are doing. */
if (mtstate && mtstate->operation == CMD_UPDATE)
+ is_update = true;
+ else if (mtstate && mtstate->operation == CMD_MERGE)
+ is_merge = true;
+
+ if (is_update)
{
ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
@@ -93,7 +100,11 @@ ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel)
proute->subplan_partition_offsets =
palloc(num_update_rri * sizeof(int));
proute->num_subplan_partition_offsets = num_update_rri;
+ }
+
+ if (is_update || is_merge)
+ {
/*
* We need an additional tuple slot for storing transient tuples that
* are converted to the root table descriptor.
@@ -297,6 +308,25 @@ ExecFindPartition(ResultRelInfo *resultRelInfo, PartitionDispatch *pd,
return result;
}
+/*
+ * Given OID of the partition leaf, return the index of the leaf in the
+ * partition hierarchy.
+ */
+int
+ExecFindPartitionByOid(PartitionTupleRouting *proute, Oid partoid)
+{
+ int i;
+
+ for (i = 0; i < proute->num_partitions; i++)
+ {
+ if (proute->partition_oids[i] == partoid)
+ break;
+ }
+
+ Assert(i < proute->num_partitions);
+ return i;
+}
+
/*
* ExecInitPartitionInfo
* Initialize ResultRelInfo and other information for a partition if not
@@ -335,6 +365,8 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
rootrel,
estate->es_instrument);
+ leaf_part_rri->ri_PartitionLeafIndex = partidx;
+
/*
* Verify result relation is a valid target for an INSERT. An UPDATE of a
* partition-key becomes a DELETE+INSERT operation, so this check is still
@@ -487,6 +519,100 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
RelationGetDescr(partrel),
gettext_noop("could not convert row type"));
+ /*
+ * Initialize information about this partition that's needed to handle
+ * MERGE.
+ */
+ if (node && node->operation == CMD_MERGE)
+ {
+ TupleDesc partrelDesc = RelationGetDescr(partrel);
+ TupleConversionMap *map = proute->parent_child_tupconv_maps[partidx];
+ int firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+ Relation firstResultRel = mtstate->resultRelInfo[0].ri_RelationDesc;
+
+ /*
+ * If the root parent and partition have the same tuple
+ * descriptor, just reuse the original MERGE state for partition.
+ */
+ if (map == NULL)
+ {
+ leaf_part_rri->ri_mergeState = resultRelInfo->ri_mergeState;
+ }
+ else
+ {
+ /* Convert expressions contain partition's attnos. */
+ List *conv_tl, *conv_qual;
+ ListCell *l;
+ List *matchedActionStates = NIL;
+ List *notMatchedActionStates = NIL;
+
+ leaf_part_rri->ri_mergeState->mergeSlot =
+ ExecInitExtraTupleSlot(estate, NULL);
+
+ foreach (l, node->mergeActionList)
+ {
+ MergeAction *action = lfirst_node(MergeAction, l);
+ MergeActionState *action_state = makeNode(MergeActionState);
+ TupleDesc tupDesc;
+ ExprContext *econtext;
+
+ action_state->matched = action->matched;
+ action_state->commandType = action->commandType;
+
+ conv_qual = (List *) action->qual;
+ conv_qual = map_partition_varattnos(conv_qual,
+ firstVarno, partrel,
+ firstResultRel, NULL);
+
+ action_state->whenqual = ExecInitQual(conv_qual, &mtstate->ps);
+
+ conv_tl = (List *) action->targetList;
+ conv_tl = map_partition_varattnos(conv_tl,
+ firstVarno, partrel,
+ firstResultRel, NULL);
+
+ conv_tl = adjust_and_expand_partition_tlist(
+ RelationGetDescr(firstResultRel),
+ RelationGetDescr(partrel),
+ RelationGetRelationName(partrel),
+ firstVarno,
+ conv_tl);
+
+ tupDesc = ExecTypeFromTL(conv_tl, partrelDesc->tdhasoid);
+ action_state->tupDesc = tupDesc;
+
+ /* build action projection state */
+ econtext = mtstate->ps.ps_ExprContext;
+ ExecSetSlotDescriptor(leaf_part_rri->ri_mergeState->mergeSlot,
+ action_state->tupDesc);
+ action_state->proj =
+ ExecBuildProjectionInfo(conv_tl, econtext,
+ leaf_part_rri->ri_mergeState->mergeSlot,
+ &mtstate->ps,
+ partrelDesc);
+
+ if (action_state->matched)
+ matchedActionStates =
+ lappend(matchedActionStates, action_state);
+ else
+ notMatchedActionStates =
+ lappend(notMatchedActionStates, action_state);
+ }
+ leaf_part_rri->ri_mergeState->matchedActionStates =
+ matchedActionStates;
+ leaf_part_rri->ri_mergeState->notMatchedActionStates =
+ notMatchedActionStates;
+ }
+
+ /*
+ * get_partition_dispatch_recurse() and expand_partitioned_rtentry()
+ * fetch the leaf OIDs in the same order. So we can safely derive the
+ * index of the merge target relation corresponding to this partition
+ * by simply adding partidx + 1 to the root's merge target relation.
+ */
+ leaf_part_rri->ri_mergeTargetRTI = node->mergeTargetRelation +
+ partidx + 1;
+ }
MemoryContextSwitchTo(oldContext);
return leaf_part_rri;
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 32891abbdf..971f92a938 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -454,7 +454,7 @@ ExecSimpleRelationUpdate(EState *estate, EPQState *epqstate,
{
slot = ExecBRUpdateTriggers(estate, epqstate, resultRelInfo,
&searchslot->tts_tuple->t_self,
- NULL, slot);
+ NULL, slot, NULL);
if (slot == NULL) /* "do nothing" */
skip_tuple = true;
@@ -515,7 +515,7 @@ ExecSimpleRelationDelete(EState *estate, EPQState *epqstate,
{
skip_tuple = !ExecBRDeleteTriggers(estate, epqstate, resultRelInfo,
&searchslot->tts_tuple->t_self,
- NULL);
+ NULL, NULL);
}
if (!skip_tuple)
diff --git a/src/backend/executor/nodeMerge.c b/src/backend/executor/nodeMerge.c
new file mode 100644
index 0000000000..fed10db520
--- /dev/null
+++ b/src/backend/executor/nodeMerge.c
@@ -0,0 +1,565 @@
+/*-------------------------------------------------------------------------
+ *
+ * nodeMerge.c
+ * routines to handle Merge nodes.
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/executor/nodeMerge.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "commands/trigger.h"
+#include "executor/execPartition.h"
+#include "executor/executor.h"
+#include "executor/nodeModifyTable.h"
+#include "executor/nodeMerge.h"
+#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "storage/bufmgr.h"
+#include "storage/lmgr.h"
+#include "utils/builtins.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/tqual.h"
+
+
+/*
+ * Check and execute the first qualifying MATCHED action. The current target
+ * tuple is identified by tupleid.
+ *
+ * We start from the first WHEN MATCHED action and check if the additional WHEN
+ * quals pass. If the additional 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.
+ *
+ * If we do find a qualifying action, then we attempt to execute the action. In
+ * case the tuple is concurrently updated, EvalPlanQual is run with the updated
+ * tuple to recheck the join quals. Note that the additional quals associated
+ * with individual actions are evaluated separately by the MERGE code, 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 top and
+ * again look for a qualifying action. Otherwise, we return false indicating
+ * that a NOT MATCHED action must now be executed for the current source tuple.
+ */
+static bool
+ExecMergeMatched(ModifyTableState *mtstate, EState *estate,
+ TupleTableSlot *slot, JunkFilter *junkfilter,
+ ItemPointer tupleid)
+{
+ ExprContext *econtext = mtstate->ps.ps_ExprContext;
+ bool isNull;
+ Datum datum;
+ Oid tableoid = InvalidOid;
+ List *mergeMatchedActionStates = NIL;
+ HeapUpdateFailureData hufd;
+ bool tuple_updated,
+ tuple_deleted;
+ Buffer buffer;
+ HeapTupleData tuple;
+ EPQState *epqstate = &mtstate->mt_epqstate;
+ ResultRelInfo *saved_resultRelInfo;
+ ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
+ ListCell *l;
+ TupleTableSlot *saved_slot = slot;
+
+
+ /*
+ * We always fetch the tableoid while performing MATCHED MERGE action.
+ * This is strictly not required if the target table is not a partitioned
+ * table. But we are not yet optimising for that case.
+ */
+ datum = ExecGetJunkAttribute(slot, junkfilter->jf_otherJunkAttNo,
+ &isNull);
+ Assert(!isNull);
+ tableoid = DatumGetObjectId(datum);
+
+ if (mtstate->mt_partition_tuple_routing)
+ {
+ int leaf_part_index;
+ PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
+
+ /*
+ * If we're dealing with a MATCHED tuple, then tableoid must have been
+ * set correctly. In case of partitioned table, we must now fetch the
+ * correct result relation corresponding to the child table emitting
+ * the matching target row. For normal table, there is just one result
+ * relation and it must be the one emitting the matching row.
+ */
+ leaf_part_index = ExecFindPartitionByOid(proute, tableoid);
+
+ resultRelInfo = proute->partitions[leaf_part_index];
+ if (resultRelInfo == NULL)
+ {
+ resultRelInfo = ExecInitPartitionInfo(mtstate,
+ mtstate->resultRelInfo,
+ proute, estate, leaf_part_index);
+ Assert(resultRelInfo != NULL);
+ }
+ }
+
+ /*
+ * Save the current information and work with the correct result relation.
+ */
+ saved_resultRelInfo = resultRelInfo;
+ estate->es_result_relation_info = resultRelInfo;
+
+ /*
+ * And get the correct action lists.
+ */
+ mergeMatchedActionStates =
+ resultRelInfo->ri_mergeState->matchedActionStates;
+
+ /*
+ * If there are not WHEN MATCHED actions, we are done.
+ */
+ if (mergeMatchedActionStates == NIL)
+ return true;
+
+ /*
+ * Make tuple and any needed join variables available to ExecQual and
+ * ExecProject. The target's existing tuple is installed in the scantuple.
+ * Again, this target relation's slot is required only in the case of a
+ * MATCHED tuple and UPDATE/DELETE actions.
+ */
+ ExecSetSlotDescriptor(mtstate->mt_existing,
+ resultRelInfo->ri_RelationDesc->rd_att);
+ econtext->ecxt_scantuple = mtstate->mt_existing;
+ econtext->ecxt_innertuple = slot;
+ econtext->ecxt_outertuple = NULL;
+
+lmerge_matched:;
+ slot = saved_slot;
+ buffer = InvalidBuffer;
+
+ /*
+ * UPDATE/DELETE is only invoked for matched rows. And we must have found
+ * the tupleid of the target row in that case. We fetch using SnapshotAny
+ * because we might get called again after EvalPlanQual returns us a new
+ * tuple. This tuple may not be visible to our MVCC snapshot.
+ */
+ Assert(tupleid != NULL);
+
+ tuple.t_self = *tupleid;
+ if (!heap_fetch(resultRelInfo->ri_RelationDesc, SnapshotAny, &tuple,
+ &buffer, true, NULL))
+ elog(ERROR, "Failed to fetch the target tuple");
+
+ /* Store target's existing tuple in the state's dedicated slot */
+ ExecStoreTuple(&tuple, mtstate->mt_existing, buffer, false);
+
+ foreach(l, mergeMatchedActionStates)
+ {
+ MergeActionState *action = (MergeActionState *) lfirst(l);
+
+ /*
+ * Test condition, if any
+ *
+ * In the absence of a condition we perform the action unconditionally
+ * (no need to check separately since ExecQual() will return true if
+ * there are no conditions to evaluate).
+ */
+ if (!ExecQual(action->whenqual, econtext))
+ continue;
+
+ /*
+ * Check if the existing target tuple meet the USING checks of
+ * UPDATE/DELETE RLS policies. If those checks fail, we throw an
+ * error.
+ *
+ * The WITH CHECK quals are applied in ExecUpdate() and hence we need
+ * not do anything special to handle them.
+ *
+ * NOTE: We must do this after WHEN quals are evaluated so that we
+ * check policies only when they matter.
+ */
+ if (resultRelInfo->ri_WithCheckOptions)
+ {
+ ExecWithCheckOptions(action->commandType == CMD_UPDATE ?
+ WCO_RLS_MERGE_UPDATE_CHECK : WCO_RLS_MERGE_DELETE_CHECK,
+ resultRelInfo,
+ mtstate->mt_existing,
+ mtstate->ps.state);
+ }
+
+ /* Perform stated action */
+ switch (action->commandType)
+ {
+ case CMD_UPDATE:
+
+ /*
+ * We set up the projection earlier, so all we do here is
+ * Project, no need for any other tasks prior to the
+ * ExecUpdate.
+ */
+ ExecSetSlotDescriptor(resultRelInfo->ri_mergeState->mergeSlot,
+ action->tupDesc);
+ ExecProject(action->proj);
+
+ /*
+ * We don't call ExecFilterJunk() because the projected tuple
+ * using the UPDATE action's targetlist doesn't have a junk
+ * attribute.
+ */
+ slot = ExecUpdate(mtstate, tupleid, NULL,
+ resultRelInfo->ri_mergeState->mergeSlot,
+ slot, epqstate, estate,
+ &tuple_updated, &hufd,
+ action, mtstate->canSetTag);
+ break;
+
+ case CMD_DELETE:
+ /* Nothing to Project for a DELETE action */
+ slot = ExecDelete(mtstate, tupleid, NULL,
+ slot, epqstate, estate,
+ &tuple_deleted, false, &hufd, action,
+ mtstate->canSetTag);
+
+ break;
+
+ default:
+ elog(ERROR, "unknown action in MERGE WHEN MATCHED clause");
+
+ }
+
+ /*
+ * Check for any concurrent update/delete operation which may have
+ * prevented our update/delete. We also check for situations where we
+ * might be trying to update/delete the same tuple twice.
+ */
+ if ((action->commandType == CMD_UPDATE && !tuple_updated) ||
+ (action->commandType == CMD_DELETE && !tuple_deleted))
+
+ {
+ switch (hufd.result)
+ {
+ case HeapTupleMayBeUpdated:
+ break;
+ case HeapTupleInvisible:
+
+ /*
+ * This state should never be reached since the underlying
+ * JOIN runs with a MVCC snapshot and should only return
+ * rows visible to us.
+ */
+ elog(ERROR, "unexpected invisible tuple");
+ break;
+
+ case HeapTupleSelfUpdated:
+
+ /*
+ * SQLStandard disallows this for MERGE.
+ */
+ if (TransactionIdIsCurrentTransactionId(hufd.xmax))
+ ereport(ERROR,
+ (errcode(ERRCODE_CARDINALITY_VIOLATION),
+ errmsg("MERGE command cannot affect row a second time"),
+ errhint("Ensure that not more than one source rows match any one target row")));
+ /* This shouldn't happen */
+ elog(ERROR, "attempted to update or delete invisible tuple");
+ break;
+
+ case HeapTupleUpdated:
+
+ /*
+ * The target tuple was concurrently updated/deleted by
+ * some other transaction.
+ *
+ * If the current tuple is that last tuple in the update
+ * chain, then we know that the tuple was concurrently
+ * deleted. Just return and let the caller try NOT MATCHED
+ * actions.
+ *
+ * If the current tuple was concurrently updated, then we
+ * must run the EvalPlanQual() with the new version of the
+ * tuple. If EvalPlanQual() does not return a tuple (can
+ * that happen?), then again we switch to NOT MATCHED
+ * action. If it does return a tuple and the join qual is
+ * still satified, then we just need to recheck the
+ * MATCHED actions, starting from the top, and execute the
+ * first qualifying action.
+ */
+ if (!ItemPointerEquals(tupleid, &hufd.ctid))
+ {
+ TupleTableSlot *epqslot;
+
+ /*
+ * Since we generate a JOIN query with a target table
+ * RTE different than the result relation RTE, we must
+ * pass in the RTI of the relation used in the join
+ * query and not the one from result relation.
+ */
+ epqslot = EvalPlanQual(estate,
+ epqstate,
+ resultRelInfo->ri_RelationDesc,
+ resultRelInfo->ri_mergeTargetRTI,
+ LockTupleExclusive,
+ &hufd.ctid,
+ hufd.xmax);
+
+ if (!TupIsNull(epqslot))
+ {
+ (void) ExecGetJunkAttribute(epqslot,
+ resultRelInfo->ri_junkFilter->jf_junkAttNo,
+ &isNull);
+
+ /*
+ * A valid ctid means that we are still dealing
+ * with MATCHED case. But we must retry from the
+ * start with the updated tuple to ensure that the
+ * first qualifying WHEN MATCHED action is
+ * executed.
+ *
+ * We don't use the new slot returned by
+ * EvalPlanQual because we anyways re-install the
+ * new target tuple in econtext->ecxt_scantuple
+ * before re-evaluating WHEN AND conditions and
+ * re-projecting the update targetlists. The
+ * source side tuple does not change and hence we
+ * can safely continue to use the old slot.
+ */
+ if (!isNull)
+ {
+ /*
+ * Must update *tupleid to the TID of the
+ * newer tuple found in the update chain.
+ */
+ *tupleid = hufd.ctid;
+ if (BufferIsValid(buffer))
+ ReleaseBuffer(buffer);
+ goto lmerge_matched;
+ }
+ }
+ }
+
+ /*
+ * Tell the caller about the updated TID, restore the
+ * state back and return.
+ */
+ *tupleid = hufd.ctid;
+ estate->es_result_relation_info = saved_resultRelInfo;
+ if (BufferIsValid(buffer))
+ ReleaseBuffer(buffer);
+ return false;
+
+ default:
+ break;
+
+ }
+ }
+
+ if (action->commandType == CMD_UPDATE && tuple_updated)
+ InstrCountFiltered1(&mtstate->ps, 1);
+ if (action->commandType == CMD_DELETE && tuple_deleted)
+ InstrCountFiltered2(&mtstate->ps, 1);
+
+ /*
+ * We've activated one of the WHEN clauses, so we don't search
+ * further. This is required behaviour, not an optimisation.
+ */
+ estate->es_result_relation_info = saved_resultRelInfo;
+ break;
+ }
+
+ if (BufferIsValid(buffer))
+ ReleaseBuffer(buffer);
+
+ /*
+ * Successfully executed an action or no qualifying action was found.
+ */
+ return true;
+}
+
+/*
+ * Execute the first qualifying NOT MATCHED action.
+ */
+static void
+ExecMergeNotMatched(ModifyTableState *mtstate, EState *estate,
+ TupleTableSlot *slot)
+{
+ PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
+ ExprContext *econtext = mtstate->ps.ps_ExprContext;
+ List *mergeNotMatchedActionStates = NIL;
+ ResultRelInfo *resultRelInfo;
+ ListCell *l;
+ TupleTableSlot *myslot;
+
+ /*
+ * We are dealing with NOT MATCHED tuple. Since for MERGE, partition tree
+ * is not expanded for the result relation, we continue to work with the
+ * currently active result relation, which should be of the root of the
+ * partition tree.
+ */
+ resultRelInfo = mtstate->resultRelInfo;
+
+ /*
+ * For INSERT actions, root relation's merge action is OK since the the
+ * INSERT's targetlist and the WHEN conditions can only refer to the
+ * source relation and hence it does not matter which result relation we
+ * work with.
+ */
+ mergeNotMatchedActionStates =
+ resultRelInfo->ri_mergeState->notMatchedActionStates;
+
+ /*
+ * Make source tuple available to ExecQual and ExecProject. We don't need
+ * the target tuple since the WHEN quals and the targetlist can't refer to
+ * the target columns.
+ */
+ econtext->ecxt_scantuple = NULL;
+ econtext->ecxt_innertuple = slot;
+ econtext->ecxt_outertuple = NULL;
+
+ foreach(l, mergeNotMatchedActionStates)
+ {
+ MergeActionState *action = (MergeActionState *) lfirst(l);
+
+ /*
+ * Test condition, if any
+ *
+ * In the absence of a condition we perform the action unconditionally
+ * (no need to check separately since ExecQual() will return true if
+ * there are no conditions to evaluate).
+ */
+ if (!ExecQual(action->whenqual, econtext))
+ continue;
+
+ /* Perform stated action */
+ switch (action->commandType)
+ {
+ case CMD_INSERT:
+
+ /*
+ * We set up the projection earlier, so all we do here is
+ * Project, no need for any other tasks prior to the
+ * ExecInsert.
+ */
+ ExecSetSlotDescriptor(resultRelInfo->ri_mergeState->mergeSlot,
+ action->tupDesc);
+ ExecProject(action->proj);
+
+ /*
+ * ExecPrepareTupleRouting may modify the passed-in slot. Hence
+ * pass a local reference so that action->slot is not modified.
+ */
+ myslot = resultRelInfo->ri_mergeState->mergeSlot;
+
+ /* Prepare for tuple routing if needed. */
+ if (proute)
+ myslot = ExecPrepareTupleRouting(mtstate, estate, proute,
+ resultRelInfo, myslot);
+ slot = ExecInsert(mtstate, myslot, slot,
+ estate, action,
+ mtstate->canSetTag);
+ /* Revert ExecPrepareTupleRouting's state change. */
+ if (proute)
+ estate->es_result_relation_info = resultRelInfo;
+ break;
+ case CMD_NOTHING:
+ /* Do Nothing */
+ break;
+ default:
+ elog(ERROR, "unknown action in MERGE WHEN NOT MATCHED clause");
+ }
+
+ break;
+ }
+}
+
+/*
+ * Perform MERGE.
+ */
+void
+ExecMerge(ModifyTableState *mtstate, EState *estate, TupleTableSlot *slot,
+ JunkFilter *junkfilter, ResultRelInfo *resultRelInfo)
+{
+ ItemPointer tupleid;
+ ItemPointerData tuple_ctid;
+ bool matched = false;
+ char relkind;
+ Datum datum;
+ bool isNull;
+
+ relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind;
+ Assert(relkind == RELKIND_RELATION ||
+ relkind == RELKIND_MATVIEW ||
+ relkind == RELKIND_PARTITIONED_TABLE);
+
+ /*
+ * We run a JOIN between the target relation and the source relation to
+ * find a set of candidate source rows that has matching row in the target
+ * table and a set of candidate source rows that does not have matching
+ * row in the target table. If the join returns us a tuple with target
+ * relation's tid set, that implies that the join found a matching row for
+ * the given source tuple. This case triggers the WHEN MATCHED clause of
+ * the MERGE. Whereas a NULL in the target relation's ctid column
+ * indicates a NOT MATCHED case.
+ */
+ datum = ExecGetJunkAttribute(slot, junkfilter->jf_junkAttNo, &isNull);
+
+ if (!isNull)
+ {
+ matched = true;
+ tupleid = (ItemPointer) DatumGetPointer(datum);
+ tuple_ctid = *tupleid; /* be sure we don't free ctid!! */
+ tupleid = &tuple_ctid;
+ }
+ else
+ {
+ matched = false;
+ tupleid = NULL; /* we don't need it for INSERT actions */
+ }
+
+ /*
+ * If we are dealing with a WHEN MATCHED case, we look at the given WHEN
+ * MATCHED actions in an order and execute the first action which also
+ * satisfies the additional WHEN MATCHED AND quals. If an action without
+ * any additional quals is found, that action is executed.
+ *
+ * Similarly, if we are dealing with WHEN NOT MATCHED case, we look at the
+ * given WHEN NOT MATCHED actions in an ordr and execute the first
+ * qualifying action.
+ *
+ * Things get interesting in case of concurrent update/delete of the
+ * target tuple. Such concurrent update/delete is detected while we are
+ * executing a WHEN MATCHED action.
+ *
+ * A concurrent update for example can:
+ *
+ * 1. modify the target tuple so that it no longer satisfies the
+ * additional quals attached to the current WHEN MATCHED action OR 2.
+ * modify the target tuple so that the join quals no longer pass and hence
+ * the source tuple no longer has a match.
+ *
+ * In the first case, we are still dealing with a WHEN MATCHED case, but
+ * we should recheck the list of WHEN MATCHED actions and choose the first
+ * one that satisfies the new target tuple. In the second case, since the
+ * source tuple no longer matches the target tuple, we now instead find a
+ * qualifying WHEN NOT MATCHED action and execute that.
+ *
+ * A concurrent delete, changes a WHEN MATCHED case to WHEN NOT MATCHED.
+ *
+ * ExecMergeMatched takes care of following the update chain and
+ * re-finding the qualifying WHEN MATCHED action, as long as the updated
+ * target tuple still satisfies the join quals i.e. it still remains a
+ * WHEN MATCHED case. If the tuple gets deleted or the join quals fail, it
+ * returns and we try ExecMergeNotMatched. Given that ExecMergeMatched
+ * always make progress by following the update chain and we never switch
+ * from ExecMergeNotMatched to ExecMergeMatched, there is no risk of a
+ * livelock.
+ */
+ if (!matched ||
+ !ExecMergeMatched(mtstate, estate, slot, junkfilter, tupleid))
+ ExecMergeNotMatched(mtstate, estate, slot);
+}
+
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 4fa2d7265f..f75d54d17f 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -42,6 +42,7 @@
#include "commands/trigger.h"
#include "executor/execPartition.h"
#include "executor/executor.h"
+#include "executor/nodeMerge.h"
#include "executor/nodeModifyTable.h"
#include "foreign/fdwapi.h"
#include "miscadmin.h"
@@ -62,11 +63,6 @@ static bool ExecOnConflictUpdate(ModifyTableState *mtstate,
EState *estate,
bool canSetTag,
TupleTableSlot **returning);
-static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
- EState *estate,
- PartitionTupleRouting *proute,
- ResultRelInfo *targetRelInfo,
- TupleTableSlot *slot);
static ResultRelInfo *getTargetResultRelInfo(ModifyTableState *node);
static void ExecSetupChildParentMapForTcs(ModifyTableState *mtstate);
static void ExecSetupChildParentMapForSubplan(ModifyTableState *mtstate);
@@ -259,11 +255,12 @@ ExecCheckTIDVisible(EState *estate,
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
-static TupleTableSlot *
+extern TupleTableSlot *
ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
EState *estate,
+ MergeActionState *actionState,
bool canSetTag)
{
HeapTuple tuple;
@@ -390,9 +387,17 @@ ExecInsert(ModifyTableState *mtstate,
* partition, we should instead check UPDATE policies, because we are
* executing policies defined on the target table, and not those
* defined on the child partitions.
+ *
+ * If we're running MERGE, we refer to the action that we're executing
+ * to know if we're doing an INSERT or UPDATE to a partition table.
*/
- wco_kind = (mtstate->operation == CMD_UPDATE) ?
- WCO_RLS_UPDATE_CHECK : WCO_RLS_INSERT_CHECK;
+ if (mtstate->operation == CMD_UPDATE)
+ wco_kind = WCO_RLS_UPDATE_CHECK;
+ else if (mtstate->operation == CMD_INSERT)
+ wco_kind = WCO_RLS_INSERT_CHECK;
+ else if (mtstate->operation == CMD_MERGE)
+ wco_kind = (actionState->commandType == CMD_UPDATE) ?
+ WCO_RLS_UPDATE_CHECK : WCO_RLS_INSERT_CHECK;
/*
* ExecWithCheckOptions() will skip any WCOs which are not of the kind
@@ -617,10 +622,19 @@ ExecInsert(ModifyTableState *mtstate,
* passed to foreign table triggers; it is NULL when the foreign
* table has no relevant triggers.
*
+ * MERGE passes actionState of the action it's currently executing;
+ * regular DELETE passes NULL. This is used by ExecDelete to know if it's
+ * being called from MERGE or regular DELETE operation.
+ *
+ * If the DELETE fails because the tuple is concurrently updated/deleted
+ * by this or some other transaction, hufdp is filled with the reason as
+ * well as other important information. Currently only MERGE needs this
+ * information.
+ *
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
-static TupleTableSlot *
+TupleTableSlot *
ExecDelete(ModifyTableState *mtstate,
ItemPointer tupleid,
HeapTuple oldtuple,
@@ -629,6 +643,8 @@ ExecDelete(ModifyTableState *mtstate,
EState *estate,
bool *tupleDeleted,
bool processReturning,
+ HeapUpdateFailureData *hufdp,
+ MergeActionState *actionState,
bool canSetTag)
{
ResultRelInfo *resultRelInfo;
@@ -641,6 +657,14 @@ ExecDelete(ModifyTableState *mtstate,
if (tupleDeleted)
*tupleDeleted = false;
+ /*
+ * Initialize hufdp. Since the caller is only interested in the failure
+ * status, initialize with the state that is used to indicate successful
+ * operation.
+ */
+ if (hufdp)
+ hufdp->result = HeapTupleMayBeUpdated;
+
/*
* get information on the (current) result relation
*/
@@ -654,7 +678,7 @@ ExecDelete(ModifyTableState *mtstate,
bool dodelete;
dodelete = ExecBRDeleteTriggers(estate, epqstate, resultRelInfo,
- tupleid, oldtuple);
+ tupleid, oldtuple, hufdp);
if (!dodelete) /* "do nothing" */
return NULL;
@@ -721,6 +745,15 @@ ldelete:;
estate->es_crosscheck_snapshot,
true /* wait for commit */ ,
&hufd);
+
+ /*
+ * Copy the necessary information, if the caller has asked for it. We
+ * must do this irrespective of whether the tuple was updated or
+ * deleted.
+ */
+ if (hufdp)
+ *hufdp = hufd;
+
switch (result)
{
case HeapTupleSelfUpdated:
@@ -755,7 +788,11 @@ ldelete:;
errmsg("tuple to be updated 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.")));
- /* Else, already deleted by self; nothing to do */
+ /*
+ * Else, already deleted by self; nothing to do but inform
+ * MERGE about it anyways so that it can take necessary
+ * action.
+ */
return NULL;
case HeapTupleMayBeUpdated:
@@ -766,10 +803,20 @@ ldelete:;
ereport(ERROR,
(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
errmsg("could not serialize access due to concurrent update")));
+
if (!ItemPointerEquals(tupleid, &hufd.ctid))
{
TupleTableSlot *epqslot;
+ /*
+ * If we're executing MERGE, then the onus of running
+ * EvalPlanQual() and handling its outcome lies with the
+ * caller.
+ */
+ if (actionState != NULL)
+ return NULL;
+
+ /* Normal DELETE path. */
epqslot = EvalPlanQual(estate,
epqstate,
resultRelationDesc,
@@ -783,7 +830,12 @@ ldelete:;
goto ldelete;
}
}
- /* tuple already deleted; nothing to do */
+
+ /*
+ * tuple already deleted; nothing to do. But MERGE might want
+ * to handle it differently. We've already filled-in hufdp
+ * with sufficient information for MERGE to look at.
+ */
return NULL;
default:
@@ -911,10 +963,21 @@ ldelete:;
* foreign table triggers; it is NULL when the foreign table has
* no relevant triggers.
*
+ * MERGE passes actionState of the action it's currently executing;
+ * regular UPDATE passes NULL. This is used by ExecUpdate to know if it's
+ * being called from MERGE or regular UPDATE operation. ExecUpdate may
+ * pass this information to ExecInsert if it ends up running DELETE+INSERT
+ * for partition key updates.
+ *
+ * If the UPDATE fails because the tuple is concurrently updated/deleted
+ * by this or some other transaction, hufdp is filled with the reason as
+ * well as other important information. Currently only MERGE needs this
+ * information.
+ *
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
-static TupleTableSlot *
+extern TupleTableSlot *
ExecUpdate(ModifyTableState *mtstate,
ItemPointer tupleid,
HeapTuple oldtuple,
@@ -922,6 +985,9 @@ ExecUpdate(ModifyTableState *mtstate,
TupleTableSlot *planSlot,
EPQState *epqstate,
EState *estate,
+ bool *tuple_updated,
+ HeapUpdateFailureData *hufdp,
+ MergeActionState *actionState,
bool canSetTag)
{
HeapTuple tuple;
@@ -938,6 +1004,17 @@ ExecUpdate(ModifyTableState *mtstate,
if (IsBootstrapProcessingMode())
elog(ERROR, "cannot UPDATE during bootstrap");
+ if (tuple_updated)
+ *tuple_updated = false;
+
+ /*
+ * Initialize hufdp. Since the caller is only interested in the failure
+ * status, initialize with the state that is used to indicate successful
+ * operation.
+ */
+ if (hufdp)
+ hufdp->result = HeapTupleMayBeUpdated;
+
/*
* get the heap tuple out of the tuple table slot, making sure we have a
* writable copy
@@ -955,7 +1032,7 @@ ExecUpdate(ModifyTableState *mtstate,
resultRelInfo->ri_TrigDesc->trig_update_before_row)
{
slot = ExecBRUpdateTriggers(estate, epqstate, resultRelInfo,
- tupleid, oldtuple, slot);
+ tupleid, oldtuple, slot, hufdp);
if (slot == NULL) /* "do nothing" */
return NULL;
@@ -1067,8 +1144,9 @@ lreplace:;
* Row movement, part 1. Delete the tuple, but skip RETURNING
* processing. We want to return rows from INSERT.
*/
- ExecDelete(mtstate, tupleid, oldtuple, planSlot, epqstate, estate,
- &tuple_deleted, false, false);
+ ExecDelete(mtstate, tupleid, oldtuple, planSlot, epqstate,
+ estate, &tuple_deleted, false, hufdp, NULL,
+ false);
/*
* For some reason if DELETE didn't happen (e.g. trigger prevented
@@ -1111,9 +1189,20 @@ lreplace:;
* retrieve the one for this resultRel, we need to know the
* position of the resultRel in mtstate->resultRelInfo[].
*/
- map_index = resultRelInfo - mtstate->resultRelInfo;
- Assert(map_index >= 0 && map_index < mtstate->mt_nplans);
- tupconv_map = tupconv_map_for_subplan(mtstate, map_index);
+ if (mtstate->operation == CMD_MERGE)
+ {
+ map_index = resultRelInfo->ri_PartitionLeafIndex;
+ Assert(mtstate->rootResultRelInfo == NULL);
+ tupconv_map = TupConvMapForLeaf(proute,
+ mtstate->resultRelInfo,
+ map_index);
+ }
+ else
+ {
+ map_index = resultRelInfo - mtstate->resultRelInfo;
+ Assert(map_index >= 0 && map_index < mtstate->mt_nplans);
+ tupconv_map = tupconv_map_for_subplan(mtstate, map_index);
+ }
tuple = ConvertPartitionTupleSlot(tupconv_map,
tuple,
proute->root_tuple_slot,
@@ -1123,12 +1212,16 @@ lreplace:;
* Prepare for tuple routing, making it look like we're inserting
* into the root.
*/
- Assert(mtstate->rootResultRelInfo != NULL);
slot = ExecPrepareTupleRouting(mtstate, estate, proute,
- mtstate->rootResultRelInfo, slot);
+ getTargetResultRelInfo(mtstate),
+ slot);
ret_slot = ExecInsert(mtstate, slot, planSlot,
- estate, canSetTag);
+ estate, actionState, canSetTag);
+
+ /* Update is successful. */
+ if (tuple_updated)
+ *tuple_updated = true;
/* Revert ExecPrepareTupleRouting's node change. */
estate->es_result_relation_info = resultRelInfo;
@@ -1167,6 +1260,15 @@ lreplace:;
estate->es_crosscheck_snapshot,
true /* wait for commit */ ,
&hufd, &lockmode);
+
+ /*
+ * Copy the necessary information, if the caller has asked for it. We
+ * must do this irrespective of whether the tuple was updated or
+ * deleted.
+ */
+ if (hufdp)
+ *hufdp = hufd;
+
switch (result)
{
case HeapTupleSelfUpdated:
@@ -1211,10 +1313,20 @@ lreplace:;
ereport(ERROR,
(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
errmsg("could not serialize access due to concurrent update")));
+
if (!ItemPointerEquals(tupleid, &hufd.ctid))
{
TupleTableSlot *epqslot;
+ /*
+ * If we're executing MERGE, then the onus of running
+ * EvalPlanQual() and handling its outcome lies with the
+ * caller.
+ */
+ if (actionState != NULL)
+ return NULL;
+
+ /* Regular UPDATE path. */
epqslot = EvalPlanQual(estate,
epqstate,
resultRelationDesc,
@@ -1225,12 +1337,18 @@ lreplace:;
if (!TupIsNull(epqslot))
{
*tupleid = hufd.ctid;
+ /* Normal UPDATE path */
slot = ExecFilterJunk(resultRelInfo->ri_junkFilter, epqslot);
tuple = ExecMaterializeSlot(slot);
goto lreplace;
}
}
- /* tuple already deleted; nothing to do */
+
+ /*
+ * tuple already deleted; nothing to do. But MERGE might want
+ * to handle it differently. We've already filled-in hufdp
+ * with sufficient information for MERGE to look at.
+ */
return NULL;
default:
@@ -1259,6 +1377,9 @@ lreplace:;
estate, false, NULL, NIL);
}
+ if (tuple_updated)
+ *tuple_updated = true;
+
if (canSetTag)
(estate->es_processed)++;
@@ -1353,9 +1474,9 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
* there's no historical behavior to break.
*
* It is the user's responsibility to prevent this situation from
- * occurring. These problems are why SQL-2003 similarly specifies
- * that for SQL MERGE, an exception must be raised in the event of
- * an attempt to update the same row twice.
+ * occurring. These problems are why SQL Standard similarly
+ * specifies that for SQL MERGE, an exception must be raised in
+ * the event of an attempt to update the same row twice.
*/
if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple.t_data)))
ereport(ERROR,
@@ -1419,6 +1540,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
ExecCheckHeapTupleVisible(estate, &tuple, buffer);
/* Store target's existing tuple in the state's dedicated slot */
+ ExecSetSlotDescriptor(mtstate->mt_existing, relation->rd_att);
ExecStoreTuple(&tuple, mtstate->mt_existing, buffer, false);
/*
@@ -1477,7 +1599,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
*returning = ExecUpdate(mtstate, &tuple.t_self, NULL,
mtstate->mt_conflproj, planSlot,
&mtstate->mt_epqstate, mtstate->ps.state,
- canSetTag);
+ NULL, NULL, NULL, canSetTag);
ReleaseBuffer(buffer);
return true;
@@ -1515,6 +1637,14 @@ fireBSTriggers(ModifyTableState *node)
case CMD_DELETE:
ExecBSDeleteTriggers(node->ps.state, resultRelInfo);
break;
+ case CMD_MERGE:
+ if (node->mt_merge_subcommands & ACL_INSERT)
+ ExecBSInsertTriggers(node->ps.state, resultRelInfo);
+ if (node->mt_merge_subcommands & ACL_UPDATE)
+ ExecBSUpdateTriggers(node->ps.state, resultRelInfo);
+ if (node->mt_merge_subcommands & ACL_DELETE)
+ ExecBSDeleteTriggers(node->ps.state, resultRelInfo);
+ break;
default:
elog(ERROR, "unknown operation");
break;
@@ -1570,6 +1700,17 @@ fireASTriggers(ModifyTableState *node)
ExecASDeleteTriggers(node->ps.state, resultRelInfo,
node->mt_transition_capture);
break;
+ case CMD_MERGE:
+ if (node->mt_merge_subcommands & ACL_DELETE)
+ ExecASDeleteTriggers(node->ps.state, resultRelInfo,
+ node->mt_transition_capture);
+ if (node->mt_merge_subcommands & ACL_UPDATE)
+ ExecASUpdateTriggers(node->ps.state, resultRelInfo,
+ node->mt_transition_capture);
+ if (node->mt_merge_subcommands & ACL_INSERT)
+ ExecASInsertTriggers(node->ps.state, resultRelInfo,
+ node->mt_transition_capture);
+ break;
default:
elog(ERROR, "unknown operation");
break;
@@ -1632,7 +1773,7 @@ ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate)
*
* Returns a slot holding the tuple of the partition rowtype.
*/
-static TupleTableSlot *
+TupleTableSlot *
ExecPrepareTupleRouting(ModifyTableState *mtstate,
EState *estate,
PartitionTupleRouting *proute,
@@ -1941,6 +2082,7 @@ ExecModifyTable(PlanState *pstate)
{
/* advance to next subplan if any */
node->mt_whichplan++;
+
if (node->mt_whichplan < node->mt_nplans)
{
resultRelInfo++;
@@ -1989,6 +2131,12 @@ ExecModifyTable(PlanState *pstate)
EvalPlanQualSetSlot(&node->mt_epqstate, planSlot);
slot = planSlot;
+ if (operation == CMD_MERGE)
+ {
+ ExecMerge(node, estate, slot, junkfilter, resultRelInfo);
+ continue;
+ }
+
tupleid = NULL;
oldtuple = NULL;
if (junkfilter != NULL)
@@ -2070,19 +2218,20 @@ ExecModifyTable(PlanState *pstate)
slot = ExecPrepareTupleRouting(node, estate, proute,
resultRelInfo, slot);
slot = ExecInsert(node, slot, planSlot,
- estate, node->canSetTag);
+ estate, NULL, node->canSetTag);
/* Revert ExecPrepareTupleRouting's state change. */
if (proute)
estate->es_result_relation_info = resultRelInfo;
break;
case CMD_UPDATE:
slot = ExecUpdate(node, tupleid, oldtuple, slot, planSlot,
- &node->mt_epqstate, estate, node->canSetTag);
+ &node->mt_epqstate, estate,
+ NULL, NULL, NULL, node->canSetTag);
break;
case CMD_DELETE:
slot = ExecDelete(node, tupleid, oldtuple, planSlot,
&node->mt_epqstate, estate,
- NULL, true, node->canSetTag);
+ NULL, true, NULL, NULL, node->canSetTag);
break;
default:
elog(ERROR, "unknown operation");
@@ -2172,6 +2321,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
saved_resultRelInfo = estate->es_result_relation_info;
resultRelInfo = mtstate->resultRelInfo;
+ resultRelInfo->ri_mergeTargetRTI = node->mergeTargetRelation;
+
i = 0;
foreach(l, node->plans)
{
@@ -2250,7 +2401,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
* partition key.
*/
if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE &&
- (operation == CMD_INSERT || update_tuple_routing_needed))
+ (operation == CMD_INSERT || operation == CMD_MERGE ||
+ update_tuple_routing_needed))
mtstate->mt_partition_tuple_routing =
ExecSetupPartitionTupleRouting(mtstate, rel);
@@ -2261,6 +2413,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
if (!(eflags & EXEC_FLAG_EXPLAIN_ONLY))
ExecSetupTransitionCaptureState(mtstate, estate);
+ /*
+ * If we are doing MERGE then setup child-parent mapping. This will be
+ * required in case we end up doing a partition-key update, triggering a
+ * tuple routing.
+ */
+ if (mtstate->operation == CMD_MERGE &&
+ mtstate->mt_partition_tuple_routing != NULL)
+ ExecSetupChildParentMapForLeaf(mtstate->mt_partition_tuple_routing);
+
/*
* Construct mapping from each of the per-subplan partition attnos to the
* root attno. This is required when during update row movement the tuple
@@ -2370,7 +2531,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
/* initialize slot for the existing tuple */
mtstate->mt_existing =
- ExecInitExtraTupleSlot(mtstate->ps.state, relationDesc);
+ ExecInitExtraTupleSlot(mtstate->ps.state, NULL);
/* carried forward solely for the benefit of explain */
mtstate->mt_excludedtlist = node->exclRelTlist;
@@ -2428,6 +2589,96 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
}
}
+ resultRelInfo = mtstate->resultRelInfo;
+
+ if (node->mergeActionList)
+ {
+ ListCell *l;
+ ExprContext *econtext;
+ List *mergeMatchedActionStates = NIL;
+ List *mergeNotMatchedActionStates = NIL;
+
+ mtstate->mt_merge_subcommands = 0;
+
+ if (mtstate->ps.ps_ExprContext == NULL)
+ ExecAssignExprContext(estate, &mtstate->ps);
+
+ econtext = mtstate->ps.ps_ExprContext;
+
+ /* initialize slot for the existing tuple */
+ mtstate->mt_existing = ExecInitExtraTupleSlot(estate, NULL);
+
+ /* initialise slot for merge actions */
+ resultRelInfo->ri_mergeState->mergeSlot =
+ ExecInitExtraTupleSlot(estate, NULL);
+
+ /*
+ * Create a MergeActionState for each action on the mergeActionList
+ * and add it to either a list of matched actions or not-matched
+ * actions.
+ */
+ foreach(l, node->mergeActionList)
+ {
+ MergeAction *action = (MergeAction *) lfirst(l);
+ MergeActionState *action_state = makeNode(MergeActionState);
+ TupleDesc tupDesc;
+
+ action_state->matched = action->matched;
+ action_state->commandType = action->commandType;
+ action_state->whenqual = ExecInitQual((List *) action->qual,
+ &mtstate->ps);
+
+ /* create target slot for this action's projection */
+ tupDesc = ExecTypeFromTL((List *) action->targetList,
+ resultRelInfo->ri_RelationDesc->rd_rel->relhasoids);
+ action_state->tupDesc = tupDesc;
+ ExecSetSlotDescriptor(resultRelInfo->ri_mergeState->mergeSlot,
+ action_state->tupDesc);
+
+ /* build action projection state */
+ action_state->proj =
+ ExecBuildProjectionInfo(action->targetList, econtext,
+ resultRelInfo->ri_mergeState->mergeSlot, &mtstate->ps,
+ resultRelInfo->ri_RelationDesc->rd_att);
+
+ /*
+ * We create two lists - one for WHEN MATCHED actions and one
+ * for WHEN NOT MATCHED actions - and stick the
+ * MergeActionState into the appropriate list.
+ */
+ if (action_state->matched)
+ mergeMatchedActionStates =
+ lappend(mergeMatchedActionStates, action_state);
+ else
+ mergeNotMatchedActionStates =
+ lappend(mergeNotMatchedActionStates, action_state);
+
+ switch (action->commandType)
+ {
+ case CMD_INSERT:
+ mtstate->mt_merge_subcommands |= ACL_INSERT;
+ break;
+ case CMD_UPDATE:
+ mtstate->mt_merge_subcommands |= ACL_UPDATE;
+ break;
+ case CMD_DELETE:
+ mtstate->mt_merge_subcommands |= ACL_DELETE;
+ break;
+ case CMD_NOTHING:
+ break;
+ default:
+ elog(ERROR, "unknown operation");
+ break;
+ }
+
+ resultRelInfo->ri_mergeState->matchedActionStates =
+ mergeMatchedActionStates;
+ resultRelInfo->ri_mergeState->notMatchedActionStates =
+ mergeNotMatchedActionStates;
+
+ }
+ }
+
/* select first subplan */
mtstate->mt_whichplan = 0;
subplan = (Plan *) linitial(node->plans);
@@ -2441,7 +2692,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
* --- no need to look first. Typically, this will be a 'ctid' or
* 'wholerow' attribute, but in the case of a foreign data wrapper it
* might be a set of junk attributes sufficient to identify the remote
- * row.
+ * row. We follow this logic for MERGE, so it always has a junk 'ctid'.
*
* If there are multiple result relations, each one needs its own junk
* filter. Note multiple rels are only possible for UPDATE/DELETE, so we
@@ -2469,6 +2720,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
break;
case CMD_UPDATE:
case CMD_DELETE:
+ case CMD_MERGE:
junk_filter_needed = true;
break;
default:
@@ -2484,6 +2736,11 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
JunkFilter *j;
subplan = mtstate->mt_plans[i]->plan;
+
+ /*
+ * XXX we probably need to check plan output for CMD_MERGE
+ * also
+ */
if (operation == CMD_INSERT || operation == CMD_UPDATE)
ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
subplan->targetlist);
@@ -2492,7 +2749,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
resultRelInfo->ri_RelationDesc->rd_att->tdhasoid,
ExecInitExtraTupleSlot(estate, NULL));
- if (operation == CMD_UPDATE || operation == CMD_DELETE)
+ if (operation == CMD_UPDATE ||
+ operation == CMD_DELETE ||
+ operation == CMD_MERGE)
{
/* For UPDATE/DELETE, find the appropriate junk attr now */
char relkind;
@@ -2505,6 +2764,14 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
j->jf_junkAttNo = ExecFindJunkAttribute(j, "ctid");
if (!AttributeNumberIsValid(j->jf_junkAttNo))
elog(ERROR, "could not find junk ctid column");
+
+ if (operation == CMD_MERGE)
+ {
+ j->jf_otherJunkAttNo = ExecFindJunkAttribute(j, "tableoid");
+ if (!AttributeNumberIsValid(j->jf_otherJunkAttNo))
+ elog(ERROR, "could not find junk tableoid column");
+
+ }
}
else if (relkind == RELKIND_FOREIGN_TABLE)
{
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 9fc4431b80..e050a317c0 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2404,6 +2404,9 @@ _SPI_pquery(QueryDesc *queryDesc, bool fire_triggers, uint64 tcount)
else
res = SPI_OK_UPDATE;
break;
+ case CMD_MERGE:
+ res = SPI_OK_MERGE;
+ break;
default:
return SPI_ERROR_OPUNKNOWN;
}
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 3ad4da64aa..6dfb3edf42 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -206,6 +206,7 @@ _copyModifyTable(const ModifyTable *from)
COPY_NODE_FIELD(partitioned_rels);
COPY_SCALAR_FIELD(partColsUpdated);
COPY_NODE_FIELD(resultRelations);
+ COPY_SCALAR_FIELD(mergeTargetRelation);
COPY_SCALAR_FIELD(resultRelIndex);
COPY_SCALAR_FIELD(rootResultRelIndex);
COPY_NODE_FIELD(plans);
@@ -221,6 +222,8 @@ _copyModifyTable(const ModifyTable *from)
COPY_NODE_FIELD(onConflictWhere);
COPY_SCALAR_FIELD(exclRelRTI);
COPY_NODE_FIELD(exclRelTlist);
+ COPY_NODE_FIELD(mergeSourceTargetList);
+ COPY_NODE_FIELD(mergeActionList);
return newnode;
}
@@ -2976,6 +2979,9 @@ _copyQuery(const Query *from)
COPY_NODE_FIELD(setOperations);
COPY_NODE_FIELD(constraintDeps);
COPY_NODE_FIELD(withCheckOptions);
+ COPY_SCALAR_FIELD(mergeTarget_relation);
+ COPY_NODE_FIELD(mergeSourceTargetList);
+ COPY_NODE_FIELD(mergeActionList);
COPY_LOCATION_FIELD(stmt_location);
COPY_LOCATION_FIELD(stmt_len);
@@ -3039,6 +3045,34 @@ _copyUpdateStmt(const UpdateStmt *from)
return newnode;
}
+static MergeStmt *
+_copyMergeStmt(const MergeStmt *from)
+{
+ MergeStmt *newnode = makeNode(MergeStmt);
+
+ COPY_NODE_FIELD(relation);
+ COPY_NODE_FIELD(source_relation);
+ COPY_NODE_FIELD(join_condition);
+ COPY_NODE_FIELD(mergeActionList);
+
+ return newnode;
+}
+
+static MergeAction *
+_copyMergeAction(const MergeAction *from)
+{
+ MergeAction *newnode = makeNode(MergeAction);
+
+ COPY_SCALAR_FIELD(matched);
+ COPY_SCALAR_FIELD(commandType);
+ COPY_NODE_FIELD(condition);
+ COPY_NODE_FIELD(qual);
+ COPY_NODE_FIELD(stmt);
+ COPY_NODE_FIELD(targetList);
+
+ return newnode;
+}
+
static SelectStmt *
_copySelectStmt(const SelectStmt *from)
{
@@ -5101,6 +5135,12 @@ copyObjectImpl(const void *from)
case T_UpdateStmt:
retval = _copyUpdateStmt(from);
break;
+ case T_MergeStmt:
+ retval = _copyMergeStmt(from);
+ break;
+ case T_MergeAction:
+ retval = _copyMergeAction(from);
+ break;
case T_SelectStmt:
retval = _copySelectStmt(from);
break;
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 765b1be74b..5a0151eece 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -987,6 +987,8 @@ _equalQuery(const Query *a, const Query *b)
COMPARE_NODE_FIELD(setOperations);
COMPARE_NODE_FIELD(constraintDeps);
COMPARE_NODE_FIELD(withCheckOptions);
+ COMPARE_NODE_FIELD(mergeSourceTargetList);
+ COMPARE_NODE_FIELD(mergeActionList);
COMPARE_LOCATION_FIELD(stmt_location);
COMPARE_LOCATION_FIELD(stmt_len);
@@ -1042,6 +1044,30 @@ _equalUpdateStmt(const UpdateStmt *a, const UpdateStmt *b)
return true;
}
+static bool
+_equalMergeStmt(const MergeStmt *a, const MergeStmt *b)
+{
+ COMPARE_NODE_FIELD(relation);
+ COMPARE_NODE_FIELD(source_relation);
+ COMPARE_NODE_FIELD(join_condition);
+ COMPARE_NODE_FIELD(mergeActionList);
+
+ return true;
+}
+
+static bool
+_equalMergeAction(const MergeAction *a, const MergeAction *b)
+{
+ COMPARE_SCALAR_FIELD(matched);
+ COMPARE_SCALAR_FIELD(commandType);
+ COMPARE_NODE_FIELD(condition);
+ COMPARE_NODE_FIELD(qual);
+ COMPARE_NODE_FIELD(stmt);
+ COMPARE_NODE_FIELD(targetList);
+
+ return true;
+}
+
static bool
_equalSelectStmt(const SelectStmt *a, const SelectStmt *b)
{
@@ -3233,6 +3259,12 @@ equal(const void *a, const void *b)
case T_UpdateStmt:
retval = _equalUpdateStmt(a, b);
break;
+ case T_MergeStmt:
+ retval = _equalMergeStmt(a, b);
+ break;
+ case T_MergeAction:
+ retval = _equalMergeAction(a, b);
+ break;
case T_SelectStmt:
retval = _equalSelectStmt(a, b);
break;
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 6c76c41ebe..68e2cec66e 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2146,6 +2146,16 @@ expression_tree_walker(Node *node,
return true;
}
break;
+ case T_MergeAction:
+ {
+ MergeAction *action = (MergeAction *) node;
+
+ if (walker(action->targetList, context))
+ return true;
+ if (walker(action->qual, context))
+ return true;
+ }
+ break;
case T_JoinExpr:
{
JoinExpr *join = (JoinExpr *) node;
@@ -2255,6 +2265,10 @@ query_tree_walker(Query *query,
return true;
if (walker((Node *) query->onConflict, context))
return true;
+ if (walker((Node *) query->mergeSourceTargetList, context))
+ return true;
+ if (walker((Node *) query->mergeActionList, context))
+ return true;
if (walker((Node *) query->returningList, context))
return true;
if (walker((Node *) query->jointree, context))
@@ -2932,6 +2946,18 @@ expression_tree_mutator(Node *node,
return (Node *) newnode;
}
break;
+ case T_MergeAction:
+ {
+ MergeAction *action = (MergeAction *) node;
+ MergeAction *newnode;
+
+ FLATCOPY(newnode, action, MergeAction);
+ MUTATE(newnode->qual, action->qual, Node *);
+ MUTATE(newnode->targetList, action->targetList, List *);
+
+ return (Node *) newnode;
+ }
+ break;
case T_JoinExpr:
{
JoinExpr *join = (JoinExpr *) node;
@@ -3083,6 +3109,8 @@ query_tree_mutator(Query *query,
MUTATE(query->targetList, query->targetList, List *);
MUTATE(query->withCheckOptions, query->withCheckOptions, List *);
MUTATE(query->onConflict, query->onConflict, OnConflictExpr *);
+ MUTATE(query->mergeSourceTargetList, query->mergeSourceTargetList, List *);
+ MUTATE(query->mergeActionList, query->mergeActionList, List *);
MUTATE(query->returningList, query->returningList, List *);
MUTATE(query->jointree, query->jointree, FromExpr *);
MUTATE(query->setOperations, query->setOperations, Node *);
@@ -3224,9 +3252,9 @@ query_or_expression_tree_mutator(Node *node,
* boundaries: we descend to everything that's possibly interesting.
*
* Currently, the node type coverage here extends only to DML statements
- * (SELECT/INSERT/UPDATE/DELETE) and nodes that can appear in them, because
- * this is used mainly during analysis of CTEs, and only DML statements can
- * appear in CTEs.
+ * (SELECT/INSERT/UPDATE/DELETE/MERGE) and nodes that can appear in them,
+ * because this is used mainly during analysis of CTEs, and only DML
+ * statements can appear in CTEs.
*/
bool
raw_expression_tree_walker(Node *node,
@@ -3406,6 +3434,20 @@ raw_expression_tree_walker(Node *node,
return true;
}
break;
+ case T_MergeStmt:
+ {
+ MergeStmt *stmt = (MergeStmt *) node;
+
+ if (walker(stmt->relation, context))
+ return true;
+ if (walker(stmt->source_relation, context))
+ return true;
+ if (walker(stmt->join_condition, context))
+ return true;
+ if (walker(stmt->mergeActionList, context))
+ return true;
+ }
+ break;
case T_SelectStmt:
{
SelectStmt *stmt = (SelectStmt *) node;
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index fd80891954..7de5b59ade 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -374,6 +374,7 @@ _outModifyTable(StringInfo str, const ModifyTable *node)
WRITE_NODE_FIELD(partitioned_rels);
WRITE_BOOL_FIELD(partColsUpdated);
WRITE_NODE_FIELD(resultRelations);
+ WRITE_INT_FIELD(mergeTargetRelation);
WRITE_INT_FIELD(resultRelIndex);
WRITE_INT_FIELD(rootResultRelIndex);
WRITE_NODE_FIELD(plans);
@@ -389,6 +390,21 @@ _outModifyTable(StringInfo str, const ModifyTable *node)
WRITE_NODE_FIELD(onConflictWhere);
WRITE_UINT_FIELD(exclRelRTI);
WRITE_NODE_FIELD(exclRelTlist);
+ WRITE_NODE_FIELD(mergeSourceTargetList);
+ WRITE_NODE_FIELD(mergeActionList);
+}
+
+static void
+_outMergeAction(StringInfo str, const MergeAction *node)
+{
+ WRITE_NODE_TYPE("MERGEACTION");
+
+ WRITE_BOOL_FIELD(matched);
+ WRITE_ENUM_FIELD(commandType, CmdType);
+ WRITE_NODE_FIELD(condition);
+ WRITE_NODE_FIELD(qual);
+ /* We don't dump the stmt node */
+ WRITE_NODE_FIELD(targetList);
}
static void
@@ -2113,6 +2129,7 @@ _outModifyTablePath(StringInfo str, const ModifyTablePath *node)
WRITE_NODE_FIELD(partitioned_rels);
WRITE_BOOL_FIELD(partColsUpdated);
WRITE_NODE_FIELD(resultRelations);
+ WRITE_INT_FIELD(mergeTargetRelation);
WRITE_NODE_FIELD(subpaths);
WRITE_NODE_FIELD(subroots);
WRITE_NODE_FIELD(withCheckOptionLists);
@@ -2120,6 +2137,8 @@ _outModifyTablePath(StringInfo str, const ModifyTablePath *node)
WRITE_NODE_FIELD(rowMarks);
WRITE_NODE_FIELD(onconflict);
WRITE_INT_FIELD(epqParam);
+ WRITE_NODE_FIELD(mergeSourceTargetList);
+ WRITE_NODE_FIELD(mergeActionList);
}
static void
@@ -2941,6 +2960,9 @@ _outQuery(StringInfo str, const Query *node)
WRITE_NODE_FIELD(setOperations);
WRITE_NODE_FIELD(constraintDeps);
/* withCheckOptions intentionally omitted, see comment in parsenodes.h */
+ WRITE_INT_FIELD(mergeTarget_relation);
+ WRITE_NODE_FIELD(mergeSourceTargetList);
+ WRITE_NODE_FIELD(mergeActionList);
WRITE_LOCATION_FIELD(stmt_location);
WRITE_LOCATION_FIELD(stmt_len);
}
@@ -3656,6 +3678,9 @@ outNode(StringInfo str, const void *obj)
case T_ModifyTable:
_outModifyTable(str, obj);
break;
+ case T_MergeAction:
+ _outMergeAction(str, obj);
+ break;
case T_Append:
_outAppend(str, obj);
break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 068db353d7..65fe1934b5 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -270,6 +270,9 @@ _readQuery(void)
READ_NODE_FIELD(setOperations);
READ_NODE_FIELD(constraintDeps);
/* withCheckOptions intentionally omitted, see comment in parsenodes.h */
+ READ_INT_FIELD(mergeTarget_relation);
+ READ_NODE_FIELD(mergeSourceTargetList);
+ READ_NODE_FIELD(mergeActionList);
READ_LOCATION_FIELD(stmt_location);
READ_LOCATION_FIELD(stmt_len);
@@ -1575,6 +1578,7 @@ _readModifyTable(void)
READ_NODE_FIELD(partitioned_rels);
READ_BOOL_FIELD(partColsUpdated);
READ_NODE_FIELD(resultRelations);
+ READ_INT_FIELD(mergeTargetRelation);
READ_INT_FIELD(resultRelIndex);
READ_INT_FIELD(rootResultRelIndex);
READ_NODE_FIELD(plans);
@@ -1590,6 +1594,8 @@ _readModifyTable(void)
READ_NODE_FIELD(onConflictWhere);
READ_UINT_FIELD(exclRelRTI);
READ_NODE_FIELD(exclRelTlist);
+ READ_NODE_FIELD(mergeSourceTargetList);
+ READ_NODE_FIELD(mergeActionList);
READ_DONE();
}
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 8b4f031d96..b4d376b4ba 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -282,9 +282,13 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
CmdType operation, bool canSetTag,
Index nominalRelation, List *partitioned_rels,
bool partColsUpdated,
- List *resultRelations, List *subplans,
+ List *resultRelations,
+ Index mergeTargetRelation,
+ List *subplans,
List *withCheckOptionLists, List *returningLists,
- List *rowMarks, OnConflictExpr *onconflict, int epqParam);
+ List *rowMarks, OnConflictExpr *onconflict,
+ List *mergeSourceTargetList,
+ List *mergeActionList, int epqParam);
static GatherMerge *create_gather_merge_plan(PlannerInfo *root,
GatherMergePath *best_path);
@@ -2394,11 +2398,14 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
best_path->partitioned_rels,
best_path->partColsUpdated,
best_path->resultRelations,
+ best_path->mergeTargetRelation,
subplans,
best_path->withCheckOptionLists,
best_path->returningLists,
best_path->rowMarks,
best_path->onconflict,
+ best_path->mergeSourceTargetList,
+ best_path->mergeActionList,
best_path->epqParam);
copy_generic_path_info(&plan->plan, &best_path->path);
@@ -6465,9 +6472,13 @@ make_modifytable(PlannerInfo *root,
CmdType operation, bool canSetTag,
Index nominalRelation, List *partitioned_rels,
bool partColsUpdated,
- List *resultRelations, List *subplans,
+ List *resultRelations,
+ Index mergeTargetRelation,
+ List *subplans,
List *withCheckOptionLists, List *returningLists,
- List *rowMarks, OnConflictExpr *onconflict, int epqParam)
+ List *rowMarks, OnConflictExpr *onconflict,
+ List *mergeSourceTargetList,
+ List *mergeActionList, int epqParam)
{
ModifyTable *node = makeNode(ModifyTable);
List *fdw_private_list;
@@ -6493,6 +6504,7 @@ make_modifytable(PlannerInfo *root,
node->partitioned_rels = partitioned_rels;
node->partColsUpdated = partColsUpdated;
node->resultRelations = resultRelations;
+ node->mergeTargetRelation = mergeTargetRelation;
node->resultRelIndex = -1; /* will be set correctly in setrefs.c */
node->rootResultRelIndex = -1; /* will be set correctly in setrefs.c */
node->plans = subplans;
@@ -6525,6 +6537,8 @@ make_modifytable(PlannerInfo *root,
node->withCheckOptionLists = withCheckOptionLists;
node->returningLists = returningLists;
node->rowMarks = rowMarks;
+ node->mergeSourceTargetList = mergeSourceTargetList;
+ node->mergeActionList = mergeActionList;
node->epqParam = epqParam;
/*
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 54f2da70cb..7c42ca7e54 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -767,6 +767,24 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
/* exclRelTlist contains only Vars, so no preprocessing needed */
}
+ foreach(l, parse->mergeActionList)
+ {
+ MergeAction *action = (MergeAction *) lfirst(l);
+
+ action->targetList = (List *)
+ preprocess_expression(root,
+ (Node *) action->targetList,
+ EXPRKIND_TARGET);
+ action->qual =
+ preprocess_expression(root,
+ (Node *) action->qual,
+ EXPRKIND_QUAL);
+ }
+
+ parse->mergeSourceTargetList = (List *)
+ preprocess_expression(root, (Node *) parse->mergeSourceTargetList,
+ EXPRKIND_TARGET);
+
root->append_rel_list = (List *)
preprocess_expression(root, (Node *) root->append_rel_list,
EXPRKIND_APPINFO);
@@ -1508,6 +1526,7 @@ inheritance_planner(PlannerInfo *root)
subroot->parse->returningList);
Assert(!parse->onConflict);
+ Assert(parse->mergeActionList == NIL);
}
/* Result path must go into outer query's FINAL upperrel */
@@ -1566,12 +1585,15 @@ inheritance_planner(PlannerInfo *root)
partitioned_rels,
partColsUpdated,
resultRelations,
+ 0,
subpaths,
subroots,
withCheckOptionLists,
returningLists,
rowMarks,
NULL,
+ NULL,
+ NULL,
SS_assign_special_param(root)));
}
@@ -2105,8 +2127,8 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
}
/*
- * If this is an INSERT/UPDATE/DELETE, and we're not being called from
- * inheritance_planner, add the ModifyTable node.
+ * If this is an INSERT/UPDATE/DELETE/MERGE, and we're not being
+ * called from inheritance_planner, add the ModifyTable node.
*/
if (parse->commandType != CMD_SELECT && !inheritance_update)
{
@@ -2146,12 +2168,15 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
NIL,
false,
list_make1_int(parse->resultRelation),
+ parse->mergeTarget_relation,
list_make1(path),
list_make1(root),
withCheckOptionLists,
returningLists,
rowMarks,
parse->onConflict,
+ parse->mergeSourceTargetList,
+ parse->mergeActionList,
SS_assign_special_param(root));
}
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 4617d12cb9..d63a975823 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -851,8 +851,61 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
fix_scan_list(root, splan->exclRelTlist, rtoffset);
}
+ /*
+ * The MERGE produces the target rows by performing a right
+ * join between the target relation and the source relation
+ * (which could be a plain relation or a subquery). The INSERT
+ * and UPDATE actions of the MERGE requires access to the
+ * columns from the source relation. We arrange things so that
+ * the source relation attributes are available as INNER_VAR
+ * and the target relation attributes are available from the
+ * scan tuple.
+ */
+ if (splan->mergeActionList != NIL)
+ {
+ /*
+ * mergeSourceTargetList is already setup correctly to
+ * include all Vars coming from the source relation. So we
+ * fix the targetList of individual action nodes by
+ * ensuring that the source relation Vars are referenced
+ * as INNER_VAR. Note that for this to work correctly,
+ * during execution, the ecxt_innertuple must be set to
+ * the tuple obtained from the source relation.
+ *
+ * We leave the Vars from the result relation (i.e. the
+ * target relation) unchanged i.e. those Vars would be
+ * picked from the scan slot. So during execution, we must
+ * ensure that ecxt_scantuple is setup correctly to refer
+ * to the tuple from the target relation.
+ */
+
+ indexed_tlist *itlist;
+
+ itlist = build_tlist_index(splan->mergeSourceTargetList);
+
+ foreach(l, splan->mergeActionList)
+ {
+ MergeAction *action = (MergeAction *) lfirst(l);
+
+ /* Fix targetList of each action. */
+ action->targetList = fix_join_expr(root,
+ action->targetList,
+ NULL, itlist,
+ linitial_int(splan->resultRelations),
+ rtoffset);
+
+ /* Fix quals too. */
+ action->qual = (Node *) fix_join_expr(root,
+ (List *) action->qual,
+ NULL, itlist,
+ linitial_int(splan->resultRelations),
+ rtoffset);
+ }
+ }
+
splan->nominalRelation += rtoffset;
splan->exclRelRTI += rtoffset;
+ splan->mergeTargetRelation += rtoffset;
foreach(l, splan->partitioned_rels)
{
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index e2995e6592..3cefc4fe7a 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -103,9 +103,13 @@ preprocess_targetlist(PlannerInfo *root)
* scribbles on parse->targetList, which is not very desirable, but we
* keep it that way to avoid changing APIs used by FDWs.
*/
- if (command_type == CMD_UPDATE || command_type == CMD_DELETE)
+ if (command_type == CMD_UPDATE ||
+ command_type == CMD_DELETE)
rewriteTargetListUD(parse, target_rte, target_relation);
+ if (command_type == CMD_MERGE)
+ rewriteTargetListMerge(parse, target_relation);
+
/*
* for heap_form_tuple to work, the targetlist must match the exact order
* of the attributes. We also need to fill in any missing attributes. -ay
@@ -117,6 +121,39 @@ preprocess_targetlist(PlannerInfo *root)
result_relation,
RelationGetDescr(target_relation));
+ /*
+ * For MERGE command, handle targetlist of each MergeAction separately. We
+ * give the same treatment to MergeAction->targetList as we would have
+ * given to a regular INSERT/UPDATE/DELETE.
+ */
+ if (command_type == CMD_MERGE)
+ {
+ ListCell *l;
+
+ foreach(l, parse->mergeActionList)
+ {
+ MergeAction *action = (MergeAction *) lfirst(l);
+
+ switch (action->commandType)
+ {
+ case CMD_INSERT:
+ case CMD_UPDATE:
+ action->targetList = expand_targetlist(action->targetList,
+ action->commandType,
+ result_relation,
+ RelationGetDescr(target_relation));
+ break;
+ case CMD_DELETE:
+ break;
+ case CMD_NOTHING:
+ break;
+ default:
+ elog(ERROR, "unknown action in MERGE WHEN clause");
+
+ }
+ }
+ }
+
/*
* Add necessary junk columns for rowmarked rels. These values are needed
* for locking of rels selected FOR UPDATE/SHARE, and to do EvalPlanQual
@@ -347,6 +384,7 @@ expand_targetlist(List *tlist, int command_type,
true /* byval */ );
}
break;
+ case CMD_MERGE:
case CMD_UPDATE:
if (!att_tup->attisdropped)
{
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 22133fcf12..416b3f9578 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3284,17 +3284,21 @@ create_lockrows_path(PlannerInfo *root, RelOptInfo *rel,
* 'rowMarks' is a list of PlanRowMarks (non-locking only)
* 'onconflict' is the ON CONFLICT clause, or NULL
* 'epqParam' is the ID of Param for EvalPlanQual re-eval
+ * 'mergeActionList' is a list of MERGE actions
*/
ModifyTablePath *
create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
CmdType operation, bool canSetTag,
Index nominalRelation, List *partitioned_rels,
bool partColsUpdated,
- List *resultRelations, List *subpaths,
+ List *resultRelations,
+ Index mergeTargetRelation,
+ List *subpaths,
List *subroots,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- int epqParam)
+ List *mergeSourceTargetList,
+ List *mergeActionList, int epqParam)
{
ModifyTablePath *pathnode = makeNode(ModifyTablePath);
double total_size;
@@ -3359,6 +3363,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
pathnode->partitioned_rels = list_copy(partitioned_rels);
pathnode->partColsUpdated = partColsUpdated;
pathnode->resultRelations = resultRelations;
+ pathnode->mergeTargetRelation = mergeTargetRelation;
pathnode->subpaths = subpaths;
pathnode->subroots = subroots;
pathnode->withCheckOptionLists = withCheckOptionLists;
@@ -3366,6 +3371,8 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
pathnode->rowMarks = rowMarks;
pathnode->onconflict = onconflict;
pathnode->epqParam = epqParam;
+ pathnode->mergeSourceTargetList = mergeSourceTargetList;
+ pathnode->mergeActionList = mergeActionList;
return pathnode;
}
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index bd3a0c4a0a..8626cb2904 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1835,6 +1835,10 @@ has_row_triggers(PlannerInfo *root, Index rti, CmdType event)
trigDesc->trig_delete_before_row))
result = true;
break;
+ /* There is no separate event for MERGE, only INSERT/UPDATE/DELETE */
+ case CMD_MERGE:
+ result = false;
+ break;
default:
elog(ERROR, "unrecognized CmdType: %d", (int) event);
break;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index da8f0f93fc..901cf24e20 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -1237,7 +1237,6 @@ find_childrel_parents(PlannerInfo *root, RelOptInfo *rel)
return result;
}
-
/*
* get_baserel_parampathinfo
* Get the ParamPathInfo for a parameterized path for a base relation,
diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile
index 4b97f83803..8d0246322b 100644
--- a/src/backend/parser/Makefile
+++ b/src/backend/parser/Makefile
@@ -14,7 +14,7 @@ override CPPFLAGS := -I. -I$(srcdir) $(CPPFLAGS)
OBJS= analyze.o gram.o scan.o parser.o \
parse_agg.o parse_clause.o parse_coerce.o parse_collate.o parse_cte.o \
- parse_enr.o parse_expr.o parse_func.o parse_node.o parse_oper.o \
+ parse_enr.o parse_expr.o parse_func.o parse_merge.o parse_node.o parse_oper.o \
parse_param.o parse_relation.o parse_target.o parse_type.o \
parse_utilcmd.o scansup.o
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index cf1a34e41a..06a06b4f66 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -38,6 +38,7 @@
#include "parser/parse_cte.h"
#include "parser/parse_expr.h"
#include "parser/parse_func.h"
+#include "parser/parse_merge.h"
#include "parser/parse_oper.h"
#include "parser/parse_param.h"
#include "parser/parse_relation.h"
@@ -53,9 +54,6 @@ post_parse_analyze_hook_type post_parse_analyze_hook = NULL;
static Query *transformOptionalSelectInto(ParseState *pstate, Node *parseTree);
static Query *transformDeleteStmt(ParseState *pstate, DeleteStmt *stmt);
static Query *transformInsertStmt(ParseState *pstate, InsertStmt *stmt);
-static List *transformInsertRow(ParseState *pstate, List *exprlist,
- List *stmtcols, List *icolumns, List *attrnos,
- bool strip_indirection);
static OnConflictExpr *transformOnConflictClause(ParseState *pstate,
OnConflictClause *onConflictClause);
static int count_rowexpr_columns(ParseState *pstate, Node *expr);
@@ -68,8 +66,6 @@ static void determineRecursiveColTypes(ParseState *pstate,
Node *larg, List *nrtargetlist);
static Query *transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt);
static List *transformReturningList(ParseState *pstate, List *returningList);
-static List *transformUpdateTargetList(ParseState *pstate,
- List *targetList);
static Query *transformDeclareCursorStmt(ParseState *pstate,
DeclareCursorStmt *stmt);
static Query *transformExplainStmt(ParseState *pstate,
@@ -267,6 +263,7 @@ transformStmt(ParseState *pstate, Node *parseTree)
case T_InsertStmt:
case T_UpdateStmt:
case T_DeleteStmt:
+ case T_MergeStmt:
(void) test_raw_expression_coverage(parseTree, NULL);
break;
default:
@@ -291,6 +288,10 @@ transformStmt(ParseState *pstate, Node *parseTree)
result = transformUpdateStmt(pstate, (UpdateStmt *) parseTree);
break;
+ case T_MergeStmt:
+ result = transformMergeStmt(pstate, (MergeStmt *) parseTree);
+ break;
+
case T_SelectStmt:
{
SelectStmt *n = (SelectStmt *) parseTree;
@@ -366,6 +367,7 @@ analyze_requires_snapshot(RawStmt *parseTree)
case T_InsertStmt:
case T_DeleteStmt:
case T_UpdateStmt:
+ case T_MergeStmt:
case T_SelectStmt:
result = true;
break;
@@ -896,7 +898,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
* attrnos: integer column numbers (must be same length as icolumns)
* strip_indirection: if true, remove any field/array assignment nodes
*/
-static List *
+List *
transformInsertRow(ParseState *pstate, List *exprlist,
List *stmtcols, List *icolumns, List *attrnos,
bool strip_indirection)
@@ -2267,9 +2269,9 @@ transformUpdateStmt(ParseState *pstate, UpdateStmt *stmt)
/*
* transformUpdateTargetList -
- * handle SET clause in UPDATE/INSERT ... ON CONFLICT UPDATE
+ * handle SET clause in UPDATE/MERGE/INSERT ... ON CONFLICT UPDATE
*/
-static List *
+List *
transformUpdateTargetList(ParseState *pstate, List *origTlist)
{
List *tlist = NIL;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index cd5ba2d4d8..ebca5f3eb7 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -282,6 +282,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
CreateMatViewStmt RefreshMatViewStmt CreateAmStmt
CreatePublicationStmt AlterPublicationStmt
CreateSubscriptionStmt AlterSubscriptionStmt DropSubscriptionStmt
+ MergeStmt
%type <node> select_no_parens select_with_parens select_clause
simple_select values_clause
@@ -584,6 +585,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
%type <list> hash_partbound partbound_datum_list range_datum_list
%type <defelt> hash_partbound_elem
+%type <node> merge_when_clause opt_and_condition
+%type <list> merge_when_list
+%type <node> merge_update merge_delete merge_insert
+
/*
* Non-keyword token types. These are hard-wired into the "flex" lexer.
* They must be listed first so that their numeric codes do not depend on
@@ -651,7 +656,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
- MAPPING MATCH MATERIALIZED MAXVALUE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE
+ MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+ MINUTE_P MINVALUE MODE MONTH_P MOVE
NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NO NONE
NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF
@@ -920,6 +926,7 @@ stmt :
| RefreshMatViewStmt
| LoadStmt
| LockStmt
+ | MergeStmt
| NotifyStmt
| PrepareStmt
| ReassignOwnedStmt
@@ -10660,6 +10667,7 @@ ExplainableStmt:
| InsertStmt
| UpdateStmt
| DeleteStmt
+ | MergeStmt
| DeclareCursorStmt
| CreateAsStmt
| CreateMatViewStmt
@@ -10722,6 +10730,7 @@ PreparableStmt:
| InsertStmt
| UpdateStmt
| DeleteStmt /* by default all are $$=$1 */
+ | MergeStmt
;
/*****************************************************************************
@@ -11088,6 +11097,151 @@ set_target_list:
;
+/*****************************************************************************
+ *
+ * QUERY:
+ * MERGE STATEMENT
+ *
+ *****************************************************************************/
+
+MergeStmt:
+ MERGE INTO relation_expr_opt_alias
+ USING table_ref
+ ON a_expr
+ merge_when_list
+ {
+ MergeStmt *m = makeNode(MergeStmt);
+
+ m->relation = $3;
+ m->source_relation = $5;
+ m->join_condition = $7;
+ m->mergeActionList = $8;
+
+ $$ = (Node *)m;
+ }
+ ;
+
+
+merge_when_list:
+ merge_when_clause { $$ = list_make1($1); }
+ | merge_when_list merge_when_clause { $$ = lappend($1,$2); }
+ ;
+
+merge_when_clause:
+ WHEN MATCHED opt_and_condition THEN merge_update
+ {
+ MergeAction *m = makeNode(MergeAction);
+
+ m->matched = true;
+ m->commandType = CMD_UPDATE;
+ m->condition = $3;
+ m->stmt = $5;
+
+ $$ = (Node *)m;
+ }
+ | WHEN MATCHED opt_and_condition THEN merge_delete
+ {
+ MergeAction *m = makeNode(MergeAction);
+
+ m->matched = true;
+ m->commandType = CMD_DELETE;
+ m->condition = $3;
+ m->stmt = $5;
+
+ $$ = (Node *)m;
+ }
+ | WHEN NOT MATCHED opt_and_condition THEN merge_insert
+ {
+ MergeAction *m = makeNode(MergeAction);
+
+ m->matched = false;
+ m->commandType = CMD_INSERT;
+ m->condition = $4;
+ m->stmt = $6;
+
+ $$ = (Node *)m;
+ }
+ | WHEN NOT MATCHED opt_and_condition THEN DO NOTHING
+ {
+ MergeAction *m = makeNode(MergeAction);
+
+ m->matched = false;
+ m->commandType = CMD_NOTHING;
+ m->condition = $4;
+ m->stmt = NULL;
+
+ $$ = (Node *)m;
+ }
+ ;
+
+opt_and_condition:
+ AND a_expr { $$ = $2; }
+ | { $$ = NULL; }
+ ;
+
+merge_delete:
+ DELETE_P
+ {
+ DeleteStmt *n = makeNode(DeleteStmt);
+ $$ = (Node *)n;
+ }
+ ;
+
+merge_update:
+ UPDATE SET set_clause_list
+ {
+ UpdateStmt *n = makeNode(UpdateStmt);
+ n->targetList = $3;
+
+ $$ = (Node *)n;
+ }
+ ;
+
+merge_insert:
+ INSERT values_clause
+ {
+ InsertStmt *n = makeNode(InsertStmt);
+ n->cols = NIL;
+ n->selectStmt = $2;
+
+ $$ = (Node *)n;
+ }
+ | INSERT OVERRIDING override_kind VALUE_P values_clause
+ {
+ InsertStmt *n = makeNode(InsertStmt);
+ n->cols = NIL;
+ n->override = $3;
+ n->selectStmt = $5;
+
+ $$ = (Node *)n;
+ }
+ | INSERT '(' insert_column_list ')' values_clause
+ {
+ InsertStmt *n = makeNode(InsertStmt);
+ n->cols = $3;
+ n->selectStmt = $5;
+
+ $$ = (Node *)n;
+ }
+ | INSERT '(' insert_column_list ')' OVERRIDING override_kind VALUE_P values_clause
+ {
+ InsertStmt *n = makeNode(InsertStmt);
+ n->cols = $3;
+ n->override = $6;
+ n->selectStmt = $8;
+
+ $$ = (Node *)n;
+ }
+ | INSERT DEFAULT VALUES
+ {
+ InsertStmt *n = makeNode(InsertStmt);
+ n->cols = NIL;
+ n->selectStmt = NULL;
+
+ $$ = (Node *)n;
+ }
+ ;
+
/*****************************************************************************
*
* QUERY:
@@ -15088,8 +15242,10 @@ unreserved_keyword:
| LOGGED
| MAPPING
| MATCH
+ | MATCHED
| MATERIALIZED
| MAXVALUE
+ | MERGE
| METHOD
| MINUTE_P
| MINVALUE
diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 377a7ed6d0..544e7300b8 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -455,6 +455,13 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr)
case EXPR_KIND_VALUES_SINGLE:
errkind = true;
break;
+ case EXPR_KIND_MERGE_WHEN_AND:
+ if (isAgg)
+ err = _("aggregate functions are not allowed in WHEN AND conditions");
+ else
+ err = _("grouping operations are not allowed in WHEN AND conditions");
+
+ break;
case EXPR_KIND_CHECK_CONSTRAINT:
case EXPR_KIND_DOMAIN_CHECK:
if (isAgg)
@@ -873,6 +880,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc,
case EXPR_KIND_VALUES_SINGLE:
errkind = true;
break;
+ case EXPR_KIND_MERGE_WHEN_AND:
+ err = _("window functions are not allowed in WHEN AND conditions");
+ break;
case EXPR_KIND_CHECK_CONSTRAINT:
case EXPR_KIND_DOMAIN_CHECK:
err = _("window functions are not allowed in check constraints");
diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 3a02307bd9..9df9828df8 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -76,9 +76,6 @@ static RangeTblEntry *transformRangeTableFunc(ParseState *pstate,
RangeTableFunc *t);
static TableSampleClause *transformRangeTableSample(ParseState *pstate,
RangeTableSample *rts);
-static Node *transformFromClauseItem(ParseState *pstate, Node *n,
- RangeTblEntry **top_rte, int *top_rti,
- List **namespace);
static Node *buildMergedJoinVar(ParseState *pstate, JoinType jointype,
Var *l_colvar, Var *r_colvar);
static ParseNamespaceItem *makeNamespaceItem(RangeTblEntry *rte,
@@ -139,6 +136,7 @@ transformFromClause(ParseState *pstate, List *frmList)
n = transformFromClauseItem(pstate, n,
&rte,
&rtindex,
+ NULL, NULL,
&namespace);
checkNameSpaceConflicts(pstate, pstate->p_namespace, namespace);
@@ -1100,9 +1098,10 @@ getRTEForSpecialRelationTypes(ParseState *pstate, RangeVar *rv)
* as table/column names by this item. (The lateral_only flags in these items
* are indeterminate and should be explicitly set by the caller before use.)
*/
-static Node *
+Node *
transformFromClauseItem(ParseState *pstate, Node *n,
RangeTblEntry **top_rte, int *top_rti,
+ RangeTblEntry **right_rte, int *right_rti,
List **namespace)
{
if (IsA(n, RangeVar))
@@ -1194,7 +1193,7 @@ transformFromClauseItem(ParseState *pstate, Node *n,
/* Recursively transform the contained relation */
rel = transformFromClauseItem(pstate, rts->relation,
- top_rte, top_rti, namespace);
+ top_rte, top_rti, NULL, NULL, namespace);
/* Currently, grammar could only return a RangeVar as contained rel */
rtr = castNode(RangeTblRef, rel);
rte = rt_fetch(rtr->rtindex, pstate->p_rtable);
@@ -1222,6 +1221,7 @@ transformFromClauseItem(ParseState *pstate, Node *n,
List *l_namespace,
*r_namespace,
*my_namespace,
+ *save_namespace,
*l_colnames,
*r_colnames,
*res_colnames,
@@ -1240,6 +1240,7 @@ transformFromClauseItem(ParseState *pstate, Node *n,
j->larg = transformFromClauseItem(pstate, j->larg,
&l_rte,
&l_rtindex,
+ NULL, NULL,
&l_namespace);
/*
@@ -1263,12 +1264,34 @@ transformFromClauseItem(ParseState *pstate, Node *n,
sv_namespace_length = list_length(pstate->p_namespace);
pstate->p_namespace = list_concat(pstate->p_namespace, l_namespace);
+ /*
+ * If we are running MERGE, don't make the other RTEs visible while
+ * parsing the source relation. It mustn't see them.
+ *
+ * XXX Currently, only MERGE passes non-NULL value for right_rte, so we
+ * can safely deduce if we're running MERGE or not by just looking at
+ * the right_rte. If that ever changes, we should look at other means
+ * to find that.
+ */
+ if (right_rte)
+ {
+ save_namespace = pstate->p_namespace;
+ pstate->p_namespace = NIL;
+ }
+
/* And now we can process the RHS */
j->rarg = transformFromClauseItem(pstate, j->rarg,
&r_rte,
&r_rtindex,
+ NULL, NULL,
&r_namespace);
+ /*
+ * And now restore the namespace again so that join-quals can see it.
+ */
+ if (right_rte)
+ pstate->p_namespace = save_namespace;
+
/* Remove the left-side RTEs from the namespace list again */
pstate->p_namespace = list_truncate(pstate->p_namespace,
sv_namespace_length);
@@ -1295,6 +1318,12 @@ transformFromClauseItem(ParseState *pstate, Node *n,
expandRTE(r_rte, r_rtindex, 0, -1, false,
&r_colnames, &r_colvars);
+ if (right_rte)
+ *right_rte = r_rte;
+
+ if (right_rti)
+ *right_rti = r_rtindex;
+
/*
* Natural join does not explicitly specify columns; must generate
* columns to join. Need to run through the list of columns from each
diff --git a/src/backend/parser/parse_collate.c b/src/backend/parser/parse_collate.c
index 6d34245083..51c73c4018 100644
--- a/src/backend/parser/parse_collate.c
+++ b/src/backend/parser/parse_collate.c
@@ -485,6 +485,7 @@ assign_collations_walker(Node *node, assign_collations_context *context)
case T_FromExpr:
case T_OnConflictExpr:
case T_SortGroupClause:
+ case T_MergeAction:
(void) expression_tree_walker(node,
assign_collations_walker,
(void *) &loccontext);
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 385e54a9b6..38fbe3366f 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1818,6 +1818,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
case EXPR_KIND_RETURNING:
case EXPR_KIND_VALUES:
case EXPR_KIND_VALUES_SINGLE:
+ case EXPR_KIND_MERGE_WHEN_AND:
/* okay */
break;
case EXPR_KIND_CHECK_CONSTRAINT:
@@ -3475,6 +3476,8 @@ ParseExprKindName(ParseExprKind exprKind)
return "PARTITION BY";
case EXPR_KIND_CALL_ARGUMENT:
return "CALL";
+ case EXPR_KIND_MERGE_WHEN_AND:
+ return "MERGE WHEN AND";
/*
* There is intentionally no default: case here, so that the
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index ea5d5212b4..615aee6d15 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2277,6 +2277,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location)
/* okay, since we process this like a SELECT tlist */
pstate->p_hasTargetSRFs = true;
break;
+ case EXPR_KIND_MERGE_WHEN_AND:
+ err = _("set-returning functions are not allowed in WHEN AND conditions");
+ break;
case EXPR_KIND_CHECK_CONSTRAINT:
case EXPR_KIND_DOMAIN_CHECK:
err = _("set-returning functions are not allowed in check constraints");
diff --git a/src/backend/parser/parse_merge.c b/src/backend/parser/parse_merge.c
new file mode 100644
index 0000000000..c3981de0b5
--- /dev/null
+++ b/src/backend/parser/parse_merge.c
@@ -0,0 +1,580 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_merge.c
+ * handle merge-statement in parser
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/parser/parse_merge.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "miscadmin.h"
+
+#include "access/sysattr.h"
+#include "nodes/makefuncs.h"
+#include "parser/analyze.h"
+#include "parser/parse_collate.h"
+#include "parser/parsetree.h"
+#include "parser/parser.h"
+#include "parser/parse_clause.h"
+#include "parser/parse_merge.h"
+#include "parser/parse_relation.h"
+#include "parser/parse_target.h"
+#include "utils/rel.h"
+#include "utils/relcache.h"
+
+static int transformMergeJoinClause(ParseState *pstate, Node *merge,
+ List **mergeSourceTargetList);
+static void setNamespaceForMergeAction(ParseState *pstate,
+ MergeAction *action);
+static void setNamespaceVisibilityForRTE(List *namespace, RangeTblEntry *rte,
+ bool rel_visible,
+ bool cols_visible);
+
+/*
+ * Special handling for MERGE statement is required because we assemble
+ * the query manually. This is similar to setTargetTable() followed
+ * by transformFromClause() but with a few less steps.
+ *
+ * Process the FROM clause and add items to the query's range table,
+ * joinlist, and namespace.
+ *
+ * A special targetlist comprising of the columns from the right-subtree of
+ * the join is populated and returned. Note that when the JoinExpr is
+ * setup by transformMergeStmt, the left subtree has the target result
+ * relation and the right subtree has the source relation.
+ *
+ * Returns the rangetable index of the target relation.
+ */
+static int
+transformMergeJoinClause(ParseState *pstate, Node *merge,
+ List **mergeSourceTargetList)
+{
+ RangeTblEntry *rte,
+ *rt_rte;
+ List *namespace;
+ int rtindex,
+ rt_rtindex;
+ Node *n;
+ int mergeTarget_relation = list_length(pstate->p_rtable) + 1;
+ Var *var;
+ TargetEntry *te;
+
+ n = transformFromClauseItem(pstate, merge,
+ &rte,
+ &rtindex,
+ &rt_rte,
+ &rt_rtindex,
+ &namespace);
+
+ pstate->p_joinlist = list_make1(n);
+
+ /*
+ * We created an internal join between the target and the source relation
+ * to carry out the MERGE actions. Normally such an unaliased join hides
+ * the joining relations, unless the column references are qualified.
+ * Also, any unqualified column refernces are resolved to the Join RTE, if
+ * there is a matching entry in the targetlist. But the way MERGE
+ * execution is later setup, we expect all column references to resolve to
+ * either the source or the target relation. Hence we must not add the
+ * Join RTE to the namespace.
+ *
+ * The last entry must be for the top-level Join RTE. We don't want to
+ * resolve any references to the Join RTE. So discard that.
+ *
+ * We also do not want to resolve any references from the leftside of the
+ * Join since that corresponds to the target relation. References to the
+ * columns of the target relation must be resolved from the result
+ * relation and not the one that is used in the join. So the
+ * mergeTarget_relation is marked invisible to both qualified as well as
+ * unqualified references.
+ */
+ Assert(list_length(namespace) > 1);
+ namespace = list_truncate(namespace, list_length(namespace) - 1);
+ pstate->p_namespace = list_concat(pstate->p_namespace, namespace);
+
+ setNamespaceVisibilityForRTE(pstate->p_namespace,
+ rt_fetch(mergeTarget_relation, pstate->p_rtable), false, false);
+
+ /*
+ * Expand the right relation and add its columns to the
+ * mergeSourceTargetList. Note that the right relation can either be a
+ * plain relation or a subquery or anything that can have a
+ * RangeTableEntry.
+ */
+ *mergeSourceTargetList = expandRelAttrs(pstate, rt_rte, rt_rtindex, 0, -1);
+
+ /*
+ * Add a whole-row-Var entry to support references to "source.*".
+ */
+ var = makeWholeRowVar(rt_rte, rt_rtindex, 0, false);
+ te = makeTargetEntry((Expr *) var, list_length(*mergeSourceTargetList) + 1,
+ NULL, true);
+ *mergeSourceTargetList = lappend(*mergeSourceTargetList, te);
+
+ return mergeTarget_relation;
+}
+
+/*
+ * Make appropriate changes to the namespace visibility while transforming
+ * individual action's quals and targetlist expressions. In particular, for
+ * INSERT actions we must only see the source relation (since INSERT action is
+ * invoked for NOT MATCHED tuples and hence there is no target tuple to deal
+ * with). On the other hand, UPDATE and DELETE actions can see both source and
+ * target relations.
+ *
+ * Also, since the internal Join node can hide the source and target
+ * relations, we must explicitly make the respective relation as visible so
+ * that columns can be referenced unqualified from these relations.
+ */
+static void
+setNamespaceForMergeAction(ParseState *pstate, MergeAction *action)
+{
+ RangeTblEntry *targetRelRTE,
+ *sourceRelRTE;
+
+ /* Assume target relation is at index 1 */
+ targetRelRTE = rt_fetch(1, pstate->p_rtable);
+
+ /*
+ * Assume that the top-level join RTE is at the end. The source relation
+ * is just before that.
+ */
+ sourceRelRTE = rt_fetch(list_length(pstate->p_rtable) - 1, pstate->p_rtable);
+
+ switch (action->commandType)
+ {
+ case CMD_INSERT:
+
+ /*
+ * Inserts can't see target relation, but they can see source
+ * relation.
+ */
+ setNamespaceVisibilityForRTE(pstate->p_namespace,
+ targetRelRTE, false, false);
+ setNamespaceVisibilityForRTE(pstate->p_namespace,
+ sourceRelRTE, true, true);
+ break;
+
+ case CMD_UPDATE:
+ case CMD_DELETE:
+
+ /*
+ * Updates and deletes can see both target and source relations.
+ */
+ setNamespaceVisibilityForRTE(pstate->p_namespace,
+ targetRelRTE, true, true);
+ setNamespaceVisibilityForRTE(pstate->p_namespace,
+ sourceRelRTE, true, true);
+ break;
+
+ case CMD_NOTHING:
+ break;
+ default:
+ elog(ERROR, "unknown action in MERGE WHEN clause");
+ }
+}
+
+/*
+ * transformMergeStmt -
+ * transforms a MERGE statement
+ */
+Query *
+transformMergeStmt(ParseState *pstate, MergeStmt *stmt)
+{
+ Query *qry = makeNode(Query);
+ ListCell *l;
+ AclMode targetPerms = ACL_NO_RIGHTS;
+ bool is_terminal[2];
+ JoinExpr *joinexpr;
+
+ /* There can't be any outer WITH to worry about */
+ Assert(pstate->p_ctenamespace == NIL);
+
+ qry->commandType = CMD_MERGE;
+
+ /*
+ * Check WHEN clauses for permissions and sanity
+ */
+ is_terminal[0] = false;
+ is_terminal[1] = false;
+ foreach(l, stmt->mergeActionList)
+ {
+ MergeAction *action = (MergeAction *) lfirst(l);
+ uint when_type = (action->matched ? 0 : 1);
+
+ /*
+ * Collect action types so we can check Target permissions
+ */
+ switch (action->commandType)
+ {
+ case CMD_INSERT:
+ {
+ InsertStmt *istmt = (InsertStmt *) action->stmt;
+ SelectStmt *selectStmt = (SelectStmt *) istmt->selectStmt;
+
+ /*
+ * The grammar allows attaching ORDER BY, LIMIT, FOR
+ * UPDATE, or WITH to a VALUES clause and also multiple
+ * VALUES clauses. If we have any of those, ERROR.
+ */
+ if (selectStmt && (selectStmt->valuesLists == NIL ||
+ selectStmt->sortClause != NIL ||
+ selectStmt->limitOffset != NULL ||
+ selectStmt->limitCount != NULL ||
+ selectStmt->lockingClause != NIL ||
+ selectStmt->withClause != NULL))
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("SELECT not allowed in MERGE INSERT statement")));
+
+ if (selectStmt && list_length(selectStmt->valuesLists) > 1)
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("Multiple VALUES clauses not allowed in MERGE INSERT statement")));
+
+ targetPerms |= ACL_INSERT;
+ }
+ break;
+ case CMD_UPDATE:
+ targetPerms |= ACL_UPDATE;
+ break;
+ case CMD_DELETE:
+ targetPerms |= ACL_DELETE;
+ break;
+ case CMD_NOTHING:
+ break;
+ default:
+ elog(ERROR, "unknown action in MERGE WHEN clause");
+ }
+
+ /*
+ * Check for unreachable WHEN clauses
+ */
+ if (action->condition == NULL)
+ is_terminal[when_type] = true;
+ else if (is_terminal[when_type])
+ ereport(ERROR,
+ (errcode(ERRCODE_SYNTAX_ERROR),
+ errmsg("unreachable WHEN clause specified after unconditional WHEN clause")));
+ }
+
+ /*
+ * Construct a query of the form SELECT relation.ctid --junk attribute
+ * ,relation.tableoid --junk attribute ,source_relation.<somecols>
+ * ,relation.<somecols> FROM relation RIGHT JOIN source_relation ON
+ * join_condition -- no WHERE clause - all conditions are applied in
+ * executor
+ *
+ * stmt->relation is the target relation, given as a RangeVar
+ * stmt->source_relation is a RangeVar or subquery
+ *
+ * We specify the join as a RIGHT JOIN as a simple way of forcing the
+ * first (larg) RTE to refer to the target table.
+ *
+ * The MERGE query's join can be tuned in some cases, see below for these
+ * special case tweaks.
+ *
+ * We set QSRC_PARSER to show query constructed in parse analysis
+ *
+ * Note that we have only one Query for a MERGE statement and the planner
+ * is called only once. That query is executed once to produce our stream
+ * of candidate change rows, so the query must contain all of the columns
+ * required by each of the targetlist or conditions for each action.
+ *
+ * As top-level statements INSERT, UPDATE and DELETE have a Query, whereas
+ * with MERGE the individual actions do not require separate planning,
+ * only different handling in the executor. See nodeModifyTable handling
+ * of commandType CMD_MERGE.
+ *
+ * A sub-query can include the Target, but otherwise the sub-query cannot
+ * reference the outermost Target table at all.
+ */
+ qry->querySource = QSRC_PARSER;
+
+ /*
+ * Setup the target table. Unlike regular UPDATE/DELETE, we don't expand
+ * inheritance for the target relation in case of MERGE. Instead, once a
+ * target row is returned by the underlying join, we find the correct
+ * partition and setup required state to carry out UPDATE/DELETE. All of
+ * this happens during execution.
+ */
+ qry->resultRelation = setTargetTable(pstate, stmt->relation,
+ false, /* do not expand inheritance */
+ true, targetPerms);
+
+ joinexpr = makeNode(JoinExpr);
+ joinexpr->isNatural = false;
+ joinexpr->alias = NULL;
+ joinexpr->usingClause = NIL;
+ joinexpr->quals = stmt->join_condition;
+
+ /*
+ * Simplify the MERGE query as much as possible
+ *
+ * These seem like things that could go into Optimizer, but they are
+ * semantic simplications rather than optimizations, per se.
+ *
+ * If there are no INSERT actions we won't be using the non-matching
+ * candidate rows for anything, so no need for an outer join. We do still
+ * need an inner join for UPDATE and DELETE actions.
+ *
+ * Possible additional simplifications...
+ *
+ * XXX if we have a constant ON clause, we can skip join altogether
+ *
+ * XXX if we have a constant subquery, we can also skip join
+ *
+ * XXX if we were really keen we could look through the actionList and
+ * pull out common conditions, if there were no terminal clauses and put
+ * them into the main query as an early row filter but that seems like an
+ * atypical case and so checking for it would be likely to just be wasted
+ * effort.
+ */
+ joinexpr->larg = (Node *) stmt->relation;
+ joinexpr->rarg = (Node *) stmt->source_relation;
+ if (targetPerms & ACL_INSERT)
+ joinexpr->jointype = JOIN_RIGHT;
+ else
+ joinexpr->jointype = JOIN_INNER;
+
+ /*
+ * We use a special purpose transformation here because the normal
+ * routines don't quite work right for the MERGE case.
+ *
+ * A special mergeSourceTargetList is setup by transformMergeJoinClause().
+ * It refers to all the attributes provided by the source relation. This
+ * is later used by set_plan_refs() to fix the UPDATE/INSERT target lists
+ * to so that they can correctly fetch the attributes from the source
+ * relation.
+ *
+ * Track the RTE index of the target table used in the join query. This is
+ * used by rewriteTargetListMerge to add required junk attributes to the
+ * targetlist.
+ */
+ qry->mergeTarget_relation = transformMergeJoinClause(pstate, (Node *) joinexpr,
+ &qry->mergeSourceTargetList);
+
+ /*
+ * This query should just provide the source relation columns. Later, in
+ * preprocess_targetlist(), we shall also add "ctid" attribute of the
+ * target relation to ensure that the target tuple can be fetched
+ * correctly.
+ */
+ qry->targetList = qry->mergeSourceTargetList;
+
+ /* qry has no WHERE clause so absent quals are shown as NULL */
+ qry->jointree = makeFromExpr(pstate->p_joinlist, NULL);
+ qry->rtable = pstate->p_rtable;
+
+ /*
+ * XXX MERGE is unsupported in various cases
+ */
+ if (!(pstate->p_target_relation->rd_rel->relkind == RELKIND_RELATION ||
+ pstate->p_target_relation->rd_rel->relkind == RELKIND_MATVIEW ||
+ pstate->p_target_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE))
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("MERGE is not supported for this relation type")));
+
+ if (pstate->p_target_relation->rd_rel->relkind != RELKIND_PARTITIONED_TABLE &&
+ pstate->p_target_relation->rd_rel->relhassubclass)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("MERGE is not supported for relations with inheritance")));
+
+ if (pstate->p_target_relation->rd_rel->relhasrules)
+ ereport(ERROR,
+ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("MERGE is not supported for relations with rules")));
+
+ /*
+ * We now have a good query shape, so now look at the when conditions and
+ * action targetlists.
+ *
+ * Overall, the MERGE Query's targetlist is NIL.
+ *
+ * Each individual action has its own targetlist that needs separate
+ * transformation. These transforms don't do anything to the overall
+ * targetlist, since that is only used for resjunk columns.
+ *
+ * We can reference any column in Target or Source, which is OK because
+ * both of those already have RTEs. There is nothing like the EXCLUDED
+ * pseudo-relation for INSERT ON CONFLICT.
+ */
+ foreach(l, stmt->mergeActionList)
+ {
+ MergeAction *action = (MergeAction *) lfirst(l);
+
+ /*
+ * Set namespace for the specific action. This must be done before
+ * analysing the WHEN quals and the action targetlisst.
+ */
+ setNamespaceForMergeAction(pstate, action);
+
+ /*
+ * Transform the when condition.
+ *
+ * Note that these quals are NOT added to the join quals; instead they
+ * are evaluated sepaartely during execution to decide which of the
+ * WHEN MATCHED or WHEN NOT MATCHED actions to execute.
+ */
+ action->qual = transformWhereClause(pstate, action->condition,
+ EXPR_KIND_MERGE_WHEN_AND, "WHEN");
+
+ /*
+ * Transform target lists for each INSERT and UPDATE action stmt
+ */
+ switch (action->commandType)
+ {
+ case CMD_INSERT:
+ {
+ InsertStmt *istmt = (InsertStmt *) action->stmt;
+ SelectStmt *selectStmt = (SelectStmt *) istmt->selectStmt;
+ List *exprList = NIL;
+ ListCell *lc;
+ RangeTblEntry *rte;
+ ListCell *icols;
+ ListCell *attnos;
+ List *icolumns;
+ List *attrnos;
+
+ pstate->p_is_insert = true;
+
+ icolumns = checkInsertTargets(pstate, istmt->cols, &attrnos);
+ Assert(list_length(icolumns) == list_length(attrnos));
+
+ /*
+ * Handle INSERT much like in transformInsertStmt
+ */
+ if (selectStmt == NULL)
+ {
+ /*
+ * We have INSERT ... DEFAULT VALUES. We can handle
+ * this case by emitting an empty targetlist --- all
+ * columns will be defaulted when the planner expands
+ * the targetlist.
+ */
+ exprList = NIL;
+ }
+ else
+ {
+ /*
+ * Process INSERT ... VALUES with a single VALUES
+ * sublist. We treat this case separately for
+ * efficiency. The sublist is just computed directly
+ * as the Query's targetlist, with no VALUES RTE. So
+ * it works just like a SELECT without any FROM.
+ */
+ List *valuesLists = selectStmt->valuesLists;
+
+ Assert(list_length(valuesLists) == 1);
+ Assert(selectStmt->intoClause == NULL);
+
+ /*
+ * Do basic expression transformation (same as a ROW()
+ * expr, but allow SetToDefault at top level)
+ */
+ exprList = transformExpressionList(pstate,
+ (List *) linitial(valuesLists),
+ EXPR_KIND_VALUES_SINGLE,
+ true);
+
+ /* Prepare row for assignment to target table */
+ exprList = transformInsertRow(pstate, exprList,
+ istmt->cols,
+ icolumns, attrnos,
+ false);
+ }
+
+ /*
+ * Generate action's target list using the computed list
+ * of expressions. Also, mark all the target columns as
+ * needing insert permissions.
+ */
+ rte = pstate->p_target_rangetblentry;
+ icols = list_head(icolumns);
+ attnos = list_head(attrnos);
+ foreach(lc, exprList)
+ {
+ Expr *expr = (Expr *) lfirst(lc);
+ ResTarget *col;
+ AttrNumber attr_num;
+ TargetEntry *tle;
+
+ col = lfirst_node(ResTarget, icols);
+ attr_num = (AttrNumber) lfirst_int(attnos);
+
+ tle = makeTargetEntry(expr,
+ attr_num,
+ col->name,
+ false);
+ action->targetList = lappend(action->targetList, tle);
+
+ rte->insertedCols = bms_add_member(rte->insertedCols,
+ attr_num - FirstLowInvalidHeapAttributeNumber);
+
+ icols = lnext(icols);
+ attnos = lnext(attnos);
+ }
+ }
+ break;
+ case CMD_UPDATE:
+ {
+ UpdateStmt *ustmt = (UpdateStmt *) action->stmt;
+
+ pstate->p_is_insert = false;
+ action->targetList = transformUpdateTargetList(pstate, ustmt->targetList);
+ }
+ break;
+ case CMD_DELETE:
+ break;
+
+ case CMD_NOTHING:
+ action->targetList = NIL;
+ break;
+ default:
+ elog(ERROR, "unknown action in MERGE WHEN clause");
+ }
+ }
+
+ qry->mergeActionList = stmt->mergeActionList;
+
+ /* XXX maybe later */
+ qry->returningList = NULL;
+
+ qry->hasTargetSRFs = false;
+ qry->hasSubLinks = pstate->p_hasSubLinks;
+
+ assign_query_collations(pstate, qry);
+
+ return qry;
+}
+
+static void
+setNamespaceVisibilityForRTE(List *namespace, RangeTblEntry *rte,
+ bool rel_visible,
+ bool cols_visible)
+{
+ ListCell *lc;
+
+ foreach(lc, namespace)
+ {
+ ParseNamespaceItem *nsitem = (ParseNamespaceItem *) lfirst(lc);
+
+ if (nsitem->p_rte == rte)
+ {
+ nsitem->p_rel_visible = rel_visible;
+ nsitem->p_cols_visible = cols_visible;
+ break;
+ }
+ }
+
+}
diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c
index 053ae02c9f..5583404e1b 100644
--- a/src/backend/parser/parse_relation.c
+++ b/src/backend/parser/parse_relation.c
@@ -728,6 +728,16 @@ scanRTEForColumn(ParseState *pstate, RangeTblEntry *rte, const char *colname,
colname),
parser_errposition(pstate, location)));
+ /* In MERGE when and condition, no system column is allowed */
+ if (pstate->p_expr_kind == EXPR_KIND_MERGE_WHEN_AND &&
+ attnum < InvalidAttrNumber &&
+ !(attnum == TableOidAttributeNumber || attnum == ObjectIdAttributeNumber))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_COLUMN_REFERENCE),
+ errmsg("system column \"%s\" reference in WHEN AND condition is invalid",
+ colname),
+ parser_errposition(pstate, location)));
+
if (attnum != InvalidAttrNumber)
{
/* now check to see if column actually is defined */
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 66253fc3d3..45875ec289 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1376,6 +1376,53 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
}
}
+void
+rewriteTargetListMerge(Query *parsetree, Relation target_relation)
+{
+ Var *var = NULL;
+ const char *attrname;
+ TargetEntry *tle;
+
+ Assert(target_relation->rd_rel->relkind == RELKIND_RELATION ||
+ target_relation->rd_rel->relkind == RELKIND_MATVIEW ||
+ target_relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+
+ /*
+ * Emit CTID so that executor can find the row to update or delete.
+ */
+ var = makeVar(parsetree->mergeTarget_relation,
+ SelfItemPointerAttributeNumber,
+ TIDOID,
+ -1,
+ InvalidOid,
+ 0);
+
+ attrname = "ctid";
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->targetList) + 1,
+ pstrdup(attrname),
+ true);
+
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+
+ /*
+ * Emit TABLEOID so that executor can find the row to update or delete.
+ */
+ var = makeVar(parsetree->mergeTarget_relation,
+ TableOidAttributeNumber,
+ OIDOID,
+ -1,
+ InvalidOid,
+ 0);
+
+ attrname = "tableoid";
+ tle = makeTargetEntry((Expr *) var,
+ list_length(parsetree->targetList) + 1,
+ pstrdup(attrname),
+ true);
+
+ parsetree->targetList = lappend(parsetree->targetList, tle);
+}
/*
* matchLocks -
@@ -3330,6 +3377,7 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
}
else if (event == CMD_UPDATE)
{
+ Assert(parsetree->override == OVERRIDING_NOT_SET);
parsetree->targetList =
rewriteTargetListIU(parsetree->targetList,
parsetree->commandType,
@@ -3337,6 +3385,50 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
rt_entry_relation,
parsetree->resultRelation, NULL);
}
+ else if (event == CMD_MERGE)
+ {
+ Assert(parsetree->override == OVERRIDING_NOT_SET);
+
+ /*
+ * Rewrite each action targetlist separately
+ */
+ foreach(lc1, parsetree->mergeActionList)
+ {
+ MergeAction *action = (MergeAction *) lfirst(lc1);
+
+ switch (action->commandType)
+ {
+ case CMD_NOTHING:
+ case CMD_DELETE: /* Nothing to do here */
+ break;
+ case CMD_UPDATE:
+ action->targetList =
+ rewriteTargetListIU(action->targetList,
+ action->commandType,
+ parsetree->override,
+ rt_entry_relation,
+ parsetree->resultRelation,
+ NULL);
+ break;
+ case CMD_INSERT:
+ {
+ InsertStmt *istmt = (InsertStmt *) action->stmt;
+
+ action->targetList =
+ rewriteTargetListIU(action->targetList,
+ action->commandType,
+ istmt->override,
+ rt_entry_relation,
+ parsetree->resultRelation,
+ NULL);
+ }
+ break;
+ default:
+ elog(ERROR, "unrecognized commandType: %d", action->commandType);
+ break;
+ }
+ }
+ }
else if (event == CMD_DELETE)
{
/* Nothing to do here */
@@ -3350,13 +3442,19 @@ RewriteQuery(Query *parsetree, List *rewrite_events)
locks = matchLocks(event, rt_entry_relation->rd_rules,
result_relation, parsetree, &hasUpdate);
- product_queries = fireRules(parsetree,
- result_relation,
- event,
- locks,
- &instead,
- &returning,
- &qual_product);
+ /*
+ * MERGE doesn't support rules as it's unclear how that could work.
+ */
+ if (event == CMD_MERGE)
+ product_queries = NIL;
+ else
+ product_queries = fireRules(parsetree,
+ result_relation,
+ event,
+ locks,
+ &instead,
+ &returning,
+ &qual_product);
/*
* If there were no INSTEAD rules, and the target relation is a view
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
index ce77a18bc9..6e85886e64 100644
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -379,6 +379,95 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
}
}
+ /*
+ * FOR MERGE, we fetch policies for UPDATE, DELETE and INSERT (and ALL)
+ * and set them up so that we can enforce the appropriate policy depending
+ * on the final action we take.
+ *
+ * We don't fetch the SELECT policies since they are correctly applied to
+ * the root->mergeTarget_relation. The target rows are selected after
+ * joining the mergeTarget_relation and the source relation and hence it's
+ * enough to apply SELECT policies to the mergeTarget_relation.
+ *
+ * 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
+ * know whether we will be doing a UPDATE or a DELETE at the end. We apply
+ * the respective policy once we decide the final action on the target
+ * tuple.
+ *
+ * XXX We are setting up USING quals as WITH CHECK. If RLS prohibits
+ * UPDATE/DELETE on the target row, we shall throw an error instead of
+ * silently ignoring the row. This is different than how normal
+ * UPDATE/DELETE works and more in line with INSERT ON CONFLICT DO UPDATE
+ * handling.
+ */
+ if (commandType == CMD_MERGE)
+ {
+ List *merge_permissive_policies;
+ List *merge_restrictive_policies;
+
+ /*
+ * Fetch the UPDATE policies and set them up to execute on the
+ * existing target row before doing UPDATE.
+ */
+ get_policies_for_relation(rel, CMD_UPDATE, user_id,
+ &merge_permissive_policies,
+ &merge_restrictive_policies);
+
+ /*
+ * WCO_RLS_MERGE_UPDATE_CHECK is used to check UPDATE USING quals on
+ * the existing target row.
+ */
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_MERGE_UPDATE_CHECK,
+ merge_permissive_policies,
+ merge_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
+
+ /*
+ * Same with DELETE policies.
+ */
+ get_policies_for_relation(rel, CMD_DELETE, user_id,
+ &merge_permissive_policies,
+ &merge_restrictive_policies);
+
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_MERGE_DELETE_CHECK,
+ merge_permissive_policies,
+ merge_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ true);
+
+ /*
+ * No special handling is required for INSERT policies. They will be
+ * checked and enforced during ExecInsert(). But we must add them to
+ * withCheckOptions.
+ */
+ get_policies_for_relation(rel, CMD_INSERT, user_id,
+ &merge_permissive_policies,
+ &merge_restrictive_policies);
+
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_INSERT_CHECK,
+ merge_permissive_policies,
+ merge_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ false);
+
+ /* Enforce the WITH CHECK clauses of the UPDATE policies */
+ add_with_check_options(rel, rt_index,
+ WCO_RLS_UPDATE_CHECK,
+ merge_permissive_policies,
+ merge_restrictive_policies,
+ withCheckOptions,
+ hasSubLinks,
+ false);
+ }
+
heap_close(rel, NoLock);
/*
@@ -438,6 +527,14 @@ get_policies_for_relation(Relation relation, CmdType cmd, Oid user_id,
if (policy->polcmd == ACL_DELETE_CHR)
cmd_matches = true;
break;
+ case CMD_MERGE:
+
+ /*
+ * We do not support a separate policy for MERGE command.
+ * Instead it derives from the policies defined for other
+ * commands.
+ */
+ break;
default:
elog(ERROR, "unrecognized policy command type %d",
(int) cmd);
diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c
index 66cc5c35c6..50f852a4aa 100644
--- a/src/backend/tcop/pquery.c
+++ b/src/backend/tcop/pquery.c
@@ -193,6 +193,11 @@ ProcessQuery(PlannedStmt *plan,
"DELETE " UINT64_FORMAT,
queryDesc->estate->es_processed);
break;
+ case CMD_MERGE:
+ snprintf(completionTag, COMPLETION_TAG_BUFSIZE,
+ "MERGE " UINT64_FORMAT,
+ queryDesc->estate->es_processed);
+ break;
default:
strcpy(completionTag, "???");
break;
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index ed55521a0c..2fdeb5cf2b 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -110,6 +110,7 @@ CommandIsReadOnly(PlannedStmt *pstmt)
case CMD_UPDATE:
case CMD_INSERT:
case CMD_DELETE:
+ case CMD_MERGE:
return false;
case CMD_UTILITY:
/* For now, treat all utility commands as read/write */
@@ -1833,6 +1834,8 @@ QueryReturnsTuples(Query *parsetree)
case CMD_SELECT:
/* returns tuples */
return true;
+ case CMD_MERGE:
+ return false;
case CMD_INSERT:
case CMD_UPDATE:
case CMD_DELETE:
@@ -2077,6 +2080,10 @@ CreateCommandTag(Node *parsetree)
tag = "UPDATE";
break;
+ case T_MergeStmt:
+ tag = "MERGE";
+ break;
+
case T_SelectStmt:
tag = "SELECT";
break;
@@ -2820,6 +2827,9 @@ CreateCommandTag(Node *parsetree)
case CMD_DELETE:
tag = "DELETE";
break;
+ case CMD_MERGE:
+ tag = "MERGE";
+ break;
case CMD_UTILITY:
tag = CreateCommandTag(stmt->utilityStmt);
break;
@@ -2880,6 +2890,9 @@ CreateCommandTag(Node *parsetree)
case CMD_DELETE:
tag = "DELETE";
break;
+ case CMD_MERGE:
+ tag = "MERGE";
+ break;
case CMD_UTILITY:
tag = CreateCommandTag(stmt->utilityStmt);
break;
@@ -2928,6 +2941,7 @@ GetCommandLogLevel(Node *parsetree)
case T_InsertStmt:
case T_DeleteStmt:
case T_UpdateStmt:
+ case T_MergeStmt:
lev = LOGSTMT_MOD;
break;
@@ -3367,6 +3381,7 @@ GetCommandLogLevel(Node *parsetree)
case CMD_UPDATE:
case CMD_INSERT:
case CMD_DELETE:
+ case CMD_MERGE:
lev = LOGSTMT_MOD;
break;
@@ -3397,6 +3412,7 @@ GetCommandLogLevel(Node *parsetree)
case CMD_UPDATE:
case CMD_INSERT:
case CMD_DELETE:
+ case CMD_MERGE:
lev = LOGSTMT_MOD;
break;
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 4c0256b18a..100174138d 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -67,6 +67,7 @@ typedef enum LockTupleMode
*/
typedef struct HeapUpdateFailureData
{
+ HTSU_Result result;
ItemPointerData ctid;
TransactionId xmax;
CommandId cmax;
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index ff5546cf28..3c316c892c 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -205,7 +205,8 @@ extern bool ExecBRDeleteTriggers(EState *estate,
EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
- HeapTuple fdw_trigtuple);
+ HeapTuple fdw_trigtuple,
+ HeapUpdateFailureData *hufdp);
extern void ExecARDeleteTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
@@ -224,7 +225,8 @@ extern TupleTableSlot *ExecBRUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
- TupleTableSlot *slot);
+ TupleTableSlot *slot,
+ HeapUpdateFailureData *hufdp);
extern void ExecARUpdateTriggers(EState *estate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
diff --git a/src/include/executor/execPartition.h b/src/include/executor/execPartition.h
index 03a599ad57..9f55f6409e 100644
--- a/src/include/executor/execPartition.h
+++ b/src/include/executor/execPartition.h
@@ -114,6 +114,7 @@ extern int ExecFindPartition(ResultRelInfo *resultRelInfo,
PartitionDispatch *pd,
TupleTableSlot *slot,
EState *estate);
+extern int ExecFindPartitionByOid(PartitionTupleRouting *proute, Oid partoid);
extern ResultRelInfo *ExecInitPartitionInfo(ModifyTableState *mtstate,
ResultRelInfo *resultRelInfo,
PartitionTupleRouting *proute,
diff --git a/src/include/executor/nodeMerge.h b/src/include/executor/nodeMerge.h
new file mode 100644
index 0000000000..c222e9ee65
--- /dev/null
+++ b/src/include/executor/nodeMerge.h
@@ -0,0 +1,22 @@
+/*-------------------------------------------------------------------------
+ *
+ * nodeMerge.h
+ *
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/executor/nodeMerge.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef NODEMERGE_H
+#define NODEMERGE_H
+
+#include "nodes/execnodes.h"
+
+extern void
+ExecMerge(ModifyTableState *mtstate, EState *estate, TupleTableSlot *slot,
+ JunkFilter *junkfilter, ResultRelInfo *resultRelInfo);
+
+#endif /* NODEMERGE_H */
diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h
index 0d7e579e1c..686cfa6171 100644
--- a/src/include/executor/nodeModifyTable.h
+++ b/src/include/executor/nodeModifyTable.h
@@ -18,5 +18,26 @@
extern ModifyTableState *ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags);
extern void ExecEndModifyTable(ModifyTableState *node);
extern void ExecReScanModifyTable(ModifyTableState *node);
+extern TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
+ EState *estate,
+ struct PartitionTupleRouting *proute,
+ ResultRelInfo *targetRelInfo,
+ TupleTableSlot *slot);
+extern TupleTableSlot *ExecDelete(ModifyTableState *mtstate,
+ ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *planSlot,
+ EPQState *epqstate, EState *estate, bool *tupleDeleted,
+ bool processReturning, HeapUpdateFailureData *hufdp,
+ MergeActionState *actionState, bool canSetTag);
+extern TupleTableSlot *ExecUpdate(ModifyTableState *mtstate,
+ ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot,
+ TupleTableSlot *planSlot, EPQState *epqstate, EState *estate,
+ bool *tuple_updated, HeapUpdateFailureData *hufdp,
+ MergeActionState *actionState, bool canSetTag);
+extern TupleTableSlot *ExecInsert(ModifyTableState *mtstate,
+ TupleTableSlot *slot,
+ TupleTableSlot *planSlot,
+ EState *estate,
+ MergeActionState *actionState,
+ bool canSetTag);
#endif /* NODEMODIFYTABLE_H */
diff --git a/src/include/executor/spi.h b/src/include/executor/spi.h
index e5bdaecc4e..78410b9f77 100644
--- a/src/include/executor/spi.h
+++ b/src/include/executor/spi.h
@@ -64,6 +64,7 @@ typedef struct _SPI_plan *SPIPlanPtr;
#define SPI_OK_REL_REGISTER 15
#define SPI_OK_REL_UNREGISTER 16
#define SPI_OK_TD_REGISTER 17
+#define SPI_OK_MERGE 18
#define SPI_OPT_NONATOMIC (1 << 0)
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index a4574cd533..dbfb5d2a1a 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -444,5 +444,6 @@ extern bool has_rolreplication(Oid roleid);
/* in access/transam/xlog.c */
extern bool BackupInProgress(void);
extern void CancelBackup(void);
+extern int64 GetXactWALBytes(void);
#endif /* MISCADMIN_H */
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index d9e591802f..36e6584c38 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -348,8 +348,19 @@ typedef struct JunkFilter
AttrNumber *jf_cleanMap;
TupleTableSlot *jf_resultSlot;
AttrNumber jf_junkAttNo;
+ AttrNumber jf_otherJunkAttNo;
} JunkFilter;
+typedef struct MergeState
+{
+ /* List of MERGE MATCHED action states */
+ List *matchedActionStates;
+ /* List of MERGE NOT MATCHED action states */
+ List *notMatchedActionStates;
+ /* Slot for MERGE actions */
+ TupleTableSlot *mergeSlot;
+} MergeState;
+
/*
* ResultRelInfo
*
@@ -426,6 +437,12 @@ typedef struct ResultRelInfo
/* relation descriptor for root partitioned table */
Relation ri_PartitionRoot;
+
+ int ri_PartitionLeafIndex;
+ /* for running MERGE on this result relation */
+ MergeState *ri_mergeState;
+ /* RTI of the target relation for MERGE */
+ Index ri_mergeTargetRTI;
} ResultRelInfo;
/* ----------------
@@ -970,6 +987,24 @@ typedef struct ProjectSetState
MemoryContext argcontext; /* context for SRF arguments */
} ProjectSetState;
+/* ----------------
+ * MergeActionState information
+ * ----------------
+ */
+typedef struct MergeActionState
+{
+ NodeTag type;
+ bool matched; /* true if a WHEN MATCHED action,
+ * false if a NOT MATCHED action. */
+ ExprState *whenqual; /* additional conditions attached to
+ * WHEN [NOT] MATCHED clause */
+ CmdType commandType; /* INSERT/UPDATE/DELETE/DO NOTHING */
+ ProjectionInfo *proj; /* projection information for the tuple
+ * produced by this action */
+ TupleDesc tupDesc; /* tuple descriptor associated with the
+ * projection */
+} MergeActionState;
+
/* ----------------
* ModifyTableState information
* ----------------
@@ -977,7 +1012,7 @@ typedef struct ProjectSetState
typedef struct ModifyTableState
{
PlanState ps; /* its first field is NodeTag */
- CmdType operation; /* INSERT, UPDATE, or DELETE */
+ CmdType operation; /* INSERT, UPDATE, DELETE or MERGE */
bool canSetTag; /* do we set the command tag/es_processed? */
bool mt_done; /* are we done? */
PlanState **mt_plans; /* subplans (one per target rel) */
@@ -1004,6 +1039,9 @@ typedef struct ModifyTableState
/* Per plan map for tuple conversion from child to root */
TupleConversionMap **mt_per_subplan_tupconv_maps;
+
+ AclMode mt_merge_subcommands; /* Flags show which cmd types are
+ * present */
} ModifyTableState;
/* ----------------
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index 74b094a9c3..8e960a8a3b 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -96,6 +96,7 @@ typedef enum NodeTag
T_PlanState,
T_ResultState,
T_ProjectSetState,
+ T_MergeActionState,
T_ModifyTableState,
T_AppendState,
T_MergeAppendState,
@@ -307,6 +308,8 @@ typedef enum NodeTag
T_InsertStmt,
T_DeleteStmt,
T_UpdateStmt,
+ T_MergeStmt,
+ T_MergeAction,
T_SelectStmt,
T_AlterTableStmt,
T_AlterTableCmd,
@@ -656,7 +659,8 @@ typedef enum CmdType
CMD_SELECT, /* select stmt */
CMD_UPDATE, /* update stmt */
CMD_INSERT, /* insert stmt */
- CMD_DELETE,
+ CMD_DELETE, /* delete stmt */
+ CMD_MERGE, /* merge stmt */
CMD_UTILITY, /* cmds like create, destroy, copy, vacuum,
* etc. */
CMD_NOTHING /* dummy command for instead nothing rules
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 92082b3a7a..0c904f4d7f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -38,7 +38,7 @@ typedef enum OverridingKind
typedef enum QuerySource
{
QSRC_ORIGINAL, /* original parsetree (explicit query) */
- QSRC_PARSER, /* added by parse analysis (now unused) */
+ QSRC_PARSER, /* added by parse analysis in MERGE */
QSRC_INSTEAD_RULE, /* added by unconditional INSTEAD rule */
QSRC_QUAL_INSTEAD_RULE, /* added by conditional INSTEAD rule */
QSRC_NON_INSTEAD_RULE /* added by non-INSTEAD rule */
@@ -107,7 +107,7 @@ typedef struct Query
{
NodeTag type;
- CmdType commandType; /* select|insert|update|delete|utility */
+ CmdType commandType; /* select|insert|update|delete|merge|utility */
QuerySource querySource; /* where did I come from? */
@@ -118,7 +118,7 @@ typedef struct Query
Node *utilityStmt; /* non-null if commandType == CMD_UTILITY */
int resultRelation; /* rtable index of target relation for
- * INSERT/UPDATE/DELETE; 0 for SELECT */
+ * INSERT/UPDATE/DELETE/MERGE; 0 for SELECT */
bool hasAggs; /* has aggregates in tlist or havingQual */
bool hasWindowFuncs; /* has window functions in tlist */
@@ -169,6 +169,9 @@ typedef struct Query
List *withCheckOptions; /* a list of WithCheckOption's, which are
* only added during rewrite and therefore
* are not written out as part of Query. */
+ int mergeTarget_relation;
+ List *mergeSourceTargetList;
+ List *mergeActionList; /* list of actions for MERGE (only) */
/*
* The following two fields identify the portion of the source text string
@@ -1128,7 +1131,9 @@ typedef enum WCOKind
WCO_VIEW_CHECK, /* WCO on an auto-updatable view */
WCO_RLS_INSERT_CHECK, /* RLS INSERT WITH CHECK policy */
WCO_RLS_UPDATE_CHECK, /* RLS UPDATE WITH CHECK policy */
- WCO_RLS_CONFLICT_CHECK /* RLS ON CONFLICT DO UPDATE USING policy */
+ WCO_RLS_CONFLICT_CHECK, /* RLS ON CONFLICT DO UPDATE USING policy */
+ WCO_RLS_MERGE_UPDATE_CHECK, /* RLS MERGE UPDATE USING policy */
+ WCO_RLS_MERGE_DELETE_CHECK /* RLS MERGE DELETE USING policy */
} WCOKind;
typedef struct WithCheckOption
@@ -1503,6 +1508,32 @@ typedef struct UpdateStmt
WithClause *withClause; /* WITH clause */
} UpdateStmt;
+/* ----------------------
+ * Merge Statement
+ * ----------------------
+ */
+typedef struct MergeStmt
+{
+ NodeTag type;
+ RangeVar *relation; /* target relation to merge */
+ Node *source_relation; /* source relation */
+ Node *join_condition; /* join condition between source and target */
+ List *mergeActionList; /* list of MergeAction(s) */
+} MergeStmt;
+
+typedef struct MergeAction
+{
+ NodeTag type;
+ bool matched; /* true if a WHEN MATCHED action,
+ * false if a WHEN NOT MATCHED action */
+ Node *condition; /* WHEN AND conditions (raw parser) */
+ Node *qual; /* transformed WHEN AND conditions */
+ CmdType commandType; /* type of action - INSERT/UPDATE/DELETE/DO
+ * NOTHING */
+ Node *stmt; /* T_UpdateStmt etc */
+ List *targetList; /* the target list (of ResTarget) */
+} MergeAction;
+
/* ----------------------
* Select Statement
*
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index f2e19eae68..52bb9b7aa3 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -18,6 +18,7 @@
#include "lib/stringinfo.h"
#include "nodes/bitmapset.h"
#include "nodes/lockoptions.h"
+#include "nodes/parsenodes.h"
#include "nodes/primnodes.h"
@@ -42,7 +43,7 @@ typedef struct PlannedStmt
{
NodeTag type;
- CmdType commandType; /* select|insert|update|delete|utility */
+ CmdType commandType; /* select|insert|update|delete|merge|utility */
uint64 queryId; /* query identifier (copied from Query) */
@@ -214,13 +215,14 @@ typedef struct ProjectSet
typedef struct ModifyTable
{
Plan plan;
- CmdType operation; /* INSERT, UPDATE, or DELETE */
+ CmdType operation; /* INSERT, UPDATE, DELETE or MERGE */
bool canSetTag; /* do we set the command tag/es_processed? */
Index nominalRelation; /* Parent RT index for use of EXPLAIN */
/* RT indexes of non-leaf tables in a partition tree */
List *partitioned_rels;
bool partColsUpdated; /* some part key in hierarchy updated */
List *resultRelations; /* integer list of RT indexes */
+ Index mergeTargetRelation; /* RT index of the merge target */
int resultRelIndex; /* index of first resultRel in plan's list */
int rootResultRelIndex; /* index of the partitioned table root */
List *plans; /* plan(s) producing source data */
@@ -236,6 +238,8 @@ typedef struct ModifyTable
Node *onConflictWhere; /* WHERE for ON CONFLICT UPDATE */
Index exclRelRTI; /* RTI of the EXCLUDED pseudo relation */
List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */
+ List *mergeSourceTargetList;
+ List *mergeActionList; /* actions for MERGE */
} ModifyTable;
/* ----------------
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index abbbda9e91..91dfff4cb5 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -1670,7 +1670,7 @@ typedef struct LockRowsPath
} LockRowsPath;
/*
- * ModifyTablePath represents performing INSERT/UPDATE/DELETE modifications
+ * ModifyTablePath represents performing INSERT/UPDATE/DELETE/MERGE
*
* We represent most things that will be in the ModifyTable plan node
* literally, except we have child Path(s) not Plan(s). But analysis of the
@@ -1679,13 +1679,14 @@ typedef struct LockRowsPath
typedef struct ModifyTablePath
{
Path path;
- CmdType operation; /* INSERT, UPDATE, or DELETE */
+ CmdType operation; /* INSERT, UPDATE, DELETE or MERGE */
bool canSetTag; /* do we set the command tag/es_processed? */
Index nominalRelation; /* Parent RT index for use of EXPLAIN */
/* RT indexes of non-leaf tables in a partition tree */
List *partitioned_rels;
bool partColsUpdated; /* some part key in hierarchy updated */
List *resultRelations; /* integer list of RT indexes */
+ Index mergeTargetRelation;/* RT index of merge target relation */
List *subpaths; /* Path(s) producing source data */
List *subroots; /* per-target-table PlannerInfos */
List *withCheckOptionLists; /* per-target-table WCO lists */
@@ -1693,6 +1694,8 @@ typedef struct ModifyTablePath
List *rowMarks; /* PlanRowMarks (non-locking only) */
OnConflictExpr *onconflict; /* ON CONFLICT clause, or NULL */
int epqParam; /* ID of Param for EvalPlanQual re-eval */
+ List *mergeSourceTargetList;
+ List *mergeActionList; /* actions for MERGE */
} ModifyTablePath;
/*
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 381bc30813..895bf6959d 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -241,11 +241,14 @@ extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
CmdType operation, bool canSetTag,
Index nominalRelation, List *partitioned_rels,
bool partColsUpdated,
- List *resultRelations, List *subpaths,
+ List *resultRelations,
+ Index mergeTargetRelation,
+ List *subpaths,
List *subroots,
List *withCheckOptionLists, List *returningLists,
List *rowMarks, OnConflictExpr *onconflict,
- int epqParam);
+ List *mergeSourceTargetList,
+ List *mergeActionList, int epqParam);
extern LimitPath *create_limit_path(PlannerInfo *root, RelOptInfo *rel,
Path *subpath,
Node *limitOffset, Node *limitCount,
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index 687ae1b5b7..41fb10666e 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -32,6 +32,11 @@ extern Query *parse_sub_analyze(Node *parseTree, ParseState *parentParseState,
bool locked_from_parent,
bool resolve_unknowns);
+extern List *transformInsertRow(ParseState *pstate, List *exprlist,
+ List *stmtcols, List *icolumns, List *attrnos,
+ bool strip_indirection);
+extern List *transformUpdateTargetList(ParseState *pstate,
+ List *targetList);
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
index cf32197bc3..4dff55a8e9 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -244,8 +244,10 @@ PG_KEYWORD("locked", LOCKED, UNRESERVED_KEYWORD)
PG_KEYWORD("logged", LOGGED, UNRESERVED_KEYWORD)
PG_KEYWORD("mapping", MAPPING, UNRESERVED_KEYWORD)
PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD)
+PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD)
PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD)
PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD)
+PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD)
PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD)
PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD)
PG_KEYWORD("minvalue", MINVALUE, UNRESERVED_KEYWORD)
diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h
index 2c0e092862..30121c98ed 100644
--- a/src/include/parser/parse_clause.h
+++ b/src/include/parser/parse_clause.h
@@ -20,7 +20,10 @@ extern void transformFromClause(ParseState *pstate, List *frmList);
extern int setTargetTable(ParseState *pstate, RangeVar *relation,
bool inh, bool alsoSource, AclMode requiredPerms);
extern bool interpretOidsOption(List *defList, bool allowOids);
-
+extern Node *transformFromClauseItem(ParseState *pstate, Node *n,
+ RangeTblEntry **top_rte, int *top_rti,
+ RangeTblEntry **right_rte, int *right_rti,
+ List **namespace);
extern Node *transformWhereClause(ParseState *pstate, Node *clause,
ParseExprKind exprKind, const char *constructName);
extern Node *transformLimitClause(ParseState *pstate, Node *clause,
diff --git a/src/include/parser/parse_merge.h b/src/include/parser/parse_merge.h
new file mode 100644
index 0000000000..0151809e09
--- /dev/null
+++ b/src/include/parser/parse_merge.h
@@ -0,0 +1,19 @@
+/*-------------------------------------------------------------------------
+ *
+ * parse_merge.h
+ * handle merge-stmt in parser
+ *
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/parser/parse_merge.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PARSE_MERGE_H
+#define PARSE_MERGE_H
+
+#include "parser/parse_node.h"
+extern Query *transformMergeStmt(ParseState *pstate, MergeStmt *stmt);
+#endif
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index 0230543810..8b80e79fd2 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -50,6 +50,7 @@ typedef enum ParseExprKind
EXPR_KIND_INSERT_TARGET, /* INSERT target list item */
EXPR_KIND_UPDATE_SOURCE, /* UPDATE assignment source item */
EXPR_KIND_UPDATE_TARGET, /* UPDATE assignment target item */
+ EXPR_KIND_MERGE_WHEN_AND, /* MERGE WHEN ... AND condition */
EXPR_KIND_GROUP_BY, /* GROUP BY */
EXPR_KIND_ORDER_BY, /* ORDER BY */
EXPR_KIND_DISTINCT_ON, /* DISTINCT ON */
@@ -127,7 +128,8 @@ typedef Node *(*CoerceParamHook) (ParseState *pstate, Param *param,
* p_parent_cte: CommonTableExpr that immediately contains the current query,
* if any.
*
- * p_target_relation: target relation, if query is INSERT, UPDATE, or DELETE.
+ * p_target_relation: target relation, if query is INSERT, UPDATE, DELETE
+ * or MERGE.
*
* p_target_rangetblentry: target relation's entry in the rtable list.
*
@@ -181,7 +183,7 @@ struct ParseState
List *p_ctenamespace; /* current namespace for common table exprs */
List *p_future_ctes; /* common table exprs not yet in namespace */
CommonTableExpr *p_parent_cte; /* this query's containing CTE */
- Relation p_target_relation; /* INSERT/UPDATE/DELETE target rel */
+ Relation p_target_relation; /* INSERT/UPDATE/DELETE/MERGE target rel */
RangeTblEntry *p_target_rangetblentry; /* target rel's RTE */
bool p_is_insert; /* process assignment like INSERT not UPDATE */
List *p_windowdefs; /* raw representations of window clauses */
diff --git a/src/include/rewrite/rewriteHandler.h b/src/include/rewrite/rewriteHandler.h
index 8128199fc3..1ab5de3942 100644
--- a/src/include/rewrite/rewriteHandler.h
+++ b/src/include/rewrite/rewriteHandler.h
@@ -25,6 +25,7 @@ extern void AcquireRewriteLocks(Query *parsetree,
extern Node *build_column_default(Relation rel, int attrno);
extern void rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
Relation target_relation);
+extern void rewriteTargetListMerge(Query *parsetree, Relation target_relation);
extern Query *get_view_query(Relation view);
extern const char *view_query_is_auto_updatable(Query *viewquery,
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index 4c0114c514..a432636322 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -3055,9 +3055,9 @@ PQoidValue(const PGresult *res)
/*
* PQcmdTuples -
- * If the last command was INSERT/UPDATE/DELETE/MOVE/FETCH/COPY, return
- * a string containing the number of inserted/affected tuples. If not,
- * return "".
+ * If the last command was INSERT/UPDATE/DELETE/MERGE/MOVE/FETCH/COPY,
+ * return a string containing the number of inserted/affected tuples.
+ * If not, return "".
*
* XXX: this should probably return an int
*/
@@ -3084,7 +3084,8 @@ PQcmdTuples(PGresult *res)
strncmp(res->cmdStatus, "DELETE ", 7) == 0 ||
strncmp(res->cmdStatus, "UPDATE ", 7) == 0)
p = res->cmdStatus + 7;
- else if (strncmp(res->cmdStatus, "FETCH ", 6) == 0)
+ else if (strncmp(res->cmdStatus, "FETCH ", 6) == 0 ||
+ strncmp(res->cmdStatus, "MERGE ", 6) == 0)
p = res->cmdStatus + 6;
else if (strncmp(res->cmdStatus, "MOVE ", 5) == 0 ||
strncmp(res->cmdStatus, "COPY ", 5) == 0)
diff --git a/src/pl/plpgsql/src/pl_exec.c b/src/pl/plpgsql/src/pl_exec.c
index 38ea7a091f..8a1d32a95c 100644
--- a/src/pl/plpgsql/src/pl_exec.c
+++ b/src/pl/plpgsql/src/pl_exec.c
@@ -3932,7 +3932,7 @@ exec_stmt_execsql(PLpgSQL_execstate *estate,
/*
* On the first call for this statement generate the plan, and detect
- * whether the statement is INSERT/UPDATE/DELETE
+ * whether the statement is INSERT/UPDATE/DELETE/MERGE
*/
if (expr->plan == NULL)
{
@@ -3953,6 +3953,7 @@ exec_stmt_execsql(PLpgSQL_execstate *estate,
{
if (q->commandType == CMD_INSERT ||
q->commandType == CMD_UPDATE ||
+ q->commandType == CMD_MERGE ||
q->commandType == CMD_DELETE)
stmt->mod_stmt = true;
}
@@ -4010,6 +4011,7 @@ exec_stmt_execsql(PLpgSQL_execstate *estate,
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
+ case SPI_OK_MERGE:
Assert(stmt->mod_stmt);
exec_set_found(estate, (SPI_processed != 0));
break;
@@ -4187,6 +4189,7 @@ exec_stmt_dynexecute(PLpgSQL_execstate *estate,
case SPI_OK_INSERT_RETURNING:
case SPI_OK_UPDATE_RETURNING:
case SPI_OK_DELETE_RETURNING:
+ case SPI_OK_MERGE:
case SPI_OK_UTILITY:
case SPI_OK_REWRITTEN:
break;
diff --git a/src/pl/plpgsql/src/pl_gram.y b/src/pl/plpgsql/src/pl_gram.y
index 4c80936678..7d9fb2f039 100644
--- a/src/pl/plpgsql/src/pl_gram.y
+++ b/src/pl/plpgsql/src/pl_gram.y
@@ -303,6 +303,7 @@ static void check_raise_parameters(PLpgSQL_stmt_raise *stmt);
%token <keyword> K_LAST
%token <keyword> K_LOG
%token <keyword> K_LOOP
+%token <keyword> K_MERGE
%token <keyword> K_MESSAGE
%token <keyword> K_MESSAGE_TEXT
%token <keyword> K_MOVE
@@ -1930,6 +1931,10 @@ stmt_execsql : K_IMPORT
{
$$ = make_execsql_stmt(K_INSERT, @1);
}
+ | K_MERGE
+ {
+ $$ = make_execsql_stmt(K_MERGE, @1);
+ }
| T_WORD
{
int tok;
@@ -2451,6 +2456,7 @@ unreserved_keyword :
| K_IS
| K_LAST
| K_LOG
+ | K_MERGE
| K_MESSAGE
| K_MESSAGE_TEXT
| K_MOVE
@@ -2912,6 +2918,8 @@ make_execsql_stmt(int firsttoken, int location)
{
if (prev_tok == K_INSERT)
continue; /* INSERT INTO is not an INTO-target */
+ if (prev_tok == K_MERGE)
+ continue; /* MERGE INTO is not an INTO-target */
if (firsttoken == K_IMPORT)
continue; /* IMPORT ... INTO is not an INTO-target */
if (have_into)
diff --git a/src/pl/plpgsql/src/pl_scanner.c b/src/pl/plpgsql/src/pl_scanner.c
index 65774f9902..85078ac15b 100644
--- a/src/pl/plpgsql/src/pl_scanner.c
+++ b/src/pl/plpgsql/src/pl_scanner.c
@@ -137,6 +137,7 @@ static const ScanKeyword unreserved_keywords[] = {
PG_KEYWORD("is", K_IS, UNRESERVED_KEYWORD)
PG_KEYWORD("last", K_LAST, UNRESERVED_KEYWORD)
PG_KEYWORD("log", K_LOG, UNRESERVED_KEYWORD)
+ PG_KEYWORD("merge", K_MERGE, UNRESERVED_KEYWORD)
PG_KEYWORD("message", K_MESSAGE, UNRESERVED_KEYWORD)
PG_KEYWORD("message_text", K_MESSAGE_TEXT, UNRESERVED_KEYWORD)
PG_KEYWORD("move", K_MOVE, UNRESERVED_KEYWORD)
diff --git a/src/pl/plpgsql/src/plpgsql.h b/src/pl/plpgsql/src/plpgsql.h
index f7619a63f9..8d6ef3326f 100644
--- a/src/pl/plpgsql/src/plpgsql.h
+++ b/src/pl/plpgsql/src/plpgsql.h
@@ -845,8 +845,8 @@ typedef struct PLpgSQL_stmt_execsql
PLpgSQL_stmt_type cmd_type;
int lineno;
PLpgSQL_expr *sqlstmt;
- bool mod_stmt; /* is the stmt INSERT/UPDATE/DELETE? Note:
- * mod_stmt is set when we plan the query */
+ bool mod_stmt; /* is the stmt INSERT/UPDATE/DELETE/MERGE?
+ * Note mod_stmt is set when we plan the query */
bool into; /* INTO supplied? */
bool strict; /* INTO STRICT flag */
PLpgSQL_variable *target; /* INTO target (record or row) */
diff --git a/src/test/isolation/expected/merge-delete.out b/src/test/isolation/expected/merge-delete.out
new file mode 100644
index 0000000000..40e62901b7
--- /dev/null
+++ b/src/test/isolation/expected/merge-delete.out
@@ -0,0 +1,97 @@
+Parsed test spec with 2 sessions
+
+starting permutation: delete c1 select2 c2
+step delete: DELETE FROM target t WHERE t.key = 1;
+step c1: COMMIT;
+step select2: SELECT * FROM target;
+key val
+
+step c2: COMMIT;
+
+starting permutation: merge_delete c1 select2 c2
+step merge_delete: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key WHEN MATCHED THEN DELETE;
+step c1: COMMIT;
+step select2: SELECT * FROM target;
+key val
+
+step c2: COMMIT;
+
+starting permutation: delete c1 update1 select2 c2
+step delete: DELETE FROM target t WHERE t.key = 1;
+step c1: COMMIT;
+step update1: UPDATE target t SET val = t.val || ' updated by update1' WHERE t.key = 1;
+step select2: SELECT * FROM target;
+key val
+
+step c2: COMMIT;
+
+starting permutation: merge_delete c1 update1 select2 c2
+step merge_delete: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key WHEN MATCHED THEN DELETE;
+step c1: COMMIT;
+step update1: UPDATE target t SET val = t.val || ' updated by update1' WHERE t.key = 1;
+step select2: SELECT * FROM target;
+key val
+
+step c2: COMMIT;
+
+starting permutation: delete c1 merge2 select2 c2
+step delete: DELETE FROM target t WHERE t.key = 1;
+step c1: COMMIT;
+step merge2: MERGE INTO target t USING (SELECT 1 as key, 'merge2a' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+step select2: SELECT * FROM target;
+key val
+
+1 merge2a
+step c2: COMMIT;
+
+starting permutation: merge_delete c1 merge2 select2 c2
+step merge_delete: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key WHEN MATCHED THEN DELETE;
+step c1: COMMIT;
+step merge2: MERGE INTO target t USING (SELECT 1 as key, 'merge2a' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+step select2: SELECT * FROM target;
+key val
+
+1 merge2a
+step c2: COMMIT;
+
+starting permutation: delete update1 c1 select2 c2
+step delete: DELETE FROM target t WHERE t.key = 1;
+step update1: UPDATE target t SET val = t.val || ' updated by update1' WHERE t.key = 1; <waiting ...>
+step c1: COMMIT;
+step update1: <... completed>
+step select2: SELECT * FROM target;
+key val
+
+step c2: COMMIT;
+
+starting permutation: merge_delete update1 c1 select2 c2
+step merge_delete: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key WHEN MATCHED THEN DELETE;
+step update1: UPDATE target t SET val = t.val || ' updated by update1' WHERE t.key = 1; <waiting ...>
+step c1: COMMIT;
+step update1: <... completed>
+step select2: SELECT * FROM target;
+key val
+
+step c2: COMMIT;
+
+starting permutation: delete merge2 c1 select2 c2
+step delete: DELETE FROM target t WHERE t.key = 1;
+step merge2: MERGE INTO target t USING (SELECT 1 as key, 'merge2a' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; <waiting ...>
+step c1: COMMIT;
+step merge2: <... completed>
+step select2: SELECT * FROM target;
+key val
+
+1 merge2a
+step c2: COMMIT;
+
+starting permutation: merge_delete merge2 c1 select2 c2
+step merge_delete: MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key WHEN MATCHED THEN DELETE;
+step merge2: MERGE INTO target t USING (SELECT 1 as key, 'merge2a' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; <waiting ...>
+step c1: COMMIT;
+step merge2: <... completed>
+step select2: SELECT * FROM target;
+key val
+
+1 merge2a
+step c2: COMMIT;
diff --git a/src/test/isolation/expected/merge-insert-update.out b/src/test/isolation/expected/merge-insert-update.out
new file mode 100644
index 0000000000..317fa16a3d
--- /dev/null
+++ b/src/test/isolation/expected/merge-insert-update.out
@@ -0,0 +1,84 @@
+Parsed test spec with 2 sessions
+
+starting permutation: merge1 c1 select2 c2
+step merge1: MERGE INTO target t USING (SELECT 1 as key, 'merge1' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge1';
+step c1: COMMIT;
+step select2: SELECT * FROM target;
+key val
+
+1 merge1
+step c2: COMMIT;
+
+starting permutation: merge1 c1 merge2 select2 c2
+step merge1: MERGE INTO target t USING (SELECT 1 as key, 'merge1' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge1';
+step c1: COMMIT;
+step merge2: MERGE INTO target t USING (SELECT 1 as key, 'merge2' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge2';
+step select2: SELECT * FROM target;
+key val
+
+1 merge1 updated by merge2
+step c2: COMMIT;
+
+starting permutation: insert1 merge2 c1 select2 c2
+step insert1: INSERT INTO target VALUES (1, 'insert1');
+step merge2: MERGE INTO target t USING (SELECT 1 as key, 'merge2' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge2'; <waiting ...>
+step c1: COMMIT;
+step merge2: <... completed>
+error in steps c1 merge2: ERROR: duplicate key value violates unique constraint "target_pkey"
+step select2: SELECT * FROM target;
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+step c2: COMMIT;
+
+starting permutation: merge1 merge2 c1 select2 c2
+step merge1: MERGE INTO target t USING (SELECT 1 as key, 'merge1' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge1';
+step merge2: MERGE INTO target t USING (SELECT 1 as key, 'merge2' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge2'; <waiting ...>
+step c1: COMMIT;
+step merge2: <... completed>
+error in steps c1 merge2: ERROR: duplicate key value violates unique constraint "target_pkey"
+step select2: SELECT * FROM target;
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+step c2: COMMIT;
+
+starting permutation: merge1 merge2 a1 select2 c2
+step merge1: MERGE INTO target t USING (SELECT 1 as key, 'merge1' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge1';
+step merge2: MERGE INTO target t USING (SELECT 1 as key, 'merge2' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge2'; <waiting ...>
+step a1: ABORT;
+step merge2: <... completed>
+step select2: SELECT * FROM target;
+key val
+
+1 merge2
+step c2: COMMIT;
+
+starting permutation: delete1 insert1 c1 merge2 select2 c2
+step delete1: DELETE FROM target WHERE key = 1;
+step insert1: INSERT INTO target VALUES (1, 'insert1');
+step c1: COMMIT;
+step merge2: MERGE INTO target t USING (SELECT 1 as key, 'merge2' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge2';
+step select2: SELECT * FROM target;
+key val
+
+1 insert1 updated by merge2
+step c2: COMMIT;
+
+starting permutation: delete1 insert1 merge2 c1 select2 c2
+step delete1: DELETE FROM target WHERE key = 1;
+step insert1: INSERT INTO target VALUES (1, 'insert1');
+step merge2: MERGE INTO target t USING (SELECT 1 as key, 'merge2' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge2'; <waiting ...>
+step c1: COMMIT;
+step merge2: <... completed>
+error in steps c1 merge2: ERROR: duplicate key value violates unique constraint "target_pkey"
+step select2: SELECT * FROM target;
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+step c2: COMMIT;
+
+starting permutation: delete1 insert1 merge2i c1 select2 c2
+step delete1: DELETE FROM target WHERE key = 1;
+step insert1: INSERT INTO target VALUES (1, 'insert1');
+step merge2i: MERGE INTO target t USING (SELECT 1 as key, 'merge2' as val) s ON s.key = t.key WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge2';
+step c1: COMMIT;
+step select2: SELECT * FROM target;
+key val
+
+1 insert1
+step c2: COMMIT;
diff --git a/src/test/isolation/expected/merge-match-recheck.out b/src/test/isolation/expected/merge-match-recheck.out
new file mode 100644
index 0000000000..96a9f45ac8
--- /dev/null
+++ b/src/test/isolation/expected/merge-match-recheck.out
@@ -0,0 +1,106 @@
+Parsed test spec with 2 sessions
+
+starting permutation: update1 merge_status c2 select1 c1
+step update1: UPDATE target t SET balance = balance + 10, val = t.val || ' updated by update1' WHERE t.key = 1;
+step merge_status:
+ MERGE INTO target t
+ USING (SELECT 1 as key) s
+ ON s.key = t.key
+ WHEN MATCHED AND status = 's1' THEN
+ UPDATE SET status = 's2', val = t.val || ' when1'
+ WHEN MATCHED AND status = 's2' THEN
+ UPDATE SET status = 's3', val = t.val || ' when2'
+ WHEN MATCHED AND status = 's3' THEN
+ UPDATE SET status = 's4', val = t.val || ' when3';
+ <waiting ...>
+step c2: COMMIT;
+step merge_status: <... completed>
+step select1: SELECT * FROM target;
+key balance status val
+
+1 170 s2 setup updated by update1 when1
+step c1: COMMIT;
+
+starting permutation: update2 merge_status c2 select1 c1
+step update2: UPDATE target t SET status = 's2', val = t.val || ' updated by update2' WHERE t.key = 1;
+step merge_status:
+ MERGE INTO target t
+ USING (SELECT 1 as key) s
+ ON s.key = t.key
+ WHEN MATCHED AND status = 's1' THEN
+ UPDATE SET status = 's2', val = t.val || ' when1'
+ WHEN MATCHED AND status = 's2' THEN
+ UPDATE SET status = 's3', val = t.val || ' when2'
+ WHEN MATCHED AND status = 's3' THEN
+ UPDATE SET status = 's4', val = t.val || ' when3';
+ <waiting ...>
+step c2: COMMIT;
+step merge_status: <... completed>
+step select1: SELECT * FROM target;
+key balance status val
+
+1 160 s3 setup updated by update2 when2
+step c1: COMMIT;
+
+starting permutation: update3 merge_status c2 select1 c1
+step update3: UPDATE target t SET status = 's3', val = t.val || ' updated by update3' WHERE t.key = 1;
+step merge_status:
+ MERGE INTO target t
+ USING (SELECT 1 as key) s
+ ON s.key = t.key
+ WHEN MATCHED AND status = 's1' THEN
+ UPDATE SET status = 's2', val = t.val || ' when1'
+ WHEN MATCHED AND status = 's2' THEN
+ UPDATE SET status = 's3', val = t.val || ' when2'
+ WHEN MATCHED AND status = 's3' THEN
+ UPDATE SET status = 's4', val = t.val || ' when3';
+ <waiting ...>
+step c2: COMMIT;
+step merge_status: <... completed>
+step select1: SELECT * FROM target;
+key balance status val
+
+1 160 s4 setup updated by update3 when3
+step c1: COMMIT;
+
+starting permutation: update5 merge_status c2 select1 c1
+step update5: UPDATE target t SET status = 's5', val = t.val || ' updated by update5' WHERE t.key = 1;
+step merge_status:
+ MERGE INTO target t
+ USING (SELECT 1 as key) s
+ ON s.key = t.key
+ WHEN MATCHED AND status = 's1' THEN
+ UPDATE SET status = 's2', val = t.val || ' when1'
+ WHEN MATCHED AND status = 's2' THEN
+ UPDATE SET status = 's3', val = t.val || ' when2'
+ WHEN MATCHED AND status = 's3' THEN
+ UPDATE SET status = 's4', val = t.val || ' when3';
+ <waiting ...>
+step c2: COMMIT;
+step merge_status: <... completed>
+step select1: SELECT * FROM target;
+key balance status val
+
+1 160 s5 setup updated by update5
+step c1: COMMIT;
+
+starting permutation: update_bal1 merge_bal c2 select1 c1
+step update_bal1: UPDATE target t SET balance = 50, val = t.val || ' updated by update_bal1' WHERE t.key = 1;
+step merge_bal:
+ MERGE INTO target t
+ USING (SELECT 1 as key) s
+ ON s.key = t.key
+ WHEN MATCHED AND balance < 100 THEN
+ UPDATE SET balance = balance * 2, val = t.val || ' when1'
+ WHEN MATCHED AND balance < 200 THEN
+ UPDATE SET balance = balance * 4, val = t.val || ' when2'
+ WHEN MATCHED AND balance < 300 THEN
+ UPDATE SET balance = balance * 8, val = t.val || ' when3';
+ <waiting ...>
+step c2: COMMIT;
+step merge_bal: <... completed>
+step select1: SELECT * FROM target;
+key balance status val
+
+1 100 s1 setup updated by update_bal1 when1
+step c1: COMMIT;
diff --git a/src/test/isolation/expected/merge-update.out b/src/test/isolation/expected/merge-update.out
new file mode 100644
index 0000000000..60ae42ebd0
--- /dev/null
+++ b/src/test/isolation/expected/merge-update.out
@@ -0,0 +1,213 @@
+Parsed test spec with 2 sessions
+
+starting permutation: merge1 c1 select2 c2
+step merge1:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+
+step c1: COMMIT;
+step select2: SELECT * FROM target;
+key val
+
+2 setup1 updated by merge1
+step c2: COMMIT;
+
+starting permutation: merge1 c1 merge2a select2 c2
+step merge1:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+
+step c1: COMMIT;
+step merge2a:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge2a' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+
+step select2: SELECT * FROM target;
+key val
+
+2 setup1 updated by merge1
+1 merge2a
+step c2: COMMIT;
+
+starting permutation: merge1 merge2a c1 select2 c2
+step merge1:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+
+step merge2a:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge2a' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+ <waiting ...>
+step c1: COMMIT;
+step merge2a: <... completed>
+step select2: SELECT * FROM target;
+key val
+
+2 setup1 updated by merge1
+1 merge2a
+step c2: COMMIT;
+
+starting permutation: merge1 merge2a a1 select2 c2
+step merge1:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+
+step merge2a:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge2a' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+ <waiting ...>
+step a1: ABORT;
+step merge2a: <... completed>
+step select2: SELECT * FROM target;
+key val
+
+2 setup1 updated by merge2a
+step c2: COMMIT;
+
+starting permutation: merge1 merge2b c1 select2 c2
+step merge1:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+
+step merge2b:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge2b' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED AND t.key < 2 THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+ <waiting ...>
+step c1: COMMIT;
+step merge2b: <... completed>
+step select2: SELECT * FROM target;
+key val
+
+2 setup1 updated by merge1
+1 merge2b
+step c2: COMMIT;
+
+starting permutation: merge1 merge2c c1 select2 c2
+step merge1:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+
+step merge2c:
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge2c' as val) s
+ ON s.key = t.key AND t.key < 2
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+ <waiting ...>
+step c1: COMMIT;
+step merge2c: <... completed>
+step select2: SELECT * FROM target;
+key val
+
+2 setup1 updated by merge1
+1 merge2c
+step c2: COMMIT;
+
+starting permutation: pa_merge1 pa_merge2a c1 pa_select2 c2
+step pa_merge1:
+ MERGE INTO pa_target t
+ USING (SELECT 1 as key, 'pa_merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set val = t.val || ' updated by ' || s.val;
+
+step pa_merge2a:
+ MERGE INTO pa_target t
+ USING (SELECT 1 as key, 'pa_merge2a' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+ <waiting ...>
+step c1: COMMIT;
+step pa_merge2a: <... completed>
+step pa_select2: SELECT * FROM pa_target;
+key val
+
+2 initial
+2 initial updated by pa_merge2a
+step c2: COMMIT;
+
+starting permutation: pa_merge2 pa_merge2a c1 pa_select2 c2
+step pa_merge2:
+ MERGE INTO pa_target t
+ USING (SELECT 1 as key, 'pa_merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+
+step pa_merge2a:
+ MERGE INTO pa_target t
+ USING (SELECT 1 as key, 'pa_merge2a' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+ <waiting ...>
+step c1: COMMIT;
+step pa_merge2a: <... completed>
+step pa_select2: SELECT * FROM pa_target;
+key val
+
+1 pa_merge2a
+2 initial
+2 initial updated by pa_merge1
+step c2: COMMIT;
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 74d7d59546..644e071112 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -33,6 +33,10 @@ test: insert-conflict-do-update
test: insert-conflict-do-update-2
test: insert-conflict-do-update-3
test: insert-conflict-toast
+test: merge-insert-update
+test: merge-delete
+test: merge-update
+test: merge-match-recheck
test: delete-abort-savept
test: delete-abort-savept-2
test: aborted-keyrevoke
diff --git a/src/test/isolation/specs/merge-delete.spec b/src/test/isolation/specs/merge-delete.spec
new file mode 100644
index 0000000000..656954f847
--- /dev/null
+++ b/src/test/isolation/specs/merge-delete.spec
@@ -0,0 +1,51 @@
+# MERGE DELETE
+#
+# This test looks at the interactions involving concurrent deletes
+# comparing the behavior of MERGE, DELETE and UPDATE
+
+setup
+{
+ CREATE TABLE target (key int primary key, val text);
+ INSERT INTO target VALUES (1, 'setup1');
+}
+
+teardown
+{
+ DROP TABLE target;
+}
+
+session "s1"
+setup
+{
+ BEGIN ISOLATION LEVEL READ COMMITTED;
+}
+step "delete" { DELETE FROM target t WHERE t.key = 1; }
+step "merge_delete" { MERGE INTO target t USING (SELECT 1 as key) s ON s.key = t.key WHEN MATCHED THEN DELETE; }
+step "c1" { COMMIT; }
+step "a1" { ABORT; }
+
+session "s2"
+setup
+{
+ BEGIN ISOLATION LEVEL READ COMMITTED;
+}
+step "update1" { UPDATE target t SET val = t.val || ' updated by update1' WHERE t.key = 1; }
+step "merge2" { MERGE INTO target t USING (SELECT 1 as key, 'merge2a' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; }
+step "select2" { SELECT * FROM target; }
+step "c2" { COMMIT; }
+
+# Basic effects
+permutation "delete" "c1" "select2" "c2"
+permutation "merge_delete" "c1" "select2" "c2"
+
+# One after the other, no concurrency
+permutation "delete" "c1" "update1" "select2" "c2"
+permutation "merge_delete" "c1" "update1" "select2" "c2"
+permutation "delete" "c1" "merge2" "select2" "c2"
+permutation "merge_delete" "c1" "merge2" "select2" "c2"
+
+# Now with concurrency
+permutation "delete" "update1" "c1" "select2" "c2"
+permutation "merge_delete" "update1" "c1" "select2" "c2"
+permutation "delete" "merge2" "c1" "select2" "c2"
+permutation "merge_delete" "merge2" "c1" "select2" "c2"
diff --git a/src/test/isolation/specs/merge-insert-update.spec b/src/test/isolation/specs/merge-insert-update.spec
new file mode 100644
index 0000000000..704492be1f
--- /dev/null
+++ b/src/test/isolation/specs/merge-insert-update.spec
@@ -0,0 +1,52 @@
+# MERGE INSERT UPDATE
+#
+# This looks at how we handle concurrent INSERTs, illustrating how the
+# behavior differs from INSERT ... ON CONFLICT
+
+setup
+{
+ CREATE TABLE target (key int primary key, val text);
+}
+
+teardown
+{
+ DROP TABLE target;
+}
+
+session "s1"
+setup
+{
+ BEGIN ISOLATION LEVEL READ COMMITTED;
+}
+step "merge1" { MERGE INTO target t USING (SELECT 1 as key, 'merge1' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge1'; }
+step "delete1" { DELETE FROM target WHERE key = 1; }
+step "insert1" { INSERT INTO target VALUES (1, 'insert1'); }
+step "c1" { COMMIT; }
+step "a1" { ABORT; }
+
+session "s2"
+setup
+{
+ BEGIN ISOLATION LEVEL READ COMMITTED;
+}
+step "merge2" { MERGE INTO target t USING (SELECT 1 as key, 'merge2' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge2'; }
+
+step "merge2i" { MERGE INTO target t USING (SELECT 1 as key, 'merge2' as val) s ON s.key = t.key WHEN MATCHED THEN UPDATE set val = t.val || ' updated by merge2'; }
+
+step "select2" { SELECT * FROM target; }
+step "c2" { COMMIT; }
+step "a2" { ABORT; }
+
+# Basic effects
+permutation "merge1" "c1" "select2" "c2"
+permutation "merge1" "c1" "merge2" "select2" "c2"
+
+# check concurrent inserts
+permutation "insert1" "merge2" "c1" "select2" "c2"
+permutation "merge1" "merge2" "c1" "select2" "c2"
+permutation "merge1" "merge2" "a1" "select2" "c2"
+
+# check how we handle when visible row has been concurrently deleted, then same key re-inserted
+permutation "delete1" "insert1" "c1" "merge2" "select2" "c2"
+permutation "delete1" "insert1" "merge2" "c1" "select2" "c2"
+permutation "delete1" "insert1" "merge2i" "c1" "select2" "c2"
diff --git a/src/test/isolation/specs/merge-match-recheck.spec b/src/test/isolation/specs/merge-match-recheck.spec
new file mode 100644
index 0000000000..193033da17
--- /dev/null
+++ b/src/test/isolation/specs/merge-match-recheck.spec
@@ -0,0 +1,79 @@
+# MERGE MATCHED RECHECK
+#
+# This test looks at what happens when we have complex
+# WHEN MATCHED AND conditions and a concurrent UPDATE causes a
+# recheck of the AND condition on the new row
+
+setup
+{
+ CREATE TABLE target (key int primary key, balance integer, status text, val text);
+ INSERT INTO target VALUES (1, 160, 's1', 'setup');
+}
+
+teardown
+{
+ DROP TABLE target;
+}
+
+session "s1"
+setup
+{
+ BEGIN ISOLATION LEVEL READ COMMITTED;
+}
+step "merge_status"
+{
+ MERGE INTO target t
+ USING (SELECT 1 as key) s
+ ON s.key = t.key
+ WHEN MATCHED AND status = 's1' THEN
+ UPDATE SET status = 's2', val = t.val || ' when1'
+ WHEN MATCHED AND status = 's2' THEN
+ UPDATE SET status = 's3', val = t.val || ' when2'
+ WHEN MATCHED AND status = 's3' THEN
+ UPDATE SET status = 's4', val = t.val || ' when3';
+}
+
+step "merge_bal"
+{
+ MERGE INTO target t
+ USING (SELECT 1 as key) s
+ ON s.key = t.key
+ WHEN MATCHED AND balance < 100 THEN
+ UPDATE SET balance = balance * 2, val = t.val || ' when1'
+ WHEN MATCHED AND balance < 200 THEN
+ UPDATE SET balance = balance * 4, val = t.val || ' when2'
+ WHEN MATCHED AND balance < 300 THEN
+ UPDATE SET balance = balance * 8, val = t.val || ' when3';
+}
+
+step "select1" { SELECT * FROM target; }
+step "c1" { COMMIT; }
+step "a1" { ABORT; }
+
+session "s2"
+setup
+{
+ BEGIN ISOLATION LEVEL READ COMMITTED;
+}
+step "update1" { UPDATE target t SET balance = balance + 10, val = t.val || ' updated by update1' WHERE t.key = 1; }
+step "update2" { UPDATE target t SET status = 's2', val = t.val || ' updated by update2' WHERE t.key = 1; }
+step "update3" { UPDATE target t SET status = 's3', val = t.val || ' updated by update3' WHERE t.key = 1; }
+step "update5" { UPDATE target t SET status = 's5', val = t.val || ' updated by update5' WHERE t.key = 1; }
+step "update_bal1" { UPDATE target t SET balance = 50, val = t.val || ' updated by update_bal1' WHERE t.key = 1; }
+step "select2" { SELECT * FROM target; }
+step "c2" { COMMIT; }
+
+# merge_status sees concurrently updated row and rechecks WHEN conditions, but recheck passes and final status = 's2'
+permutation "update1" "merge_status" "c2" "select1" "c1"
+
+# merge_status sees concurrently updated row and rechecks WHEN conditions, recheck fails, so final status = 's3' not 's2'
+permutation "update2" "merge_status" "c2" "select1" "c1"
+
+# merge_status sees concurrently updated row and rechecks WHEN conditions, recheck fails, so final status = 's4' not 's2'
+permutation "update3" "merge_status" "c2" "select1" "c1"
+
+# merge_status sees concurrently updated row and rechecks WHEN conditions, recheck fails, but we skip update and MERGE does nothing
+permutation "update5" "merge_status" "c2" "select1" "c1"
+
+# merge_bal sees concurrently updated row and rechecks WHEN conditions, recheck fails, so final balance = 100 not 640
+permutation "update_bal1" "merge_bal" "c2" "select1" "c1"
diff --git a/src/test/isolation/specs/merge-update.spec b/src/test/isolation/specs/merge-update.spec
new file mode 100644
index 0000000000..af7c9b6a63
--- /dev/null
+++ b/src/test/isolation/specs/merge-update.spec
@@ -0,0 +1,132 @@
+# MERGE UPDATE
+#
+# This test exercises atypical cases
+# 1. UPDATEs of PKs that change the join in the ON clause
+# 2. UPDATEs with WHEN AND conditions that would fail after concurrent update
+# 3. UPDATEs with extra ON conditions that would fail after concurrent update
+
+setup
+{
+ CREATE TABLE target (key int primary key, val text);
+ INSERT INTO target VALUES (1, 'setup1');
+
+ CREATE TABLE pa_target (key integer, val text)
+ PARTITION BY LIST (key);
+ CREATE TABLE part1 (key integer, val text);
+ CREATE TABLE part2 (val text, key integer);
+ CREATE TABLE part3 (key integer, val text);
+
+ ALTER TABLE pa_target ATTACH PARTITION part1 FOR VALUES IN (1,4);
+ ALTER TABLE pa_target ATTACH PARTITION part2 FOR VALUES IN (2,5,6);
+ ALTER TABLE pa_target ATTACH PARTITION part3 DEFAULT;
+
+ INSERT INTO pa_target VALUES (1, 'initial');
+ INSERT INTO pa_target VALUES (2, 'initial');
+}
+
+teardown
+{
+ DROP TABLE target;
+ DROP TABLE pa_target CASCADE;
+}
+
+session "s1"
+setup
+{
+ BEGIN ISOLATION LEVEL READ COMMITTED;
+}
+step "merge1"
+{
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+}
+step "pa_merge1"
+{
+ MERGE INTO pa_target t
+ USING (SELECT 1 as key, 'pa_merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set val = t.val || ' updated by ' || s.val;
+}
+step "pa_merge2"
+{
+ MERGE INTO pa_target t
+ USING (SELECT 1 as key, 'pa_merge1' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+}
+step "c1" { COMMIT; }
+step "a1" { ABORT; }
+
+session "s2"
+setup
+{
+ BEGIN ISOLATION LEVEL READ COMMITTED;
+}
+step "merge2a"
+{
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge2a' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+}
+step "merge2b"
+{
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge2b' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED AND t.key < 2 THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+}
+step "merge2c"
+{
+ MERGE INTO target t
+ USING (SELECT 1 as key, 'merge2c' as val) s
+ ON s.key = t.key AND t.key < 2
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+}
+step "pa_merge2a"
+{
+ MERGE INTO pa_target t
+ USING (SELECT 1 as key, 'pa_merge2a' as val) s
+ ON s.key = t.key
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.key, s.val)
+ WHEN MATCHED THEN
+ UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val;
+}
+step "select2" { SELECT * FROM target; }
+step "pa_select2" { SELECT * FROM pa_target; }
+step "c2" { COMMIT; }
+
+# Basic effects
+permutation "merge1" "c1" "select2" "c2"
+
+# One after the other, no concurrency
+permutation "merge1" "c1" "merge2a" "select2" "c2"
+
+# Now with concurrency
+permutation "merge1" "merge2a" "c1" "select2" "c2"
+permutation "merge1" "merge2a" "a1" "select2" "c2"
+permutation "merge1" "merge2b" "c1" "select2" "c2"
+permutation "merge1" "merge2c" "c1" "select2" "c2"
+permutation "pa_merge1" "pa_merge2a" "c1" "pa_select2" "c2"
+permutation "pa_merge2" "pa_merge2a" "c1" "pa_select2" "c2"
diff --git a/src/test/regress/expected/identity.out b/src/test/regress/expected/identity.out
index d7d5178f5d..3a6016c80a 100644
--- a/src/test/regress/expected/identity.out
+++ b/src/test/regress/expected/identity.out
@@ -386,3 +386,58 @@ CREATE TABLE itest_child PARTITION OF itest_parent (
) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error
ERROR: identity columns are not supported on partitions
DROP TABLE itest_parent;
+-- MERGE tests
+CREATE TABLE itest14 (a int GENERATED ALWAYS AS IDENTITY, b text);
+CREATE TABLE itest15 (a int GENERATED BY DEFAULT AS IDENTITY, b text);
+MERGE INTO itest14 t
+USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) VALUES (s.s_a, s.s_b);
+ERROR: cannot insert into column "a"
+DETAIL: Column "a" is an identity column defined as GENERATED ALWAYS.
+HINT: Use OVERRIDING SYSTEM VALUE to override.
+MERGE INTO itest14 t
+USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b);
+ERROR: cannot insert into column "a"
+DETAIL: Column "a" is an identity column defined as GENERATED ALWAYS.
+HINT: Use OVERRIDING SYSTEM VALUE to override.
+MERGE INTO itest14 t
+USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b);
+MERGE INTO itest15 t
+USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) VALUES (s.s_a, s.s_b);
+MERGE INTO itest15 t
+USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b);
+MERGE INTO itest15 t
+USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b);
+SELECT * FROM itest14;
+ a | b
+----+-------------------
+ 30 | inserted by merge
+(1 row)
+
+SELECT * FROM itest15;
+ a | b
+----+-------------------
+ 10 | inserted by merge
+ 1 | inserted by merge
+ 30 | inserted by merge
+(3 rows)
+
+DROP TABLE itest14;
+DROP TABLE itest15;
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
new file mode 100644
index 0000000000..54d05f1d27
--- /dev/null
+++ b/src/test/regress/expected/merge.out
@@ -0,0 +1,1599 @@
+--
+-- MERGE
+--
+--\set VERBOSITY verbose
+--set debug_print_rewritten = true;
+--set debug_print_parse = true;
+--set debug_print_pretty = true;
+CREATE USER merge_privs;
+CREATE USER merge_no_privs;
+DROP TABLE IF EXISTS target;
+NOTICE: table "target" does not exist, skipping
+DROP TABLE IF EXISTS source;
+NOTICE: table "source" does not exist, skipping
+CREATE TABLE target (tid integer, balance integer);
+CREATE TABLE source (sid integer, delta integer); --no index
+INSERT INTO target VALUES (1, 10);
+INSERT INTO target VALUES (2, 20);
+INSERT INTO target VALUES (3, 30);
+SELECT t.ctid is not null as matched, t.*, s.* FROM source s FULL OUTER JOIN target t ON s.sid = t.tid ORDER BY t.tid, s.sid;
+ matched | tid | balance | sid | delta
+---------+-----+---------+-----+-------
+ t | 1 | 10 | |
+ t | 2 | 20 | |
+ t | 3 | 30 | |
+(3 rows)
+
+ALTER TABLE target OWNER TO merge_privs;
+ALTER TABLE source OWNER TO merge_privs;
+CREATE TABLE target2 (tid integer, balance integer);
+CREATE TABLE source2 (sid integer, delta integer);
+ALTER TABLE target2 OWNER TO merge_no_privs;
+ALTER TABLE source2 OWNER TO merge_no_privs;
+GRANT INSERT ON target TO merge_no_privs;
+SET SESSION AUTHORIZATION merge_privs;
+EXPLAIN (COSTS OFF)
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+ QUERY PLAN
+------------------------------------------
+ Merge on target t
+ -> Merge Join
+ Merge Cond: (t_1.tid = s.sid)
+ -> Sort
+ Sort Key: t_1.tid
+ -> Seq Scan on target t_1
+ -> Sort
+ Sort Key: s.sid
+ -> Seq Scan on source s
+(9 rows)
+
+--
+-- Errors
+--
+MERGE INTO target t RANDOMWORD
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+ERROR: syntax error at or near "RANDOMWORD"
+LINE 1: MERGE INTO target t RANDOMWORD
+ ^
+-- MATCHED/INSERT error
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ INSERT DEFAULT VALUES
+;
+ERROR: syntax error at or near "INSERT"
+LINE 5: INSERT DEFAULT VALUES
+ ^
+-- incorrectly specifying INTO target
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT INTO target DEFAULT VALUES
+;
+ERROR: syntax error at or near "INTO"
+LINE 5: INSERT INTO target DEFAULT VALUES
+ ^
+-- NOT MATCHED/UPDATE
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ UPDATE SET balance = 0
+;
+ERROR: syntax error at or near "UPDATE"
+LINE 5: UPDATE SET balance = 0
+ ^
+-- UPDATE tablename
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE target SET balance = 0
+;
+ERROR: syntax error at or near "target"
+LINE 5: UPDATE target SET balance = 0
+ ^
+-- permissions
+MERGE INTO target
+USING source2
+ON target.tid = source2.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+ERROR: permission denied for table source2
+GRANT INSERT ON target TO merge_no_privs;
+SET SESSION AUTHORIZATION merge_no_privs;
+MERGE INTO target
+USING source2
+ON target.tid = source2.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+ERROR: permission denied for table target
+GRANT UPDATE ON target2 TO merge_privs;
+SET SESSION AUTHORIZATION merge_privs;
+MERGE INTO target2
+USING source
+ON target2.tid = source.sid
+WHEN MATCHED THEN
+ DELETE
+;
+ERROR: permission denied for table target2
+MERGE INTO target2
+USING source
+ON target2.tid = source.sid
+WHEN NOT MATCHED THEN
+ INSERT DEFAULT VALUES
+;
+ERROR: permission denied for table target2
+-- check if the target can be accessed from source relation subquery; we should
+-- not be able to do so
+MERGE INTO target t
+USING (SELECT * FROM source WHERE t.tid > sid) s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT DEFAULT VALUES
+;
+ERROR: invalid reference to FROM-clause entry for table "t"
+LINE 2: USING (SELECT * FROM source WHERE t.tid > sid) s
+ ^
+HINT: There is an entry for table "t", but it cannot be referenced from this part of the query.
+--
+-- initial tests
+--
+-- zero rows in source has no effect
+MERGE INTO target
+USING source
+ON target.tid = source.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT DEFAULT VALUES
+;
+ROLLBACK;
+-- insert some non-matching source rows to work from
+INSERT INTO source VALUES (4, 40);
+SELECT * FROM source ORDER BY sid;
+ sid | delta
+-----+-------
+ 4 | 40
+(1 row)
+
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+(3 rows)
+
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ DO NOTHING
+;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT DEFAULT VALUES
+;
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+ |
+(4 rows)
+
+ROLLBACK;
+-- index plans
+INSERT INTO target SELECT generate_series(1000,2500), 0;
+ALTER TABLE target ADD PRIMARY KEY (tid);
+ANALYZE target;
+EXPLAIN (COSTS OFF)
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+ QUERY PLAN
+------------------------------------------
+ Merge on target t
+ -> Hash Join
+ Hash Cond: (s.sid = t_1.tid)
+ -> Seq Scan on source s
+ -> Hash
+ -> Seq Scan on target t_1
+(6 rows)
+
+EXPLAIN (COSTS OFF)
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+ QUERY PLAN
+------------------------------------------
+ Merge on target t
+ -> Hash Join
+ Hash Cond: (s.sid = t_1.tid)
+ -> Seq Scan on source s
+ -> Hash
+ -> Seq Scan on target t_1
+(6 rows)
+
+EXPLAIN (COSTS OFF)
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT VALUES (4, NULL);
+ QUERY PLAN
+------------------------------------------
+ Merge on target t
+ -> Hash Left Join
+ Hash Cond: (s.sid = t_1.tid)
+ -> Seq Scan on source s
+ -> Hash
+ -> Seq Scan on target t_1
+(6 rows)
+
+;
+DELETE FROM target WHERE tid > 100;
+ANALYZE target;
+-- insert some matching source rows to work from
+INSERT INTO source VALUES (2, 5);
+INSERT INTO source VALUES (3, 20);
+SELECT * FROM source ORDER BY sid;
+ sid | delta
+-----+-------
+ 2 | 5
+ 3 | 20
+ 4 | 40
+(3 rows)
+
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+(3 rows)
+
+-- equivalent of an UPDATE join
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 0
+ 3 | 0
+(3 rows)
+
+ROLLBACK;
+-- equivalent of a DELETE join
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+(1 row)
+
+ROLLBACK;
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT VALUES (4, NULL)
+;
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+ 4 |
+(4 rows)
+
+ROLLBACK;
+-- duplicate source row causes multiple target row update ERROR
+INSERT INTO source VALUES (2, 5);
+SELECT * FROM source ORDER BY sid;
+ sid | delta
+-----+-------
+ 2 | 5
+ 2 | 5
+ 3 | 20
+ 4 | 40
+(4 rows)
+
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+(3 rows)
+
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+ERROR: MERGE command cannot affect row a second time
+HINT: Ensure that not more than one source rows match any one target row
+ROLLBACK;
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+ERROR: MERGE command cannot affect row a second time
+HINT: Ensure that not more than one source rows match any one target row
+ROLLBACK;
+-- correct source data
+DELETE FROM source WHERE sid = 2;
+INSERT INTO source VALUES (2, 5);
+SELECT * FROM source ORDER BY sid;
+ sid | delta
+-----+-------
+ 2 | 5
+ 3 | 20
+ 4 | 40
+(3 rows)
+
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+(3 rows)
+
+-- remove constraints
+alter table target drop CONSTRAINT target_pkey;
+alter table target alter column tid drop not null;
+-- multiple actions
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT VALUES (4, 4)
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 0
+ 3 | 0
+ 4 | 4
+(4 rows)
+
+ROLLBACK;
+-- should be equivalent
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+WHEN NOT MATCHED THEN
+ INSERT VALUES (4, 4);
+;
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 0
+ 3 | 0
+ 4 | 4
+(4 rows)
+
+ROLLBACK;
+-- column references
+-- do a simple equivalent of an UPDATE join
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = t.balance + s.delta
+;
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 25
+ 3 | 50
+(3 rows)
+
+ROLLBACK;
+-- do a simple equivalent of an INSERT SELECT
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+ 4 | 40
+(4 rows)
+
+ROLLBACK;
+-- and again with explicitly identified column list
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+ 4 | 40
+(4 rows)
+
+ROLLBACK;
+-- and again with a subtle error: referring to non-existent target row for NOT MATCHED
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (t.tid, s.delta)
+;
+ERROR: invalid reference to FROM-clause entry for table "t"
+LINE 5: INSERT (tid, balance) VALUES (t.tid, s.delta)
+ ^
+HINT: There is an entry for table "t", but it cannot be referenced from this part of the query.
+-- and again with a constant ON clause
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON (SELECT true)
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (t.tid, s.delta)
+;
+ERROR: invalid reference to FROM-clause entry for table "t"
+LINE 5: INSERT (tid, balance) VALUES (t.tid, s.delta)
+ ^
+HINT: There is an entry for table "t", but it cannot be referenced from this part of the query.
+SELECT * FROM target ORDER BY tid;
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+ROLLBACK;
+-- now the classic UPSERT
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = t.balance + s.delta
+WHEN NOT MATCHED THEN
+ INSERT VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 25
+ 3 | 50
+ 4 | 40
+(4 rows)
+
+ROLLBACK;
+-- unreachable WHEN clause should ERROR
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN /* Terminal WHEN clause for MATCHED */
+ DELETE
+WHEN MATCHED AND s.delta > 0 THEN
+ UPDATE SET balance = t.balance - s.delta
+;
+ERROR: unreachable WHEN clause specified after unconditional WHEN clause
+ROLLBACK;
+-- conditional WHEN clause
+CREATE TABLE wq_target (tid integer not null, balance integer DEFAULT -1);
+CREATE TABLE wq_source (balance integer, sid integer);
+INSERT INTO wq_source (sid, balance) VALUES (1, 100);
+BEGIN;
+-- try a simple INSERT with default values first
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | -1
+(1 row)
+
+ROLLBACK;
+-- this time with a FALSE condition
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED AND FALSE THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+(0 rows)
+
+-- this time with an actual condition which returns false
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED AND s.balance <> 100 THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+(0 rows)
+
+BEGIN;
+-- and now with a condition which returns true
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED AND s.balance = 100 THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | -1
+(1 row)
+
+ROLLBACK;
+-- conditions in the NOT MATCHED clause can only refer to source columns
+BEGIN;
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED AND t.balance = 100 THEN
+ INSERT (tid) VALUES (s.sid);
+ERROR: invalid reference to FROM-clause entry for table "t"
+LINE 3: WHEN NOT MATCHED AND t.balance = 100 THEN
+ ^
+HINT: There is an entry for table "t", but it cannot be referenced from this part of the query.
+SELECT * FROM wq_target;
+ERROR: current transaction is aborted, commands ignored until end of transaction block
+ROLLBACK;
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED AND s.balance = 100 THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | -1
+(1 row)
+
+-- conditions in MATCHED clause can refer to both source and target
+SELECT * FROM wq_source;
+ balance | sid
+---------+-----
+ 100 | 1
+(1 row)
+
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND s.balance = 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | 99
+(1 row)
+
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance = 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | 99
+(1 row)
+
+-- check if AND works
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance = 99 AND s.balance > 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | 99
+(1 row)
+
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance = 99 AND s.balance = 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | 199
+(1 row)
+
+-- check if OR works
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance = 99 OR s.balance > 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | 199
+(1 row)
+
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance = 199 OR s.balance > 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | 299
+(1 row)
+
+-- check if subqueries work in the conditions?
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance > (SELECT max(balance) FROM target) THEN
+ UPDATE SET balance = t.balance + s.balance;
+-- check if we can access system columns in the conditions
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.xmin = t.xmax THEN
+ UPDATE SET balance = t.balance + s.balance;
+ERROR: system column "xmin" reference in WHEN AND condition is invalid
+LINE 3: WHEN MATCHED AND t.xmin = t.xmax THEN
+ ^
+ALTER TABLE wq_target SET WITH OIDS;
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | 399
+(1 row)
+
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.oid >= 0 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+ tid | balance
+-----+---------
+ 1 | 499
+(1 row)
+
+-- test preventing WHEN AND conditions from writing to the database
+create or replace function merge_when_and_write() returns boolean
+language plpgsql as
+$$
+BEGIN
+ INSERT INTO target VALUES (100, 100);
+ RETURN TRUE;
+END;
+$$;
+BEGIN;
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND (merge_when_and_write()) THEN
+ UPDATE SET balance = t.balance + s.balance;
+ROLLBACK;
+drop function merge_when_and_write();
+DROP TABLE wq_target, wq_source;
+-- test triggers
+create or replace function merge_trigfunc () returns trigger
+language plpgsql as
+$$
+BEGIN
+ RAISE NOTICE '% % % trigger', TG_WHEN, TG_OP, TG_LEVEL;
+ IF (TG_WHEN = 'BEFORE' AND TG_LEVEL = 'ROW') THEN
+ IF (TG_OP = 'DELETE') THEN
+ RETURN OLD;
+ ELSE
+ RETURN NEW;
+ END IF;
+ ELSE
+ RETURN NULL;
+ END IF;
+END;
+$$;
+CREATE TRIGGER merge_bsi BEFORE INSERT ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_bsu BEFORE UPDATE ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_bsd BEFORE DELETE ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_asi AFTER INSERT ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_asu AFTER UPDATE ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_asd AFTER DELETE ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_bri BEFORE INSERT ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_bru BEFORE UPDATE ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_brd BEFORE DELETE ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_ari AFTER INSERT ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_aru AFTER UPDATE ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_ard AFTER DELETE ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+-- now the classic UPSERT, with a DELETE
+BEGIN;
+UPDATE target SET balance = 0 WHERE tid = 3;
+NOTICE: BEFORE UPDATE STATEMENT trigger
+NOTICE: BEFORE UPDATE ROW trigger
+NOTICE: AFTER UPDATE ROW trigger
+NOTICE: AFTER UPDATE STATEMENT trigger
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED AND t.balance > s.delta THEN
+ UPDATE SET balance = t.balance - s.delta
+WHEN MATCHED THEN
+ DELETE
+WHEN NOT MATCHED THEN
+ INSERT VALUES (s.sid, s.delta)
+;
+NOTICE: BEFORE INSERT STATEMENT trigger
+NOTICE: BEFORE UPDATE STATEMENT trigger
+NOTICE: BEFORE DELETE STATEMENT trigger
+NOTICE: BEFORE INSERT ROW trigger
+NOTICE: BEFORE DELETE ROW trigger
+NOTICE: BEFORE UPDATE ROW trigger
+NOTICE: AFTER INSERT ROW trigger
+NOTICE: AFTER DELETE ROW trigger
+NOTICE: AFTER UPDATE ROW trigger
+NOTICE: AFTER DELETE STATEMENT trigger
+NOTICE: AFTER UPDATE STATEMENT trigger
+NOTICE: AFTER INSERT STATEMENT trigger
+ QUERY PLAN
+------------------------------------------------------------------
+ Merge on target t (actual rows=0 loops=1)
+ Tuples Inserted: 1
+ Tuples Updated: 1
+ Tuples Deleted: 1
+ -> Hash Left Join (actual rows=3 loops=1)
+ Hash Cond: (s.sid = t_1.tid)
+ -> Seq Scan on source s (actual rows=3 loops=1)
+ -> Hash (actual rows=3 loops=1)
+ Buckets: 1024 Batches: 1 Memory Usage: 9kB
+ -> Seq Scan on target t_1 (actual rows=3 loops=1)
+ Trigger merge_ard: calls=1
+ Trigger merge_ari: calls=1
+ Trigger merge_aru: calls=1
+ Trigger merge_asd: calls=1
+ Trigger merge_asi: calls=1
+ Trigger merge_asu: calls=1
+ Trigger merge_brd: calls=1
+ Trigger merge_bri: calls=1
+ Trigger merge_bru: calls=1
+ Trigger merge_bsd: calls=1
+ Trigger merge_bsi: calls=1
+ Trigger merge_bsu: calls=1
+(22 rows)
+
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 15
+ 4 | 40
+(3 rows)
+
+ROLLBACK;
+-- test from PL/pgSQL
+-- make sure MERGE INTO isn't interpreted to mean returning variables like SELECT INTO
+BEGIN;
+DO LANGUAGE plpgsql $$
+BEGIN
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED AND t.balance > s.delta THEN
+ UPDATE SET balance = t.balance - s.delta
+;
+END;
+$$;
+NOTICE: BEFORE UPDATE STATEMENT trigger
+NOTICE: BEFORE UPDATE ROW trigger
+NOTICE: BEFORE UPDATE ROW trigger
+NOTICE: AFTER UPDATE ROW trigger
+NOTICE: AFTER UPDATE ROW trigger
+NOTICE: AFTER UPDATE STATEMENT trigger
+ROLLBACK;
+--source constants
+BEGIN;
+MERGE INTO target t
+USING (SELECT 9 AS sid, 57 AS delta) AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.delta)
+;
+NOTICE: BEFORE INSERT STATEMENT trigger
+NOTICE: BEFORE INSERT ROW trigger
+NOTICE: AFTER INSERT ROW trigger
+NOTICE: AFTER INSERT STATEMENT trigger
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+ 9 | 57
+(4 rows)
+
+ROLLBACK;
+--source query
+BEGIN;
+MERGE INTO target t
+USING (SELECT sid, delta FROM source WHERE delta > 0) AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.delta)
+;
+NOTICE: BEFORE INSERT STATEMENT trigger
+NOTICE: BEFORE INSERT ROW trigger
+NOTICE: AFTER INSERT ROW trigger
+NOTICE: AFTER INSERT STATEMENT trigger
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+ 4 | 40
+(4 rows)
+
+ROLLBACK;
+BEGIN;
+MERGE INTO target t
+USING (SELECT sid, delta as newname FROM source WHERE delta > 0) AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.newname)
+;
+NOTICE: BEFORE INSERT STATEMENT trigger
+NOTICE: BEFORE INSERT ROW trigger
+NOTICE: AFTER INSERT ROW trigger
+NOTICE: AFTER INSERT STATEMENT trigger
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+ 4 | 40
+(4 rows)
+
+ROLLBACK;
+--self-merge
+BEGIN;
+MERGE INTO target t1
+USING target t2
+ON t1.tid = t2.tid
+WHEN MATCHED THEN
+ UPDATE SET balance = t1.balance + t2.balance
+WHEN NOT MATCHED THEN
+ INSERT VALUES (t2.tid, t2.balance)
+;
+NOTICE: BEFORE INSERT STATEMENT trigger
+NOTICE: BEFORE UPDATE STATEMENT trigger
+NOTICE: BEFORE UPDATE ROW trigger
+NOTICE: BEFORE UPDATE ROW trigger
+NOTICE: BEFORE UPDATE ROW trigger
+NOTICE: AFTER UPDATE ROW trigger
+NOTICE: AFTER UPDATE ROW trigger
+NOTICE: AFTER UPDATE ROW trigger
+NOTICE: AFTER UPDATE STATEMENT trigger
+NOTICE: AFTER INSERT STATEMENT trigger
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 20
+ 2 | 40
+ 3 | 60
+(3 rows)
+
+ROLLBACK;
+BEGIN;
+MERGE INTO target t
+USING (SELECT tid as sid, balance as delta FROM target WHERE balance > 0) AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.delta)
+;
+NOTICE: BEFORE INSERT STATEMENT trigger
+NOTICE: AFTER INSERT STATEMENT trigger
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+(3 rows)
+
+ROLLBACK;
+BEGIN;
+MERGE INTO target t
+USING
+(SELECT sid, max(delta) AS delta
+ FROM source
+ GROUP BY sid
+ HAVING count(*) = 1
+ ORDER BY sid ASC) AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.delta)
+;
+NOTICE: BEFORE INSERT STATEMENT trigger
+NOTICE: BEFORE INSERT ROW trigger
+NOTICE: AFTER INSERT ROW trigger
+NOTICE: AFTER INSERT STATEMENT trigger
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 30
+ 4 | 40
+(4 rows)
+
+ROLLBACK;
+-- plpgsql parameters and results
+BEGIN;
+CREATE FUNCTION merge_func (p_id integer, p_bal integer)
+RETURNS INTEGER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ result integer;
+BEGIN
+MERGE INTO target t
+USING (SELECT p_id AS sid) AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = t.balance - p_bal
+;
+IF FOUND THEN
+ GET DIAGNOSTICS result := ROW_COUNT;
+END IF;
+RETURN result;
+END;
+$$;
+SELECT merge_func(3, 4);
+NOTICE: BEFORE UPDATE STATEMENT trigger
+NOTICE: BEFORE UPDATE ROW trigger
+NOTICE: AFTER UPDATE ROW trigger
+NOTICE: AFTER UPDATE STATEMENT trigger
+ merge_func
+------------
+ 1
+(1 row)
+
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 10
+ 2 | 20
+ 3 | 26
+(3 rows)
+
+ROLLBACK;
+-- PREPARE
+BEGIN;
+prepare foom as merge into target t using (select 1 as sid) s on (t.tid = s.sid) when matched then update set balance = 1;
+execute foom;
+NOTICE: BEFORE UPDATE STATEMENT trigger
+NOTICE: BEFORE UPDATE ROW trigger
+NOTICE: AFTER UPDATE ROW trigger
+NOTICE: AFTER UPDATE STATEMENT trigger
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 1
+ 2 | 20
+ 3 | 30
+(3 rows)
+
+ROLLBACK;
+BEGIN;
+PREPARE foom2 (integer, integer) AS
+MERGE INTO target t
+USING (SELECT 1) s
+ON t.tid = $1
+WHEN MATCHED THEN
+UPDATE SET balance = $2;
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+execute foom2 (1, 1);
+NOTICE: BEFORE UPDATE STATEMENT trigger
+NOTICE: BEFORE UPDATE ROW trigger
+NOTICE: AFTER UPDATE ROW trigger
+NOTICE: AFTER UPDATE STATEMENT trigger
+ QUERY PLAN
+------------------------------------------------------
+ Merge on target t (actual rows=0 loops=1)
+ Tuples Inserted: 0
+ Tuples Updated: 1
+ Tuples Deleted: 0
+ -> Seq Scan on target t_1 (actual rows=1 loops=1)
+ Filter: (tid = 1)
+ Rows Removed by Filter: 2
+ Trigger merge_aru: calls=1
+ Trigger merge_asu: calls=1
+ Trigger merge_bru: calls=1
+ Trigger merge_bsu: calls=1
+(11 rows)
+
+SELECT * FROM target ORDER BY tid;
+ tid | balance
+-----+---------
+ 1 | 1
+ 2 | 20
+ 3 | 30
+(3 rows)
+
+ROLLBACK;
+-- subqueries in source relation
+CREATE TABLE sq_target (tid integer NOT NULL, balance integer);
+CREATE TABLE sq_source (delta integer, sid integer, balance integer DEFAULT 0);
+INSERT INTO sq_target(tid, balance) VALUES (1,100), (2,200), (3,300);
+INSERT INTO sq_source(sid, delta) VALUES (1,10), (2,20), (4,40);
+BEGIN;
+MERGE INTO sq_target t
+USING (SELECT * FROM sq_source) s
+ON tid = sid
+WHEN MATCHED AND t.balance > delta THEN
+ UPDATE SET balance = t.balance + delta;
+SELECT * FROM sq_target;
+ tid | balance
+-----+---------
+ 3 | 300
+ 1 | 110
+ 2 | 220
+(3 rows)
+
+ROLLBACK;
+-- try a view
+CREATE VIEW v AS SELECT * FROM sq_source WHERE sid < 2;
+BEGIN;
+MERGE INTO sq_target
+USING v
+ON tid = sid
+WHEN MATCHED THEN
+ UPDATE SET balance = v.balance + delta;
+SELECT * FROM sq_target;
+ tid | balance
+-----+---------
+ 2 | 200
+ 3 | 300
+ 1 | 10
+(3 rows)
+
+ROLLBACK;
+-- ambiguous reference to a column
+BEGIN;
+MERGE INTO sq_target
+USING v
+ON tid = sid
+WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = balance + delta
+WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (balance + delta, sid)
+WHEN MATCHED AND tid < 2 THEN
+ DELETE;
+ERROR: column reference "balance" is ambiguous
+LINE 5: UPDATE SET balance = balance + delta
+ ^
+ROLLBACK;
+BEGIN;
+INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+MERGE INTO sq_target t
+USING v
+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;
+SELECT * FROM sq_target;
+ tid | balance
+-----+---------
+ 2 | 200
+ 3 | 300
+ -1 | -11
+(3 rows)
+
+ROLLBACK;
+-- CTEs
+BEGIN;
+INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+WITH targq AS (
+ SELECT * FROM v
+)
+MERGE INTO sq_target t
+USING v
+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
+;
+ERROR: syntax error at or near "MERGE"
+LINE 4: MERGE INTO sq_target t
+ ^
+ROLLBACK;
+-- RETURNING
+BEGIN;
+INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+MERGE INTO sq_target t
+USING v
+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 *
+;
+ERROR: syntax error at or near "RETURNING"
+LINE 10: RETURNING *
+ ^
+ROLLBACK;
+-- Subqueries
+BEGIN;
+MERGE INTO sq_target t
+USING v
+ON tid = sid
+WHEN MATCHED THEN
+ UPDATE SET balance = (SELECT count(*) FROM sq_target)
+;
+ROLLBACK;
+BEGIN;
+MERGE INTO sq_target t
+USING v
+ON tid = sid
+WHEN MATCHED AND (SELECT count(*) > 0 FROM sq_target) THEN
+ UPDATE SET balance = 42
+;
+ROLLBACK;
+BEGIN;
+MERGE INTO sq_target t
+USING v
+ON tid = sid AND (SELECT count(*) > 0 FROM sq_target)
+WHEN MATCHED THEN
+ UPDATE SET balance = 42
+;
+ROLLBACK;
+DROP TABLE sq_target, sq_source CASCADE;
+NOTICE: drop cascades to view v
+CREATE TABLE pa_target (tid integer, balance float, val text)
+ PARTITION BY LIST (tid);
+CREATE TABLE part1 PARTITION OF pa_target FOR VALUES IN (1,4);
+CREATE TABLE part2 PARTITION OF pa_target FOR VALUES IN (2,5,6);
+CREATE TABLE part3 PARTITION OF pa_target FOR VALUES IN (3,8,9);
+CREATE TABLE part4 PARTITION OF pa_target DEFAULT;
+CREATE TABLE pa_source (sid integer, delta float);
+-- insert many rows to the source table
+INSERT INTO pa_source SELECT id, id * 10 FROM generate_series(1,14) AS id;
+-- insert a few rows in the target table (odd numbered tid)
+INSERT INTO pa_target SELECT id, id * 100, 'initial' FROM generate_series(1,14,2) AS id;
+-- try simple MERGE
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid
+ WHEN MATCHED THEN
+ UPDATE SET balance = balance + delta, val = val || ' updated by merge'
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (sid, delta, 'inserted by merge');
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 1 | 110 | initial updated by merge
+ 2 | 20 | inserted by merge
+ 3 | 330 | initial updated by merge
+ 4 | 40 | inserted by merge
+ 5 | 550 | initial updated by merge
+ 6 | 60 | inserted by merge
+ 7 | 770 | initial updated by merge
+ 8 | 80 | inserted by merge
+ 9 | 990 | initial updated by merge
+ 10 | 100 | inserted by merge
+ 11 | 1210 | initial updated by merge
+ 12 | 120 | inserted by merge
+ 13 | 1430 | initial updated by merge
+ 14 | 140 | inserted by merge
+(14 rows)
+
+ROLLBACK;
+-- same with a constant qual
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET balance = balance + delta, val = val || ' updated by merge'
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (sid, delta, 'inserted by merge');
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 1 | 110 | initial updated by merge
+ 2 | 20 | inserted by merge
+ 3 | 30 | inserted by merge
+ 3 | 300 | initial
+ 4 | 40 | inserted by merge
+ 5 | 500 | initial
+ 5 | 50 | inserted by merge
+ 6 | 60 | inserted by merge
+ 7 | 700 | initial
+ 7 | 70 | inserted by merge
+ 8 | 80 | inserted by merge
+ 9 | 90 | inserted by merge
+ 9 | 900 | initial
+ 10 | 100 | inserted by merge
+ 11 | 1100 | initial
+ 11 | 110 | inserted by merge
+ 12 | 120 | inserted by merge
+ 13 | 1300 | initial
+ 13 | 130 | inserted by merge
+ 14 | 140 | inserted by merge
+(20 rows)
+
+ROLLBACK;
+-- try updating the partition key column
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid
+ 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');
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 2 | 20 | inserted by merge
+ 4 | 40 | inserted by merge
+ 4 | 330 | initial updated by merge
+ 6 | 550 | initial updated by merge
+ 6 | 60 | inserted by merge
+ 8 | 80 | inserted by merge
+ 8 | 770 | initial updated by merge
+ 10 | 990 | initial updated by merge
+ 10 | 100 | inserted by merge
+ 12 | 1210 | initial updated by merge
+ 12 | 120 | inserted by merge
+ 14 | 1430 | initial updated by merge
+ 14 | 140 | inserted by merge
+(14 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.
+CREATE TABLE pa_target (tid integer, balance float, val text)
+ PARTITION BY LIST (tid);
+CREATE TABLE part1 (tid integer, balance float, val text);
+CREATE TABLE part2 (balance float, tid integer, val text);
+CREATE TABLE part3 (tid integer, balance float, val text);
+CREATE TABLE part4 (extraid text, tid integer, balance float, val text);
+ALTER TABLE part4 DROP COLUMN extraid;
+ALTER TABLE pa_target ATTACH PARTITION part1 FOR VALUES IN (1,4);
+ALTER TABLE pa_target ATTACH PARTITION part2 FOR VALUES IN (2,5,6);
+ALTER TABLE pa_target ATTACH PARTITION part3 FOR VALUES IN (3,8,9);
+ALTER TABLE pa_target ATTACH PARTITION part4 DEFAULT;
+-- insert a few rows in the target table (odd numbered tid)
+INSERT INTO pa_target SELECT id, id * 100, 'initial' FROM generate_series(1,14,2) AS id;
+-- try simple MERGE
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid
+ WHEN MATCHED THEN
+ UPDATE SET balance = balance + delta, val = val || ' updated by merge'
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (sid, delta, 'inserted by merge');
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 1 | 110 | initial updated by merge
+ 2 | 20 | inserted by merge
+ 3 | 330 | initial updated by merge
+ 4 | 40 | inserted by merge
+ 5 | 550 | initial updated by merge
+ 6 | 60 | inserted by merge
+ 7 | 770 | initial updated by merge
+ 8 | 80 | inserted by merge
+ 9 | 990 | initial updated by merge
+ 10 | 100 | inserted by merge
+ 11 | 1210 | initial updated by merge
+ 12 | 120 | inserted by merge
+ 13 | 1430 | initial updated by merge
+ 14 | 140 | inserted by merge
+(14 rows)
+
+ROLLBACK;
+-- same with a constant qual
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET balance = balance + delta, val = val || ' updated by merge'
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (sid, delta, 'inserted by merge');
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 1 | 110 | initial updated by merge
+ 2 | 20 | inserted by merge
+ 3 | 30 | inserted by merge
+ 3 | 300 | initial
+ 4 | 40 | inserted by merge
+ 5 | 500 | initial
+ 5 | 50 | inserted by merge
+ 6 | 60 | inserted by merge
+ 7 | 700 | initial
+ 7 | 70 | inserted by merge
+ 8 | 80 | inserted by merge
+ 9 | 90 | inserted by merge
+ 9 | 900 | initial
+ 10 | 100 | inserted by merge
+ 11 | 1100 | initial
+ 11 | 110 | inserted by merge
+ 12 | 120 | inserted by merge
+ 13 | 1300 | initial
+ 13 | 130 | inserted by merge
+ 14 | 140 | inserted by merge
+(20 rows)
+
+ROLLBACK;
+-- try updating the partition key column
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid
+ 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');
+SELECT * FROM pa_target ORDER BY tid;
+ tid | balance | val
+-----+---------+--------------------------
+ 2 | 110 | initial updated by merge
+ 2 | 20 | inserted by merge
+ 4 | 40 | inserted by merge
+ 4 | 330 | initial updated by merge
+ 6 | 550 | initial updated by merge
+ 6 | 60 | inserted by merge
+ 8 | 80 | inserted by merge
+ 8 | 770 | initial updated by merge
+ 10 | 990 | initial updated by merge
+ 10 | 100 | inserted by merge
+ 12 | 1210 | initial updated by merge
+ 12 | 120 | inserted by merge
+ 14 | 1430 | initial updated by merge
+ 14 | 140 | inserted by merge
+(14 rows)
+
+ROLLBACK;
+DROP TABLE pa_source;
+DROP TABLE pa_target CASCADE;
+-- Sub-partitionin
+CREATE TABLE pa_target (logts timestamp, tid integer, balance float, val text)
+ PARTITION BY RANGE (logts);
+CREATE TABLE part_m01 PARTITION OF pa_target
+ FOR VALUES FROM ('2017-01-01') TO ('2017-02-01')
+ PARTITION BY LIST (tid);
+CREATE TABLE part_m01_odd PARTITION OF part_m01
+ FOR VALUES IN (1,3,5,7,9);
+CREATE TABLE part_m01_even PARTITION OF part_m01
+ FOR VALUES IN (2,4,6,8);
+CREATE TABLE part_m02 PARTITION OF pa_target
+ FOR VALUES FROM ('2017-02-01') TO ('2017-03-01')
+ PARTITION BY LIST (tid);
+CREATE TABLE part_m02_odd PARTITION OF part_m02
+ FOR VALUES IN (1,3,5,7,9);
+CREATE TABLE part_m02_even PARTITION OF part_m02
+ FOR VALUES IN (2,4,6,8);
+CREATE TABLE pa_source (sid integer, delta float);
+-- insert many rows to the source table
+INSERT INTO pa_source SELECT id, id * 10 FROM generate_series(1,14) AS id;
+-- insert a few rows in the target table (odd numbered tid)
+INSERT INTO pa_target SELECT '2017-01-31', id, id * 100, 'initial' FROM generate_series(1,9,3) AS id;
+INSERT INTO pa_target SELECT '2017-02-28', id, id * 100, 'initial' FROM generate_series(2,9,3) AS id;
+-- try simple MERGE
+BEGIN;
+MERGE INTO pa_target t
+ USING (SELECT '2017-01-15' AS slogts, * FROM pa_source WHERE sid < 10) s
+ ON t.tid = s.sid
+ 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');
+SELECT * FROM pa_target ORDER BY tid;
+ logts | tid | balance | val
+--------------------------+-----+---------+--------------------------
+ Tue Jan 31 00:00:00 2017 | 1 | 110 | initial updated by merge
+ Tue Feb 28 00:00:00 2017 | 2 | 220 | initial updated by merge
+ Sun Jan 15 00:00:00 2017 | 3 | 30 | inserted by merge
+ Tue Jan 31 00:00:00 2017 | 4 | 440 | initial updated by merge
+ Tue Feb 28 00:00:00 2017 | 5 | 550 | initial updated by merge
+ Sun Jan 15 00:00:00 2017 | 6 | 60 | inserted by merge
+ Tue Jan 31 00:00:00 2017 | 7 | 770 | initial updated by merge
+ Tue Feb 28 00:00:00 2017 | 8 | 880 | initial updated by merge
+ Sun Jan 15 00:00:00 2017 | 9 | 90 | inserted by merge
+(9 rows)
+
+ROLLBACK;
+DROP TABLE pa_source;
+DROP TABLE pa_target CASCADE;
+-- some complex joins on the source side
+CREATE TABLE cj_target (tid integer, balance float, val text);
+CREATE TABLE cj_source1 (sid1 integer, scat integer, delta integer);
+CREATE TABLE cj_source2 (sid2 integer, sval text);
+INSERT INTO cj_source1 VALUES (1, 10, 100);
+INSERT INTO cj_source1 VALUES (1, 20, 200);
+INSERT INTO cj_source1 VALUES (2, 20, 300);
+INSERT INTO cj_source1 VALUES (3, 10, 400);
+INSERT INTO cj_source2 VALUES (1, 'initial source2');
+INSERT INTO cj_source2 VALUES (2, 'initial source2');
+INSERT INTO cj_source2 VALUES (3, 'initial source2');
+-- source relation is an unalised join
+MERGE INTO cj_target t
+USING cj_source1 s1
+ INNER JOIN cj_source2 s2 ON sid1 = sid2
+ON t.tid = sid1
+WHEN NOT MATCHED THEN
+ INSERT VALUES (sid1, delta, sval);
+-- try accessing columns from either side of the source join
+MERGE INTO cj_target t
+USING cj_source2 s2
+ INNER JOIN cj_source1 s1 ON sid1 = sid2 AND scat = 20
+ON t.tid = sid1
+WHEN NOT MATCHED THEN
+ INSERT VALUES (sid2, delta, sval)
+WHEN MATCHED THEN
+ DELETE;
+-- some simple expressions in INSERT targetlist
+MERGE INTO cj_target t
+USING cj_source2 s2
+ INNER JOIN cj_source1 s1 ON sid1 = sid2
+ON t.tid = sid1
+WHEN NOT MATCHED THEN
+ INSERT VALUES (sid2, delta + scat, sval)
+WHEN MATCHED THEN
+ UPDATE SET val = val || ' updated by merge';
+MERGE INTO cj_target t
+USING cj_source2 s2
+ INNER JOIN cj_source1 s1 ON sid1 = sid2 AND scat = 20
+ON t.tid = sid1
+WHEN MATCHED THEN
+ UPDATE SET val = val || ' ' || delta::text;
+SELECT * FROM cj_target;
+ tid | balance | val
+-----+---------+----------------------------------
+ 3 | 400 | initial source2 updated by merge
+ 1 | 220 | initial source2 200
+ 1 | 110 | initial source2 200
+ 2 | 320 | initial source2 300
+(4 rows)
+
+ALTER TABLE cj_source1 RENAME COLUMN sid1 TO sid;
+ALTER TABLE cj_source2 RENAME COLUMN sid2 TO sid;
+TRUNCATE cj_target;
+MERGE INTO cj_target t
+USING cj_source1 s1
+ INNER JOIN cj_source2 s2 ON s1.sid = s2.sid
+ON t.tid = s1.sid
+WHEN NOT MATCHED THEN
+ INSERT VALUES (s2.sid, delta, sval);
+DROP TABLE cj_source2, cj_source1, cj_target;
+-- Function scans
+CREATE TABLE fs_target (a int, b int, c text);
+MERGE INTO fs_target t
+USING generate_series(1,100,1) AS id
+ON t.a = id
+WHEN MATCHED THEN
+ UPDATE SET b = b + id
+WHEN NOT MATCHED THEN
+ INSERT VALUES (id, -1);
+MERGE INTO fs_target t
+USING generate_series(1,100,2) AS id
+ON t.a = id
+WHEN MATCHED THEN
+ UPDATE SET b = b + id, c = 'updated '|| id.*::text
+WHEN NOT MATCHED THEN
+ INSERT VALUES (id, -1, 'inserted ' || id.*::text);
+SELECT count(*) FROM fs_target;
+ count
+-------
+ 100
+(1 row)
+
+DROP TABLE fs_target;
+-- SERIALIZABLE test
+-- handled in isolation tests
+-- prepare
+RESET SESSION AUTHORIZATION;
+DROP TABLE target, target2;
+DROP TABLE source, source2;
+DROP FUNCTION merge_trigfunc();
+DROP USER merge_privs;
+DROP USER merge_no_privs;
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index f1ae40df61..b14a91e186 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2138,6 +2138,159 @@ ERROR: new row violates row-level security policy (USING expression) for table
INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'my first novel')
ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol';
ERROR: new row violates row-level security policy for table "document"
+--
+-- MERGE
+--
+RESET SESSION AUTHORIZATION;
+DROP POLICY p3_with_all ON document;
+ALTER TABLE document ADD COLUMN dnotes text DEFAULT '';
+-- all documents are readable
+CREATE POLICY p1 ON document FOR SELECT USING (true);
+-- one may insert documents only authored by them
+CREATE POLICY p2 ON document FOR INSERT WITH CHECK (dauthor = current_user);
+-- one may only update documents in 'novel' category
+CREATE POLICY p3 ON document FOR UPDATE
+ USING (cid = (SELECT cid from category WHERE cname = 'novel'))
+ WITH CHECK (dauthor = current_user);
+-- one may only delete documents in 'manga' category
+CREATE POLICY p4 ON document FOR DELETE
+ USING (cid = (SELECT cid from category WHERE cname = 'manga'));
+SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+-------------------+----------------------------------+--------
+ 1 | 11 | 1 | regress_rls_bob | my first novel |
+ 3 | 22 | 2 | regress_rls_bob | my science fiction |
+ 4 | 44 | 1 | regress_rls_bob | my first manga |
+ 5 | 44 | 2 | regress_rls_bob | my second manga |
+ 6 | 22 | 1 | regress_rls_carol | great science fiction |
+ 7 | 33 | 2 | regress_rls_carol | great technology book |
+ 8 | 44 | 1 | regress_rls_carol | great manga |
+ 9 | 22 | 1 | regress_rls_dave | awesome science fiction |
+ 10 | 33 | 2 | regress_rls_dave | awesome technology book |
+ 11 | 33 | 1 | regress_rls_carol | hoge |
+ 33 | 22 | 1 | regress_rls_bob | okay science fiction |
+ 2 | 11 | 2 | regress_rls_bob | my first novel |
+ 78 | 33 | 1 | regress_rls_bob | some technology novel |
+ 79 | 33 | 1 | regress_rls_bob | technology book, can only insert |
+(14 rows)
+
+SET SESSION AUTHORIZATION regress_rls_bob;
+-- Fails, since update violates WITH CHECK qual on dauthor
+MERGE INTO document d
+USING (SELECT 1 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge1 ', dauthor = 'regress_rls_alice';
+ERROR: new row violates row-level security policy for table "document"
+-- Should be OK since USING and WITH CHECK quals pass
+MERGE INTO document d
+USING (SELECT 1 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge2 ';
+-- Even when dauthor is updated explicitly, but to the existing value
+MERGE INTO document d
+USING (SELECT 1 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge3 ', dauthor = 'regress_rls_bob';
+-- There is a MATCH for did = 3, but UPDATE's USING qual does not allow
+-- updating an item in category 'science fiction'
+MERGE INTO document d
+USING (SELECT 3 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge ';
+ERROR: target row violates row-level security policy (USING expression) for table "document"
+-- The same thing with DELETE action, but fails again because no permissions
+-- to delete items in 'science fiction' category that did 3 belongs to.
+MERGE INTO document d
+USING (SELECT 3 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ DELETE;
+ERROR: target row violates row-level security policy (USING expression) for table "document"
+-- Document with did 4 belongs to 'manga' category which is allowed for
+-- deletion. But this fails because the UPDATE action is matched first and
+-- UPDATE policy does not allow updation in the category.
+MERGE INTO document d
+USING (SELECT 4 as sdid) s
+ON did = s.sdid
+WHEN MATCHED AND dnotes = '' THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge '
+WHEN MATCHED THEN
+ DELETE;
+ERROR: target row violates row-level security policy (USING expression) for table "document"
+-- UPDATE action is not matched this time because of the WHEN AND qual.
+-- DELETE still fails because role regress_rls_bob does not have SELECT
+-- privileges on 'manga' category row in the category table.
+MERGE INTO document d
+USING (SELECT 4 as sdid) s
+ON did = s.sdid
+WHEN MATCHED AND dnotes <> '' THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge '
+WHEN MATCHED THEN
+ DELETE;
+ERROR: target row violates row-level security policy (USING expression) for table "document"
+SELECT * FROM document WHERE did = 4;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+-----------------+----------------+--------
+ 4 | 44 | 1 | regress_rls_bob | my first manga |
+(1 row)
+
+-- Switch to regress_rls_carol role and try the DELETE again. It should succeed
+-- this time
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION regress_rls_carol;
+MERGE INTO document d
+USING (SELECT 4 as sdid) s
+ON did = s.sdid
+WHEN MATCHED AND dnotes <> '' THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge '
+WHEN MATCHED THEN
+ DELETE;
+-- Switch back to regress_rls_bob role
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION regress_rls_bob;
+-- Try INSERT action. This fails because we are trying to insert
+-- dauthor = regress_rls_dave and INSERT's WITH CHECK does not allow
+-- that
+MERGE INTO document d
+USING (SELECT 12 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ DELETE
+WHEN NOT MATCHED THEN
+ INSERT VALUES (12, 11, 1, 'regress_rls_dave', 'another novel');
+ERROR: new row violates row-level security policy for table "document"
+-- This should be fine
+MERGE INTO document d
+USING (SELECT 12 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ DELETE
+WHEN NOT MATCHED THEN
+ INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel');
+-- Just check everything went per plan
+SELECT * FROM document;
+ did | cid | dlevel | dauthor | dtitle | dnotes
+-----+-----+--------+-------------------+----------------------------------+------------------------------------------------
+ 3 | 22 | 2 | regress_rls_bob | my science fiction |
+ 5 | 44 | 2 | regress_rls_bob | my second manga |
+ 6 | 22 | 1 | regress_rls_carol | great science fiction |
+ 7 | 33 | 2 | regress_rls_carol | great technology book |
+ 8 | 44 | 1 | regress_rls_carol | great manga |
+ 9 | 22 | 1 | regress_rls_dave | awesome science fiction |
+ 10 | 33 | 2 | regress_rls_dave | awesome technology book |
+ 11 | 33 | 1 | regress_rls_carol | hoge |
+ 33 | 22 | 1 | regress_rls_bob | okay science fiction |
+ 2 | 11 | 2 | regress_rls_bob | my first novel |
+ 78 | 33 | 1 | regress_rls_bob | some technology novel |
+ 79 | 33 | 1 | regress_rls_bob | technology book, can only insert |
+ 1 | 11 | 1 | regress_rls_bob | my first novel | notes added by merge2 notes added by merge3
+ 12 | 11 | 1 | regress_rls_bob | another novel |
+(14 rows)
+
--
-- ROLE/GROUP
--
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 5149b72fe9..d369a73173 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -3263,6 +3263,37 @@ CREATE RULE rules_parted_table_insert AS ON INSERT to rules_parted_table
ALTER RULE rules_parted_table_insert ON rules_parted_table RENAME TO rules_parted_table_insert_redirect;
DROP TABLE rules_parted_table;
--
+-- test MERGE
+--
+CREATE TABLE rule_merge1 (a int, b text);
+CREATE TABLE rule_merge2 (a int, b text);
+CREATE RULE rule1 AS ON INSERT TO rule_merge1
+ DO INSTEAD INSERT INTO rule_merge2 VALUES (NEW.*);
+CREATE RULE rule2 AS ON UPDATE TO rule_merge1
+ DO INSTEAD UPDATE rule_merge2 SET a = NEW.a, b = NEW.b
+ WHERE a = OLD.a;
+CREATE RULE rule3 AS ON DELETE TO rule_merge1
+ DO INSTEAD DELETE FROM rule_merge2 WHERE a = OLD.a;
+-- MERGE not supported for table with rules
+MERGE INTO rule_merge1 t USING (SELECT 1 AS a) s
+ ON t.a = s.a
+ WHEN MATCHED AND t.a < 2 THEN
+ UPDATE SET b = b || ' updated by merge'
+ WHEN MATCHED AND t.a > 2 THEN
+ DELETE
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.a, '');
+ERROR: MERGE is not supported for relations with rules
+-- should be ok with the other table though
+MERGE INTO rule_merge2 t USING (SELECT 1 AS a) s
+ ON t.a = s.a
+ WHEN MATCHED AND t.a < 2 THEN
+ UPDATE SET b = b || ' updated by merge'
+ WHEN MATCHED AND t.a > 2 THEN
+ DELETE
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.a, '');
+--
-- Test enabling/disabling
--
CREATE TABLE ruletest1 (a int);
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 99be9ac6e9..e9d9a87ceb 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2431,6 +2431,54 @@ delete from self_ref where a = 1;
NOTICE: trigger_func(self_ref) called: action = DELETE, when = BEFORE, level = STATEMENT
NOTICE: trigger = self_ref_s_trig, old table = (1,), (2,1), (3,2), (4,3)
drop table self_ref;
+--
+-- test transition tables with MERGE
+--
+create table merge_target_table (a int primary key, b text);
+create trigger merge_target_table_insert_trig
+ after insert on merge_target_table referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger merge_target_table_update_trig
+ after update on merge_target_table referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger merge_target_table_delete_trig
+ after delete on merge_target_table referencing old table as old_table
+ for each statement execute procedure dump_delete();
+create table merge_source_table (a int, b text);
+insert into merge_source_table
+ values (1, 'initial1'), (2, 'initial2'),
+ (3, 'initial3'), (4, 'initial4');
+merge into merge_target_table t
+using merge_source_table s
+on t.a = s.a
+when not matched then
+ insert values (a, b);
+NOTICE: trigger = merge_target_table_insert_trig, new table = (1,initial1), (2,initial2), (3,initial3), (4,initial4)
+merge into merge_target_table t
+using merge_source_table s
+on t.a = s.a
+when matched and s.a <= 2 then
+ update set b = t.b || ' updated by merge'
+when matched and s.a > 2 then
+ delete
+when not matched then
+ insert values (a, b);
+NOTICE: trigger = merge_target_table_delete_trig, old table = (3,initial3), (4,initial4)
+NOTICE: trigger = merge_target_table_update_trig, old table = (1,initial1), (2,initial2), new table = (1,"initial1 updated by merge"), (2,"initial2 updated by merge")
+NOTICE: trigger = merge_target_table_insert_trig, new table = <NULL>
+merge into merge_target_table t
+using merge_source_table s
+on t.a = s.a
+when matched and s.a <= 2 then
+ update set b = t.b || ' updated again by merge'
+when matched and s.a > 2 then
+ delete
+when not matched then
+ insert values (a, b);
+NOTICE: trigger = merge_target_table_delete_trig, old table = <NULL>
+NOTICE: trigger = merge_target_table_update_trig, old table = (1,"initial1 updated by merge"), (2,"initial2 updated by merge"), new table = (1,"initial1 updated by merge updated again by merge"), (2,"initial2 updated by merge updated again by merge")
+NOTICE: trigger = merge_target_table_insert_trig, new table = (3,initial3), (4,initial4)
+drop table merge_source_table, merge_target_table;
-- cleanup
drop function dump_insert();
drop function dump_update();
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index d308a05117..bfb9f11156 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -84,7 +84,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi
# ----------
# Another group of parallel tests
# ----------
-test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password
+test: brin gin gist spgist privileges init_privs security_label collate matview lock replica_identity rowsecurity object_address tablesample groupingsets drop_operator password merge
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 45147e9328..900814d33c 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -122,6 +122,7 @@ test: tablesample
test: groupingsets
test: drop_operator
test: password
+test: merge
test: alter_generic
test: alter_operator
test: misc
diff --git a/src/test/regress/sql/identity.sql b/src/test/regress/sql/identity.sql
index a35f331f4e..f8f34eaf18 100644
--- a/src/test/regress/sql/identity.sql
+++ b/src/test/regress/sql/identity.sql
@@ -246,3 +246,48 @@ CREATE TABLE itest_child PARTITION OF itest_parent (
f3 WITH OPTIONS GENERATED ALWAYS AS IDENTITY
) FOR VALUES FROM ('2016-07-01') TO ('2016-08-01'); -- error
DROP TABLE itest_parent;
+
+-- MERGE tests
+CREATE TABLE itest14 (a int GENERATED ALWAYS AS IDENTITY, b text);
+CREATE TABLE itest15 (a int GENERATED BY DEFAULT AS IDENTITY, b text);
+
+MERGE INTO itest14 t
+USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) VALUES (s.s_a, s.s_b);
+
+MERGE INTO itest14 t
+USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b);
+
+MERGE INTO itest14 t
+USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b);
+
+MERGE INTO itest15 t
+USING (SELECT 10 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) VALUES (s.s_a, s.s_b);
+
+MERGE INTO itest15 t
+USING (SELECT 20 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) OVERRIDING USER VALUE VALUES (s.s_a, s.s_b);
+
+MERGE INTO itest15 t
+USING (SELECT 30 AS s_a, 'inserted by merge' AS s_b) s
+ON t.a = s.s_a
+WHEN NOT MATCHED THEN
+ INSERT (a, b) OVERRIDING SYSTEM VALUE VALUES (s.s_a, s.s_b);
+
+SELECT * FROM itest14;
+SELECT * FROM itest15;
+DROP TABLE itest14;
+DROP TABLE itest15;
diff --git a/src/test/regress/sql/merge.sql b/src/test/regress/sql/merge.sql
new file mode 100644
index 0000000000..6fb833c5c7
--- /dev/null
+++ b/src/test/regress/sql/merge.sql
@@ -0,0 +1,1068 @@
+--
+-- MERGE
+--
+--\set VERBOSITY verbose
+
+--set debug_print_rewritten = true;
+--set debug_print_parse = true;
+--set debug_print_pretty = true;
+
+
+CREATE USER merge_privs;
+CREATE USER merge_no_privs;
+DROP TABLE IF EXISTS target;
+DROP TABLE IF EXISTS source;
+CREATE TABLE target (tid integer, balance integer);
+CREATE TABLE source (sid integer, delta integer); --no index
+INSERT INTO target VALUES (1, 10);
+INSERT INTO target VALUES (2, 20);
+INSERT INTO target VALUES (3, 30);
+SELECT t.ctid is not null as matched, t.*, s.* FROM source s FULL OUTER JOIN target t ON s.sid = t.tid ORDER BY t.tid, s.sid;
+
+ALTER TABLE target OWNER TO merge_privs;
+ALTER TABLE source OWNER TO merge_privs;
+
+CREATE TABLE target2 (tid integer, balance integer);
+CREATE TABLE source2 (sid integer, delta integer);
+
+ALTER TABLE target2 OWNER TO merge_no_privs;
+ALTER TABLE source2 OWNER TO merge_no_privs;
+
+GRANT INSERT ON target TO merge_no_privs;
+
+SET SESSION AUTHORIZATION merge_privs;
+
+EXPLAIN (COSTS OFF)
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+
+--
+-- Errors
+--
+MERGE INTO target t RANDOMWORD
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+-- MATCHED/INSERT error
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ INSERT DEFAULT VALUES
+;
+-- incorrectly specifying INTO target
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT INTO target DEFAULT VALUES
+;
+-- NOT MATCHED/UPDATE
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ UPDATE SET balance = 0
+;
+-- UPDATE tablename
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE target SET balance = 0
+;
+
+-- permissions
+
+MERGE INTO target
+USING source2
+ON target.tid = source2.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+
+GRANT INSERT ON target TO merge_no_privs;
+SET SESSION AUTHORIZATION merge_no_privs;
+
+MERGE INTO target
+USING source2
+ON target.tid = source2.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+
+GRANT UPDATE ON target2 TO merge_privs;
+SET SESSION AUTHORIZATION merge_privs;
+
+MERGE INTO target2
+USING source
+ON target2.tid = source.sid
+WHEN MATCHED THEN
+ DELETE
+;
+
+MERGE INTO target2
+USING source
+ON target2.tid = source.sid
+WHEN NOT MATCHED THEN
+ INSERT DEFAULT VALUES
+;
+
+-- check if the target can be accessed from source relation subquery; we should
+-- not be able to do so
+MERGE INTO target t
+USING (SELECT * FROM source WHERE t.tid > sid) s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT DEFAULT VALUES
+;
+
+--
+-- initial tests
+--
+-- zero rows in source has no effect
+MERGE INTO target
+USING source
+ON target.tid = source.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT DEFAULT VALUES
+;
+ROLLBACK;
+
+-- insert some non-matching source rows to work from
+INSERT INTO source VALUES (4, 40);
+SELECT * FROM source ORDER BY sid;
+SELECT * FROM target ORDER BY tid;
+
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ DO NOTHING
+;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT DEFAULT VALUES
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- index plans
+INSERT INTO target SELECT generate_series(1000,2500), 0;
+ALTER TABLE target ADD PRIMARY KEY (tid);
+ANALYZE target;
+
+EXPLAIN (COSTS OFF)
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+EXPLAIN (COSTS OFF)
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+EXPLAIN (COSTS OFF)
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT VALUES (4, NULL);
+;
+DELETE FROM target WHERE tid > 100;
+ANALYZE target;
+
+-- insert some matching source rows to work from
+INSERT INTO source VALUES (2, 5);
+INSERT INTO source VALUES (3, 20);
+SELECT * FROM source ORDER BY sid;
+SELECT * FROM target ORDER BY tid;
+
+-- equivalent of an UPDATE join
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- equivalent of a DELETE join
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT VALUES (4, NULL)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- duplicate source row causes multiple target row update ERROR
+INSERT INTO source VALUES (2, 5);
+SELECT * FROM source ORDER BY sid;
+SELECT * FROM target ORDER BY tid;
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+ROLLBACK;
+
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ DELETE
+;
+ROLLBACK;
+
+-- correct source data
+DELETE FROM source WHERE sid = 2;
+INSERT INTO source VALUES (2, 5);
+SELECT * FROM source ORDER BY sid;
+SELECT * FROM target ORDER BY tid;
+
+-- remove constraints
+alter table target drop CONSTRAINT target_pkey;
+alter table target alter column tid drop not null;
+
+-- multiple actions
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT VALUES (4, 4)
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- should be equivalent
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = 0
+WHEN NOT MATCHED THEN
+ INSERT VALUES (4, 4);
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- column references
+-- do a simple equivalent of an UPDATE join
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = t.balance + s.delta
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- do a simple equivalent of an INSERT SELECT
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- and again with explicitly identified column list
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- and again with a subtle error: referring to non-existent target row for NOT MATCHED
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (t.tid, s.delta)
+;
+
+-- and again with a constant ON clause
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON (SELECT true)
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (t.tid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- now the classic UPSERT
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = t.balance + s.delta
+WHEN NOT MATCHED THEN
+ INSERT VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- unreachable WHEN clause should ERROR
+BEGIN;
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN /* Terminal WHEN clause for MATCHED */
+ DELETE
+WHEN MATCHED AND s.delta > 0 THEN
+ UPDATE SET balance = t.balance - s.delta
+;
+ROLLBACK;
+
+-- conditional WHEN clause
+CREATE TABLE wq_target (tid integer not null, balance integer DEFAULT -1);
+CREATE TABLE wq_source (balance integer, sid integer);
+
+INSERT INTO wq_source (sid, balance) VALUES (1, 100);
+
+BEGIN;
+-- try a simple INSERT with default values first
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+ROLLBACK;
+
+-- this time with a FALSE condition
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED AND FALSE THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+
+-- this time with an actual condition which returns false
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED AND s.balance <> 100 THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+
+BEGIN;
+-- and now with a condition which returns true
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED AND s.balance = 100 THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+ROLLBACK;
+
+-- conditions in the NOT MATCHED clause can only refer to source columns
+BEGIN;
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED AND t.balance = 100 THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+ROLLBACK;
+
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN NOT MATCHED AND s.balance = 100 THEN
+ INSERT (tid) VALUES (s.sid);
+SELECT * FROM wq_target;
+
+-- conditions in MATCHED clause can refer to both source and target
+SELECT * FROM wq_source;
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND s.balance = 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance = 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+
+-- check if AND works
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance = 99 AND s.balance > 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance = 99 AND s.balance = 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+
+-- check if OR works
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance = 99 OR s.balance > 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance = 199 OR s.balance > 100 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+
+-- check if subqueries work in the conditions?
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.balance > (SELECT max(balance) FROM target) THEN
+ UPDATE SET balance = t.balance + s.balance;
+
+-- check if we can access system columns in the conditions
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.xmin = t.xmax THEN
+ UPDATE SET balance = t.balance + s.balance;
+
+ALTER TABLE wq_target SET WITH OIDS;
+SELECT * FROM wq_target;
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND t.oid >= 0 THEN
+ UPDATE SET balance = t.balance + s.balance;
+SELECT * FROM wq_target;
+
+-- test preventing WHEN AND conditions from writing to the database
+create or replace function merge_when_and_write() returns boolean
+language plpgsql as
+$$
+BEGIN
+ INSERT INTO target VALUES (100, 100);
+ RETURN TRUE;
+END;
+$$;
+
+BEGIN;
+MERGE INTO wq_target t
+USING wq_source s ON t.tid = s.sid
+WHEN MATCHED AND (merge_when_and_write()) THEN
+ UPDATE SET balance = t.balance + s.balance;
+ROLLBACK;
+drop function merge_when_and_write();
+
+DROP TABLE wq_target, wq_source;
+
+-- test triggers
+create or replace function merge_trigfunc () returns trigger
+language plpgsql as
+$$
+BEGIN
+ RAISE NOTICE '% % % trigger', TG_WHEN, TG_OP, TG_LEVEL;
+ IF (TG_WHEN = 'BEFORE' AND TG_LEVEL = 'ROW') THEN
+ IF (TG_OP = 'DELETE') THEN
+ RETURN OLD;
+ ELSE
+ RETURN NEW;
+ END IF;
+ ELSE
+ RETURN NULL;
+ END IF;
+END;
+$$;
+CREATE TRIGGER merge_bsi BEFORE INSERT ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_bsu BEFORE UPDATE ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_bsd BEFORE DELETE ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_asi AFTER INSERT ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_asu AFTER UPDATE ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_asd AFTER DELETE ON target FOR EACH STATEMENT EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_bri BEFORE INSERT ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_bru BEFORE UPDATE ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_brd BEFORE DELETE ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_ari AFTER INSERT ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_aru AFTER UPDATE ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+CREATE TRIGGER merge_ard AFTER DELETE ON target FOR EACH ROW EXECUTE PROCEDURE merge_trigfunc ();
+
+-- now the classic UPSERT, with a DELETE
+BEGIN;
+UPDATE target SET balance = 0 WHERE tid = 3;
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED AND t.balance > s.delta THEN
+ UPDATE SET balance = t.balance - s.delta
+WHEN MATCHED THEN
+ DELETE
+WHEN NOT MATCHED THEN
+ INSERT VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- test from PL/pgSQL
+-- make sure MERGE INTO isn't interpreted to mean returning variables like SELECT INTO
+BEGIN;
+DO LANGUAGE plpgsql $$
+BEGIN
+MERGE INTO target t
+USING source AS s
+ON t.tid = s.sid
+WHEN MATCHED AND t.balance > s.delta THEN
+ UPDATE SET balance = t.balance - s.delta
+;
+END;
+$$;
+ROLLBACK;
+
+--source constants
+BEGIN;
+MERGE INTO target t
+USING (SELECT 9 AS sid, 57 AS delta) AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+--source query
+BEGIN;
+MERGE INTO target t
+USING (SELECT sid, delta FROM source WHERE delta > 0) AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+BEGIN;
+MERGE INTO target t
+USING (SELECT sid, delta as newname FROM source WHERE delta > 0) AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.newname)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+--self-merge
+BEGIN;
+MERGE INTO target t1
+USING target t2
+ON t1.tid = t2.tid
+WHEN MATCHED THEN
+ UPDATE SET balance = t1.balance + t2.balance
+WHEN NOT MATCHED THEN
+ INSERT VALUES (t2.tid, t2.balance)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+BEGIN;
+MERGE INTO target t
+USING (SELECT tid as sid, balance as delta FROM target WHERE balance > 0) AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+BEGIN;
+MERGE INTO target t
+USING
+(SELECT sid, max(delta) AS delta
+ FROM source
+ GROUP BY sid
+ HAVING count(*) = 1
+ ORDER BY sid ASC) AS s
+ON t.tid = s.sid
+WHEN NOT MATCHED THEN
+ INSERT (tid, balance) VALUES (s.sid, s.delta)
+;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- plpgsql parameters and results
+BEGIN;
+CREATE FUNCTION merge_func (p_id integer, p_bal integer)
+RETURNS INTEGER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ result integer;
+BEGIN
+MERGE INTO target t
+USING (SELECT p_id AS sid) AS s
+ON t.tid = s.sid
+WHEN MATCHED THEN
+ UPDATE SET balance = t.balance - p_bal
+;
+IF FOUND THEN
+ GET DIAGNOSTICS result := ROW_COUNT;
+END IF;
+RETURN result;
+END;
+$$;
+SELECT merge_func(3, 4);
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- PREPARE
+BEGIN;
+prepare foom as merge into target t using (select 1 as sid) s on (t.tid = s.sid) when matched then update set balance = 1;
+execute foom;
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+BEGIN;
+PREPARE foom2 (integer, integer) AS
+MERGE INTO target t
+USING (SELECT 1) s
+ON t.tid = $1
+WHEN MATCHED THEN
+UPDATE SET balance = $2;
+EXPLAIN (ANALYZE ON, COSTS OFF, SUMMARY OFF, TIMING OFF)
+execute foom2 (1, 1);
+SELECT * FROM target ORDER BY tid;
+ROLLBACK;
+
+-- subqueries in source relation
+
+CREATE TABLE sq_target (tid integer NOT NULL, balance integer);
+CREATE TABLE sq_source (delta integer, sid integer, balance integer DEFAULT 0);
+
+INSERT INTO sq_target(tid, balance) VALUES (1,100), (2,200), (3,300);
+INSERT INTO sq_source(sid, delta) VALUES (1,10), (2,20), (4,40);
+
+BEGIN;
+MERGE INTO sq_target t
+USING (SELECT * FROM sq_source) s
+ON tid = sid
+WHEN MATCHED AND t.balance > delta THEN
+ UPDATE SET balance = t.balance + delta;
+SELECT * FROM sq_target;
+ROLLBACK;
+
+-- try a view
+CREATE VIEW v AS SELECT * FROM sq_source WHERE sid < 2;
+
+BEGIN;
+MERGE INTO sq_target
+USING v
+ON tid = sid
+WHEN MATCHED THEN
+ UPDATE SET balance = v.balance + delta;
+SELECT * FROM sq_target;
+ROLLBACK;
+
+-- ambiguous reference to a column
+BEGIN;
+MERGE INTO sq_target
+USING v
+ON tid = sid
+WHEN MATCHED AND tid > 2 THEN
+ UPDATE SET balance = balance + delta
+WHEN NOT MATCHED THEN
+ INSERT (balance, tid) VALUES (balance + delta, sid)
+WHEN MATCHED AND tid < 2 THEN
+ DELETE;
+ROLLBACK;
+
+BEGIN;
+INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+MERGE INTO sq_target t
+USING v
+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;
+SELECT * FROM sq_target;
+ROLLBACK;
+
+-- CTEs
+BEGIN;
+INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+WITH targq AS (
+ SELECT * FROM v
+)
+MERGE INTO sq_target t
+USING v
+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
+;
+ROLLBACK;
+
+-- RETURNING
+BEGIN;
+INSERT INTO sq_source (sid, balance, delta) VALUES (-1, -1, -10);
+MERGE INTO sq_target t
+USING v
+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 *
+;
+ROLLBACK;
+
+-- Subqueries
+BEGIN;
+MERGE INTO sq_target t
+USING v
+ON tid = sid
+WHEN MATCHED THEN
+ UPDATE SET balance = (SELECT count(*) FROM sq_target)
+;
+ROLLBACK;
+
+BEGIN;
+MERGE INTO sq_target t
+USING v
+ON tid = sid
+WHEN MATCHED AND (SELECT count(*) > 0 FROM sq_target) THEN
+ UPDATE SET balance = 42
+;
+ROLLBACK;
+
+BEGIN;
+MERGE INTO sq_target t
+USING v
+ON tid = sid AND (SELECT count(*) > 0 FROM sq_target)
+WHEN MATCHED THEN
+ UPDATE SET balance = 42
+;
+ROLLBACK;
+
+DROP TABLE sq_target, sq_source CASCADE;
+
+CREATE TABLE pa_target (tid integer, balance float, val text)
+ PARTITION BY LIST (tid);
+
+CREATE TABLE part1 PARTITION OF pa_target FOR VALUES IN (1,4);
+CREATE TABLE part2 PARTITION OF pa_target FOR VALUES IN (2,5,6);
+CREATE TABLE part3 PARTITION OF pa_target FOR VALUES IN (3,8,9);
+CREATE TABLE part4 PARTITION OF pa_target DEFAULT;
+
+CREATE TABLE pa_source (sid integer, delta float);
+-- insert many rows to the source table
+INSERT INTO pa_source SELECT id, id * 10 FROM generate_series(1,14) AS id;
+-- insert a few rows in the target table (odd numbered tid)
+INSERT INTO pa_target SELECT id, id * 100, 'initial' FROM generate_series(1,14,2) AS id;
+
+-- try simple MERGE
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid
+ WHEN MATCHED THEN
+ UPDATE SET balance = balance + delta, val = val || ' updated by merge'
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (sid, delta, 'inserted by merge');
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
+-- same with a constant qual
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET balance = balance + delta, val = val || ' updated by merge'
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (sid, delta, 'inserted by merge');
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
+-- try updating the partition key column
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid
+ 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');
+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
+-- partitions which have columns in different order, dropped columns etc.
+CREATE TABLE pa_target (tid integer, balance float, val text)
+ PARTITION BY LIST (tid);
+CREATE TABLE part1 (tid integer, balance float, val text);
+CREATE TABLE part2 (balance float, tid integer, val text);
+CREATE TABLE part3 (tid integer, balance float, val text);
+CREATE TABLE part4 (extraid text, tid integer, balance float, val text);
+ALTER TABLE part4 DROP COLUMN extraid;
+
+ALTER TABLE pa_target ATTACH PARTITION part1 FOR VALUES IN (1,4);
+ALTER TABLE pa_target ATTACH PARTITION part2 FOR VALUES IN (2,5,6);
+ALTER TABLE pa_target ATTACH PARTITION part3 FOR VALUES IN (3,8,9);
+ALTER TABLE pa_target ATTACH PARTITION part4 DEFAULT;
+
+-- insert a few rows in the target table (odd numbered tid)
+INSERT INTO pa_target SELECT id, id * 100, 'initial' FROM generate_series(1,14,2) AS id;
+
+-- try simple MERGE
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid
+ WHEN MATCHED THEN
+ UPDATE SET balance = balance + delta, val = val || ' updated by merge'
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (sid, delta, 'inserted by merge');
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
+-- same with a constant qual
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid AND tid = 1
+ WHEN MATCHED THEN
+ UPDATE SET balance = balance + delta, val = val || ' updated by merge'
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (sid, delta, 'inserted by merge');
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
+-- try updating the partition key column
+BEGIN;
+MERGE INTO pa_target t
+ USING pa_source s
+ ON t.tid = s.sid
+ 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');
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
+DROP TABLE pa_source;
+DROP TABLE pa_target CASCADE;
+
+-- Sub-partitionin
+CREATE TABLE pa_target (logts timestamp, tid integer, balance float, val text)
+ PARTITION BY RANGE (logts);
+
+CREATE TABLE part_m01 PARTITION OF pa_target
+ FOR VALUES FROM ('2017-01-01') TO ('2017-02-01')
+ PARTITION BY LIST (tid);
+CREATE TABLE part_m01_odd PARTITION OF part_m01
+ FOR VALUES IN (1,3,5,7,9);
+CREATE TABLE part_m01_even PARTITION OF part_m01
+ FOR VALUES IN (2,4,6,8);
+CREATE TABLE part_m02 PARTITION OF pa_target
+ FOR VALUES FROM ('2017-02-01') TO ('2017-03-01')
+ PARTITION BY LIST (tid);
+CREATE TABLE part_m02_odd PARTITION OF part_m02
+ FOR VALUES IN (1,3,5,7,9);
+CREATE TABLE part_m02_even PARTITION OF part_m02
+ FOR VALUES IN (2,4,6,8);
+
+CREATE TABLE pa_source (sid integer, delta float);
+-- insert many rows to the source table
+INSERT INTO pa_source SELECT id, id * 10 FROM generate_series(1,14) AS id;
+-- insert a few rows in the target table (odd numbered tid)
+INSERT INTO pa_target SELECT '2017-01-31', id, id * 100, 'initial' FROM generate_series(1,9,3) AS id;
+INSERT INTO pa_target SELECT '2017-02-28', id, id * 100, 'initial' FROM generate_series(2,9,3) AS id;
+
+-- try simple MERGE
+BEGIN;
+MERGE INTO pa_target t
+ USING (SELECT '2017-01-15' AS slogts, * FROM pa_source WHERE sid < 10) s
+ ON t.tid = s.sid
+ 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');
+SELECT * FROM pa_target ORDER BY tid;
+ROLLBACK;
+
+DROP TABLE pa_source;
+DROP TABLE pa_target CASCADE;
+
+-- some complex joins on the source side
+
+CREATE TABLE cj_target (tid integer, balance float, val text);
+CREATE TABLE cj_source1 (sid1 integer, scat integer, delta integer);
+CREATE TABLE cj_source2 (sid2 integer, sval text);
+INSERT INTO cj_source1 VALUES (1, 10, 100);
+INSERT INTO cj_source1 VALUES (1, 20, 200);
+INSERT INTO cj_source1 VALUES (2, 20, 300);
+INSERT INTO cj_source1 VALUES (3, 10, 400);
+INSERT INTO cj_source2 VALUES (1, 'initial source2');
+INSERT INTO cj_source2 VALUES (2, 'initial source2');
+INSERT INTO cj_source2 VALUES (3, 'initial source2');
+
+-- source relation is an unalised join
+MERGE INTO cj_target t
+USING cj_source1 s1
+ INNER JOIN cj_source2 s2 ON sid1 = sid2
+ON t.tid = sid1
+WHEN NOT MATCHED THEN
+ INSERT VALUES (sid1, delta, sval);
+
+-- try accessing columns from either side of the source join
+MERGE INTO cj_target t
+USING cj_source2 s2
+ INNER JOIN cj_source1 s1 ON sid1 = sid2 AND scat = 20
+ON t.tid = sid1
+WHEN NOT MATCHED THEN
+ INSERT VALUES (sid2, delta, sval)
+WHEN MATCHED THEN
+ DELETE;
+
+-- some simple expressions in INSERT targetlist
+MERGE INTO cj_target t
+USING cj_source2 s2
+ INNER JOIN cj_source1 s1 ON sid1 = sid2
+ON t.tid = sid1
+WHEN NOT MATCHED THEN
+ INSERT VALUES (sid2, delta + scat, sval)
+WHEN MATCHED THEN
+ UPDATE SET val = val || ' updated by merge';
+
+MERGE INTO cj_target t
+USING cj_source2 s2
+ INNER JOIN cj_source1 s1 ON sid1 = sid2 AND scat = 20
+ON t.tid = sid1
+WHEN MATCHED THEN
+ UPDATE SET val = val || ' ' || delta::text;
+
+SELECT * FROM cj_target;
+
+ALTER TABLE cj_source1 RENAME COLUMN sid1 TO sid;
+ALTER TABLE cj_source2 RENAME COLUMN sid2 TO sid;
+
+TRUNCATE cj_target;
+
+MERGE INTO cj_target t
+USING cj_source1 s1
+ INNER JOIN cj_source2 s2 ON s1.sid = s2.sid
+ON t.tid = s1.sid
+WHEN NOT MATCHED THEN
+ INSERT VALUES (s2.sid, delta, sval);
+
+DROP TABLE cj_source2, cj_source1, cj_target;
+
+-- Function scans
+CREATE TABLE fs_target (a int, b int, c text);
+MERGE INTO fs_target t
+USING generate_series(1,100,1) AS id
+ON t.a = id
+WHEN MATCHED THEN
+ UPDATE SET b = b + id
+WHEN NOT MATCHED THEN
+ INSERT VALUES (id, -1);
+
+MERGE INTO fs_target t
+USING generate_series(1,100,2) AS id
+ON t.a = id
+WHEN MATCHED THEN
+ UPDATE SET b = b + id, c = 'updated '|| id.*::text
+WHEN NOT MATCHED THEN
+ INSERT VALUES (id, -1, 'inserted ' || id.*::text);
+
+SELECT count(*) FROM fs_target;
+DROP TABLE fs_target;
+
+-- SERIALIZABLE test
+-- handled in isolation tests
+
+-- prepare
+
+RESET SESSION AUTHORIZATION;
+DROP TABLE target, target2;
+DROP TABLE source, source2;
+DROP FUNCTION merge_trigfunc();
+DROP USER merge_privs;
+DROP USER merge_no_privs;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index f3a31dbee0..480ec3481e 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -812,6 +812,130 @@ INSERT INTO document VALUES (4, (SELECT cid from category WHERE cname = 'novel')
INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'my first novel')
ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol';
+--
+-- MERGE
+--
+RESET SESSION AUTHORIZATION;
+DROP POLICY p3_with_all ON document;
+
+ALTER TABLE document ADD COLUMN dnotes text DEFAULT '';
+-- all documents are readable
+CREATE POLICY p1 ON document FOR SELECT USING (true);
+-- one may insert documents only authored by them
+CREATE POLICY p2 ON document FOR INSERT WITH CHECK (dauthor = current_user);
+-- one may only update documents in 'novel' category
+CREATE POLICY p3 ON document FOR UPDATE
+ USING (cid = (SELECT cid from category WHERE cname = 'novel'))
+ WITH CHECK (dauthor = current_user);
+-- one may only delete documents in 'manga' category
+CREATE POLICY p4 ON document FOR DELETE
+ USING (cid = (SELECT cid from category WHERE cname = 'manga'));
+
+SELECT * FROM document;
+
+SET SESSION AUTHORIZATION regress_rls_bob;
+
+-- Fails, since update violates WITH CHECK qual on dauthor
+MERGE INTO document d
+USING (SELECT 1 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge1 ', dauthor = 'regress_rls_alice';
+
+-- Should be OK since USING and WITH CHECK quals pass
+MERGE INTO document d
+USING (SELECT 1 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge2 ';
+
+-- Even when dauthor is updated explicitly, but to the existing value
+MERGE INTO document d
+USING (SELECT 1 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge3 ', dauthor = 'regress_rls_bob';
+
+-- There is a MATCH for did = 3, but UPDATE's USING qual does not allow
+-- updating an item in category 'science fiction'
+MERGE INTO document d
+USING (SELECT 3 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge ';
+
+-- The same thing with DELETE action, but fails again because no permissions
+-- to delete items in 'science fiction' category that did 3 belongs to.
+MERGE INTO document d
+USING (SELECT 3 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ DELETE;
+
+-- Document with did 4 belongs to 'manga' category which is allowed for
+-- deletion. But this fails because the UPDATE action is matched first and
+-- UPDATE policy does not allow updation in the category.
+MERGE INTO document d
+USING (SELECT 4 as sdid) s
+ON did = s.sdid
+WHEN MATCHED AND dnotes = '' THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge '
+WHEN MATCHED THEN
+ DELETE;
+
+-- UPDATE action is not matched this time because of the WHEN AND qual.
+-- DELETE still fails because role regress_rls_bob does not have SELECT
+-- privileges on 'manga' category row in the category table.
+MERGE INTO document d
+USING (SELECT 4 as sdid) s
+ON did = s.sdid
+WHEN MATCHED AND dnotes <> '' THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge '
+WHEN MATCHED THEN
+ DELETE;
+
+SELECT * FROM document WHERE did = 4;
+
+-- Switch to regress_rls_carol role and try the DELETE again. It should succeed
+-- this time
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION regress_rls_carol;
+
+MERGE INTO document d
+USING (SELECT 4 as sdid) s
+ON did = s.sdid
+WHEN MATCHED AND dnotes <> '' THEN
+ UPDATE SET dnotes = dnotes || ' notes added by merge '
+WHEN MATCHED THEN
+ DELETE;
+
+-- Switch back to regress_rls_bob role
+RESET SESSION AUTHORIZATION;
+SET SESSION AUTHORIZATION regress_rls_bob;
+
+-- Try INSERT action. This fails because we are trying to insert
+-- dauthor = regress_rls_dave and INSERT's WITH CHECK does not allow
+-- that
+MERGE INTO document d
+USING (SELECT 12 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ DELETE
+WHEN NOT MATCHED THEN
+ INSERT VALUES (12, 11, 1, 'regress_rls_dave', 'another novel');
+
+-- This should be fine
+MERGE INTO document d
+USING (SELECT 12 as sdid) s
+ON did = s.sdid
+WHEN MATCHED THEN
+ DELETE
+WHEN NOT MATCHED THEN
+ INSERT VALUES (12, 11, 1, 'regress_rls_bob', 'another novel');
+
+-- Just check everything went per plan
+SELECT * FROM document;
+
--
-- ROLE/GROUP
--
diff --git a/src/test/regress/sql/rules.sql b/src/test/regress/sql/rules.sql
index a82f52d154..b866268892 100644
--- a/src/test/regress/sql/rules.sql
+++ b/src/test/regress/sql/rules.sql
@@ -1191,6 +1191,39 @@ CREATE RULE rules_parted_table_insert AS ON INSERT to rules_parted_table
ALTER RULE rules_parted_table_insert ON rules_parted_table RENAME TO rules_parted_table_insert_redirect;
DROP TABLE rules_parted_table;
+--
+-- test MERGE
+--
+CREATE TABLE rule_merge1 (a int, b text);
+CREATE TABLE rule_merge2 (a int, b text);
+CREATE RULE rule1 AS ON INSERT TO rule_merge1
+ DO INSTEAD INSERT INTO rule_merge2 VALUES (NEW.*);
+CREATE RULE rule2 AS ON UPDATE TO rule_merge1
+ DO INSTEAD UPDATE rule_merge2 SET a = NEW.a, b = NEW.b
+ WHERE a = OLD.a;
+CREATE RULE rule3 AS ON DELETE TO rule_merge1
+ DO INSTEAD DELETE FROM rule_merge2 WHERE a = OLD.a;
+
+-- MERGE not supported for table with rules
+MERGE INTO rule_merge1 t USING (SELECT 1 AS a) s
+ ON t.a = s.a
+ WHEN MATCHED AND t.a < 2 THEN
+ UPDATE SET b = b || ' updated by merge'
+ WHEN MATCHED AND t.a > 2 THEN
+ DELETE
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.a, '');
+
+-- should be ok with the other table though
+MERGE INTO rule_merge2 t USING (SELECT 1 AS a) s
+ ON t.a = s.a
+ WHEN MATCHED AND t.a < 2 THEN
+ UPDATE SET b = b || ' updated by merge'
+ WHEN MATCHED AND t.a > 2 THEN
+ DELETE
+ WHEN NOT MATCHED THEN
+ INSERT VALUES (s.a, '');
+
--
-- Test enabling/disabling
--
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 3354f4899f..81ec74a66b 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1866,6 +1866,53 @@ delete from self_ref where a = 1;
drop table self_ref;
+--
+-- test transition tables with MERGE
+--
+create table merge_target_table (a int primary key, b text);
+create trigger merge_target_table_insert_trig
+ after insert on merge_target_table referencing new table as new_table
+ for each statement execute procedure dump_insert();
+create trigger merge_target_table_update_trig
+ after update on merge_target_table referencing old table as old_table new table as new_table
+ for each statement execute procedure dump_update();
+create trigger merge_target_table_delete_trig
+ after delete on merge_target_table referencing old table as old_table
+ for each statement execute procedure dump_delete();
+
+create table merge_source_table (a int, b text);
+insert into merge_source_table
+ values (1, 'initial1'), (2, 'initial2'),
+ (3, 'initial3'), (4, 'initial4');
+
+merge into merge_target_table t
+using merge_source_table s
+on t.a = s.a
+when not matched then
+ insert values (a, b);
+
+merge into merge_target_table t
+using merge_source_table s
+on t.a = s.a
+when matched and s.a <= 2 then
+ update set b = t.b || ' updated by merge'
+when matched and s.a > 2 then
+ delete
+when not matched then
+ insert values (a, b);
+
+merge into merge_target_table t
+using merge_source_table s
+on t.a = s.a
+when matched and s.a <= 2 then
+ update set b = t.b || ' updated again by merge'
+when matched and s.a > 2 then
+ delete
+when not matched then
+ insert values (a, b);
+
+drop table merge_source_table, merge_target_table;
+
-- cleanup
drop function dump_insert();
drop function dump_update();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 63d8c634e2..f0d2766f58 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1226,6 +1226,8 @@ MemoryContextCallbackFunction
MemoryContextCounters
MemoryContextData
MemoryContextMethods
+MergeAction
+MergeActionState
MergeAppend
MergeAppendPath
MergeAppendState
@@ -1233,6 +1235,7 @@ MergeJoin
MergeJoinClause
MergeJoinState
MergePath
+MergeStmt
MergeScanSelCache
MetaCommand
MinMaxAggInfo