diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c new file mode 100644 index 2632431..a0efe7e --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -3561,10 +3561,21 @@ lmerge_matched: errmsg("could not serialize access due to concurrent delete"))); /* - * If the tuple was already deleted, set matched to false to - * let caller handle it under NOT MATCHED [BY TARGET] clauses. + * The target tuple was concurrently deleted by some other + * transaction. If this was a MATCHED case, the source row + * still exists, so let the caller handle it using any NOT + * MATCHED [BY TARGET] actions. + * + * If this was originally a NOT MATCHED BY SOURCE case, leave + * matched true. Since neither a source row nor a target row + * now exists, there is no further action to execute. + * + * If this was originally MATCHED but a concurrent update had + * already changed it to NOT MATCHED BY SOURCE, matched is + * already false and should remain so. */ - *matched = false; + if (relaction->mas_action->matchKind == MERGE_WHEN_MATCHED) + *matched = false; goto out; case TM_Updated: @@ -3717,10 +3728,24 @@ lmerge_matched: case TM_Deleted: /* - * tuple already deleted; tell caller to run NOT - * MATCHED [BY TARGET] actions + * The target tuple was concurrently deleted by + * some other transaction. If this was a MATCHED + * case, the source row still exists, so let the + * caller handle it using any NOT MATCHED [BY + * TARGET] actions. + * + * If this was originally a NOT MATCHED BY SOURCE + * case, leave matched true. Since neither a + * source row nor a target row now exists, there + * is no further action to execute. + * + * If this was originally MATCHED but a concurrent + * update had already changed it to NOT MATCHED BY + * SOURCE, matched is already false and should + * remain so. */ - *matched = false; + if (was_matched) + *matched = false; goto out; case TM_SelfModified: diff --git a/src/test/isolation/expected/merge-delete.out b/src/test/isolation/expected/merge-delete.out new file mode 100644 index 897b935..c0e3b2b --- a/src/test/isolation/expected/merge-delete.out +++ b/src/test/isolation/expected/merge-delete.out @@ -234,3 +234,161 @@ key|val (1 row) step c2: COMMIT; + +starting permutation: delete merge_wnmbs_src2 c1 select2 c2 +step delete: DELETE FROM target t WHERE t.key = 1; +step merge_wnmbs_src2: + MERGE INTO target t + USING (SELECT 2 AS key, 'merge2' AS val) s ON t.key = s.key + WHEN MATCHED THEN UPDATE SET key = t.key + 1, val = t.val || ' match updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN UPDATE SET key = t.key + 1, val = t.val || ' not matched by source' + WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) + RETURNING merge_action(), t.*; + +step c1: COMMIT; +step merge_wnmbs_src2: <... completed> +merge_action|key|val +------------+---+------ +INSERT | 2|merge2 +(1 row) + +step select2: SELECT * FROM target; +key|val +---+------ + 2|merge2 +(1 row) + +step c2: COMMIT; + +starting permutation: delete_pa merge_wnmbs_src2_pa c1 select2_pa c2 +step delete_pa: DELETE FROM target_pa t WHERE t.key = 1; +step merge_wnmbs_src2_pa: + MERGE INTO target_pa t + USING (SELECT 2 AS key, 'merge2_pa' AS val) s ON t.key = s.key + WHEN MATCHED THEN UPDATE SET key = t.key + 1, val = t.val || ' match updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN UPDATE SET key = t.key + 1, val = t.val || ' not matched by source' + WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) + RETURNING merge_action(), t.*; + +step c1: COMMIT; +step merge_wnmbs_src2_pa: <... completed> +merge_action|key|val +------------+---+--------- +INSERT | 2|merge2_pa +(1 row) + +step select2_pa: SELECT * FROM target_pa; +key|val +---+--------- + 2|merge2_pa +(1 row) + +step c2: COMMIT; + +starting permutation: delete_tg merge_wnmbs_src2_tg c1 select2_tg c2 +s1: NOTICE: Delete: (1,setup1) +step delete_tg: DELETE FROM target_tg t WHERE t.key = 1; +step merge_wnmbs_src2_tg: + MERGE INTO target_tg t + USING (SELECT 2 AS key, 'merge2_tg' AS val) s ON t.key = s.key + WHEN MATCHED THEN UPDATE SET key = t.key + 1, val = t.val || ' match updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN UPDATE SET key = t.key + 1, val = t.val || ' not matched by source' + WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) + RETURNING merge_action(), t.*; + +step c1: COMMIT; +s2: NOTICE: Insert: (2,merge2_tg) +step merge_wnmbs_src2_tg: <... completed> +merge_action|key|val +------------+---+--------- +INSERT | 2|merge2_tg +(1 row) + +step select2_tg: SELECT * FROM target_tg; +key|val +---+--------- + 2|merge2_tg +(1 row) + +step c2: COMMIT; + +starting permutation: update delete merge_wnmbs_src2 c1 select2 c2 +step update: UPDATE target t SET val = t.val || ' update1' WHERE t.key = 1; +step delete: DELETE FROM target t WHERE t.key = 1; +step merge_wnmbs_src2: + MERGE INTO target t + USING (SELECT 2 AS key, 'merge2' AS val) s ON t.key = s.key + WHEN MATCHED THEN UPDATE SET key = t.key + 1, val = t.val || ' match updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN UPDATE SET key = t.key + 1, val = t.val || ' not matched by source' + WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) + RETURNING merge_action(), t.*; + +step c1: COMMIT; +step merge_wnmbs_src2: <... completed> +merge_action|key|val +------------+---+------ +INSERT | 2|merge2 +(1 row) + +step select2: SELECT * FROM target; +key|val +---+------ + 2|merge2 +(1 row) + +step c2: COMMIT; + +starting permutation: update_pa delete_pa merge_wnmbs_src2_pa c1 select2_pa c2 +step update_pa: UPDATE target_pa t SET val = t.val || ' update1' WHERE t.key = 1; +step delete_pa: DELETE FROM target_pa t WHERE t.key = 1; +step merge_wnmbs_src2_pa: + MERGE INTO target_pa t + USING (SELECT 2 AS key, 'merge2_pa' AS val) s ON t.key = s.key + WHEN MATCHED THEN UPDATE SET key = t.key + 1, val = t.val || ' match updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN UPDATE SET key = t.key + 1, val = t.val || ' not matched by source' + WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) + RETURNING merge_action(), t.*; + +step c1: COMMIT; +step merge_wnmbs_src2_pa: <... completed> +merge_action|key|val +------------+---+--------- +INSERT | 2|merge2_pa +(1 row) + +step select2_pa: SELECT * FROM target_pa; +key|val +---+--------- + 2|merge2_pa +(1 row) + +step c2: COMMIT; + +starting permutation: update_tg delete_tg merge_wnmbs_src2_tg c1 select2_tg c2 +s1: NOTICE: Update: (1,setup1) -> (1,"setup1 update1") +step update_tg: UPDATE target_tg t SET val = t.val || ' update1' WHERE t.key = 1; +s1: NOTICE: Delete: (1,"setup1 update1") +step delete_tg: DELETE FROM target_tg t WHERE t.key = 1; +step merge_wnmbs_src2_tg: + MERGE INTO target_tg t + USING (SELECT 2 AS key, 'merge2_tg' AS val) s ON t.key = s.key + WHEN MATCHED THEN UPDATE SET key = t.key + 1, val = t.val || ' match updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN UPDATE SET key = t.key + 1, val = t.val || ' not matched by source' + WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) + RETURNING merge_action(), t.*; + +step c1: COMMIT; +s2: NOTICE: Insert: (2,merge2_tg) +step merge_wnmbs_src2_tg: <... completed> +merge_action|key|val +------------+---+--------- +INSERT | 2|merge2_tg +(1 row) + +step select2_tg: SELECT * FROM target_tg; +key|val +---+--------- + 2|merge2_tg +(1 row) + +step c2: COMMIT; diff --git a/src/test/isolation/specs/merge-delete.spec b/src/test/isolation/specs/merge-delete.spec new file mode 100644 index ba5f70e..833ce1f --- a/src/test/isolation/specs/merge-delete.spec +++ b/src/test/isolation/specs/merge-delete.spec @@ -50,6 +50,9 @@ setup step "delete" { DELETE FROM target t WHERE t.key = 1; } step "delete_pa" { DELETE FROM target_pa t WHERE t.key = 1; } step "delete_tg" { DELETE FROM target_tg t WHERE t.key = 1; } +step "update" { UPDATE target t SET val = t.val || ' update1' WHERE t.key = 1; } +step "update_pa" { UPDATE target_pa t SET val = t.val || ' update1' WHERE t.key = 1; } +step "update_tg" { UPDATE target_tg t SET val = t.val || ' update1' WHERE t.key = 1; } step "c1" { COMMIT; } session "s2" @@ -65,6 +68,30 @@ step "merge2_pa" { MERGE INTO target_pa step "merge2_tg" { MERGE INTO target_tg t USING (SELECT 1 as key, 'merge2_tg' 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 "merge_delete2" { MERGE INTO target t USING (SELECT 1 as key, 'merge_delete2' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN DELETE; } step "merge_delete2_tg" { MERGE INTO target_tg t USING (SELECT 1 as key, 'merge_delete2_tg' as val) s ON s.key = t.key WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) WHEN MATCHED THEN DELETE; } +step "merge_wnmbs_src2" { + MERGE INTO target t + USING (SELECT 2 AS key, 'merge2' AS val) s ON t.key = s.key + WHEN MATCHED THEN UPDATE SET key = t.key + 1, val = t.val || ' match updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN UPDATE SET key = t.key + 1, val = t.val || ' not matched by source' + WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) + RETURNING merge_action(), t.*; +} +step "merge_wnmbs_src2_pa" { + MERGE INTO target_pa t + USING (SELECT 2 AS key, 'merge2_pa' AS val) s ON t.key = s.key + WHEN MATCHED THEN UPDATE SET key = t.key + 1, val = t.val || ' match updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN UPDATE SET key = t.key + 1, val = t.val || ' not matched by source' + WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) + RETURNING merge_action(), t.*; +} +step "merge_wnmbs_src2_tg" { + MERGE INTO target_tg t + USING (SELECT 2 AS key, 'merge2_tg' AS val) s ON t.key = s.key + WHEN MATCHED THEN UPDATE SET key = t.key + 1, val = t.val || ' match updated by ' || s.val + WHEN NOT MATCHED BY SOURCE THEN UPDATE SET key = t.key + 1, val = t.val || ' not matched by source' + WHEN NOT MATCHED THEN INSERT VALUES (s.key, s.val) + RETURNING merge_action(), t.*; +} step "select2" { SELECT * FROM target; } step "select2_pa" { SELECT * FROM target_pa; } step "select2_tg" { SELECT * FROM target_tg; } @@ -94,3 +121,13 @@ permutation "delete_pa" "merge2_pa" "c1" permutation "delete_tg" "merge2_tg" "c1" "select2_tg" "c2" permutation "delete" "merge_delete2" "c1" "select2" "c2" permutation "delete_tg" "merge_delete2_tg" "c1" "select2_tg" "c2" + +# Concurrent DELETE vs MERGE ... WHEN NOT MATCHED BY SOURCE +permutation "delete" "merge_wnmbs_src2" "c1" "select2" "c2" +permutation "delete_pa" "merge_wnmbs_src2_pa" "c1" "select2_pa" "c2" +permutation "delete_tg" "merge_wnmbs_src2_tg" "c1" "select2_tg" "c2" + +# Concurrent UPDATE then DELETE vs MERGE ... WHEN NOT MATCHED BY SOURCE +permutation "update" "delete" "merge_wnmbs_src2" "c1" "select2" "c2" +permutation "update_pa" "delete_pa" "merge_wnmbs_src2_pa" "c1" "select2_pa" "c2" +permutation "update_tg" "delete_tg" "merge_wnmbs_src2_tg" "c1" "select2_tg" "c2"