diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index 04214fc2031..d59027fd07a 100644
--- a/src/backend/catalog/pg_inherits.c
+++ b/src/backend/catalog/pg_inherits.c
@@ -273,6 +273,28 @@ has_subclass(Oid relationId)
 	return result;
 }
 
+/*
+ * has_superclass - does this relation inherit from another?
+ */
+bool
+has_superclass(Oid relationId)
+{
+	Relation	catalog;
+	SysScanDesc scan;
+	ScanKeyData skey;
+	bool		result;
+
+	catalog = heap_open(InheritsRelationId, AccessShareLock);
+	ScanKeyInit(&skey, Anum_pg_inherits_inhrelid, BTEqualStrategyNumber,
+				F_OIDEQ, ObjectIdGetDatum(relationId));
+	scan = systable_beginscan(catalog, InheritsRelidSeqnoIndexId, true,
+							  NULL, 1, &skey);
+	result = HeapTupleIsValid(systable_getnext(scan));
+	systable_endscan(scan);
+	heap_close(catalog, AccessShareLock);
+
+	return result;
+}
 
 /*
  * Given two type OIDs, determine whether the first is a complex type
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 137b1ef42d9..839db726623 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -171,6 +171,7 @@ typedef struct CopyStateData
 	ResultRelInfo *partitions;	/* Per partition result relation */
 	TupleConversionMap **partition_tupconv_maps;
 	TupleTableSlot *partition_tuple_slot;
+	TriggerTransitionState *transitions;
 
 	/*
 	 * These variables are used to reduce overhead in textual COPY FROM.
@@ -1436,6 +1437,13 @@ BeginCopy(ParseState *pstate,
 			cstate->num_partitions = num_partitions;
 			cstate->partition_tupconv_maps = partition_tupconv_maps;
 			cstate->partition_tuple_slot = partition_tuple_slot;
+
+			/*
+			 * If there are any triggers with transition tables on the named
+			 * relation, we need to be prepared to capture transition tuples
+			 * from child relations too.
+			 */
+			cstate->transitions = MakeTriggerTransitionState(rel->trigdesc);
 		}
 	}
 	else
@@ -2292,6 +2300,7 @@ uint64
 CopyFrom(CopyState cstate)
 {
 	HeapTuple	tuple;
+	HeapTuple	original_tuple = NULL;
 	TupleDesc	tupDesc;
 	Datum	   *values;
 	bool	   *nulls;
@@ -2595,6 +2604,7 @@ CopyFrom(CopyState cstate)
 			 * We might need to convert from the parent rowtype to the
 			 * partition rowtype.
 			 */
+			original_tuple = tuple;
 			map = cstate->partition_tupconv_maps[leaf_part_index];
 			if (map)
 			{
@@ -2686,9 +2696,20 @@ CopyFrom(CopyState cstate)
 															   NULL,
 															   NIL);
 
+					/*
+					 * If there are transition tables and we've routed the
+					 * insertion to a partition, make sure that the transition
+					 * tables (if configured) also receive the tuple in the
+					 * named relation's format without having to convert it
+					 * back.
+					 */
+					if (cstate->transitions != NULL)
+						cstate->transitions->original_insert_tuple =
+							original_tuple;
+
 					/* AFTER ROW INSERT Triggers */
 					ExecARInsertTriggers(estate, resultRelInfo, tuple,
-										 recheckIndexes);
+										 recheckIndexes, cstate->transitions);
 
 					list_free(recheckIndexes);
 				}
@@ -2841,7 +2862,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 									  estate, false, NULL, NIL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
-								 recheckIndexes);
+								 recheckIndexes, NULL);
 			list_free(recheckIndexes);
 		}
 	}
@@ -2858,7 +2879,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/tablecmds.c b/src/backend/commands/tablecmds.c
index e2593780511..eb11b3fc309 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10978,6 +10978,7 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode)
 	Relation	parent_rel;
 	List	   *children;
 	ObjectAddress address;
+	const char *trigger_name;
 
 	/*
 	 * A self-exclusive lock is needed here.  See the similar case in
@@ -11059,6 +11060,19 @@ ATExecAddInherit(Relation child_rel, RangeVar *parent, LOCKMODE lockmode)
 						RelationGetRelationName(child_rel),
 						RelationGetRelationName(parent_rel))));
 
+	/*
+	 * If child_rel has row-level triggers with transition tables, we
+	 * currently don't allow it to become an inheritance child.  See also
+	 * prohibitions in ATExecAttachPartition() and CreateTrigger().
+	 */
+	trigger_name = FindTriggerIncompatibleWithInheritance(child_rel->trigdesc);
+	if (trigger_name != NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("trigger \"%s\" prevents table \"%s\" from becoming an inheritance child",
+						trigger_name, RelationGetRelationName(child_rel)),
+				 errdetail("ROW triggers with transition tables are not supported in inheritance hierarchies")));
+
 	/* OK to create inheritance */
 	CreateInheritance(child_rel, parent_rel);
 
@@ -13430,6 +13444,7 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd)
 	TupleDesc	tupleDesc;
 	bool		skip_validate = false;
 	ObjectAddress address;
+	const char *trigger_name;
 
 	attachRel = heap_openrv(cmd->name, AccessExclusiveLock);
 
@@ -13559,6 +13574,19 @@ ATExecAttachPartition(List **wqueue, Relation rel, PartitionCmd *cmd)
 					 errdetail("New partition should contain only the columns present in parent.")));
 	}
 
+	/*
+	 * If child_rel has row-level triggers with transition tables, we
+	 * currently don't allow it to become a partition.  See also prohibitions
+	 * in ATExecAddInherit() and CreateTrigger().
+	 */
+	trigger_name = FindTriggerIncompatibleWithInheritance(attachRel->trigdesc);
+	if (trigger_name != NULL)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("trigger \"%s\" prevents table \"%s\" from becoming a partition",
+						trigger_name, RelationGetRelationName(attachRel)),
+				 errdetail("ROW triggers with transition tables are not supported on partitions")));
+
 	/* OK to create inheritance.  Rest of the checks performed there */
 	CreateInheritance(attachRel, rel);
 
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 1566fb46074..9e20f0e833d 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -24,6 +24,7 @@
 #include "catalog/objectaccess.h"
 #include "catalog/pg_constraint.h"
 #include "catalog/pg_constraint_fn.h"
+#include "catalog/pg_inherits_fn.h"
 #include "catalog/pg_proc.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
@@ -96,7 +97,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,
+					  TriggerTransitionState *transitions);
 static void AfterTriggerEnlargeQueryState(void);
 
 
@@ -354,13 +356,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 (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -375,6 +370,27 @@ CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 								RelationGetRelationName(rel)),
 					 errdetail("Triggers on views cannot have transition tables.")));
 
+			/*
+			 * We currently don't allow row-level triggers with transition
+			 * tables on partition or inheritance children.  Such triggers
+			 * would somehow need to see tuples converted to the format of the
+			 * table they're attached to, and it's not clear which subset of
+			 * tuples each child should see.  See also the prohibitions in
+			 * ATExecAttachPartition() and ATExecAddInherit().
+			 */
+			if (TRIGGER_FOR_ROW(tgtype) && has_superclass(rel->rd_id))
+			{
+				/* Use appropriate error message. */
+				if (rel->rd_rel->relispartition)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("ROW triggers with transition tables are not support on partitions")));
+				else
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("ROW triggers with transition tables are not supported on inheritance children")));
+			}
+
 			if (stmt->timing != TRIGGER_TYPE_AFTER)
 				ereport(ERROR,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -2029,6 +2045,64 @@ equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2)
 #endif   /* NOT_USED */
 
 /*
+ * Check if there is a row-level trigger with transition tables that prevents
+ * a table from becoming an inheritance child or partition.  Return the name
+ * of the first such incompatible trigger, or NULL if there is none.
+ */
+const char *
+FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc)
+{
+	if (trigdesc != NULL)
+	{
+		int		i;
+
+		for (i = 0; i < trigdesc->numtriggers; ++i)
+		{
+			Trigger	   *trigger = &trigdesc->triggers[i];
+
+			if (trigger->tgoldtable != NULL || trigger->tgnewtable != NULL)
+				return trigger->tgname;
+		}
+	}
+
+	return NULL;
+}
+
+/*
+ * Make a TriggerTransitionState object from a given TriggerDesc.  The
+ * resulting object 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.
+ *
+ * If there are no triggers with transition tables configured for 'trigdesc',
+ * then return NULL.
+ *
+ * The resulting object can be passed to the ExecAR* functions.  The caller
+ * should set ttf_map or original_insert_tuple as appropriate when dealing
+ * with child tables.
+ */
+TriggerTransitionState *
+MakeTriggerTransitionState(TriggerDesc *trigdesc)
+{
+	TriggerTransitionState *state = NULL;
+
+	if (trigdesc != NULL &&
+		(trigdesc->trig_delete_old_table || trigdesc->trig_update_old_table ||
+		 trigdesc->trig_update_new_table || trigdesc->trig_insert_new_table))
+	{
+		state = (TriggerTransitionState *)
+			palloc0(sizeof(TriggerTransitionState));
+		state->ttf_delete_old_table = trigdesc->trig_delete_old_table;
+		state->ttf_update_old_table = trigdesc->trig_update_old_table;
+		state->ttf_update_new_table = trigdesc->trig_update_new_table;
+		state->ttf_insert_new_table = trigdesc->trig_insert_new_table;
+	}
+
+	return state;
+}
+
+/*
  * Call a trigger function.
  *
  *		trigdata: trigger descriptor.
@@ -2192,7 +2266,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 *
@@ -2263,14 +2337,18 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
 
 void
 ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
-					 HeapTuple trigtuple, List *recheckIndexes)
+					 HeapTuple trigtuple, List *recheckIndexes,
+					 TriggerTransitionState *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 && !transitions && 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 *
@@ -2398,7 +2476,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
@@ -2473,12 +2551,14 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
 void
 ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
-					 HeapTuple fdw_trigtuple)
+					 HeapTuple fdw_trigtuple,
+					 TriggerTransitionState *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 && !transitions && trigdesc->trig_delete_old_table) ||
+		(transitions && transitions->ttf_delete_old_table))
 	{
 		HeapTuple	trigtuple;
 
@@ -2494,7 +2574,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);
 	}
@@ -2610,7 +2691,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 *
@@ -2735,12 +2817,16 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
 					 HeapTuple fdw_trigtuple,
 					 HeapTuple newtuple,
-					 List *recheckIndexes)
+					 List *recheckIndexes,
+					 TriggerTransitionState *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 && !transitions && (trigdesc->trig_update_old_table ||
+									  trigdesc->trig_update_new_table)) ||
+		(transitions && (transitions->ttf_update_old_table ||
+						 transitions->ttf_update_new_table)))
 	{
 		HeapTuple	trigtuple;
 
@@ -2757,7 +2843,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);
 	}
@@ -2888,7 +2975,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);
 }
 
 
@@ -5090,7 +5177,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,
+					  TriggerTransitionState *transitions)
 {
 	Relation	rel = relinfo->ri_RelationDesc;
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5120,35 +5208,89 @@ 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))
+		HeapTuple original_insert_tuple = NULL;
+		TupleConversionMap *map = NULL;
+		bool delete_old_table = false;
+		bool update_old_table = false;
+		bool update_new_table = false;
+		bool insert_new_table = false;
+
+		if (transitions != NULL)
+		{
+			/*
+			 * A TriggerTransitionState object 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, or we'll need the original tuple (to skip a
+			 * wasteful parent->child->parent conversion round trip).
+			 */
+			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;
+			original_insert_tuple = transitions->original_insert_tuple;
+		}
+		else if (trigdesc != NULL)
+		{
+			/*
+			 * Check if we need to capture transition tuples for triggers
+			 * defined on this relation directly.  This case is useful for
+			 * cases like execReplication.c which don't set up a
+			 * TriggerTransitionState because they don't know how to work with
+			 * partitions.
+			 */
+			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 ((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 (original_insert_tuple != NULL)
+				tuplestore_puttuple(new_tuplestore, original_insert_tuple);
+			else 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 fb2ba3302c0..9979be65278 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -3207,7 +3207,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 6af8018b711..f49d1db62d6 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..997e95852e3 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -258,6 +258,7 @@ ExecInsert(ModifyTableState *mtstate,
 		   bool canSetTag)
 {
 	HeapTuple	tuple;
+	HeapTuple	original_tuple = NULL;
 	ResultRelInfo *resultRelInfo;
 	ResultRelInfo *saved_resultRelInfo = NULL;
 	Relation	resultRelationDesc;
@@ -317,6 +318,7 @@ ExecInsert(ModifyTableState *mtstate,
 		 * We might need to convert from the parent rowtype to the partition
 		 * rowtype.
 		 */
+		original_tuple = tuple;
 		map = mtstate->mt_partition_tupconv_maps[leaf_part_index];
 		if (map)
 		{
@@ -570,8 +572,17 @@ ExecInsert(ModifyTableState *mtstate,
 		setLastTid(&(tuple->t_self));
 	}
 
+	/*
+	 * If we inserted into a partitioned table, then insert routing logic may
+	 * have converted the tuple to a partition's format.  Make the original
+	 * unconverted tuple available for transition tables.
+	 */
+	if (mtstate->mt_transition_state != NULL)
+		mtstate->mt_transition_state->original_insert_tuple = original_tuple;
+
 	/* AFTER ROW INSERT Triggers */
-	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
+	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes,
+						 mtstate->mt_transition_state);
 
 	list_free(recheckIndexes);
 
@@ -619,7 +630,8 @@ ExecInsert(ModifyTableState *mtstate,
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecDelete(ItemPointer tupleid,
+ExecDelete(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
@@ -796,7 +808,8 @@ ldelete:;
 		(estate->es_processed)++;
 
 	/* AFTER ROW DELETE Triggers */
-	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple);
+	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+						 mtstate->mt_transition_state);
 
 	/* Process RETURNING if present */
 	if (resultRelInfo->ri_projectReturning)
@@ -877,7 +890,8 @@ ldelete:;
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecUpdate(ItemPointer tupleid,
+ExecUpdate(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
 		   TupleTableSlot *planSlot,
@@ -1105,7 +1119,8 @@ lreplace:;
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
-						 recheckIndexes);
+						 recheckIndexes,
+						 mtstate->mt_transition_state);
 
 	list_free(recheckIndexes);
 
@@ -1312,7 +1327,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);
@@ -1394,6 +1409,72 @@ fireASTriggers(ModifyTableState *node)
 	}
 }
 
+/*
+ * Set up the state needed for collecting transition tuples for AFTER
+ * triggers.
+ */
+static void
+ExecSetupTriggerTransitionState(ModifyTableState *mtstate, EState *estate)
+{
+	ResultRelInfo *targetRelInfo;
+	int		i;
+
+	/*
+	 * Find the ResultRelInfo corresponding to the relation that was
+	 * explicitly named in the statement.
+	 */
+	if (mtstate->rootResultRelInfo != NULL)
+	{
+		/* Partitioned table. */
+		targetRelInfo = mtstate->rootResultRelInfo;
+	}
+	else
+	{
+		/*
+		 * Inheritance hierarchy or single relation.  The named relation is
+		 * the first subplan.
+		 */
+		targetRelInfo = &mtstate->resultRelInfo[0];
+	}
+
+	/* Check for transition tables on the directly targeted relation. */
+	mtstate->mt_transition_state =
+		MakeTriggerTransitionState(targetRelInfo->ri_TrigDesc);
+
+	/*
+	 * If we found that we need to collect transition tuples then we may also
+	 * need tuple conversion maps for any children that have TupleDescs that
+	 * aren't compatible with the tuplestores.  Only needed for UPDATE and
+	 * DELETE, because INSERT already has tuples in the appropriate format.
+	 */
+	if (mtstate->mt_transition_state != NULL &&
+		(mtstate->operation == CMD_UPDATE || mtstate->operation == CMD_DELETE))
+	{
+		/*
+		 * Build array of conversion maps from each child's TupleDesc to the
+		 * one used in the tuplestore.  The map pointers may be NULL when no
+		 * conversion is necessary, which is hopefully a common case for
+		 * partitions.
+		 */
+		mtstate->mt_transition_tupconv_maps = (TupleConversionMap **)
+			palloc0(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),
+									   RelationGetDescr(targetRelInfo->ri_RelationDesc),
+									   gettext_noop("could not convert row type"));
+		}
+
+		/*
+		 * Install the conversion map for the first plan for UPDATE and DELETE
+		 * operations.  It will be advanced each time we switch to the next
+		 * plan.
+		 */
+		mtstate->mt_transition_state->ttf_map =
+			mtstate->mt_transition_tupconv_maps[0];
+	}
+}
 
 /* ----------------------------------------------------------------
  *	   ExecModifyTable
@@ -1492,6 +1573,13 @@ 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_state != NULL)
+				{
+					/* Prepare to convert transition tuples from this child. */
+					Assert(node->mt_transition_tupconv_maps != NULL);
+					node->mt_transition_state->ttf_map =
+						node->mt_transition_tupconv_maps[node->mt_whichplan];
+				}
 				continue;
 			}
 			else
@@ -1602,11 +1690,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:
@@ -1788,6 +1876,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		mtstate->mt_partition_tuple_slot = partition_tuple_slot;
 	}
 
+	/* Build state for collecting transition tuples */
+	ExecSetupTriggerTransitionState(mtstate, estate);
+
 	/*
 	 * Initialize any WITH CHECK OPTION constraints if needed.
 	 */
diff --git a/src/backend/utils/misc/queryenvironment.c b/src/backend/utils/misc/queryenvironment.c
index a0b10d402bd..dfb23d14f15 100644
--- a/src/backend/utils/misc/queryenvironment.c
+++ b/src/backend/utils/misc/queryenvironment.c
@@ -127,7 +127,8 @@ ENRMetadataGetTupDesc(EphemeralNamedRelationMetadata enrmd)
 	TupleDesc	tupdesc;
 
 	/* One, and only one, of these fields must be filled. */
-	Assert((enrmd->reliddesc == InvalidOid) != (enrmd->tupdesc == NULL));
+	Assert((enrmd->reliddesc == InvalidOid) !=
+		   (enrmd->tupdesc == NULL));
 
 	if (enrmd->tupdesc != NULL)
 		tupdesc = enrmd->tupdesc;
diff --git a/src/include/catalog/pg_inherits_fn.h b/src/include/catalog/pg_inherits_fn.h
index e33b39e71ec..6eb623d6fbd 100644
--- a/src/include/catalog/pg_inherits_fn.h
+++ b/src/include/catalog/pg_inherits_fn.h
@@ -21,6 +21,7 @@ extern List *find_inheritance_children(Oid parentrelId, LOCKMODE lockmode);
 extern List *find_all_inheritors(Oid parentrelId, LOCKMODE lockmode,
 					List **parents);
 extern bool has_subclass(Oid relationId);
+extern bool has_superclass(Oid relationId);
 extern bool typeInheritsFrom(Oid subclassTypeId, Oid superclassTypeId);
 
 #endif   /* PG_INHERITS_FN_H */
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index d73969c8747..bd43da9cc62 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -42,6 +42,39 @@ typedef struct TriggerData
 } TriggerData;
 
 /*
+ * Meta-data to control the capture of old and new tuples into transition
+ * tables from child tables.
+ */
+typedef struct TriggerTransitionState
+{
+	/*
+	 * Is there at least one trigger specifying each transition relation on
+	 * the relation explicitly named in the DML statement or COPY command?
+	 */
+	bool		ttf_delete_old_table;
+	bool		ttf_update_old_table;
+	bool		ttf_update_new_table;
+	bool		ttf_insert_new_table;
+
+	/*
+	 * For UPDATE and DELETE, AfterTriggerSaveEvent may need to convert the
+	 * new and old tuples from a child table's format to the format of the
+	 * relation named in a query so that it is compatible with the transition
+	 * tuplestores.
+	 */
+	TupleConversionMap *ttf_map;
+
+	/*
+	 * For INSERT and COPY, it would be wasteful to convert tuples from child
+	 * format to parent format after they have already been converted in the
+	 * opposite direction during routing.  In that case we bypass conversion
+	 * and allow the inserting code (copy.c and nodeModifyTable.c) to provide
+	 * the original tuple directly.
+	 */
+	HeapTuple	original_insert_tuple;
+} TriggerTransitionState;
+
+/*
  * TriggerEvent bit flags
  *
  * Note that we assume different event types (INSERT/DELETE/UPDATE/TRUNCATE)
@@ -127,6 +160,9 @@ extern void RelationBuildTriggers(Relation relation);
 
 extern TriggerDesc *CopyTriggerDesc(TriggerDesc *trigdesc);
 
+extern const char *FindTriggerIncompatibleWithInheritance(TriggerDesc *trigdesc);
+extern TriggerTransitionState *MakeTriggerTransitionState(TriggerDesc *trigdesc);
+
 extern void FreeTriggerDesc(TriggerDesc *trigdesc);
 
 extern void ExecBSInsertTriggers(EState *estate,
@@ -139,7 +175,8 @@ extern TupleTableSlot *ExecBRInsertTriggers(EState *estate,
 extern void ExecARInsertTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple,
-					 List *recheckIndexes);
+					 List *recheckIndexes,
+					 TriggerTransitionState *transitions);
 extern TupleTableSlot *ExecIRInsertTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 TupleTableSlot *slot);
@@ -155,7 +192,8 @@ extern bool ExecBRDeleteTriggers(EState *estate,
 extern void ExecARDeleteTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
-					 HeapTuple fdw_trigtuple);
+					 HeapTuple fdw_trigtuple,
+					 TriggerTransitionState *transitions);
 extern bool ExecIRDeleteTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple);
@@ -174,7 +212,8 @@ extern void ExecARUpdateTriggers(EState *estate,
 					 ItemPointer tupleid,
 					 HeapTuple fdw_trigtuple,
 					 HeapTuple newtuple,
-					 List *recheckIndexes);
+					 List *recheckIndexes,
+					 TriggerTransitionState *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..24359cafb1a 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 TriggerTransitionState *mt_transition_state;
+									/* 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 0d560fb3eed..95da4516066 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -1764,31 +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 function my_trigger_function();
-drop table my_table_42;
-drop table my_table;
---
 -- Verify that triggers with transition tables are not allowed on
 -- views
 --
@@ -1893,3 +1868,229 @@ copy parted_stmt_trig1(a) from stdin;
 NOTICE:  trigger on parted_stmt_trig1 BEFORE INSERT for ROW
 NOTICE:  trigger on parted_stmt_trig1 AFTER INSERT for ROW
 drop table parted_stmt_trig, parted2_stmt_trig;
+--
+-- Test the interaction between transition tables and both kinds of
+-- inheritance.  We'll dump the contents of the transition tables in a
+-- format that shows the attribute order, so that we can distinguish
+-- tuple formats (though not dropped attributes).
+--
+create or replace function dump_transition_tables() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, old table = %, new table = %',
+                 TG_NAME,
+                 (select string_agg(old_table::text, ', ' order by a) from old_table),
+                 (select string_agg(new_table::text, ', ' order by a) from new_table);
+    return null;
+  end;
+$$;
+--
+-- Verify behavior of statement triggers on partition hierarchy with
+-- transition tables.  Tuples should appear to each trigger in the
+-- format of the the relation the trigger is attached to.
+--
+-- set up a partition hierarchy with some different TupleDescriptors
+create table parent (a text, b int) partition by list (a);
+-- a child matching parent
+create table child1 partition of parent for values in ('AAA');
+-- a child with a dropped column
+create table child2 (x int, a text, b int);
+alter table child2 drop column x;
+alter table parent attach partition child2 for values in ('BBB');
+-- a child with a different column order
+create table child3 (b int, a text);
+alter table parent attach partition child3 for values in ('CCC');
+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();
+create trigger child1_stmt_trig
+  after insert or update or delete on child1
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+create trigger child2_stmt_trig
+  after insert or update or delete on child2
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+create trigger child3_stmt_trig
+  after insert or update or delete on child3
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_stmt_trig, old table = <NULL>, new table = (AAA,42)
+insert into child2 values ('BBB', 42);
+NOTICE:  trigger = child2_stmt_trig, old table = <NULL>, new table = (BBB,42)
+insert into child3 values (42, 'CCC');
+NOTICE:  trigger = child3_stmt_trig, old table = <NULL>, new table = (42,CCC)
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+NOTICE:  trigger = parent_stmt_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = (AAA,43), (BBB,43), (CCC,43)
+-- delete via parent sees parent-format tuples
+delete from parent;
+NOTICE:  trigger = parent_stmt_trig, old table = (AAA,43), (BBB,43), (CCC,43), new table = <NULL>
+-- insert into parent sees parent-format tuples
+insert into parent values ('AAA', 42);
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (AAA,42)
+insert into parent values ('BBB', 42);
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (BBB,42)
+insert into parent values ('CCC', 42);
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (CCC,42)
+-- delete from children sees respective child-format tuples
+delete from child1;
+NOTICE:  trigger = child1_stmt_trig, old table = (AAA,42), new table = <NULL>
+delete from child2;
+NOTICE:  trigger = child2_stmt_trig, old table = (BBB,42), new table = <NULL>
+delete from child3;
+NOTICE:  trigger = child3_stmt_trig, old table = (42,CCC), new table = <NULL>
+-- copy into parent sees parent-format tuples
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (AAA,42), (BBB,42), (CCC,42)
+-- DML affecting parent sees tuples collected from children even if
+-- there is no transition table trigger on the children
+drop trigger child1_stmt_trig on child1;
+drop trigger child2_stmt_trig on child2;
+drop trigger child3_stmt_trig on child3;
+delete from parent;
+NOTICE:  trigger = parent_stmt_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = <NULL>
+-- copy into parent sees tuples collected from children even if there
+-- is no transition-table trigger on the children
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (AAA,42), (BBB,42), (CCC,42)
+drop table child1, child2, child3, parent;
+--
+-- Verify prohibition of row triggers with transition triggers on
+-- partitions
+--
+create table parent (a text, b int) partition by list (a);
+create table child partition of parent for values in ('AAA');
+-- adding row trigger with transition table fails
+create trigger child_row_trig
+  after insert or update or delete on child
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+ERROR:  ROW triggers with transition tables are not support on partitions
+-- detaching it first works
+alter table parent detach partition child;
+create trigger child_row_trig
+  after insert or update or delete on child
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+-- but now we're not allowed to reattach it
+alter table parent attach partition child for values in ('AAA');
+ERROR:  trigger "child_row_trig" prevents table "child" from becoming a partition
+DETAIL:  ROW triggers with transition tables are not supported on partitions
+-- drop the trigger, and now we're allowed to attach it again
+drop trigger child_row_trig on child;
+alter table parent attach partition child for values in ('AAA');
+drop table child, parent;
+--
+-- Verify behavior of statement triggers on (non-partition)
+-- inheritance hierarchy with transition tables; similar to the
+-- partition case, except there is no rerouting on insertion and child
+-- tables can have extra columns
+--
+-- set up inheritance hierarchy with different TupleDescriptors
+create table parent (a text, b int);
+-- a child matching parent
+create table child1 () inherits (parent);
+-- a child with a different column order
+create table child2 (b int, a text);
+alter table child2 inherit parent;
+-- a child with an extra column
+create table child3 (c text) inherits (parent);
+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();
+create trigger child1_stmt_trig
+  after insert or update or delete on child1
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+create trigger child2_stmt_trig
+  after insert or update or delete on child2
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+create trigger child3_stmt_trig
+  after insert or update or delete on child3
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_stmt_trig, old table = <NULL>, new table = (AAA,42)
+insert into child2 values (42, 'BBB');
+NOTICE:  trigger = child2_stmt_trig, old table = <NULL>, new table = (42,BBB)
+insert into child3 values ('CCC', 42, 'foo');
+NOTICE:  trigger = child3_stmt_trig, old table = <NULL>, new table = (CCC,42,foo)
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+NOTICE:  trigger = parent_stmt_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = (AAA,43), (BBB,43), (CCC,43)
+-- delete via parent sees parent-format tuples
+delete from parent;
+NOTICE:  trigger = parent_stmt_trig, old table = (AAA,43), (BBB,43), (CCC,43), new table = <NULL>
+-- reinsert values into children for next test...
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_stmt_trig, old table = <NULL>, new table = (AAA,42)
+insert into child2 values (42, 'BBB');
+NOTICE:  trigger = child2_stmt_trig, old table = <NULL>, new table = (42,BBB)
+insert into child3 values ('CCC', 42, 'foo');
+NOTICE:  trigger = child3_stmt_trig, old table = <NULL>, new table = (CCC,42,foo)
+-- delete from children sees respective child-format tuples
+delete from child1;
+NOTICE:  trigger = child1_stmt_trig, old table = (AAA,42), new table = <NULL>
+delete from child2;
+NOTICE:  trigger = child2_stmt_trig, old table = (42,BBB), new table = <NULL>
+delete from child3;
+NOTICE:  trigger = child3_stmt_trig, old table = (CCC,42,foo), new table = <NULL>
+-- copy into parent sees parent-format tuples (no rerouting, so these
+-- are really inserted into the parent)
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = <NULL>
+-- DML affecting parent sees tuples collected from children even if
+-- there is no transition table trigger on the children
+drop trigger child1_stmt_trig on child1;
+drop trigger child2_stmt_trig on child2;
+drop trigger child3_stmt_trig on child3;
+delete from parent;
+NOTICE:  trigger = parent_stmt_trig, old table = (AAA,42), (BBB,42), (CCC,42), new table = <NULL>
+drop table child1, child2, child3, parent;
+--
+-- Verify prohibition of row triggers with transition triggers on
+-- inheritance children
+--
+create table parent (a text, b int);
+create table child () inherits (parent);
+-- adding row trigger with transition table fails
+create trigger child_row_trig
+  after insert or update or delete on child
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+ERROR:  ROW triggers with transition tables are not supported on inheritance children
+-- disinheriting it first works
+alter table child no inherit parent;
+create trigger child_row_trig
+  after insert or update or delete on child
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+-- but now we're not allowed to make it inherit anymore
+alter table child inherit parent;
+ERROR:  trigger "child_row_trig" prevents table "child" from becoming an inheritance child
+DETAIL:  ROW triggers with transition tables are not supported in inheritance hierarchies
+-- drop the trigger, and now we're allowed to make it inherit again
+drop trigger child_row_trig on child;
+alter table child inherit parent;
+drop table child, parent;
+-- cleanup
+drop function dump_transition_tables();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 5581fcb1648..7675df516dc 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1242,30 +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 function my_trigger_function();
-drop table my_table_42;
-drop table my_table;
-
---
 -- Verify that triggers with transition tables are not allowed on
 -- views
 --
@@ -1360,3 +1336,260 @@ copy parted_stmt_trig1(a) from stdin;
 \.
 
 drop table parted_stmt_trig, parted2_stmt_trig;
+
+--
+-- Test the interaction between transition tables and both kinds of
+-- inheritance.  We'll dump the contents of the transition tables in a
+-- format that shows the attribute order, so that we can distinguish
+-- tuple formats (though not dropped attributes).
+--
+
+create or replace function dump_transition_tables() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, old table = %, new table = %',
+                 TG_NAME,
+                 (select string_agg(old_table::text, ', ' order by a) from old_table),
+                 (select string_agg(new_table::text, ', ' order by a) from new_table);
+    return null;
+  end;
+$$;
+
+--
+-- Verify behavior of statement triggers on partition hierarchy with
+-- transition tables.  Tuples should appear to each trigger in the
+-- format of the the relation the trigger is attached to.
+--
+
+-- set up a partition hierarchy with some different TupleDescriptors
+create table parent (a text, b int) partition by list (a);
+
+-- a child matching parent
+create table child1 partition of parent for values in ('AAA');
+
+-- a child with a dropped column
+create table child2 (x int, a text, b int);
+alter table child2 drop column x;
+alter table parent attach partition child2 for values in ('BBB');
+
+-- a child with a different column order
+create table child3 (b int, a text);
+alter table parent attach partition child3 for values in ('CCC');
+
+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();
+
+create trigger child1_stmt_trig
+  after insert or update or delete on child1
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+
+create trigger child2_stmt_trig
+  after insert or update or delete on child2
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+
+create trigger child3_stmt_trig
+  after insert or update or delete on child3
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+insert into child2 values ('BBB', 42);
+insert into child3 values (42, 'CCC');
+
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+
+-- delete via parent sees parent-format tuples
+delete from parent;
+
+-- insert into parent sees parent-format tuples
+insert into parent values ('AAA', 42);
+insert into parent values ('BBB', 42);
+insert into parent values ('CCC', 42);
+
+-- delete from children sees respective child-format tuples
+delete from child1;
+delete from child2;
+delete from child3;
+
+-- copy into parent sees parent-format tuples
+copy parent (a, b) from stdin;
+AAA	42
+BBB	42
+CCC	42
+\.
+
+-- DML affecting parent sees tuples collected from children even if
+-- there is no transition table trigger on the children
+drop trigger child1_stmt_trig on child1;
+drop trigger child2_stmt_trig on child2;
+drop trigger child3_stmt_trig on child3;
+delete from parent;
+
+-- copy into parent sees tuples collected from children even if there
+-- is no transition-table trigger on the children
+copy parent (a, b) from stdin;
+AAA	42
+BBB	42
+CCC	42
+\.
+
+drop table child1, child2, child3, parent;
+
+--
+-- Verify prohibition of row triggers with transition triggers on
+-- partitions
+--
+create table parent (a text, b int) partition by list (a);
+create table child partition of parent for values in ('AAA');
+
+-- adding row trigger with transition table fails
+create trigger child_row_trig
+  after insert or update or delete on child
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+
+-- detaching it first works
+alter table parent detach partition child;
+
+create trigger child_row_trig
+  after insert or update or delete on child
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+
+-- but now we're not allowed to reattach it
+alter table parent attach partition child for values in ('AAA');
+
+-- drop the trigger, and now we're allowed to attach it again
+drop trigger child_row_trig on child;
+alter table parent attach partition child for values in ('AAA');
+
+drop table child, parent;
+
+--
+-- Verify behavior of statement triggers on (non-partition)
+-- inheritance hierarchy with transition tables; similar to the
+-- partition case, except there is no rerouting on insertion and child
+-- tables can have extra columns
+--
+
+-- set up inheritance hierarchy with different TupleDescriptors
+create table parent (a text, b int);
+
+-- a child matching parent
+create table child1 () inherits (parent);
+
+-- a child with a different column order
+create table child2 (b int, a text);
+alter table child2 inherit parent;
+
+-- a child with an extra column
+create table child3 (c text) inherits (parent);
+
+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();
+
+create trigger child1_stmt_trig
+  after insert or update or delete on child1
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+
+create trigger child2_stmt_trig
+  after insert or update or delete on child2
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+
+create trigger child3_stmt_trig
+  after insert or update or delete on child3
+  referencing old table as old_table new table as new_table
+  for each statement
+  execute procedure dump_transition_tables();
+
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+insert into child2 values (42, 'BBB');
+insert into child3 values ('CCC', 42, 'foo');
+
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+
+-- delete via parent sees parent-format tuples
+delete from parent;
+
+-- reinsert values into children for next test...
+insert into child1 values ('AAA', 42);
+insert into child2 values (42, 'BBB');
+insert into child3 values ('CCC', 42, 'foo');
+
+-- delete from children sees respective child-format tuples
+delete from child1;
+delete from child2;
+delete from child3;
+
+-- copy into parent sees parent-format tuples (no rerouting, so these
+-- are really inserted into the parent)
+copy parent (a, b) from stdin;
+AAA	42
+BBB	42
+CCC	42
+\.
+
+-- DML affecting parent sees tuples collected from children even if
+-- there is no transition table trigger on the children
+drop trigger child1_stmt_trig on child1;
+drop trigger child2_stmt_trig on child2;
+drop trigger child3_stmt_trig on child3;
+delete from parent;
+
+drop table child1, child2, child3, parent;
+
+--
+-- Verify prohibition of row triggers with transition triggers on
+-- inheritance children
+--
+create table parent (a text, b int);
+create table child () inherits (parent);
+
+-- adding row trigger with transition table fails
+create trigger child_row_trig
+  after insert or update or delete on child
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+
+-- disinheriting it first works
+alter table child no inherit parent;
+
+create trigger child_row_trig
+  after insert or update or delete on child
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+
+-- but now we're not allowed to make it inherit anymore
+alter table child inherit parent;
+
+-- drop the trigger, and now we're allowed to make it inherit again
+drop trigger child_row_trig on child;
+alter table child inherit parent;
+
+drop table child, parent;
+
+-- cleanup
+drop function dump_transition_tables();
