diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 666eec36d37..d0109ea16f6 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -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 diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 7372921d71f..427516d224b 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -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. diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 23eef6043d8..dac514548ca 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -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; diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c index 8fe4fc57bf6..4698f74f910 100644 --- a/src/backend/executor/execPartition.c +++ b/src/backend/executor/execPartition.c @@ -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; } diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index a87f6f90940..8bc99dd11a9 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -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; } } diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index cb7b23d98d8..fc1afaafc17 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -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); diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out index 72c61de9add..bec31a1af1c 100644 --- a/src/test/regress/expected/update.out +++ b/src/test/regress/expected/update.out @@ -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 diff --git a/src/test/regress/sql/update.sql b/src/test/regress/sql/update.sql index ccaec05926e..95a1b9d5e0b 100644 --- a/src/test/regress/sql/update.sql +++ b/src/test/regress/sql/update.sql @@ -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