Hi, On 2018-04-02 11:26:38 -0400, Robert Haas wrote: > On Wed, Mar 28, 2018 at 2:12 PM, Andres Freund <and...@anarazel.de> wrote: > > How will it break it? They'll see an invalid ctid and conclude that the > > tuple is dead? Without any changes that's already something that can > > happen if a later tuple in the chain has been pruned away. Sure, that > > code won't realize it should error out because the tuple is now in a > > different partition, but neither would a infomask bit. > > > > I think my big problem is that I just don't see what the worst that can > > happen is. We'd potentially see a broken ctid chain, something that very > > commonly happens, and consider the tuple to be invisible. That seems > > pretty sane behaviour for unadapted code, and not any worse than other > > potential solutions. > > This is more or less my feeling as well. I think it's better to > conserve our limited supply of infomask bits as much as we can, and I > do think that we should try to reclaimed HEAP_MOVED_IN and > HEAP_MOVED_OFF in the future instead of defining the combination of > the two of them to mean something now.
Yep. It'd also make locking more complicated or require to keep more information around in HeapUpdateFailureData. In a number of places we currently release the buffer pin before switching over heap_lock_tuple etc results, or there's not even a way to get at the infomask currently (heap_update failing). > Modulo implementation quality, I think the risk > level of this patch is somewhat but not vastly higher than > 37484ad2aacef5ec794f4dd3d5cf814475180a78, which similarly defined a > previously-unused bit pattern in the tuple header. Personally I think that change was vastly riskier, because it affected freezing and wraparounds. Which is something we've repeatedly gotten wrong. > The reason I think this one might be somewhat riskier is because > AFAICS it's not so easy to make sure we've found all the code, even in > core, that might care, as it was in that case; and also because > updates happen more than freezing. Butthe consequences of not catching a changed piece of code are fairly harmless. And I'd say things that happen more often are actually easier to validate than something that with default settings requires hours of testing... I've attached a noticeably editorialized patch: - I'm uncomfortable with the "moved" information not being crash-safe / replicated. Thus I added a new flag to preserve it, and removed the masking of the moved bit in the ctid from heap_mask(). - renamed macros to not mention valid / invalid block numbers, but rather HeapTupleHeaderSetMovedPartitions / HeapTupleHeaderIndicatesMovedPartitions and ItemPointerSetMovedPartitions / ItemPointerIndicatesMovedPartitions I'm not wedded to these names, but I'l be adamant they they're not talking about invalid block numbers. Makes code harder to understand imo. - removed new assertion from heap_get_latest_tid(), it's wrong for the case where all row versions are invisible. - editorialized comments a bit - added a few more assertions I went through the existing code to make sure that a) no checks where missed b) to evaluate what the consequences when chasing chains would be c) to evaluate what the consequences when we miss erroring out WRT b), it's usually just superflous extra work if the new checks weren't there. I went through all callers accessing xmax (via GetRawXmax and GetUpdateXid): b) - heap rewrites will keep a tuple in hashtable till end of run, then reset the ctid to self. No real corruption, but we'd not detect further errors when attempting to follow chain. - EvalPlanQualFetch would fail to abort loop, attempt to fetch tuple. This'll extend the relation by a single page, because P_NEW == InvalidBlockNumber. - heap_prune_chain - no changes needed (delete isn't recursed through) - heap_get_root_tuples - same - heap_hot_search_buffer - only continues over hot updates - heap_lock_tuple (and subsidiary routines) - same as EvalPlanQualFetch, would then return HeapTupleUpdated. c) - GetTupleForTrigger - the proper error wouldn't be raised, instead a NULL tuple would be passed to the trigger - EvalPlanQualFetch - a NULL tuple would be returned after the consequences above - RelationFindReplTupleBy* - wrong error message - ExecLockRows - no error would be raised, continue normally - ExecDelete() - tuple ignored without error - ExecUpdate() - same Questions: - I'm not perfectly happy with "tuple to be locked was already moved to another partition due to concurrent update" as the error message. If somebody has a better suggestions. - should heap_get_latest_tid() error out when the chain ends in a moved tuple? I personally think it doesn't matter much, the functionality is so bonkers and underspecified that it doesn't matter anyway ;) - I'm not that happy with the number of added spec test files with number postfixes. Can't we combine them into a single file? - as remarked elsewhere on this thread, I think the used errcode should be a serialization failure Greetings, Andres Freund
>From 49108d22baad33f1aae253e7c45ac18a2c41ab33 Mon Sep 17 00:00:00 2001 From: Andres Freund <and...@anarazel.de> Date: Wed, 4 Apr 2018 18:43:36 -0700 Subject: [PATCH v8] Raise error when affecting tuple moved into different partition. --- src/backend/access/heap/heapam.c | 39 ++++- src/backend/access/heap/pruneheap.c | 6 + src/backend/access/heap/rewriteheap.c | 1 + src/backend/commands/trigger.c | 5 + src/backend/executor/execMain.c | 7 +- src/backend/executor/execReplication.c | 22 ++- src/backend/executor/nodeLockRows.c | 5 + src/backend/executor/nodeMerge.c | 2 +- src/backend/executor/nodeModifyTable.c | 27 +++- src/include/access/heapam.h | 2 +- src/include/access/heapam_xlog.h | 1 + src/include/access/htup_details.h | 12 +- src/include/executor/nodeModifyTable.h | 3 +- src/include/storage/itemptr.h | 16 ++ src/test/isolation/expected/merge-update.out | 25 ++++ .../expected/partition-key-update-1.out | 43 ++++++ .../expected/partition-key-update-2.out | 23 +++ .../expected/partition-key-update-3.out | 9 ++ .../expected/partition-key-update-4.out | 29 ++++ .../expected/partition-key-update-5.out | 139 ++++++++++++++++++ src/test/isolation/isolation_schedule | 5 + src/test/isolation/specs/merge-update.spec | 3 +- .../specs/partition-key-update-1.spec | 39 +++++ .../specs/partition-key-update-2.spec | 41 ++++++ .../specs/partition-key-update-3.spec | 32 ++++ .../specs/partition-key-update-4.spec | 45 ++++++ .../specs/partition-key-update-5.spec | 44 ++++++ 27 files changed, 602 insertions(+), 23 deletions(-) create mode 100644 src/test/isolation/expected/partition-key-update-1.out create mode 100644 src/test/isolation/expected/partition-key-update-2.out create mode 100644 src/test/isolation/expected/partition-key-update-3.out create mode 100644 src/test/isolation/expected/partition-key-update-4.out create mode 100644 src/test/isolation/expected/partition-key-update-5.out create mode 100644 src/test/isolation/specs/partition-key-update-1.spec create mode 100644 src/test/isolation/specs/partition-key-update-2.spec create mode 100644 src/test/isolation/specs/partition-key-update-3.spec create mode 100644 src/test/isolation/specs/partition-key-update-4.spec create mode 100644 src/test/isolation/specs/partition-key-update-5.spec diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index f96567f5d51..8ffbf6471ca 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -2308,6 +2308,7 @@ heap_get_latest_tid(Relation relation, */ if ((tp.t_data->t_infomask & HEAP_XMAX_INVALID) || HeapTupleHeaderIsOnlyLocked(tp.t_data) || + HeapTupleHeaderIndicatesMovedPartitions(tp.t_data) || ItemPointerEquals(&tp.t_self, &tp.t_data->t_ctid)) { UnlockReleaseBuffer(buffer); @@ -3041,6 +3042,8 @@ xmax_infomask_changed(uint16 new_infomask, uint16 old_infomask) * crosscheck - if not InvalidSnapshot, also check tuple against this * wait - true if should wait for any conflicting update to commit/abort * hufd - output parameter, filled in failure cases (see below) + * changing_part - true iff the tuple is being moved to another partition + * table due to an update of the partition key. Otherwise, false. * * Normal, successful return value is HeapTupleMayBeUpdated, which * actually means we did delete it. Failure return codes are @@ -3056,7 +3059,7 @@ xmax_infomask_changed(uint16 new_infomask, uint16 old_infomask) HTSU_Result heap_delete(Relation relation, ItemPointer tid, CommandId cid, Snapshot crosscheck, bool wait, - HeapUpdateFailureData *hufd) + HeapUpdateFailureData *hufd, bool changing_part) { HTSU_Result result; TransactionId xid = GetCurrentTransactionId(); @@ -3325,6 +3328,10 @@ l1: /* Make sure there is no forward chain link in t_ctid */ tp.t_data->t_ctid = tp.t_self; + /* Signal that this is actually a move into another partition */ + if (changing_part) + HeapTupleHeaderSetMovedPartitions(tp.t_data); + MarkBufferDirty(buffer); /* @@ -3342,7 +3349,11 @@ l1: if (RelationIsAccessibleInLogicalDecoding(relation)) log_heap_new_cid(relation, &tp); - xlrec.flags = all_visible_cleared ? XLH_DELETE_ALL_VISIBLE_CLEARED : 0; + xlrec.flags = 0; + if (all_visible_cleared) + xlrec.flags |= XLH_DELETE_ALL_VISIBLE_CLEARED; + if (changing_part) + xlrec.flags |= XLH_DELETE_IS_PARTITION_MOVE; xlrec.infobits_set = compute_infobits(tp.t_data->t_infomask, tp.t_data->t_infomask2); xlrec.offnum = ItemPointerGetOffsetNumber(&tp.t_self); @@ -3450,7 +3461,7 @@ simple_heap_delete(Relation relation, ItemPointer tid) result = heap_delete(relation, tid, GetCurrentCommandId(true), InvalidSnapshot, true /* wait for commit */ , - &hufd); + &hufd, false /* changing_part */); switch (result) { case HeapTupleSelfUpdated: @@ -6051,6 +6062,7 @@ l4: next: /* if we find the end of update chain, we're done. */ if (mytup.t_data->t_infomask & HEAP_XMAX_INVALID || + HeapTupleHeaderIndicatesMovedPartitions(mytup.t_data) || ItemPointerEquals(&mytup.t_self, &mytup.t_data->t_ctid) || HeapTupleHeaderIsOnlyLocked(mytup.t_data)) { @@ -6102,7 +6114,12 @@ static HTSU_Result heap_lock_updated_tuple(Relation rel, HeapTuple tuple, ItemPointer ctid, TransactionId xid, LockTupleMode mode) { - if (!ItemPointerEquals(&tuple->t_self, ctid)) + /* + * If the tuple has not been updated, or has moved into another partition + * (effectively a delete) stop here. + */ + if (!HeapTupleHeaderIndicatesMovedPartitions(tuple->t_data) && + !ItemPointerEquals(&tuple->t_self, ctid)) { /* * If this is the first possibly-multixact-able operation in the @@ -8495,8 +8512,11 @@ heap_xlog_delete(XLogReaderState *record) if (xlrec->flags & XLH_DELETE_ALL_VISIBLE_CLEARED) PageClearAllVisible(page); - /* Make sure there is no forward chain link in t_ctid */ - htup->t_ctid = target_tid; + /* Make sure t_ctid is set correctly */ + if (xlrec->flags & XLH_DELETE_IS_PARTITION_MOVE) + HeapTupleHeaderSetMovedPartitions(htup); + else + htup->t_ctid = target_tid; PageSetLSN(page, lsn); MarkBufferDirty(buffer); } @@ -9417,6 +9437,13 @@ heap_mask(char *pagedata, BlockNumber blkno) */ if (HeapTupleHeaderIsSpeculative(page_htup)) ItemPointerSet(&page_htup->t_ctid, blkno, off); + + /* + * NB: Not ignoring ctid changes due to the tuple having moved + * (i.e. HeapTupleHeaderIndicatesMovedPartitions), because that's + * important information that needs to be in-sync between primary + * and standby, and thus is WAL logged. + */ } /* diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index f67d7d15df1..c2f5343dac8 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -552,6 +552,9 @@ heap_prune_chain(Relation relation, Buffer buffer, OffsetNumber rootoffnum, if (!HeapTupleHeaderIsHotUpdated(htup)) break; + /* HOT implies it can't have moved to different partition */ + Assert(!HeapTupleHeaderIndicatesMovedPartitions(htup)); + /* * Advance to next chain member. */ @@ -823,6 +826,9 @@ heap_get_root_tuples(Page page, OffsetNumber *root_offsets) if (!HeapTupleHeaderIsHotUpdated(htup)) break; + /* HOT implies it can't have moved to different partition */ + Assert(!HeapTupleHeaderIndicatesMovedPartitions(htup)); + nextoffnum = ItemPointerGetOffsetNumber(&htup->t_ctid); priorXmax = HeapTupleHeaderGetUpdateXid(htup); } diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c index 7d466c2588c..8d3c861a330 100644 --- a/src/backend/access/heap/rewriteheap.c +++ b/src/backend/access/heap/rewriteheap.c @@ -424,6 +424,7 @@ rewrite_heap_tuple(RewriteState state, */ if (!((old_tuple->t_data->t_infomask & HEAP_XMAX_INVALID) || HeapTupleHeaderIsOnlyLocked(old_tuple->t_data)) && + !HeapTupleHeaderIndicatesMovedPartitions(old_tuple->t_data) && !(ItemPointerEquals(&(old_tuple->t_self), &(old_tuple->t_data->t_ctid)))) { diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c index e71f921fda1..c263f3a149a 100644 --- a/src/backend/commands/trigger.c +++ b/src/backend/commands/trigger.c @@ -3308,6 +3308,11 @@ ltrmark:; ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("could not serialize access due to concurrent update"))); + if (ItemPointerIndicatesMovedPartitions(&hufd.ctid)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("tuple to be locked was already moved to another partition due to concurrent update"))); + if (!ItemPointerEquals(&hufd.ctid, &tuple.t_self)) { /* it was updated, so look at the updated version */ diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index e4d9b0b3f88..69a839c9c60 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -2739,6 +2739,10 @@ EvalPlanQualFetch(EState *estate, Relation relation, int lockmode, ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("could not serialize access due to concurrent update"))); + if (ItemPointerIndicatesMovedPartitions(&hufd.ctid)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("tuple to be locked was already moved to another partition due to concurrent update"))); /* Should not encounter speculative tuple on recheck */ Assert(!HeapTupleHeaderIsSpeculative(tuple.t_data)); @@ -2807,7 +2811,8 @@ EvalPlanQualFetch(EState *estate, Relation relation, int lockmode, * As above, it should be safe to examine xmax and t_ctid without the * buffer content lock, because they can't be changing. */ - if (ItemPointerEquals(&tuple.t_self, &tuple.t_data->t_ctid)) + if (HeapTupleHeaderIndicatesMovedPartitions(tuple.t_data) || + ItemPointerEquals(&tuple.t_self, &tuple.t_data->t_ctid)) { /* deleted, so forget about it */ ReleaseBuffer(buffer); diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index 971f92a938a..c90db13f9ca 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -191,9 +191,14 @@ retry: break; case HeapTupleUpdated: /* XXX: Improve handling here */ - ereport(LOG, - (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("concurrent update, retrying"))); + if (ItemPointerIndicatesMovedPartitions(&hufd.ctid)) + ereport(LOG, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("tuple to be locked was already moved to another partition due to concurrent update, retrying"))); + else + ereport(LOG, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("concurrent update, retrying"))); goto retry; case HeapTupleInvisible: elog(ERROR, "attempted to lock invisible tuple"); @@ -349,9 +354,14 @@ retry: break; case HeapTupleUpdated: /* XXX: Improve handling here */ - ereport(LOG, - (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("concurrent update, retrying"))); + if (ItemPointerIndicatesMovedPartitions(&hufd.ctid)) + ereport(LOG, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("tuple to be locked was already moved to another partition due to concurrent update, retrying"))); + else + ereport(LOG, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("concurrent update, retrying"))); goto retry; case HeapTupleInvisible: elog(ERROR, "attempted to lock invisible tuple"); diff --git a/src/backend/executor/nodeLockRows.c b/src/backend/executor/nodeLockRows.c index b39ccf7dc13..cfe8e630d38 100644 --- a/src/backend/executor/nodeLockRows.c +++ b/src/backend/executor/nodeLockRows.c @@ -218,6 +218,11 @@ lnext: ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("could not serialize access due to concurrent update"))); + if (ItemPointerIndicatesMovedPartitions(&hufd.ctid)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("tuple to be locked was already moved to another partition due to concurrent update"))); + if (ItemPointerEquals(&hufd.ctid, &tuple.t_self)) { /* Tuple was deleted, so don't return it */ diff --git a/src/backend/executor/nodeMerge.c b/src/backend/executor/nodeMerge.c index 0e0d0795d4d..7dd354dde2f 100644 --- a/src/backend/executor/nodeMerge.c +++ b/src/backend/executor/nodeMerge.c @@ -222,7 +222,7 @@ lmerge_matched:; slot = ExecDelete(mtstate, tupleid, NULL, slot, epqstate, estate, &tuple_deleted, false, &hufd, action, - mtstate->canSetTag); + mtstate->canSetTag, false /* changingPart */); break; diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index b03db64e8e1..68d95774607 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -650,7 +650,8 @@ ExecDelete(ModifyTableState *mtstate, bool processReturning, HeapUpdateFailureData *hufdp, MergeActionState *actionState, - bool canSetTag) + bool canSetTag, + bool changingPart) { ResultRelInfo *resultRelInfo; Relation resultRelationDesc; @@ -749,7 +750,8 @@ ldelete:; estate->es_output_cid, estate->es_crosscheck_snapshot, true /* wait for commit */ , - &hufd); + &hufd, + changingPart); /* * Copy the necessary information, if the caller has asked for it. We @@ -808,6 +810,10 @@ ldelete:; ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("could not serialize access due to concurrent update"))); + if (ItemPointerIndicatesMovedPartitions(&hufd.ctid)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("tuple to be updated was already moved to another partition due to concurrent update"))); if (!ItemPointerEquals(tupleid, &hufd.ctid)) { @@ -1162,7 +1168,7 @@ lreplace:; */ ExecDelete(mtstate, tupleid, oldtuple, planSlot, epqstate, estate, &tuple_deleted, false, hufdp, NULL, - false); + false /* canSetTag */, true /* changingPart */); /* * For some reason if DELETE didn't happen (e.g. trigger prevented @@ -1338,6 +1344,10 @@ lreplace:; ereport(ERROR, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("could not serialize access due to concurrent update"))); + if (ItemPointerIndicatesMovedPartitions(&hufd.ctid)) + ereport(ERROR, + (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("tuple to be updated was already moved to another partition due to concurrent update"))); if (!ItemPointerEquals(tupleid, &hufd.ctid)) { @@ -1527,6 +1537,14 @@ ExecOnConflictUpdate(ModifyTableState *mtstate, (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("could not serialize access due to concurrent update"))); + /* + * As long as we don't support an UPDATE of INSERT ON CONFLICT for + * a partitioned table we shouldn't reach to a case where tuple to + * be lock is moved to another partition due to concurrent update + * of the partition key. + */ + Assert(!ItemPointerIndicatesMovedPartitions(&hufd.ctid)); + /* * Tell caller to try again from the very start. * @@ -2269,7 +2287,8 @@ ExecModifyTable(PlanState *pstate) case CMD_DELETE: slot = ExecDelete(node, tupleid, oldtuple, planSlot, &node->mt_epqstate, estate, - NULL, true, NULL, NULL, node->canSetTag); + NULL, true, NULL, NULL, node->canSetTag, + false /* changingPart */); break; default: elog(ERROR, "unknown operation"); diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index 608f50b0616..048d6317f79 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -167,7 +167,7 @@ extern void heap_multi_insert(Relation relation, HeapTuple *tuples, int ntuples, CommandId cid, int options, BulkInsertState bistate); extern HTSU_Result heap_delete(Relation relation, ItemPointer tid, CommandId cid, Snapshot crosscheck, bool wait, - HeapUpdateFailureData *hufd); + HeapUpdateFailureData *hufd, bool changing_part); extern void heap_finish_speculative(Relation relation, HeapTuple tuple); extern void heap_abort_speculative(Relation relation, HeapTuple tuple); extern HTSU_Result heap_update(Relation relation, ItemPointer otid, diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h index 700e25c36a1..3c9214da6f5 100644 --- a/src/include/access/heapam_xlog.h +++ b/src/include/access/heapam_xlog.h @@ -93,6 +93,7 @@ #define XLH_DELETE_CONTAINS_OLD_TUPLE (1<<1) #define XLH_DELETE_CONTAINS_OLD_KEY (1<<2) #define XLH_DELETE_IS_SUPER (1<<3) +#define XLH_DELETE_IS_PARTITION_MOVE (1<<4) /* convenience macro for checking whether any form of old tuple was logged */ #define XLH_DELETE_CONTAINS_OLD \ diff --git a/src/include/access/htup_details.h b/src/include/access/htup_details.h index cebaea097d1..cf56d4ace43 100644 --- a/src/include/access/htup_details.h +++ b/src/include/access/htup_details.h @@ -83,8 +83,10 @@ * * A word about t_ctid: whenever a new tuple is stored on disk, its t_ctid * is initialized with its own TID (location). If the tuple is ever updated, - * its t_ctid is changed to point to the replacement version of the tuple. - * Thus, a tuple is the latest version of its row iff XMAX is invalid or + * its t_ctid is changed to point to the replacement version of the tuple or + * the block number (ip_blkid) is invalidated if the tuple is moved from one + * partition to another partition relation due to an update of the partition + * key. Thus, a tuple is the latest version of its row iff XMAX is invalid or * t_ctid points to itself (in which case, if XMAX is valid, the tuple is * either locked or deleted). One can follow the chain of t_ctid links * to find the newest version of the row. Beware however that VACUUM might @@ -445,6 +447,12 @@ do { \ ItemPointerSet(&(tup)->t_ctid, token, SpecTokenOffsetNumber) \ ) +#define HeapTupleHeaderSetMovedPartitions(tup) \ + ItemPointerSetMovedPartitions(&(tup)->t_ctid) + +#define HeapTupleHeaderIndicatesMovedPartitions(tup) \ + ItemPointerIndicatesMovedPartitions(&tup->t_ctid) + #define HeapTupleHeaderGetDatumLength(tup) \ VARSIZE(tup) diff --git a/src/include/executor/nodeModifyTable.h b/src/include/executor/nodeModifyTable.h index 686cfa61710..182506ea5fd 100644 --- a/src/include/executor/nodeModifyTable.h +++ b/src/include/executor/nodeModifyTable.h @@ -27,7 +27,8 @@ extern TupleTableSlot *ExecDelete(ModifyTableState *mtstate, ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *planSlot, EPQState *epqstate, EState *estate, bool *tupleDeleted, bool processReturning, HeapUpdateFailureData *hufdp, - MergeActionState *actionState, bool canSetTag); + MergeActionState *actionState, bool canSetTag, + bool changingPart); extern TupleTableSlot *ExecUpdate(ModifyTableState *mtstate, ItemPointer tupleid, HeapTuple oldtuple, TupleTableSlot *slot, TupleTableSlot *planSlot, EPQState *epqstate, EState *estate, diff --git a/src/include/storage/itemptr.h b/src/include/storage/itemptr.h index 6c9ed3696b7..626c98f9691 100644 --- a/src/include/storage/itemptr.h +++ b/src/include/storage/itemptr.h @@ -154,6 +154,22 @@ typedef ItemPointerData *ItemPointer; (pointer)->ip_posid = InvalidOffsetNumber \ ) +/* + * ItemPointerIndicatesMovedPartitions + * True iff the block number indicates the tuple has moved to another + * partition. + */ +#define ItemPointerIndicatesMovedPartitions(pointer) \ + !BlockNumberIsValid(ItemPointerGetBlockNumberNoCheck(pointer)) + +/* + * ItemPointerSetMovedPartitions + * Indicate that the item referenced by the itempointer has moved into a + * different partition. + */ +#define ItemPointerSetMovedPartitions(pointer) \ + ItemPointerSetBlockNumber((pointer), InvalidBlockNumber) + /* ---------------- * externs * ---------------- diff --git a/src/test/isolation/expected/merge-update.out b/src/test/isolation/expected/merge-update.out index 60ae42ebd0f..321063b1a44 100644 --- a/src/test/isolation/expected/merge-update.out +++ b/src/test/isolation/expected/merge-update.out @@ -204,6 +204,31 @@ step pa_merge2a: <waiting ...> step c1: COMMIT; step pa_merge2a: <... completed> +error in steps c1 pa_merge2a: ERROR: tuple to be updated was already moved to another partition due to concurrent update +step pa_select2: SELECT * FROM pa_target; +ERROR: current transaction is aborted, commands ignored until end of transaction block +step c2: COMMIT; + +starting permutation: pa_merge2 c1 pa_merge2a pa_select2 c2 +step pa_merge2: + MERGE INTO pa_target t + USING (SELECT 1 as key, 'pa_merge1' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; + +step c1: COMMIT; +step pa_merge2a: + MERGE INTO pa_target t + USING (SELECT 1 as key, 'pa_merge2a' as val) s + ON s.key = t.key + WHEN NOT MATCHED THEN + INSERT VALUES (s.key, s.val) + WHEN MATCHED THEN + UPDATE set key = t.key + 1, val = t.val || ' updated by ' || s.val; + step pa_select2: SELECT * FROM pa_target; key val diff --git a/src/test/isolation/expected/partition-key-update-1.out b/src/test/isolation/expected/partition-key-update-1.out new file mode 100644 index 00000000000..bfbeccc852d --- /dev/null +++ b/src/test/isolation/expected/partition-key-update-1.out @@ -0,0 +1,43 @@ +Parsed test spec with 2 sessions + +starting permutation: s1u s1c s2u s2c +step s1u: UPDATE foo SET a=2 WHERE a=1; +step s1c: COMMIT; +step s2u: UPDATE foo SET b='EFG' WHERE a=1; +step s2c: COMMIT; + +starting permutation: s1u s2u s1c s2c +step s1u: UPDATE foo SET a=2 WHERE a=1; +step s2u: UPDATE foo SET b='EFG' WHERE a=1; <waiting ...> +step s1c: COMMIT; +step s2u: <... completed> +error in steps s1c s2u: ERROR: tuple to be updated was already moved to another partition due to concurrent update +step s2c: COMMIT; + +starting permutation: s2u s1u s2c s1c +step s2u: UPDATE foo SET b='EFG' WHERE a=1; +step s1u: UPDATE foo SET a=2 WHERE a=1; <waiting ...> +step s2c: COMMIT; +step s1u: <... completed> +step s1c: COMMIT; + +starting permutation: s1u s1c s2d s2c +step s1u: UPDATE foo SET a=2 WHERE a=1; +step s1c: COMMIT; +step s2d: DELETE FROM foo WHERE a=1; +step s2c: COMMIT; + +starting permutation: s1u s2d s1c s2c +step s1u: UPDATE foo SET a=2 WHERE a=1; +step s2d: DELETE FROM foo WHERE a=1; <waiting ...> +step s1c: COMMIT; +step s2d: <... completed> +error in steps s1c s2d: ERROR: tuple to be updated was already moved to another partition due to concurrent update +step s2c: COMMIT; + +starting permutation: s2d s1u s2c s1c +step s2d: DELETE FROM foo WHERE a=1; +step s1u: UPDATE foo SET a=2 WHERE a=1; <waiting ...> +step s2c: COMMIT; +step s1u: <... completed> +step s1c: COMMIT; diff --git a/src/test/isolation/expected/partition-key-update-2.out b/src/test/isolation/expected/partition-key-update-2.out new file mode 100644 index 00000000000..06460a8da76 --- /dev/null +++ b/src/test/isolation/expected/partition-key-update-2.out @@ -0,0 +1,23 @@ +Parsed test spec with 2 sessions + +starting permutation: s1u s1c s2u s2c +step s1u: UPDATE foo SET b='EFG' WHERE a=1; +step s1c: COMMIT; +step s2u: UPDATE foo SET b='XYZ' WHERE a=1; +step s2c: COMMIT; + +starting permutation: s1u s2u s1c s2c +step s1u: UPDATE foo SET b='EFG' WHERE a=1; +step s2u: UPDATE foo SET b='XYZ' WHERE a=1; <waiting ...> +step s1c: COMMIT; +step s2u: <... completed> +error in steps s1c s2u: ERROR: tuple to be locked was already moved to another partition due to concurrent update +step s2c: COMMIT; + +starting permutation: s2u s1u s2c s1c +step s2u: UPDATE foo SET b='XYZ' WHERE a=1; +step s1u: UPDATE foo SET b='EFG' WHERE a=1; <waiting ...> +step s2c: COMMIT; +step s1u: <... completed> +error in steps s2c s1u: ERROR: tuple to be locked was already moved to another partition due to concurrent update +step s1c: COMMIT; diff --git a/src/test/isolation/expected/partition-key-update-3.out b/src/test/isolation/expected/partition-key-update-3.out new file mode 100644 index 00000000000..1be63dfb8be --- /dev/null +++ b/src/test/isolation/expected/partition-key-update-3.out @@ -0,0 +1,9 @@ +Parsed test spec with 2 sessions + +starting permutation: s1u3 s2i s1c s2c +step s1u3: UPDATE foo_r SET a=11 WHERE a=7 AND b = 'ABC'; +step s2i: INSERT INTO bar VALUES(7); <waiting ...> +step s1c: COMMIT; +step s2i: <... completed> +error in steps s1c s2i: ERROR: tuple to be locked was already moved to another partition due to concurrent update +step s2c: COMMIT; diff --git a/src/test/isolation/expected/partition-key-update-4.out b/src/test/isolation/expected/partition-key-update-4.out new file mode 100644 index 00000000000..363de0d69c2 --- /dev/null +++ b/src/test/isolation/expected/partition-key-update-4.out @@ -0,0 +1,29 @@ +Parsed test spec with 3 sessions + +starting permutation: s1u s2donothing s3donothing s1c s2c s3select s3c +step s1u: UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; +step s2donothing: INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; <waiting ...> +step s3donothing: INSERT INTO foo VALUES(2, 'session-3 donothing') ON CONFLICT DO NOTHING; <waiting ...> +step s1c: COMMIT; +step s2donothing: <... completed> +step s3donothing: <... completed> +step s2c: COMMIT; +step s3select: SELECT * FROM foo ORDER BY a; +a b + +1 session-2 donothing +2 initial tuple -> moved by session-1 +step s3c: COMMIT; + +starting permutation: s2donothing s1u s3donothing s1c s2c s3select s3c +step s2donothing: INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; +step s1u: UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; +step s3donothing: INSERT INTO foo VALUES(2, 'session-3 donothing') ON CONFLICT DO NOTHING; <waiting ...> +step s1c: COMMIT; +step s3donothing: <... completed> +step s2c: COMMIT; +step s3select: SELECT * FROM foo ORDER BY a; +a b + +2 initial tuple -> moved by session-1 +step s3c: COMMIT; diff --git a/src/test/isolation/expected/partition-key-update-5.out b/src/test/isolation/expected/partition-key-update-5.out new file mode 100644 index 00000000000..42dfe64ad31 --- /dev/null +++ b/src/test/isolation/expected/partition-key-update-5.out @@ -0,0 +1,139 @@ +Parsed test spec with 3 sessions + +starting permutation: s2beginrr s3beginrr s1u s2donothing s1c s2c s3donothing s3c s2select +step s2beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s3beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1u: UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; +step s2donothing: INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; <waiting ...> +step s1c: COMMIT; +step s2donothing: <... completed> +step s2c: COMMIT; +step s3donothing: INSERT INTO foo VALUES(2, 'session-3 donothing'), (2, 'session-3 donothing2') ON CONFLICT DO NOTHING; +step s3c: COMMIT; +step s2select: SELECT * FROM foo ORDER BY a; +a b + +1 session-2 donothing +2 initial tuple -> moved by session-1 + +starting permutation: s2beginrr s3beginrr s1u s3donothing s1c s3c s2donothing s2c s2select +step s2beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s3beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1u: UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; +step s3donothing: INSERT INTO foo VALUES(2, 'session-3 donothing'), (2, 'session-3 donothing2') ON CONFLICT DO NOTHING; <waiting ...> +step s1c: COMMIT; +step s3donothing: <... completed> +error in steps s1c s3donothing: ERROR: could not serialize access due to concurrent update +step s3c: COMMIT; +step s2donothing: INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; +step s2c: COMMIT; +step s2select: SELECT * FROM foo ORDER BY a; +a b + +1 session-2 donothing +2 initial tuple -> moved by session-1 + +starting permutation: s2beginrr s3beginrr s1u s2donothing s3donothing s1c s2c s3c s2select +step s2beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s3beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1u: UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; +step s2donothing: INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; <waiting ...> +step s3donothing: INSERT INTO foo VALUES(2, 'session-3 donothing'), (2, 'session-3 donothing2') ON CONFLICT DO NOTHING; <waiting ...> +step s1c: COMMIT; +step s2donothing: <... completed> +step s3donothing: <... completed> +error in steps s1c s2donothing s3donothing: ERROR: could not serialize access due to concurrent update +step s2c: COMMIT; +step s3c: COMMIT; +step s2select: SELECT * FROM foo ORDER BY a; +a b + +1 session-2 donothing +2 initial tuple -> moved by session-1 + +starting permutation: s2beginrr s3beginrr s1u s3donothing s2donothing s1c s3c s2c s2select +step s2beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s3beginrr: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1u: UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; +step s3donothing: INSERT INTO foo VALUES(2, 'session-3 donothing'), (2, 'session-3 donothing2') ON CONFLICT DO NOTHING; <waiting ...> +step s2donothing: INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; <waiting ...> +step s1c: COMMIT; +step s3donothing: <... completed> +step s2donothing: <... completed> +error in steps s1c s3donothing s2donothing: ERROR: could not serialize access due to concurrent update +step s3c: COMMIT; +step s2c: COMMIT; +step s2select: SELECT * FROM foo ORDER BY a; +a b + +1 session-2 donothing +2 initial tuple -> moved by session-1 + +starting permutation: s2begins s3begins s1u s2donothing s1c s2c s3donothing s3c s2select +step s2begins: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s3begins: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1u: UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; +step s2donothing: INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; <waiting ...> +step s1c: COMMIT; +step s2donothing: <... completed> +step s2c: COMMIT; +step s3donothing: INSERT INTO foo VALUES(2, 'session-3 donothing'), (2, 'session-3 donothing2') ON CONFLICT DO NOTHING; +step s3c: COMMIT; +step s2select: SELECT * FROM foo ORDER BY a; +a b + +1 session-2 donothing +2 initial tuple -> moved by session-1 + +starting permutation: s2begins s3begins s1u s3donothing s1c s3c s2donothing s2c s2select +step s2begins: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s3begins: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1u: UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; +step s3donothing: INSERT INTO foo VALUES(2, 'session-3 donothing'), (2, 'session-3 donothing2') ON CONFLICT DO NOTHING; <waiting ...> +step s1c: COMMIT; +step s3donothing: <... completed> +error in steps s1c s3donothing: ERROR: could not serialize access due to concurrent update +step s3c: COMMIT; +step s2donothing: INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; +step s2c: COMMIT; +step s2select: SELECT * FROM foo ORDER BY a; +a b + +1 session-2 donothing +2 initial tuple -> moved by session-1 + +starting permutation: s2begins s3begins s1u s2donothing s3donothing s1c s2c s3c s2select +step s2begins: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s3begins: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1u: UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; +step s2donothing: INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; <waiting ...> +step s3donothing: INSERT INTO foo VALUES(2, 'session-3 donothing'), (2, 'session-3 donothing2') ON CONFLICT DO NOTHING; <waiting ...> +step s1c: COMMIT; +step s2donothing: <... completed> +step s3donothing: <... completed> +error in steps s1c s2donothing s3donothing: ERROR: could not serialize access due to concurrent update +step s2c: COMMIT; +step s3c: COMMIT; +step s2select: SELECT * FROM foo ORDER BY a; +a b + +1 session-2 donothing +2 initial tuple -> moved by session-1 + +starting permutation: s2begins s3begins s1u s3donothing s2donothing s1c s3c s2c s2select +step s2begins: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s3begins: BEGIN ISOLATION LEVEL SERIALIZABLE; +step s1u: UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; +step s3donothing: INSERT INTO foo VALUES(2, 'session-3 donothing'), (2, 'session-3 donothing2') ON CONFLICT DO NOTHING; <waiting ...> +step s2donothing: INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; <waiting ...> +step s1c: COMMIT; +step s3donothing: <... completed> +step s2donothing: <... completed> +error in steps s1c s3donothing s2donothing: ERROR: could not serialize access due to concurrent update +step s3c: COMMIT; +step s2c: COMMIT; +step s2select: SELECT * FROM foo ORDER BY a; +a b + +1 session-2 donothing +2 initial tuple -> moved by session-1 diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index 99dd7c6bdbf..14b9f2e7122 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -72,3 +72,8 @@ test: timeouts test: vacuum-concurrent-drop test: predicate-gist test: predicate-gin +test: partition-key-update-1 +test: partition-key-update-2 +test: partition-key-update-3 +test: partition-key-update-4 +test: partition-key-update-5 diff --git a/src/test/isolation/specs/merge-update.spec b/src/test/isolation/specs/merge-update.spec index 64e849966ec..625b477eb9f 100644 --- a/src/test/isolation/specs/merge-update.spec +++ b/src/test/isolation/specs/merge-update.spec @@ -129,4 +129,5 @@ permutation "merge1" "merge2a" "a1" "select2" "c2" permutation "merge1" "merge2b" "c1" "select2" "c2" permutation "merge1" "merge2c" "c1" "select2" "c2" permutation "pa_merge1" "pa_merge2a" "c1" "pa_select2" "c2" -permutation "pa_merge2" "pa_merge2a" "c1" "pa_select2" "c2" +permutation "pa_merge2" "pa_merge2a" "c1" "pa_select2" "c2" # fails +permutation "pa_merge2" "c1" "pa_merge2a" "pa_select2" "c2" # succeeds diff --git a/src/test/isolation/specs/partition-key-update-1.spec b/src/test/isolation/specs/partition-key-update-1.spec new file mode 100644 index 00000000000..32d555c37cd --- /dev/null +++ b/src/test/isolation/specs/partition-key-update-1.spec @@ -0,0 +1,39 @@ +# Concurrency error from ExecUpdate and ExecDelete. + +# Throw an error to indicate that the targeted row has been already moved to +# another partition in the case of concurrency where a session trying to +# update/delete a row that's locked for a concurrent update by the another +# session cause tuple movement to the another partition due update of partition +# key. + +setup +{ + CREATE TABLE foo (a int, b text) PARTITION BY LIST(a); + CREATE TABLE foo1 PARTITION OF foo FOR VALUES IN (1); + CREATE TABLE foo2 PARTITION OF foo FOR VALUES IN (2); + INSERT INTO foo VALUES (1, 'ABC'); +} + +teardown +{ + DROP TABLE foo; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step "s1u" { UPDATE foo SET a=2 WHERE a=1; } +step "s1c" { COMMIT; } + +session "s2" +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step "s2u" { UPDATE foo SET b='EFG' WHERE a=1; } +step "s2d" { DELETE FROM foo WHERE a=1; } +step "s2c" { COMMIT; } + +permutation "s1u" "s1c" "s2u" "s2c" +permutation "s1u" "s2u" "s1c" "s2c" +permutation "s2u" "s1u" "s2c" "s1c" + +permutation "s1u" "s1c" "s2d" "s2c" +permutation "s1u" "s2d" "s1c" "s2c" +permutation "s2d" "s1u" "s2c" "s1c" diff --git a/src/test/isolation/specs/partition-key-update-2.spec b/src/test/isolation/specs/partition-key-update-2.spec new file mode 100644 index 00000000000..8a952892c28 --- /dev/null +++ b/src/test/isolation/specs/partition-key-update-2.spec @@ -0,0 +1,41 @@ +# Concurrency error from GetTupleForTrigger + +# Like partition-key-update-1.spec, throw an error where a session trying to +# update a row that has been moved to another partition due to a concurrent +# update by other seesion. + +setup +{ + CREATE TABLE foo (a int, b text) PARTITION BY LIST(a); + CREATE TABLE foo1 PARTITION OF foo FOR VALUES IN (1); + CREATE TABLE foo2 PARTITION OF foo FOR VALUES IN (2); + INSERT INTO foo VALUES (1, 'ABC'); + CREATE FUNCTION func_foo_mod_a() RETURNS TRIGGER AS $$ + BEGIN + NEW.a = 2; -- This is changing partition key column. + RETURN NEW; + END $$ LANGUAGE PLPGSQL; + CREATE TRIGGER foo_mod_a BEFORE UPDATE ON foo1 + FOR EACH ROW EXECUTE PROCEDURE func_foo_mod_a(); +} + +teardown +{ + DROP TRIGGER foo_mod_a ON foo1; + DROP FUNCTION func_foo_mod_a(); + DROP TABLE foo; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step "s1u" { UPDATE foo SET b='EFG' WHERE a=1; } +step "s1c" { COMMIT; } + +session "s2" +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step "s2u" { UPDATE foo SET b='XYZ' WHERE a=1; } +step "s2c" { COMMIT; } + +permutation "s1u" "s1c" "s2u" "s2c" +permutation "s1u" "s2u" "s1c" "s2c" +permutation "s2u" "s1u" "s2c" "s1c" diff --git a/src/test/isolation/specs/partition-key-update-3.spec b/src/test/isolation/specs/partition-key-update-3.spec new file mode 100644 index 00000000000..1baa0159de1 --- /dev/null +++ b/src/test/isolation/specs/partition-key-update-3.spec @@ -0,0 +1,32 @@ +# Concurrency error from ExecLockRows + +# Like partition-key-update-1.spec, throw an error where a session trying to +# lock a row that has been moved to another partition due to a concurrent +# update by other seesion. + +setup +{ + CREATE TABLE foo_r (a int, b text) PARTITION BY RANGE(a); + CREATE TABLE foo_r1 PARTITION OF foo_r FOR VALUES FROM (1) TO (10); + CREATE TABLE foo_r2 PARTITION OF foo_r FOR VALUES FROM (10) TO (20); + INSERT INTO foo_r VALUES(7, 'ABC'); + CREATE UNIQUE INDEX foo_r1_a_unique ON foo_r1 (a); + CREATE TABLE bar (a int REFERENCES foo_r1(a)); +} + +teardown +{ + DROP TABLE bar, foo_r; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step "s1u3" { UPDATE foo_r SET a=11 WHERE a=7 AND b = 'ABC'; } +step "s1c" { COMMIT; } + +session "s2" +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step "s2i" { INSERT INTO bar VALUES(7); } +step "s2c" { COMMIT; } + +permutation "s1u3" "s2i" "s1c" "s2c" diff --git a/src/test/isolation/specs/partition-key-update-4.spec b/src/test/isolation/specs/partition-key-update-4.spec new file mode 100644 index 00000000000..699e2e727f7 --- /dev/null +++ b/src/test/isolation/specs/partition-key-update-4.spec @@ -0,0 +1,45 @@ +# Concurrent update of a partition key and INSERT...ON CONFLICT DO NOTHING test +# +# This test tries to expose problems with the interaction between concurrent +# sessions during an update of the partition key and INSERT...ON CONFLICT DO +# NOTHING on a partitioned table. +# +# The convention here is that session 1 moves row from one partition to +# another due update of the partition key and session 2 always ends up +# inserting, and session 3 always ends up doing nothing. +# +# Note: This test is slightly resemble to insert-conflict-do-nothing test. + +setup +{ + CREATE TABLE foo (a int primary key, b text) PARTITION BY LIST(a); + CREATE TABLE foo1 PARTITION OF foo FOR VALUES IN (1); + CREATE TABLE foo2 PARTITION OF foo FOR VALUES IN (2); + INSERT INTO foo VALUES (1, 'initial tuple'); +} + +teardown +{ + DROP TABLE foo; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step "s1u" { UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; } +step "s1c" { COMMIT; } + +session "s2" +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step "s2donothing" { INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; } +step "s2c" { COMMIT; } + +session "s3" +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step "s3donothing" { INSERT INTO foo VALUES(2, 'session-3 donothing') ON CONFLICT DO NOTHING; } +step "s3select" { SELECT * FROM foo ORDER BY a; } +step "s3c" { COMMIT; } + +# Regular case where one session block-waits on another to determine if it +# should proceed with an insert or do nothing. +permutation "s1u" "s2donothing" "s3donothing" "s1c" "s2c" "s3select" "s3c" +permutation "s2donothing" "s1u" "s3donothing" "s1c" "s2c" "s3select" "s3c" diff --git a/src/test/isolation/specs/partition-key-update-5.spec b/src/test/isolation/specs/partition-key-update-5.spec new file mode 100644 index 00000000000..a6efea13817 --- /dev/null +++ b/src/test/isolation/specs/partition-key-update-5.spec @@ -0,0 +1,44 @@ +# Concurrent update of a partition key and INSERT...ON CONFLICT DO NOTHING +# test on partitioned table with multiple rows in higher isolation levels. +# +# Note: This test is resemble to insert-conflict-do-nothing-2 test + +setup +{ + CREATE TABLE foo (a int primary key, b text) PARTITION BY LIST(a); + CREATE TABLE foo1 PARTITION OF foo FOR VALUES IN (1); + CREATE TABLE foo2 PARTITION OF foo FOR VALUES IN (2); + INSERT INTO foo VALUES (1, 'initial tuple'); +} + +teardown +{ + DROP TABLE foo; +} + +session "s1" +setup { BEGIN ISOLATION LEVEL READ COMMITTED; } +step "s1u" { UPDATE foo SET a=2, b=b || ' -> moved by session-1' WHERE a=1; } +step "s1c" { COMMIT; } + +session "s2" +step "s2beginrr" { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step "s2begins" { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step "s2donothing" { INSERT INTO foo VALUES(1, 'session-2 donothing') ON CONFLICT DO NOTHING; } +step "s2c" { COMMIT; } +step "s2select" { SELECT * FROM foo ORDER BY a; } + +session "s3" +step "s3beginrr" { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step "s3begins" { BEGIN ISOLATION LEVEL SERIALIZABLE; } +step "s3donothing" { INSERT INTO foo VALUES(2, 'session-3 donothing'), (2, 'session-3 donothing2') ON CONFLICT DO NOTHING; } +step "s3c" { COMMIT; } + +permutation "s2beginrr" "s3beginrr" "s1u" "s2donothing" "s1c" "s2c" "s3donothing" "s3c" "s2select" +permutation "s2beginrr" "s3beginrr" "s1u" "s3donothing" "s1c" "s3c" "s2donothing" "s2c" "s2select" +permutation "s2beginrr" "s3beginrr" "s1u" "s2donothing" "s3donothing" "s1c" "s2c" "s3c" "s2select" +permutation "s2beginrr" "s3beginrr" "s1u" "s3donothing" "s2donothing" "s1c" "s3c" "s2c" "s2select" +permutation "s2begins" "s3begins" "s1u" "s2donothing" "s1c" "s2c" "s3donothing" "s3c" "s2select" +permutation "s2begins" "s3begins" "s1u" "s3donothing" "s1c" "s3c" "s2donothing" "s2c" "s2select" +permutation "s2begins" "s3begins" "s1u" "s2donothing" "s3donothing" "s1c" "s2c" "s3c" "s2select" +permutation "s2begins" "s3begins" "s1u" "s3donothing" "s2donothing" "s1c" "s3c" "s2c" "s2select" -- 2.17.0.rc1.dirty