diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index bcaa58cae0e..1a8ea8e8888 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -2688,7 +2688,7 @@ CopyFrom(CopyState cstate)
 
 					/* AFTER ROW INSERT Triggers */
 					ExecARInsertTriggers(estate, resultRelInfo, tuple,
-										 recheckIndexes);
+										 recheckIndexes, NULL);
 
 					list_free(recheckIndexes);
 				}
@@ -2838,7 +2838,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 									  estate, false, NULL, NIL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
-								 recheckIndexes);
+								 recheckIndexes, NULL);
 			list_free(recheckIndexes);
 		}
 	}
@@ -2855,7 +2855,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 			cstate->cur_lineno = firstBufferedLineNo + i;
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
-								 NIL);
+								 NIL, NULL);
 		}
 	}
 
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index d05e51c8208..90216693eef 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -96,7 +96,8 @@ static HeapTuple ExecCallTriggerFunc(TriggerData *trigdata,
 static void AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 					  int event, bool row_trigger,
 					  HeapTuple oldtup, HeapTuple newtup,
-					  List *recheckIndexes, Bitmapset *modifiedCols);
+					  List *recheckIndexes, Bitmapset *modifiedCols,
+					  TriggerTransitionFilter *transitions);
 static void AfterTriggerEnlargeQueryState(void);
 
 
@@ -354,13 +355,6 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 			 * adjustments will be needed below.
 			 */
 
-			if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
-				ereport(ERROR,
-						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-						 errmsg("\"%s\" is a partitioned table",
-								RelationGetRelationName(rel)),
-					 errdetail("Triggers on partitioned tables cannot have transition tables.")));
-
 			if (stmt->timing != TRIGGER_TYPE_AFTER)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -2010,6 +2004,35 @@ equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2)
 #endif   /* NOT_USED */
 
 /*
+ * Make a TriggerTransitionFilter object based on a given TriggerDesc.  This
+ * holds the flags which control whether transition tuples are collected when
+ * tables are modified.  This allows us to use the flags from a parent table
+ * to control the collection of transition tuples from child tables.  The
+ * resulting object can be passed to the ExecAR* functions, but the caller
+ * should also set ttf_map as appropriate when dealing with child tables.  If
+ * there are no triggers with transition tables, then return NULL.
+ */
+TriggerTransitionFilter *
+MakeTriggerTransitionFilter(TriggerDesc *trigdesc)
+{
+	TriggerTransitionFilter *result = NULL;
+
+	if (trigdesc != NULL &&
+		(trigdesc->trig_delete_old_table || trigdesc->trig_update_old_table ||
+		 trigdesc->trig_update_new_table || trigdesc->trig_insert_new_table))
+	{
+		result = (TriggerTransitionFilter *)
+			palloc0(sizeof(TriggerTransitionFilter));
+		result->ttf_delete_old_table = trigdesc->trig_delete_old_table;
+		result->ttf_update_old_table = trigdesc->trig_update_old_table;
+		result->ttf_update_new_table = trigdesc->trig_update_new_table;
+		result->ttf_insert_new_table = trigdesc->trig_insert_new_table;
+	}
+
+	return result;
+}
+
+/*
  * Call a trigger function.
  *
  *		trigdata: trigger descriptor.
@@ -2173,7 +2196,7 @@ ExecASInsertTriggers(EState *estate, ResultRelInfo *relinfo)
 
 	if (trigdesc && trigdesc->trig_insert_after_statement)
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
-							  false, NULL, NULL, NIL, NULL);
+							  false, NULL, NULL, NIL, NULL, NULL);
 }
 
 TupleTableSlot *
@@ -2244,14 +2267,17 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
 
 void
 ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
-					 HeapTuple trigtuple, List *recheckIndexes)
+					 HeapTuple trigtuple, List *recheckIndexes,
+					 TriggerTransitionFilter *transitions)
 {
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
 
-	if (trigdesc &&
-		(trigdesc->trig_insert_after_row || trigdesc->trig_insert_new_table))
+	if ((trigdesc && trigdesc->trig_insert_after_row) ||
+		(trigdesc && trigdesc->trig_insert_new_table) ||
+		(transitions && transitions->ttf_insert_new_table))
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
-							  true, NULL, trigtuple, recheckIndexes, NULL);
+							  true, NULL, trigtuple, recheckIndexes, NULL,
+							  transitions);
 }
 
 TupleTableSlot *
@@ -2379,7 +2405,7 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo)
 
 	if (trigdesc && trigdesc->trig_delete_after_statement)
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
-							  false, NULL, NULL, NIL, NULL);
+							  false, NULL, NULL, NIL, NULL, NULL);
 }
 
 bool
@@ -2454,12 +2480,14 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
 void
 ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
-					 HeapTuple fdw_trigtuple)
+					 HeapTuple fdw_trigtuple,
+					 TriggerTransitionFilter *transitions)
 {
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
 
-	if (trigdesc &&
-		(trigdesc->trig_delete_after_row || trigdesc->trig_delete_old_table))
+	if ((trigdesc && trigdesc->trig_delete_after_row) ||
+		(trigdesc && trigdesc->trig_delete_old_table) ||
+		(transitions && transitions->ttf_delete_old_table))
 	{
 		HeapTuple	trigtuple;
 
@@ -2475,7 +2503,8 @@ ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
 			trigtuple = fdw_trigtuple;
 
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_DELETE,
-							  true, trigtuple, NULL, NIL, NULL);
+							  true, trigtuple, NULL, NIL, NULL,
+							  transitions);
 		if (trigtuple != fdw_trigtuple)
 			heap_freetuple(trigtuple);
 	}
@@ -2591,7 +2620,8 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo)
 	if (trigdesc && trigdesc->trig_update_after_statement)
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
 							  false, NULL, NULL, NIL,
-							  GetUpdatedColumns(relinfo, estate));
+							  GetUpdatedColumns(relinfo, estate),
+							  NULL);
 }
 
 TupleTableSlot *
@@ -2716,12 +2746,16 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
 					 HeapTuple fdw_trigtuple,
 					 HeapTuple newtuple,
-					 List *recheckIndexes)
+					 List *recheckIndexes,
+					 TriggerTransitionFilter *transitions)
 {
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
 
-	if (trigdesc && (trigdesc->trig_update_after_row ||
-		 trigdesc->trig_update_old_table || trigdesc->trig_update_new_table))
+	if ((trigdesc && trigdesc->trig_update_after_row) ||
+		(trigdesc && (trigdesc->trig_update_old_table ||
+					  trigdesc->trig_update_new_table)) ||
+		(transitions && (transitions->ttf_update_old_table ||
+						 transitions->ttf_update_new_table)))
 	{
 		HeapTuple	trigtuple;
 
@@ -2738,7 +2772,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
 							  true, trigtuple, newtuple, recheckIndexes,
-							  GetUpdatedColumns(relinfo, estate));
+							  GetUpdatedColumns(relinfo, estate),
+							  transitions);
 		if (trigtuple != fdw_trigtuple)
 			heap_freetuple(trigtuple);
 	}
@@ -2869,7 +2904,7 @@ ExecASTruncateTriggers(EState *estate, ResultRelInfo *relinfo)
 
 	if (trigdesc && trigdesc->trig_truncate_after_statement)
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_TRUNCATE,
-							  false, NULL, NULL, NIL, NULL);
+							  false, NULL, NULL, NIL, NULL, NULL);
 }
 
 
@@ -5080,7 +5115,8 @@ static void
 AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 					  int event, bool row_trigger,
 					  HeapTuple oldtup, HeapTuple newtup,
-					  List *recheckIndexes, Bitmapset *modifiedCols)
+					  List *recheckIndexes, Bitmapset *modifiedCols,
+					  TriggerTransitionFilter *transitions)
 {
 	Relation	rel = relinfo->ri_RelationDesc;
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5110,35 +5146,81 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 	 */
 	if (row_trigger)
 	{
-		if ((event == TRIGGER_EVENT_DELETE &&
-			 trigdesc->trig_delete_old_table) ||
-			(event == TRIGGER_EVENT_UPDATE &&
-			 trigdesc->trig_update_old_table))
+		TupleConversionMap *map = NULL;
+		bool delete_old_table = false;
+		bool update_old_table = false;
+		bool update_new_table = false;
+		bool insert_new_table = false;
+
+		if (trigdesc != NULL)
+		{
+			/*
+			 * Check if we need to capture transition tuples for triggers
+			 * defined on this relation.
+			 */
+			delete_old_table = trigdesc->trig_delete_old_table;
+			update_old_table = trigdesc->trig_update_old_table;
+			update_new_table = trigdesc->trig_update_new_table;
+			insert_new_table = trigdesc->trig_insert_new_table;
+		}
+		if (transitions != NULL)
+		{
+			/*
+			 * A TriggerTransitionFilter was provided to tell us which tuples
+			 * to capture based on a parent table named in a DML statement.
+			 * We may be dealing with a child table with an incompatible
+			 * TupleDescriptor, in which case we'll need a map to convert
+			 * them.
+			 */
+			delete_old_table |= transitions->ttf_delete_old_table;
+			update_old_table |= transitions->ttf_update_old_table;
+			update_new_table |= transitions->ttf_update_new_table;
+			insert_new_table |= transitions->ttf_insert_new_table;
+			map = transitions->ttf_map;
+		}
+
+		if ((event == TRIGGER_EVENT_DELETE && delete_old_table) ||
+			(event == TRIGGER_EVENT_UPDATE && update_old_table))
 		{
 			Tuplestorestate *old_tuplestore;
 
 			Assert(oldtup != NULL);
 			old_tuplestore =
 				GetTriggerTransitionTuplestore
-					(afterTriggers.old_tuplestores);
-			tuplestore_puttuple(old_tuplestore, oldtup);
+						(afterTriggers.old_tuplestores);
+			if (map != NULL)
+			{
+				HeapTuple	converted = do_convert_tuple(oldtup, map);
+
+				tuplestore_puttuple(old_tuplestore, converted);
+				pfree(converted);
+			}
+			else
+				tuplestore_puttuple(old_tuplestore, oldtup);
 		}
-		if ((event == TRIGGER_EVENT_INSERT &&
-			 trigdesc->trig_insert_new_table) ||
-			(event == TRIGGER_EVENT_UPDATE &&
-			 trigdesc->trig_update_new_table))
+		if ((event == TRIGGER_EVENT_INSERT && insert_new_table) ||
+			(event == TRIGGER_EVENT_UPDATE && update_new_table))
 		{
 			Tuplestorestate *new_tuplestore;
 
 			Assert(newtup != NULL);
 			new_tuplestore =
 				GetTriggerTransitionTuplestore
-					(afterTriggers.new_tuplestores);
-			tuplestore_puttuple(new_tuplestore, newtup);
+						(afterTriggers.new_tuplestores);
+			if (map != NULL)
+			{
+				HeapTuple	converted = do_convert_tuple(newtup, map);
+
+				tuplestore_puttuple(new_tuplestore, converted);
+				pfree(converted);
+			}
+			else
+				tuplestore_puttuple(new_tuplestore, newtup);
 		}
 
 		/* If transition tables are the only reason we're here, return. */
-		if ((event == TRIGGER_EVENT_DELETE && !trigdesc->trig_delete_after_row) ||
+		if (trigdesc == NULL ||
+			(event == TRIGGER_EVENT_DELETE && !trigdesc->trig_delete_after_row) ||
 			(event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) ||
 			(event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_after_row))
 			return;
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index cdb1a6a5f5d..ab7384f2c86 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -3204,7 +3204,7 @@ EvalPlanQualEnd(EPQState *epqstate)
  * 'tup_conv_maps' receives an array of TupleConversionMap objects with one
  *		entry for every leaf partition (required to convert input tuple based
  *		on the root table's rowtype to a leaf partition's rowtype after tuple
- *		routing is done
+ *		routing is done)
  * 'partition_tuple_slot' receives a standalone TupleTableSlot to be used
  *		to manipulate any given leaf partition's rowtype after that partition
  *		is chosen by tuple-routing.
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 327a0bad388..a70d48291f9 100644
--- a/src/backend/executor/execReplication.c
+++ b/src/backend/executor/execReplication.c
@@ -404,7 +404,7 @@ ExecSimpleRelationInsert(EState *estate, TupleTableSlot *slot)
 
 		/* AFTER ROW INSERT Triggers */
 		ExecARInsertTriggers(estate, resultRelInfo, tuple,
-							 recheckIndexes);
+							 recheckIndexes, NULL);
 
 		list_free(recheckIndexes);
 	}
@@ -466,7 +466,7 @@ ExecSimpleRelationUpdate(EState *estate, EPQState *epqstate,
 		/* AFTER ROW UPDATE Triggers */
 		ExecARUpdateTriggers(estate, resultRelInfo,
 							 &searchslot->tts_tuple->t_self,
-							 NULL, tuple, recheckIndexes);
+							 NULL, tuple, recheckIndexes, NULL);
 
 		list_free(recheckIndexes);
 	}
@@ -509,7 +509,7 @@ ExecSimpleRelationDelete(EState *estate, EPQState *epqstate,
 
 		/* AFTER ROW DELETE Triggers */
 		ExecARDeleteTriggers(estate, resultRelInfo,
-							 &searchslot->tts_tuple->t_self, NULL);
+							 &searchslot->tts_tuple->t_self, NULL, NULL);
 
 		list_free(recheckIndexes);
 	}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 652cd975996..b9f44bb2bad 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -571,7 +571,8 @@ ExecInsert(ModifyTableState *mtstate,
 	}
 
 	/* AFTER ROW INSERT Triggers */
-	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
+	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes,
+						 mtstate->mt_transition_filter);
 
 	list_free(recheckIndexes);
 
@@ -619,7 +620,8 @@ ExecInsert(ModifyTableState *mtstate,
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecDelete(ItemPointer tupleid,
+ExecDelete(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
@@ -796,7 +798,8 @@ ldelete:;
 		(estate->es_processed)++;
 
 	/* AFTER ROW DELETE Triggers */
-	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple);
+	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+						 mtstate->mt_transition_filter);
 
 	/* Process RETURNING if present */
 	if (resultRelInfo->ri_projectReturning)
@@ -877,7 +880,8 @@ ldelete:;
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecUpdate(ItemPointer tupleid,
+ExecUpdate(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
 		   TupleTableSlot *planSlot,
@@ -1105,7 +1109,8 @@ lreplace:;
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
-						 recheckIndexes);
+						 recheckIndexes,
+						 mtstate->mt_transition_filter);
 
 	list_free(recheckIndexes);
 
@@ -1312,7 +1317,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
 	 */
 
 	/* Execute UPDATE with projection */
-	*returning = ExecUpdate(&tuple.t_self, NULL,
+	*returning = ExecUpdate(mtstate, &tuple.t_self, NULL,
 							mtstate->mt_conflproj, planSlot,
 							&mtstate->mt_epqstate, mtstate->ps.state,
 							canSetTag);
@@ -1492,6 +1497,11 @@ ExecModifyTable(ModifyTableState *node)
 				estate->es_result_relation_info = resultRelInfo;
 				EvalPlanQualSetPlan(&node->mt_epqstate, subplanstate->plan,
 									node->mt_arowmarks[node->mt_whichplan]);
+				if (node->mt_transition_filter != NULL)
+				{
+					node->mt_transition_filter->ttf_map =
+						node->mt_transition_tupconv_maps[node->mt_whichplan];
+				}
 				continue;
 			}
 			else
@@ -1602,11 +1612,11 @@ ExecModifyTable(ModifyTableState *node)
 								  estate, node->canSetTag);
 				break;
 			case CMD_UPDATE:
-				slot = ExecUpdate(tupleid, oldtuple, slot, planSlot,
+				slot = ExecUpdate(node, tupleid, oldtuple, slot, planSlot,
 								&node->mt_epqstate, estate, node->canSetTag);
 				break;
 			case CMD_DELETE:
-				slot = ExecDelete(tupleid, oldtuple, planSlot,
+				slot = ExecDelete(node, tupleid, oldtuple, planSlot,
 								&node->mt_epqstate, estate, node->canSetTag);
 				break;
 			default:
@@ -1650,7 +1660,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	int			nplans = list_length(node->plans);
 	ResultRelInfo *saved_resultRelInfo;
 	ResultRelInfo *resultRelInfo;
-	TupleDesc	tupDesc;
+	TupleDesc	tupDesc = NULL;
 	Plan	   *subplan;
 	ListCell   *l;
 	int			i;
@@ -1788,6 +1798,48 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		mtstate->mt_partition_tuple_slot = partition_tuple_slot;
 	}
 
+	/* Check if we need to capture transition tuples from child tables. */
+	if (estate->es_num_root_result_relations > 0)
+	{
+		/* Partitioned table.  The named relation is from the first root. */
+		mtstate->mt_transition_filter =
+			MakeTriggerTransitionFilter(estate->es_root_result_relations[0].ri_TrigDesc);
+		tupDesc = RelationGetDescr(estate->es_root_result_relations[0].ri_RelationDesc);
+	}
+	else if (mtstate->mt_nplans > 1)
+	{
+		/* Inheritance hierarchy.  The named relation is from the first plan. */
+		mtstate->mt_transition_filter =
+			MakeTriggerTransitionFilter(mtstate->resultRelInfo[0].ri_TrigDesc);
+		tupDesc = RelationGetDescr(mtstate->resultRelInfo[0].ri_RelationDesc);
+	}
+
+	if (mtstate->mt_transition_filter != NULL)
+	{
+		int		i;
+
+		/*
+		 * If there are any partitioning or inheritance child tables, then
+		 * we'll need to be able to convert their tuples to match the target
+		 * table's TupleDescriptor before putting any new and old images into
+		 * its tuplestores.  So we'll need a list of TupleConversionMaps
+		 * corresponding to the list of subplans.
+		 */
+		mtstate->mt_transition_tupconv_maps = (TupleConversionMap **)
+			palloc(sizeof(TupleConversionMap *) * mtstate->mt_nplans);
+		for (i = 0; i < mtstate->mt_nplans; ++i)
+		{
+			mtstate->mt_transition_tupconv_maps[i] =
+				convert_tuples_by_name(RelationGetDescr(mtstate->resultRelInfo[i].ri_RelationDesc),
+									   tupDesc,
+									   gettext_noop("could not convert row type"));
+		}
+
+		/* Install conversion map for first plan. */
+		mtstate->mt_transition_filter->ttf_map =
+			mtstate->mt_transition_tupconv_maps[0];
+	}
+
 	/*
 	 * Initialize any WITH CHECK OPTION constraints if needed.
 	 */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index d73969c8747..1db8f3d2d25 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -42,6 +42,21 @@ typedef struct TriggerData
 } TriggerData;
 
 /*
+ * Meta-data to control the capture of old and new tuples into transition
+ * tables for a trigger on a partitioned table or a parent in an inheritance
+ * hierarchy.
+ */
+typedef struct TriggerTransitionFilter
+{
+	/* Is there at least one trigger specifying each transition relation? */
+	bool		ttf_delete_old_table;
+	bool		ttf_update_old_table;
+	bool		ttf_update_new_table;
+	bool		ttf_insert_new_table;
+	TupleConversionMap *ttf_map;
+} TriggerTransitionFilter;
+
+/*
  * TriggerEvent bit flags
  *
  * Note that we assume different event types (INSERT/DELETE/UPDATE/TRUNCATE)
@@ -127,6 +142,8 @@ extern void RelationBuildTriggers(Relation relation);
 
 extern TriggerDesc *CopyTriggerDesc(TriggerDesc *trigdesc);
 
+extern TriggerTransitionFilter *MakeTriggerTransitionFilter(TriggerDesc *trigdesc);
+
 extern void FreeTriggerDesc(TriggerDesc *trigdesc);
 
 extern void ExecBSInsertTriggers(EState *estate,
@@ -139,7 +156,8 @@ extern TupleTableSlot *ExecBRInsertTriggers(EState *estate,
 extern void ExecARInsertTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple,
-					 List *recheckIndexes);
+					 List *recheckIndexes,
+					 TriggerTransitionFilter *transitions);
 extern TupleTableSlot *ExecIRInsertTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 TupleTableSlot *slot);
@@ -155,7 +173,8 @@ extern bool ExecBRDeleteTriggers(EState *estate,
 extern void ExecARDeleteTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
-					 HeapTuple fdw_trigtuple);
+					 HeapTuple fdw_trigtuple,
+					 TriggerTransitionFilter *transitions);
 extern bool ExecIRDeleteTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple);
@@ -174,7 +193,8 @@ extern void ExecARUpdateTriggers(EState *estate,
 					 ItemPointer tupleid,
 					 HeapTuple fdw_trigtuple,
 					 HeapTuple newtuple,
-					 List *recheckIndexes);
+					 List *recheckIndexes,
+					 TriggerTransitionFilter *transitions);
 extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index f289f3c3c25..5b7ce1e6cd4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -947,6 +947,10 @@ typedef struct ModifyTableState
 	TupleConversionMap **mt_partition_tupconv_maps;
 									/* Per partition tuple conversion map */
 	TupleTableSlot *mt_partition_tuple_slot;
+	struct TriggerTransitionFilter *mt_transition_filter;
+									/* controls transition table population */
+	TupleConversionMap **mt_transition_tupconv_maps;
+									/* Per subplan tuple conversion map */
 } ModifyTableState;
 
 /* ----------------
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 10a301310b4..fa47034b8e1 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -1764,30 +1764,6 @@ drop table upsert;
 drop function upsert_before_func();
 drop function upsert_after_func();
 --
--- Verify that triggers are prevented on partitioned tables if they would
--- access row data (ROW and STATEMENT-with-transition-table)
---
-create table my_table (i int) partition by list (i);
-create table my_table_42 partition of my_table for values in (42);
-create function my_trigger_function() returns trigger as $$ begin end; $$ language plpgsql;
-create trigger my_trigger before update on my_table for each row execute procedure my_trigger_function();
-ERROR:  "my_table" is a partitioned table
-DETAIL:  Partitioned tables cannot have ROW triggers.
-create trigger my_trigger after update on my_table referencing old table as old_table
-   for each statement execute procedure my_trigger_function();
-ERROR:  "my_table" is a partitioned table
-DETAIL:  Triggers on partitioned tables cannot have transition tables.
---
--- Verify that triggers are allowed on partitions
---
-create trigger my_trigger before update on my_table_42 for each row execute procedure my_trigger_function();
-drop trigger my_trigger on my_table_42;
-create trigger my_trigger after update on my_table_42 referencing old table as old_table
-   for each statement execute procedure my_trigger_function();
-drop trigger my_trigger on my_table_42;
-drop table my_table_42;
-drop table my_table;
---
 -- Verify that per-statement triggers are fired for partitioned tables
 --
 create table parted_stmt_trig (a int) partition by list (a);
@@ -1868,3 +1844,61 @@ delete from parted_stmt_trig;
 NOTICE:  trigger on parted_stmt_trig BEFORE DELETE for STATEMENT
 NOTICE:  trigger on parted_stmt_trig AFTER DELETE for STATEMENT
 drop table parted_stmt_trig, parted2_stmt_trig;
+--
+-- Verify behavior of statement triggers on partition parent with
+-- transition tables
+--
+-- set up a partition hierarchy with some different TupleDescriptors
+create table parent (a text, b int) partition by list (a);
+create table child1 partition of parent for values in ('AAA');
+insert into child1 values ('AAA', 42);
+-- a child with a dropped column
+create table child2 (x int, a text, b int);
+insert into child2 values (42, 'BBB', 42);
+alter table child2 drop column x;
+alter table parent attach partition child2 for values in ('BBB');
+-- a child with a different order
+create table child3 (b int, a text);
+insert into child3 values (42, 'CCC');
+alter table parent attach partition child3 for values in ('CCC');
+create or replace function dump_transition_tables() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'old table = %, new table = %',
+                 (select json_agg(row_to_json(old_table) order by a) from old_table),
+                 (select json_agg(row_to_json(new_table) order by a) from new_table);
+    return null;
+  end;
+$$;
+create trigger parent_stmt_trig
+  after insert or update or delete on parent
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+update parent set b = b + 1;
+NOTICE:  old table = [{"a":"AAA","b":42}, {"a":"BBB","b":42}, {"a":"CCC","b":42}], new table = [{"a":"AAA","b":43}, {"a":"BBB","b":43}, {"a":"CCC","b":43}]
+drop table child1, child2, child3, parent;
+--
+-- Verify behavior of statement triggers on inheritance parent with
+-- transition tables
+--
+-- set up inheritance hierarchy with different TupleDescriptors
+create table parent (a text, b int);
+create table child1 () inherits (parent);
+insert into child1 values ('AAA', 42);
+-- a child with a different order
+create table child2 (b int, a text);
+alter table child2 inherit parent;
+insert into child2 values (42, 'BBB');
+-- a child with an extra column that should be sliced off
+create table child3 (c text) inherits (parent);
+insert into child3 values ('CCC', 42, 'foo');
+create trigger parent_stmt_trig
+  after insert or update or delete on parent
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+update parent set b = b + 1;
+NOTICE:  old table = [{"a":"AAA","b":42}, {"a":"BBB","b":42}, {"a":"CCC","b":42}], new table = [{"a":"AAA","b":43}, {"a":"BBB","b":43}, {"a":"CCC","b":43}]
+drop table child1, child2, child3, parent;
+drop function dump_transition_tables();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 84b5ada5544..1ece6a3e74e 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1242,29 +1242,6 @@ drop function upsert_before_func();
 drop function upsert_after_func();
 
 --
--- Verify that triggers are prevented on partitioned tables if they would
--- access row data (ROW and STATEMENT-with-transition-table)
---
-
-create table my_table (i int) partition by list (i);
-create table my_table_42 partition of my_table for values in (42);
-create function my_trigger_function() returns trigger as $$ begin end; $$ language plpgsql;
-create trigger my_trigger before update on my_table for each row execute procedure my_trigger_function();
-create trigger my_trigger after update on my_table referencing old table as old_table
-   for each statement execute procedure my_trigger_function();
-
---
--- Verify that triggers are allowed on partitions
---
-create trigger my_trigger before update on my_table_42 for each row execute procedure my_trigger_function();
-drop trigger my_trigger on my_table_42;
-create trigger my_trigger after update on my_table_42 referencing old table as old_table
-   for each statement execute procedure my_trigger_function();
-drop trigger my_trigger on my_table_42;
-drop table my_table_42;
-drop table my_table;
-
---
 -- Verify that per-statement triggers are fired for partitioned tables
 --
 create table parted_stmt_trig (a int) partition by list (a);
@@ -1333,3 +1310,74 @@ with upd as (
 
 delete from parted_stmt_trig;
 drop table parted_stmt_trig, parted2_stmt_trig;
+
+--
+-- Verify behavior of statement triggers on partition parent with
+-- transition tables
+--
+
+-- set up a partition hierarchy with some different TupleDescriptors
+create table parent (a text, b int) partition by list (a);
+create table child1 partition of parent for values in ('AAA');
+insert into child1 values ('AAA', 42);
+
+-- a child with a dropped column
+create table child2 (x int, a text, b int);
+insert into child2 values (42, 'BBB', 42);
+alter table child2 drop column x;
+alter table parent attach partition child2 for values in ('BBB');
+
+-- a child with a different order
+create table child3 (b int, a text);
+insert into child3 values (42, 'CCC');
+alter table parent attach partition child3 for values in ('CCC');
+
+create or replace function dump_transition_tables() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'old table = %, new table = %',
+                 (select json_agg(row_to_json(old_table) order by a) from old_table),
+                 (select json_agg(row_to_json(new_table) order by a) from new_table);
+    return null;
+  end;
+$$;
+
+create trigger parent_stmt_trig
+  after insert or update or delete on parent
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+
+update parent set b = b + 1;
+
+drop table child1, child2, child3, parent;
+
+--
+-- Verify behavior of statement triggers on inheritance parent with
+-- transition tables
+--
+
+-- set up inheritance hierarchy with different TupleDescriptors
+create table parent (a text, b int);
+create table child1 () inherits (parent);
+insert into child1 values ('AAA', 42);
+
+-- a child with a different order
+create table child2 (b int, a text);
+alter table child2 inherit parent;
+insert into child2 values (42, 'BBB');
+
+-- a child with an extra column that should be sliced off
+create table child3 (c text) inherits (parent);
+insert into child3 values ('CCC', 42, 'foo');
+
+create trigger parent_stmt_trig
+  after insert or update or delete on parent
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+
+update parent set b = b + 1;
+
+drop table child1, child2, child3, parent;
+drop function dump_transition_tables();
