aboutsummaryrefslogtreecommitdiff
path: root/src/backend/executor/nodeModifyTable.c
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2021-04-22 11:46:41 -0400
committerTom Lane <tgl@sss.pgh.pa.us>2021-04-22 11:46:41 -0400
commita71cfc56bf6013e3ea1d673acaf73fe7ebbd6bf3 (patch)
treea9e63060e7f10fc03592d40b85c579933ab48e78 /src/backend/executor/nodeModifyTable.c
parent574a1b8fb42a9a74b84137a7600d561b4b2d48a4 (diff)
downloadpostgresql-a71cfc56bf6013e3ea1d673acaf73fe7ebbd6bf3.tar.gz
postgresql-a71cfc56bf6013e3ea1d673acaf73fe7ebbd6bf3.zip
Fix bugs in RETURNING in cross-partition UPDATE cases.
If the source and destination partitions don't have identical rowtypes (for example, one has dropped columns the other lacks), then the planSlot contents will be different because of that. If the query has a RETURNING list that tries to return resjunk columns out of the planSlot, that is columns from tables that were joined to the target table, we'd get errors or wrong answers. That's because we used the RETURNING list generated for the destination partition, which expects a planSlot matching that partition's subplan. The most practical fix seems to be to convert the updated destination tuple back to the source partition's rowtype, and then apply the RETURNING list generated for the source partition. This avoids making fragile assumptions about whether the per-subpartition subplans generated all the resjunk columns in the same order. This has been broken since v11 introduced cross-partition UPDATE. The lack of field complaints shows that non-identical partitions aren't a common case; therefore, don't stress too hard about making the conversion efficient. There's no such bug in HEAD, because commit 86dc90056 got rid of per-target-relation variance in the contents of the planSlot. Hence, patch v11-v13 only. Amit Langote and Etsuro Fujita, small changes by me Discussion: https://postgr.es/m/CA+HiwqE_UK1jTSNrjb8mpTdivzd3dum6mK--xqKq0Y9VmfwWQA@mail.gmail.com
Diffstat (limited to 'src/backend/executor/nodeModifyTable.c')
-rw-r--r--src/backend/executor/nodeModifyTable.c88
1 files changed, 76 insertions, 12 deletions
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 9572ef0690e..cea3c13af1b 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -149,34 +149,40 @@ ExecCheckPlanOutput(Relation resultRel, List *targetList)
/*
* ExecProcessReturning --- evaluate a RETURNING list
*
- * resultRelInfo: current result rel
+ * projectReturning: the projection to evaluate
+ * resultRelOid: result relation's OID
* tupleSlot: slot holding tuple actually inserted/updated/deleted
* planSlot: slot holding tuple returned by top subplan node
*
+ * In cross-partition UPDATE cases, projectReturning and planSlot are as
+ * for the source partition, and tupleSlot must conform to that. But
+ * resultRelOid is for the destination partition.
+ *
* Note: If tupleSlot is NULL, the FDW should have already provided econtext's
* scan tuple.
*
* Returns a slot holding the result tuple
*/
static TupleTableSlot *
-ExecProcessReturning(ResultRelInfo *resultRelInfo,
+ExecProcessReturning(ProjectionInfo *projectReturning,
+ Oid resultRelOid,
TupleTableSlot *tupleSlot,
TupleTableSlot *planSlot)
{
- ProjectionInfo *projectReturning = resultRelInfo->ri_projectReturning;
ExprContext *econtext = projectReturning->pi_exprContext;
/* Make tuple and any needed join variables available to ExecProject */
if (tupleSlot)
econtext->ecxt_scantuple = tupleSlot;
+ else
+ Assert(econtext->ecxt_scantuple);
econtext->ecxt_outertuple = planSlot;
/*
- * RETURNING expressions might reference the tableoid column, so
- * reinitialize tts_tableOid before evaluating them.
+ * RETURNING expressions might reference the tableoid column, so be sure
+ * we expose the desired OID, ie that of the real target relation.
*/
- econtext->ecxt_scantuple->tts_tableOid =
- RelationGetRelid(resultRelInfo->ri_RelationDesc);
+ econtext->ecxt_scantuple->tts_tableOid = resultRelOid;
/* Compute the RETURNING expressions */
return ExecProject(projectReturning);
@@ -368,6 +374,16 @@ ExecComputeStoredGenerated(EState *estate, TupleTableSlot *slot, CmdType cmdtype
* For INSERT, we have to insert the tuple into the target relation
* and insert appropriate tuples into the index relations.
*
+ * slot contains the new tuple value to be stored.
+ * planSlot is the output of the ModifyTable's subplan; we use it
+ * to access "junk" columns that are not going to be stored.
+ * In a cross-partition UPDATE, srcSlot is the slot that held the
+ * updated tuple for the source relation; otherwise it's NULL.
+ *
+ * returningRelInfo is the resultRelInfo for the source relation of a
+ * cross-partition UPDATE; otherwise it's the current result relation.
+ * We use it to process RETURNING lists, for reasons explained below.
+ *
* Returns RETURNING result if any, otherwise NULL.
* ----------------------------------------------------------------
*/
@@ -375,6 +391,8 @@ static TupleTableSlot *
ExecInsert(ModifyTableState *mtstate,
TupleTableSlot *slot,
TupleTableSlot *planSlot,
+ TupleTableSlot *srcSlot,
+ ResultRelInfo *returningRelInfo,
EState *estate,
bool canSetTag)
{
@@ -677,8 +695,45 @@ ExecInsert(ModifyTableState *mtstate,
ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
- if (resultRelInfo->ri_projectReturning)
- result = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ if (returningRelInfo->ri_projectReturning)
+ {
+ /*
+ * In a cross-partition UPDATE with RETURNING, we have to use the
+ * source partition's RETURNING list, because that matches the output
+ * of the planSlot, while the destination partition might have
+ * different resjunk columns. This means we have to map the
+ * destination tuple back to the source's format so we can apply that
+ * RETURNING list. This is expensive, but it should be an uncommon
+ * corner case, so we won't spend much effort on making it fast.
+ *
+ * We assume that we can use srcSlot to hold the re-converted tuple.
+ * Note that in the common case where the child partitions both match
+ * the root's format, previous optimizations will have resulted in
+ * slot and srcSlot being identical, cueing us that there's nothing to
+ * do here.
+ */
+ if (returningRelInfo != resultRelInfo && slot != srcSlot)
+ {
+ Relation srcRelationDesc = returningRelInfo->ri_RelationDesc;
+ AttrMap *map;
+
+ map = build_attrmap_by_name_if_req(RelationGetDescr(resultRelationDesc),
+ RelationGetDescr(srcRelationDesc));
+ if (map)
+ {
+ TupleTableSlot *origSlot = slot;
+
+ slot = execute_attr_map_slot(map, slot, srcSlot);
+ slot->tts_tid = origSlot->tts_tid;
+ slot->tts_tableOid = origSlot->tts_tableOid;
+ free_attrmap(map);
+ }
+ }
+
+ result = ExecProcessReturning(returningRelInfo->ri_projectReturning,
+ RelationGetRelid(resultRelationDesc),
+ slot, planSlot);
+ }
return result;
}
@@ -1027,7 +1082,9 @@ ldelete:;
}
}
- rslot = ExecProcessReturning(resultRelInfo, slot, planSlot);
+ rslot = ExecProcessReturning(resultRelInfo->ri_projectReturning,
+ RelationGetRelid(resultRelationDesc),
+ slot, planSlot);
/*
* Before releasing the target tuple again, make sure rslot has a
@@ -1203,6 +1260,7 @@ lreplace:;
{
bool tuple_deleted;
TupleTableSlot *ret_slot;
+ TupleTableSlot *orig_slot = slot;
TupleTableSlot *epqslot = NULL;
PartitionTupleRouting *proute = mtstate->mt_partition_tuple_routing;
int map_index;
@@ -1309,6 +1367,7 @@ lreplace:;
mtstate->rootResultRelInfo, slot);
ret_slot = ExecInsert(mtstate, slot, planSlot,
+ orig_slot, resultRelInfo,
estate, canSetTag);
/* Revert ExecPrepareTupleRouting's node change. */
@@ -1505,7 +1564,9 @@ lreplace:;
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
- return ExecProcessReturning(resultRelInfo, slot, planSlot);
+ return ExecProcessReturning(resultRelInfo->ri_projectReturning,
+ RelationGetRelid(resultRelationDesc),
+ slot, planSlot);
return NULL;
}
@@ -2154,7 +2215,9 @@ ExecModifyTable(PlanState *pstate)
* ExecProcessReturning by IterateDirectModify, so no need to
* provide it here.
*/
- slot = ExecProcessReturning(resultRelInfo, NULL, planSlot);
+ slot = ExecProcessReturning(resultRelInfo->ri_projectReturning,
+ RelationGetRelid(resultRelInfo->ri_RelationDesc),
+ NULL, planSlot);
estate->es_result_relation_info = saved_resultRelInfo;
return slot;
@@ -2244,6 +2307,7 @@ ExecModifyTable(PlanState *pstate)
slot = ExecPrepareTupleRouting(node, estate, proute,
resultRelInfo, slot);
slot = ExecInsert(node, slot, planSlot,
+ NULL, estate->es_result_relation_info,
estate, node->canSetTag);
/* Revert ExecPrepareTupleRouting's state change. */
if (proute)