AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
					  int event, bool row_trigger,
					  HeapTuple oldtup, HeapTuple newtup,
					  List *recheckIndexes, Bitmapset *modifiedCols)
{
	Relation	rel = relinfo->ri_RelationDesc;
	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
	AfterTriggerEventData new_event;
	AfterTriggerSharedData new_shared;
	int			tgtype_event;
	int			tgtype_level;
	int			i;

	/*
	 * Check state.  We use normal tests not Asserts because it is possible to
	 * reach here in the wrong state given misconfigured RI triggers, in
	 * particular deferring a cascade action trigger.
	 */
	if (afterTriggers == NULL)
		elog(ERROR, "AfterTriggerSaveEvent() called outside of transaction");
	if (afterTriggers->query_depth < 0)
		elog(ERROR, "AfterTriggerSaveEvent() called outside of query");

	/*
	 * Validate the event code and collect the associated tuple CTIDs.
	 *
	 * The event code will be used both as a bitmask and an array offset, so
	 * validation is important to make sure we don't walk off the edge of our
	 * arrays.
	 */
	new_event.ate_flags = 0;
	switch (event)
	{
		case TRIGGER_EVENT_INSERT:
			tgtype_event = TRIGGER_TYPE_INSERT;
			if (row_trigger)
			{
				Assert(oldtup == NULL);
				Assert(newtup != NULL);
				ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid1));
				ItemPointerSetInvalid(&(new_event.ate_ctid2));
			}
			else
			{
				Assert(oldtup == NULL);
				Assert(newtup == NULL);
				ItemPointerSetInvalid(&(new_event.ate_ctid1));
				ItemPointerSetInvalid(&(new_event.ate_ctid2));
			}
			break;
		case TRIGGER_EVENT_DELETE:
			tgtype_event = TRIGGER_TYPE_DELETE;
			if (row_trigger)
			{
				Assert(oldtup != NULL);
				Assert(newtup == NULL);
				ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
				ItemPointerSetInvalid(&(new_event.ate_ctid2));
			}
			else
			{
				Assert(oldtup == NULL);
				Assert(newtup == NULL);
				ItemPointerSetInvalid(&(new_event.ate_ctid1));
				ItemPointerSetInvalid(&(new_event.ate_ctid2));
			}
			break;
		case TRIGGER_EVENT_UPDATE:
			tgtype_event = TRIGGER_TYPE_UPDATE;
			if (row_trigger)
			{
				Assert(oldtup != NULL);
				Assert(newtup != NULL);
				ItemPointerCopy(&(oldtup->t_self), &(new_event.ate_ctid1));
				ItemPointerCopy(&(newtup->t_self), &(new_event.ate_ctid2));
				new_event.ate_flags |= AFTER_TRIGGER_2CTIDS;
			}
			else
			{
				Assert(oldtup == NULL);
				Assert(newtup == NULL);
				ItemPointerSetInvalid(&(new_event.ate_ctid1));
				ItemPointerSetInvalid(&(new_event.ate_ctid2));
			}
			break;
		case TRIGGER_EVENT_TRUNCATE:
			tgtype_event = TRIGGER_TYPE_TRUNCATE;
			Assert(oldtup == NULL);
			Assert(newtup == NULL);
			ItemPointerSetInvalid(&(new_event.ate_ctid1));
			ItemPointerSetInvalid(&(new_event.ate_ctid2));
			break;
		default:
			elog(ERROR, "invalid after-trigger event code: %d", event);
			tgtype_event = 0;	/* keep compiler quiet */
			break;
	}

	tgtype_level = (row_trigger ? TRIGGER_TYPE_ROW : TRIGGER_TYPE_STATEMENT);

	for (i = 0; i < trigdesc->numtriggers; i++)
	{
		Trigger    *trigger = &trigdesc->triggers[i];

		if (!TRIGGER_TYPE_MATCHES(trigger->tgtype,
								  tgtype_level,
								  TRIGGER_TYPE_AFTER,
								  tgtype_event))
			continue;
		if (!TriggerEnabled(estate, relinfo, trigger, event,
							modifiedCols, oldtup, newtup))
			continue;

		/*
		 * If the trigger is a foreign key enforcement trigger, there are
		 * certain cases where we can skip queueing the event because we can
		 * tell by inspection that the FK constraint will still pass.
		 */
		if (TRIGGER_FIRED_BY_UPDATE(event))
		{
			switch (RI_FKey_trigger_type(trigger->tgfoid))
			{
				case RI_TRIGGER_PK:
					/* Update on trigger's PK table */
					if (!RI_FKey_pk_upd_check_required(trigger, rel,
													   oldtup, newtup))
					{
						/* skip queuing this event */
						continue;
					}
					break;

				case RI_TRIGGER_FK:
					/* Update on trigger's FK table */
					if (!RI_FKey_fk_upd_check_required(trigger, rel,
													   oldtup, newtup))
					{
						/* skip queuing this event */
						continue;
					}
					break;

				case RI_TRIGGER_NONE:
					/* Not an FK trigger */
					break;
			}
		}

		/*
		 * If the trigger is a deferred unique constraint check trigger, only
		 * queue it if the unique constraint was potentially violated, which
		 * we know from index insertion time.
		 */
		if (trigger->tgfoid == F_UNIQUE_KEY_RECHECK)
		{
			if (!list_member_oid(recheckIndexes, trigger->tgconstrindid))
				continue;		/* Uniqueness definitely not violated */
		}

		/*
		 * Fill in event structure and add it to the current query's queue.
		 */
		new_shared.ats_event =
			(event & TRIGGER_EVENT_OPMASK) |
			(row_trigger ? TRIGGER_EVENT_ROW : 0) |
			(trigger->tgdeferrable ? AFTER_TRIGGER_DEFERRABLE : 0) |
			(trigger->tginitdeferred ? AFTER_TRIGGER_INITDEFERRED : 0);
		new_shared.ats_tgoid = trigger->tgoid;
		new_shared.ats_relid = RelationGetRelid(rel);
		new_shared.ats_firing_id = 0;

		afterTriggerAddEvent(&afterTriggers->query_stack[afterTriggers->query_depth],
							 &new_event, &new_shared);
	}
}