]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Add support for INSERT ... ON CONFLICT DO SELECT.
authorDean Rasheed <dean.a.rasheed@gmail.com>
Thu, 12 Feb 2026 09:55:06 +0000 (09:55 +0000)
committerDean Rasheed <dean.a.rasheed@gmail.com>
Thu, 12 Feb 2026 09:57:04 +0000 (09:57 +0000)
This adds a new ON CONFLICT action DO SELECT [FOR UPDATE/SHARE], which
returns the pre-existing rows when conflicts are detected. The INSERT
statement must have a RETURNING clause, when DO SELECT is specified.

The optional FOR UPDATE/SHARE clause allows the rows to be locked
before they are are returned. As with a DO UPDATE conflict action, an
optional WHERE clause may be used to prevent rows from being selected
for return (but as with a DO UPDATE action, rows filtered out by the
WHERE clause are still locked).

Bumps catversion as stored rules change.

Author: Andreas Karlsson <andreas@proxel.se>
Author: Marko Tiikkaja <marko@joh.to>
Author: Viktor Holmberg <v@viktorh.net>
Reviewed-by: Joel Jacobson <joel@compiler.org>
Reviewed-by: Kirill Reshke <reshkekirill@gmail.com>
Reviewed-by: Dean Rasheed <dean.a.rasheed@gmail.com>
Reviewed-by: Jian He <jian.universality@gmail.com>
Discussion: https://postgr.es/m/d631b406-13b7-433e-8c0b-c6040c4b4663@Spark
Discussion: https://postgr.es/m/5fca222d-62ae-4a2f-9fcb-0eca56277094@Spark
Discussion: https://postgr.es/m/2b5db2e6-8ece-44d0-9890-f256fdca9f7e@proxel.se
Discussion: https://postgr.es/m/CAL9smLCdV-v3KgOJX3mU19FYK82N7yzqJj2HAwWX70E=P98kgQ@mail.gmail.com

48 files changed:
contrib/postgres_fdw/postgres_fdw.c
doc/src/sgml/dml.sgml
doc/src/sgml/fdwhandler.sgml
doc/src/sgml/mvcc.sgml
doc/src/sgml/postgres-fdw.sgml
doc/src/sgml/ref/create_policy.sgml
doc/src/sgml/ref/create_view.sgml
doc/src/sgml/ref/insert.sgml
doc/src/sgml/ref/merge.sgml
src/backend/access/heap/heapam.c
src/backend/commands/explain.c
src/backend/executor/execIndexing.c
src/backend/executor/execPartition.c
src/backend/executor/nodeModifyTable.c
src/backend/optimizer/plan/createplan.c
src/backend/optimizer/plan/setrefs.c
src/backend/optimizer/util/plancat.c
src/backend/parser/analyze.c
src/backend/parser/gram.y
src/backend/parser/parse_clause.c
src/backend/rewrite/rewriteHandler.c
src/backend/rewrite/rowsecurity.c
src/backend/utils/adt/ruleutils.c
src/include/catalog/catversion.h
src/include/nodes/execnodes.h
src/include/nodes/lockoptions.h
src/include/nodes/nodes.h
src/include/nodes/parsenodes.h
src/include/nodes/plannodes.h
src/include/nodes/primnodes.h
src/test/isolation/expected/insert-conflict-do-select.out [new file with mode: 0644]
src/test/isolation/isolation_schedule
src/test/isolation/specs/insert-conflict-do-select.spec [new file with mode: 0644]
src/test/regress/expected/constraints.out
src/test/regress/expected/insert_conflict.out
src/test/regress/expected/privileges.out
src/test/regress/expected/rowsecurity.out
src/test/regress/expected/rules.out
src/test/regress/expected/triggers.out
src/test/regress/expected/updatable_views.out
src/test/regress/sql/constraints.sql
src/test/regress/sql/insert_conflict.sql
src/test/regress/sql/privileges.sql
src/test/regress/sql/rowsecurity.sql
src/test/regress/sql/rules.sql
src/test/regress/sql/triggers.sql
src/test/regress/sql/updatable_views.sql
src/tools/pgindent/typedefs.list

index 3572689e33bb6de947c5dd02200435ac76f7c594..60d90329a651d98129e83f4bdab2f2822e9e161f 100644 (file)
@@ -1856,7 +1856,7 @@ postgresPlanForeignModify(PlannerInfo *root,
                returningList = (List *) list_nth(plan->returningLists, subplan_index);
 
        /*
-        * ON CONFLICT DO UPDATE and DO NOTHING case with inference specification
+        * ON CONFLICT DO NOTHING/SELECT/UPDATE with inference specification
         * should have already been rejected in the optimizer, as presently there
         * is no way to recognize an arbiter index on a foreign table.  Only DO
         * NOTHING is supported without an inference specification.
index 61c64cf6c498c664ad144c91cf3fbdd45b9c49b2..cd348d5773a05c7b57fea7c73393429931cffa3d 100644 (file)
@@ -385,7 +385,7 @@ UPDATE products SET price = price * 1.10
    for a <command>DELETE</command>.  However, there are situations where it
    can still be useful for those commands.  For example, in an
    <command>INSERT</command> with an
-   <link linkend="sql-on-conflict"><literal>ON CONFLICT DO UPDATE</literal></link>
+   <link linkend="sql-on-conflict"><literal>ON CONFLICT DO SELECT/UPDATE</literal></link>
    clause, the old values will be non-<literal>NULL</literal> for conflicting
    rows.  Similarly, if a <command>DELETE</command> is turned into an
    <command>UPDATE</command> by a <link linkend="sql-createrule">rewrite rule</link>,
index c6d66414b8ea70c5ef78e26bbbf69e4be90b43ec..9826e09f98360e9393d3f1870370d275db442260 100644 (file)
@@ -2045,7 +2045,7 @@ GetForeignServerByName(const char *name, bool missing_ok);
      <command>INSERT</command> with an <literal>ON CONFLICT</literal> clause does not
      support specifying the conflict target, as unique constraints or
      exclusion constraints on remote tables are not locally known. This
-     in turn implies that <literal>ON CONFLICT DO UPDATE</literal> is not supported,
+     in turn implies that <literal>ON CONFLICT DO SELECT/UPDATE</literal> is not supported,
      since the specification is mandatory there.
     </para>
 
index 049ee75a4ba3bba507f36b980c44d3684bb0864a..e775260936aa3bf17281cee797cc1d37b09c8415 100644 (file)
     conventionally visible to the command.
    </para>
 
+   <para>
+    <command>INSERT</command> with an <literal>ON CONFLICT DO
+    SELECT</literal> clause behaves similarly to <literal>ON CONFLICT DO
+    UPDATE</literal>.  In Read Committed mode, each row proposed for insertion
+    is guaranteed to either insert or return the conflicting row (unless there are
+    unrelated errors).  If a conflict originates in another transaction whose
+    effects are not yet visible to the <command>INSERT</command>, the command
+    will wait for that transaction to commit or roll back, then return the
+    conflicting row if it was committed (even though that row was not visible
+    when the command started).
+   </para>
+
    <para>
     <command>INSERT</command> with an <literal>ON CONFLICT DO
     NOTHING</literal> clause may have insertion not proceed for a row due to
index 9b032fbf675093b0a70ca7faa47e69d43231bf1f..fcf10e4317e80b27afcb0debcbd225e76c5eabe6 100644 (file)
@@ -82,7 +82,7 @@
  <para>
   Note that <filename>postgres_fdw</filename> currently lacks support for
   <command>INSERT</command> statements with an <literal>ON CONFLICT DO
-  UPDATE</literal> clause.  However, the <literal>ON CONFLICT DO NOTHING</literal>
+  SELECT/UPDATE</literal> clause.  However, the <literal>ON CONFLICT DO NOTHING</literal>
   clause is supported, provided a unique index inference specification
   is omitted.
   Note also that <filename>postgres_fdw</filename> supports row movement
index 9065ccb65f9008a8fbd9ff3a60ad832ced3e76c9..d8a036739c0eb34c55dee6176df7e7c8bc5af7bd 100644 (file)
@@ -294,7 +294,7 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
        </para>
        <para>
          If an <literal>INSERT</literal> has an <literal>ON CONFLICT DO
-         UPDATE</literal> clause, or an <literal>ON CONFLICT DO
+         SELECT/UPDATE</literal> clause, or an <literal>ON CONFLICT DO
          NOTHING</literal> clause with an arbiter index or constraint
          specification, then <literal>SELECT</literal>
          permissions are required on the relation, and the rows proposed for
@@ -338,8 +338,8 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
          where records are being added to the relation.
        </para>
        <para>
-         Note that an <literal>INSERT</literal> with an <literal>ON CONFLICT
-         DO NOTHING/UPDATE</literal> clause will check the
+         Note that an <literal>INSERT</literal> with an
+         <literal>ON CONFLICT</literal> clause will check the
          <literal>INSERT</literal> policies' <literal>WITH CHECK</literal>
          expressions for all rows proposed for insertion, regardless of
          whether or not they end up being inserted.
@@ -352,9 +352,10 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
       <listitem>
        <para>
          Using <literal>UPDATE</literal> for a policy means that it will apply
-         to <literal>UPDATE</literal>, <literal>SELECT FOR UPDATE</literal>,
-         and <literal>SELECT FOR SHARE</literal> commands, as well as
-         auxiliary <literal>ON CONFLICT DO UPDATE</literal> clauses of
+         to <literal>UPDATE</literal> and
+         <literal>SELECT FOR UPDATE/SHARE</literal> commands, as well as
+         auxiliary <literal>ON CONFLICT DO UPDATE</literal> and
+         <literal>ON CONFLICT DO SELECT FOR UPDATE/SHARE</literal> clauses of
          <literal>INSERT</literal> commands, and <literal>MERGE</literal>
          commands containing <literal>UPDATE</literal> actions.
          Since an <literal>UPDATE</literal> command
@@ -578,6 +579,22 @@ CREATE POLICY <replaceable class="parameter">name</replaceable> ON <replaceable
        </entry>
        <entry>&mdash;</entry>
       </row>
+      <row>
+       <entry><command>ON CONFLICT DO SELECT</command></entry>
+       <entry>Check existing row</entry>
+       <entry>&mdash;</entry>
+       <entry>&mdash;</entry>
+       <entry>&mdash;</entry>
+       <entry>&mdash;</entry>
+      </row>
+      <row>
+       <entry><command>ON CONFLICT DO SELECT FOR UPDATE/SHARE</command></entry>
+       <entry>Check existing row</entry>
+       <entry>&mdash;</entry>
+       <entry>Check existing row</entry>
+       <entry>&mdash;</entry>
+       <entry>&mdash;</entry>
+      </row>
       <row>
        <entry><command>MERGE</command></entry>
        <entry>Filter source &amp; target rows</entry>
index f8a4740608a184dd596ce69dc159b54424851095..60215eba3b81c55ec451dc18a124c55a133bbed9 100644 (file)
@@ -415,7 +415,7 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
     <command>DELETE</command>, or <command>MERGE</command> statement
     on the view into the corresponding statement on the underlying base
     relation.  <command>INSERT</command> statements that have an <literal>ON
-    CONFLICT DO UPDATE</literal> clause are fully supported.
+    CONFLICT</literal> clause are fully supported.
    </para>
 
    <para>
@@ -430,7 +430,7 @@ CREATE VIEW vista AS SELECT text 'Hello World' AS hello;
     an <command>INSERT</command> or <command>MERGE</command> command can
     potentially insert base-relation rows
     that do not satisfy the <literal>WHERE</literal> condition and thus are not
-    visible through the view (<literal>ON CONFLICT DO UPDATE</literal> may
+    visible through the view (<literal>ON CONFLICT DO SELECT/UPDATE</literal> may
     similarly affect an existing row not visible through the view).
     The <literal>CHECK OPTION</literal> may be used to prevent
     <command>INSERT</command>, <command>UPDATE</command>, and
index 42eec5f4edd436c235475f1d3f32daa6b2c3c398..121a9edcb99daf97db9f56c48517b9ffe54109ec 100644 (file)
@@ -37,6 +37,7 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
 <phrase>and <replaceable class="parameter">conflict_action</replaceable> is one of:</phrase>
 
     DO NOTHING
+    DO SELECT [ FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE } ] [ WHERE <replaceable class="parameter">condition</replaceable> ]
     DO UPDATE SET { <replaceable class="parameter">column_name</replaceable> = { <replaceable class="parameter">expression</replaceable> | DEFAULT } |
                     ( <replaceable class="parameter">column_name</replaceable> [, ...] ) = [ ROW ] ( { <replaceable class="parameter">expression</replaceable> | DEFAULT } [, ...] ) |
                     ( <replaceable class="parameter">column_name</replaceable> [, ...] ) = ( <replaceable class="parameter">sub-SELECT</replaceable> )
@@ -89,24 +90,27 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
   <para>
    The optional <literal>RETURNING</literal> clause causes <command>INSERT</command>
    to compute and return value(s) based on each row actually inserted
-   (or updated, if an <literal>ON CONFLICT DO UPDATE</literal> clause was
-   used).  This is primarily useful for obtaining values that were
+   (or selected or updated, if an <literal>ON CONFLICT DO SELECT/UPDATE</literal>
+   clause was used).  This is primarily useful for obtaining values that were
    supplied by defaults, such as a serial sequence number.  However,
    any expression using the table's columns is allowed.  The syntax of
    the <literal>RETURNING</literal> list is identical to that of the output
    list of <command>SELECT</command>.  Only rows that were successfully
-   inserted or updated will be returned.  For example, if a row was
-   locked but not updated because an <literal>ON CONFLICT DO UPDATE
-   ... WHERE</literal> clause <replaceable
-   class="parameter">condition</replaceable> was not satisfied, the
-   row will not be returned.
+   inserted, updated, or selected will be returned.  For example, if a row was
+   locked but not updated or selected because an <literal>ON CONFLICT ...
+   WHERE</literal> clause <replaceable class="parameter">condition</replaceable>
+   was not satisfied, the row will not be returned.
   </para>
 
   <para>
    You must have <literal>INSERT</literal> privilege on a table in
    order to insert into it.  If <literal>ON CONFLICT DO UPDATE</literal> is
    present, <literal>UPDATE</literal> privilege on the table is also
-   required.
+   required.  If <literal>ON CONFLICT DO SELECT</literal> is present,
+   <literal>SELECT</literal> privilege on the table is required.
+   If <literal>ON CONFLICT DO SELECT FOR UPDATE/SHARE</literal> is used,
+   <literal>UPDATE</literal> privilege is required on at least one
+   column, in addition to <literal>SELECT</literal> privilege.
   </para>
 
   <para>
@@ -343,8 +347,11 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
        <para>
         For a simple <command>INSERT</command>, all old values will be
         <literal>NULL</literal>.  However, for an <command>INSERT</command>
-        with an <literal>ON CONFLICT DO UPDATE</literal> clause, the old
-        values may be non-<literal>NULL</literal>.
+        with an <literal>ON CONFLICT DO SELECT/UPDATE</literal> clause, the
+        old values may be non-<literal>NULL</literal> (when the row proposed
+        for insertion conflicts with an existing row).  If the
+        <literal>SELECT</literal> path is taken, the new values will be
+        identical to the old values, since no modification takes place.
        </para>
       </listitem>
      </varlistentry>
@@ -380,6 +387,9 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
     a row as its alternative action.  <literal>ON CONFLICT DO
     UPDATE</literal> updates the existing row that conflicts with the
     row proposed for insertion as its alternative action.
+    <literal>ON CONFLICT DO SELECT</literal> returns the existing row
+    that conflicts with the row proposed for insertion, optionally
+    with row-level locking.
    </para>
 
    <para>
@@ -411,6 +421,15 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
     INSERT</quote>.
    </para>
 
+   <para>
+    <literal>ON CONFLICT DO SELECT</literal> similarly allows an atomic
+    <command>INSERT</command> or <command>SELECT</command> outcome. This
+    is also known as <firstterm>idempotent insert</firstterm> or
+    <firstterm>get or create</firstterm>.  For <literal>ON CONFLICT DO
+    SELECT</literal>, a <literal>RETURNING</literal> clause
+    <emphasis>must</emphasis> be provided.
+   </para>
+
     <variablelist>
      <varlistentry>
       <term><replaceable class="parameter">conflict_target</replaceable></term>
@@ -424,7 +443,8 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
         specify a <parameter>conflict_target</parameter>; when
         omitted, conflicts with all usable constraints (and unique
         indexes) are handled.  For <literal>ON CONFLICT DO
-        UPDATE</literal>, a <parameter>conflict_target</parameter>
+        UPDATE</literal> and <literal>ON CONFLICT DO SELECT</literal>,
+        a <parameter>conflict_target</parameter>
         <emphasis>must</emphasis> be provided.
        </para>
       </listitem>
@@ -434,19 +454,23 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
       <term><replaceable class="parameter">conflict_action</replaceable></term>
       <listitem>
        <para>
-        <parameter>conflict_action</parameter> specifies an
-        alternative <literal>ON CONFLICT</literal> action.  It can be
-        either <literal>DO NOTHING</literal>, or a <literal>DO
-        UPDATE</literal> clause specifying the exact details of the
-        <literal>UPDATE</literal> action to be performed in case of a
-        conflict.  The <literal>SET</literal> and
-        <literal>WHERE</literal> clauses in <literal>ON CONFLICT DO
-        UPDATE</literal> have access to the existing row using the
-        table's name (or an alias), and to the row proposed for insertion
-        using the special <varname>excluded</varname> table.
-        <literal>SELECT</literal> privilege is required on any column in the
-        target table where corresponding <varname>excluded</varname>
-        columns are read.
+        <parameter>conflict_action</parameter> specifies an alternative
+        <literal>ON CONFLICT</literal> action.  It can be
+        <literal>DO NOTHING</literal>, a <literal>DO SELECT</literal>
+        clause that allows conflicting rows to be returned, or a
+        <literal>DO UPDATE</literal> clause specifying the exact details
+        of the <literal>UPDATE</literal> action to be performed in case
+        of a conflict.
+       </para>
+       <para>
+        The <literal>SET</literal> clause in <literal>DO UPDATE</literal>
+        and the <literal>WHERE</literal> clause in both
+        <literal>DO SELECT</literal> and <literal>DO UPDATE</literal> have
+        access to the existing row using the table's name (or an alias),
+        and to the row proposed for insertion using the special
+        <varname>excluded</varname> table.  <literal>SELECT</literal>
+        privilege is required on any column in the target table where
+        corresponding <varname>excluded</varname> columns are read.
        </para>
        <para>
         Note that the effects of all per-row <literal>BEFORE
@@ -545,24 +569,41 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><literal>FOR UPDATE</literal></term>
+      <term><literal>FOR NO KEY UPDATE</literal></term>
+      <term><literal>FOR SHARE</literal></term>
+      <term><literal>FOR KEY SHARE</literal></term>
+      <listitem>
+       <para>
+        When specified in an <literal>ON CONFLICT DO SELECT</literal> clause,
+        conflicting table rows are locked against concurrent updates.
+        See <xref linkend="sql-for-update-share"/> in the
+        <xref linkend="sql-select"/> documentation.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><replaceable class="parameter">condition</replaceable></term>
       <listitem>
        <para>
         An expression that returns a value of type
         <type>boolean</type>.  Only rows for which this expression
-        returns <literal>true</literal> will be updated, although all
-        rows will be locked when the <literal>ON CONFLICT DO UPDATE</literal>
-        action is taken.  Note that
-        <replaceable>condition</replaceable> is evaluated last, after
-        a conflict has been identified as a candidate to update.
+        returns <literal>true</literal> will be updated or selected for
+        return, although all conflicting rows will be locked when
+        <literal>ON CONFLICT DO UPDATE</literal> or
+        <literal>ON CONFLICT DO SELECT FOR UPDATE/SHARE</literal> is
+        specified.  Note that <replaceable>condition</replaceable> is
+        evaluated last, after a conflict has been identified as a candidate
+        to update or select.
        </para>
       </listitem>
      </varlistentry>
     </variablelist>
    <para>
     Note that exclusion constraints are not supported as arbiters with
-    <literal>ON CONFLICT DO UPDATE</literal>. In all cases, only
+    <literal>ON CONFLICT DO SELECT/UPDATE</literal>. In all cases, only
     <literal>NOT DEFERRABLE</literal> constraints and unique indexes
     are supported as arbiters.
    </para>
@@ -610,7 +651,7 @@ INSERT INTO <replaceable class="parameter">table_name</replaceable> [ AS <replac
 INSERT <replaceable>oid</replaceable> <replaceable class="parameter">count</replaceable>
 </screen>
    The <replaceable class="parameter">count</replaceable> is the number of
-   rows inserted or updated.  <replaceable>oid</replaceable> is always 0 (it
+   rows inserted, updated, or selected for return.  <replaceable>oid</replaceable> is always 0 (it
    used to be the <acronym>OID</acronym> assigned to the inserted row if
    <replaceable>count</replaceable> was exactly one and the target table was
    declared <literal>WITH OIDS</literal> and 0 otherwise, but creating a table
@@ -621,8 +662,7 @@ INSERT <replaceable>oid</replaceable> <replaceable class="parameter">count</repl
    If the <command>INSERT</command> command contains a <literal>RETURNING</literal>
    clause, the result will be similar to that of a <command>SELECT</command>
    statement containing the columns and values defined in the
-   <literal>RETURNING</literal> list, computed over the row(s) inserted or
-   updated by the command.
+   <literal>RETURNING</literal> list, computed over the row(s) affected by the command.
   </para>
  </refsect1>
 
@@ -796,6 +836,35 @@ INSERT INTO distributors AS d (did, dname) VALUES (8, 'Anvil Distribution')
 -- index to arbitrate taking the DO NOTHING action)
 INSERT INTO distributors (did, dname) VALUES (9, 'Antwerp Design')
     ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING;
+</programlisting>
+  </para>
+  <para>
+   Insert new distributor if possible, otherwise return the existing
+   distributor row.  Example assumes a unique index has been defined
+   that constrains values appearing in the <literal>did</literal> column.
+   This is useful for get-or-create patterns:
+<programlisting>
+INSERT INTO distributors (did, dname) VALUES (11, 'Global Electronics')
+    ON CONFLICT (did) DO SELECT
+    RETURNING *;
+</programlisting>
+  </para>
+  <para>
+   Insert a new distributor if the ID doesn't match, otherwise return
+   the existing row, if its name doesn't match:
+<programlisting>
+INSERT INTO distributors AS d (did, dname) VALUES (12, 'Micro Devices Inc')
+    ON CONFLICT (did) DO SELECT WHERE d.dname != EXCLUDED.dname
+    RETURNING *;
+</programlisting>
+  </para>
+  <para>
+   Insert a new distributor or return and lock the existing row for update.
+   This is useful when you need to ensure exclusive access to the row:
+<programlisting>
+INSERT INTO distributors (did, dname) VALUES (13, 'Advanced Systems')
+    ON CONFLICT (did) DO SELECT FOR UPDATE
+    RETURNING *;
 </programlisting>
   </para>
   <para>
index c2e181066a4e19c354328c739e22d79568f376f4..765fe7a7d625f779b6f3fa170405808477f04352 100644 (file)
@@ -714,7 +714,8 @@ MERGE <replaceable class="parameter">total_count</replaceable>
    on the behavior at each isolation level.
    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>
+   <command>UPDATE</command> or return the existing row (with
+   <literal>DO SELECT</literal>) 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>
index ff8500993047d0c230e2e36a7d7dfa0b28a32b7a..98d53caeea8226038a99b2c94218e3a0913e82a3 100644 (file)
@@ -4689,10 +4689,10 @@ l3:
        if (result == TM_Invisible)
        {
                /*
-                * This is possible, but only when locking a tuple for ON CONFLICT
-                * UPDATE.  We return this value here rather than throwing an error in
-                * order to give that case the opportunity to throw a more specific
-                * error.
+                * This is possible, but only when locking a tuple for ON CONFLICT DO
+                * SELECT/UPDATE.  We return this value here rather than throwing an
+                * error in order to give that case the opportunity to throw a more
+                * specific error.
                 */
                result = TM_Invisible;
                goto out_locked;
index b7bb111688cec06aafa4ed27b466d493a44faedd..b9587983f8800930cabe4ea561ee1a32a1e1fa94 100644 (file)
@@ -4672,10 +4672,36 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 
        if (node->onConflictAction != ONCONFLICT_NONE)
        {
-               ExplainPropertyText("Conflict Resolution",
-                                                       node->onConflictAction == ONCONFLICT_NOTHING ?
-                                                       "NOTHING" : "UPDATE",
-                                                       es);
+               const char *resolution = NULL;
+
+               if (node->onConflictAction == ONCONFLICT_NOTHING)
+                       resolution = "NOTHING";
+               else if (node->onConflictAction == ONCONFLICT_UPDATE)
+                       resolution = "UPDATE";
+               else
+               {
+                       Assert(node->onConflictAction == ONCONFLICT_SELECT);
+                       switch (node->onConflictLockStrength)
+                       {
+                               case LCS_NONE:
+                                       resolution = "SELECT";
+                                       break;
+                               case LCS_FORKEYSHARE:
+                                       resolution = "SELECT FOR KEY SHARE";
+                                       break;
+                               case LCS_FORSHARE:
+                                       resolution = "SELECT FOR SHARE";
+                                       break;
+                               case LCS_FORNOKEYUPDATE:
+                                       resolution = "SELECT FOR NO KEY UPDATE";
+                                       break;
+                               case LCS_FORUPDATE:
+                                       resolution = "SELECT FOR UPDATE";
+                                       break;
+                       }
+               }
+
+               ExplainPropertyText("Conflict Resolution", resolution, es);
 
                /*
                 * Don't display arbiter indexes at all when DO NOTHING variant
@@ -4684,7 +4710,7 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
                if (idxNames)
                        ExplainPropertyList("Conflict Arbiter Indexes", idxNames, es);
 
-               /* ON CONFLICT DO UPDATE WHERE qual is specially displayed */
+               /* ON CONFLICT DO SELECT/UPDATE WHERE qual is specially displayed */
                if (node->onConflictWhere)
                {
                        show_upper_qual((List *) node->onConflictWhere, "Conflict Filter",
index 6ae0f9595922c5c8cabf226241353c0f62563344..f0ba7eac87dd4d2f8a1adc23a975b1496d8f4cb8 100644 (file)
@@ -54,9 +54,9 @@
  * ---------------------
  *
  * Speculative insertion is a two-phase mechanism used to implement
- * INSERT ... ON CONFLICT DO UPDATE/NOTHING.  The tuple is first inserted
- * to the heap and update the indexes as usual, but if a constraint is
- * violated, we can still back out the insertion without aborting the whole
+ * INSERT ... ON CONFLICT.  The tuple is first inserted into the heap
+ * and the indexes are updated as usual, but if a constraint is violated,
+ * we can still back out of the insertion without aborting the whole
  * transaction.  In an INSERT ... ON CONFLICT statement, if a conflict is
  * detected, the inserted tuple is backed out and the ON CONFLICT action is
  * executed instead.
index d13e786cf134388a6a965977a0974b747d819253..bab294f5e91ad1280ce47cfe02349e47b0c0f8f5 100644 (file)
@@ -883,20 +883,27 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
                leaf_part_rri->ri_onConflictArbiterIndexes = arbiterIndexes;
 
                /*
-                * In the DO UPDATE case, we have some more state to initialize.
+                * In the DO UPDATE and DO SELECT cases, we have some more state to
+                * initialize.
                 */
-               if (node->onConflictAction == ONCONFLICT_UPDATE)
+               if (node->onConflictAction == ONCONFLICT_UPDATE ||
+                       node->onConflictAction == ONCONFLICT_SELECT)
                {
-                       OnConflictSetState *onconfl = makeNode(OnConflictSetState);
+                       OnConflictActionState *onconfl = makeNode(OnConflictActionState);
                        TupleConversionMap *map;
 
                        map = ExecGetRootToChildMap(leaf_part_rri, estate);
 
-                       Assert(node->onConflictSet != NIL);
+                       Assert(node->onConflictSet != NIL ||
+                                  node->onConflictAction == ONCONFLICT_SELECT);
                        Assert(rootResultRelInfo->ri_onConflict != NULL);
 
                        leaf_part_rri->ri_onConflict = onconfl;
 
+                       /* Lock strength for DO SELECT [FOR UPDATE/SHARE] */
+                       onconfl->oc_LockStrength =
+                               rootResultRelInfo->ri_onConflict->oc_LockStrength;
+
                        /*
                         * Need a separate existing slot for each partition, as the
                         * partition could be of a different AM, even if the tuple
@@ -909,7 +916,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
                        /*
                         * If the partition's tuple descriptor matches exactly the root
                         * parent (the common case), we can re-use most of the parent's ON
-                        * CONFLICT SET state, skipping a bunch of work.  Otherwise, we
+                        * CONFLICT action state, skipping a bunch of work.  Otherwise, we
                         * need to create state specific to this partition.
                         */
                        if (map == NULL)
@@ -917,7 +924,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
                                /*
                                 * It's safe to reuse these from the partition root, as we
                                 * only process one tuple at a time (therefore we won't
-                                * overwrite needed data in slots), and the results of
+                                * overwrite needed data in slots), and the results of any
                                 * projections are independent of the underlying storage.
                                 * Projections and where clauses themselves don't store state
                                 * / are independent of the underlying storage.
@@ -931,66 +938,81 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
                        }
                        else
                        {
-                               List       *onconflset;
-                               List       *onconflcols;
-
                                /*
-                                * Translate expressions in onConflictSet to account for
-                                * different attribute numbers.  For that, map partition
-                                * varattnos twice: first to catch the EXCLUDED
-                                * pseudo-relation (INNER_VAR), and second to handle the main
-                                * target relation (firstVarno).
+                                * For ON CONFLICT DO UPDATE, translate expressions in
+                                * onConflictSet to account for different attribute numbers.
+                                * For that, map partition varattnos twice: first to catch the
+                                * EXCLUDED pseudo-relation (INNER_VAR), and second to handle
+                                * the main target relation (firstVarno).
                                 */
-                               onconflset = copyObject(node->onConflictSet);
-                               if (part_attmap == NULL)
-                                       part_attmap =
-                                               build_attrmap_by_name(RelationGetDescr(partrel),
-                                                                                         RelationGetDescr(firstResultRel),
-                                                                                         false);
-                               onconflset = (List *)
-                                       map_variable_attnos((Node *) onconflset,
-                                                                               INNER_VAR, 0,
-                                                                               part_attmap,
-                                                                               RelationGetForm(partrel)->reltype,
-                                                                               &found_whole_row);
-                               /* We ignore the value of found_whole_row. */
-                               onconflset = (List *)
-                                       map_variable_attnos((Node *) onconflset,
-                                                                               firstVarno, 0,
-                                                                               part_attmap,
-                                                                               RelationGetForm(partrel)->reltype,
-                                                                               &found_whole_row);
-                               /* We ignore the value of found_whole_row. */
-
-                               /* Finally, adjust the target colnos to match the partition. */
-                               onconflcols = adjust_partition_colnos(node->onConflictCols,
-                                                                                                         leaf_part_rri);
-
-                               /* create the tuple slot for the UPDATE SET projection */
-                               onconfl->oc_ProjSlot =
-                                       table_slot_create(partrel,
-                                                                         &mtstate->ps.state->es_tupleTable);
+                               if (node->onConflictAction == ONCONFLICT_UPDATE)
+                               {
+                                       List       *onconflset;
+                                       List       *onconflcols;
+
+                                       onconflset = copyObject(node->onConflictSet);
+                                       if (part_attmap == NULL)
+                                               part_attmap =
+                                                       build_attrmap_by_name(RelationGetDescr(partrel),
+                                                                                                 RelationGetDescr(firstResultRel),
+                                                                                                 false);
+                                       onconflset = (List *)
+                                               map_variable_attnos((Node *) onconflset,
+                                                                                       INNER_VAR, 0,
+                                                                                       part_attmap,
+                                                                                       RelationGetForm(partrel)->reltype,
+                                                                                       &found_whole_row);
+                                       /* We ignore the value of found_whole_row. */
+                                       onconflset = (List *)
+                                               map_variable_attnos((Node *) onconflset,
+                                                                                       firstVarno, 0,
+                                                                                       part_attmap,
+                                                                                       RelationGetForm(partrel)->reltype,
+                                                                                       &found_whole_row);
+                                       /* We ignore the value of found_whole_row. */
 
-                               /* build UPDATE SET projection state */
-                               onconfl->oc_ProjInfo =
-                                       ExecBuildUpdateProjection(onconflset,
-                                                                                         true,
-                                                                                         onconflcols,
-                                                                                         partrelDesc,
-                                                                                         econtext,
-                                                                                         onconfl->oc_ProjSlot,
-                                                                                         &mtstate->ps);
+                                       /*
+                                        * Finally, adjust the target colnos to match the
+                                        * partition.
+                                        */
+                                       onconflcols = adjust_partition_colnos(node->onConflictCols,
+                                                                                                                 leaf_part_rri);
+
+                                       /* create the tuple slot for the UPDATE SET projection */
+                                       onconfl->oc_ProjSlot =
+                                               table_slot_create(partrel,
+                                                                                 &mtstate->ps.state->es_tupleTable);
+
+                                       /* build UPDATE SET projection state */
+                                       onconfl->oc_ProjInfo =
+                                               ExecBuildUpdateProjection(onconflset,
+                                                                                                 true,
+                                                                                                 onconflcols,
+                                                                                                 partrelDesc,
+                                                                                                 econtext,
+                                                                                                 onconfl->oc_ProjSlot,
+                                                                                                 &mtstate->ps);
+                               }
 
                                /*
-                                * If there is a WHERE clause, initialize state where it will
-                                * be evaluated, mapping the attribute numbers appropriately.
-                                * As with onConflictSet, we need to map partition varattnos
-                                * to the partition's tupdesc.
+                                * For both ON CONFLICT DO UPDATE and ON CONFLICT DO SELECT,
+                                * there may be a WHERE clause.  If so, initialize state where
+                                * it will be evaluated, mapping the attribute numbers
+                                * appropriately.  As with onConflictSet, we need to map
+                                * partition varattnos twice, to catch both the EXCLUDED
+                                * pseudo-relation (INNER_VAR), and the main target relation
+                                * (firstVarno).
                                 */
                                if (node->onConflictWhere)
                                {
                                        List       *clause;
 
+                                       if (part_attmap == NULL)
+                                               part_attmap =
+                                                       build_attrmap_by_name(RelationGetDescr(partrel),
+                                                                                                 RelationGetDescr(firstResultRel),
+                                                                                                 false);
+
                                        clause = copyObject((List *) node->onConflictWhere);
                                        clause = (List *)
                                                map_variable_attnos((Node *) clause,
index f5e9d369940f3434c913003c9bd34244ec1cc350..6802fc13e9583acff9e93514aec31d1cd94bccfb 100644 (file)
@@ -147,12 +147,24 @@ static void ExecCrossPartitionUpdateForeignKey(ModifyTableContext *context,
                                                                                           ItemPointer tupleid,
                                                                                           TupleTableSlot *oldslot,
                                                                                           TupleTableSlot *newslot);
+static bool ExecOnConflictLockRow(ModifyTableContext *context,
+                                                                 TupleTableSlot *existing,
+                                                                 ItemPointer conflictTid,
+                                                                 Relation relation,
+                                                                 LockTupleMode lockmode,
+                                                                 bool isUpdate);
 static bool ExecOnConflictUpdate(ModifyTableContext *context,
                                                                 ResultRelInfo *resultRelInfo,
                                                                 ItemPointer conflictTid,
                                                                 TupleTableSlot *excludedSlot,
                                                                 bool canSetTag,
                                                                 TupleTableSlot **returning);
+static bool ExecOnConflictSelect(ModifyTableContext *context,
+                                                                ResultRelInfo *resultRelInfo,
+                                                                ItemPointer conflictTid,
+                                                                TupleTableSlot *excludedSlot,
+                                                                bool canSetTag,
+                                                                TupleTableSlot **returning);
 static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate,
                                                                                           EState *estate,
                                                                                           PartitionTupleRouting *proute,
@@ -274,7 +286,7 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList)
  *
  * context: context for the ModifyTable operation
  * resultRelInfo: current result rel
- * cmdType: operation/merge action performed (INSERT, UPDATE, or DELETE)
+ * isDelete: true if the operation/merge action is a DELETE
  * oldSlot: slot holding old tuple deleted or updated
  * newSlot: slot holding new tuple inserted or updated
  * planSlot: slot holding tuple returned by top subplan node
@@ -283,12 +295,15 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList)
  * econtext's scan tuple and its old & new tuples are not needed (FDW direct-
  * modify is disabled if the RETURNING list refers to any OLD/NEW values).
  *
+ * Note: For the SELECT path of INSERT ... ON CONFLICT DO SELECT, oldSlot and
+ * newSlot are both the existing tuple, since it's not changed.
+ *
  * Returns a slot holding the result tuple
  */
 static TupleTableSlot *
 ExecProcessReturning(ModifyTableContext *context,
                                         ResultRelInfo *resultRelInfo,
-                                        CmdType cmdType,
+                                        bool isDelete,
                                         TupleTableSlot *oldSlot,
                                         TupleTableSlot *newSlot,
                                         TupleTableSlot *planSlot)
@@ -298,23 +313,17 @@ ExecProcessReturning(ModifyTableContext *context,
        ExprContext *econtext = projectReturning->pi_exprContext;
 
        /* Make tuple and any needed join variables available to ExecProject */
-       switch (cmdType)
+       if (isDelete)
        {
-               case CMD_INSERT:
-               case CMD_UPDATE:
-                       /* return new tuple by default */
-                       if (newSlot)
-                               econtext->ecxt_scantuple = newSlot;
-                       break;
-
-               case CMD_DELETE:
-                       /* return old tuple by default */
-                       if (oldSlot)
-                               econtext->ecxt_scantuple = oldSlot;
-                       break;
-
-               default:
-                       elog(ERROR, "unrecognized commandType: %d", (int) cmdType);
+               /* return old tuple by default */
+               if (oldSlot)
+                       econtext->ecxt_scantuple = oldSlot;
+       }
+       else
+       {
+               /* return new tuple by default */
+               if (newSlot)
+                       econtext->ecxt_scantuple = newSlot;
        }
        econtext->ecxt_outertuple = planSlot;
 
@@ -1158,6 +1167,26 @@ ExecInsert(ModifyTableContext *context,
                                        else
                                                goto vlock;
                                }
+                               else if (onconflict == ONCONFLICT_SELECT)
+                               {
+                                       /*
+                                        * In case of ON CONFLICT DO SELECT, optionally lock the
+                                        * conflicting tuple, fetch it and project RETURNING on
+                                        * it. Be prepared to retry if locking fails because of a
+                                        * concurrent UPDATE/DELETE to the conflict tuple.
+                                        */
+                                       TupleTableSlot *returning = NULL;
+
+                                       if (ExecOnConflictSelect(context, resultRelInfo,
+                                                                                        &conflictTid, slot, canSetTag,
+                                                                                        &returning))
+                                       {
+                                               InstrCountTuples2(&mtstate->ps, 1);
+                                               return returning;
+                                       }
+                                       else
+                                               goto vlock;
+                               }
                                else
                                {
                                        /*
@@ -1329,7 +1358,7 @@ ExecInsert(ModifyTableContext *context,
                        }
                }
 
-               result = ExecProcessReturning(context, resultRelInfo, CMD_INSERT,
+               result = ExecProcessReturning(context, resultRelInfo, false,
                                                                          oldSlot, slot, planSlot);
 
                /*
@@ -1890,7 +1919,7 @@ ldelete:
                        return NULL;
                }
 
-               rslot = ExecProcessReturning(context, resultRelInfo, CMD_DELETE,
+               rslot = ExecProcessReturning(context, resultRelInfo, true,
                                                                         slot, NULL, context->planSlot);
 
                /*
@@ -2692,56 +2721,37 @@ redo_act:
 
        /* Process RETURNING if present */
        if (resultRelInfo->ri_projectReturning)
-               return ExecProcessReturning(context, resultRelInfo, CMD_UPDATE,
+               return ExecProcessReturning(context, resultRelInfo, false,
                                                                        oldSlot, slot, context->planSlot);
 
        return NULL;
 }
 
 /*
- * ExecOnConflictUpdate --- execute UPDATE of INSERT ON CONFLICT DO UPDATE
+ * ExecOnConflictLockRow --- lock the row for ON CONFLICT DO SELECT/UPDATE
  *
- * Try to lock tuple for update as part of speculative insertion.  If
- * a qual originating from ON CONFLICT DO UPDATE is satisfied, update
- * (but still lock row, even though it may not satisfy estate's
- * snapshot).
+ * Try to lock tuple for update as part of speculative insertion for ON
+ * CONFLICT DO UPDATE or ON CONFLICT DO SELECT FOR UPDATE/SHARE.
  *
- * Returns true if we're done (with or without an update), or false if
- * the caller must retry the INSERT from scratch.
+ * Returns true if the row is successfully locked, or false if the caller must
+ * retry the INSERT from scratch.
  */
 static bool
-ExecOnConflictUpdate(ModifyTableContext *context,
-                                        ResultRelInfo *resultRelInfo,
-                                        ItemPointer conflictTid,
-                                        TupleTableSlot *excludedSlot,
-                                        bool canSetTag,
-                                        TupleTableSlot **returning)
+ExecOnConflictLockRow(ModifyTableContext *context,
+                                         TupleTableSlot *existing,
+                                         ItemPointer conflictTid,
+                                         Relation relation,
+                                         LockTupleMode lockmode,
+                                         bool isUpdate)
 {
-       ModifyTableState *mtstate = context->mtstate;
-       ExprContext *econtext = mtstate->ps.ps_ExprContext;
-       Relation        relation = resultRelInfo->ri_RelationDesc;
-       ExprState  *onConflictSetWhere = resultRelInfo->ri_onConflict->oc_WhereClause;
-       TupleTableSlot *existing = resultRelInfo->ri_onConflict->oc_Existing;
        TM_FailureData tmfd;
-       LockTupleMode lockmode;
        TM_Result       test;
        Datum           xminDatum;
        TransactionId xmin;
        bool            isnull;
 
        /*
-        * Parse analysis should have blocked ON CONFLICT for all system
-        * relations, which includes these.  There's no fundamental obstacle to
-        * supporting this; we'd just need to handle LOCKTAG_TUPLE like the other
-        * ExecUpdate() caller.
-        */
-       Assert(!resultRelInfo->ri_needLockTagTuple);
-
-       /* Determine lock mode to use */
-       lockmode = ExecUpdateLockMode(context->estate, resultRelInfo);
-
-       /*
-        * Lock tuple for update.  Don't follow updates when tuple cannot be
+        * Lock tuple with lockmode.  Don't follow updates when tuple cannot be
         * locked without doing so.  A row locking conflict here means our
         * previous conclusion that the tuple is conclusively committed is not
         * true anymore.
@@ -2786,7 +2796,7 @@ ExecOnConflictUpdate(ModifyTableContext *context,
                                                (errcode(ERRCODE_CARDINALITY_VIOLATION),
                                /* translator: %s is a SQL command name */
                                                 errmsg("%s command cannot affect row a second time",
-                                                               "ON CONFLICT DO UPDATE"),
+                                                               isUpdate ? "ON CONFLICT DO UPDATE" : "ON CONFLICT DO SELECT"),
                                                 errhint("Ensure that no rows proposed for insertion within the same command have duplicate constrained values.")));
 
                        /* This shouldn't happen */
@@ -2834,6 +2844,50 @@ ExecOnConflictUpdate(ModifyTableContext *context,
        }
 
        /* Success, the tuple is locked. */
+       return true;
+}
+
+/*
+ * ExecOnConflictUpdate --- execute UPDATE of INSERT ON CONFLICT DO UPDATE
+ *
+ * Try to lock tuple for update as part of speculative insertion.  If
+ * a qual originating from ON CONFLICT DO UPDATE is satisfied, update
+ * (but still lock row, even though it may not satisfy estate's
+ * snapshot).
+ *
+ * Returns true if we're done (with or without an update), or false if
+ * the caller must retry the INSERT from scratch.
+ */
+static bool
+ExecOnConflictUpdate(ModifyTableContext *context,
+                                        ResultRelInfo *resultRelInfo,
+                                        ItemPointer conflictTid,
+                                        TupleTableSlot *excludedSlot,
+                                        bool canSetTag,
+                                        TupleTableSlot **returning)
+{
+       ModifyTableState *mtstate = context->mtstate;
+       ExprContext *econtext = mtstate->ps.ps_ExprContext;
+       Relation        relation = resultRelInfo->ri_RelationDesc;
+       ExprState  *onConflictSetWhere = resultRelInfo->ri_onConflict->oc_WhereClause;
+       TupleTableSlot *existing = resultRelInfo->ri_onConflict->oc_Existing;
+       LockTupleMode lockmode;
+
+       /*
+        * Parse analysis should have blocked ON CONFLICT for all system
+        * relations, which includes these.  There's no fundamental obstacle to
+        * supporting this; we'd just need to handle LOCKTAG_TUPLE like the other
+        * ExecUpdate() caller.
+        */
+       Assert(!resultRelInfo->ri_needLockTagTuple);
+
+       /* Determine lock mode to use */
+       lockmode = ExecUpdateLockMode(context->estate, resultRelInfo);
+
+       /* Lock tuple for update */
+       if (!ExecOnConflictLockRow(context, existing, conflictTid,
+                                                          resultRelInfo->ri_RelationDesc, lockmode, true))
+               return false;
 
        /*
         * Verify that the tuple is visible to our MVCC snapshot if the current
@@ -2875,11 +2929,13 @@ ExecOnConflictUpdate(ModifyTableContext *context,
                 * security barrier quals (if any), enforced here as RLS checks/WCOs.
                 *
                 * The rewriter creates UPDATE RLS checks/WCOs for UPDATE security
-                * quals, and stores them as WCOs of "kind" WCO_RLS_CONFLICT_CHECK,
-                * but that's almost the extent of its special handling for ON
-                * CONFLICT DO UPDATE.
+                * quals, and stores them as WCOs of "kind" WCO_RLS_CONFLICT_CHECK.
+                * Since SELECT permission on the target table is always required for
+                * INSERT ... ON CONFLICT DO UPDATE, the rewriter also adds SELECT RLS
+                * checks/WCOs for SELECT security quals, using WCOs of the same kind,
+                * and this check enforces them too.
                 *
-                * The rewriter will also have associated UPDATE applicable straight
+                * The rewriter will also have associated UPDATE-applicable straight
                 * RLS checks/WCOs for the benefit of the ExecUpdate() call that
                 * follows.  INSERTs and UPDATEs naturally have mutually exclusive WCO
                 * kinds, so there is no danger of spurious over-enforcement in the
@@ -2924,6 +2980,141 @@ ExecOnConflictUpdate(ModifyTableContext *context,
        return true;
 }
 
+/*
+ * ExecOnConflictSelect --- execute SELECT of INSERT ON CONFLICT DO SELECT
+ *
+ * If SELECT FOR UPDATE/SHARE is specified, try to lock tuple as part of
+ * speculative insertion.  If a qual originating from ON CONFLICT DO SELECT is
+ * satisfied, select (but still lock row, even though it may not satisfy
+ * estate's snapshot).
+ *
+ * Returns true if we're done (with or without a select), or false if the
+ * caller must retry the INSERT from scratch.
+ */
+static bool
+ExecOnConflictSelect(ModifyTableContext *context,
+                                        ResultRelInfo *resultRelInfo,
+                                        ItemPointer conflictTid,
+                                        TupleTableSlot *excludedSlot,
+                                        bool canSetTag,
+                                        TupleTableSlot **returning)
+{
+       ModifyTableState *mtstate = context->mtstate;
+       ExprContext *econtext = mtstate->ps.ps_ExprContext;
+       Relation        relation = resultRelInfo->ri_RelationDesc;
+       ExprState  *onConflictSelectWhere = resultRelInfo->ri_onConflict->oc_WhereClause;
+       TupleTableSlot *existing = resultRelInfo->ri_onConflict->oc_Existing;
+       LockClauseStrength lockStrength = resultRelInfo->ri_onConflict->oc_LockStrength;
+
+       /*
+        * Parse analysis should have blocked ON CONFLICT for all system
+        * relations, which includes these.  There's no fundamental obstacle to
+        * supporting this; we'd just need to handle LOCKTAG_TUPLE appropriately.
+        */
+       Assert(!resultRelInfo->ri_needLockTagTuple);
+
+       /* Fetch/lock existing tuple, according to the requested lock strength */
+       if (lockStrength == LCS_NONE)
+       {
+               if (!table_tuple_fetch_row_version(relation,
+                                                                                  conflictTid,
+                                                                                  SnapshotAny,
+                                                                                  existing))
+                       elog(ERROR, "failed to fetch conflicting tuple for ON CONFLICT");
+       }
+       else
+       {
+               LockTupleMode lockmode;
+
+               switch (lockStrength)
+               {
+                       case LCS_FORKEYSHARE:
+                               lockmode = LockTupleKeyShare;
+                               break;
+                       case LCS_FORSHARE:
+                               lockmode = LockTupleShare;
+                               break;
+                       case LCS_FORNOKEYUPDATE:
+                               lockmode = LockTupleNoKeyExclusive;
+                               break;
+                       case LCS_FORUPDATE:
+                               lockmode = LockTupleExclusive;
+                               break;
+                       default:
+                               elog(ERROR, "Unexpected lock strength %d", (int) lockStrength);
+               }
+
+               if (!ExecOnConflictLockRow(context, existing, conflictTid,
+                                                                  resultRelInfo->ri_RelationDesc, lockmode, false))
+                       return false;
+       }
+
+       /*
+        * Verify that the tuple is visible to our MVCC snapshot if the current
+        * isolation level mandates that.  See comments in ExecOnConflictUpdate().
+        */
+       ExecCheckTupleVisible(context->estate, relation, existing);
+
+       /*
+        * Make tuple and any needed join variables available to ExecQual.  The
+        * EXCLUDED tuple is installed in ecxt_innertuple, while the target's
+        * existing tuple is installed in the scantuple.  EXCLUDED has been made
+        * to reference INNER_VAR in setrefs.c, but there is no other redirection.
+        */
+       econtext->ecxt_scantuple = existing;
+       econtext->ecxt_innertuple = excludedSlot;
+       econtext->ecxt_outertuple = NULL;
+
+       if (!ExecQual(onConflictSelectWhere, econtext))
+       {
+               ExecClearTuple(existing);       /* see return below */
+               InstrCountFiltered1(&mtstate->ps, 1);
+               return true;                    /* done with the tuple */
+       }
+
+       if (resultRelInfo->ri_WithCheckOptions != NIL)
+       {
+               /*
+                * Check target's existing tuple against SELECT-applicable USING
+                * security barrier quals (if any), enforced here as RLS checks/WCOs.
+                *
+                * The rewriter creates WCOs from the USING quals of SELECT policies,
+                * and stores them as WCOs of "kind" WCO_RLS_CONFLICT_CHECK.  If FOR
+                * UPDATE/SHARE was specified, UPDATE permissions are required on the
+                * target table, and the rewriter also adds WCOs built from the USING
+                * quals of UPDATE policies, using WCOs of the same kind, and this
+                * check enforces them too.
+                */
+               ExecWithCheckOptions(WCO_RLS_CONFLICT_CHECK, resultRelInfo,
+                                                        existing,
+                                                        mtstate->ps.state);
+       }
+
+       /* RETURNING is required for DO SELECT */
+       Assert(resultRelInfo->ri_projectReturning);
+
+       *returning = ExecProcessReturning(context, resultRelInfo, false,
+                                                                         existing, existing, context->planSlot);
+
+       if (canSetTag)
+               context->estate->es_processed++;
+
+       /*
+        * Before releasing the existing tuple, make sure that the returning slot
+        * has a local copy of any pass-by-reference values.
+        */
+       ExecMaterializeSlot(*returning);
+
+       /*
+        * Clear out existing tuple, as there might not be another conflict among
+        * the next input rows. Don't want to hold resources till the end of the
+        * query.
+        */
+       ExecClearTuple(existing);
+
+       return true;
+}
+
 /*
  * Perform MERGE.
  */
@@ -3549,7 +3740,7 @@ lmerge_matched:
                                case CMD_UPDATE:
                                        rslot = ExecProcessReturning(context,
                                                                                                 resultRelInfo,
-                                                                                                CMD_UPDATE,
+                                                                                                false,
                                                                                                 resultRelInfo->ri_oldTupleSlot,
                                                                                                 newslot,
                                                                                                 context->planSlot);
@@ -3558,7 +3749,7 @@ lmerge_matched:
                                case CMD_DELETE:
                                        rslot = ExecProcessReturning(context,
                                                                                                 resultRelInfo,
-                                                                                                CMD_DELETE,
+                                                                                                true,
                                                                                                 resultRelInfo->ri_oldTupleSlot,
                                                                                                 NULL,
                                                                                                 context->planSlot);
@@ -4329,7 +4520,8 @@ ExecModifyTable(PlanState *pstate)
                        Assert((resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_OLD) == 0 &&
                                   (resultRelInfo->ri_projectReturning->pi_state.flags & EEO_FLAG_HAS_NEW) == 0);
 
-                       slot = ExecProcessReturning(&context, resultRelInfo, operation,
+                       slot = ExecProcessReturning(&context, resultRelInfo,
+                                                                               operation == CMD_DELETE,
                                                                                NULL, NULL, context.planSlot);
 
                        return slot;
@@ -5031,49 +5223,60 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
        }
 
        /*
-        * If needed, Initialize target list, projection and qual for ON CONFLICT
-        * DO UPDATE.
+        * For ON CONFLICT DO SELECT/UPDATE, initialize the ON CONFLICT action
+        * state.
         */
-       if (node->onConflictAction == ONCONFLICT_UPDATE)
+       if (node->onConflictAction == ONCONFLICT_UPDATE ||
+               node->onConflictAction == ONCONFLICT_SELECT)
        {
-               OnConflictSetState *onconfl = makeNode(OnConflictSetState);
-               ExprContext *econtext;
-               TupleDesc       relationDesc;
+               OnConflictActionState *onconfl = makeNode(OnConflictActionState);
 
                /* already exists if created by RETURNING processing above */
                if (mtstate->ps.ps_ExprContext == NULL)
                        ExecAssignExprContext(estate, &mtstate->ps);
 
-               econtext = mtstate->ps.ps_ExprContext;
-               relationDesc = resultRelInfo->ri_RelationDesc->rd_att;
-
-               /* create state for DO UPDATE SET operation */
+               /* action state for DO SELECT/UPDATE */
                resultRelInfo->ri_onConflict = onconfl;
 
+               /* lock strength for DO SELECT [FOR UPDATE/SHARE] */
+               onconfl->oc_LockStrength = node->onConflictLockStrength;
+
                /* initialize slot for the existing tuple */
                onconfl->oc_Existing =
                        table_slot_create(resultRelInfo->ri_RelationDesc,
                                                          &mtstate->ps.state->es_tupleTable);
 
                /*
-                * Create the tuple slot for the UPDATE SET projection. We want a slot
-                * of the table's type here, because the slot will be used to insert
-                * into the table, and for RETURNING processing - which may access
-                * system attributes.
+                * For ON CONFLICT DO UPDATE, initialize target list and projection.
                 */
-               onconfl->oc_ProjSlot =
-                       table_slot_create(resultRelInfo->ri_RelationDesc,
-                                                         &mtstate->ps.state->es_tupleTable);
+               if (node->onConflictAction == ONCONFLICT_UPDATE)
+               {
+                       ExprContext *econtext;
+                       TupleDesc       relationDesc;
+
+                       econtext = mtstate->ps.ps_ExprContext;
+                       relationDesc = resultRelInfo->ri_RelationDesc->rd_att;
 
-               /* build UPDATE SET projection state */
-               onconfl->oc_ProjInfo =
-                       ExecBuildUpdateProjection(node->onConflictSet,
-                                                                         true,
-                                                                         node->onConflictCols,
-                                                                         relationDesc,
-                                                                         econtext,
-                                                                         onconfl->oc_ProjSlot,
-                                                                         &mtstate->ps);
+                       /*
+                        * Create the tuple slot for the UPDATE SET projection. We want a
+                        * slot of the table's type here, because the slot will be used to
+                        * insert into the table, and for RETURNING processing - which may
+                        * access system attributes.
+                        */
+                       onconfl->oc_ProjSlot =
+                               table_slot_create(resultRelInfo->ri_RelationDesc,
+                                                                 &mtstate->ps.state->es_tupleTable);
+
+                       /* build UPDATE SET projection state */
+                       onconfl->oc_ProjInfo =
+                               ExecBuildUpdateProjection(node->onConflictSet,
+                                                                                 true,
+                                                                                 node->onConflictCols,
+                                                                                 relationDesc,
+                                                                                 econtext,
+                                                                                 onconfl->oc_ProjSlot,
+                                                                                 &mtstate->ps);
+               }
 
                /* initialize state to evaluate the WHERE clause, if any */
                if (node->onConflictWhere)
index 959df43c39e4fbdc27b4916ff280de4293bbc40d..21f1988cf22e7fe9838bd8611ba16595bb47cb8b 100644 (file)
@@ -7043,6 +7043,7 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
        if (!onconflict)
        {
                node->onConflictAction = ONCONFLICT_NONE;
+               node->onConflictLockStrength = LCS_NONE;
                node->onConflictSet = NIL;
                node->onConflictCols = NIL;
                node->onConflictWhere = NULL;
@@ -7054,6 +7055,9 @@ make_modifytable(PlannerInfo *root, Plan *subplan,
        {
                node->onConflictAction = onconflict->action;
 
+               /* Lock strength for ON CONFLICT DO SELECT [FOR UPDATE/SHARE] */
+               node->onConflictLockStrength = onconflict->lockStrength;
+
                /*
                 * Here we convert the ON CONFLICT UPDATE tlist, if any, to the
                 * executor's convention of having consecutive resno's.  The actual
index 5ad6c13830bbe0c62094bad094d0ca8b78f02e0b..1b5b9b5ed9cc8cd5214cab7d1f7d531fc1cb90ad 100644 (file)
@@ -1163,7 +1163,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
                                 * those are already used by RETURNING and it seems better to
                                 * be non-conflicting.
                                 */
-                               if (splan->onConflictSet)
+                               if (splan->onConflictAction == ONCONFLICT_UPDATE ||
+                                       splan->onConflictAction == ONCONFLICT_SELECT)
                                {
                                        indexed_tlist *itlist;
 
@@ -3146,7 +3147,7 @@ search_indexed_tlist_for_sortgroupref(Expr *node,
  *       other-relation Vars by OUTER_VAR references, while leaving target Vars
  *       alone. Thus inner_itlist = NULL and acceptable_rel = the ID of the
  *       target relation should be passed.
- * 3) ON CONFLICT UPDATE SET/WHERE clauses.  Here references to EXCLUDED are
+ * 3) ON CONFLICT SET and WHERE clauses.  Here references to EXCLUDED are
  *       to be replaced with INNER_VAR references, while leaving target Vars (the
  *       to-be-updated relation) alone. Correspondingly inner_itlist is to be
  *       EXCLUDED elements, outer_itlist = NULL and acceptable_rel the target
index 1b20bc805e6f0f8e6c5db45fb5aa0b09ee85a7c9..d63e7390be764c1d10096d0ec41662912de88ffb 100644 (file)
@@ -841,9 +841,9 @@ infer_arbiter_indexes(PlannerInfo *root)
 
        /*
         * Quickly return NIL for ON CONFLICT DO NOTHING without an inference
-        * specification or named constraint.  ON CONFLICT DO UPDATE statements
-        * must always provide one or the other (but parser ought to have caught
-        * that already).
+        * specification or named constraint.  ON CONFLICT DO SELECT/UPDATE
+        * statements must always provide one or the other (but parser ought to
+        * have caught that already).
         */
        if (onconflict->arbiterElems == NIL &&
                onconflict->constraint == InvalidOid)
@@ -1024,10 +1024,17 @@ infer_arbiter_indexes(PlannerInfo *root)
                 */
                if (indexOidFromConstraint == idxForm->indexrelid)
                {
-                       if (idxForm->indisexclusion && onconflict->action == ONCONFLICT_UPDATE)
+                       /*
+                        * ON CONFLICT DO UPDATE and ON CONFLICT DO SELECT are not
+                        * supported with exclusion constraints.
+                        */
+                       if (idxForm->indisexclusion &&
+                               (onconflict->action == ONCONFLICT_UPDATE ||
+                                onconflict->action == ONCONFLICT_SELECT))
                                ereport(ERROR,
-                                               (errcode(ERRCODE_WRONG_OBJECT_TYPE),
-                                                errmsg("ON CONFLICT DO UPDATE not supported with exclusion constraints")));
+                                               errcode(ERRCODE_WRONG_OBJECT_TYPE),
+                                               errmsg("ON CONFLICT DO %s not supported with exclusion constraints",
+                                                          onconflict->action == ONCONFLICT_UPDATE ? "UPDATE" : "SELECT"));
 
                        /* Consider this one a match already */
                        results = lappend_oid(results, idxForm->indexrelid);
@@ -1037,10 +1044,12 @@ infer_arbiter_indexes(PlannerInfo *root)
                else if (indexOidFromConstraint != InvalidOid)
                {
                        /*
-                        * In the case of "ON constraint_name DO UPDATE" we need to skip
-                        * non-unique candidates.
+                        * In the case of "ON constraint_name DO SELECT/UPDATE" we need to
+                        * skip non-unique candidates.
                         */
-                       if (!idxForm->indisunique && onconflict->action == ONCONFLICT_UPDATE)
+                       if (!idxForm->indisunique &&
+                               (onconflict->action == ONCONFLICT_UPDATE ||
+                                onconflict->action == ONCONFLICT_SELECT))
                                continue;
                }
                else
index 50d51c880d6f6ae08cd6c0ade12ddb7b71f7d63f..539c16c4f794ed7803c38279403953650c6da7e9 100644 (file)
@@ -650,7 +650,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
        ListCell   *icols;
        ListCell   *attnos;
        ListCell   *lc;
-       bool            isOnConflictUpdate;
+       bool            requiresUpdatePerm;
        AclMode         targetPerms;
 
        /* There can't be any outer WITH to worry about */
@@ -668,8 +668,14 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
 
        qry->override = stmt->override;
 
-       isOnConflictUpdate = (stmt->onConflictClause &&
-                                                 stmt->onConflictClause->action == ONCONFLICT_UPDATE);
+       /*
+        * ON CONFLICT DO UPDATE and ON CONFLICT DO SELECT FOR UPDATE/SHARE
+        * require UPDATE permission on the target relation.
+        */
+       requiresUpdatePerm = (stmt->onConflictClause &&
+                                                 (stmt->onConflictClause->action == ONCONFLICT_UPDATE ||
+                                                  (stmt->onConflictClause->action == ONCONFLICT_SELECT &&
+                                                       stmt->onConflictClause->lockStrength != LCS_NONE)));
 
        /*
         * We have three cases to deal with: DEFAULT VALUES (selectStmt == NULL),
@@ -719,7 +725,7 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
         * to the joinlist or namespace.
         */
        targetPerms = ACL_INSERT;
-       if (isOnConflictUpdate)
+       if (requiresUpdatePerm)
                targetPerms |= ACL_UPDATE;
        qry->resultRelation = setTargetTable(pstate, stmt->relation,
                                                                                 false, false, targetPerms);
@@ -1026,6 +1032,15 @@ transformInsertStmt(ParseState *pstate, InsertStmt *stmt)
                                                 false, true, true);
        }
 
+       /* ON CONFLICT DO SELECT requires a RETURNING clause */
+       if (stmt->onConflictClause &&
+               stmt->onConflictClause->action == ONCONFLICT_SELECT &&
+               !stmt->returningClause)
+               ereport(ERROR,
+                               errcode(ERRCODE_SYNTAX_ERROR),
+                               errmsg("ON CONFLICT DO SELECT requires a RETURNING clause"),
+                               parser_errposition(pstate, stmt->onConflictClause->location));
+
        /* Process ON CONFLICT, if any. */
        if (stmt->onConflictClause)
                qry->onConflict = transformOnConflictClause(pstate,
@@ -1184,12 +1199,13 @@ transformOnConflictClause(ParseState *pstate,
        OnConflictExpr *result;
 
        /*
-        * If this is ON CONFLICT ... UPDATE, first create the range table entry
-        * for the EXCLUDED pseudo relation, so that that will be present while
-        * processing arbiter expressions.  (You can't actually reference it from
-        * there, but this provides a useful error message if you try.)
+        * If this is ON CONFLICT DO SELECT/UPDATE, first create the range table
+        * entry for the EXCLUDED pseudo relation, so that that will be present
+        * while processing arbiter expressions.  (You can't actually reference it
+        * from there, but this provides a useful error message if you try.)
         */
-       if (onConflictClause->action == ONCONFLICT_UPDATE)
+       if (onConflictClause->action == ONCONFLICT_UPDATE ||
+               onConflictClause->action == ONCONFLICT_SELECT)
        {
                Relation        targetrel = pstate->p_target_relation;
                RangeTblEntry *exclRte;
@@ -1218,21 +1234,22 @@ transformOnConflictClause(ParseState *pstate,
        transformOnConflictArbiter(pstate, onConflictClause, &arbiterElems,
                                                           &arbiterWhere, &arbiterConstraint);
 
-       /* Process DO UPDATE */
-       if (onConflictClause->action == ONCONFLICT_UPDATE)
+       /* Process DO SELECT/UPDATE */
+       if (onConflictClause->action == ONCONFLICT_UPDATE ||
+               onConflictClause->action == ONCONFLICT_SELECT)
        {
                /*
                 * Add the EXCLUDED pseudo relation to the query namespace, making it
-                * available in the UPDATE subexpressions.
+                * available in SET and WHERE subexpressions.
                 */
                addNSItemToQuery(pstate, exclNSItem, false, true, true);
 
-               /*
-                * Now transform the UPDATE subexpressions.
-                */
-               onConflictSet =
-                       transformUpdateTargetList(pstate, onConflictClause->targetList);
+               /* Process the UPDATE SET clause */
+               if (onConflictClause->action == ONCONFLICT_UPDATE)
+                       onConflictSet =
+                               transformUpdateTargetList(pstate, onConflictClause->targetList);
 
+               /* Process the SELECT/UPDATE WHERE clause */
                onConflictWhere = transformWhereClause(pstate,
                                                                                           onConflictClause->whereClause,
                                                                                           EXPR_KIND_WHERE, "WHERE");
@@ -1246,13 +1263,14 @@ transformOnConflictClause(ParseState *pstate,
                pstate->p_namespace = list_delete_last(pstate->p_namespace);
        }
 
-       /* Finally, build ON CONFLICT DO [NOTHING | UPDATE] expression */
+       /* Finally, build ON CONFLICT DO [NOTHING | SELECT | UPDATE] expression */
        result = makeNode(OnConflictExpr);
 
        result->action = onConflictClause->action;
        result->arbiterElems = arbiterElems;
        result->arbiterWhere = arbiterWhere;
        result->constraint = arbiterConstraint;
+       result->lockStrength = onConflictClause->lockStrength;
        result->onConflictSet = onConflictSet;
        result->onConflictWhere = onConflictWhere;
        result->exclRelIndex = exclRelIndex;
index 713ee5c10a219787bde83e46615a854e18bb662a..c567252acc47be9ea91393cb96e156c53c1aec88 100644 (file)
@@ -481,7 +481,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <ival>    OptNoLog
 %type <oncommit> OnCommitOption
 
-%type <ival>   for_locking_strength
+%type <ival>   for_locking_strength opt_for_locking_strength
 %type <node>   for_locking_item
 %type <list>   for_locking_clause opt_for_locking_clause for_locking_items
 %type <list>   locked_rels_list
@@ -12496,12 +12496,24 @@ insert_column_item:
                ;
 
 opt_on_conflict:
+                       ON CONFLICT opt_conf_expr DO SELECT opt_for_locking_strength where_clause
+                               {
+                                       $$ = makeNode(OnConflictClause);
+                                       $$->action = ONCONFLICT_SELECT;
+                                       $$->infer = $3;
+                                       $$->targetList = NIL;
+                                       $$->lockStrength = $6;
+                                       $$->whereClause = $7;
+                                       $$->location = @1;
+                               }
+                       |
                        ON CONFLICT opt_conf_expr DO UPDATE SET set_clause_list where_clause
                                {
                                        $$ = makeNode(OnConflictClause);
                                        $$->action = ONCONFLICT_UPDATE;
                                        $$->infer = $3;
                                        $$->targetList = $7;
+                                       $$->lockStrength = LCS_NONE;
                                        $$->whereClause = $8;
                                        $$->location = @1;
                                }
@@ -12512,6 +12524,7 @@ opt_on_conflict:
                                        $$->action = ONCONFLICT_NOTHING;
                                        $$->infer = $3;
                                        $$->targetList = NIL;
+                                       $$->lockStrength = LCS_NONE;
                                        $$->whereClause = NULL;
                                        $$->location = @1;
                                }
@@ -13741,6 +13754,11 @@ for_locking_strength:
                        | FOR KEY SHARE                                         { $$ = LCS_FORKEYSHARE; }
                ;
 
+opt_for_locking_strength:
+                       for_locking_strength                            { $$ = $1; }
+                       | /* EMPTY */                                           { $$ = LCS_NONE; }
+               ;
+
 locked_rels_list:
                        OF qualified_name_list                                  { $$ = $2; }
                        | /* EMPTY */                                                   { $$ = NIL; }
index e35fd25c9bb2b8124418165ea582cfc2e8f078a1..06b65d4a605208e32ca3b739783da3a17d97eb53 100644 (file)
@@ -3373,13 +3373,15 @@ transformOnConflictArbiter(ParseState *pstate,
        *arbiterWhere = NULL;
        *constraint = InvalidOid;
 
-       if (onConflictClause->action == ONCONFLICT_UPDATE && !infer)
+       if ((onConflictClause->action == ONCONFLICT_UPDATE ||
+                onConflictClause->action == ONCONFLICT_SELECT) && !infer)
                ereport(ERROR,
-                               (errcode(ERRCODE_SYNTAX_ERROR),
-                                errmsg("ON CONFLICT DO UPDATE requires inference specification or constraint name"),
-                                errhint("For example, ON CONFLICT (column_name)."),
-                                parser_errposition(pstate,
-                                                                       exprLocation((Node *) onConflictClause))));
+                               errcode(ERRCODE_SYNTAX_ERROR),
+                               errmsg("ON CONFLICT DO %s requires inference specification or constraint name",
+                                          onConflictClause->action == ONCONFLICT_UPDATE ? "UPDATE" : "SELECT"),
+                               errhint("For example, ON CONFLICT (column_name)."),
+                               parser_errposition(pstate,
+                                                                  exprLocation((Node *) onConflictClause)));
 
        /*
         * To simplify certain aspects of its design, speculative insertion into
index 19dcce80ec4c70f215cbd8097759b696d7164a70..7c99290be4d2147ce65f8b1ac3ecf81c09fa1054 100644 (file)
@@ -658,6 +658,19 @@ rewriteRuleAction(Query *parsetree,
                        rule_action = sub_action;
        }
 
+       /*
+        * If rule_action is INSERT .. ON CONFLICT DO SELECT, the parser should
+        * have verified that it has a RETURNING clause, but we must also check
+        * that the triggering query has a RETURNING clause.
+        */
+       if (rule_action->onConflict &&
+               rule_action->onConflict->action == ONCONFLICT_SELECT &&
+               (!rule_action->returningList || !parsetree->returningList))
+               ereport(ERROR,
+                               errcode(ERRCODE_SYNTAX_ERROR),
+                               errmsg("ON CONFLICT DO SELECT requires a RETURNING clause"),
+                               errdetail("A rule action is INSERT ... ON CONFLICT DO SELECT, which requires a RETURNING clause."));
+
        /*
         * If rule_action has a RETURNING clause, then either throw it away if the
         * triggering query has no RETURNING clause, or rewrite it to emit what
@@ -3643,11 +3656,12 @@ rewriteTargetView(Query *parsetree, Relation view)
        }
 
        /*
-        * For INSERT .. ON CONFLICT .. DO UPDATE, we must also update assorted
-        * stuff in the onConflict data structure.
+        * For INSERT .. ON CONFLICT .. DO SELECT/UPDATE, we must also update
+        * assorted stuff in the onConflict data structure.
         */
        if (parsetree->onConflict &&
-               parsetree->onConflict->action == ONCONFLICT_UPDATE)
+               (parsetree->onConflict->action == ONCONFLICT_UPDATE ||
+                parsetree->onConflict->action == ONCONFLICT_SELECT))
        {
                Index           old_exclRelIndex,
                                        new_exclRelIndex;
@@ -3656,9 +3670,8 @@ rewriteTargetView(Query *parsetree, Relation view)
                List       *tmp_tlist;
 
                /*
-                * Like the INSERT/UPDATE code above, update the resnos in the
-                * auxiliary UPDATE targetlist to refer to columns of the base
-                * relation.
+                * For ON CONFLICT DO UPDATE, update the resnos in the auxiliary
+                * UPDATE targetlist to refer to columns of the base relation.
                 */
                foreach(lc, parsetree->onConflict->onConflictSet)
                {
@@ -3677,7 +3690,7 @@ rewriteTargetView(Query *parsetree, Relation view)
                }
 
                /*
-                * Also, create a new RTE for the EXCLUDED pseudo-relation, using the
+                * Create a new RTE for the EXCLUDED pseudo-relation, using the
                 * query's new base rel (which may well have a different column list
                 * from the view, hence we need a new column alias list).  This should
                 * match transformOnConflictClause.  In particular, note that the
index 93a205d02bcb4b9646209ac1269ef6cbbff30064..e88a1bc1a89b4e72f23b0be0da822df1c4d1c10a 100644 (file)
@@ -301,40 +301,48 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
                }
 
                /*
-                * For INSERT ... ON CONFLICT DO UPDATE we need additional policy
-                * checks for the UPDATE which may be applied to the same RTE.
+                * For INSERT ... ON CONFLICT DO SELECT/UPDATE we need additional
+                * policy checks for the SELECT/UPDATE which may be applied to the
+                * same RTE.
                 */
-               if (commandType == CMD_INSERT &&
-                       root->onConflict && root->onConflict->action == ONCONFLICT_UPDATE)
+               if (commandType == CMD_INSERT && root->onConflict &&
+                       (root->onConflict->action == ONCONFLICT_UPDATE ||
+                        root->onConflict->action == ONCONFLICT_SELECT))
                {
-                       List       *conflict_permissive_policies;
-                       List       *conflict_restrictive_policies;
+                       List       *conflict_permissive_policies = NIL;
+                       List       *conflict_restrictive_policies = NIL;
                        List       *conflict_select_permissive_policies = NIL;
                        List       *conflict_select_restrictive_policies = NIL;
 
-                       /* Get the policies that apply to the auxiliary UPDATE */
-                       get_policies_for_relation(rel, CMD_UPDATE, user_id,
-                                                                         &conflict_permissive_policies,
-                                                                         &conflict_restrictive_policies);
-
-                       /*
-                        * Enforce the USING clauses of the UPDATE policies using WCOs
-                        * rather than security quals.  This ensures that an error is
-                        * raised if the conflicting row cannot be updated due to RLS,
-                        * rather than the change being silently dropped.
-                        */
-                       add_with_check_options(rel, rt_index,
-                                                                  WCO_RLS_CONFLICT_CHECK,
-                                                                  conflict_permissive_policies,
-                                                                  conflict_restrictive_policies,
-                                                                  withCheckOptions,
-                                                                  hasSubLinks,
-                                                                  true);
+                       if (perminfo->requiredPerms & ACL_UPDATE)
+                       {
+                               /*
+                                * Get the policies that apply to the auxiliary UPDATE or
+                                * SELECT FOR UPDATE/SHARE.
+                                */
+                               get_policies_for_relation(rel, CMD_UPDATE, user_id,
+                                                                                 &conflict_permissive_policies,
+                                                                                 &conflict_restrictive_policies);
+
+                               /*
+                                * Enforce the USING clauses of the UPDATE policies using WCOs
+                                * rather than security quals.  This ensures that an error is
+                                * raised if the conflicting row cannot be updated/locked due
+                                * to RLS, rather than the change being silently dropped.
+                                */
+                               add_with_check_options(rel, rt_index,
+                                                                          WCO_RLS_CONFLICT_CHECK,
+                                                                          conflict_permissive_policies,
+                                                                          conflict_restrictive_policies,
+                                                                          withCheckOptions,
+                                                                          hasSubLinks,
+                                                                          true);
+                       }
 
                        /*
                         * Get and add ALL/SELECT policies, as WCO_RLS_CONFLICT_CHECK WCOs
-                        * to ensure they are considered when taking the UPDATE path of an
-                        * INSERT .. ON CONFLICT DO UPDATE, if SELECT rights are required
+                        * to ensure they are considered when taking the SELECT/UPDATE
+                        * path of an INSERT .. ON CONFLICT, if SELECT rights are required
                         * for this relation, also as WCO policies, again, to avoid
                         * silently dropping data.  See above.
                         */
@@ -352,29 +360,36 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
                                                                           true);
                        }
 
-                       /* Enforce the WITH CHECK clauses of the UPDATE policies */
-                       add_with_check_options(rel, rt_index,
-                                                                  WCO_RLS_UPDATE_CHECK,
-                                                                  conflict_permissive_policies,
-                                                                  conflict_restrictive_policies,
-                                                                  withCheckOptions,
-                                                                  hasSubLinks,
-                                                                  false);
-
                        /*
-                        * Add ALL/SELECT policies as WCO_RLS_UPDATE_CHECK WCOs, to ensure
-                        * that the final updated row is visible when taking the UPDATE
-                        * path of an INSERT .. ON CONFLICT DO UPDATE, if SELECT rights
-                        * are required for this relation.
+                        * For INSERT .. ON CONFLICT DO UPDATE, add additional policies to
+                        * be checked when the auxiliary UPDATE is executed.
                         */
-                       if (perminfo->requiredPerms & ACL_SELECT)
+                       if (root->onConflict->action == ONCONFLICT_UPDATE)
+                       {
+                               /* Enforce the WITH CHECK clauses of the UPDATE policies */
                                add_with_check_options(rel, rt_index,
                                                                           WCO_RLS_UPDATE_CHECK,
-                                                                          conflict_select_permissive_policies,
-                                                                          conflict_select_restrictive_policies,
+                                                                          conflict_permissive_policies,
+                                                                          conflict_restrictive_policies,
                                                                           withCheckOptions,
                                                                           hasSubLinks,
-                                                                          true);
+                                                                          false);
+
+                               /*
+                                * Add ALL/SELECT policies as WCO_RLS_UPDATE_CHECK WCOs, to
+                                * ensure that the final updated row is visible when taking
+                                * the UPDATE path of an INSERT .. ON CONFLICT, if SELECT
+                                * rights are required for this relation.
+                                */
+                               if (perminfo->requiredPerms & ACL_SELECT)
+                                       add_with_check_options(rel, rt_index,
+                                                                                  WCO_RLS_UPDATE_CHECK,
+                                                                                  conflict_select_permissive_policies,
+                                                                                  conflict_select_restrictive_policies,
+                                                                                  withCheckOptions,
+                                                                                  hasSubLinks,
+                                                                                  true);
+                       }
                }
        }
 
@@ -398,8 +413,8 @@ get_row_security_policies(Query *root, RangeTblEntry *rte, int rt_index,
         * XXX We are setting up USING quals as WITH CHECK. If RLS prohibits
         * UPDATE/DELETE on the target row, we shall throw an error instead of
         * silently ignoring the row. This is different than how normal
-        * UPDATE/DELETE works and more in line with INSERT ON CONFLICT DO UPDATE
-        * handling.
+        * UPDATE/DELETE works and more in line with INSERT ON CONFLICT DO
+        * SELECT/UPDATE handling.
         */
        if (commandType == CMD_MERGE)
        {
@@ -784,9 +799,9 @@ add_security_quals(int rt_index,
  * added by an INSERT or UPDATE are consistent with the specified RLS
  * policies.  Normally new data must satisfy the WITH CHECK clauses from the
  * policies.  If a policy has no explicit WITH CHECK clause, its USING clause
- * is used instead.  In the special case of an UPDATE arising from an
- * INSERT ... ON CONFLICT DO UPDATE, existing records are first checked using
- * a WCO_RLS_CONFLICT_CHECK WithCheckOption, which always uses the USING
+ * is used instead.  In the special case of a SELECT or UPDATE arising from an
+ * INSERT ... ON CONFLICT DO SELECT/UPDATE, existing records are first checked
+ * using a WCO_RLS_CONFLICT_CHECK WithCheckOption, which always uses the USING
  * clauses from RLS policies.
  *
  * New WCOs are added to withCheckOptions, and hasSubLinks is set to true if
index b5a7ad9066e7ebe013545792368984d539926227..89cbdd3b1e7bb9fe7bc9822aa2e7bb3268d1a431 100644 (file)
@@ -426,6 +426,7 @@ static void get_update_query_targetlist_def(Query *query, List *targetList,
 static void get_delete_query_def(Query *query, deparse_context *context);
 static void get_merge_query_def(Query *query, deparse_context *context);
 static void get_utility_query_def(Query *query, deparse_context *context);
+static char *get_lock_clause_strength(LockClauseStrength strength);
 static void get_basic_select_query(Query *query, deparse_context *context);
 static void get_target_list(List *targetList, deparse_context *context);
 static void get_returning_clause(Query *query, deparse_context *context);
@@ -5186,10 +5187,10 @@ set_deparse_plan(deparse_namespace *dpns, Plan *plan)
         * source, and all INNER_VAR Vars in other parts of the query refer to its
         * targetlist.
         *
-        * For ON CONFLICT .. UPDATE we just need the inner tlist to point to the
-        * excluded expression's tlist. (Similar to the SubqueryScan we don't want
-        * to reuse OUTER, it's used for RETURNING in some modify table cases,
-        * although not INSERT .. CONFLICT).
+        * For ON CONFLICT DO SELECT/UPDATE we just need the inner tlist to point
+        * to the excluded expression's tlist. (Similar to the SubqueryScan we
+        * don't want to reuse OUTER, it's used for RETURNING in some modify table
+        * cases, although not INSERT .. CONFLICT).
         */
        if (IsA(plan, SubqueryScan))
                dpns->inner_plan = ((SubqueryScan *) plan)->subplan;
@@ -5997,30 +5998,9 @@ get_select_query_def(Query *query, deparse_context *context)
                        if (rc->pushedDown)
                                continue;
 
-                       switch (rc->strength)
-                       {
-                               case LCS_NONE:
-                                       /* we intentionally throw an error for LCS_NONE */
-                                       elog(ERROR, "unrecognized LockClauseStrength %d",
-                                                (int) rc->strength);
-                                       break;
-                               case LCS_FORKEYSHARE:
-                                       appendContextKeyword(context, " FOR KEY SHARE",
-                                                                                -PRETTYINDENT_STD, PRETTYINDENT_STD, 0);
-                                       break;
-                               case LCS_FORSHARE:
-                                       appendContextKeyword(context, " FOR SHARE",
-                                                                                -PRETTYINDENT_STD, PRETTYINDENT_STD, 0);
-                                       break;
-                               case LCS_FORNOKEYUPDATE:
-                                       appendContextKeyword(context, " FOR NO KEY UPDATE",
-                                                                                -PRETTYINDENT_STD, PRETTYINDENT_STD, 0);
-                                       break;
-                               case LCS_FORUPDATE:
-                                       appendContextKeyword(context, " FOR UPDATE",
-                                                                                -PRETTYINDENT_STD, PRETTYINDENT_STD, 0);
-                                       break;
-                       }
+                       appendContextKeyword(context,
+                                                                get_lock_clause_strength(rc->strength),
+                                                                -PRETTYINDENT_STD, PRETTYINDENT_STD, 0);
 
                        appendStringInfo(buf, " OF %s",
                                                         quote_identifier(get_rtable_name(rc->rti,
@@ -6033,6 +6013,28 @@ get_select_query_def(Query *query, deparse_context *context)
        }
 }
 
+static char *
+get_lock_clause_strength(LockClauseStrength strength)
+{
+       switch (strength)
+       {
+               case LCS_NONE:
+                       /* we intentionally throw an error for LCS_NONE */
+                       elog(ERROR, "unrecognized LockClauseStrength %d",
+                                (int) strength);
+                       break;
+               case LCS_FORKEYSHARE:
+                       return " FOR KEY SHARE";
+               case LCS_FORSHARE:
+                       return " FOR SHARE";
+               case LCS_FORNOKEYUPDATE:
+                       return " FOR NO KEY UPDATE";
+               case LCS_FORUPDATE:
+                       return " FOR UPDATE";
+       }
+       return NULL;                            /* keep compiler quiet */
+}
+
 /*
  * Detect whether query looks like SELECT ... FROM VALUES(),
  * with no need to rename the output columns of the VALUES RTE.
@@ -7125,7 +7127,7 @@ get_insert_query_def(Query *query, deparse_context *context)
                {
                        appendStringInfoString(buf, " DO NOTHING");
                }
-               else
+               else if (confl->action == ONCONFLICT_UPDATE)
                {
                        appendStringInfoString(buf, " DO UPDATE SET ");
                        /* Deparse targetlist */
@@ -7140,6 +7142,23 @@ get_insert_query_def(Query *query, deparse_context *context)
                                get_rule_expr(confl->onConflictWhere, context, false);
                        }
                }
+               else
+               {
+                       Assert(confl->action == ONCONFLICT_SELECT);
+                       appendStringInfoString(buf, " DO SELECT");
+
+                       /* Add FOR [KEY] UPDATE/SHARE clause if present */
+                       if (confl->lockStrength != LCS_NONE)
+                               appendStringInfoString(buf, get_lock_clause_strength(confl->lockStrength));
+
+                       /* Add a WHERE clause if given */
+                       if (confl->onConflictWhere != NULL)
+                       {
+                               appendContextKeyword(context, " WHERE ",
+                                                                        -PRETTYINDENT_STD, PRETTYINDENT_STD, 1);
+                               get_rule_expr(confl->onConflictWhere, context, false);
+                       }
+               }
        }
 
        /* Add RETURNING if present */
index a910b3d04e63c84a4d6c5bcbb1abb7b81ed018a0..0bdd42a2b86e8ef106851df6138c1b8f442dbecb 100644 (file)
@@ -57,6 +57,6 @@
  */
 
 /*                                                     yyyymmddN */
-#define CATALOG_VERSION_NO     202602101
+#define CATALOG_VERSION_NO     202602121
 
 #endif
index f8053d9e572d767200ff1c8dd9fabc87d292ce4e..63c067d5aae614c4127186f663700da030551acc 100644 (file)
@@ -424,19 +424,20 @@ typedef struct JunkFilter
 } JunkFilter;
 
 /*
- * OnConflictSetState
+ * OnConflictActionState
  *
- * Executor state of an ON CONFLICT DO UPDATE operation.
+ * Executor state of an ON CONFLICT DO SELECT/UPDATE operation.
  */
-typedef struct OnConflictSetState
+typedef struct OnConflictActionState
 {
        NodeTag         type;
 
        TupleTableSlot *oc_Existing;    /* slot to store existing target tuple in */
        TupleTableSlot *oc_ProjSlot;    /* CONFLICT ... SET ... projection target */
        ProjectionInfo *oc_ProjInfo;    /* for ON CONFLICT DO UPDATE SET */
+       LockClauseStrength oc_LockStrength; /* lock strength for DO SELECT */
        ExprState  *oc_WhereClause; /* state for the WHERE clause */
-} OnConflictSetState;
+} OnConflictActionState;
 
 /* ----------------
  *      MergeActionState information
@@ -581,8 +582,8 @@ typedef struct ResultRelInfo
        /* list of arbiter indexes to use to check conflicts */
        List       *ri_onConflictArbiterIndexes;
 
-       /* ON CONFLICT evaluation state */
-       OnConflictSetState *ri_onConflict;
+       /* ON CONFLICT evaluation state for DO SELECT/UPDATE */
+       OnConflictActionState *ri_onConflict;
 
        /* for MERGE, lists of MergeActionState (one per MergeMatchKind) */
        List       *ri_MergeActions[NUM_MERGE_MATCH_KINDS];
index 22864454c3eb32f109f5d88b8d8bd8f922bba959..7961444eed1c392e0a82d67681d75698335e7a3c 100644 (file)
@@ -20,7 +20,8 @@
  */
 typedef enum LockClauseStrength
 {
-       LCS_NONE,                                       /* no such clause - only used in PlanRowMark */
+       LCS_NONE,                                       /* no such clause - only used in PlanRowMark
+                                                                * and ON CONFLICT DO SELECT */
        LCS_FORKEYSHARE,                        /* FOR KEY SHARE */
        LCS_FORSHARE,                           /* FOR SHARE */
        LCS_FORNOKEYUPDATE,                     /* FOR NO KEY UPDATE */
index b6ad28618ab874d0b74236e904bf30a2a6f6e504..59a7df31aba65940bf2d9ee5f819b99d86f223a7 100644 (file)
@@ -428,6 +428,7 @@ typedef enum OnConflictAction
        ONCONFLICT_NONE,                        /* No "ON CONFLICT" clause */
        ONCONFLICT_NOTHING,                     /* ON CONFLICT ... DO NOTHING */
        ONCONFLICT_UPDATE,                      /* ON CONFLICT ... DO UPDATE */
+       ONCONFLICT_SELECT,                      /* ON CONFLICT ... DO SELECT */
 } OnConflictAction;
 
 /*
index 646d6ced763cf28a4d1197858face3872a601304..0aec49bdd222db5cb2e1cad4c9832bbd9885201c 100644 (file)
@@ -200,7 +200,7 @@ typedef struct Query
        /* OVERRIDING clause */
        OverridingKind override pg_node_attr(query_jumble_ignore);
 
-       OnConflictExpr *onConflict; /* ON CONFLICT DO [NOTHING | UPDATE] */
+       OnConflictExpr *onConflict; /* ON CONFLICT DO NOTHING/SELECT/UPDATE */
 
        /*
         * The following three fields describe the contents of the RETURNING list
@@ -1417,7 +1417,8 @@ typedef enum WCOKind
        WCO_VIEW_CHECK,                         /* WCO on an auto-updatable view */
        WCO_RLS_INSERT_CHECK,           /* RLS INSERT WITH CHECK policy */
        WCO_RLS_UPDATE_CHECK,           /* RLS UPDATE WITH CHECK policy */
-       WCO_RLS_CONFLICT_CHECK,         /* RLS ON CONFLICT DO UPDATE USING policy */
+       WCO_RLS_CONFLICT_CHECK,         /* RLS ON CONFLICT DO SELECT/UPDATE USING
+                                                                * policy */
        WCO_RLS_MERGE_UPDATE_CHECK, /* RLS MERGE UPDATE USING policy */
        WCO_RLS_MERGE_DELETE_CHECK, /* RLS MERGE DELETE USING policy */
 } WCOKind;
@@ -1679,9 +1680,10 @@ typedef struct InferClause
 typedef struct OnConflictClause
 {
        NodeTag         type;
-       OnConflictAction action;        /* DO NOTHING or UPDATE? */
+       OnConflictAction action;        /* DO NOTHING, SELECT, or UPDATE */
        InferClause *infer;                     /* Optional index inference clause */
-       List       *targetList;         /* the target list (of ResTarget) */
+       LockClauseStrength lockStrength;        /* lock strength for DO SELECT */
+       List       *targetList;         /* target list (of ResTarget) for DO UPDATE */
        Node       *whereClause;        /* qualifications */
        ParseLoc        location;               /* token location, or -1 if unknown */
 } OnConflictClause;
index 485bec5aabdd083546b285056ec3b821b9a03f4f..8c9321aab8cb77bd598afb01f94f214b1f5a1ee7 100644 (file)
@@ -368,11 +368,13 @@ typedef struct ModifyTable
        OnConflictAction onConflictAction;
        /* List of ON CONFLICT arbiter index OIDs  */
        List       *arbiterIndexes;
+       /* lock strength for ON CONFLICT DO SELECT */
+       LockClauseStrength onConflictLockStrength;
        /* INSERT ON CONFLICT DO UPDATE targetlist */
        List       *onConflictSet;
        /* target column numbers for onConflictSet */
        List       *onConflictCols;
-       /* WHERE for ON CONFLICT UPDATE */
+       /* WHERE for ON CONFLICT DO SELECT/UPDATE */
        Node       *onConflictWhere;
        /* RTI of the EXCLUDED pseudo relation */
        Index           exclRelRTI;
index 5211cadc2581fa86d09139443e655c9ed8aa150d..384df50c80aeac7513b519de1e46230639f2e692 100644 (file)
@@ -20,6 +20,7 @@
 #include "access/attnum.h"
 #include "access/cmptype.h"
 #include "nodes/bitmapset.h"
+#include "nodes/lockoptions.h"
 #include "nodes/pg_list.h"
 
 
@@ -2370,7 +2371,7 @@ typedef struct FromExpr
 typedef struct OnConflictExpr
 {
        NodeTag         type;
-       OnConflictAction action;        /* DO NOTHING or UPDATE? */
+       OnConflictAction action;        /* DO NOTHING, SELECT, or UPDATE */
 
        /* Arbiter */
        List       *arbiterElems;       /* unique index arbiter list (of
@@ -2378,9 +2379,14 @@ typedef struct OnConflictExpr
        Node       *arbiterWhere;       /* unique index arbiter WHERE clause */
        Oid                     constraint;             /* pg_constraint OID for arbiter */
 
-       /* ON CONFLICT UPDATE */
+       /* ON CONFLICT DO SELECT */
+       LockClauseStrength lockStrength;        /* strength of lock for DO SELECT */
+
+       /* ON CONFLICT DO UPDATE */
        List       *onConflictSet;      /* List of ON CONFLICT SET TargetEntrys */
-       Node       *onConflictWhere;    /* qualifiers to restrict UPDATE to */
+
+       /* both ON CONFLICT DO SELECT and UPDATE */
+       Node       *onConflictWhere;    /* qualifiers to restrict SELECT/UPDATE */
        int                     exclRelIndex;   /* RT index of 'excluded' relation */
        List       *exclRelTlist;       /* tlist of the EXCLUDED pseudo relation */
 } OnConflictExpr;
diff --git a/src/test/isolation/expected/insert-conflict-do-select.out b/src/test/isolation/expected/insert-conflict-do-select.out
new file mode 100644 (file)
index 0000000..bccfd47
--- /dev/null
@@ -0,0 +1,138 @@
+Parsed test spec with 2 sessions
+
+starting permutation: insert1 insert2 c1 select2 c2
+step insert1: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT RETURNING *;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step insert2: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT RETURNING *;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step c1: COMMIT;
+step select2: SELECT * FROM doselect;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step c2: COMMIT;
+
+starting permutation: insert1_update insert2_update c1 select2 c2
+step insert1_update: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step insert2_update: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; <waiting ...>
+step c1: COMMIT;
+step insert2_update: <... completed>
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step select2: SELECT * FROM doselect;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step c2: COMMIT;
+
+starting permutation: insert1_update insert2_update a1 select2 c2
+step insert1_update: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step insert2_update: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; <waiting ...>
+step a1: ABORT;
+step insert2_update: <... completed>
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step select2: SELECT * FROM doselect;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step c2: COMMIT;
+
+starting permutation: insert1_keyshare insert2_update c1 select2 c2
+step insert1_keyshare: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR KEY SHARE RETURNING *;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step insert2_update: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; <waiting ...>
+step c1: COMMIT;
+step insert2_update: <... completed>
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step select2: SELECT * FROM doselect;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step c2: COMMIT;
+
+starting permutation: insert1_share insert2_update c1 select2 c2
+step insert1_share: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR SHARE RETURNING *;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step insert2_update: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; <waiting ...>
+step c1: COMMIT;
+step insert2_update: <... completed>
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step select2: SELECT * FROM doselect;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step c2: COMMIT;
+
+starting permutation: insert1_nokeyupd insert2_update c1 select2 c2
+step insert1_nokeyupd: INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR NO KEY UPDATE RETURNING *;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step insert2_update: INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; <waiting ...>
+step c1: COMMIT;
+step insert2_update: <... completed>
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step select2: SELECT * FROM doselect;
+key|val     
+---+--------
+  1|original
+(1 row)
+
+step c2: COMMIT;
index 6a4d3532e03676c5f83b91c24f5ea6b80b58bf9e..4e466580cd4d8dfa9301c21c2eb721b22526d021 100644 (file)
@@ -54,6 +54,7 @@ test: insert-conflict-do-update-2
 test: insert-conflict-do-update-3
 test: insert-conflict-do-update-4
 test: insert-conflict-specconflict
+test: insert-conflict-do-select
 test: merge-insert-update
 test: merge-delete
 test: merge-update
diff --git a/src/test/isolation/specs/insert-conflict-do-select.spec b/src/test/isolation/specs/insert-conflict-do-select.spec
new file mode 100644 (file)
index 0000000..dcfd9f8
--- /dev/null
@@ -0,0 +1,53 @@
+# INSERT...ON CONFLICT DO SELECT test
+#
+# This test verifies locking behavior of ON CONFLICT DO SELECT with different
+# lock strengths: no lock, FOR KEY SHARE, FOR SHARE, FOR NO KEY UPDATE, and
+# FOR UPDATE.
+
+setup
+{
+  CREATE TABLE doselect (key int primary key, val text);
+  INSERT INTO doselect VALUES (1, 'original');
+}
+
+teardown
+{
+  DROP TABLE doselect;
+}
+
+session s1
+setup
+{
+  BEGIN ISOLATION LEVEL READ COMMITTED;
+}
+step insert1 { INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT RETURNING *; }
+step insert1_keyshare { INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR KEY SHARE RETURNING *; }
+step insert1_share { INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR SHARE RETURNING *; }
+step insert1_nokeyupd { INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR NO KEY UPDATE RETURNING *; }
+step insert1_update { INSERT INTO doselect(key, val) VALUES(1, 'insert1') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; }
+step c1 { COMMIT; }
+step a1 { ABORT; }
+
+session s2
+setup
+{
+  BEGIN ISOLATION LEVEL READ COMMITTED;
+}
+step insert2 { INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT RETURNING *; }
+step insert2_update { INSERT INTO doselect(key, val) VALUES(1, 'insert2') ON CONFLICT (key) DO SELECT FOR UPDATE RETURNING *; }
+step select2 { SELECT * FROM doselect; }
+step c2 { COMMIT; }
+
+# Test 1: DO SELECT without locking - should not block
+permutation insert1 insert2 c1 select2 c2
+
+# Test 2: DO SELECT FOR UPDATE - should block until first transaction commits
+permutation insert1_update insert2_update c1 select2 c2
+
+# Test 3: DO SELECT FOR UPDATE - should unblock when first transaction aborts
+permutation insert1_update insert2_update a1 select2 c2
+
+# Test 4: Different lock strengths all properly acquire locks
+permutation insert1_keyshare insert2_update c1 select2 c2
+permutation insert1_share insert2_update c1 select2 c2
+permutation insert1_nokeyupd insert2_update c1 select2 c2
index ebc892a2a4280589ed51f3c1189ef77299eb3e66..a6fa9cacb727a559338e15f16bd0c1a740dd57c9 100644 (file)
@@ -780,6 +780,10 @@ INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
 INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
   ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO UPDATE SET c2 = EXCLUDED.c2;
 ERROR:  ON CONFLICT DO UPDATE not supported with exclusion constraints
+-- fail, because DO SELECT variant requires unique index
+INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
+  ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO SELECT RETURNING *;
+ERROR:  ON CONFLICT DO SELECT not supported with exclusion constraints
 -- succeed because c1 doesn't overlap
 INSERT INTO circles VALUES('<(20,20), 1>', '<(0,0), 5>');
 -- succeed because c2 doesn't overlap
index b0e12962088e4cb7d62230c558c779f838381b90..34e2e7ee355e15316c494da5318b0744c8487f25 100644 (file)
@@ -249,6 +249,25 @@ explain (costs off, format json) insert into insertconflicttest values (0, 'Bilb
  ]
 (1 row)
 
+-- Should display lock strength, if specified
+explain (costs off) insert into insertconflicttest values (1, 'Apple') on conflict (key) do select returning *;
+              QUERY PLAN               
+---------------------------------------
+ Insert on insertconflicttest
+   Conflict Resolution: SELECT
+   Conflict Arbiter Indexes: key_index
+   ->  Result
+(4 rows)
+
+explain (costs off) insert into insertconflicttest values (1, 'Apple') on conflict (key) do select for key share returning *;
+                 QUERY PLAN                  
+---------------------------------------------
+ Insert on insertconflicttest
+   Conflict Resolution: SELECT FOR KEY SHARE
+   Conflict Arbiter Indexes: key_index
+   ->  Result
+(4 rows)
+
 -- Fails (no unique index inference specification, required for do update variant):
 insert into insertconflicttest values (1, 'Apple') on conflict do update set fruit = excluded.fruit;
 ERROR:  ON CONFLICT DO UPDATE requires inference specification or constraint name
@@ -304,6 +323,48 @@ ERROR:  column "insertconflicttest" of relation "insertconflicttest" does not ex
 LINE 1: ...3, 'Kiwi') on conflict (key, fruit) do update set insertconf...
                                                              ^
 HINT:  SET target columns cannot be qualified with the relation name.
+--
+-- DO SELECT tests
+--
+delete from insertconflicttest where fruit = 'Apple';
+insert into insertconflicttest values (1, 'Apple') on conflict (key) do select; -- fails
+ERROR:  ON CONFLICT DO SELECT requires a RETURNING clause
+LINE 1: ...nsert into insertconflicttest values (1, 'Apple') on conflic...
+                                                             ^
+insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select returning old, new, i;
+ old |    new    |     i     
+-----+-----------+-----------
+     | (1,Apple) | (1,Apple)
+(1 row)
+
+insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select returning old, new, i;
+    old    |    new    |     i     
+-----------+-----------+-----------
+ (1,Apple) | (1,Apple) | (1,Apple)
+(1 row)
+
+insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select where i.fruit = 'Apple' returning *;
+ key | fruit 
+-----+-------
+   1 | Apple
+(1 row)
+
+insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select where i.fruit = 'Orange' returning *;
+ key | fruit 
+-----+-------
+(0 rows)
+
+insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select where excluded.fruit = 'Apple' returning *;
+ key | fruit 
+-----+-------
+(0 rows)
+
+insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select where excluded.fruit = 'Orange' returning *;
+ key | fruit 
+-----+-------
+   1 | Apple
+(1 row)
+
 drop index key_index;
 --
 -- Composite key tests
@@ -748,13 +809,58 @@ insert into selfconflict values (6,1), (6,2) on conflict(f1) do update set f2 =
 ERROR:  ON CONFLICT DO UPDATE command cannot affect row a second time
 HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.
 commit;
+begin transaction isolation level read committed;
+insert into selfconflict values (7,1), (7,2) on conflict(f1) do select returning *;
+ f1 | f2 
+----+----
+  7 |  1
+  7 |  1
+(2 rows)
+
+commit;
+begin transaction isolation level repeatable read;
+insert into selfconflict values (8,1), (8,2) on conflict(f1) do select returning *;
+ f1 | f2 
+----+----
+  8 |  1
+  8 |  1
+(2 rows)
+
+commit;
+begin transaction isolation level serializable;
+insert into selfconflict values (9,1), (9,2) on conflict(f1) do select returning *;
+ f1 | f2 
+----+----
+  9 |  1
+  9 |  1
+(2 rows)
+
+commit;
+begin transaction isolation level read committed;
+insert into selfconflict values (10,1), (10,2) on conflict(f1) do select for update returning *;
+ERROR:  ON CONFLICT DO SELECT command cannot affect row a second time
+HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.
+commit;
+begin transaction isolation level repeatable read;
+insert into selfconflict values (11,1), (11,2) on conflict(f1) do select for update returning *;
+ERROR:  ON CONFLICT DO SELECT command cannot affect row a second time
+HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.
+commit;
+begin transaction isolation level serializable;
+insert into selfconflict values (12,1), (12,2) on conflict(f1) do select for update returning *;
+ERROR:  ON CONFLICT DO SELECT command cannot affect row a second time
+HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.
+commit;
 select * from selfconflict;
  f1 | f2 
 ----+----
   1 |  1
   2 |  1
   3 |  1
-(3 rows)
+  7 |  1
+  8 |  1
+  9 |  1
+(6 rows)
 
 drop table selfconflict;
 -- check ON CONFLICT handling with partitioned tables
@@ -765,11 +871,31 @@ insert into parted_conflict_test values (1, 'a') on conflict do nothing;
 -- index on a required, which does exist in parent
 insert into parted_conflict_test values (1, 'a') on conflict (a) do nothing;
 insert into parted_conflict_test values (1, 'a') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test values (1, 'a') on conflict (a) do select returning *;
+ a | b 
+---+---
+ 1 | a
+(1 row)
+
+insert into parted_conflict_test values (1, 'a') on conflict (a) do select for update returning *;
+ a | b 
+---+---
+ 1 | a
+(1 row)
+
 -- targeting partition directly will work
 insert into parted_conflict_test_1 values (1, 'a') on conflict (a) do nothing;
 insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do select returning b;
+ b 
+---
+ b
+(1 row)
+
 -- index on b required, which doesn't exist in parent
-insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a;
+insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a; -- fail
+ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+insert into parted_conflict_test values (2, 'b') on conflict (b) do select returning b; -- fail
 ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
 -- targeting partition directly will work
 insert into parted_conflict_test_1 values (2, 'b') on conflict (b) do update set a = excluded.a;
@@ -780,13 +906,31 @@ select * from parted_conflict_test order by a;
  2 | b
 (1 row)
 
--- now check that DO UPDATE works correctly for target partition with
--- different attribute numbers
+-- now check that DO UPDATE and DO SELECT work correctly for target partition
+-- with different attribute numbers
 create table parted_conflict_test_2 (b char, a int unique);
 alter table parted_conflict_test attach partition parted_conflict_test_2 for values in (3);
 truncate parted_conflict_test;
 insert into parted_conflict_test values (3, 'a') on conflict (a) do update set b = excluded.b;
 insert into parted_conflict_test values (3, 'b') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test values (3, 'a') on conflict (a) do select returning b;
+ b 
+---
+ b
+(1 row)
+
+insert into parted_conflict_test values (3, 'a') on conflict (a) do select where excluded.b = 'a' returning parted_conflict_test;
+ parted_conflict_test 
+----------------------
+ (3,b)
+(1 row)
+
+insert into parted_conflict_test values (3, 'a') on conflict (a) do select where parted_conflict_test.b = 'b' returning b;
+ b 
+---
+ b
+(1 row)
+
 -- should see (3, 'b')
 select * from parted_conflict_test order by a;
  a | b 
@@ -800,6 +944,12 @@ create table parted_conflict_test_3 partition of parted_conflict_test for values
 truncate parted_conflict_test;
 insert into parted_conflict_test (a, b) values (4, 'a') on conflict (a) do update set b = excluded.b;
 insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a';
+insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do select returning b;
+ b 
+---
+ b
+(1 row)
+
 -- should see (4, 'b')
 select * from parted_conflict_test order by a;
  a | b 
@@ -813,6 +963,11 @@ create table parted_conflict_test_4_1 partition of parted_conflict_test_4 for va
 truncate parted_conflict_test;
 insert into parted_conflict_test (a, b) values (5, 'a') on conflict (a) do update set b = excluded.b;
 insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a';
+insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do select where parted_conflict_test.b = 'a' returning b;
+ b 
+---
+(0 rows)
+
 -- should see (5, 'b')
 select * from parted_conflict_test order by a;
  a | b 
@@ -833,6 +988,59 @@ select * from parted_conflict_test order by a;
  4 | b
 (3 rows)
 
+-- test DO SELECT with multiple rows hitting different partitions
+truncate parted_conflict_test;
+insert into parted_conflict_test (a, b) values (1, 'a'), (2, 'b'), (4, 'c');
+insert into parted_conflict_test (a, b) values (1, 'x'), (2, 'y'), (4, 'z')
+  on conflict (a) do select returning *, tableoid::regclass;
+ a | b |        tableoid        
+---+---+------------------------
+ 1 | a | parted_conflict_test_1
+ 2 | b | parted_conflict_test_1
+ 4 | c | parted_conflict_test_3
+(3 rows)
+
+-- should see original values (1, 'a'), (2, 'b'), (4, 'c')
+select * from parted_conflict_test order by a;
+ a | b 
+---+---
+ 1 | a
+ 2 | b
+ 4 | c
+(3 rows)
+
+-- test DO SELECT with WHERE filtering across partitions
+insert into parted_conflict_test (a, b) values (1, 'n') on conflict (a) do select where parted_conflict_test.b = 'a' returning *;
+ a | b 
+---+---
+ 1 | a
+(1 row)
+
+insert into parted_conflict_test (a, b) values (2, 'n') on conflict (a) do select where parted_conflict_test.b = 'x' returning *;
+ a | b 
+---+---
+(0 rows)
+
+-- test DO SELECT with EXCLUDED in WHERE across partitions with different layouts
+insert into parted_conflict_test (a, b) values (3, 't') on conflict (a) do select where excluded.b = 't' returning *;
+ a | b 
+---+---
+ 3 | t
+(1 row)
+
+-- test DO SELECT FOR UPDATE across different partition layouts
+insert into parted_conflict_test (a, b) values (1, 'l') on conflict (a) do select for update returning *;
+ a | b 
+---+---
+ 1 | a
+(1 row)
+
+insert into parted_conflict_test (a, b) values (3, 'l') on conflict (a) do select for update returning *;
+ a | b 
+---+---
+ 3 | t
+(1 row)
+
 drop table parted_conflict_test;
 -- test behavior of inserting a conflicting tuple into an intermediate
 -- partitioning level
index daafaa94fdec0b09baf2001204a96e1e77b95d36..84c1c1ca38dd87f4cf7c1b78cf14af7835b3dab5 100644 (file)
@@ -921,6 +921,32 @@ INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set one = 8; -- f
 ERROR:  permission denied for table atest5
 INSERT INTO atest5(three) VALUES (4) ON CONFLICT (two) DO UPDATE set three = 10; -- fails (due to INSERT)
 ERROR:  permission denied for table atest5
+-- Check that column level privileges are enforced for ON CONFLICT ... WHERE
+-- Ok. we may select one
+INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT WHERE atest5.one = 1 RETURNING atest5.two;
+ two 
+-----
+   2
+(1 row)
+
+-- Error. No select rights on three
+INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT WHERE atest5.three = 1 RETURNING atest5.two;
+ERROR:  permission denied for table atest5
+-- Check that ON CONFLICT ... SELECT FOR UPDATE/SHARE requires an updatable column
+SET SESSION AUTHORIZATION regress_priv_user1;
+REVOKE UPDATE (three) ON atest5 FROM regress_priv_user4;
+SET SESSION AUTHORIZATION regress_priv_user4;
+INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT FOR UPDATE RETURNING atest5.two; -- fails
+ERROR:  permission denied for table atest5
+SET SESSION AUTHORIZATION regress_priv_user1;
+GRANT UPDATE (three) ON atest5 TO regress_priv_user4;
+SET SESSION AUTHORIZATION regress_priv_user4;
+INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT FOR UPDATE RETURNING atest5.two; -- ok
+ two 
+-----
+   2
+(1 row)
+
 -- Check that the columns in the inference require select privileges
 INSERT INTO atest5(four) VALUES (4); -- fail
 ERROR:  permission denied for table atest5
index e17f9188dfa9c31b854d9c4ba83bb7d2a3531ebf..07d93e7def1ddf11a156178902ddf68bfc6f991b 100644 (file)
@@ -218,6 +218,50 @@ NOTICE:  SELECT USING on rls_test_tgt.(3,"tgt d","TGT D")
  3 | tgt d | TGT D
 (1 row)
 
+ROLLBACK;
+-- INSERT ... ON CONFLICT DO SELECT should apply INSERT CHECK and SELECT USING
+-- policy clauses to values proposed for insert. In the event of a conflict it
+-- should also apply SELECT USING policy clauses to the existing values.
+BEGIN;
+INSERT INTO rls_test_tgt VALUES (4, 'tgt a') ON CONFLICT (a) DO SELECT RETURNING *;
+NOTICE:  INSERT CHECK on rls_test_tgt.(4,"tgt a","TGT A")
+NOTICE:  SELECT USING on rls_test_tgt.(4,"tgt a","TGT A")
+ a |   b   |   c   
+---+-------+-------
+ 4 | tgt a | TGT A
+(1 row)
+
+INSERT INTO rls_test_tgt VALUES (4, 'tgt b') ON CONFLICT (a) DO SELECT RETURNING *;
+NOTICE:  INSERT CHECK on rls_test_tgt.(4,"tgt b","TGT B")
+NOTICE:  SELECT USING on rls_test_tgt.(4,"tgt b","TGT B")
+NOTICE:  SELECT USING on rls_test_tgt.(4,"tgt a","TGT A")
+ a |   b   |   c   
+---+-------+-------
+ 4 | tgt a | TGT A
+(1 row)
+
+ROLLBACK;
+-- INSERT ... ON CONFLICT DO SELECT FOR UPDATE should also apply UPDATE USING
+-- policy clauses to the existing values, in the event of a conflict.
+BEGIN;
+INSERT INTO rls_test_tgt VALUES (5, 'tgt a') ON CONFLICT (a) DO SELECT FOR UPDATE RETURNING *;
+NOTICE:  INSERT CHECK on rls_test_tgt.(5,"tgt a","TGT A")
+NOTICE:  SELECT USING on rls_test_tgt.(5,"tgt a","TGT A")
+ a |   b   |   c   
+---+-------+-------
+ 5 | tgt a | TGT A
+(1 row)
+
+INSERT INTO rls_test_tgt VALUES (5, 'tgt b') ON CONFLICT (a) DO SELECT FOR UPDATE RETURNING *;
+NOTICE:  INSERT CHECK on rls_test_tgt.(5,"tgt b","TGT B")
+NOTICE:  SELECT USING on rls_test_tgt.(5,"tgt b","TGT B")
+NOTICE:  UPDATE USING on rls_test_tgt.(5,"tgt a","TGT A")
+NOTICE:  SELECT USING on rls_test_tgt.(5,"tgt a","TGT A")
+ a |   b   |   c   
+---+-------+-------
+ 5 | tgt a | TGT A
+(1 row)
+
 ROLLBACK;
 -- MERGE should always apply SELECT USING policy clauses to both source and
 -- target rows
@@ -2395,10 +2439,58 @@ INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel')
     ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol';
 ERROR:  new row violates row-level security policy for table "document"
 --
+-- INSERT ... ON CONFLICT DO SELECT and Row-level security
+--
+SET SESSION AUTHORIZATION regress_rls_alice;
+DROP POLICY p3_with_all ON document;
+CREATE POLICY p1_select_novels ON document FOR SELECT
+  USING (cid = (SELECT cid from category WHERE cname = 'novel'));
+CREATE POLICY p2_insert_own ON document FOR INSERT
+  WITH CHECK (dauthor = current_user);
+CREATE POLICY p3_update_novels ON document FOR UPDATE
+  USING (cid = (SELECT cid from category WHERE cname = 'novel') AND dlevel = 1)
+  WITH CHECK (dauthor = current_user);
+SET SESSION AUTHORIZATION regress_rls_bob;
+-- DO SELECT requires SELECT rights, should succeed for novel
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+    ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle;
+ did |     dauthor     |     dtitle     
+-----+-----------------+----------------
+   1 | regress_rls_bob | my first novel
+(1 row)
+
+-- DO SELECT requires SELECT rights, should fail for non-novel
+INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'science fiction'), 1, 'regress_rls_bob', 'another sci-fi')
+    ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle;
+ERROR:  new row violates row-level security policy for table "document"
+-- DO SELECT with WHERE and EXCLUDED reference
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+    ON CONFLICT (did) DO SELECT WHERE excluded.dlevel = 1 RETURNING did, dauthor, dtitle;
+ did |     dauthor     |     dtitle     
+-----+-----------------+----------------
+   1 | regress_rls_bob | my first novel
+(1 row)
+
+-- DO SELECT FOR UPDATE requires both SELECT and UPDATE rights, should succeed for novel and dlevel = 1
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+    ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle;
+ did |     dauthor     |     dtitle     
+-----+-----------------+----------------
+   1 | regress_rls_bob | my first novel
+(1 row)
+
+-- should fail UPDATE USING policy for novel with dlevel = 2
+INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+    ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle;
+ERROR:  new row violates row-level security policy (USING expression) for table "document"
+SET SESSION AUTHORIZATION regress_rls_alice;
+DROP POLICY p1_select_novels ON document;
+DROP POLICY p2_insert_own ON document;
+DROP POLICY p3_update_novels ON document;
+--
 -- MERGE
 --
 RESET SESSION AUTHORIZATION;
-DROP POLICY p3_with_all ON document;
 ALTER TABLE document ADD COLUMN dnotes text DEFAULT '';
 -- all documents are readable
 CREATE POLICY p1 ON document FOR SELECT USING (true);
index f9bc213e5a106bc523a73248ea9eaba02454eb9a..78a37d9fc8f8c2f07a5e7fb245e2865a3ac99c6a 100644 (file)
@@ -3608,6 +3608,61 @@ SELECT * FROM hat_data WHERE hat_name IN ('h8', 'h9', 'h7') ORDER BY hat_name;
 (3 rows)
 
 DROP RULE hat_upsert ON hats;
+-- DO SELECT with a WHERE clause
+CREATE RULE hat_confsel AS ON INSERT TO hats
+    DO INSTEAD
+    INSERT INTO hat_data VALUES (
+           NEW.hat_name,
+           NEW.hat_color)
+        ON CONFLICT (hat_name)
+        DO SELECT FOR UPDATE
+           WHERE excluded.hat_color <>  'forbidden' AND hat_data.* != excluded.*
+        RETURNING *;
+SELECT definition FROM pg_rules WHERE tablename = 'hats' ORDER BY rulename;
+                                      definition                                      
+--------------------------------------------------------------------------------------
+ CREATE RULE hat_confsel AS                                                          +
+     ON INSERT TO public.hats DO INSTEAD  INSERT INTO hat_data (hat_name, hat_color) +
+   VALUES (new.hat_name, new.hat_color) ON CONFLICT(hat_name) DO SELECT FOR UPDATE   +
+   WHERE ((excluded.hat_color <> 'forbidden'::bpchar) AND (hat_data.* <> excluded.*))+
+   RETURNING hat_data.hat_name,                                                      +
+     hat_data.hat_color;
+(1 row)
+
+-- fails without RETURNING
+INSERT INTO hats VALUES ('h7', 'blue');
+ERROR:  ON CONFLICT DO SELECT requires a RETURNING clause
+DETAIL:  A rule action is INSERT ... ON CONFLICT DO SELECT, which requires a RETURNING clause.
+-- works (returns conflicts)
+EXPLAIN (costs off)
+INSERT INTO hats VALUES ('h7', 'blue') RETURNING *;
+                                           QUERY PLAN                                            
+-------------------------------------------------------------------------------------------------
+ Insert on hat_data
+   Conflict Resolution: SELECT FOR UPDATE
+   Conflict Arbiter Indexes: hat_data_unique_idx
+   Conflict Filter: ((excluded.hat_color <> 'forbidden'::bpchar) AND (hat_data.* <> excluded.*))
+   ->  Result
+(5 rows)
+
+INSERT INTO hats VALUES ('h7', 'blue') RETURNING *;
+  hat_name  | hat_color  
+------------+------------
+ h7         | black     
+(1 row)
+
+-- conflicts excluded by WHERE clause
+INSERT INTO hats VALUES ('h7', 'forbidden') RETURNING *;
+ hat_name | hat_color 
+----------+-----------
+(0 rows)
+
+INSERT INTO hats VALUES ('h7', 'black') RETURNING *;
+ hat_name | hat_color 
+----------+-----------
+(0 rows)
+
+DROP RULE hat_confsel ON hats;
 drop table hats;
 drop table hat_data;
 -- test for pg_get_functiondef properly regurgitating SET parameters
index 1acdd12d29eaa19cc27018d8a7e8263a257f1518..98dee63b50a7195339f431bdb58f696474c76b8d 100644 (file)
@@ -1670,7 +1670,7 @@ drop table trigger_ddl_table;
 drop function trigger_ddl_func();
 --
 -- Verify behavior of before and after triggers with INSERT...ON CONFLICT
--- DO UPDATE
+-- DO UPDATE and DO SELECT
 --
 create table upsert (key int4 primary key, color text);
 create function upsert_before_func()
@@ -1745,6 +1745,14 @@ insert into upsert values(8, 'yellow') on conflict (key) do update set color = '
 WARNING:  before insert (new): (8,yellow)
 WARNING:  before insert (new, modified): (9,"yellow trig modified")
 WARNING:  after insert (new): (9,"yellow trig modified")
+insert into upsert values(8, 'blue') on conflict (key) do select for update where upsert.color = 'yellow trig modified' returning old.*, new.*, upsert.*;
+WARNING:  before insert (new): (8,blue)
+WARNING:  before insert (new, modified): (9,"blue trig modified")
+ key |        color         | key |        color         | key |        color         
+-----+----------------------+-----+----------------------+-----+----------------------
+   9 | yellow trig modified |   9 | yellow trig modified |   9 | yellow trig modified
+(1 row)
+
 select * from upsert;
  key |            color            
 -----+-----------------------------
index 03df7e75b7b6eab20fb04daa729f478a362f0fff..9cea538b8e802efe0dee24b9acdba641615e2215 100644 (file)
@@ -316,6 +316,21 @@ SELECT * FROM rw_view15;
   3 | UNSPECIFIED
 (6 rows)
 
+INSERT INTO rw_view15 (a) VALUES (3)
+  ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.upper = 'UNSPECIFIED'
+  RETURNING old, new;
+       old       |       new       
+-----------------+-----------------
+ (3,UNSPECIFIED) | (3,UNSPECIFIED)
+(1 row)
+
+INSERT INTO rw_view15 (a) VALUES (3)
+  ON CONFLICT (a) DO SELECT WHERE excluded.upper = 'UNSPECIFIED' RETURNING old, new;
+       old       |       new       
+-----------------+-----------------
+ (3,UNSPECIFIED) | (3,UNSPECIFIED)
+(1 row)
+
 SELECT * FROM rw_view15;
  a  |    upper    
 ----+-------------
@@ -3646,7 +3661,7 @@ ERROR:  new row violates check option for view "wcowrtest_v2"
 DETAIL:  Failing row contains (2, no such row in sometable).
 drop view wcowrtest_v, wcowrtest_v2;
 drop table wcowrtest, sometable;
--- Check INSERT .. ON CONFLICT DO UPDATE works correctly when the view's
+-- Check INSERT .. ON CONFLICT DO SELECT/UPDATE works correctly when the view's
 -- columns are named and ordered differently than the underlying table's.
 create table uv_iocu_tab (a text unique, b float);
 insert into uv_iocu_tab values ('xyxyxy', 0);
@@ -3668,6 +3683,13 @@ select * from uv_iocu_tab;
  xyxyxy | 1
 (1 row)
 
+insert into uv_iocu_view (a, b) values ('xyxyxy', 1)
+   on conflict (a) do select where uv_iocu_view.c = 2 and excluded.c = 2 returning *;
+ b | c |   a    | two 
+---+---+--------+-----
+ 1 | 2 | xyxyxy | 2.0
+(1 row)
+
 -- OK to access view columns that are not present in underlying base
 -- relation in the ON CONFLICT portion of the query
 insert into uv_iocu_view (a, b) values ('xyxyxy', 3)
@@ -3731,6 +3753,25 @@ select * from uv_iocu_view;
  Rejected: (y,1,"(1,y)") |  1 | (1,"Rejected: (y,1,""(1,y)"")")
 (1 row)
 
+explain (costs off)
+insert into uv_iocu_view (aa,bb) values (1,'Rejected: (y,1,"(1,y)")')
+   on conflict (aa) do select where uv_iocu_view.* = excluded.* returning *;
+                                                           QUERY PLAN                                                            
+---------------------------------------------------------------------------------------------------------------------------------
+ Insert on uv_iocu_tab
+   Conflict Resolution: SELECT
+   Conflict Arbiter Indexes: uv_iocu_tab_a_key
+   Conflict Filter: (ROW(uv_iocu_tab.b, uv_iocu_tab.a, (uv_iocu_tab.*)::text) = ROW(excluded.b, excluded.a, (excluded.*)::text))
+   ->  Result
+(5 rows)
+
+insert into uv_iocu_view (aa,bb) values (1,'Rejected: (y,1,"(1,y)")')
+   on conflict (aa) do select where uv_iocu_view.* = excluded.* returning *;
+           bb            | aa |               cc                
+-------------------------+----+---------------------------------
+ Rejected: (y,1,"(1,y)") |  1 | (1,"Rejected: (y,1,""(1,y)"")")
+(1 row)
+
 -- Test omitting a column of the base relation
 delete from uv_iocu_view;
 insert into uv_iocu_view (aa,bb) values (1,'x');
@@ -3751,6 +3792,13 @@ select * from uv_iocu_view;
  Rejected: ("table default",1,"(1,""table default"")") |  1 | (1,"Rejected: (""table default"",1,""(1,""""table default"""")"")")
 (1 row)
 
+insert into uv_iocu_view (aa) values (1)
+   on conflict (aa) do select returning *;
+                          bb                           | aa |                                 cc                                  
+-------------------------------------------------------+----+---------------------------------------------------------------------
+ Rejected: ("table default",1,"(1,""table default"")") |  1 | (1,"Rejected: (""table default"",1,""(1,""""table default"""")"")")
+(1 row)
+
 alter view uv_iocu_view alter column bb set default 'view default';
 insert into uv_iocu_view (aa) values (1)
    on conflict (aa) do update set bb = 'Rejected: '||excluded.*;
@@ -3760,6 +3808,13 @@ select * from uv_iocu_view;
  Rejected: ("view default",1,"(1,""view default"")") |  1 | (1,"Rejected: (""view default"",1,""(1,""""view default"""")"")")
 (1 row)
 
+insert into uv_iocu_view (aa) values (1)
+   on conflict (aa) do select returning *;
+                         bb                          | aa |                                cc                                 
+-----------------------------------------------------+----+-------------------------------------------------------------------
+ Rejected: ("view default",1,"(1,""view default"")") |  1 | (1,"Rejected: (""view default"",1,""(1,""""view default"""")"")")
+(1 row)
+
 -- Should fail to update non-updatable columns
 insert into uv_iocu_view (aa) values (1)
    on conflict (aa) do update set cc = 'XXX';
@@ -3767,7 +3822,7 @@ ERROR:  cannot insert into column "cc" of view "uv_iocu_view"
 DETAIL:  View columns that are not columns of their base relation are not updatable.
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
--- ON CONFLICT DO UPDATE permissions checks
+-- ON CONFLICT DO SELECT/UPDATE permissions checks
 create user regress_view_user1;
 create user regress_view_user2;
 set session authorization regress_view_user1;
@@ -3791,6 +3846,16 @@ insert into rw_view1 values ('zzz',2.0,1)
 insert into rw_view1 values ('zzz',2.0,1)
   on conflict (aa) do update set cc = 3.0; -- Not allowed
 ERROR:  permission denied for view rw_view1
+insert into rw_view1 values ('yyy',2.0,1)
+  on conflict (aa) do select for update returning cc; -- Not allowed
+ERROR:  permission denied for view rw_view1
+insert into rw_view1 values ('yyy',2.0,1)
+  on conflict (aa) do select for update returning aa, bb;
+ aa |   bb   
+----+--------
+  1 | yyyxxx
+(1 row)
+
 reset session authorization;
 select * from base_tbl;
  a |   b    | c 
@@ -3807,9 +3872,19 @@ create view rw_view2 as select b as bb, c as cc, a as aa from base_tbl;
 insert into rw_view2 (aa,bb) values (1,'xxx')
   on conflict (aa) do update set bb = excluded.bb; -- Not allowed
 ERROR:  permission denied for table base_tbl
+insert into rw_view2 (aa,bb) values (1,'xxx')
+  on conflict (aa) do select returning 1;          -- Not allowed
+ERROR:  permission denied for table base_tbl
 create view rw_view3 as select b as bb, a as aa from base_tbl;
 insert into rw_view3 (aa,bb) values (1,'xxx')
   on conflict (aa) do update set bb = excluded.bb; -- OK
+insert into rw_view3 (aa,bb) values (1,'xxx')
+  on conflict (aa) do select returning aa, bb;     -- OK
+ aa | bb  
+----+-----
+  1 | xxx
+(1 row)
+
 reset session authorization;
 select * from base_tbl;
  a |  b  | c 
@@ -3822,6 +3897,9 @@ create view rw_view4 as select aa, bb, cc FROM rw_view1;
 insert into rw_view4 (aa,bb) values (1,'yyy')
   on conflict (aa) do update set bb = excluded.bb; -- Not allowed
 ERROR:  permission denied for view rw_view1
+insert into rw_view4 (aa,bb) values (1,'yyy')
+  on conflict (aa) do select returning 1; -- Not allowed
+ERROR:  permission denied for view rw_view1
 create view rw_view5 as select aa, bb FROM rw_view1;
 insert into rw_view5 (aa,bb) values (1,'yyy')
   on conflict (aa) do update set bb = excluded.bb; -- OK
index 1e9989698b68669080b5eebcdacc8a12ab1bfeb4..b7f6efdd8145feab7dadc12fe31ccd3a7d7121c6 100644 (file)
@@ -568,6 +568,9 @@ INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
 -- fail, because DO UPDATE variant requires unique index
 INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
   ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO UPDATE SET c2 = EXCLUDED.c2;
+-- fail, because DO SELECT variant requires unique index
+INSERT INTO circles VALUES('<(20,20), 10>', '<(0,0), 4>')
+  ON CONFLICT ON CONSTRAINT circles_c1_c2_excl DO SELECT RETURNING *;
 -- succeed because c1 doesn't overlap
 INSERT INTO circles VALUES('<(20,20), 1>', '<(0,0), 5>');
 -- succeed because c2 doesn't overlap
index 03b1f0e44b057617f08a821433f55535487c2549..a5a84d1d4b8d37af55ef6fdbb8a08bb6d4a87cce 100644 (file)
@@ -93,6 +93,9 @@ explain (costs off) insert into insertconflicttest values (0, 'Bilberry') on con
 explain (costs off) insert into insertconflicttest values(0, 'Crowberry') on conflict (key) do update set fruit = excluded.fruit where excluded.fruit != 'Elderberry';
 -- Does the same, but JSON format shows "Conflict Arbiter Index" as JSON array:
 explain (costs off, format json) insert into insertconflicttest values (0, 'Bilberry') on conflict (key) do update set fruit = excluded.fruit where insertconflicttest.fruit != 'Lime' returning *;
+-- Should display lock strength, if specified
+explain (costs off) insert into insertconflicttest values (1, 'Apple') on conflict (key) do select returning *;
+explain (costs off) insert into insertconflicttest values (1, 'Apple') on conflict (key) do select for key share returning *;
 
 -- Fails (no unique index inference specification, required for do update variant):
 insert into insertconflicttest values (1, 'Apple') on conflict do update set fruit = excluded.fruit;
@@ -130,6 +133,18 @@ insert into insertconflicttest AS ict values (6, 'Passionfruit') on conflict (ke
 -- Check helpful hint when qualifying set column with target table
 insert into insertconflicttest values (3, 'Kiwi') on conflict (key, fruit) do update set insertconflicttest.fruit = 'Mango';
 
+--
+-- DO SELECT tests
+--
+delete from insertconflicttest where fruit = 'Apple';
+insert into insertconflicttest values (1, 'Apple') on conflict (key) do select; -- fails
+insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select returning old, new, i;
+insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select returning old, new, i;
+insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select where i.fruit = 'Apple' returning *;
+insert into insertconflicttest as i values (1, 'Apple') on conflict (key) do select where i.fruit = 'Orange' returning *;
+insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select where excluded.fruit = 'Apple' returning *;
+insert into insertconflicttest as i values (1, 'Orange') on conflict (key) do select where excluded.fruit = 'Orange' returning *;
+
 drop index key_index;
 
 --
@@ -459,6 +474,30 @@ begin transaction isolation level serializable;
 insert into selfconflict values (6,1), (6,2) on conflict(f1) do update set f2 = 0;
 commit;
 
+begin transaction isolation level read committed;
+insert into selfconflict values (7,1), (7,2) on conflict(f1) do select returning *;
+commit;
+
+begin transaction isolation level repeatable read;
+insert into selfconflict values (8,1), (8,2) on conflict(f1) do select returning *;
+commit;
+
+begin transaction isolation level serializable;
+insert into selfconflict values (9,1), (9,2) on conflict(f1) do select returning *;
+commit;
+
+begin transaction isolation level read committed;
+insert into selfconflict values (10,1), (10,2) on conflict(f1) do select for update returning *;
+commit;
+
+begin transaction isolation level repeatable read;
+insert into selfconflict values (11,1), (11,2) on conflict(f1) do select for update returning *;
+commit;
+
+begin transaction isolation level serializable;
+insert into selfconflict values (12,1), (12,2) on conflict(f1) do select for update returning *;
+commit;
+
 select * from selfconflict;
 
 drop table selfconflict;
@@ -473,13 +512,17 @@ insert into parted_conflict_test values (1, 'a') on conflict do nothing;
 -- index on a required, which does exist in parent
 insert into parted_conflict_test values (1, 'a') on conflict (a) do nothing;
 insert into parted_conflict_test values (1, 'a') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test values (1, 'a') on conflict (a) do select returning *;
+insert into parted_conflict_test values (1, 'a') on conflict (a) do select for update returning *;
 
 -- targeting partition directly will work
 insert into parted_conflict_test_1 values (1, 'a') on conflict (a) do nothing;
 insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test_1 values (1, 'b') on conflict (a) do select returning b;
 
 -- index on b required, which doesn't exist in parent
-insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a;
+insert into parted_conflict_test values (2, 'b') on conflict (b) do update set a = excluded.a; -- fail
+insert into parted_conflict_test values (2, 'b') on conflict (b) do select returning b; -- fail
 
 -- targeting partition directly will work
 insert into parted_conflict_test_1 values (2, 'b') on conflict (b) do update set a = excluded.a;
@@ -487,13 +530,16 @@ insert into parted_conflict_test_1 values (2, 'b') on conflict (b) do update set
 -- should see (2, 'b')
 select * from parted_conflict_test order by a;
 
--- now check that DO UPDATE works correctly for target partition with
--- different attribute numbers
+-- now check that DO UPDATE and DO SELECT work correctly for target partition
+-- with different attribute numbers
 create table parted_conflict_test_2 (b char, a int unique);
 alter table parted_conflict_test attach partition parted_conflict_test_2 for values in (3);
 truncate parted_conflict_test;
 insert into parted_conflict_test values (3, 'a') on conflict (a) do update set b = excluded.b;
 insert into parted_conflict_test values (3, 'b') on conflict (a) do update set b = excluded.b;
+insert into parted_conflict_test values (3, 'a') on conflict (a) do select returning b;
+insert into parted_conflict_test values (3, 'a') on conflict (a) do select where excluded.b = 'a' returning parted_conflict_test;
+insert into parted_conflict_test values (3, 'a') on conflict (a) do select where parted_conflict_test.b = 'b' returning b;
 
 -- should see (3, 'b')
 select * from parted_conflict_test order by a;
@@ -504,6 +550,7 @@ create table parted_conflict_test_3 partition of parted_conflict_test for values
 truncate parted_conflict_test;
 insert into parted_conflict_test (a, b) values (4, 'a') on conflict (a) do update set b = excluded.b;
 insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a';
+insert into parted_conflict_test (a, b) values (4, 'b') on conflict (a) do select returning b;
 
 -- should see (4, 'b')
 select * from parted_conflict_test order by a;
@@ -514,6 +561,7 @@ create table parted_conflict_test_4_1 partition of parted_conflict_test_4 for va
 truncate parted_conflict_test;
 insert into parted_conflict_test (a, b) values (5, 'a') on conflict (a) do update set b = excluded.b;
 insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do update set b = excluded.b where parted_conflict_test.b = 'a';
+insert into parted_conflict_test (a, b) values (5, 'b') on conflict (a) do select where parted_conflict_test.b = 'a' returning b;
 
 -- should see (5, 'b')
 select * from parted_conflict_test order by a;
@@ -526,6 +574,26 @@ insert into parted_conflict_test (a, b) values (1, 'b'), (2, 'c'), (4, 'b') on c
 -- should see (1, 'b'), (2, 'a'), (4, 'b')
 select * from parted_conflict_test order by a;
 
+-- test DO SELECT with multiple rows hitting different partitions
+truncate parted_conflict_test;
+insert into parted_conflict_test (a, b) values (1, 'a'), (2, 'b'), (4, 'c');
+insert into parted_conflict_test (a, b) values (1, 'x'), (2, 'y'), (4, 'z')
+  on conflict (a) do select returning *, tableoid::regclass;
+
+-- should see original values (1, 'a'), (2, 'b'), (4, 'c')
+select * from parted_conflict_test order by a;
+
+-- test DO SELECT with WHERE filtering across partitions
+insert into parted_conflict_test (a, b) values (1, 'n') on conflict (a) do select where parted_conflict_test.b = 'a' returning *;
+insert into parted_conflict_test (a, b) values (2, 'n') on conflict (a) do select where parted_conflict_test.b = 'x' returning *;
+
+-- test DO SELECT with EXCLUDED in WHERE across partitions with different layouts
+insert into parted_conflict_test (a, b) values (3, 't') on conflict (a) do select where excluded.b = 't' returning *;
+
+-- test DO SELECT FOR UPDATE across different partition layouts
+insert into parted_conflict_test (a, b) values (1, 'l') on conflict (a) do select for update returning *;
+insert into parted_conflict_test (a, b) values (3, 'l') on conflict (a) do select for update returning *;
+
 drop table parted_conflict_test;
 
 -- test behavior of inserting a conflicting tuple into an intermediate
index 96eff1104d2583fdd80810bafdf9580c290162e0..66e06d91a415401e19ed918f08c700f84639cd87 100644 (file)
@@ -565,6 +565,24 @@ INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set three = EXCLU
 INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set three = EXCLUDED.three;
 INSERT INTO atest5(two) VALUES (6) ON CONFLICT (two) DO UPDATE set one = 8; -- fails (due to UPDATE)
 INSERT INTO atest5(three) VALUES (4) ON CONFLICT (two) DO UPDATE set three = 10; -- fails (due to INSERT)
+-- Check that column level privileges are enforced for ON CONFLICT ... WHERE
+-- Ok. we may select one
+INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT WHERE atest5.one = 1 RETURNING atest5.two;
+-- Error. No select rights on three
+INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT WHERE atest5.three = 1 RETURNING atest5.two;
+
+-- Check that ON CONFLICT ... SELECT FOR UPDATE/SHARE requires an updatable column
+SET SESSION AUTHORIZATION regress_priv_user1;
+REVOKE UPDATE (three) ON atest5 FROM regress_priv_user4;
+SET SESSION AUTHORIZATION regress_priv_user4;
+
+INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT FOR UPDATE RETURNING atest5.two; -- fails
+
+SET SESSION AUTHORIZATION regress_priv_user1;
+GRANT UPDATE (three) ON atest5 TO regress_priv_user4;
+SET SESSION AUTHORIZATION regress_priv_user4;
+
+INSERT INTO atest5(two) VALUES (2) ON CONFLICT (two) DO SELECT FOR UPDATE RETURNING atest5.two; -- ok
 
 -- Check that the columns in the inference require select privileges
 INSERT INTO atest5(four) VALUES (4); -- fail
index fb6502d497f515ac19d60dd58baa46c4193a1e9b..6b3566271df548649e0eeb3deca7821214c004b9 100644 (file)
@@ -142,6 +142,21 @@ INSERT INTO rls_test_tgt VALUES (3, 'tgt a') ON CONFLICT (a) DO UPDATE SET b = '
 INSERT INTO rls_test_tgt VALUES (3, 'tgt c') ON CONFLICT (a) DO UPDATE SET b = 'tgt d' RETURNING *;
 ROLLBACK;
 
+-- INSERT ... ON CONFLICT DO SELECT should apply INSERT CHECK and SELECT USING
+-- policy clauses to values proposed for insert. In the event of a conflict it
+-- should also apply SELECT USING policy clauses to the existing values.
+BEGIN;
+INSERT INTO rls_test_tgt VALUES (4, 'tgt a') ON CONFLICT (a) DO SELECT RETURNING *;
+INSERT INTO rls_test_tgt VALUES (4, 'tgt b') ON CONFLICT (a) DO SELECT RETURNING *;
+ROLLBACK;
+
+-- INSERT ... ON CONFLICT DO SELECT FOR UPDATE should also apply UPDATE USING
+-- policy clauses to the existing values, in the event of a conflict.
+BEGIN;
+INSERT INTO rls_test_tgt VALUES (5, 'tgt a') ON CONFLICT (a) DO SELECT FOR UPDATE RETURNING *;
+INSERT INTO rls_test_tgt VALUES (5, 'tgt b') ON CONFLICT (a) DO SELECT FOR UPDATE RETURNING *;
+ROLLBACK;
+
 -- MERGE should always apply SELECT USING policy clauses to both source and
 -- target rows
 MERGE INTO rls_test_tgt t USING rls_test_src s ON t.a = s.a
@@ -953,11 +968,51 @@ INSERT INTO document VALUES (4, (SELECT cid from category WHERE cname = 'novel')
 INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'my first novel')
     ON CONFLICT (did) DO UPDATE SET dauthor = 'regress_rls_carol';
 
+--
+-- INSERT ... ON CONFLICT DO SELECT and Row-level security
+--
+SET SESSION AUTHORIZATION regress_rls_alice;
+DROP POLICY p3_with_all ON document;
+
+CREATE POLICY p1_select_novels ON document FOR SELECT
+  USING (cid = (SELECT cid from category WHERE cname = 'novel'));
+CREATE POLICY p2_insert_own ON document FOR INSERT
+  WITH CHECK (dauthor = current_user);
+CREATE POLICY p3_update_novels ON document FOR UPDATE
+  USING (cid = (SELECT cid from category WHERE cname = 'novel') AND dlevel = 1)
+  WITH CHECK (dauthor = current_user);
+
+SET SESSION AUTHORIZATION regress_rls_bob;
+
+-- DO SELECT requires SELECT rights, should succeed for novel
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+    ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle;
+
+-- DO SELECT requires SELECT rights, should fail for non-novel
+INSERT INTO document VALUES (33, (SELECT cid from category WHERE cname = 'science fiction'), 1, 'regress_rls_bob', 'another sci-fi')
+    ON CONFLICT (did) DO SELECT RETURNING did, dauthor, dtitle;
+
+-- DO SELECT with WHERE and EXCLUDED reference
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+    ON CONFLICT (did) DO SELECT WHERE excluded.dlevel = 1 RETURNING did, dauthor, dtitle;
+
+-- DO SELECT FOR UPDATE requires both SELECT and UPDATE rights, should succeed for novel and dlevel = 1
+INSERT INTO document VALUES (1, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+    ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle;
+
+-- should fail UPDATE USING policy for novel with dlevel = 2
+INSERT INTO document VALUES (2, (SELECT cid from category WHERE cname = 'novel'), 1, 'regress_rls_bob', 'another novel')
+    ON CONFLICT (did) DO SELECT FOR UPDATE RETURNING did, dauthor, dtitle;
+
+SET SESSION AUTHORIZATION regress_rls_alice;
+DROP POLICY p1_select_novels ON document;
+DROP POLICY p2_insert_own ON document;
+DROP POLICY p3_update_novels ON document;
+
 --
 -- MERGE
 --
 RESET SESSION AUTHORIZATION;
-DROP POLICY p3_with_all ON document;
 
 ALTER TABLE document ADD COLUMN dnotes text DEFAULT '';
 -- all documents are readable
index 3f240bec7b0afb414a2de85a479dbbdfcb546e5c..40f5c16e54035ed83645761bb192c5cbf6458216 100644 (file)
@@ -1205,6 +1205,32 @@ SELECT * FROM hat_data WHERE hat_name IN ('h8', 'h9', 'h7') ORDER BY hat_name;
 
 DROP RULE hat_upsert ON hats;
 
+-- DO SELECT with a WHERE clause
+CREATE RULE hat_confsel AS ON INSERT TO hats
+    DO INSTEAD
+    INSERT INTO hat_data VALUES (
+           NEW.hat_name,
+           NEW.hat_color)
+        ON CONFLICT (hat_name)
+        DO SELECT FOR UPDATE
+           WHERE excluded.hat_color <>  'forbidden' AND hat_data.* != excluded.*
+        RETURNING *;
+SELECT definition FROM pg_rules WHERE tablename = 'hats' ORDER BY rulename;
+
+-- fails without RETURNING
+INSERT INTO hats VALUES ('h7', 'blue');
+
+-- works (returns conflicts)
+EXPLAIN (costs off)
+INSERT INTO hats VALUES ('h7', 'blue') RETURNING *;
+INSERT INTO hats VALUES ('h7', 'blue') RETURNING *;
+
+-- conflicts excluded by WHERE clause
+INSERT INTO hats VALUES ('h7', 'forbidden') RETURNING *;
+INSERT INTO hats VALUES ('h7', 'black') RETURNING *;
+
+DROP RULE hat_confsel ON hats;
+
 drop table hats;
 drop table hat_data;
 
index cc878455ace910cdc1374f1745a3669edad3cacf..ea39817ee3d7f9590079b51a3d1c4f2273ba5e07 100644 (file)
@@ -1148,7 +1148,7 @@ drop function trigger_ddl_func();
 
 --
 -- Verify behavior of before and after triggers with INSERT...ON CONFLICT
--- DO UPDATE
+-- DO UPDATE and DO SELECT
 --
 create table upsert (key int4 primary key, color text);
 
@@ -1197,6 +1197,7 @@ insert into upsert values(5, 'purple') on conflict (key) do update set color = '
 insert into upsert values(6, 'white') on conflict (key) do update set color = 'updated ' || upsert.color;
 insert into upsert values(7, 'pink') on conflict (key) do update set color = 'updated ' || upsert.color;
 insert into upsert values(8, 'yellow') on conflict (key) do update set color = 'updated ' || upsert.color;
+insert into upsert values(8, 'blue') on conflict (key) do select for update where upsert.color = 'yellow trig modified' returning old.*, new.*, upsert.*;
 
 select * from upsert;
 
index c071fffc11639ffc9f616cf00948d24c5cad36cc..1635adde2d4b4be55e01c4762e8a2f75b27b2ae4 100644 (file)
@@ -106,6 +106,12 @@ INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE set a = excluded.
 SELECT * FROM rw_view15;
 INSERT INTO rw_view15 (a) VALUES (3) ON CONFLICT (a) DO UPDATE set upper = 'blarg'; -- fails
 SELECT * FROM rw_view15;
+INSERT INTO rw_view15 (a) VALUES (3)
+  ON CONFLICT (a) DO UPDATE SET a = excluded.a WHERE excluded.upper = 'UNSPECIFIED'
+  RETURNING old, new;
+INSERT INTO rw_view15 (a) VALUES (3)
+  ON CONFLICT (a) DO SELECT WHERE excluded.upper = 'UNSPECIFIED' RETURNING old, new;
+
 SELECT * FROM rw_view15;
 ALTER VIEW rw_view15 ALTER COLUMN upper SET DEFAULT 'NOT SET';
 INSERT INTO rw_view15 (a) VALUES (4); -- should fail
@@ -1850,7 +1856,7 @@ insert into wcowrtest_v2 values (2, 'no such row in sometable');
 drop view wcowrtest_v, wcowrtest_v2;
 drop table wcowrtest, sometable;
 
--- Check INSERT .. ON CONFLICT DO UPDATE works correctly when the view's
+-- Check INSERT .. ON CONFLICT DO SELECT/UPDATE works correctly when the view's
 -- columns are named and ordered differently than the underlying table's.
 create table uv_iocu_tab (a text unique, b float);
 insert into uv_iocu_tab values ('xyxyxy', 0);
@@ -1863,6 +1869,8 @@ select * from uv_iocu_tab;
 insert into uv_iocu_view (a, b) values ('xyxyxy', 1)
    on conflict (a) do update set b = excluded.b;
 select * from uv_iocu_tab;
+insert into uv_iocu_view (a, b) values ('xyxyxy', 1)
+   on conflict (a) do select where uv_iocu_view.c = 2 and excluded.c = 2 returning *;
 
 -- OK to access view columns that are not present in underlying base
 -- relation in the ON CONFLICT portion of the query
@@ -1899,6 +1907,11 @@ insert into uv_iocu_view (aa,bb) values (1,'y')
    and excluded.bb != ''
    and excluded.cc is not null;
 select * from uv_iocu_view;
+explain (costs off)
+insert into uv_iocu_view (aa,bb) values (1,'Rejected: (y,1,"(1,y)")')
+   on conflict (aa) do select where uv_iocu_view.* = excluded.* returning *;
+insert into uv_iocu_view (aa,bb) values (1,'Rejected: (y,1,"(1,y)")')
+   on conflict (aa) do select where uv_iocu_view.* = excluded.* returning *;
 
 -- Test omitting a column of the base relation
 delete from uv_iocu_view;
@@ -1911,11 +1924,15 @@ alter table uv_iocu_tab alter column b set default 'table default';
 insert into uv_iocu_view (aa) values (1)
    on conflict (aa) do update set bb = 'Rejected: '||excluded.*;
 select * from uv_iocu_view;
+insert into uv_iocu_view (aa) values (1)
+   on conflict (aa) do select returning *;
 
 alter view uv_iocu_view alter column bb set default 'view default';
 insert into uv_iocu_view (aa) values (1)
    on conflict (aa) do update set bb = 'Rejected: '||excluded.*;
 select * from uv_iocu_view;
+insert into uv_iocu_view (aa) values (1)
+   on conflict (aa) do select returning *;
 
 -- Should fail to update non-updatable columns
 insert into uv_iocu_view (aa) values (1)
@@ -1924,7 +1941,7 @@ insert into uv_iocu_view (aa) values (1)
 drop view uv_iocu_view;
 drop table uv_iocu_tab;
 
--- ON CONFLICT DO UPDATE permissions checks
+-- ON CONFLICT DO SELECT/UPDATE permissions checks
 create user regress_view_user1;
 create user regress_view_user2;
 
@@ -1948,6 +1965,10 @@ insert into rw_view1 values ('zzz',2.0,1)
   on conflict (aa) do update set bb = rw_view1.bb||'xxx'; -- OK
 insert into rw_view1 values ('zzz',2.0,1)
   on conflict (aa) do update set cc = 3.0; -- Not allowed
+insert into rw_view1 values ('yyy',2.0,1)
+  on conflict (aa) do select for update returning cc; -- Not allowed
+insert into rw_view1 values ('yyy',2.0,1)
+  on conflict (aa) do select for update returning aa, bb;
 reset session authorization;
 select * from base_tbl;
 
@@ -1960,9 +1981,13 @@ set session authorization regress_view_user2;
 create view rw_view2 as select b as bb, c as cc, a as aa from base_tbl;
 insert into rw_view2 (aa,bb) values (1,'xxx')
   on conflict (aa) do update set bb = excluded.bb; -- Not allowed
+insert into rw_view2 (aa,bb) values (1,'xxx')
+  on conflict (aa) do select returning 1;          -- Not allowed
 create view rw_view3 as select b as bb, a as aa from base_tbl;
 insert into rw_view3 (aa,bb) values (1,'xxx')
   on conflict (aa) do update set bb = excluded.bb; -- OK
+insert into rw_view3 (aa,bb) values (1,'xxx')
+  on conflict (aa) do select returning aa, bb;     -- OK
 reset session authorization;
 select * from base_tbl;
 
@@ -1970,6 +1995,8 @@ set session authorization regress_view_user2;
 create view rw_view4 as select aa, bb, cc FROM rw_view1;
 insert into rw_view4 (aa,bb) values (1,'yyy')
   on conflict (aa) do update set bb = excluded.bb; -- Not allowed
+insert into rw_view4 (aa,bb) values (1,'yyy')
+  on conflict (aa) do select returning 1; -- Not allowed
 create view rw_view5 as select aa, bb FROM rw_view1;
 insert into rw_view5 (aa,bb) values (1,'yyy')
   on conflict (aa) do update set bb = excluded.bb; -- OK
index 39c76691c86b3905981c924976475fcf4cb5bd43..6e2d876a40f9b7c5fe3d4ccef7628507fa91eb95 100644 (file)
@@ -1840,9 +1840,9 @@ OldToNewMappingData
 OnCommitAction
 OnCommitItem
 OnConflictAction
+OnConflictActionState
 OnConflictClause
 OnConflictExpr
-OnConflictSetState
 OpClassCacheEnt
 OpExpr
 OpFamilyMember