mirror of
				https://github.com/postgres/postgres.git
				synced 2025-10-31 10:30:33 +03:00 
			
		
		
		
	Fix concurrent update trigger issues with MERGE in a CTE.
If a MERGE inside a CTE attempts an UPDATE or DELETE on a table with BEFORE ROW triggers, and a concurrent UPDATE or DELETE happens, the merge code would fail (crashing in the case of an UPDATE action, and potentially executing the wrong action for a DELETE action). This is the same issue that9321c79c86attempted to fix, except now for a MERGE inside a CTE. As noted in9321c79c86, what needs to happen is for the trigger code to exit early, returning the TM_Result and TM_FailureData information to the merge code, if a concurrent modification is detected, rather than attempting to do an EPQ recheck. The merge code will then do its own rechecking, and rescan the action list, potentially executing a different action in light of the concurrent update. In particular, the trigger code must never call ExecGetUpdateNewTuple() for MERGE, since that is bound to fail because MERGE has its own per-action projection information. Commit9321c79c86did this using estate->es_plannedstmt->commandType in the trigger code to detect that a MERGE was being executed, which is fine for a plain MERGE command, but does not work for a MERGE inside a CTE. Fix by passing that information to the trigger code as an additional parameter passed to ExecBRUpdateTriggers() and ExecBRDeleteTriggers(). Back-patch as far as v17 only, since MERGE cannot appear inside a CTE prior to that. Additionally, take care to preserve the trigger ABI in v17 (though not in v18, which is still in beta). Bug: #18986 Reported-by: Yaroslav Syrytsia <me@ys.lc> Author: Dean Rasheed <dean.a.rasheed@gmail.com> Reviewed-by: Michael Paquier <michael@paquier.xyz> Discussion: https://postgr.es/m/18986-e7a8aac3d339fa47@postgresql.org Backpatch-through: 17
This commit is contained in:
		| @@ -79,6 +79,7 @@ static bool GetTupleForTrigger(EState *estate, | |||||||
| 							   ItemPointer tid, | 							   ItemPointer tid, | ||||||
| 							   LockTupleMode lockmode, | 							   LockTupleMode lockmode, | ||||||
| 							   TupleTableSlot *oldslot, | 							   TupleTableSlot *oldslot, | ||||||
|  | 							   bool do_epq_recheck, | ||||||
| 							   TupleTableSlot **epqslot, | 							   TupleTableSlot **epqslot, | ||||||
| 							   TM_Result *tmresultp, | 							   TM_Result *tmresultp, | ||||||
| 							   TM_FailureData *tmfdp); | 							   TM_FailureData *tmfdp); | ||||||
| @@ -2679,13 +2680,14 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo, | |||||||
|  * back the concurrently updated tuple if any. |  * back the concurrently updated tuple if any. | ||||||
|  */ |  */ | ||||||
| bool | bool | ||||||
| ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, | ExecBRDeleteTriggersNew(EState *estate, EPQState *epqstate, | ||||||
| 					 ResultRelInfo *relinfo, | 						ResultRelInfo *relinfo, | ||||||
| 					 ItemPointer tupleid, | 						ItemPointer tupleid, | ||||||
| 					 HeapTuple fdw_trigtuple, | 						HeapTuple fdw_trigtuple, | ||||||
| 					 TupleTableSlot **epqslot, | 						TupleTableSlot **epqslot, | ||||||
| 					 TM_Result *tmresult, | 						TM_Result *tmresult, | ||||||
| 					 TM_FailureData *tmfd) | 						TM_FailureData *tmfd, | ||||||
|  | 						bool is_merge_delete) | ||||||
| { | { | ||||||
| 	TupleTableSlot *slot = ExecGetTriggerOldSlot(estate, relinfo); | 	TupleTableSlot *slot = ExecGetTriggerOldSlot(estate, relinfo); | ||||||
| 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc; | 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc; | ||||||
| @@ -2700,9 +2702,17 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, | |||||||
| 	{ | 	{ | ||||||
| 		TupleTableSlot *epqslot_candidate = NULL; | 		TupleTableSlot *epqslot_candidate = NULL; | ||||||
|  |  | ||||||
|  | 		/* | ||||||
|  | 		 * Get a copy of the on-disk tuple we are planning to delete.  In | ||||||
|  | 		 * general, if the tuple has been concurrently updated, we should | ||||||
|  | 		 * recheck it using EPQ.  However, if this is a MERGE DELETE action, | ||||||
|  | 		 * we skip this EPQ recheck and leave it to the caller (it must do | ||||||
|  | 		 * additional rechecking, and might end up executing a different | ||||||
|  | 		 * action entirely). | ||||||
|  | 		 */ | ||||||
| 		if (!GetTupleForTrigger(estate, epqstate, relinfo, tupleid, | 		if (!GetTupleForTrigger(estate, epqstate, relinfo, tupleid, | ||||||
| 								LockTupleExclusive, slot, &epqslot_candidate, | 								LockTupleExclusive, slot, !is_merge_delete, | ||||||
| 								tmresult, tmfd)) | 								&epqslot_candidate, tmresult, tmfd)) | ||||||
| 			return false; | 			return false; | ||||||
|  |  | ||||||
| 		/* | 		/* | ||||||
| @@ -2765,6 +2775,24 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, | |||||||
| 	return result; | 	return result; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* | ||||||
|  |  * ABI-compatible wrapper to emulate old version of the above function. | ||||||
|  |  * Do not call this version in new code. | ||||||
|  |  */ | ||||||
|  | bool | ||||||
|  | ExecBRDeleteTriggers(EState *estate, EPQState *epqstate, | ||||||
|  | 					 ResultRelInfo *relinfo, | ||||||
|  | 					 ItemPointer tupleid, | ||||||
|  | 					 HeapTuple fdw_trigtuple, | ||||||
|  | 					 TupleTableSlot **epqslot, | ||||||
|  | 					 TM_Result *tmresult, | ||||||
|  | 					 TM_FailureData *tmfd) | ||||||
|  | { | ||||||
|  | 	return ExecBRDeleteTriggersNew(estate, epqstate, relinfo, tupleid, | ||||||
|  | 								   fdw_trigtuple, epqslot, tmresult, tmfd, | ||||||
|  | 								   false); | ||||||
|  | } | ||||||
|  |  | ||||||
| /* | /* | ||||||
|  * Note: is_crosspart_update must be true if the DELETE is being performed |  * Note: is_crosspart_update must be true if the DELETE is being performed | ||||||
|  * as part of a cross-partition update. |  * as part of a cross-partition update. | ||||||
| @@ -2792,6 +2820,7 @@ ExecARDeleteTriggers(EState *estate, | |||||||
| 							   tupleid, | 							   tupleid, | ||||||
| 							   LockTupleExclusive, | 							   LockTupleExclusive, | ||||||
| 							   slot, | 							   slot, | ||||||
|  | 							   false, | ||||||
| 							   NULL, | 							   NULL, | ||||||
| 							   NULL, | 							   NULL, | ||||||
| 							   NULL); | 							   NULL); | ||||||
| @@ -2930,13 +2959,14 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo, | |||||||
| } | } | ||||||
|  |  | ||||||
| bool | bool | ||||||
| ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, | ExecBRUpdateTriggersNew(EState *estate, EPQState *epqstate, | ||||||
| 					 ResultRelInfo *relinfo, | 						ResultRelInfo *relinfo, | ||||||
| 					 ItemPointer tupleid, | 						ItemPointer tupleid, | ||||||
| 					 HeapTuple fdw_trigtuple, | 						HeapTuple fdw_trigtuple, | ||||||
| 					 TupleTableSlot *newslot, | 						TupleTableSlot *newslot, | ||||||
| 					 TM_Result *tmresult, | 						TM_Result *tmresult, | ||||||
| 					 TM_FailureData *tmfd) | 						TM_FailureData *tmfd, | ||||||
|  | 						bool is_merge_update) | ||||||
| { | { | ||||||
| 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc; | 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc; | ||||||
| 	TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo); | 	TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo); | ||||||
| @@ -2957,10 +2987,17 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, | |||||||
| 	{ | 	{ | ||||||
| 		TupleTableSlot *epqslot_candidate = NULL; | 		TupleTableSlot *epqslot_candidate = NULL; | ||||||
|  |  | ||||||
| 		/* get a copy of the on-disk tuple we are planning to update */ | 		/* | ||||||
|  | 		 * Get a copy of the on-disk tuple we are planning to update.  In | ||||||
|  | 		 * general, if the tuple has been concurrently updated, we should | ||||||
|  | 		 * recheck it using EPQ.  However, if this is a MERGE UPDATE action, | ||||||
|  | 		 * we skip this EPQ recheck and leave it to the caller (it must do | ||||||
|  | 		 * additional rechecking, and might end up executing a different | ||||||
|  | 		 * action entirely). | ||||||
|  | 		 */ | ||||||
| 		if (!GetTupleForTrigger(estate, epqstate, relinfo, tupleid, | 		if (!GetTupleForTrigger(estate, epqstate, relinfo, tupleid, | ||||||
| 								lockmode, oldslot, &epqslot_candidate, | 								lockmode, oldslot, !is_merge_update, | ||||||
| 								tmresult, tmfd)) | 								&epqslot_candidate, tmresult, tmfd)) | ||||||
| 			return false;		/* cancel the update action */ | 			return false;		/* cancel the update action */ | ||||||
|  |  | ||||||
| 		/* | 		/* | ||||||
| @@ -3082,6 +3119,24 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, | |||||||
| 	return true; | 	return true; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* | ||||||
|  |  * ABI-compatible wrapper to emulate old version of the above function. | ||||||
|  |  * Do not call this version in new code. | ||||||
|  |  */ | ||||||
|  | bool | ||||||
|  | ExecBRUpdateTriggers(EState *estate, EPQState *epqstate, | ||||||
|  | 					 ResultRelInfo *relinfo, | ||||||
|  | 					 ItemPointer tupleid, | ||||||
|  | 					 HeapTuple fdw_trigtuple, | ||||||
|  | 					 TupleTableSlot *newslot, | ||||||
|  | 					 TM_Result *tmresult, | ||||||
|  | 					 TM_FailureData *tmfd) | ||||||
|  | { | ||||||
|  | 	return ExecBRUpdateTriggersNew(estate, epqstate, relinfo, tupleid, | ||||||
|  | 								   fdw_trigtuple, newslot, tmresult, tmfd, | ||||||
|  | 								   false); | ||||||
|  | } | ||||||
|  |  | ||||||
| /* | /* | ||||||
|  * Note: 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source |  * Note: 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source | ||||||
|  * and destination partitions, respectively, of a cross-partition update of |  * and destination partitions, respectively, of a cross-partition update of | ||||||
| @@ -3132,6 +3187,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo, | |||||||
| 							   tupleid, | 							   tupleid, | ||||||
| 							   LockTupleExclusive, | 							   LockTupleExclusive, | ||||||
| 							   oldslot, | 							   oldslot, | ||||||
|  | 							   false, | ||||||
| 							   NULL, | 							   NULL, | ||||||
| 							   NULL, | 							   NULL, | ||||||
| 							   NULL); | 							   NULL); | ||||||
| @@ -3288,6 +3344,7 @@ GetTupleForTrigger(EState *estate, | |||||||
| 				   ItemPointer tid, | 				   ItemPointer tid, | ||||||
| 				   LockTupleMode lockmode, | 				   LockTupleMode lockmode, | ||||||
| 				   TupleTableSlot *oldslot, | 				   TupleTableSlot *oldslot, | ||||||
|  | 				   bool do_epq_recheck, | ||||||
| 				   TupleTableSlot **epqslot, | 				   TupleTableSlot **epqslot, | ||||||
| 				   TM_Result *tmresultp, | 				   TM_Result *tmresultp, | ||||||
| 				   TM_FailureData *tmfdp) | 				   TM_FailureData *tmfdp) | ||||||
| @@ -3347,31 +3404,32 @@ GetTupleForTrigger(EState *estate, | |||||||
| 				if (tmfd.traversed) | 				if (tmfd.traversed) | ||||||
| 				{ | 				{ | ||||||
| 					/* | 					/* | ||||||
| 					 * Recheck the tuple using EPQ. For MERGE, we leave this | 					 * Recheck the tuple using EPQ, if requested.  Otherwise, | ||||||
| 					 * to the caller (it must do additional rechecking, and | 					 * just return that it was concurrently updated. | ||||||
| 					 * might end up executing a different action entirely). |  | ||||||
| 					 */ | 					 */ | ||||||
| 					if (estate->es_plannedstmt->commandType == CMD_MERGE) | 					if (do_epq_recheck) | ||||||
|  | 					{ | ||||||
|  | 						*epqslot = EvalPlanQual(epqstate, | ||||||
|  | 												relation, | ||||||
|  | 												relinfo->ri_RangeTableIndex, | ||||||
|  | 												oldslot); | ||||||
|  |  | ||||||
|  | 						/* | ||||||
|  | 						 * If PlanQual failed for updated tuple - we must not | ||||||
|  | 						 * process this tuple! | ||||||
|  | 						 */ | ||||||
|  | 						if (TupIsNull(*epqslot)) | ||||||
|  | 						{ | ||||||
|  | 							*epqslot = NULL; | ||||||
|  | 							return false; | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 					else | ||||||
| 					{ | 					{ | ||||||
| 						if (tmresultp) | 						if (tmresultp) | ||||||
| 							*tmresultp = TM_Updated; | 							*tmresultp = TM_Updated; | ||||||
| 						return false; | 						return false; | ||||||
| 					} | 					} | ||||||
|  |  | ||||||
| 					*epqslot = EvalPlanQual(epqstate, |  | ||||||
| 											relation, |  | ||||||
| 											relinfo->ri_RangeTableIndex, |  | ||||||
| 											oldslot); |  | ||||||
|  |  | ||||||
| 					/* |  | ||||||
| 					 * If PlanQual failed for updated tuple - we must not |  | ||||||
| 					 * process this tuple! |  | ||||||
| 					 */ |  | ||||||
| 					if (TupIsNull(*epqslot)) |  | ||||||
| 					{ |  | ||||||
| 						*epqslot = NULL; |  | ||||||
| 						return false; |  | ||||||
| 					} |  | ||||||
| 				} | 				} | ||||||
| 				break; | 				break; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1349,9 +1349,10 @@ ExecDeletePrologue(ModifyTableContext *context, ResultRelInfo *resultRelInfo, | |||||||
| 		if (context->estate->es_insert_pending_result_relations != NIL) | 		if (context->estate->es_insert_pending_result_relations != NIL) | ||||||
| 			ExecPendingInserts(context->estate); | 			ExecPendingInserts(context->estate); | ||||||
|  |  | ||||||
| 		return ExecBRDeleteTriggers(context->estate, context->epqstate, | 		return ExecBRDeleteTriggersNew(context->estate, context->epqstate, | ||||||
| 									resultRelInfo, tupleid, oldtuple, | 									   resultRelInfo, tupleid, oldtuple, | ||||||
| 									epqreturnslot, result, &context->tmfd); | 									   epqreturnslot, result, &context->tmfd, | ||||||
|  | 									   context->mtstate->operation == CMD_MERGE); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return true; | 	return true; | ||||||
| @@ -1947,9 +1948,10 @@ ExecUpdatePrologue(ModifyTableContext *context, ResultRelInfo *resultRelInfo, | |||||||
| 		if (context->estate->es_insert_pending_result_relations != NIL) | 		if (context->estate->es_insert_pending_result_relations != NIL) | ||||||
| 			ExecPendingInserts(context->estate); | 			ExecPendingInserts(context->estate); | ||||||
|  |  | ||||||
| 		return ExecBRUpdateTriggers(context->estate, context->epqstate, | 		return ExecBRUpdateTriggersNew(context->estate, context->epqstate, | ||||||
| 									resultRelInfo, tupleid, oldtuple, slot, | 									   resultRelInfo, tupleid, oldtuple, slot, | ||||||
| 									result, &context->tmfd); | 									   result, &context->tmfd, | ||||||
|  | 									   context->mtstate->operation == CMD_MERGE); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return true; | 	return true; | ||||||
|   | |||||||
| @@ -206,6 +206,15 @@ extern void ExecBSDeleteTriggers(EState *estate, | |||||||
| extern void ExecASDeleteTriggers(EState *estate, | extern void ExecASDeleteTriggers(EState *estate, | ||||||
| 								 ResultRelInfo *relinfo, | 								 ResultRelInfo *relinfo, | ||||||
| 								 TransitionCaptureState *transition_capture); | 								 TransitionCaptureState *transition_capture); | ||||||
|  | extern bool ExecBRDeleteTriggersNew(EState *estate, | ||||||
|  | 									EPQState *epqstate, | ||||||
|  | 									ResultRelInfo *relinfo, | ||||||
|  | 									ItemPointer tupleid, | ||||||
|  | 									HeapTuple fdw_trigtuple, | ||||||
|  | 									TupleTableSlot **epqslot, | ||||||
|  | 									TM_Result *tmresult, | ||||||
|  | 									TM_FailureData *tmfd, | ||||||
|  | 									bool is_merge_delete); | ||||||
| extern bool ExecBRDeleteTriggers(EState *estate, | extern bool ExecBRDeleteTriggers(EState *estate, | ||||||
| 								 EPQState *epqstate, | 								 EPQState *epqstate, | ||||||
| 								 ResultRelInfo *relinfo, | 								 ResultRelInfo *relinfo, | ||||||
| @@ -228,6 +237,15 @@ extern void ExecBSUpdateTriggers(EState *estate, | |||||||
| extern void ExecASUpdateTriggers(EState *estate, | extern void ExecASUpdateTriggers(EState *estate, | ||||||
| 								 ResultRelInfo *relinfo, | 								 ResultRelInfo *relinfo, | ||||||
| 								 TransitionCaptureState *transition_capture); | 								 TransitionCaptureState *transition_capture); | ||||||
|  | extern bool ExecBRUpdateTriggersNew(EState *estate, | ||||||
|  | 									EPQState *epqstate, | ||||||
|  | 									ResultRelInfo *relinfo, | ||||||
|  | 									ItemPointer tupleid, | ||||||
|  | 									HeapTuple fdw_trigtuple, | ||||||
|  | 									TupleTableSlot *newslot, | ||||||
|  | 									TM_Result *tmresult, | ||||||
|  | 									TM_FailureData *tmfd, | ||||||
|  | 									bool is_merge_update); | ||||||
| extern bool ExecBRUpdateTriggers(EState *estate, | extern bool ExecBRUpdateTriggers(EState *estate, | ||||||
| 								 EPQState *epqstate, | 								 EPQState *epqstate, | ||||||
| 								 ResultRelInfo *relinfo, | 								 ResultRelInfo *relinfo, | ||||||
|   | |||||||
| @@ -241,19 +241,28 @@ starting permutation: update_bal1_tg merge_bal_tg c2 select1_tg c1 | |||||||
| s2: NOTICE:  Update: (1,160,s1,setup) -> (1,50,s1,"setup updated by update_bal1_tg") | s2: NOTICE:  Update: (1,160,s1,setup) -> (1,50,s1,"setup updated by update_bal1_tg") | ||||||
| step update_bal1_tg: UPDATE target_tg t SET balance = 50, val = t.val || ' updated by update_bal1_tg' WHERE t.key = 1; | step update_bal1_tg: UPDATE target_tg t SET balance = 50, val = t.val || ' updated by update_bal1_tg' WHERE t.key = 1; | ||||||
| step merge_bal_tg:  | step merge_bal_tg:  | ||||||
|   MERGE INTO target_tg t |   WITH t AS ( | ||||||
|   USING (SELECT 1 as key) s |     MERGE INTO target_tg t | ||||||
|   ON s.key = t.key |     USING (SELECT 1 as key) s | ||||||
|   WHEN MATCHED AND balance < 100 THEN |     ON s.key = t.key | ||||||
| 	UPDATE SET balance = balance * 2, val = t.val || ' when1' |     WHEN MATCHED AND balance < 100 THEN | ||||||
|   WHEN MATCHED AND balance < 200 THEN |       UPDATE SET balance = balance * 2, val = t.val || ' when1' | ||||||
| 	UPDATE SET balance = balance * 4, val = t.val || ' when2' |     WHEN MATCHED AND balance < 200 THEN | ||||||
|   WHEN MATCHED AND balance < 300 THEN |       UPDATE SET balance = balance * 4, val = t.val || ' when2' | ||||||
| 	UPDATE SET balance = balance * 8, val = t.val || ' when3'; |     WHEN MATCHED AND balance < 300 THEN | ||||||
|  |       UPDATE SET balance = balance * 8, val = t.val || ' when3' | ||||||
|  |     RETURNING t.* | ||||||
|  |   ) | ||||||
|  |   SELECT * FROM t; | ||||||
|  <waiting ...> |  <waiting ...> | ||||||
| step c2: COMMIT; | step c2: COMMIT; | ||||||
| s1: NOTICE:  Update: (1,50,s1,"setup updated by update_bal1_tg") -> (1,100,s1,"setup updated by update_bal1_tg when1") | s1: NOTICE:  Update: (1,50,s1,"setup updated by update_bal1_tg") -> (1,100,s1,"setup updated by update_bal1_tg when1") | ||||||
| step merge_bal_tg: <... completed> | step merge_bal_tg: <... completed> | ||||||
|  | key|balance|status|val                                   | ||||||
|  | ---+-------+------+------------------------------------- | ||||||
|  |   1|    100|s1    |setup updated by update_bal1_tg when1 | ||||||
|  | (1 row) | ||||||
|  |  | ||||||
| step select1_tg: SELECT * FROM target_tg; | step select1_tg: SELECT * FROM target_tg; | ||||||
| key|balance|status|val                                   | key|balance|status|val                                   | ||||||
| ---+-------+------+------------------------------------- | ---+-------+------+------------------------------------- | ||||||
|   | |||||||
| @@ -99,15 +99,19 @@ step "merge_bal_pa" | |||||||
| } | } | ||||||
| step "merge_bal_tg" | step "merge_bal_tg" | ||||||
| { | { | ||||||
|   MERGE INTO target_tg t |   WITH t AS ( | ||||||
|   USING (SELECT 1 as key) s |     MERGE INTO target_tg t | ||||||
|   ON s.key = t.key |     USING (SELECT 1 as key) s | ||||||
|   WHEN MATCHED AND balance < 100 THEN |     ON s.key = t.key | ||||||
| 	UPDATE SET balance = balance * 2, val = t.val || ' when1' |     WHEN MATCHED AND balance < 100 THEN | ||||||
|   WHEN MATCHED AND balance < 200 THEN |       UPDATE SET balance = balance * 2, val = t.val || ' when1' | ||||||
| 	UPDATE SET balance = balance * 4, val = t.val || ' when2' |     WHEN MATCHED AND balance < 200 THEN | ||||||
|   WHEN MATCHED AND balance < 300 THEN |       UPDATE SET balance = balance * 4, val = t.val || ' when2' | ||||||
| 	UPDATE SET balance = balance * 8, val = t.val || ' when3'; |     WHEN MATCHED AND balance < 300 THEN | ||||||
|  |       UPDATE SET balance = balance * 8, val = t.val || ' when3' | ||||||
|  |     RETURNING t.* | ||||||
|  |   ) | ||||||
|  |   SELECT * FROM t; | ||||||
| } | } | ||||||
|  |  | ||||||
| step "merge_delete" | step "merge_delete" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user