]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix mishandling of resjunk columns in ON CONFLICT ... UPDATE tlists.
authorTom Lane <tgl@sss.pgh.pa.us>
Mon, 10 May 2021 15:02:29 +0000 (11:02 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Mon, 10 May 2021 15:02:29 +0000 (11:02 -0400)
It's unusual to have any resjunk columns in an ON CONFLICT ... UPDATE
list, but it can happen when MULTIEXPR_SUBLINK SubPlans are present.
If it happens, the ON CONFLICT UPDATE code path would end up storing
tuples that include the values of the extra resjunk columns.  That's
fairly harmless in the short run, but if new columns are added to
the table then the values would become accessible, possibly leading
to malfunctions if they don't match the datatypes of the new columns.

This had escaped notice through a confluence of missing sanity checks,
including

* There's no cross-check that a tuple presented to heap_insert or
heap_update matches the table rowtype.  While it's difficult to
check that fully at reasonable cost, we can easily add assertions
that there aren't too many columns.

* The output-column-assignment cases in execExprInterp.c lacked
any sanity checks on the output column numbers, which seems like
an oversight considering there are plenty of assertion checks on
input column numbers.  Add assertions there too.

* We failed to apply nodeModifyTable's ExecCheckPlanOutput() to
the ON CONFLICT UPDATE tlist.  That wouldn't have caught this
specific error, since that function is chartered to ignore resjunk
columns; but it sure seems like a bad omission now that we've seen
this bug.

In HEAD, the right way to fix this is to make the processing of
ON CONFLICT UPDATE tlists work the same as regular UPDATE tlists
now do, that is don't add "SET x = x" entries, and use
ExecBuildUpdateProjection to evaluate the tlist and combine it with
old values of the not-set columns.  This adds a little complication
to ExecBuildUpdateProjection, but allows removal of a comparable
amount of now-dead code from the planner.

In the back branches, the most expedient solution seems to be to
(a) use an output slot for the ON CONFLICT UPDATE projection that
actually matches the target table, and then (b) invent a variant of
ExecBuildProjectionInfo that can be told to not store values resulting
from resjunk columns, so it doesn't try to store into nonexistent
columns of the output slot.  (We can't simply ignore the resjunk columns
altogether; they have to be evaluated for MULTIEXPR_SUBLINK to work.)
This works back to v10.  In 9.6, projections work much differently and
we can't cheaply give them such an option.  The 9.6 version of this
patch works by inserting a JunkFilter when it's necessary to get rid
of resjunk columns.

In addition, v11 and up have the reverse problem when trying to
perform ON CONFLICT UPDATE on a partitioned table.  Through a
further oversight, adjust_partition_tlist() discarded resjunk columns
when re-ordering the ON CONFLICT UPDATE tlist to match a partition.
This accidentally prevented the storing-bogus-tuples problem, but
at the cost that MULTIEXPR_SUBLINK cases didn't work, typically
crashing if more than one row has to be updated.  Fix by preserving
resjunk columns in that routine.  (I failed to resist the temptation
to add more assertions there too, and to do some minor code
beautification.)

Per report from Andres Freund.  Back-patch to all supported branches.

Security: CVE-2021-32028

src/backend/access/heap/heapam.c
src/backend/executor/execExpr.c
src/backend/executor/execExprInterp.c
src/backend/executor/execPartition.c
src/backend/executor/nodeModifyTable.c
src/include/executor/executor.h
src/test/regress/expected/update.out
src/test/regress/sql/update.sql

index 666eec36d377d1b6d4cd69f2bc55e321fe0778b2..d0109ea16f66f9e62ef6386e9a315e69c666ac0d 100644 (file)
@@ -1886,6 +1886,10 @@ heap_insert(Relation relation, HeapTuple tup, CommandId cid,
        Buffer          vmbuffer = InvalidBuffer;
        bool            all_visible_cleared = false;
 
+       /* Cheap, simplistic check that the tuple matches the rel's rowtype. */
+       Assert(HeapTupleHeaderGetNatts(tup->t_data) <=
+                  RelationGetNumberOfAttributes(relation));
+
        /*
         * Fill in tuple header fields and toast the tuple if necessary.
         *
@@ -2946,6 +2950,10 @@ heap_update(Relation relation, ItemPointer otid, HeapTuple newtup,
 
        Assert(ItemPointerIsValid(otid));
 
+       /* Cheap, simplistic check that the tuple matches the rel's rowtype. */
+       Assert(HeapTupleHeaderGetNatts(newtup->t_data) <=
+                  RelationGetNumberOfAttributes(relation));
+
        /*
         * Forbid this during a parallel operation, lest it allocate a combocid.
         * Other workers might need that combocid for visibility checks, and we
index 7372921d71f39f579273408fb9cb14723d7b51ff..427516d224bebef8c18802d200aea48ec979f857 100644 (file)
@@ -352,6 +352,32 @@ ExecBuildProjectionInfo(List *targetList,
                                                TupleTableSlot *slot,
                                                PlanState *parent,
                                                TupleDesc inputDesc)
+{
+       return ExecBuildProjectionInfoExt(targetList,
+                                                                         econtext,
+                                                                         slot,
+                                                                         true,
+                                                                         parent,
+                                                                         inputDesc);
+}
+
+/*
+ *             ExecBuildProjectionInfoExt
+ *
+ * As above, with one additional option.
+ *
+ * If assignJunkEntries is true (the usual case), resjunk entries in the tlist
+ * are not handled specially: they are evaluated and assigned to the proper
+ * column of the result slot.  If assignJunkEntries is false, resjunk entries
+ * are evaluated, but their result is discarded without assignment.
+ */
+ProjectionInfo *
+ExecBuildProjectionInfoExt(List *targetList,
+                                                  ExprContext *econtext,
+                                                  TupleTableSlot *slot,
+                                                  bool assignJunkEntries,
+                                                  PlanState *parent,
+                                                  TupleDesc inputDesc)
 {
        ProjectionInfo *projInfo = makeNode(ProjectionInfo);
        ExprState  *state;
@@ -389,7 +415,8 @@ ExecBuildProjectionInfo(List *targetList,
                 */
                if (tle->expr != NULL &&
                        IsA(tle->expr, Var) &&
-                       ((Var *) tle->expr)->varattno > 0)
+                       ((Var *) tle->expr)->varattno > 0 &&
+                       (assignJunkEntries || !tle->resjunk))
                {
                        /* Non-system Var, but how safe is it? */
                        variable = (Var *) tle->expr;
@@ -453,6 +480,10 @@ ExecBuildProjectionInfo(List *targetList,
                        ExecInitExprRec(tle->expr, state,
                                                        &state->resvalue, &state->resnull);
 
+                       /* This makes it easy to discard resjunk results when told to. */
+                       if (!assignJunkEntries && tle->resjunk)
+                               continue;
+
                        /*
                         * Column might be referenced multiple times in upper nodes, so
                         * force value to R/O - but only if it could be an expanded datum.
index 23eef6043d84c67cd85dd81ed3199148f543f67a..dac514548cad04991d6d8865a61d771c836e040e 100644 (file)
@@ -531,6 +531,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
                         * care of at compilation time.  But see EEOP_INNER_VAR comments.
                         */
                        Assert(attnum >= 0 && attnum < innerslot->tts_nvalid);
+                       Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
                        resultslot->tts_values[resultnum] = innerslot->tts_values[attnum];
                        resultslot->tts_isnull[resultnum] = innerslot->tts_isnull[attnum];
 
@@ -547,6 +548,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
                         * care of at compilation time.  But see EEOP_INNER_VAR comments.
                         */
                        Assert(attnum >= 0 && attnum < outerslot->tts_nvalid);
+                       Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
                        resultslot->tts_values[resultnum] = outerslot->tts_values[attnum];
                        resultslot->tts_isnull[resultnum] = outerslot->tts_isnull[attnum];
 
@@ -563,6 +565,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
                         * care of at compilation time.  But see EEOP_INNER_VAR comments.
                         */
                        Assert(attnum >= 0 && attnum < scanslot->tts_nvalid);
+                       Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
                        resultslot->tts_values[resultnum] = scanslot->tts_values[attnum];
                        resultslot->tts_isnull[resultnum] = scanslot->tts_isnull[attnum];
 
@@ -573,6 +576,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
                {
                        int                     resultnum = op->d.assign_tmp.resultnum;
 
+                       Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
                        resultslot->tts_values[resultnum] = state->resvalue;
                        resultslot->tts_isnull[resultnum] = state->resnull;
 
@@ -583,6 +587,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
                {
                        int                     resultnum = op->d.assign_tmp.resultnum;
 
+                       Assert(resultnum >= 0 && resultnum < resultslot->tts_tupleDescriptor->natts);
                        resultslot->tts_isnull[resultnum] = state->resnull;
                        if (!resultslot->tts_isnull[resultnum])
                                resultslot->tts_values[resultnum] =
@@ -2070,8 +2075,10 @@ ExecJustAssignInnerVar(ExprState *state, ExprContext *econtext, bool *isnull)
         *
         * Since we use slot_getattr(), we don't need to implement the FETCHSOME
         * step explicitly, and we also needn't Assert that the attnum is in range
-        * --- slot_getattr() will take care of any problems.
+        * --- slot_getattr() will take care of any problems.  Nonetheless, check
+        * that resultnum is in range.
         */
+       Assert(resultnum >= 0 && resultnum < outslot->tts_tupleDescriptor->natts);
        outslot->tts_values[resultnum] =
                slot_getattr(inslot, attnum, &outslot->tts_isnull[resultnum]);
        return 0;
@@ -2090,6 +2097,7 @@ ExecJustAssignOuterVar(ExprState *state, ExprContext *econtext, bool *isnull)
        CheckOpSlotCompatibility(&state->steps[0], inslot);
 
        /* See comments in ExecJustAssignInnerVar */
+       Assert(resultnum >= 0 && resultnum < outslot->tts_tupleDescriptor->natts);
        outslot->tts_values[resultnum] =
                slot_getattr(inslot, attnum, &outslot->tts_isnull[resultnum]);
        return 0;
@@ -2108,6 +2116,7 @@ ExecJustAssignScanVar(ExprState *state, ExprContext *econtext, bool *isnull)
        CheckOpSlotCompatibility(&state->steps[0], inslot);
 
        /* See comments in ExecJustAssignInnerVar */
+       Assert(resultnum >= 0 && resultnum < outslot->tts_tupleDescriptor->natts);
        outslot->tts_values[resultnum] =
                slot_getattr(inslot, attnum, &outslot->tts_isnull[resultnum]);
        return 0;
index 8fe4fc57bf614ba0042717a59abba19e987e4014..4698f74f91065c64e67667c36db9fa336d1f1ffb 100644 (file)
@@ -788,6 +788,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
                 */
                if (node->onConflictAction == ONCONFLICT_UPDATE)
                {
+                       OnConflictSetState *onconfl = makeNode(OnConflictSetState);
                        TupleConversionMap *map;
 
                        map = leaf_part_rri->ri_PartitionInfo->pi_RootToPartitionMap;
@@ -795,14 +796,14 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
                        Assert(node->onConflictSet != NIL);
                        Assert(rootResultRelInfo->ri_onConflict != NULL);
 
-                       leaf_part_rri->ri_onConflict = makeNode(OnConflictSetState);
+                       leaf_part_rri->ri_onConflict = onconfl;
 
                        /*
                         * Need a separate existing slot for each partition, as the
                         * partition could be of a different AM, even if the tuple
                         * descriptors match.
                         */
-                       leaf_part_rri->ri_onConflict->oc_Existing =
+                       onconfl->oc_Existing =
                                table_slot_create(leaf_part_rri->ri_RelationDesc,
                                                                  &mtstate->ps.state->es_tupleTable);
 
@@ -822,17 +823,16 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
                                 * Projections and where clauses themselves don't store state
                                 * / are independent of the underlying storage.
                                 */
-                               leaf_part_rri->ri_onConflict->oc_ProjSlot =
+                               onconfl->oc_ProjSlot =
                                        rootResultRelInfo->ri_onConflict->oc_ProjSlot;
-                               leaf_part_rri->ri_onConflict->oc_ProjInfo =
+                               onconfl->oc_ProjInfo =
                                        rootResultRelInfo->ri_onConflict->oc_ProjInfo;
-                               leaf_part_rri->ri_onConflict->oc_WhereClause =
+                               onconfl->oc_WhereClause =
                                        rootResultRelInfo->ri_onConflict->oc_WhereClause;
                        }
                        else
                        {
                                List       *onconflset;
-                               TupleDesc       tupDesc;
                                bool            found_whole_row;
 
                                /*
@@ -842,7 +842,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
                                 * pseudo-relation (INNER_VAR), and second to handle the main
                                 * target relation (firstVarno).
                                 */
-                               onconflset = (List *) copyObject((Node *) node->onConflictSet);
+                               onconflset = copyObject(node->onConflictSet);
                                if (part_attnos == NULL)
                                        part_attnos =
                                                convert_tuples_by_name_map(RelationGetDescr(partrel),
@@ -865,20 +865,19 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
                                                                                &found_whole_row);
                                /* We ignore the value of found_whole_row. */
 
-                               /* Finally, adjust this tlist to match the partition. */
+                               /* Finally, reorder the tlist to match the partition. */
                                onconflset = adjust_partition_tlist(onconflset, map);
 
                                /* create the tuple slot for the UPDATE SET projection */
-                               tupDesc = ExecTypeFromTL(onconflset);
-                               leaf_part_rri->ri_onConflict->oc_ProjSlot =
-                                       ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc,
-                                                                                  &TTSOpsVirtual);
+                               onconfl->oc_ProjSlot =
+                                       table_slot_create(partrel,
+                                                                         &mtstate->ps.state->es_tupleTable);
 
                                /* build UPDATE SET projection state */
-                               leaf_part_rri->ri_onConflict->oc_ProjInfo =
-                                       ExecBuildProjectionInfo(onconflset, econtext,
-                                                                                       leaf_part_rri->ri_onConflict->oc_ProjSlot,
-                                                                                       &mtstate->ps, partrelDesc);
+                               onconfl->oc_ProjInfo =
+                                       ExecBuildProjectionInfoExt(onconflset, econtext,
+                                                                                          onconfl->oc_ProjSlot, false,
+                                                                                          &mtstate->ps, partrelDesc);
 
                                /*
                                 * If there is a WHERE clause, initialize state where it will
@@ -907,7 +906,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
                                                                                        RelationGetForm(partrel)->reltype,
                                                                                        &found_whole_row);
                                        /* We ignore the value of found_whole_row. */
-                                       leaf_part_rri->ri_onConflict->oc_WhereClause =
+                                       onconfl->oc_WhereClause =
                                                ExecInitQual((List *) clause, &mtstate->ps);
                                }
                        }
@@ -1498,18 +1497,15 @@ ExecBuildSlotPartitionKeyDescription(Relation rel,
 
 /*
  * adjust_partition_tlist
- *             Adjust the targetlist entries for a given partition to account for
- *             attribute differences between parent and the partition
+ *             Re-order the targetlist entries for a given partition to account for
+ *             column position differences between the parent and the partition.
  *
- * The expressions have already been fixed, but here we fix the list to make
- * target resnos match the partition's attribute numbers.  This results in a
- * copy of the original target list in which the entries appear in resno
- * order, including both the existing entries (that may have their resno
- * changed in-place) and the newly added entries for columns that don't exist
- * in the parent.
+ * The expressions have already been fixed, but we must now re-order the
+ * entries in case the partition has different column order, and possibly
+ * add or remove dummy entries for dropped columns.
  *
- * Scribbles on the input tlist, so callers must make sure to make a copy
- * before passing it to us.
+ * Although a new List is returned, this feels free to scribble on resno
+ * fields of the given tlist, so that should be a working copy.
  */
 static List *
 adjust_partition_tlist(List *tlist, TupleConversionMap *map)
@@ -1518,31 +1514,35 @@ adjust_partition_tlist(List *tlist, TupleConversionMap *map)
        TupleDesc       tupdesc = map->outdesc;
        AttrNumber *attrMap = map->attrMap;
        AttrNumber      attrno;
+       ListCell   *lc;
 
        for (attrno = 1; attrno <= tupdesc->natts; attrno++)
        {
                Form_pg_attribute att_tup = TupleDescAttr(tupdesc, attrno - 1);
+               AttrNumber      parentattrno = attrMap[attrno - 1];
                TargetEntry *tle;
 
-               if (attrMap[attrno - 1] != InvalidAttrNumber)
+               if (parentattrno != InvalidAttrNumber)
                {
-                       Assert(!att_tup->attisdropped);
-
                        /*
                         * Use the corresponding entry from the parent's tlist, adjusting
-                        * the resno the match the partition's attno.
+                        * the resno to match the partition's attno.
                         */
-                       tle = (TargetEntry *) list_nth(tlist, attrMap[attrno - 1] - 1);
+                       Assert(!att_tup->attisdropped);
+                       tle = (TargetEntry *) list_nth(tlist, parentattrno - 1);
+                       Assert(!tle->resjunk);
+                       Assert(tle->resno == parentattrno);
                        tle->resno = attrno;
                }
                else
                {
-                       Const      *expr;
-
                        /*
                         * For a dropped attribute in the partition, generate a dummy
-                        * entry with resno matching the partition's attno.
+                        * entry with resno matching the partition's attno.  This should
+                        * match what expand_targetlist() does.
                         */
+                       Const      *expr;
+
                        Assert(att_tup->attisdropped);
                        expr = makeConst(INT4OID,
                                                         -1,
@@ -1560,6 +1560,18 @@ adjust_partition_tlist(List *tlist, TupleConversionMap *map)
                new_tlist = lappend(new_tlist, tle);
        }
 
+       /* Finally, attach any resjunk entries to the end of the new tlist */
+       foreach(lc, tlist)
+       {
+               TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+               if (tle->resjunk)
+               {
+                       tle->resno = list_length(new_tlist) + 1;
+                       new_tlist = lappend(new_tlist, tle);
+               }
+       }
+
        return new_tlist;
 }
 
index a87f6f9094016e3c0647d51169181459a6ab7994..8bc99dd11a968354c68ac64274c5c40d0961bb35 100644 (file)
@@ -2585,9 +2585,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
         */
        if (node->onConflictAction == ONCONFLICT_UPDATE)
        {
+               OnConflictSetState *onconfl = makeNode(OnConflictSetState);
                ExprContext *econtext;
                TupleDesc       relationDesc;
-               TupleDesc       tupDesc;
 
                /* insert may only have one plan, inheritance is not expanded */
                Assert(nplans == 1);
@@ -2603,10 +2603,10 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
                mtstate->mt_excludedtlist = node->exclRelTlist;
 
                /* create state for DO UPDATE SET operation */
-               resultRelInfo->ri_onConflict = makeNode(OnConflictSetState);
+               resultRelInfo->ri_onConflict = onconfl;
 
                /* initialize slot for the existing tuple */
-               resultRelInfo->ri_onConflict->oc_Existing =
+               onconfl->oc_Existing =
                        table_slot_create(resultRelInfo->ri_RelationDesc,
                                                          &mtstate->ps.state->es_tupleTable);
 
@@ -2616,17 +2616,25 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
                 * into the table, and for RETURNING processing - which may access
                 * system attributes.
                 */
-               tupDesc = ExecTypeFromTL((List *) node->onConflictSet);
-               resultRelInfo->ri_onConflict->oc_ProjSlot =
-                       ExecInitExtraTupleSlot(mtstate->ps.state, tupDesc,
-                                                                  table_slot_callbacks(resultRelInfo->ri_RelationDesc));
+               onconfl->oc_ProjSlot =
+                       table_slot_create(resultRelInfo->ri_RelationDesc,
+                                                         &mtstate->ps.state->es_tupleTable);
+
+               /*
+                * The onConflictSet tlist should already have been adjusted to emit
+                * the table's exact column list.  It could also contain resjunk
+                * columns, which should be evaluated but not included in the
+                * projection result.
+                */
+               ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
+                                                       node->onConflictSet);
 
                /* build UPDATE SET projection state */
-               resultRelInfo->ri_onConflict->oc_ProjInfo =
-                       ExecBuildProjectionInfo(node->onConflictSet, econtext,
-                                                                       resultRelInfo->ri_onConflict->oc_ProjSlot,
-                                                                       &mtstate->ps,
-                                                                       relationDesc);
+               onconfl->oc_ProjInfo =
+                       ExecBuildProjectionInfoExt(node->onConflictSet, econtext,
+                                                                          onconfl->oc_ProjSlot, false,
+                                                                          &mtstate->ps,
+                                                                          relationDesc);
 
                /* initialize state to evaluate the WHERE clause, if any */
                if (node->onConflictWhere)
@@ -2635,7 +2643,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
                        qualexpr = ExecInitQual((List *) node->onConflictWhere,
                                                                        &mtstate->ps);
-                       resultRelInfo->ri_onConflict->oc_WhereClause = qualexpr;
+                       onconfl->oc_WhereClause = qualexpr;
                }
        }
 
index cb7b23d98d846973473bc5899beecc3a13351a61..fc1afaafc17e8227d9a851f0dc9d61a91b883483 100644 (file)
@@ -265,6 +265,12 @@ extern ProjectionInfo *ExecBuildProjectionInfo(List *targetList,
                                                                                           TupleTableSlot *slot,
                                                                                           PlanState *parent,
                                                                                           TupleDesc inputDesc);
+extern ProjectionInfo *ExecBuildProjectionInfoExt(List *targetList,
+                                                                                                 ExprContext *econtext,
+                                                                                                 TupleTableSlot *slot,
+                                                                                                 bool assignJunkEntries,
+                                                                                                 PlanState *parent,
+                                                                                                 TupleDesc inputDesc);
 extern ExprState *ExecPrepareExpr(Expr *node, EState *estate);
 extern ExprState *ExecPrepareQual(List *qual, EState *estate);
 extern ExprState *ExecPrepareCheck(List *qual, EState *estate);
index 72c61de9add2642337199c3a93b16a9312228138..bec31a1af1c8518865734e8c1fc72ae08418b3df 100644 (file)
@@ -199,7 +199,7 @@ SELECT a, b, char_length(c) FROM update_test;
 (4 rows)
 
 -- Test ON CONFLICT DO UPDATE
-INSERT INTO upsert_test VALUES(1, 'Boo');
+INSERT INTO upsert_test VALUES(1, 'Boo'), (3, 'Zoo');
 -- uncorrelated  sub-select:
 WITH aaa AS (SELECT 1 AS a, 'Foo' AS b) INSERT INTO upsert_test
   VALUES (1, 'Bar') ON CONFLICT(a)
@@ -210,22 +210,24 @@ WITH aaa AS (SELECT 1 AS a, 'Foo' AS b) INSERT INTO upsert_test
 (1 row)
 
 -- correlated sub-select:
-INSERT INTO upsert_test VALUES (1, 'Baz') ON CONFLICT(a)
+INSERT INTO upsert_test VALUES (1, 'Baz'), (3, 'Zaz') ON CONFLICT(a)
   DO UPDATE SET (b, a) = (SELECT b || ', Correlated', a from upsert_test i WHERE i.a = upsert_test.a)
   RETURNING *;
  a |        b        
 ---+-----------------
  1 | Foo, Correlated
-(1 row)
+ 3 | Zoo, Correlated
+(2 rows)
 
 -- correlated sub-select (EXCLUDED.* alias):
-INSERT INTO upsert_test VALUES (1, 'Bat') ON CONFLICT(a)
+INSERT INTO upsert_test VALUES (1, 'Bat'), (3, 'Zot') ON CONFLICT(a)
   DO UPDATE SET (b, a) = (SELECT b || ', Excluded', a from upsert_test i WHERE i.a = excluded.a)
   RETURNING *;
  a |             b             
 ---+---------------------------
  1 | Foo, Correlated, Excluded
-(1 row)
+ 3 | Zoo, Correlated, Excluded
+(2 rows)
 
 -- ON CONFLICT using system attributes in RETURNING, testing both the
 -- inserting and updating paths. See bug report at:
@@ -251,6 +253,35 @@ INSERT INTO upsert_test VALUES (2, 'Brox') ON CONFLICT(a)
 
 DROP FUNCTION xid_current();
 DROP TABLE update_test;
+DROP TABLE upsert_test;
+-- Test ON CONFLICT DO UPDATE with partitioned table and non-identical children
+CREATE TABLE upsert_test (
+    a   INT PRIMARY KEY,
+    b   TEXT
+) PARTITION BY LIST (a);
+CREATE TABLE upsert_test_1 PARTITION OF upsert_test FOR VALUES IN (1);
+CREATE TABLE upsert_test_2 (b TEXT, a INT PRIMARY KEY);
+ALTER TABLE upsert_test ATTACH PARTITION upsert_test_2 FOR VALUES IN (2);
+INSERT INTO upsert_test VALUES(1, 'Boo'), (2, 'Zoo');
+-- uncorrelated sub-select:
+WITH aaa AS (SELECT 1 AS a, 'Foo' AS b) INSERT INTO upsert_test
+  VALUES (1, 'Bar') ON CONFLICT(a)
+  DO UPDATE SET (b, a) = (SELECT b, a FROM aaa) RETURNING *;
+ a |  b  
+---+-----
+ 1 | Foo
+(1 row)
+
+-- correlated sub-select:
+WITH aaa AS (SELECT 1 AS ctea, ' Foo' AS cteb) INSERT INTO upsert_test
+  VALUES (1, 'Bar'), (2, 'Baz') ON CONFLICT(a)
+  DO UPDATE SET (b, a) = (SELECT upsert_test.b||cteb, upsert_test.a FROM aaa) RETURNING *;
+ a |    b    
+---+---------
+ 1 | Foo Foo
+ 2 | Zoo Foo
+(2 rows)
+
 DROP TABLE upsert_test;
 ---------------------------
 -- UPDATE with row movement
index ccaec05926e6e7905f9f9bec6dd9575adf59ac05..95a1b9d5e0bfa02b1d9bd4899b25e2a8e2957e53 100644 (file)
@@ -100,17 +100,18 @@ UPDATE update_test t
 SELECT a, b, char_length(c) FROM update_test;
 
 -- Test ON CONFLICT DO UPDATE
-INSERT INTO upsert_test VALUES(1, 'Boo');
+
+INSERT INTO upsert_test VALUES(1, 'Boo'), (3, 'Zoo');
 -- uncorrelated  sub-select:
 WITH aaa AS (SELECT 1 AS a, 'Foo' AS b) INSERT INTO upsert_test
   VALUES (1, 'Bar') ON CONFLICT(a)
   DO UPDATE SET (b, a) = (SELECT b, a FROM aaa) RETURNING *;
 -- correlated sub-select:
-INSERT INTO upsert_test VALUES (1, 'Baz') ON CONFLICT(a)
+INSERT INTO upsert_test VALUES (1, 'Baz'), (3, 'Zaz') ON CONFLICT(a)
   DO UPDATE SET (b, a) = (SELECT b || ', Correlated', a from upsert_test i WHERE i.a = upsert_test.a)
   RETURNING *;
 -- correlated sub-select (EXCLUDED.* alias):
-INSERT INTO upsert_test VALUES (1, 'Bat') ON CONFLICT(a)
+INSERT INTO upsert_test VALUES (1, 'Bat'), (3, 'Zot') ON CONFLICT(a)
   DO UPDATE SET (b, a) = (SELECT b || ', Excluded', a from upsert_test i WHERE i.a = excluded.a)
   RETURNING *;
 
@@ -127,11 +128,33 @@ INSERT INTO upsert_test VALUES (2, 'Brox') ON CONFLICT(a)
   DO UPDATE SET (b, a) = (SELECT b || ', Excluded', a from upsert_test i WHERE i.a = excluded.a)
   RETURNING tableoid::regclass, xmin = xid_current() AS xmin_correct, xmax = xid_current() AS xmax_correct;
 
-
 DROP FUNCTION xid_current();
 DROP TABLE update_test;
 DROP TABLE upsert_test;
 
+-- Test ON CONFLICT DO UPDATE with partitioned table and non-identical children
+
+CREATE TABLE upsert_test (
+    a   INT PRIMARY KEY,
+    b   TEXT
+) PARTITION BY LIST (a);
+
+CREATE TABLE upsert_test_1 PARTITION OF upsert_test FOR VALUES IN (1);
+CREATE TABLE upsert_test_2 (b TEXT, a INT PRIMARY KEY);
+ALTER TABLE upsert_test ATTACH PARTITION upsert_test_2 FOR VALUES IN (2);
+
+INSERT INTO upsert_test VALUES(1, 'Boo'), (2, 'Zoo');
+-- uncorrelated sub-select:
+WITH aaa AS (SELECT 1 AS a, 'Foo' AS b) INSERT INTO upsert_test
+  VALUES (1, 'Bar') ON CONFLICT(a)
+  DO UPDATE SET (b, a) = (SELECT b, a FROM aaa) RETURNING *;
+-- correlated sub-select:
+WITH aaa AS (SELECT 1 AS ctea, ' Foo' AS cteb) INSERT INTO upsert_test
+  VALUES (1, 'Bar'), (2, 'Baz') ON CONFLICT(a)
+  DO UPDATE SET (b, a) = (SELECT upsert_test.b||cteb, upsert_test.a FROM aaa) RETURNING *;
+
+DROP TABLE upsert_test;
+
 
 ---------------------------
 -- UPDATE with row movement