mirror of
https://github.com/postgres/postgres.git
synced 2025-05-19 00:04:06 -04:00
Revert "MERGE SQL Command following SQL:2016"
This reverts commit e6597dc3533946b98acba7871bd4ca1f7a3d4c1d.
This commit is contained in:
parent
7cf8a5c302
commit
aa5877bb26
@ -1,603 +0,0 @@
|
||||
<!--
|
||||
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_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_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_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>
|
||||
|
||||
<para>
|
||||
MERGE is not supported if the <replaceable
|
||||
class="parameter">target_table_name</replaceable> has
|
||||
<literal>RULES</literal> defined on it.
|
||||
See <xref linkend="rules"/> for more information about <literal>RULES</literal>.
|
||||
</para>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<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 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.
|
||||
</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 inserted, updated, 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>
|
@ -1,575 +0,0 @@
|
||||
/*-------------------------------------------------------------------------
|
||||
*
|
||||
* nodeMerge.c
|
||||
* routines to handle Merge nodes relating to the MERGE command
|
||||
*
|
||||
* 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 WHEN AND quals
|
||||
* pass, if any. If the WHEN AND 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.
|
||||
*
|
||||
* If 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 first
|
||||
* action to look for a qualifying action. Otherwise, we return false meaning
|
||||
* that a NOT MATCHED action must now be executed for the current source tuple.
|
||||
*/
|
||||
static bool
|
||||
ExecMergeMatched(ModifyTableState *mtstate, EState *estate,
|
||||
TupleTableSlot *slot, JunkFilter *junkfilter,
|
||||
ItemPointer tupleid)
|
||||
{
|
||||
ExprContext *econtext = mtstate->ps.ps_ExprContext;
|
||||
bool isNull;
|
||||
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;
|
||||
|
||||
if (mtstate->mt_partition_tuple_routing)
|
||||
{
|
||||
Datum datum;
|
||||
Oid tableoid = InvalidOid;
|
||||
int leaf_part_index;
|
||||
PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
|
||||
|
||||
/*
|
||||
* In case of partitioned table, we fetch the tableoid while performing
|
||||
* MATCHED MERGE action.
|
||||
*/
|
||||
datum = ExecGetJunkAttribute(slot, junkfilter->jf_otherJunkAttNo,
|
||||
&isNull);
|
||||
Assert(!isNull);
|
||||
tableoid = DatumGetObjectId(datum);
|
||||
|
||||
/*
|
||||
* If we're dealing with a MATCHED tuple, then tableoid must have been
|
||||
* set correctly. In case of partitioned table, we must now fetch the
|
||||
* 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.
|
||||
*/
|
||||
if (mtstate->mt_partition_tuple_routing)
|
||||
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;
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
if (mtstate->mt_partition_tuple_routing)
|
||||
ExecSetSlotDescriptor(mtstate->mt_mergeproj, 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,
|
||||
mtstate->mt_mergeproj,
|
||||
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 row matches 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 then
|
||||
* we switch to the NOT MATCHED list of actions.
|
||||
* If it does return a tuple and the join qual is
|
||||
* still satisfied, 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.
|
||||
*/
|
||||
Assert(resultRelInfo->ri_mergeTargetRTI > 0);
|
||||
epqslot = EvalPlanQual(estate,
|
||||
epqstate,
|
||||
resultRelInfo->ri_RelationDesc,
|
||||
GetEPQRangeTableIndex(resultRelInfo),
|
||||
LockTupleExclusive,
|
||||
&hufd.ctid,
|
||||
hufd.xmax);
|
||||
|
||||
if (!TupIsNull(epqslot))
|
||||
{
|
||||
(void) ExecGetJunkAttribute(epqslot,
|
||||
resultRelInfo->ri_junkFilter->jf_junkAttNo,
|
||||
&isNull);
|
||||
|
||||
/*
|
||||
* A non-NULL 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;
|
||||
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;
|
||||
ReleaseBuffer(buffer);
|
||||
return false;
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (action->commandType == CMD_UPDATE && tuple_updated)
|
||||
InstrCountFiltered2(&mtstate->ps, 1);
|
||||
if (action->commandType == CMD_DELETE && tuple_deleted)
|
||||
InstrCountFiltered3(&mtstate->ps, 1);
|
||||
|
||||
/*
|
||||
* We've activated one of the WHEN clauses, so we don't search
|
||||
* further. This is required behaviour, not an optimization.
|
||||
*/
|
||||
estate->es_result_relation_info = saved_resultRelInfo;
|
||||
break;
|
||||
}
|
||||
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
if (mtstate->mt_partition_tuple_routing)
|
||||
ExecSetSlotDescriptor(mtstate->mt_mergeproj, 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 = mtstate->mt_mergeproj;
|
||||
|
||||
/* 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;
|
||||
InstrCountFiltered1(&mtstate->ps, 1);
|
||||
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)
|
||||
{
|
||||
ExprContext *econtext = mtstate->ps.ps_ExprContext;
|
||||
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_PARTITIONED_TABLE);
|
||||
|
||||
/*
|
||||
* Reset per-tuple memory context to free any expression evaluation
|
||||
* storage allocated in the previous cycle.
|
||||
*/
|
||||
ResetExprContext(econtext);
|
||||
|
||||
/*
|
||||
* 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 execute the first action
|
||||
* for which the additional WHEN MATCHED AND quals pass. If an action
|
||||
* without 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 sequence until one passes.
|
||||
*
|
||||
* 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 can:
|
||||
*
|
||||
* 1. modify the target tuple so that it no longer satisfies the
|
||||
* additional quals attached to the current WHEN MATCHED action OR
|
||||
*
|
||||
* In this 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.
|
||||
*
|
||||
* 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 second case, the source tuple no longer matches the target tuple,
|
||||
* so we now instead find a qualifying WHEN NOT MATCHED action to execute.
|
||||
*
|
||||
* 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)
|
||||
matched = ExecMergeMatched(mtstate, estate, slot, junkfilter, tupleid);
|
||||
|
||||
/*
|
||||
* Either we were dealing with a NOT MATCHED tuple or ExecMergeNotMatched()
|
||||
* returned "false", indicating the previously MATCHED tuple is no longer a
|
||||
* matching tuple.
|
||||
*/
|
||||
if (!matched)
|
||||
ExecMergeNotMatched(mtstate, estate, slot);
|
||||
}
|
@ -1,660 +0,0 @@
|
||||
/*-------------------------------------------------------------------------
|
||||
*
|
||||
* 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);
|
||||
static List *expandSourceTL(ParseState *pstate, RangeTblEntry *rte,
|
||||
int rtindex);
|
||||
|
||||
/*
|
||||
* 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 references 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 = expandSourceTL(pstate, rt_rte, rt_rtindex);
|
||||
|
||||
/*
|
||||
* 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;
|
||||
RangeTblEntry *resultRelRTE, *mergeRelRTE;
|
||||
|
||||
/* 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.
|
||||
*
|
||||
* This special arrangement is required for handling partitioned tables
|
||||
* because we perform an JOIN between the target and the source relation to
|
||||
* identify the matching and not-matching rows. If we take the usual path
|
||||
* of expanding the target table's inheritance and create one subplan per
|
||||
* partition, then we we won't be able to correctly identify the matching
|
||||
* and not-matching rows since for a given source row, there may not be a
|
||||
* matching row in one partition, but it may exists in some other
|
||||
* partition. So we must first append all the qualifying rows from all the
|
||||
* partitions and then do the matching.
|
||||
*
|
||||
* 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);
|
||||
|
||||
/*
|
||||
* Create a JOIN between the target and the source relation.
|
||||
*/
|
||||
joinexpr = makeNode(JoinExpr);
|
||||
joinexpr->isNatural = false;
|
||||
joinexpr->alias = NULL;
|
||||
joinexpr->usingClause = NIL;
|
||||
joinexpr->quals = stmt->join_condition;
|
||||
joinexpr->larg = (Node *) stmt->relation;
|
||||
joinexpr->rarg = (Node *) stmt->source_relation;
|
||||
|
||||
/*
|
||||
* Simplify the MERGE query as much as possible
|
||||
*
|
||||
* These seem like things that could go into Optimizer, but they are
|
||||
* semantic simplifications 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.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* The target relation when used in the underlying join, gets a new RTE
|
||||
* with rte->inh set to true. We remember this RTE (and later pass on to
|
||||
* the planner and executor) for two main reasons:
|
||||
*
|
||||
* 1. If we ever need to run EvalPlanQual while performing MERGE, we must
|
||||
* make the modified tuple available to the underlying join query, which is
|
||||
* using a different RTE from the resultRelation RTE.
|
||||
*
|
||||
* 2. rewriteTargetListMerge() requires the RTE of the underlying join in
|
||||
* order to add junk CTID and TABLEOID attributes.
|
||||
*/
|
||||
qry->mergeTarget_relation = transformMergeJoinClause(pstate, (Node *) joinexpr,
|
||||
&qry->mergeSourceTargetList);
|
||||
|
||||
/*
|
||||
* The target table referenced in the MERGE is looked up twice; once while
|
||||
* setting it up as the result relation and again when it's used in the
|
||||
* underlying the join query. In some rare situations, it may happen that
|
||||
* these lookups return different results, for example, if a new relation
|
||||
* with the same name gets created in a schema which is ahead in the
|
||||
* search_path, in between the two lookups.
|
||||
*
|
||||
* It's a very narrow case, but nevertheless we guard against it by simply
|
||||
* checking if the OIDs returned by the two lookups is the same. If not, we
|
||||
* just throw an error.
|
||||
*/
|
||||
Assert(qry->resultRelation > 0);
|
||||
Assert(qry->mergeTarget_relation > 0);
|
||||
|
||||
/* Fetch both the RTEs */
|
||||
resultRelRTE = rt_fetch(qry->resultRelation, pstate->p_rtable);
|
||||
mergeRelRTE = rt_fetch(qry->mergeTarget_relation, pstate->p_rtable);
|
||||
|
||||
if (resultRelRTE->relid != mergeRelRTE->relid)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
|
||||
errmsg("relation referenced by MERGE statement has changed")));
|
||||
|
||||
/*
|
||||
* 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_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
|
||||
* analyzing 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 separately 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* Expand the source relation to include all attributes of this RTE.
|
||||
*
|
||||
* This function is very similar to expandRelAttrs except that we don't mark
|
||||
* columns for SELECT privileges. That will be decided later when we transform
|
||||
* the action targetlists and the WHEN quals for actual references to the
|
||||
* source relation.
|
||||
*/
|
||||
static List *
|
||||
expandSourceTL(ParseState *pstate, RangeTblEntry *rte, int rtindex)
|
||||
{
|
||||
List *names,
|
||||
*vars;
|
||||
ListCell *name,
|
||||
*var;
|
||||
List *te_list = NIL;
|
||||
|
||||
expandRTE(rte, rtindex, 0, -1, false, &names, &vars);
|
||||
|
||||
/*
|
||||
* Require read access to the table.
|
||||
*/
|
||||
rte->requiredPerms |= ACL_SELECT;
|
||||
|
||||
forboth(name, names, var, vars)
|
||||
{
|
||||
char *label = strVal(lfirst(name));
|
||||
Var *varnode = (Var *) lfirst(var);
|
||||
TargetEntry *te;
|
||||
|
||||
te = makeTargetEntry((Expr *) varnode,
|
||||
(AttrNumber) pstate->p_next_resno++,
|
||||
label,
|
||||
false);
|
||||
te_list = lappend(te_list, te);
|
||||
}
|
||||
|
||||
Assert(name == NULL && var == NULL); /* lists not the same length? */
|
||||
|
||||
return te_list;
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/*-------------------------------------------------------------------------
|
||||
*
|
||||
* 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 */
|
@ -1,19 +0,0 @@
|
||||
/*-------------------------------------------------------------------------
|
||||
*
|
||||
* 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
|
@ -1,97 +0,0 @@
|
||||
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;
|
@ -1,84 +0,0 @@
|
||||
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;
|
@ -1,106 +0,0 @@
|
||||
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;
|
@ -1,213 +0,0 @@
|
||||
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;
|
@ -1,51 +0,0 @@
|
||||
# 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"
|
@ -1,52 +0,0 @@
|
||||
# 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"
|
@ -1,79 +0,0 @@
|
||||
# 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"
|
@ -1,132 +0,0 @@
|
||||
# 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"
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user