diff options
author | Alvaro Herrera <alvherre@alvh.no-ip.org> | 2022-03-17 11:47:04 +0100 |
---|---|---|
committer | Alvaro Herrera <alvherre@alvh.no-ip.org> | 2022-03-17 11:47:04 +0100 |
commit | 25e777cf8e547d7423d2e1e9da71f98b9414d59e (patch) | |
tree | e6696241a9d8e21fe0a419572cf8b8aab43fd690 /src/backend | |
parent | f2553d43060edb210b36c63187d52a632448e1d2 (diff) | |
download | postgresql-25e777cf8e547d7423d2e1e9da71f98b9414d59e.tar.gz |
Split ExecUpdate and ExecDelete into reusable pieces
Create subroutines ExecUpdatePrologue / ExecUpdateAct /
ExecUpdateEpilogue, and similar for ExecDelete.
Introduce a new struct to be used internally in nodeModifyTable.c,
dubbed ModifyTableContext, which contains all context information needed
to perform these operations, as well as ExecInsert and others.
This allows using a different schedule and a different way of evaluating
the results of these operations, which can be exploited by a later
commit introducing support for MERGE. It also makes ExecUpdate and
ExecDelete proper shorter and (hopefully) simpler.
Author: Álvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Japin Li <japinli@hotmail.com>
Reviewed-by: Zhihong Yu <zyu@yugabyte.com>
Discussion: https://postgr.es/m/202202271724.4z7xv3cf46kv@alvherre.pgsql
Diffstat (limited to 'src/backend')
-rw-r--r-- | src/backend/executor/nodeModifyTable.c | 781 |
1 files changed, 482 insertions, 299 deletions
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 5ec699a9bd..6239abae90 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -20,8 +20,8 @@ * * NOTES * The ModifyTable node receives input from its outerPlan, which is - * the data to insert for INSERT cases, or the changed columns' new - * values plus row-locating info for UPDATE cases, or just the + * the data to insert for INSERT cases, the changed columns' new + * values plus row-locating info for UPDATE and MERGE cases, or just the * row-locating info for DELETE cases. * * If the query specifies RETURNING, then the ModifyTable returns a @@ -60,6 +60,61 @@ typedef struct MTTargetRelLookup int relationIndex; /* rel's index in resultRelInfo[] array */ } MTTargetRelLookup; +/* + * Context struct for a ModifyTable operation, containing basic execution + * state and some output variables populated by ExecUpdateAct() and + * ExecDeleteAct() to report the result of their actions to callers. + */ +typedef struct ModifyTableContext +{ + /* Operation state */ + ModifyTableState *mtstate; + EPQState *epqstate; + EState *estate; + + /* + * Slot containing tuple obtained from ModifyTable's subplan. Used to + * access "junk" columns that are not going to be stored. + */ + TupleTableSlot *planSlot; + + + /* + * Information about the changes that were made concurrently to a tuple + * being updated or deleted + */ + TM_FailureData tmfd; + + /* + * The tuple produced by EvalPlanQual to retry from, if a cross-partition + * UPDATE requires it + */ + TupleTableSlot *cpUpdateRetrySlot; + + /* + * The tuple projected by the INSERT's RETURNING clause, when doing a + * cross-partition UPDATE + */ + TupleTableSlot *cpUpdateReturningSlot; + + /* + * Lock mode to acquire on the latest tuple version before performing + * EvalPlanQual on it + */ + LockTupleMode lockmode; +} ModifyTableContext; + +/* + * Context struct containing output data specific to UPDATE operations. + */ +typedef struct UpdateContext +{ + bool updated; /* did UPDATE actually occur? */ + bool updateIndexes; /* index update required? */ + bool crossPartUpdate; /* was it a cross-partition update? */ +} UpdateContext; + + static void ExecBatchInsert(ModifyTableState *mtstate, ResultRelInfo *resultRelInfo, TupleTableSlot **slots, @@ -67,12 +122,10 @@ static void ExecBatchInsert(ModifyTableState *mtstate, int numSlots, EState *estate, bool canSetTag); -static bool ExecOnConflictUpdate(ModifyTableState *mtstate, +static bool ExecOnConflictUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ItemPointer conflictTid, - TupleTableSlot *planSlot, TupleTableSlot *excludedSlot, - EState *estate, bool canSetTag, TupleTableSlot **returning); static TupleTableSlot *ExecPrepareTupleRouting(ModifyTableState *mtstate, @@ -580,8 +633,6 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo, * 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. * * Returns RETURNING result if any, otherwise NULL. * @@ -591,15 +642,16 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo, * ---------------------------------------------------------------- */ static TupleTableSlot * -ExecInsert(ModifyTableState *mtstate, +ExecInsert(ModifyTableContext *context, ResultRelInfo *resultRelInfo, TupleTableSlot *slot, - TupleTableSlot *planSlot, - EState *estate, bool canSetTag) { + ModifyTableState *mtstate = context->mtstate; + EState *estate = context->estate; Relation resultRelationDesc; List *recheckIndexes = NIL; + TupleTableSlot *planSlot = context->planSlot; TupleTableSlot *result = NULL; TransitionCaptureState *ar_insert_trig_tcs; ModifyTable *node = (ModifyTable *) mtstate->ps.plan; @@ -850,9 +902,9 @@ ExecInsert(ModifyTableState *mtstate, */ TupleTableSlot *returning = NULL; - if (ExecOnConflictUpdate(mtstate, resultRelInfo, - &conflictTid, planSlot, slot, - estate, canSetTag, &returning)) + if (ExecOnConflictUpdate(context, resultRelInfo, + &conflictTid, slot, canSetTag, + &returning)) { InstrCountTuples2(&mtstate->ps, 1); return returning; @@ -1055,6 +1107,90 @@ ExecBatchInsert(ModifyTableState *mtstate, estate->es_processed += numInserted; } +/* + * ExecDeletePrologue -- subroutine for ExecDelete + * + * Prepare executor state for DELETE. Actually, the only thing we have to do + * here is execute BEFORE ROW triggers. We return false if one of them makes + * the delete a no-op; otherwise, return true. + */ +static bool +ExecDeletePrologue(ModifyTableContext *context, ResultRelInfo *resultRelInfo, + ItemPointer tupleid, HeapTuple oldtuple, + TupleTableSlot **epqreturnslot) +{ + /* BEFORE ROW DELETE triggers */ + if (resultRelInfo->ri_TrigDesc && + resultRelInfo->ri_TrigDesc->trig_delete_before_row) + return ExecBRDeleteTriggers(context->estate, context->epqstate, + resultRelInfo, tupleid, oldtuple, + epqreturnslot); + + return true; +} + +/* + * ExecDeleteAct -- subroutine for ExecDelete + * + * Actually delete the tuple from a plain table. + * + * Caller is in charge of doing EvalPlanQual as necessary + */ +static TM_Result +ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo, + ItemPointer tupleid, bool changingPart) +{ + EState *estate = context->estate; + + return table_tuple_delete(resultRelInfo->ri_RelationDesc, tupleid, + estate->es_output_cid, + estate->es_snapshot, + estate->es_crosscheck_snapshot, + true /* wait for commit */ , + &context->tmfd, + changingPart); +} + +/* + * ExecDeleteEpilogue -- subroutine for ExecDelete + * + * Closing steps of tuple deletion; this invokes AFTER FOR EACH ROW triggers, + * including the UPDATE triggers if the deletion is being done as part of a + * cross-partition tuple move. + */ +static void +ExecDeleteEpilogue(ModifyTableContext *context, ResultRelInfo *resultRelInfo, + ItemPointer tupleid, HeapTuple oldtuple) +{ + ModifyTableState *mtstate = context->mtstate; + EState *estate = context->estate; + TransitionCaptureState *ar_delete_trig_tcs; + + /* + * If this delete is the result of a partition key update that moved the + * tuple to a new partition, put this row into the transition OLD TABLE, + * if there is one. We need to do this separately for DELETE and INSERT + * because they happen on different tables. + */ + ar_delete_trig_tcs = mtstate->mt_transition_capture; + if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture && + mtstate->mt_transition_capture->tcs_update_old_table) + { + ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, + NULL, NULL, mtstate->mt_transition_capture); + + /* + * We've already captured the NEW TABLE row, so make sure any AR + * DELETE trigger fired below doesn't capture it again. + */ + ar_delete_trig_tcs = NULL; + } + + /* AFTER ROW DELETE Triggers */ + ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple, + ar_delete_trig_tcs); +} + /* ---------------------------------------------------------------- * ExecDelete * @@ -1072,46 +1208,37 @@ ExecBatchInsert(ModifyTableState *mtstate, * whether the tuple is actually deleted, callers can use it to * decide whether to continue the operation. When this DELETE is a * part of an UPDATE of partition-key, then the slot returned by - * EvalPlanQual() is passed back using output parameter epqslot. + * EvalPlanQual() is passed back using output parameter epqreturnslot. * * Returns RETURNING result if any, otherwise NULL. * ---------------------------------------------------------------- */ static TupleTableSlot * -ExecDelete(ModifyTableState *mtstate, +ExecDelete(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ItemPointer tupleid, HeapTuple oldtuple, - TupleTableSlot *planSlot, - EPQState *epqstate, - EState *estate, bool processReturning, - bool canSetTag, bool changingPart, + bool canSetTag, bool *tupleDeleted, TupleTableSlot **epqreturnslot) { + EState *estate = context->estate; Relation resultRelationDesc = resultRelInfo->ri_RelationDesc; - TM_Result result; - TM_FailureData tmfd; TupleTableSlot *slot = NULL; - TransitionCaptureState *ar_delete_trig_tcs; + TM_Result result; if (tupleDeleted) *tupleDeleted = false; - /* BEFORE ROW DELETE Triggers */ - if (resultRelInfo->ri_TrigDesc && - resultRelInfo->ri_TrigDesc->trig_delete_before_row) - { - bool dodelete; - - dodelete = ExecBRDeleteTriggers(estate, epqstate, resultRelInfo, - tupleid, oldtuple, epqreturnslot); - - if (!dodelete) /* "do nothing" */ - return NULL; - } + /* + * Prepare for the delete. This includes BEFORE ROW triggers, so we're + * done if it says we are. + */ + if (!ExecDeletePrologue(context, resultRelInfo, tupleid, oldtuple, + epqreturnslot)) + return NULL; /* INSTEAD OF ROW DELETE Triggers */ if (resultRelInfo->ri_TrigDesc && @@ -1137,7 +1264,7 @@ ExecDelete(ModifyTableState *mtstate, slot = resultRelInfo->ri_FdwRoutine->ExecForeignDelete(estate, resultRelInfo, slot, - planSlot); + context->planSlot); if (slot == NULL) /* "do nothing" */ return NULL; @@ -1156,20 +1283,14 @@ ExecDelete(ModifyTableState *mtstate, /* * delete the tuple * - * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check - * that the row to be deleted is visible to that snapshot, and throw a - * can't-serialize error if not. This is a special-case behavior - * needed for referential integrity updates in transaction-snapshot - * mode transactions. + * Note: if context->estate->es_crosscheck_snapshot isn't + * InvalidSnapshot, we check that the row to be deleted is visible to + * that snapshot, and throw a can't-serialize error if not. This is a + * special-case behavior needed for referential integrity updates in + * transaction-snapshot mode transactions. */ ldelete:; - result = table_tuple_delete(resultRelationDesc, tupleid, - estate->es_output_cid, - estate->es_snapshot, - estate->es_crosscheck_snapshot, - true /* wait for commit */ , - &tmfd, - changingPart); + result = ExecDeleteAct(context, resultRelInfo, tupleid, changingPart); switch (result) { @@ -1199,7 +1320,7 @@ ldelete:; * can re-execute the DELETE and then return NULL to cancel * the outer delete. */ - if (tmfd.cmax != estate->es_output_cid) + if (context->tmfd.cmax != estate->es_output_cid) ereport(ERROR, (errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION), errmsg("tuple to be deleted was already modified by an operation triggered by the current command"), @@ -1225,8 +1346,8 @@ ldelete:; * Already know that we're going to need to do EPQ, so * fetch tuple directly into the right slot. */ - EvalPlanQualBegin(epqstate); - inputslot = EvalPlanQualSlot(epqstate, resultRelationDesc, + EvalPlanQualBegin(context->epqstate); + inputslot = EvalPlanQualSlot(context->epqstate, resultRelationDesc, resultRelInfo->ri_RangeTableIndex); result = table_tuple_lock(resultRelationDesc, tupleid, @@ -1234,13 +1355,13 @@ ldelete:; inputslot, estate->es_output_cid, LockTupleExclusive, LockWaitBlock, TUPLE_LOCK_FLAG_FIND_LAST_VERSION, - &tmfd); + &context->tmfd); switch (result) { case TM_Ok: - Assert(tmfd.traversed); - epqslot = EvalPlanQual(epqstate, + Assert(context->tmfd.traversed); + epqslot = EvalPlanQual(context->epqstate, resultRelationDesc, resultRelInfo->ri_RangeTableIndex, inputslot); @@ -1273,7 +1394,7 @@ ldelete:; * See also TM_SelfModified response to * table_tuple_delete() above. */ - if (tmfd.cmax != estate->es_output_cid) + if (context->tmfd.cmax != estate->es_output_cid) ereport(ERROR, (errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION), errmsg("tuple to be deleted was already modified by an operation triggered by the current command"), @@ -1336,33 +1457,7 @@ ldelete:; if (tupleDeleted) *tupleDeleted = true; - /* - * If this delete is the result of a partition key update that moved the - * tuple to a new partition, put this row into the transition OLD TABLE, - * if there is one. We need to do this separately for DELETE and INSERT - * because they happen on different tables. - */ - ar_delete_trig_tcs = mtstate->mt_transition_capture; - if (mtstate->operation == CMD_UPDATE && mtstate->mt_transition_capture - && mtstate->mt_transition_capture->tcs_update_old_table) - { - ExecARUpdateTriggers(estate, resultRelInfo, - tupleid, - oldtuple, - NULL, - NULL, - mtstate->mt_transition_capture); - - /* - * We've already captured the NEW TABLE row, so make sure any AR - * DELETE trigger fired below doesn't capture it again. - */ - ar_delete_trig_tcs = NULL; - } - - /* AFTER ROW DELETE Triggers */ - ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple, - ar_delete_trig_tcs); + ExecDeleteEpilogue(context, resultRelInfo, tupleid, oldtuple); /* Process RETURNING if present and if requested */ if (processReturning && resultRelInfo->ri_projectReturning) @@ -1393,7 +1488,7 @@ ldelete:; } } - rslot = ExecProcessReturning(resultRelInfo, slot, planSlot); + rslot = ExecProcessReturning(resultRelInfo, slot, context->planSlot); /* * Before releasing the target tuple again, make sure rslot has a @@ -1427,21 +1522,20 @@ ldelete:; * and call this function again or perform a regular update accordingly. */ static bool -ExecCrossPartitionUpdate(ModifyTableState *mtstate, +ExecCrossPartitionUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ItemPointer tupleid, HeapTuple oldtuple, - TupleTableSlot *slot, TupleTableSlot *planSlot, - EPQState *epqstate, bool canSetTag, - TupleTableSlot **retry_slot, - TupleTableSlot **inserted_tuple) + TupleTableSlot *slot, + bool canSetTag, UpdateContext *updateCxt) { + ModifyTableState *mtstate = context->mtstate; EState *estate = mtstate->ps.state; TupleConversionMap *tupconv_map; bool tuple_deleted; TupleTableSlot *epqslot = NULL; - *inserted_tuple = NULL; - *retry_slot = NULL; + context->cpUpdateReturningSlot = NULL; + context->cpUpdateRetrySlot = NULL; /* * Disallow an INSERT ON CONFLICT DO UPDATE that causes the original row @@ -1488,11 +1582,11 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate, * Row movement, part 1. Delete the tuple, but skip RETURNING processing. * We want to return rows from INSERT. */ - ExecDelete(mtstate, resultRelInfo, tupleid, oldtuple, planSlot, - epqstate, estate, + ExecDelete(context, resultRelInfo, + tupleid, oldtuple, false, /* processReturning */ - false, /* canSetTag */ true, /* changingPart */ + false, /* canSetTag */ &tuple_deleted, &epqslot); /* @@ -1538,8 +1632,9 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate, SnapshotAny, oldSlot)) elog(ERROR, "failed to fetch tuple being updated"); - *retry_slot = ExecGetUpdateNewTuple(resultRelInfo, epqslot, - oldSlot); + /* and project the new tuple to retry the UPDATE with */ + context->cpUpdateRetrySlot = + ExecGetUpdateNewTuple(resultRelInfo, epqslot, oldSlot); return false; } } @@ -1556,8 +1651,8 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate, mtstate->mt_root_tuple_slot); /* Tuple routing starts from the root table. */ - *inserted_tuple = ExecInsert(mtstate, mtstate->rootResultRelInfo, slot, - planSlot, estate, canSetTag); + context->cpUpdateReturningSlot = + ExecInsert(context, mtstate->rootResultRelInfo, slot, canSetTag); /* * Reset the transition state that may possibly have been written by @@ -1570,6 +1665,233 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate, return true; } +/* + * ExecUpdatePrologue -- subroutine for ExecUpdate + * + * Prepare executor state for UPDATE. This includes running BEFORE ROW + * triggers. We return false if one of them makes the update a no-op; + * otherwise, return true. + */ +static bool +ExecUpdatePrologue(ModifyTableContext *context, ResultRelInfo *resultRelInfo, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot) +{ + Relation resultRelationDesc = resultRelInfo->ri_RelationDesc; + + ExecMaterializeSlot(slot); + + /* + * Open the table's indexes, if we have not done so already, so that we + * can add new index entries for the updated tuple. + */ + if (resultRelationDesc->rd_rel->relhasindex && + resultRelInfo->ri_IndexRelationDescs == NULL) + ExecOpenIndices(resultRelInfo, false); + + /* BEFORE ROW UPDATE triggers */ + if (resultRelInfo->ri_TrigDesc && + resultRelInfo->ri_TrigDesc->trig_update_before_row) + return ExecBRUpdateTriggers(context->estate, context->epqstate, + resultRelInfo, tupleid, oldtuple, slot); + + return true; +} + +/* + * ExecUpdatePrepareSlot -- subroutine for ExecUpdate + * + * Apply the final modifications to the tuple slot before the update. + */ +static void +ExecUpdatePrepareSlot(ResultRelInfo *resultRelInfo, + TupleTableSlot *slot, + EState *estate) +{ + Relation resultRelationDesc = resultRelInfo->ri_RelationDesc; + + /* + * Constraints and GENERATED expressions might reference the tableoid + * column, so (re-)initialize tts_tableOid before evaluating them. + */ + slot->tts_tableOid = RelationGetRelid(resultRelationDesc); + + /* + * Compute stored generated columns + */ + if (resultRelationDesc->rd_att->constr && + resultRelationDesc->rd_att->constr->has_generated_stored) + ExecComputeStoredGenerated(resultRelInfo, estate, slot, + CMD_UPDATE); +} + +/* + * ExecUpdateAct -- subroutine for ExecUpdate + * + * Actually update the tuple, when operating on a plain table. If the + * table is a partition, and the command was called referencing an ancestor + * partitioned table, this routine migrates the resulting tuple to another + * partition. + * + * The caller is in charge of keeping indexes current as necessary. The + * caller is also in charge of doing EvalPlanQual if the tuple is found to + * be concurrently updated. However, in case of a cross-partition update, + * this routine does it. + * + * Caller is in charge of doing EvalPlanQual as necessary, and of keeping + * indexes current for the update. + */ +static TM_Result +ExecUpdateAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot, + bool canSetTag, UpdateContext *updateCxt) +{ + EState *estate = context->estate; + Relation resultRelationDesc = resultRelInfo->ri_RelationDesc; + bool partition_constraint_failed; + TM_Result result; + + updateCxt->crossPartUpdate = false; + + /* + * If we generate a new candidate tuple after EvalPlanQual testing, we + * must loop back here and recheck any RLS policies and constraints. (We + * don't need to redo triggers, however. If there are any BEFORE triggers + * then trigger.c will have done table_tuple_lock to lock the correct + * tuple, so there's no need to do them again.) + */ +lreplace:; + + /* ensure slot is independent, consider e.g. EPQ */ + ExecMaterializeSlot(slot); + + /* + * If partition constraint fails, this row might get moved to another + * partition, in which case we should check the RLS CHECK policy just + * before inserting into the new partition, rather than doing it here. + * This is because a trigger on that partition might again change the row. + * So skip the WCO checks if the partition constraint fails. + */ + partition_constraint_failed = + resultRelationDesc->rd_rel->relispartition && + !ExecPartitionCheck(resultRelInfo, slot, estate, false); + + /* Check any RLS UPDATE WITH CHECK policies */ + if (!partition_constraint_failed && + resultRelInfo->ri_WithCheckOptions != NIL) + { + /* + * ExecWithCheckOptions() will skip any WCOs which are not of the kind + * we are looking for at this point. + */ + ExecWithCheckOptions(WCO_RLS_UPDATE_CHECK, + resultRelInfo, slot, estate); + } + + /* + * If a partition check failed, try to move the row into the right + * partition. + */ + if (partition_constraint_failed) + { + /* + * ExecCrossPartitionUpdate will first DELETE the row from the + * partition it's currently in and then insert it back into the root + * table, which will re-route it to the correct partition. However, + * if the tuple has been concurrently updated, a retry is needed. + */ + if (ExecCrossPartitionUpdate(context, resultRelInfo, + tupleid, oldtuple, slot, + canSetTag, updateCxt)) + { + /* success! */ + updateCxt->updated = true; + updateCxt->crossPartUpdate = true; + return TM_Ok; + } + + /* + * ExecCrossPartitionUpdate installed an updated version of the new + * tuple in the retry slot; start over. + */ + slot = context->cpUpdateRetrySlot; + goto lreplace; + } + + /* + * Check the constraints of the tuple. We've already checked the + * partition constraint above; however, we must still ensure the tuple + * passes all other constraints, so we will call ExecConstraints() and + * have it validate all remaining checks. + */ + if (resultRelationDesc->rd_att->constr) + ExecConstraints(resultRelInfo, slot, estate); + + /* + * replace the heap tuple + * + * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check that + * the row to be updated is visible to that snapshot, and throw a + * can't-serialize error if not. This is a special-case behavior needed + * for referential integrity updates in transaction-snapshot mode + * transactions. + */ + result = table_tuple_update(resultRelationDesc, tupleid, slot, + estate->es_output_cid, + estate->es_snapshot, + estate->es_crosscheck_snapshot, + true /* wait for commit */ , + &context->tmfd, &context->lockmode, + &updateCxt->updateIndexes); + if (result == TM_Ok) + updateCxt->updated = true; + + return result; +} + +/* + * ExecUpdateEpilogue -- subroutine for ExecUpdate + * + * Closing steps of updating a tuple. Must be called if ExecUpdateAct + * returns indicating that the tuple was updated. + */ +static void +ExecUpdateEpilogue(ModifyTableContext *context, UpdateContext *updateCxt, + ResultRelInfo *resultRelInfo, ItemPointer tupleid, + HeapTuple oldtuple, TupleTableSlot *slot, + List *recheckIndexes) +{ + ModifyTableState *mtstate = context->mtstate; + + /* insert index entries for tuple if necessary */ + if (resultRelInfo->ri_NumIndices > 0 && updateCxt->updateIndexes) + recheckIndexes = ExecInsertIndexTuples(resultRelInfo, + slot, context->estate, + true, false, + NULL, NIL); + + /* AFTER ROW UPDATE Triggers */ + ExecARUpdateTriggers(context->estate, resultRelInfo, + tupleid, oldtuple, slot, + recheckIndexes, + mtstate->operation == CMD_INSERT ? + mtstate->mt_oc_transition_capture : + mtstate->mt_transition_capture); + + /* + * Check any WITH CHECK OPTION constraints from parent views. We are + * required to do this after testing all constraints and uniqueness + * violations per the SQL spec, so we do it after actually updating the + * record in the heap and all indexes. + * + * ExecWithCheckOptions() will skip any WCOs which are not of the kind we + * are looking for at this point. + */ + if (resultRelInfo->ri_WithCheckOptions != NIL) + ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, + slot, context->estate); +} + + /* ---------------------------------------------------------------- * ExecUpdate * @@ -1598,20 +1920,15 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate, * ---------------------------------------------------------------- */ static TupleTableSlot * -ExecUpdate(ModifyTableState *mtstate, - ResultRelInfo *resultRelInfo, - ItemPointer tupleid, - HeapTuple oldtuple, - TupleTableSlot *slot, - TupleTableSlot *planSlot, - EPQState *epqstate, - EState *estate, +ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, + ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot, bool canSetTag) { + EState *estate = context->estate; Relation resultRelationDesc = resultRelInfo->ri_RelationDesc; - TM_Result result; - TM_FailureData tmfd; + UpdateContext updateCxt = {0}; List *recheckIndexes = NIL; + TM_Result result; /* * abort the operation if not running transactions @@ -1619,24 +1936,12 @@ ExecUpdate(ModifyTableState *mtstate, if (IsBootstrapProcessingMode()) elog(ERROR, "cannot UPDATE during bootstrap"); - ExecMaterializeSlot(slot); - /* - * Open the table's indexes, if we have not done so already, so that we - * can add new index entries for the updated tuple. + * Prepare for the update. This includes BEFORE ROW triggers, so we're + * done if it says we are. */ - if (resultRelationDesc->rd_rel->relhasindex && - resultRelInfo->ri_IndexRelationDescs == NULL) - ExecOpenIndices(resultRelInfo, false); - - /* BEFORE ROW UPDATE Triggers */ - if (resultRelInfo->ri_TrigDesc && - resultRelInfo->ri_TrigDesc->trig_update_before_row) - { - if (!ExecBRUpdateTriggers(estate, epqstate, resultRelInfo, - tupleid, oldtuple, slot)) - return NULL; /* "do nothing" */ - } + if (!ExecUpdatePrologue(context, resultRelInfo, tupleid, oldtuple, slot)) + return NULL; /* INSTEAD OF ROW UPDATE Triggers */ if (resultRelInfo->ri_TrigDesc && @@ -1648,19 +1953,7 @@ ExecUpdate(ModifyTableState *mtstate, } else if (resultRelInfo->ri_FdwRoutine) { - /* - * GENERATED expressions might reference the tableoid column, so - * (re-)initialize tts_tableOid before evaluating them. - */ - slot->tts_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc); - - /* - * Compute stored generated columns - */ - if (resultRelationDesc->rd_att->constr && - resultRelationDesc->rd_att->constr->has_generated_stored) - ExecComputeStoredGenerated(resultRelInfo, estate, slot, - CMD_UPDATE); + ExecUpdatePrepareSlot(resultRelInfo, slot, estate); /* * update in foreign table: let the FDW do it @@ -1668,7 +1961,7 @@ ExecUpdate(ModifyTableState *mtstate, slot = resultRelInfo->ri_FdwRoutine->ExecForeignUpdate(estate, resultRelInfo, slot, - planSlot); + context->planSlot); if (slot == NULL) /* "do nothing" */ return NULL; @@ -1682,114 +1975,20 @@ ExecUpdate(ModifyTableState *mtstate, } else { - LockTupleMode lockmode; - bool partition_constraint_failed; - bool update_indexes; - - /* - * Constraints and GENERATED expressions might reference the tableoid - * column, so (re-)initialize tts_tableOid before evaluating them. - */ - slot->tts_tableOid = RelationGetRelid(resultRelationDesc); - - /* - * Compute stored generated columns - */ - if (resultRelationDesc->rd_att->constr && - resultRelationDesc->rd_att->constr->has_generated_stored) - ExecComputeStoredGenerated(resultRelInfo, estate, slot, - CMD_UPDATE); - - /* - * Check any RLS UPDATE WITH CHECK policies - * - * If we generate a new candidate tuple after EvalPlanQual testing, we - * must loop back here and recheck any RLS policies and constraints. - * (We don't need to redo triggers, however. If there are any BEFORE - * triggers then trigger.c will have done table_tuple_lock to lock the - * correct tuple, so there's no need to do them again.) - */ -lreplace:; - - /* ensure slot is independent, consider e.g. EPQ */ - ExecMaterializeSlot(slot); - - /* - * If partition constraint fails, this row might get moved to another - * partition, in which case we should check the RLS CHECK policy just - * before inserting into the new partition, rather than doing it here. - * This is because a trigger on that partition might again change the - * row. So skip the WCO checks if the partition constraint fails. - */ - partition_constraint_failed = - resultRelationDesc->rd_rel->relispartition && - !ExecPartitionCheck(resultRelInfo, slot, estate, false); - - if (!partition_constraint_failed && - resultRelInfo->ri_WithCheckOptions != NIL) - { - /* - * ExecWithCheckOptions() will skip any WCOs which are not of the - * kind we are looking for at this point. - */ - ExecWithCheckOptions(WCO_RLS_UPDATE_CHECK, - resultRelInfo, slot, estate); - } - - /* - * If a partition check failed, try to move the row into the right - * partition. - */ - if (partition_constraint_failed) - { - TupleTableSlot *inserted_tuple, - *retry_slot; - bool retry; + /* Fill in the slot appropriately */ + ExecUpdatePrepareSlot(resultRelInfo, slot, estate); - /* - * ExecCrossPartitionUpdate will first DELETE the row from the - * partition it's currently in and then insert it back into the - * root table, which will re-route it to the correct partition. - * The first part may have to be repeated if it is detected that - * the tuple we're trying to move has been concurrently updated. - */ - retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid, - oldtuple, slot, planSlot, - epqstate, canSetTag, - &retry_slot, &inserted_tuple); - if (retry) - { - slot = retry_slot; - goto lreplace; - } - - return inserted_tuple; - } +redo_act: + result = ExecUpdateAct(context, resultRelInfo, tupleid, oldtuple, slot, + canSetTag, &updateCxt); /* - * Check the constraints of the tuple. We've already checked the - * partition constraint above; however, we must still ensure the tuple - * passes all other constraints, so we will call ExecConstraints() and - * have it validate all remaining checks. + * If ExecUpdateAct reports that a cross-partition update was done, + * then the returning tuple has been projected and there's nothing + * else for us to do. */ - if (resultRelationDesc->rd_att->constr) - ExecConstraints(resultRelInfo, slot, estate); - - /* - * replace the heap tuple - * - * Note: if es_crosscheck_snapshot isn't InvalidSnapshot, we check - * that the row to be updated is visible to that snapshot, and throw a - * can't-serialize error if not. This is a special-case behavior - * needed for referential integrity updates in transaction-snapshot - * mode transactions. - */ - result = table_tuple_update(resultRelationDesc, tupleid, slot, - estate->es_output_cid, - estate->es_snapshot, - estate->es_crosscheck_snapshot, - true /* wait for commit */ , - &tmfd, &lockmode, &update_indexes); + if (updateCxt.crossPartUpdate) + return context->cpUpdateReturningSlot; switch (result) { @@ -1818,7 +2017,7 @@ lreplace:; * can re-execute the UPDATE (assuming it can figure out how) * and then return NULL to cancel the outer update. */ - if (tmfd.cmax != estate->es_output_cid) + if (context->tmfd.cmax != estate->es_output_cid) ereport(ERROR, (errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION), errmsg("tuple to be updated was already modified by an operation triggered by the current command"), @@ -1845,22 +2044,22 @@ lreplace:; * Already know that we're going to need to do EPQ, so * fetch tuple directly into the right slot. */ - inputslot = EvalPlanQualSlot(epqstate, resultRelationDesc, + inputslot = EvalPlanQualSlot(context->epqstate, resultRelationDesc, resultRelInfo->ri_RangeTableIndex); result = table_tuple_lock(resultRelationDesc, tupleid, estate->es_snapshot, inputslot, estate->es_output_cid, - lockmode, LockWaitBlock, + context->lockmode, LockWaitBlock, TUPLE_LOCK_FLAG_FIND_LAST_VERSION, - &tmfd); + &context->tmfd); switch (result) { case TM_Ok: - Assert(tmfd.traversed); + Assert(context->tmfd.traversed); - epqslot = EvalPlanQual(epqstate, + epqslot = EvalPlanQual(context->epqstate, resultRelationDesc, resultRelInfo->ri_RangeTableIndex, inputslot); @@ -1870,7 +2069,8 @@ lreplace:; /* Make sure ri_oldTupleSlot is initialized. */ if (unlikely(!resultRelInfo->ri_projectNewInfoValid)) - ExecInitUpdateProjection(mtstate, resultRelInfo); + ExecInitUpdateProjection(context->mtstate, + resultRelInfo); /* Fetch the most recent version of old tuple. */ oldSlot = resultRelInfo->ri_oldTupleSlot; @@ -1881,7 +2081,7 @@ lreplace:; elog(ERROR, "failed to fetch tuple being updated"); slot = ExecGetUpdateNewTuple(resultRelInfo, epqslot, oldSlot); - goto lreplace; + goto redo_act; case TM_Deleted: /* tuple already deleted; nothing to do */ @@ -1900,7 +2100,7 @@ lreplace:; * See also TM_SelfModified response to * table_tuple_update() above. */ - if (tmfd.cmax != estate->es_output_cid) + if (context->tmfd.cmax != estate->es_output_cid) ereport(ERROR, (errcode(ERRCODE_TRIGGERED_DATA_CHANGE_VIOLATION), errmsg("tuple to be updated was already modified by an operation triggered by the current command"), @@ -1930,41 +2130,19 @@ lreplace:; result); return NULL; } - - /* insert index entries for tuple if necessary */ - if (resultRelInfo->ri_NumIndices > 0 && update_indexes) - recheckIndexes = ExecInsertIndexTuples(resultRelInfo, - slot, estate, true, false, - NULL, NIL); } if (canSetTag) (estate->es_processed)++; - /* AFTER ROW UPDATE Triggers */ - ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, slot, - recheckIndexes, - mtstate->operation == CMD_INSERT ? - mtstate->mt_oc_transition_capture : - mtstate->mt_transition_capture); + ExecUpdateEpilogue(context, &updateCxt, resultRelInfo, tupleid, oldtuple, + slot, recheckIndexes); list_free(recheckIndexes); - /* - * Check any WITH CHECK OPTION constraints from parent views. We are - * required to do this after testing all constraints and uniqueness - * violations per the SQL spec, so we do it after actually updating the - * record in the heap and all indexes. - * - * ExecWithCheckOptions() will skip any WCOs which are not of the kind we - * are looking for at this point. - */ - if (resultRelInfo->ri_WithCheckOptions != NIL) - ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate); - /* Process RETURNING if present */ if (resultRelInfo->ri_projectReturning) - return ExecProcessReturning(resultRelInfo, slot, planSlot); + return ExecProcessReturning(resultRelInfo, slot, context->planSlot); return NULL; } @@ -1981,15 +2159,14 @@ lreplace:; * the caller must retry the INSERT from scratch. */ static bool -ExecOnConflictUpdate(ModifyTableState *mtstate, +ExecOnConflictUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo, ItemPointer conflictTid, - TupleTableSlot *planSlot, TupleTableSlot *excludedSlot, - EState *estate, 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; @@ -2002,7 +2179,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate, bool isnull; /* Determine lock mode to use */ - lockmode = ExecUpdateLockMode(estate, resultRelInfo); + lockmode = ExecUpdateLockMode(context->estate, resultRelInfo); /* * Lock tuple for update. Don't follow updates when tuple cannot be @@ -2011,8 +2188,8 @@ ExecOnConflictUpdate(ModifyTableState *mtstate, * true anymore. */ test = table_tuple_lock(relation, conflictTid, - estate->es_snapshot, - existing, estate->es_output_cid, + context->estate->es_snapshot, + existing, context->estate->es_output_cid, lockmode, LockWaitBlock, 0, &tmfd); switch (test) @@ -2119,7 +2296,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate, * snapshot. This is in line with the way UPDATE deals with newer tuple * versions. */ - ExecCheckTupleVisible(estate, relation, existing); + ExecCheckTupleVisible(context->estate, relation, existing); /* * Make tuple and any needed join variables available to ExecQual and @@ -2174,10 +2351,9 @@ ExecOnConflictUpdate(ModifyTableState *mtstate, */ /* Execute UPDATE with projection */ - *returning = ExecUpdate(mtstate, resultRelInfo, conflictTid, NULL, + *returning = ExecUpdate(context, resultRelInfo, + conflictTid, NULL, resultRelInfo->ri_onConflict->oc_ProjSlot, - planSlot, - &mtstate->mt_epqstate, mtstate->ps.state, canSetTag); /* @@ -2349,6 +2525,7 @@ static TupleTableSlot * ExecModifyTable(PlanState *pstate) { ModifyTableState *node = castNode(ModifyTableState, pstate); + ModifyTableContext context; EState *estate = node->ps.state; CmdType operation = node->operation; ResultRelInfo *resultRelInfo; @@ -2356,10 +2533,10 @@ ExecModifyTable(PlanState *pstate) TupleTableSlot *slot; TupleTableSlot *planSlot; TupleTableSlot *oldSlot; - ItemPointer tupleid; ItemPointerData tuple_ctid; HeapTupleData oldtupdata; HeapTuple oldtuple; + ItemPointer tupleid; PartitionTupleRouting *proute = node->mt_partition_tuple_routing; List *relinfos = NIL; ListCell *lc; @@ -2400,6 +2577,11 @@ ExecModifyTable(PlanState *pstate) resultRelInfo = node->resultRelInfo + node->mt_lastResultIndex; subplanstate = outerPlanState(node); + /* Set global context */ + context.mtstate = node; + context.epqstate = &node->mt_epqstate; + context.estate = estate; + /* * Fetch rows from subplan, and execute the required table modification * for each row. @@ -2551,6 +2733,10 @@ ExecModifyTable(PlanState *pstate) } } + /* complete context setup */ + context.planSlot = planSlot; + context.lockmode = 0; + switch (operation) { case CMD_INSERT: @@ -2558,9 +2744,10 @@ ExecModifyTable(PlanState *pstate) if (unlikely(!resultRelInfo->ri_projectNewInfoValid)) ExecInitInsertProjection(node, resultRelInfo); slot = ExecGetInsertNewTuple(resultRelInfo, planSlot); - slot = ExecInsert(node, resultRelInfo, slot, planSlot, - estate, node->canSetTag); + slot = ExecInsert(&context, resultRelInfo, slot, + node->canSetTag); break; + case CMD_UPDATE: /* Initialize projection info if first time for this table */ if (unlikely(!resultRelInfo->ri_projectNewInfoValid)) @@ -2581,7 +2768,6 @@ ExecModifyTable(PlanState *pstate) /* Fetch the most recent version of old tuple. */ Relation relation = resultRelInfo->ri_RelationDesc; - Assert(tupleid != NULL); if (!table_tuple_fetch_row_version(relation, tupleid, SnapshotAny, oldSlot)) @@ -2591,18 +2777,15 @@ ExecModifyTable(PlanState *pstate) oldSlot); /* Now apply the update. */ - slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot, - planSlot, &node->mt_epqstate, estate, - node->canSetTag); + slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple, + slot, node->canSetTag); break; + case CMD_DELETE: - slot = ExecDelete(node, resultRelInfo, tupleid, oldtuple, - planSlot, &node->mt_epqstate, estate, - true, /* processReturning */ - node->canSetTag, - false, /* changingPart */ - NULL, NULL); + slot = ExecDelete(&context, resultRelInfo, tupleid, oldtuple, + true, false, node->canSetTag, NULL, NULL); break; + default: elog(ERROR, "unknown operation"); break; @@ -2800,8 +2983,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) } /* Initialize the usesFdwDirectModify flag */ - resultRelInfo->ri_usesFdwDirectModify = bms_is_member(i, - node->fdwDirectModifyPlans); + resultRelInfo->ri_usesFdwDirectModify = + bms_is_member(i, node->fdwDirectModifyPlans); /* * Verify result relation is a valid target for the current operation |