From b3b833b510706eda4c387dbe7a076ce84f5ab304 Mon Sep 17 00:00:00 2001 From: April01xxx Date: Fri, 22 Sep 2023 19:44:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A1=A8=E4=B8=8A=E6=9C=89BR?= =?UTF-8?q?U=E8=A7=A6=E5=8F=91=E5=99=A8=E6=97=B6=E7=9A=84=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E9=97=AE=E9=A2=98=E3=80=82=20=20=20=20=20BRU=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E5=99=A8=E4=B8=AD=E5=8F=AF=E8=83=BD=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=88=86=E5=8C=BA=E9=94=AE=EF=BC=8C=E6=AD=A4=E6=97=B6=E6=88=91?= =?UTF-8?q?=E4=BB=AC=E9=9C=80=E8=A6=81=E9=87=8D=E6=96=B0=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E5=85=83=E7=BB=84=E6=89=80=E5=B1=9E=E7=9A=84=E5=88=86=E5=8C=BA?= =?UTF-8?q?=EF=BC=9B=20=20=20=20=20=E5=BD=93UPDATE=E6=97=B6=E9=81=87?= =?UTF-8?q?=E5=88=B0CONCURRENTLY=20UPDATE/DELETE=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=E6=97=B6=EF=BC=8CMERGE=20INTO=E9=9C=80=E8=A6=81=20=20=20=20=20?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E5=88=A4=E6=96=AD=E6=98=AF=E5=90=A6=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=EF=BC=8C=E8=A7=A6=E5=8F=91=E5=99=A8=E4=B8=AD=E6=89=A7?= =?UTF-8?q?=E8=A1=8CEPQ=E5=90=8E=E6=8A=95=E5=BD=B1=E7=9A=84=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E9=94=99=E8=AF=AF=EF=BC=8C=E5=8F=AF=E8=83=BD=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E5=AE=95=E6=9C=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/backend/utils/misc/guc/guc_sql.cpp | 2 +- .../optimizer/commands/trigger.cpp | 24 +- .../runtime/executor/execMerge.cpp | 181 +++++++++++- .../runtime/executor/nodeModifyTable.cpp | 159 +++++------ src/include/commands/trigger.h | 5 +- src/include/executor/node/nodeModifyTable.h | 2 +- src/include/miscadmin.h | 4 +- .../regress/expected/merge_into_deleted.out | 53 ++++ .../merge_into_partition_row_movement.out | 267 ++++++++++++++++++ .../expected/merge_into_selfmodified.out | 133 +++++++++ .../regress/expected/merge_into_updated.out | 140 +++++++++ src/test/regress/parallel_schedule0 | 2 +- src/test/regress/parallel_schedule0A | 2 +- src/test/regress/sql/merge_into_deleted.sql | 44 +++ .../sql/merge_into_partition_row_movement.sql | 182 ++++++++++++ .../regress/sql/merge_into_selfmodified.sql | 91 ++++++ src/test/regress/sql/merge_into_updated.sql | 118 ++++++++ 17 files changed, 1303 insertions(+), 106 deletions(-) create mode 100644 src/test/regress/expected/merge_into_deleted.out create mode 100644 src/test/regress/expected/merge_into_partition_row_movement.out create mode 100644 src/test/regress/expected/merge_into_selfmodified.out create mode 100644 src/test/regress/expected/merge_into_updated.out create mode 100644 src/test/regress/sql/merge_into_deleted.sql create mode 100644 src/test/regress/sql/merge_into_partition_row_movement.sql create mode 100644 src/test/regress/sql/merge_into_selfmodified.sql create mode 100644 src/test/regress/sql/merge_into_updated.sql diff --git a/src/common/backend/utils/misc/guc/guc_sql.cpp b/src/common/backend/utils/misc/guc/guc_sql.cpp index 26115af26..4a557c89f 100755 --- a/src/common/backend/utils/misc/guc/guc_sql.cpp +++ b/src/common/backend/utils/misc/guc/guc_sql.cpp @@ -366,7 +366,7 @@ static const struct behavior_compat_entry behavior_compat_options[OPT_MAX] = { {"unbind_divide_bound", OPT_UNBIND_DIVIDE_BOUND}, {"correct_to_number", OPT_CORRECT_TO_NUMBER}, {"compat_concat_variadic", OPT_CONCAT_VARIADIC}, - {"merge_update_multi", OPT_MEGRE_UPDATE_MULTI}, + {"merge_update_multi", OPT_MERGE_UPDATE_MULTI}, {"convert_string_digit_to_numeric", OPT_CONVERT_TO_NUMERIC}, {"plstmt_implicit_savepoint", OPT_PLSTMT_IMPLICIT_SAVEPOINT}, {"hide_tailing_zero", OPT_HIDE_TAILING_ZERO}, diff --git a/src/gausskernel/optimizer/commands/trigger.cpp b/src/gausskernel/optimizer/commands/trigger.cpp index 7d945c741..f4931891f 100644 --- a/src/gausskernel/optimizer/commands/trigger.cpp +++ b/src/gausskernel/optimizer/commands/trigger.cpp @@ -88,8 +88,6 @@ /* Local function prototypes */ static void ConvertTriggerToFK(CreateTrigStmt* stmt, Oid funcoid); static void SetTriggerFlags(TriggerDesc* trigdesc, const Trigger* trigger); -HeapTuple GetTupleForTrigger(EState* estate, EPQState* epqstate, ResultRelInfo* relinfo, Oid targetPartitionOid, - int2 bucketid, ItemPointer tid, LockTupleMode lockmode, TupleTableSlot** newSlot); static void ReleaseFakeRelation(Relation relation, Partition part, Relation* fakeRelation); static bool TriggerEnabled(EState* estate, ResultRelInfo* relinfo, Trigger* trigger, TriggerEvent event, const Bitmapset* modifiedCols, HeapTuple oldtup, HeapTuple newtup); @@ -2718,7 +2716,7 @@ TupleTableSlot* ExecBRUpdateTriggers(EState* estate, EPQState* epqstate, ResultR #ifdef PGXC HeapTupleHeader datanode_tuphead, #endif - ItemPointer tupleid, TupleTableSlot* slot) + ItemPointer tupleid, TupleTableSlot* slot, TM_Result* result, TM_FailureData* tmfd) { TriggerDesc* trigdesc = get_and_check_trigdesc_value(relinfo->ri_TrigDesc); HeapTuple slottuple = ExecMaterializeSlot(slot); @@ -2780,7 +2778,7 @@ TupleTableSlot* ExecBRUpdateTriggers(EState* estate, EPQState* epqstate, ResultR #endif /* get a copy of the on-disk tuple we are planning to update */ trigtuple = GetTupleForTrigger(estate, epqstate, relinfo, oldPartitionOid, - bucketid, tupleid, lockmode, &newSlot); + bucketid, tupleid, lockmode, &newSlot, result, tmfd); if (trigtuple == NULL) return NULL; /* cancel the update action */ @@ -3031,7 +3029,7 @@ void ExecASTruncateTriggers(EState* estate, ResultRelInfo* relinfo) } HeapTuple GetTupleForTrigger(EState* estate, EPQState* epqstate, ResultRelInfo* relinfo, Oid targetPartitionOid, - int2 bucketid, ItemPointer tid, LockTupleMode lockmode, TupleTableSlot** newSlot) + int2 bucketid, ItemPointer tid, LockTupleMode lockmode, TupleTableSlot** newSlot, TM_Result* tmresultp, TM_FailureData* tmfdp) { Relation relation = relinfo->ri_RelationDesc; HeapTupleData tuple; @@ -3203,6 +3201,13 @@ ltrmark: &tmfd, true, // fake params below are for uheap implementation false, false, NULL, NULL, false); + + /* Let the caller know about the status of this operation */ + if (tmresultp) + *tmresultp = test; + if (tmfdp) + *tmfdp = tmfd; + switch (test) { case TM_SelfUpdated: case TM_SelfModified: @@ -3236,6 +3241,15 @@ ltrmark: (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("could not serialize access due to concurrent update"))); Assert(!ItemPointerEquals(&tmfd.ctid, &tuple.t_self)); + + /* + * Recheck the tuple using EPQ. For MERGE, we leave this + * to the caller (it must do additional rechecking, and + * might end up executing a different action entirely). + */ + if (tmresultp && estate->es_plannedstmt->commandType == CMD_MERGE) + return NULL; + /* it was updated, so look at the updated version */ TupleTableSlot* epqslot = NULL; diff --git a/src/gausskernel/runtime/executor/execMerge.cpp b/src/gausskernel/runtime/executor/execMerge.cpp index 795ed95c6..fa6c71914 100644 --- a/src/gausskernel/runtime/executor/execMerge.cpp +++ b/src/gausskernel/runtime/executor/execMerge.cpp @@ -34,10 +34,43 @@ #include "utils/memutils.h" #include "utils/rel.h" #include "access/heapam.h" +#include "utils/relcache.h" +#include "utils/snapshot.h" static void ExecMergeNotMatched(ModifyTableState* mtstate, EState* estate, TupleTableSlot* slot, char* partExprKeyStr = NULL); static bool ExecMergeMatched(ModifyTableState* mtstate, EState* estate, TupleTableSlot* slot, JunkFilter* junkfilter, ItemPointer tupleid, HeapTupleHeader oldtuple, Oid oldPartitionOid, int2 bucketid, char* partExprKeyStr = NULL); +static LockTupleMode ExecUpdateLockMode(EState *estate, ResultRelInfo *relinfo); + +#define GET_ALL_UPDATED_COLUMNS(relinfo, estate) \ + (bms_union(exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->updatedCols, \ + exec_rt_fetch((relinfo)->ri_RangeTableIndex, estate)->extraUpdatedCols)) + +/* + * ExecUpdateLockMode -- find the appropriate UPDATE tuple lock mode for a + * given ResultRelInfo + */ +static LockTupleMode +ExecUpdateLockMode(EState *estate, ResultRelInfo *relinfo) +{ + Bitmapset *keyCols; + Bitmapset *updatedCols; + + /* + * Compute lock mode to use. If columns that are part of the key have not + * been modified, then we can use a weaker lock, allowing for better + * concurrency. + */ + updatedCols = GET_ALL_UPDATED_COLUMNS(relinfo, estate); + keyCols = RelationGetIndexAttrBitmap(relinfo->ri_RelationDesc, + INDEX_ATTR_BITMAP_KEY); + + if (bms_overlap(keyCols, updatedCols)) + return LockTupleExclusive; + + return LockTupleNoKeyExclusive; +} + /* * Perform MERGE. */ @@ -391,10 +424,12 @@ static bool ExecMergeMatched(ModifyTableState* mtstate, EState* estate, TupleTab if (mergeMatchedActionStates != NIL) { MergeActionState* action = (MergeActionState*)linitial(mergeMatchedActionStates); +lmerge_matched: slot = ExecMergeProjQual(mtstate, mergeMatchedActionStates, econtext, slot, slot, estate); if (slot != NULL) { - TM_Result out_result; + TM_Result result = TM_Ok; + TM_FailureData tmfd; (void)ExecUpdate(tupleid, oldPartitionOid, bucketid, @@ -405,11 +440,145 @@ static bool ExecMergeMatched(ModifyTableState* mtstate, EState* estate, TupleTab mtstate, mtstate->canSetTag, partKeyUpdated, - &out_result, - partExprKeyStr); - /* the matched row has been delted or after updated, the row does not matched, change to insert. */ - if (out_result == TM_Deleted || out_result == TM_Updated) { - return false; + &result, + partExprKeyStr, + &tmfd); + + /* + * The matched tuple has been updated or deleted by trigger or + * other session, we have to check the updated version of the + * tuple to see if we want to process it under RC rules. + */ + switch (result) { + case TM_Ok: + /* all good; perform final actions */ + break; + case TM_SelfUpdated: + case TM_SelfModified: + /* The SQL standard disallows this for MERGE, but... */ + if (TransactionIdIsCurrentTransactionId(tmfd.xmax)) { + if (!MERGE_UPDATE_MULTI) + ereport(ERROR, + (errcode(ERRCODE_CARDINALITY_VIOLATION), + errmsg("MERGE command cannot affect row a second time"), + errhint("Ensure that not more than one source row matches any one target row."))); + /* already updated or deleted. */ + break; + } + /* This shouldn't happen */ + elog(ERROR, "attempted to update or delete invisible tuple"); + break; + case TM_Deleted: + if (IsolationUsesXactSnapshot()) + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("could not serialize access due to concurrent delete"))); + + if (resultRelInfo->ri_RelationDesc->rd_rel->relrowmovement) { + Assert(RELATION_IS_PARTITIONED(resultRelInfo->ri_RelationDesc)); + /* + * the may be a row movement update action which delete tuple from original + * partition and insert tuple to new partition or we can add lock on the tuple to + * be delete or updated to avoid throw exception + */ + ereport(ERROR, + (errcode(ERRCODE_TRANSACTION_ROLLBACK), + errmsg("partition table update conflict"), + errdetail("disable row movement of table can avoid this conflict"))); + } + /* + * If the tuple was already deleted, return to let caller + * handle it under NOT MATCHED clauses. + */ + return false; + case TM_Updated: + { + Relation resultRelationDesc; + Relation partRelationDesc; + TupleTableSlot *epqslot; + LockTupleMode lockmode; + bool isNull, + isPartition; + + /* + * The target tuple was concurrently updated by some other + * transaction. Run EvalPlanQual() with the new version of + * the tuple. If it does not return a tuple, then we + * switch to the NOT MATCHED list of actions. If it does + * return a tuple and the join qual is still satisfied, + * then we just need to recheck the MATCHED actions, + * starting from the top, and execute the first qualifying + * action. + */ + resultRelationDesc = resultRelInfo->ri_RelationDesc; + isPartition = RELATION_IS_PARTITIONED(resultRelationDesc); + if (isPartition) { + Partition partition = NULL; + + searchFakeReationForPartitionOid(estate->esfRelations, + estate->es_query_cxt, + resultRelationDesc, + oldPartitionOid, + GetCurrentPartitionNo(oldPartitionOid), + partRelationDesc, + partition, + RowExclusiveLock); + } + lockmode = ExecUpdateLockMode(estate, resultRelInfo); + epqslot = EvalPlanQual(estate, epqstate, + isPartition ? partRelationDesc : resultRelationDesc, + resultRelInfo->ri_RangeTableIndex, + lockmode, &tmfd.ctid, tmfd.xmax, + resultRelationDesc->rd_rel->relrowmovement); + + /* + * If we got no tuple, or the tuple we get has a + * NULL ctid, go back to caller: this one is not a + * MATCHED tuple anymore, so they can retry with + * NOT MATCHED actions. + */ + if (TupIsNull(epqslot)) + return false; + + (void) ExecGetJunkAttribute(epqslot, + resultRelInfo->ri_junkFilter->jf_junkAttNo, + &isNull); + if (isNull) + return false; + + /* + * For partitioned table we have to check if the partition oid + * is NULL. + */ + if (isPartition) { + Datum partoid; + partoid = ExecGetJunkAttribute(epqslot, + resultRelInfo->ri_partOidAttNum, + &isNull); + if (isNull) + ereport(ERROR, + (errcode(ERRCODE_NULL_JUNK_ATTRIBUTE), + errmsg("tableoid is null when merge partitioned table"))); + Assert(oldPartitionOid == DatumGetObjectId(partoid)); + } + + /* + * A non-NULL ctid means that we are still dealing + * with MATCHED case. Restart the loop so that we + * apply all the MATCHED rules again, to ensure + * that the first qualifying WHEN MATCHED action + * is executed. + * + * Update tupleid to that of the new tuple, for + * the refetch we do at the top. + */ + saved_slot = slot = epqslot; + *tupleid = tmfd.ctid; + goto lmerge_matched; + } + default: + elog(ERROR, "unexpected tuple operation result: %d", result); + break; } } if (action->commandType == CMD_UPDATE /* && tuple_updated*/) diff --git a/src/gausskernel/runtime/executor/nodeModifyTable.cpp b/src/gausskernel/runtime/executor/nodeModifyTable.cpp index 924f3b6cf..c5f9750db 100644 --- a/src/gausskernel/runtime/executor/nodeModifyTable.cpp +++ b/src/gausskernel/runtime/executor/nodeModifyTable.cpp @@ -720,10 +720,9 @@ checktest: ExecProject(resultRelInfo->ri_updateProj, NULL); /* Evaluate where qual if exists, add to count if filtered */ if (ExecQual(upsertState->us_updateWhere, econtext, false)) { - TM_Result out_result; *returning = ExecUpdate(conflictTid, oldPartitionOid, bucketid, NULL, upsertState->us_updateproj, planSlot, &mtstate->mt_epqstate, - mtstate, canSetTag, ((ModifyTable*)mtstate->ps.plan)->partKeyUpdated, &out_result, partExprKeyStr); + mtstate, canSetTag, ((ModifyTable*)mtstate->ps.plan)->partKeyUpdated, NULL, partExprKeyStr); } else { InstrCountFiltered1(&mtstate->ps, 1); } @@ -2010,33 +2009,6 @@ end:; return NULL; } -/* - * check whether the tuple still match the merge condition after the tuple has been updated by other transaction. - * [in] node, executor node - * [in] epq_slot, the tuple slot after recheck on the updated tuple - * [in] mergeMatchedActionStates, update action state - * [in] fake_relation, heap table relation - * [in/out] slot, in: origin tuple slot, out: slot after merge projection - * [out] tuple, if the updated tuple still match the merge condition, return the new projected tuple - * [return value] true for match, false for not match. - */ -static bool MatchMergeCondition(ModifyTableState* node, TupleTableSlot* epq_slot, List* mergeMatchedActionStates, - Relation fake_relation, AttrNumber junkAttno, TupleTableSlot** slot, Tuple *tuple) -{ - /* resultRelInfo->ri_mergeState is always not null */ - *slot = ExecMergeProjQual(node, mergeMatchedActionStates, node->ps.ps_ExprContext, epq_slot, *slot, node->ps.state); - if (*slot != NULL) { - bool is_null = false; - /* Check after epq, whether the ctid is null. null means the updated row does not match */ - (void)ExecGetJunkAttribute(epq_slot, junkAttno, &is_null); - if (!is_null) { - *tuple = tableam_tslot_get_tuple_from_slot(fake_relation, *slot); - return true; - } - } - return false; -} - /* ---------------------------------------------------------------- * ExecUpdate * @@ -2060,7 +2032,7 @@ static bool MatchMergeCondition(ModifyTableState* node, TupleTableSlot* epq_slot TupleTableSlot* ExecUpdate(ItemPointer tupleid, Oid oldPartitionOid, /* when update a partitioned table , give a partitionOid to find the tuple */ int2 bucketid, HeapTupleHeader oldtuple, TupleTableSlot* slot, TupleTableSlot* planSlot, EPQState* epqstate, - ModifyTableState* node, bool canSetTag, bool partKeyUpdate, TM_Result* out_result, char* partExprKeyStr) + ModifyTableState* node, bool canSetTag, bool partKeyUpdate, TM_Result* uresultp, char* partExprKeyStr, TM_FailureData* tmfdp) { EState* estate = node->ps.state; Tuple tuple = NULL; @@ -2085,13 +2057,13 @@ TupleTableSlot* ExecUpdate(ItemPointer tupleid, ConflictInfoData conflictInfo; Oid conflictPartOid = InvalidOid; int2 conflictBucketid = InvalidBktId; + bool cross_partition = (partKeyUpdate || partExprKeyStr != NULL); #ifdef PGXC RemoteQueryState* result_remote_rel = NULL; #endif bool allow_update_self = (node->mt_upsert != NULL && node->mt_upsert->us_action != UPSERT_NONE) ? true : false; - *out_result = TM_Ok; /* * get information on the (current) result relation */ @@ -2146,9 +2118,9 @@ TupleTableSlot* ExecUpdate(ItemPointer tupleid, result_rel_info->ri_TrigDesc && result_rel_info->ri_TrigDesc->trig_update_before_row) { #ifdef PGXC slot = ExecBRUpdateTriggers(estate, epqstate, result_rel_info, oldPartitionOid, - bucketid, oldtuple, tupleid, slot); + bucketid, oldtuple, tupleid, slot, uresultp, tmfdp); #else - slot = ExecBRUpdateTriggers(estate, epqstate, result_rel_info, tupleid, slot); + slot = ExecBRUpdateTriggers(estate, epqstate, result_rel_info, tupleid, slot, uresultp, tmfdp); #endif if (slot == NULL) { @@ -2159,6 +2131,9 @@ TupleTableSlot* ExecUpdate(ItemPointer tupleid, // tableam /* trigger might have changed tuple */ tuple = tableam_tslot_get_tuple_from_slot(result_relation_desc, slot); + + /* The partition key might have been updated by the trigger. */ + cross_partition = true; } /* INSTEAD OF ROW UPDATE Triggers */ @@ -2246,6 +2221,8 @@ lreplace: bool update_fix_result = ExecComputeStoredUpdateExpr(result_rel_info, estate, slot, tuple, CMD_UPDATE, tupleid, oldPartitionOid, bucketid); if (!update_fix_result) { tuple = slot->tts_tuple; + /* The partition key might have been updated by on update rule. */ + cross_partition = true; } } @@ -2255,6 +2232,8 @@ lreplace: if (result_relation_desc->rd_att->constr && result_relation_desc->rd_att->constr->has_generated_stored) { ExecComputeStoredGenerated(result_rel_info, estate, slot, tuple, CMD_UPDATE); tuple = slot->tts_tuple; + /* The partition key might have been updated by generated expr. */ + cross_partition = true; } if (result_relation_desc->rd_att->constr) { @@ -2333,12 +2312,18 @@ lreplace: estate->es_crosscheck_snapshot, estate->es_snapshot, true, // wait for commit &oldslot, &tmfd, &update_indexes, &modifiedIdxAttrs, allow_update_self, allowInplaceUpdate, &lockmode); - *out_result = result; + + /* Let the caller know about the status of this operation */ + if (uresultp) + *uresultp = result; + if (tmfdp) + *tmfdp = tmfd; + switch (result) { case TM_SelfUpdated: case TM_SelfModified: /* can not update one row more than once for merge into */ - if (node->operation == CMD_MERGE && !MEGRE_UPDATE_MULTI) { + if (node->operation == CMD_MERGE && !MERGE_UPDATE_MULTI) { ereport(ERROR, (errmodule(MOD_EXECUTOR), (errcode(ERRCODE_TOO_MANY_ROWS), @@ -2404,27 +2389,23 @@ lreplace: errmsg("concurrent update under Stream mode is not yet supported"))); } + /* + * Recheck the tuple using EPQ. For MERGE, we leave this + * to the caller (it must do additional rechecking, and + * might end up executing a different action entirely). + */ + if (uresultp && estate->es_plannedstmt->commandType == CMD_MERGE) + return NULL; + TupleTableSlot *epq_slot = EvalPlanQual(estate, epqstate, fake_relation, result_rel_info->ri_RangeTableIndex, lockmode, &tmfd.ctid, tmfd.xmax, false); if (!TupIsNull(epq_slot)) { *tupleid = tmfd.ctid; - /* - * For merge into query, mergeMatchedAction's targetlist is not same as junk filter's - * targetlist. Here, epqslot is a plan slot, target table needs slot to be projected - * from plan slot. - */ - if (node->operation == CMD_MERGE) { - if (MatchMergeCondition(node, epq_slot, result_rel_info->ri_mergeState->matchedActionStates, - fake_relation, result_rel_info->ri_junkFilter->jf_junkAttNo, &slot, &tuple)) { - goto lreplace; - } - } else { - slot = ExecFilterJunk(result_rel_info->ri_junkFilter, epq_slot); + slot = ExecFilterJunk(result_rel_info->ri_junkFilter, epq_slot); - tuple = tableam_tslot_get_tuple_from_slot(fake_relation, slot); - goto lreplace; - } + tuple = tableam_tslot_get_tuple_from_slot(fake_relation, slot); + goto lreplace; } /* Updated tuple not matched; nothing to do */ @@ -2486,7 +2467,7 @@ lreplace: if (partExprKeyStr) { newval = ComputePartKeyExprTuple(result_relation_desc, estate, slot, NULL, partExprKeyStr); } - if (!partExprKeyStr && !partKeyUpdate) { + if (!cross_partition) { row_movement = false; new_partId = oldPartitionOid; } else { @@ -2649,12 +2630,18 @@ lreplace: allow_update_self, allowInplaceUpdate, &lockmode); - *out_result = result; + + /* Let the caller know about the status of this operation */ + if (uresultp) + *uresultp = result; + if (tmfdp) + *tmfdp = tmfd; + switch (result) { case TM_SelfUpdated: case TM_SelfModified: /* can not update one row more than once for merge into */ - if (node->operation == CMD_MERGE && !MEGRE_UPDATE_MULTI) { + if (node->operation == CMD_MERGE && !MERGE_UPDATE_MULTI) { ereport(ERROR, (errmodule(MOD_EXECUTOR), (errcode(ERRCODE_TOO_MANY_ROWS), errmsg("unable to get a stable set of rows in the source tables")))); } @@ -2707,6 +2694,15 @@ lreplace: errmsg("concurrent update under Stream mode is not yet supported"))); } + + /* + * Recheck the tuple using EPQ. For MERGE, we leave this + * to the caller (it must do additional rechecking, and + * might end up executing a different action entirely). + */ + if (uresultp && estate->es_plannedstmt->commandType == CMD_MERGE) + return NULL; + TupleTableSlot *epq_slot = EvalPlanQual(estate, epqstate, fake_relation, result_rel_info->ri_RangeTableIndex, lockmode, &tmfd.ctid, tmfd.xmax, result_relation_desc->rd_rel->relrowmovement); @@ -2714,22 +2710,10 @@ lreplace: if (!TupIsNull(epq_slot)) { *tupleid = tmfd.ctid; - /* - * For merge into query, mergeMatchedAction's targetlist is not same as junk - * filter's targetlist. Here, epq_slot is a plan slot, target table needs slot to be - * projected from plan slot. - */ - if (node->operation == CMD_MERGE) { - if (MatchMergeCondition(node, epq_slot, result_rel_info->ri_mergeState->matchedActionStates, - fake_relation, result_rel_info->ri_junkFilter->jf_junkAttNo, &slot, &tuple)) { - goto lreplace; - } - } else { - slot = ExecFilterJunk(result_rel_info->ri_junkFilter, epq_slot); + slot = ExecFilterJunk(result_rel_info->ri_junkFilter, epq_slot); - tuple = tableam_tslot_get_tuple_from_slot(fake_relation, slot); - goto lreplace; - } + tuple = tableam_tslot_get_tuple_from_slot(fake_relation, slot); + goto lreplace; } /* Updated tuple not matched; nothing to do */ @@ -2857,12 +2841,18 @@ ldelete: &oldslot, &tmfd, allow_update_self); - *out_result = result; + + /* Let the caller know about the status of this operation */ + if (uresultp) + *uresultp = result; + if (tmfdp) + *tmfdp = tmfd; + switch (result) { case TM_SelfUpdated: case TM_SelfModified: /* can not update one row more than once for merge into */ - if (node->operation == CMD_MERGE && !MEGRE_UPDATE_MULTI) { + if (node->operation == CMD_MERGE && !MERGE_UPDATE_MULTI) { ereport(ERROR, (errmodule(MOD_EXECUTOR), (errcode(ERRCODE_TOO_MANY_ROWS), errmsg("unable to get a stable set of rows in the source tables")))); } @@ -2939,6 +2929,14 @@ ldelete: errmsg("concurrent update under Stream mode is not yet supported"))); } + /* + * Recheck the tuple using EPQ. For MERGE, we leave this + * to the caller (it must do additional rechecking, and + * might end up executing a different action entirely). + */ + if (uresultp && estate->es_plannedstmt->commandType == CMD_MERGE) + return NULL; + TupleTableSlot* epq_slot = EvalPlanQual(estate, epqstate, old_fake_relation, @@ -2950,22 +2948,10 @@ ldelete: /* Try to fetch latest tuple values in row movement case */ if (!TupIsNull(epq_slot)) { *tupleid = tmfd.ctid; - /* - * For merge into query, mergeMatchedAction's targetlist is not same as - * junk filter's targetlist. Here, epqslot is a plan slot, target table - * needs slot to be projected from plan slot. - */ - if (node->operation == CMD_MERGE) { - if (MatchMergeCondition(node, epq_slot, result_rel_info->ri_mergeState->matchedActionStates, - old_fake_relation, result_rel_info->ri_junkFilter->jf_junkAttNo, &slot, &tuple)) { - goto ldelete; - } - } else { - slot = ExecFilterJunk(result_rel_info->ri_junkFilter, epq_slot); + slot = ExecFilterJunk(result_rel_info->ri_junkFilter, epq_slot); - tuple = tableam_tslot_get_tuple_from_slot(old_fake_relation, slot); - goto ldelete; - } + tuple = tableam_tslot_get_tuple_from_slot(old_fake_relation, slot); + goto ldelete; } /* Updated tuple not matched; nothing to do */ @@ -3721,7 +3707,6 @@ static TupleTableSlot* ExecModifyTable(PlanState* state) } break; case CMD_UPDATE: { - TM_Result out_result; slot = ExecUpdate(tuple_id, old_partition_oid, bucketid, @@ -3732,7 +3717,7 @@ static TupleTableSlot* ExecModifyTable(PlanState* state) node, node->canSetTag, part_key_updated, - &out_result, + NULL, partExprKeyStr); } break; case CMD_DELETE: diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h index f475272a6..5ec192602 100644 --- a/src/include/commands/trigger.h +++ b/src/include/commands/trigger.h @@ -146,7 +146,7 @@ extern TupleTableSlot* ExecBRUpdateTriggers(EState* estate, EPQState* epqstate, #ifdef PGXC HeapTupleHeader datanode_tuphead, #endif - ItemPointer tupleid, TupleTableSlot* slot); + ItemPointer tupleid, TupleTableSlot* slot, TM_Result* result = NULL, TM_FailureData* tmfd = NULL); extern void ExecARUpdateTriggers(EState* estate, ResultRelInfo* relinfo, Oid oldPartitionOid, int2 bucketid, Oid newPartitionOid, ItemPointer tupleid, HeapTuple newtuple, #ifdef PGXC @@ -201,6 +201,7 @@ extern void InvalidRelcacheForTriggerFunction(Oid funcoid, Oid returnType); extern void ResetTrigShipFlag(); extern HeapTuple GetTupleForTrigger(EState* estate, EPQState* epqstate, ResultRelInfo* relinfo, Oid targetPartitionOid, - int2 bucketid, ItemPointer tid, LockTupleMode lockmode, TupleTableSlot** newSlot); + int2 bucketid, ItemPointer tid, LockTupleMode lockmode, TupleTableSlot** newSlot, TM_Result* result = NULL, + TM_FailureData* tmfd = NULL); #endif /* TRIGGER_H */ diff --git a/src/include/executor/node/nodeModifyTable.h b/src/include/executor/node/nodeModifyTable.h index 99e6a6fa6..3fe98835a 100644 --- a/src/include/executor/node/nodeModifyTable.h +++ b/src/include/executor/node/nodeModifyTable.h @@ -46,7 +46,7 @@ extern TupleTableSlot* ExecDelete(ItemPointer tupleid, Oid deletePartitionOid, i extern TupleTableSlot* ExecUpdate(ItemPointer tupleid, Oid oldPartitionOid, int2 bucketid, HeapTupleHeader oldtuple, TupleTableSlot* slot, TupleTableSlot* planSlot, EPQState* epqstate, ModifyTableState* node, bool canSetTag, - bool partKeyUpdate, TM_Result* out_result, char* partExprKeyStr = NULL); + bool partKeyUpdate, TM_Result* uresultp = NULL, char* partExprKeyStr = NULL, TM_FailureData* tmfd = NULL); template extern TupleTableSlot* ExecInsertT(ModifyTableState* state, TupleTableSlot* slot, TupleTableSlot* planSlot, diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index a52ef3218..363bf6711 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -181,7 +181,7 @@ extern void SSUpgradeFileBeforeCommit(); #define OPT_UNBIND_DIVIDE_BOUND 64 #define OPT_CORRECT_TO_NUMBER 128 #define OPT_CONCAT_VARIADIC 256 -#define OPT_MEGRE_UPDATE_MULTI 512 +#define OPT_MERGE_UPDATE_MULTI 512 #define OPT_CONVERT_TO_NUMERIC 1024 #define OPT_PLSTMT_IMPLICIT_SAVEPOINT 2048 #define OPT_HIDE_TAILING_ZERO 4096 @@ -219,7 +219,7 @@ extern void SSUpgradeFileBeforeCommit(); * option is blank and the behavior is new and compatible with current A and C mode, if the option is set, the * behavior is old and the same as previous GAUSSDB kernel. */ #define CONCAT_VARIADIC (!(u_sess->utils_cxt.behavior_compat_flags & OPT_CONCAT_VARIADIC)) -#define MEGRE_UPDATE_MULTI (u_sess->utils_cxt.behavior_compat_flags & OPT_MEGRE_UPDATE_MULTI) +#define MERGE_UPDATE_MULTI (u_sess->utils_cxt.behavior_compat_flags & OPT_MERGE_UPDATE_MULTI) #define CONVERT_STRING_DIGIT_TO_NUMERIC (u_sess->utils_cxt.behavior_compat_flags & OPT_CONVERT_TO_NUMERIC) #define PLSTMT_IMPLICIT_SAVEPOINT (u_sess->utils_cxt.behavior_compat_flags & OPT_PLSTMT_IMPLICIT_SAVEPOINT) #define HIDE_TAILING_ZERO (u_sess->utils_cxt.behavior_compat_flags & OPT_HIDE_TAILING_ZERO) diff --git a/src/test/regress/expected/merge_into_deleted.out b/src/test/regress/expected/merge_into_deleted.out new file mode 100644 index 000000000..6c8786aca --- /dev/null +++ b/src/test/regress/expected/merge_into_deleted.out @@ -0,0 +1,53 @@ +create schema merge_into_deleted; +set search_path = 'merge_into_deleted'; +create table t1 (c1 int, c2 text, c3 timestamp); +insert into t1 values (1, 'a', '2023-09-15'); +-- concurrently deleted and insert, do update and insert +\parallel on 2 +begin + delete from t1 where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + insert into t1 values (1, 'b', '2023-09-16'); + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'c' + when not matched then insert values (2, 'c', '2023-09-17'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | c | 2023-09-16 + 2 | c | 2023-09-17 +(2 rows) + +-- concurrently deleted, not matched, do insert +\parallel on 2 +begin + delete from t1 where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 2 | c | 2023-09-17 + 3 | d | 2023-09-18 +(2 rows) + +drop schema merge_into_deleted cascade; +NOTICE: drop cascades to table t1 diff --git a/src/test/regress/expected/merge_into_partition_row_movement.out b/src/test/regress/expected/merge_into_partition_row_movement.out new file mode 100644 index 000000000..3af3d3754 --- /dev/null +++ b/src/test/regress/expected/merge_into_partition_row_movement.out @@ -0,0 +1,267 @@ +create schema merge_into_partition_row_movement; +set search_path = 'merge_into_partition_row_movement'; +create table t1 (c1 int, c2 text, c3 timestamp) +partition by range(c1) +( + partition p1 values less than (100), + partition p2 values less than (200) +); +create table t2 (c1 int); +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +insert into t2 values (2), (102); +begin + update t1 set c1 = 102 where c1 = 1; + update t1 set c1 = 2 where c1 = 101; + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18'); +end; +/ +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; + c1 | c2 | to_char +----+----+------------ + 2 | d | 2023-09-16 +(1 row) + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + c1 | c2 | to_char +-----+----+------------ + 102 | d | 2023-09-15 +(1 row) + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (2); +-- success, concurrently update +\parallel on 2 +begin + update t1 set c1 = 2 where c1 = 1; + update t1 set c1 = 1 where c1 = 101; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | b | 2023-09-16 + 2 | a | 2023-09-15 + 3 | d | 2023-09-18 + 3 | d | 2023-09-18 +(4 rows) + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + c1 | c2 | to_char +----+----+--------- +(0 rows) + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (101); +-- error +\parallel on 2 +begin + update t1 set c1 = 102 where c1 = 1; + update t1 set c1 = 2 where c1 = 101; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18'); +end; +/ +\parallel off +ERROR: partition table update conflict +DETAIL: disable row movement of table can avoid this conflict +CONTEXT: SQL statement "merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18')" +PL/pgSQL function inline_code_block line 3 at SQL statement +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; + c1 | c2 | to_char +----+----+------------ + 2 | b | 2023-09-16 +(1 row) + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + c1 | c2 | to_char +-----+----+------------ + 102 | a | 2023-09-15 +(1 row) + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (2); +create or replace function t1_tri_func() return trigger as +begin + if (old.c1 < 101) then + new.c1 = 150; + else + new.c1 = 50; + end if; + return new; +end; +/ +create trigger t1_tri + before update on t1 + for each row + execute procedure t1_tri_func(); +-- error +\parallel on 2 +begin + update t1 set c1 = 2 where c1 = 1; + update t1 set c1 = 2 where c1 = 101; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18'); +end; +/ +\parallel off +ERROR: partition table update conflict +DETAIL: disable row movement of table can avoid this conflict +CONTEXT: SQL statement "merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18')" +PL/pgSQL function inline_code_block line 3 at SQL statement +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; + c1 | c2 | to_char +----+----+------------ + 50 | b | 2023-09-16 +(1 row) + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + c1 | c2 | to_char +-----+----+------------ + 150 | a | 2023-09-15 +(1 row) + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (101); +-- error +\parallel on 2 +begin + update t1 set c3 = '2023-09-19' where c1 = 1; + update t1 set c3 = '2023-09-20' where c1 = 101; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18'); +end; +/ +\parallel off +ERROR: partition table update conflict +DETAIL: disable row movement of table can avoid this conflict +CONTEXT: SQL statement "merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18')" +PL/pgSQL function inline_code_block line 3 at SQL statement +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; + c1 | c2 | to_char +----+----+------------ + 50 | b | 2023-09-20 +(1 row) + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + c1 | c2 | to_char +-----+----+------------ + 150 | a | 2023-09-19 +(1 row) + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (101); +begin + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18'); +end; +/ +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; + c1 | c2 | to_char +----+----+------------ + 50 | d | 2023-09-16 +(1 row) + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + c1 | c2 | to_char +-----+----+------------ + 150 | d | 2023-09-15 +(1 row) + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (101); +-- error +\parallel on 2 +begin + delete from t1 where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18'); +end; +/ +\parallel off +ERROR: partition table update conflict +DETAIL: disable row movement of table can avoid this conflict +CONTEXT: SQL statement "merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18')" +PL/pgSQL function inline_code_block line 3 at SQL statement +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; + c1 | c2 | to_char +----+----+--------- +(0 rows) + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + c1 | c2 | to_char +-----+----+------------ + 101 | b | 2023-09-16 +(1 row) + +drop schema merge_into_partition_row_movement cascade; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table t1 +drop cascades to table t2 +drop cascades to function t1_tri_func() diff --git a/src/test/regress/expected/merge_into_selfmodified.out b/src/test/regress/expected/merge_into_selfmodified.out new file mode 100644 index 000000000..78cfa7145 --- /dev/null +++ b/src/test/regress/expected/merge_into_selfmodified.out @@ -0,0 +1,133 @@ +create schema merge_into_selfmodified; +set search_path = 'merge_into_selfmodified'; +create table t1 (c1 int, c2 text, c3 timestamp); +create table t2 (c1 int); +insert into t1 values (1, 'a', '2023-09-15'); +create or replace function t1_tri_func() return trigger as +begin + new.c3 = '2023-09-16'; + return new; +end; +/ +create trigger t1_tri + before update on t1 + for each row + execute procedure t1_tri_func(); +-- success, t2 is null, nothing to do +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'b' +when not matched then insert values (2, 'b', '2023-09-17'); +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | a | 2023-09-15 +(1 row) + +-- success, t2 has one row, not matched, do insert +merge into t1 using (select null c1) t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'c' +when not matched then insert values (3, 'c', '2023-09-18'); +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | a | 2023-09-15 + 3 | c | 2023-09-18 +(2 rows) + +insert into t2 values (1); +-- success, matched, do update +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'd' +when not matched then insert values (4, 'd', '2023-09-19'); +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | d | 2023-09-16 + 3 | c | 2023-09-18 +(2 rows) + +insert into t2 values (1); +-- error, affect one row a second time +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'e' +when not matched then insert values (5, 'e', '2023-09-20'); +ERROR: MERGE command cannot affect row a second time +HINT: Ensure that not more than one source row matches any one target row. +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | d | 2023-09-16 + 3 | c | 2023-09-18 +(2 rows) + +set behavior_compat_options = 'merge_update_multi'; +-- success, but update only once +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'f' +when not matched then insert values (6, 'f', '2023-09-21'); +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | f | 2023-09-16 + 3 | c | 2023-09-18 +(2 rows) + +insert into t2 values (7); +-- success, do update and insert +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'g' +when not matched then insert values (7, 'g', '2023-09-22'); +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | g | 2023-09-16 + 3 | c | 2023-09-18 + 7 | g | 2023-09-22 +(3 rows) + +insert into t2 values (8),(8); +-- success, do update and insert only once +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'h' +when not matched then insert values (8, 'h', '2023-09-23'); +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | h | 2023-09-16 + 3 | c | 2023-09-18 + 7 | h | 2023-09-16 + 8 | h | 2023-09-23 + 8 | h | 2023-09-23 +(5 rows) + +insert into t2 values (9),(10); +-- success, do update and insert only once +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'i' +when not matched then insert values (9, 'i', '2023-09-24'); +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | i | 2023-09-16 + 3 | c | 2023-09-18 + 7 | i | 2023-09-16 + 8 | i | 2023-09-16 + 8 | i | 2023-09-16 + 9 | i | 2023-09-24 + 9 | i | 2023-09-24 +(7 rows) + +reset behavior_compat_options; +drop schema merge_into_selfmodified cascade; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to table t1 +drop cascades to table t2 +drop cascades to function t1_tri_func() diff --git a/src/test/regress/expected/merge_into_updated.out b/src/test/regress/expected/merge_into_updated.out new file mode 100644 index 000000000..993e20df3 --- /dev/null +++ b/src/test/regress/expected/merge_into_updated.out @@ -0,0 +1,140 @@ +create schema merge_into_updated; +set search_path = 'merge_into_updated'; +create table t1 (c1 int, c2 text, c3 timestamp); +insert into t1 values (1, 'a', '2023-09-15'); +create or replace function t1_tri_func() return trigger as +begin + new.c3 = '2023-09-16'; + return new; +end; +/ +create trigger t1_tri + before update on t1 + for each row + execute procedure t1_tri_func(); +-- success, matched, do update +\parallel on 2 +begin + update t1 set c2 = 'b' where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'c' + when not matched then insert values (2, 'c', '2023-09-17'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | c | 2023-09-16 +(1 row) + +-- success, matched, do update +\parallel on 2 +begin + update t1 set c2 = 'b' where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + insert into t1 values (1, 'hello', '2023-09-17'); + delete from t1 where c2 = 'hello'; + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (2, 'c', '2023-09-17'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 1 | d | 2023-09-16 +(1 row) + +-- success, concurrently update join condition, not matched, do insert +\parallel on 2 +begin + update t1 set c1 = 2 where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 2 | d | 2023-09-16 + 3 | d | 2023-09-18 +(2 rows) + +-- success, not matched, do insert +\parallel on 2 +begin + update t1 set c2 = 'b'; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + insert into t1 values (1, 'hello', '2023-09-17'); + delete from t1; + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'c' + when not matched then insert values (2, 'c', '2023-09-17'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 2 | c | 2023-09-17 +(1 row) + +-- trigger update the join condition, not matched, do insert +create or replace function t1_tri_func() return trigger as +begin + new.c1 = 4; + return new; +end; +/ +\parallel on 2 +begin + update t1 set c2 = 'b'; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using (select 2 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'e' + when not matched then insert values (4, 'e', '2023-09-19'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + c1 | c2 | to_char +----+----+------------ + 4 | b | 2023-09-17 + 4 | e | 2023-09-19 +(2 rows) + +drop schema merge_into_updated cascade; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to table t1 +drop cascades to function t1_tri_func() diff --git a/src/test/regress/parallel_schedule0 b/src/test/regress/parallel_schedule0 index 6935231d7..098e0b741 100644 --- a/src/test/regress/parallel_schedule0 +++ b/src/test/regress/parallel_schedule0 @@ -277,7 +277,7 @@ test: single_node_forbidden test: single_node_mergeinto merge_subquery merge_subquery3 merge_1 test: merge_where_col -test: merge_concurrent_update_delete_1 merge_concurrent_update_delete_2 merge_concurrent_update_delete_3 +test: merge_concurrent_update_delete_1 merge_concurrent_update_delete_2 merge_concurrent_update_delete_3 merge_into_deleted merge_into_partition_row_movement merge_into_selfmodified merge_into_updated # Trigger tests test: single_node_triggers diff --git a/src/test/regress/parallel_schedule0A b/src/test/regress/parallel_schedule0A index 76da7e9e0..106f8edc9 100644 --- a/src/test/regress/parallel_schedule0A +++ b/src/test/regress/parallel_schedule0A @@ -269,7 +269,7 @@ test: single_node_forbidden test: single_node_mergeinto merge_subquery merge_subquery3 merge_1 test: merge_where_col -test: merge_concurrent_update_delete_1 merge_concurrent_update_delete_2 merge_concurrent_update_delete_3 +test: merge_concurrent_update_delete_1 merge_concurrent_update_delete_2 merge_concurrent_update_delete_3 merge_into_deleted merge_into_partition_row_movement merge_into_selfmodified merge_into_updated # Trigger tests test: single_node_triggers diff --git a/src/test/regress/sql/merge_into_deleted.sql b/src/test/regress/sql/merge_into_deleted.sql new file mode 100644 index 000000000..1432868bf --- /dev/null +++ b/src/test/regress/sql/merge_into_deleted.sql @@ -0,0 +1,44 @@ +create schema merge_into_deleted; +set search_path = 'merge_into_deleted'; + +create table t1 (c1 int, c2 text, c3 timestamp); +insert into t1 values (1, 'a', '2023-09-15'); + +-- concurrently deleted and insert, do update and insert +\parallel on 2 +begin + delete from t1 where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + insert into t1 values (1, 'b', '2023-09-16'); + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'c' + when not matched then insert values (2, 'c', '2023-09-17'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +-- concurrently deleted, not matched, do insert +\parallel on 2 +begin + delete from t1 where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +drop schema merge_into_deleted cascade; diff --git a/src/test/regress/sql/merge_into_partition_row_movement.sql b/src/test/regress/sql/merge_into_partition_row_movement.sql new file mode 100644 index 000000000..82dad97eb --- /dev/null +++ b/src/test/regress/sql/merge_into_partition_row_movement.sql @@ -0,0 +1,182 @@ +create schema merge_into_partition_row_movement; +set search_path = 'merge_into_partition_row_movement'; + +create table t1 (c1 int, c2 text, c3 timestamp) +partition by range(c1) +( + partition p1 values less than (100), + partition p2 values less than (200) +); +create table t2 (c1 int); + +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +insert into t2 values (2), (102); + +begin + update t1 set c1 = 102 where c1 = 1; + update t1 set c1 = 2 where c1 = 101; + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18'); +end; +/ +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (2); + +-- success, concurrently update +\parallel on 2 +begin + update t1 set c1 = 2 where c1 = 1; + update t1 set c1 = 1 where c1 = 101; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (101); + +-- error +\parallel on 2 +begin + update t1 set c1 = 102 where c1 = 1; + update t1 set c1 = 2 where c1 = 101; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (2); + +create or replace function t1_tri_func() return trigger as +begin + if (old.c1 < 101) then + new.c1 = 150; + else + new.c1 = 50; + end if; + return new; +end; +/ +create trigger t1_tri + before update on t1 + for each row + execute procedure t1_tri_func(); + +-- error +\parallel on 2 +begin + update t1 set c1 = 2 where c1 = 1; + update t1 set c1 = 2 where c1 = 101; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (101); + +-- error +\parallel on 2 +begin + update t1 set c3 = '2023-09-19' where c1 = 1; + update t1 set c3 = '2023-09-20' where c1 = 101; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (101); + +begin + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18'); +end; +/ +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + +delete from t1; +insert into t1 values (1, 'a', '2023-09-15'), (101, 'b', '2023-09-16'); +delete from t2; +insert into t2 values (1), (101); + +-- error +\parallel on 2 +begin + delete from t1 where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (4, 'e', '2023-09-18'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p1) order by c1; +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 partition (p2) order by c1; + +drop schema merge_into_partition_row_movement cascade; diff --git a/src/test/regress/sql/merge_into_selfmodified.sql b/src/test/regress/sql/merge_into_selfmodified.sql new file mode 100644 index 000000000..7f2d44b8f --- /dev/null +++ b/src/test/regress/sql/merge_into_selfmodified.sql @@ -0,0 +1,91 @@ +create schema merge_into_selfmodified; +set search_path = 'merge_into_selfmodified'; + +create table t1 (c1 int, c2 text, c3 timestamp); +create table t2 (c1 int); +insert into t1 values (1, 'a', '2023-09-15'); + +create or replace function t1_tri_func() return trigger as +begin + new.c3 = '2023-09-16'; + return new; +end; +/ +create trigger t1_tri + before update on t1 + for each row + execute procedure t1_tri_func(); + +-- success, t2 is null, nothing to do +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'b' +when not matched then insert values (2, 'b', '2023-09-17'); + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +-- success, t2 has one row, not matched, do insert +merge into t1 using (select null c1) t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'c' +when not matched then insert values (3, 'c', '2023-09-18'); + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +insert into t2 values (1); +-- success, matched, do update +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'd' +when not matched then insert values (4, 'd', '2023-09-19'); + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +insert into t2 values (1); +-- error, affect one row a second time +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'e' +when not matched then insert values (5, 'e', '2023-09-20'); + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +set behavior_compat_options = 'merge_update_multi'; +-- success, but update only once +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'f' +when not matched then insert values (6, 'f', '2023-09-21'); + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +insert into t2 values (7); +-- success, do update and insert +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'g' +when not matched then insert values (7, 'g', '2023-09-22'); + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +insert into t2 values (8),(8); +-- success, do update and insert only once +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'h' +when not matched then insert values (8, 'h', '2023-09-23'); + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +insert into t2 values (9),(10); +-- success, do update and insert only once +merge into t1 using t2 +on (t1.c1 = t2.c1) +when matched then update set c2 = 'i' +when not matched then insert values (9, 'i', '2023-09-24'); + +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +reset behavior_compat_options; + +drop schema merge_into_selfmodified cascade; diff --git a/src/test/regress/sql/merge_into_updated.sql b/src/test/regress/sql/merge_into_updated.sql new file mode 100644 index 000000000..4d2b13af3 --- /dev/null +++ b/src/test/regress/sql/merge_into_updated.sql @@ -0,0 +1,118 @@ +create schema merge_into_updated; +set search_path = 'merge_into_updated'; + +create table t1 (c1 int, c2 text, c3 timestamp); +insert into t1 values (1, 'a', '2023-09-15'); + +create or replace function t1_tri_func() return trigger as +begin + new.c3 = '2023-09-16'; + return new; +end; +/ +create trigger t1_tri + before update on t1 + for each row + execute procedure t1_tri_func(); + +-- success, matched, do update +\parallel on 2 +begin + update t1 set c2 = 'b' where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'c' + when not matched then insert values (2, 'c', '2023-09-17'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +-- success, matched, do update +\parallel on 2 +begin + update t1 set c2 = 'b' where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + insert into t1 values (1, 'hello', '2023-09-17'); + delete from t1 where c2 = 'hello'; + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (2, 'c', '2023-09-17'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +-- success, concurrently update join condition, not matched, do insert +\parallel on 2 +begin + update t1 set c1 = 2 where c1 = 1; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'd' + when not matched then insert values (3, 'd', '2023-09-18'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +-- success, not matched, do insert +\parallel on 2 +begin + update t1 set c2 = 'b'; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + insert into t1 values (1, 'hello', '2023-09-17'); + delete from t1; + merge into t1 using (select 1 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'c' + when not matched then insert values (2, 'c', '2023-09-17'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +-- trigger update the join condition, not matched, do insert +create or replace function t1_tri_func() return trigger as +begin + new.c1 = 4; + return new; +end; +/ +\parallel on 2 +begin + update t1 set c2 = 'b'; + perform pg_sleep(3); +end; +/ +begin + perform pg_sleep(1); + merge into t1 using (select 2 c1) t2 + on (t1.c1 = t2.c1) + when matched then update set c2 = 'e' + when not matched then insert values (4, 'e', '2023-09-19'); +end; +/ +\parallel off +select c1, c2, to_char(c3, 'yyyy-mm-dd') from t1 order by c1; + +drop schema merge_into_updated cascade;