Re: transition table behavior with inheritance appears broken (was: Declarative partitioning - another take)

Started by Robert Haasover 8 years ago68 messages
#1Robert Haas
robertmhaas@gmail.com

On Mon, May 1, 2017 at 12:51 AM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

What we could document now is that partitioned tables don't allow
specifying triggers that reference transition tables. Although, I am
wondering where this note really belongs - the partitioning chapter, the
triggers chapter or the CREATE TRIGGER reference page? Maybe, Kevin and
Thomas have something to say about that. If it turns out that the
partitioning chapter is a good place, here is a documentation patch.

I think that before we document this behavior, we'd better make sure
we understand exactly what the behavior is, and we'd better make sure
it's correct. Currently, triggers that involve transition tables are
altogether prohibited when the root relation is partitioned, but are
allowed in inheritance cases. However, the actual behavior appears to
be buggy. Here's what happens when I define a parent and a child and
update all the rows:

rhaas=# CREATE FUNCTION t() RETURNS trigger
rhaas-# LANGUAGE plpgsql
rhaas-# AS $$declare q record; begin raise notice 'table %',
tg_table_name; for q in select * from old loop raise notice 'table %
got value %', tg_table_name, q.a; end loop; return null; end;$$;
CREATE FUNCTION
rhaas=# CREATE TABLE p (a int, b text);
CREATE TABLE
rhaas=# CREATE TABLE p1 () INHERITS (p);
CREATE TABLE
rhaas=# CREATE TRIGGER x AFTER UPDATE ON p REFERENCING OLD TABLE AS
old NEW TABLE AS new FOR EACH STATEMENT EXECUTE PROCEDURE t();
CREATE TRIGGER
rhaas=# INSERT INTO p VALUES (0, 'zero');
INSERT 0 1
rhaas=# INSERT INTO p1 VALUES (1, 'one');
INSERT 0 1
rhaas=# INSERT INTO p1 VALUES (2, 'two');
INSERT 0 1
rhaas=# UPDATE p SET b = 'whatever';
NOTICE: table p
NOTICE: table p got value 0
UPDATE 3

Only the rows in the parent show up in the transition table. But now
look what happens if I add an unrelated trigger that also uses
transition tables to the children:

rhaas=# CREATE FUNCTION u() RETURNS trigger LANGUAGE plpgsql AS
$$begin null; end$$;
CREATE FUNCTION
rhaas=# CREATE TRIGGER x1 AFTER UPDATE ON p1 REFERENCING OLD TABLE AS
old NEW TABLE AS new FOR EACH STATEMENT EXECUTE PROCEDURE u();
CREATE TRIGGER
rhaas=# UPDATE p SET b = 'whatever';
NOTICE: table p
NOTICE: table p got value 0
NOTICE: table p got value 1
NOTICE: table p got value 2
UPDATE 3

It seems pretty clear to me that this is busted. The existence of
trigger x1 on p1 shouldn't affect whether trigger x on p sees changes
to p1's rows in its transition tables. Either all changes to any
descendants of p should be captured by the transition tables, or only
changes to the root table should be captured. If we do the former,
the restriction against using transition tables in triggers on
partitioned tables should be removed, I would think. If we do the
latter, then what we should document is not that partitioned tables
have a restriction that doesn't apply to inheritance but rather that
the restriction on the partitioned case flows from the fact that only
the parent's changes are captured, and the parent is always empty in
the partitioning case. In deciding between these two cases, we should
consider the case where the inheritance children have extra columns
and/or different column orderings.

Adding this as an open item. Kevin?

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#2Kevin Grittner
kgrittn@gmail.com
In reply to: Robert Haas (#1)

On Mon, May 1, 2017 at 10:01 AM, Robert Haas <robertmhaas@gmail.com> wrote:

It seems pretty clear to me that this is busted.

I don't think you actually tested anything that is dependent on any
of my patches there.

Adding this as an open item. Kevin?

It will take some time to establish what legacy behavior is and how
the new transition tables are impacted. My first reaction is that a
trigger on the parent should fire for any related action on a child
(unless maybe the trigger is defined with an ONLY keyword???) using
the TupleDesc of the parent. Note that the SQL spec mandates that
even in a AFTER EACH ROW trigger the transition tables must
represent all rows affected by the STATEMENT. I think that this
should be independent of triggers fired at the row level. I think
the rules should be similar for updateable views.

This will take some time to investigate, discuss and produce a
patch. I think best case is Friday.

--
Kevin Grittner
VMware vCenter Server
https://www.vmware.com/

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#3Robert Haas
robertmhaas@gmail.com
In reply to: Kevin Grittner (#2)

On Mon, May 1, 2017 at 12:10 PM, Kevin Grittner <kgrittn@gmail.com> wrote:

On Mon, May 1, 2017 at 10:01 AM, Robert Haas <robertmhaas@gmail.com> wrote:

It seems pretty clear to me that this is busted.

I don't think you actually tested anything that is dependent on any
of my patches there.

I was testing which rows show up in a transition table, so I assumed
that was related to the transition tables patch. Note that this is
not about which triggers are fired, just about how inheritance
interacts with transition tables.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#4Kevin Grittner
kgrittn@gmail.com
In reply to: Robert Haas (#3)

On Mon, May 1, 2017 at 11:53 AM, Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, May 1, 2017 at 12:10 PM, Kevin Grittner <kgrittn@gmail.com> wrote:

On Mon, May 1, 2017 at 10:01 AM, Robert Haas <robertmhaas@gmail.com> wrote:

It seems pretty clear to me that this is busted.

I don't think you actually tested anything that is dependent on any
of my patches there.

I was testing which rows show up in a transition table, so I assumed
that was related to the transition tables patch. Note that this is
not about which triggers are fired, just about how inheritance
interacts with transition tables.

Yeah, I got confused a bit there, comparing to the updateable views case.

--
Kevin Grittner
VMware vCenter Server
https://www.vmware.com/

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#5Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Robert Haas (#1)
1 attachment(s)

On Tue, May 2, 2017 at 3:01 AM, Robert Haas <robertmhaas@gmail.com> wrote:

[...]
Only the rows in the parent show up in the transition table. But now
look what happens if I add an unrelated trigger that also uses
transition tables to the children:

rhaas=# CREATE FUNCTION u() RETURNS trigger LANGUAGE plpgsql AS
$$begin null; end$$;
CREATE FUNCTION
rhaas=# CREATE TRIGGER x1 AFTER UPDATE ON p1 REFERENCING OLD TABLE AS
old NEW TABLE AS new FOR EACH STATEMENT EXECUTE PROCEDURE u();
CREATE TRIGGER
rhaas=# UPDATE p SET b = 'whatever';
NOTICE: table p
NOTICE: table p got value 0
NOTICE: table p got value 1
NOTICE: table p got value 2
UPDATE 3

It seems pretty clear to me that this is busted. The existence of
trigger x1 on p1 shouldn't affect whether trigger x on p sees changes
to p1's rows in its transition tables.

Yikes -- agreed. See analysis and draft patch for discussion below.

Either all changes to any
descendants of p should be captured by the transition tables, or only
changes to the root table should be captured. If we do the former,
the restriction against using transition tables in triggers on
partitioned tables should be removed, I would think. If we do the
latter, then what we should document is not that partitioned tables
have a restriction that doesn't apply to inheritance but rather that
the restriction on the partitioned case flows from the fact that only
the parent's changes are captured, and the parent is always empty in
the partitioning case. In deciding between these two cases, we should
consider the case where the inheritance children have extra columns
and/or different column orderings.

I think that we should only capture transition tuples captured from
the explicitly named relation, since we only fire AFTER STATEMENT
triggers on that relation. I see no inconsistency with the policy of
rejecting transition tables on partitioned tables (as I proposed and
Kevin accepted[1]/messages/by-id/CACjxUsNhdm4ZCgaVreLK5kAwHTZUkqJAVXiywwi-HNVsuTLMnA@mail.gmail.com), because partitioned tables can't have any data so
there would be no point. In contrast, every parent table in an
inheritance hierarchy is also a regular table and can hold data, so I
think we should allow transition tables on them, and capture
transition tuples from that table only when you modify it directly.

The transition table infrastructure only supports exactly one relation
being modified at each query level, and it's a bug that this example
captures tuples from p1 into the tuplestore intended for p's tuples
even though it is not even going to fire the after statement trigger
on p1. It's only a coincidence that the tuples have compatible
TupleDescs.

The pre-existing behaviour for triggers with inheritance is that
STATEMENT triggers fire only for the directly named relation, but ROW
triggers fire for all affected relations. The transition table patch
didn't change that, but it added code to AfterTriggerSaveEvent, which
is called by ExecAR(Insert|Update|Delete)Triggers, to capture
transitions. That gets called for every updated relation (ie
including partitions and inheritance sub-tables) to implement the ROW
policy. It needs to be taught not to capture transition tuples from
relations except the one directly named.

One solution to this problem is for nodeModifyTable.c to tell the
ExecAR* functions explicitly whether to capture transition tuples. It
knows when it has modified the explicitly named relation in a
hierarchy (mt_whichplan == 0) without rerouting via a partitioned
table (mt_partition_dispatch_info == NULL). See attached patch for
discussion (it lacks tests and needs better comments). Does this make
sense? Do you see a better way?

[1]: /messages/by-id/CACjxUsNhdm4ZCgaVreLK5kAwHTZUkqJAVXiywwi-HNVsuTLMnA@mail.gmail.com

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tables-from-one-relation-only.patchapplication/octet-stream; name=transition-tables-from-one-relation-only.patchDownload
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index bcaa58cae0e..e6d1fc4305a 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, true);
 
 					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, true);
 			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, true);
 		}
 	}
 
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index d05e51c8208..0545ce40aff 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,
+					  bool capture_transition);
 static void AfterTriggerEnlargeQueryState(void);
 
 
@@ -2173,7 +2174,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, false);
 }
 
 TupleTableSlot *
@@ -2244,14 +2245,16 @@ ExecBRInsertTriggers(EState *estate, ResultRelInfo *relinfo,
 
 void
 ExecARInsertTriggers(EState *estate, ResultRelInfo *relinfo,
-					 HeapTuple trigtuple, List *recheckIndexes)
+					 HeapTuple trigtuple, List *recheckIndexes,
+					 bool capture_transition)
 {
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
 
 	if (trigdesc &&
 		(trigdesc->trig_insert_after_row || trigdesc->trig_insert_new_table))
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
-							  true, NULL, trigtuple, recheckIndexes, NULL);
+							  true, NULL, trigtuple, recheckIndexes, NULL,
+							  capture_transition);
 }
 
 TupleTableSlot *
@@ -2379,7 +2382,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, false);
 }
 
 bool
@@ -2454,7 +2457,7 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
 void
 ExecARDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
-					 HeapTuple fdw_trigtuple)
+					 HeapTuple fdw_trigtuple, bool capture_transition)
 {
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
 
@@ -2475,7 +2478,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,
+							  capture_transition);
 		if (trigtuple != fdw_trigtuple)
 			heap_freetuple(trigtuple);
 	}
@@ -2591,7 +2595,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),
+							  false);
 }
 
 TupleTableSlot *
@@ -2716,7 +2721,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
 					 HeapTuple fdw_trigtuple,
 					 HeapTuple newtuple,
-					 List *recheckIndexes)
+					 List *recheckIndexes,
+					 bool capture_transition)
 {
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
 
@@ -2738,7 +2744,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
 							  true, trigtuple, newtuple, recheckIndexes,
-							  GetUpdatedColumns(relinfo, estate));
+							  GetUpdatedColumns(relinfo, estate),
+							  capture_transition);
 		if (trigtuple != fdw_trigtuple)
 			heap_freetuple(trigtuple);
 	}
@@ -2869,7 +2876,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, false);
 }
 
 
@@ -5080,7 +5087,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,
+					  bool capture_transition)
 {
 	Relation	rel = relinfo->ri_RelationDesc;
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5110,31 +5118,34 @@ 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))
+		if (capture_transition)
 		{
-			Tuplestorestate *old_tuplestore;
+			if ((event == TRIGGER_EVENT_DELETE &&
+				 trigdesc->trig_delete_old_table) ||
+				(event == TRIGGER_EVENT_UPDATE &&
+				 trigdesc->trig_update_old_table))
+			{
+				Tuplestorestate *old_tuplestore;
 
-			Assert(oldtup != NULL);
-			old_tuplestore =
-				GetTriggerTransitionTuplestore
+				Assert(oldtup != NULL);
+				old_tuplestore =
+					GetTriggerTransitionTuplestore
 					(afterTriggers.old_tuplestores);
-			tuplestore_puttuple(old_tuplestore, oldtup);
-		}
-		if ((event == TRIGGER_EVENT_INSERT &&
-			 trigdesc->trig_insert_new_table) ||
-			(event == TRIGGER_EVENT_UPDATE &&
-			 trigdesc->trig_update_new_table))
-		{
-			Tuplestorestate *new_tuplestore;
+				tuplestore_puttuple(old_tuplestore, oldtup);
+			}
+			if ((event == TRIGGER_EVENT_INSERT &&
+				 trigdesc->trig_insert_new_table) ||
+				(event == TRIGGER_EVENT_UPDATE &&
+				 trigdesc->trig_update_new_table))
+			{
+				Tuplestorestate *new_tuplestore;
 
-			Assert(newtup != NULL);
-			new_tuplestore =
-				GetTriggerTransitionTuplestore
+				Assert(newtup != NULL);
+				new_tuplestore =
+					GetTriggerTransitionTuplestore
 					(afterTriggers.new_tuplestores);
-			tuplestore_puttuple(new_tuplestore, newtup);
+				tuplestore_puttuple(new_tuplestore, newtup);
+			}
 		}
 
 		/* If transition tables are the only reason we're here, return. */
diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c
index 327a0bad388..78c1210764b 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, true);
 
 		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, true);
 
 		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, true);
 
 		list_free(recheckIndexes);
 	}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 652cd975996..08483de5afe 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_partition_dispatch_info == NULL);
 
 	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,9 @@ ldelete:;
 		(estate->es_processed)++;
 
 	/* AFTER ROW DELETE Triggers */
-	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple);
+	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+						 mtstate->mt_partition_dispatch_info == NULL &&
+						 mtstate->mt_whichplan == 0);
 
 	/* Process RETURNING if present */
 	if (resultRelInfo->ri_projectReturning)
@@ -877,7 +881,8 @@ ldelete:;
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecUpdate(ItemPointer tupleid,
+ExecUpdate(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
 		   TupleTableSlot *planSlot,
@@ -1105,7 +1110,9 @@ lreplace:;
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
-						 recheckIndexes);
+						 recheckIndexes,
+						 mtstate->mt_partition_dispatch_info == NULL &&
+						 mtstate->mt_whichplan == 0);
 
 	list_free(recheckIndexes);
 
@@ -1312,7 +1319,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);
@@ -1602,11 +1609,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:
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index d73969c8747..8d3cb6b4c82 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -139,7 +139,8 @@ extern TupleTableSlot *ExecBRInsertTriggers(EState *estate,
 extern void ExecARInsertTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple,
-					 List *recheckIndexes);
+					 List *recheckIndexes,
+					 bool capture_transition);
 extern TupleTableSlot *ExecIRInsertTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 TupleTableSlot *slot);
@@ -155,7 +156,8 @@ extern bool ExecBRDeleteTriggers(EState *estate,
 extern void ExecARDeleteTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
-					 HeapTuple fdw_trigtuple);
+					 HeapTuple fdw_trigtuple,
+					 bool capture_transition);
 extern bool ExecIRDeleteTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple);
@@ -174,7 +176,8 @@ extern void ExecARUpdateTriggers(EState *estate,
 					 ItemPointer tupleid,
 					 HeapTuple fdw_trigtuple,
 					 HeapTuple newtuple,
-					 List *recheckIndexes);
+					 List *recheckIndexes,
+					 bool capture_transition);
 extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple,
#6Robert Haas
robertmhaas@gmail.com
In reply to: Thomas Munro (#5)

On Tue, May 2, 2017 at 9:44 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

I think that we should only capture transition tuples captured from
the explicitly named relation, since we only fire AFTER STATEMENT
triggers on that relation. I see no inconsistency with the policy of
rejecting transition tables on partitioned tables (as I proposed and
Kevin accepted[1]), because partitioned tables can't have any data so
there would be no point. In contrast, every parent table in an
inheritance hierarchy is also a regular table and can hold data, so I
think we should allow transition tables on them, and capture
transition tuples from that table only when you modify it directly.

I suspect that most users would find it more useful to capture all of
the rows that the statement actually touched, regardless of whether
they hit the named table or an inheritance child. I just don't know
if it's practical to make that work. (And, of course, I don't know if
other people agree with my assessment of what is useful ... but
generally there seems to be support for making partitioned tables, at
least, look more like a single table that happens to have partitions
and less like a bunch of separate tables attached to each other with
duct tape.)

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Robert Haas (#6)

Robert Haas wrote:

I suspect that most users would find it more useful to capture all of
the rows that the statement actually touched, regardless of whether
they hit the named table or an inheritance child.

Yes, agreed. For the plain inheritance cases each row would need to
have an indicator of which relation it comes from (tableoid); I'm not
sure if such a thing would be useful in the partitioning case.

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#8Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#7)

On Wed, May 3, 2017 at 12:02 PM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

Robert Haas wrote:

I suspect that most users would find it more useful to capture all of
the rows that the statement actually touched, regardless of whether
they hit the named table or an inheritance child.

Yes, agreed. For the plain inheritance cases each row would need to
have an indicator of which relation it comes from (tableoid); I'm not
sure if such a thing would be useful in the partitioning case.

I think it would be about equally useful.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#9David Fetter
david@fetter.org
In reply to: Robert Haas (#6)

On Wed, May 03, 2017 at 11:47:04AM -0400, Robert Haas wrote:

On Tue, May 2, 2017 at 9:44 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

I think that we should only capture transition tuples captured from
the explicitly named relation, since we only fire AFTER STATEMENT
triggers on that relation. I see no inconsistency with the policy of
rejecting transition tables on partitioned tables (as I proposed and
Kevin accepted[1]), because partitioned tables can't have any data so
there would be no point. In contrast, every parent table in an
inheritance hierarchy is also a regular table and can hold data, so I
think we should allow transition tables on them, and capture
transition tuples from that table only when you modify it directly.

I suspect that most users would find it more useful to capture all of
the rows that the statement actually touched, regardless of whether
they hit the named table or an inheritance child. I just don't know
if it's practical to make that work. (And, of course, I don't know if
other people agree with my assessment of what is useful ... but
generally there seems to be support for making partitioned tables, at
least, look more like a single table that happens to have partitions
and less like a bunch of separate tables attached to each other with
duct tape.)

+1 on the not-duct-tape view of partitioned tables.

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david(dot)fetter(at)gmail(dot)com

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#10Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Alvaro Herrera (#7)

On Thu, May 4, 2017 at 4:02 AM, Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

Robert Haas wrote:

I suspect that most users would find it more useful to capture all of
the rows that the statement actually touched, regardless of whether
they hit the named table or an inheritance child.

Yes, agreed. For the plain inheritance cases each row would need to
have an indicator of which relation it comes from (tableoid); I'm not
sure if such a thing would be useful in the partitioning case.

On Thu, May 4, 2017 at 4:26 AM, David Fetter <david@fetter.org> wrote:

+1 on the not-duct-tape view of partitioned tables.

Hmm. Ok. Are we talking about PG10 or PG11 here? Does this approach
makes sense?

1. Remove the prohibition on creating transition-capturing triggers
on a partitioned table.

2. Make sure that the ExecAR* functions call AfterTriggerSaveEvent
when modifying partition tables if the explicitly named parent
partitioned table has after triggers with transition tables. Not sure
how exactly how but doesn't seem difficult.

3. Convert tuples to the TupleDesc of the relation that owns the
statement trigger (ie the partitioned table) when inserting them into
the tuplestore. One way to do that might be to build an array of
TupleConversionMap objects that does the opposite of the conversions
done by tup_conv_maps. While tup_conv_maps is for converting tuples
to the layout needed for a partition, tup_unconv_maps (or better name)
would be for converting the old and new tuples to the TupleDesc of the
partitioned table. Then the appropriate TupleConversionMap could be
passed into the ExecAR* functions as a new argument 'transition_map'.
AfterTriggerSaveEvent would use 'oldtup' and 'newtup' directly for ROW
triggers, but convert using the passed in map if it needs to insert
them into the transition tuplestores.

The same thing could work for inheritance, if tupconvert.c had a new
kind of conversion that allows slicing of tuples (converting a wider
child table's tuples to the parent's subset of columns) rather the
just conversion between logically equivalent TupleDescs.

To avoid the whiff of duct tape, we'd probably also want to make ROW
triggers created on the partitioned table(s) containing partition to
fire too, with appropriate TypeConversionMap treatment. Not sure what
exactly is involved there.

On the other hand, doing that with inheritance hierarchies would be an
incompatible behavioural change, which I guess people don't want -- am
I right?

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#11Robert Haas
robertmhaas@gmail.com
In reply to: Thomas Munro (#10)

On Thu, May 4, 2017 at 4:46 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Thu, May 4, 2017 at 4:02 AM, Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

Robert Haas wrote:

I suspect that most users would find it more useful to capture all of
the rows that the statement actually touched, regardless of whether
they hit the named table or an inheritance child.

Yes, agreed. For the plain inheritance cases each row would need to
have an indicator of which relation it comes from (tableoid); I'm not
sure if such a thing would be useful in the partitioning case.

On Thu, May 4, 2017 at 4:26 AM, David Fetter <david@fetter.org> wrote:

+1 on the not-duct-tape view of partitioned tables.

Hmm. Ok. Are we talking about PG10 or PG11 here? Does this approach
makes sense?

I was thinking PG10 if it can be done straightforwardly.

1. Remove the prohibition on creating transition-capturing triggers
on a partitioned table.

2. Make sure that the ExecAR* functions call AfterTriggerSaveEvent
when modifying partition tables if the explicitly named parent
partitioned table has after triggers with transition tables. Not sure
how exactly how but doesn't seem difficult.

3. Convert tuples to the TupleDesc of the relation that owns the
statement trigger (ie the partitioned table) when inserting them into
the tuplestore. One way to do that might be to build an array of
TupleConversionMap objects that does the opposite of the conversions
done by tup_conv_maps. While tup_conv_maps is for converting tuples
to the layout needed for a partition, tup_unconv_maps (or better name)
would be for converting the old and new tuples to the TupleDesc of the
partitioned table. Then the appropriate TupleConversionMap could be
passed into the ExecAR* functions as a new argument 'transition_map'.
AfterTriggerSaveEvent would use 'oldtup' and 'newtup' directly for ROW
triggers, but convert using the passed in map if it needs to insert
them into the transition tuplestores.

The same thing could work for inheritance, if tupconvert.c had a new
kind of conversion that allows slicing of tuples (converting a wider
child table's tuples to the parent's subset of columns) rather the
just conversion between logically equivalent TupleDescs.

To avoid the whiff of duct tape, we'd probably also want to make ROW
triggers created on the partitioned table(s) containing partition to
fire too, with appropriate TypeConversionMap treatment. Not sure what
exactly is involved there.

On the other hand, doing that with inheritance hierarchies would be an
incompatible behavioural change, which I guess people don't want -- am
I right?

Incompatible with what? Transition tables haven't been released yet.
If we're going to fix the definition of what they do, now's the time.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#12Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Robert Haas (#11)

On Fri, May 5, 2017 at 2:40 AM, Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, May 4, 2017 at 4:46 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Thu, May 4, 2017 at 4:02 AM, Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

Robert Haas wrote:

I suspect that most users would find it more useful to capture all of
the rows that the statement actually touched, regardless of whether
they hit the named table or an inheritance child.

Yes, agreed. For the plain inheritance cases each row would need to
have an indicator of which relation it comes from (tableoid); I'm not
sure if such a thing would be useful in the partitioning case.

On Thu, May 4, 2017 at 4:26 AM, David Fetter <david@fetter.org> wrote:

+1 on the not-duct-tape view of partitioned tables.

Hmm. Ok. Are we talking about PG10 or PG11 here? Does this approach
makes sense?

I was thinking PG10 if it can be done straightforwardly.

Ok, I will draft a patch to do it the way I described and see what people think.

To avoid the whiff of duct tape, we'd probably also want to make ROW
triggers created on the partitioned table(s) containing partition to
fire too, with appropriate TypeConversionMap treatment. Not sure what
exactly is involved there.

On the other hand, doing that with inheritance hierarchies would be an
incompatible behavioural change, which I guess people don't want -- am
I right?

Incompatible with what? Transition tables haven't been released yet.
If we're going to fix the definition of what they do, now's the time.

The last two paragraphs of my email were about ROW triggers created on
partitioned tables and tables with inheritance children, not the new
transition table stuff. I will forget that for now and look only at
making the transition tables duct-tape-free.

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#13Noah Misch
noah@leadboat.com
In reply to: Kevin Grittner (#2)

On Mon, May 01, 2017 at 11:10:52AM -0500, Kevin Grittner wrote:

On Mon, May 1, 2017 at 10:01 AM, Robert Haas <robertmhaas@gmail.com> wrote:

It seems pretty clear to me that this is busted.

I don't think you actually tested anything that is dependent on any
of my patches there.

Adding this as an open item. Kevin?

It will take some time to establish what legacy behavior is and how
the new transition tables are impacted. My first reaction is that a
trigger on the parent should fire for any related action on a child
(unless maybe the trigger is defined with an ONLY keyword???) using
the TupleDesc of the parent. Note that the SQL spec mandates that
even in a AFTER EACH ROW trigger the transition tables must
represent all rows affected by the STATEMENT. I think that this
should be independent of triggers fired at the row level. I think
the rules should be similar for updateable views.

This will take some time to investigate, discuss and produce a
patch. I think best case is Friday.

[Action required within three days. This is a generic notification.]

The above-described topic is currently a PostgreSQL 10 open item. Kevin,
since you committed the patch believed to have created it, you own this open
item. If some other commit is more relevant or if this does not belong as a
v10 open item, please let us know. Otherwise, please observe the policy on
open item ownership[1]/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com and send a status update within three calendar days of
this message. Include a date for your subsequent status update. Testers may
discover new open items at any time, and I want to plan to get them all fixed
well in advance of shipping v10. Consequently, I will appreciate your efforts
toward speedy resolution. Thanks.

[1]: /messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#14Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Thomas Munro (#12)

On Fri, May 5, 2017 at 8:29 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Fri, May 5, 2017 at 2:40 AM, Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, May 4, 2017 at 4:46 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Thu, May 4, 2017 at 4:02 AM, Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

Robert Haas wrote:

I suspect that most users would find it more useful to capture all of
the rows that the statement actually touched, regardless of whether
they hit the named table or an inheritance child.

Yes, agreed. For the plain inheritance cases each row would need to
have an indicator of which relation it comes from (tableoid); I'm not
sure if such a thing would be useful in the partitioning case.

On Thu, May 4, 2017 at 4:26 AM, David Fetter <david@fetter.org> wrote:

+1 on the not-duct-tape view of partitioned tables.

Hmm. Ok. Are we talking about PG10 or PG11 here? Does this approach
makes sense?

I was thinking PG10 if it can be done straightforwardly.

Ok, I will draft a patch to do it the way I described and see what people think.

FYI I am still working on this and will post a draft patch to do this
(that is: make transition tables capture changes from children with
appropriate tuple conversion) in the next 24 hours.

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#15Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Thomas Munro (#14)
1 attachment(s)

On Mon, May 8, 2017 at 7:09 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Fri, May 5, 2017 at 8:29 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Fri, May 5, 2017 at 2:40 AM, Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, May 4, 2017 at 4:46 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Thu, May 4, 2017 at 4:02 AM, Alvaro Herrera <alvherre@2ndquadrant.com> wrote:

Robert Haas wrote:

I suspect that most users would find it more useful to capture all of
the rows that the statement actually touched, regardless of whether
they hit the named table or an inheritance child.

Yes, agreed. For the plain inheritance cases each row would need to
have an indicator of which relation it comes from (tableoid); I'm not
sure if such a thing would be useful in the partitioning case.

On Thu, May 4, 2017 at 4:26 AM, David Fetter <david@fetter.org> wrote:

+1 on the not-duct-tape view of partitioned tables.

Hmm. Ok. Are we talking about PG10 or PG11 here? Does this approach
makes sense?

I was thinking PG10 if it can be done straightforwardly.

Ok, I will draft a patch to do it the way I described and see what people think.

FYI I am still working on this and will post a draft patch to do this
(that is: make transition tables capture changes from children with
appropriate tuple conversion) in the next 24 hours.

Ok, here is a first swing at it, for discussion.

In master, the decision to populate transition tables happens in
AfterTriggerSaveEvent (called by the ExecAR* functions) in trigger.c.
It does that if it sees that there are triggers on the
relation-being-modified that have transition tables.

With this patch, nodeModifyTuple.c makes a 'TriggerTransitionFilter'
object to override that behaviour, if there are child tables of either
kind. That is passed to the ExecAR* functions and thence
AfterTriggerSaveEvent, overriding its usual behaviour, so that it can
know that it needs collect tuples from child nodes and how to convert
them to the layout needed for the tuplestores if necessary.

Thoughts? I'm not yet sure about the locking situation. Generally
needs some more testing.

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables.patchapplication/octet-stream; name=transition-tuples-from-child-tables.patchDownload
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..d5b1c21c704 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) ||
+		(!transitions && 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) ||
+		(!transitions && 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) ||
+		(!transitions && 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,82 @@ 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;
+		bool delete_old_table;
+		bool update_old_table;
+		bool update_new_table;
+		bool 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;
+		}
+		else
+		{
+			/*
+			 * In simple cases we use the information in the TriggerDesc for
+			 * the updated relation directly.
+			 */
+			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;
+			map = NULL;
+		}
+
+		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..7c4412a9cd6 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -1775,8 +1775,6 @@ 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
 --
@@ -1868,3 +1866,52 @@ 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;
+-- 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
+  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;
+-- 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
+  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..cb9fd92dcdb 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1333,3 +1333,64 @@ with upd as (
 
 delete from parted_stmt_trig;
 drop table parted_stmt_trig, parted2_stmt_trig;
+
+-- 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
+  execute procedure dump_transition_tables();
+
+update parent set b = b + 1;
+
+drop table child1, child2, child3, parent;
+
+-- 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
+  execute procedure dump_transition_tables();
+
+update parent set b = b + 1;
+
+drop table child1, child2, child3, parent;
+drop function dump_transition_tables();
#16Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Thomas Munro (#15)
1 attachment(s)

On Tue, May 9, 2017 at 10:29 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

In master, the decision to populate transition tables happens in
AfterTriggerSaveEvent (called by the ExecAR* functions) in trigger.c.
It does that if it sees that there are triggers on the
relation-being-modified that have transition tables.

With this patch, nodeModifyTuple.c makes a 'TriggerTransitionFilter'
object to override that behaviour, if there are child tables of either
kind. That is passed to the ExecAR* functions and thence
AfterTriggerSaveEvent, overriding its usual behaviour, so that it can
know that it needs collect tuples from child nodes and how to convert
them to the layout needed for the tuplestores if necessary.

Thoughts? I'm not yet sure about the locking situation. Generally
needs some more testing.

Here is a new version with tidied up tests and a couple of small bug
fixes. However, I've realised that there is a surprising behaviour
with this approach, and I'm not sure what to do about it.

Recall that transition tables can be specified for statement-level
triggers AND row-level triggers. If you specify them for row-level
triggers, then they can see all rows changed so far each time they
fire. Now our policy of firing the statement level triggers only for
the named relation but firing row-level triggers for all modified
relations leads to a tricky problem for the inheritance case: what
type of transition tuples should the child table's row-level triggers
see?

Suppose you have an inheritance hierarchy like this:

animal
-> mammal
-> cat

You define a statement-level trigger on "animal" and another
statement-level trigger on "mammal". You define a row-level trigger
on "cat". When you update either "animal" or "mammal", the row
triggers on "cat" might run. Row-level triggers on "cat" see OLD and
NEW as "cat" tuples, of course, but if they are configured to see
transition tables, should they see "cat", "mammal" or "animal" tuples
in the transition tables? With my patch as it is, that depends on
which level of the hierarchy you explicitly updated!

No such problem exists for partition hierarchies since the tables all
appear as the same type to user code (though conversions may be
happening for technical reasons).

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables-v2.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v2.patchDownload
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();
#17Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Thomas Munro (#16)

Thomas Munro wrote:

Recall that transition tables can be specified for statement-level
triggers AND row-level triggers. If you specify them for row-level
triggers, then they can see all rows changed so far each time they
fire.

Uhmm ... why do we do this? It seems like a great way to cause much
confusion. Shouldn't we see the transition table containing the whole
set for statement-level triggers only, and give row-level triggers just
the individual affected row each time?

--
�lvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#18Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Alvaro Herrera (#17)

On Wed, May 10, 2017 at 9:57 AM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

Thomas Munro wrote:

Recall that transition tables can be specified for statement-level
triggers AND row-level triggers. If you specify them for row-level
triggers, then they can see all rows changed so far each time they
fire.

Uhmm ... why do we do this? It seems like a great way to cause much
confusion. Shouldn't we see the transition table containing the whole
set for statement-level triggers only, and give row-level triggers just
the individual affected row each time?

I assumed that had come from the standard. I don't have a published
standard, but I have just taken a quick look at one of the publicly
available drafts dated 2006. I think its model is that the transition
tables are always conceptually there, and NEW and OLD are just range
variables over those tables. That may explain why transition tables
are mentioned in the context of row-level triggers, and it may be that
the spec's authors never intended row-level triggers to be able to see
the (partial) transition table other than through the range variables
that access exactly one row, but I don't see any wording that
explicitly says so in the spec. Do you? Thoughts, Kevin?

After thinking about this some more, it's not only the conversion to
some arbitrary parent tuple type that would be surprising to a user of
inheritance + triggers + transition tables, it's the fact that a
row-level trigger on a given child table will also see tuples
collected from other tables in the hierarchy. I think I didn't quite
get that right in -v2: it should probably build
TriggerTransitionFilter from union of all child tables' transition
table flags, not just the named table, so that if any child table has
a row trigger we start collecting transition tuples from all others
for it to see... That sounds pretty crazy to me.

So, assuming we want to proceed with this plan of collecting
transition tuples from children, I see approximately 3 choices:

1. Reject transition tables on row-level triggers.
2. Reject transition tables on row-level triggers on tables that
inherit from other tables.
3. Continue to allow transition tables on row-level triggers, but
document that they must be prepared to see transition tuples as they
would when querying arbitrary parent tables, and see tuples from other
tables in the hierarchy (!)

Another possibility which I haven't seriously considered is per-table
transition tables so you'd collect each child's tuples separately.

I take Alvaro's comment as a vote for 1. I vote for 1 too.

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#19Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Thomas Munro (#16)

On 2017/05/10 6:51, Thomas Munro wrote:

No such problem exists for partition hierarchies since the tables all
appear as the same type to user code (though conversions may be
happening for technical reasons).

To clarify a bit, there may exist differences in the ordering of columns,
either between the parent and its partitions or between different
partitions. For example, while parent's rowtype is (a int, b char, c
float), a partition's may be (b char, a int, c float), and yet another
partition may have (c float, a int, b char). If some user code happens to
depend on the ordering of columns, selecting from the parent and selecting
from a partition directly may return the same result but in different formats.

Thanks,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#20Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Amit Langote (#19)

On Wed, May 10, 2017 at 2:31 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/05/10 6:51, Thomas Munro wrote:

No such problem exists for partition hierarchies since the tables all
appear as the same type to user code (though conversions may be
happening for technical reasons).

To clarify a bit, there may exist differences in the ordering of columns,
either between the parent and its partitions or between different
partitions. For example, while parent's rowtype is (a int, b char, c
float), a partition's may be (b char, a int, c float), and yet another
partition may have (c float, a int, b char). If some user code happens to
depend on the ordering of columns, selecting from the parent and selecting
from a partition directly may return the same result but in different formats.

Right. And the patch I posted converts all transition tuples it
collects from child tables to match the TupleDescriptor of the
relation you named, which it gets from
estate->es_root_result_relations[0]. Is that right? I suppose it
will be very common for partitions to have matching TupleDescriptors,
so the TupleConversionMap will usually be NULL meaning no conversion
is ever done. But in the inheritance case they might be different on
purpose, and in both inheritance and partitioning cases they might be
different in physical ways that aren't logically important as you said
(column order, dropped columns).

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#21Robert Haas
robertmhaas@gmail.com
In reply to: Thomas Munro (#20)

On Tue, May 9, 2017 at 11:40 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Wed, May 10, 2017 at 2:31 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/05/10 6:51, Thomas Munro wrote:

No such problem exists for partition hierarchies since the tables all
appear as the same type to user code (though conversions may be
happening for technical reasons).

To clarify a bit, there may exist differences in the ordering of columns,
either between the parent and its partitions or between different
partitions. For example, while parent's rowtype is (a int, b char, c
float), a partition's may be (b char, a int, c float), and yet another
partition may have (c float, a int, b char). If some user code happens to
depend on the ordering of columns, selecting from the parent and selecting
from a partition directly may return the same result but in different formats.

Right. And the patch I posted converts all transition tuples it
collects from child tables to match the TupleDescriptor of the
relation you named, which it gets from
estate->es_root_result_relations[0]. Is that right? I suppose it
will be very common for partitions to have matching TupleDescriptors,
so the TupleConversionMap will usually be NULL meaning no conversion
is ever done. But in the inheritance case they might be different on
purpose, and in both inheritance and partitioning cases they might be
different in physical ways that aren't logically important as you said
(column order, dropped columns).

Hmm. What if the partitioning hierarchy contains foreign tables?

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#22Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Thomas Munro (#18)

On Wed, May 10, 2017 at 11:22 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Wed, May 10, 2017 at 9:57 AM, Alvaro Herrera
<alvherre@2ndquadrant.com> wrote:

Thomas Munro wrote:

Recall that transition tables can be specified for statement-level
triggers AND row-level triggers. If you specify them for row-level
triggers, then they can see all rows changed so far each time they
fire.

Uhmm ... why do we do this? It seems like a great way to cause much
confusion. Shouldn't we see the transition table containing the whole
set for statement-level triggers only, and give row-level triggers just
the individual affected row each time?

I assumed that had come from the standard. I don't have a published
standard, but I have just taken a quick look at one of the publicly
available drafts dated 2006. I think its model is that the transition
tables are always conceptually there, and NEW and OLD are just range
variables over those tables. That may explain why transition tables
are mentioned in the context of row-level triggers, and it may be that
the spec's authors never intended row-level triggers to be able to see
the (partial) transition table other than through the range variables
that access exactly one row, but I don't see any wording that
explicitly says so in the spec. Do you? Thoughts, Kevin?

Hmm. DB2 has transition tables (invented them maybe?) and it allows
OLD/NEW TABLE on row-level triggers:

https://www.ibm.com/support/knowledgecenter/en/SSEPGG_10.1.0/com.ibm.db2.luw.admin.dbobj.doc/doc/t0020236.html

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#23Robert Haas
robertmhaas@gmail.com
In reply to: Thomas Munro (#22)

On Tue, May 9, 2017 at 11:48 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

Hmm. DB2 has transition tables (invented them maybe?) and it allows
OLD/NEW TABLE on row-level triggers:

https://www.ibm.com/support/knowledgecenter/en/SSEPGG_10.1.0/com.ibm.db2.luw.admin.dbobj.doc/doc/t0020236.html

Yeah, my impression is that Kevin was pretty keen on supporting that
case. I couldn't say exactly why, though.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#24Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Robert Haas (#23)
1 attachment(s)

On Wed, May 10, 2017 at 3:55 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, May 9, 2017 at 11:48 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

Hmm. DB2 has transition tables (invented them maybe?) and it allows
OLD/NEW TABLE on row-level triggers:

https://www.ibm.com/support/knowledgecenter/en/SSEPGG_10.1.0/com.ibm.db2.luw.admin.dbobj.doc/doc/t0020236.html

Yeah, my impression is that Kevin was pretty keen on supporting that
case. I couldn't say exactly why, though.

Ok, here's a new version that handles row-level triggers with
transition tables on any child table. The regression tests show
partition and inheritance examples of that. To be clear about what
this does:

1. If you attach a row-level trigger with transition tables to any
partition, it will see transition tuples from all partitions that were
modified by the same statement.

2. If you attach a row-level trigger with transition tables to any
inheritance child, it will see transition tuples from all tables in
the inheritance hierarchy at or below the directly named table that
were modified by the same statement, sliced so that they appear as
tuples from the directly named table.

On Wed, May 10, 2017 at 3:41 PM, Robert Haas <robertmhaas@gmail.com> wrote:

Hmm. What if the partitioning hierarchy contains foreign tables?

Arghalalkjhsdflg. Looking into that...

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables-v3.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v3.patchDownload
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 819395a9678..853dbf82e25 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 (rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
 				ereport(ERROR,
 						(errcode(ERRCODE_WRONG_OBJECT_TYPE),
@@ -2029,6 +2023,87 @@ equalTriggerDescs(TriggerDesc *trigdesc1, TriggerDesc *trigdesc2)
 #endif   /* NOT_USED */
 
 /*
+ * Make a TriggerTransitionFilter object from a given TriggerDesc.  The filter
+ * 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.  Even if NULL is returned, the caller should call
+ * MergeTriggerTransitionFilter for the TriggerDesc of each child table to be
+ * modified.
+ *
+ * The resulting object can be passed to the ExecAR* functions.  The caller
+ * should also set ttf_map as appropriate when dealing with child tables, and
+ * should set ttf_transition_relid.
+ */
+TriggerTransitionFilter *
+MakeTriggerTransitionFilter(TriggerDesc *trigdesc)
+{
+	TriggerTransitionFilter *filter = NULL;
+
+	if (trigdesc != NULL &&
+		(trigdesc->trig_delete_old_table || trigdesc->trig_update_old_table ||
+		 trigdesc->trig_update_new_table || trigdesc->trig_insert_new_table))
+	{
+		filter = (TriggerTransitionFilter *)
+			palloc0(sizeof(TriggerTransitionFilter));
+		filter->ttf_delete_old_table = trigdesc->trig_delete_old_table;
+		filter->ttf_update_old_table = trigdesc->trig_update_old_table;
+		filter->ttf_update_new_table = trigdesc->trig_update_new_table;
+		filter->ttf_insert_new_table = trigdesc->trig_insert_new_table;
+	}
+
+	return filter;
+}
+
+/*
+ * Update a TriggerTransitionFilter to consider a given TriggerDesc.  This
+ * should be called for each child table that is modified indirectly because
+ * it is part of a partition or inheritance hierarchy, to decide if there is
+ * any reason to collect transition tuples.
+ *
+ * Return the same TriggerTransitionFilter that was passed in, or allocate and
+ * return a new one if NULL was passed in.
+ */
+TriggerTransitionFilter *
+MergeTriggerTransitionFilter(TriggerTransitionFilter *filter,
+							 TriggerDesc *trigdesc)
+{
+	/*
+	 * We only want to start collecting transition tuples if there is a
+	 * row-level trigger and the corresponding kind of transition table on
+	 * this child table.
+	 *
+	 * Note that this strategy generates a false positive if there is a
+	 * row-level trigger AND a statement-level trigger but only the
+	 * statement-level trigger has transition tables.  TriggerDesc lacks the
+	 * meta-data required to distinguish that case for now.
+	 */
+	if (trigdesc != NULL &&
+		((trigdesc->trig_delete_old_table && trigdesc->trig_delete_after_row) ||
+		 (trigdesc->trig_update_old_table && trigdesc->trig_update_after_row) ||
+		 (trigdesc->trig_update_new_table && trigdesc->trig_update_after_row) ||
+		 (trigdesc->trig_insert_new_table && trigdesc->trig_insert_after_row)))
+	{
+		/* Create on demand. */
+		if (filter == NULL)
+		{
+			filter = (TriggerTransitionFilter *)
+				palloc0(sizeof(TriggerTransitionFilter));
+		}
+
+		/* Merge with existing flags. */
+		filter->ttf_delete_old_table |= trigdesc->trig_delete_old_table;
+		filter->ttf_update_old_table |= trigdesc->trig_update_old_table;
+		filter->ttf_update_new_table |= trigdesc->trig_update_new_table;
+		filter->ttf_insert_new_table |= trigdesc->trig_insert_new_table;
+	}
+
+	return filter;
+}
+
+/*
  * Call a trigger function.
  *
  *		trigdata: trigger descriptor.
@@ -2192,7 +2267,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 +2338,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 *
@@ -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,
+					 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;
 
@@ -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,
+					 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;
 
@@ -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);
 }
 
 
@@ -3273,6 +3360,8 @@ typedef struct AfterTriggerSharedData
 	TriggerEvent ats_event;		/* event type indicator, see trigger.h */
 	Oid			ats_tgoid;		/* the trigger's ID */
 	Oid			ats_relid;		/* the relation it's on */
+	Oid			ats_transition_relid;
+								/* determines TupleDesc of transition tables */
 	CommandId	ats_firing_id;	/* ID for firing cycle */
 } AfterTriggerSharedData;
 
@@ -3624,6 +3713,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
 	{
 		if (newshared->ats_tgoid == evtshared->ats_tgoid &&
 			newshared->ats_relid == evtshared->ats_relid &&
+			newshared->ats_transition_relid == evtshared->ats_transition_relid &&
 			newshared->ats_event == evtshared->ats_event &&
 			newshared->ats_firing_id == 0)
 			break;
@@ -3861,6 +3951,7 @@ AfterTriggerExecute(AfterTriggerEvent event,
 			GetTriggerTransitionTuplestore(afterTriggers.new_tuplestores);
 	else
 		LocTriggerData.tg_newtable = NULL;
+	LocTriggerData.tg_transition_relid = evtshared->ats_transition_relid;
 
 	/*
 	 * Setup the remaining trigger information
@@ -5099,7 +5190,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;
@@ -5129,35 +5221,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;
@@ -5326,6 +5464,9 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			(trigger->tginitdeferred ? AFTER_TRIGGER_INITDEFERRED : 0);
 		new_shared.ats_tgoid = trigger->tgoid;
 		new_shared.ats_relid = RelationGetRelid(rel);
+		new_shared.ats_transition_relid =
+			(transitions ?
+			 transitions->ttf_transition_relid : new_shared.ats_relid);
 		new_shared.ats_firing_id = 0;
 
 		afterTriggerAddEvent(&afterTriggers.query_stack[afterTriggers.query_depth],
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 2535d2ee695..b6e5b3f14ff 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..62e2133a359 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,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_filter != NULL)
+				{
+					/* Prepare to convert transition tuples from this child. */
+					Assert(node->mt_transition_tupconv_maps != NULL);
+					node->mt_transition_filter->ttf_map =
+						node->mt_transition_tupconv_maps[node->mt_whichplan];
+				}
 				continue;
 			}
 			else
@@ -1602,11 +1614,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,11 +1662,12 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	int			nplans = list_length(node->plans);
 	ResultRelInfo *saved_resultRelInfo;
 	ResultRelInfo *resultRelInfo;
-	TupleDesc	tupDesc;
+	TupleDesc	tupDesc ;
 	Plan	   *subplan;
 	ListCell   *l;
 	int			i;
 	Relation	rel;
+	ResultRelInfo *targetRelInfo;
 
 	/* check for unsupported flags */
 	Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
@@ -1788,6 +1801,83 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		mtstate->mt_partition_tuple_slot = partition_tuple_slot;
 	}
 
+	/* Check if this is a multi-table update through a named target relation. */
+	targetRelInfo = NULL;
+	if (estate->es_num_root_result_relations > 0)
+	{
+		/* Partitioned table.  The named relation is the first root. */
+		targetRelInfo = &estate->es_root_result_relations[0];
+
+	}
+	else if (mtstate->mt_nplans > 1)
+	{
+		/* Inheritance hierarchy.  The named relation is the first plan. */
+		targetRelInfo = &mtstate->resultRelInfo[0];
+	}
+
+	/*
+	 * If this is a multi-table update, check if we need to build a
+	 * TriggerTransitionFilter to control the capture of transition tuples
+	 * from the whole set of tables.
+	 */
+	if (targetRelInfo != NULL)
+	{
+		int		i;
+
+		/* Check for transition tables on the directly targeted relation. */
+		mtstate->mt_transition_filter =
+			MakeTriggerTransitionFilter(targetRelInfo->ri_TrigDesc);
+
+		/* Check for transition tables on all child tables. */
+		for (i = 0; i < mtstate->mt_nplans; ++i)
+		{
+			mtstate->mt_transition_filter =
+				MergeTriggerTransitionFilter(mtstate->mt_transition_filter,
+											 mtstate->resultRelInfo[i].ri_TrigDesc);
+		}
+
+		/*
+		 * Note that mt_transition_filter may still be NULL here if no reason
+		 * to collect transition tuples was found.
+		 */
+	}
+
+	/*
+	 * If we found that we need to collect transition tuples from multiple
+	 * tables, then we'll also need tuple conversion maps for any children
+	 * that have incompatible TupleDescriptors.
+	 */
+	if (mtstate->mt_transition_filter != NULL)
+	{
+		int		i;
+
+		/*
+		 * Record the rel ID of the target table.  This will be used to
+		 * control the transition tuplestores' TupleDesc as visible to scans.
+		 */
+		mtstate->mt_transition_filter->ttf_transition_relid =
+			targetRelInfo->ri_RelationDesc->rd_id;
+
+		/*
+		 * 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.
+		 */
+		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),
+									   RelationGetDescr(targetRelInfo->ri_RelationDesc),
+									   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/backend/executor/spi.c b/src/backend/executor/spi.c
index 35021e1839b..e1c150a7cb0 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -2739,7 +2739,7 @@ SPI_register_trigger_data(TriggerData *tdata)
 		int		rc;
 
 		enr->md.name = tdata->tg_trigger->tgnewtable;
-		enr->md.reliddesc = tdata->tg_relation->rd_id;
+		enr->md.reliddesc = tdata->tg_transition_relid;
 		enr->md.tupdesc = NULL;
 		enr->md.enrtype = ENR_NAMED_TUPLESTORE;
 		enr->md.enrtuples = tuplestore_tuple_count(tdata->tg_newtable);
@@ -2756,7 +2756,7 @@ SPI_register_trigger_data(TriggerData *tdata)
 		int		rc;
 
 		enr->md.name = tdata->tg_trigger->tgoldtable;
-		enr->md.reliddesc = tdata->tg_relation->rd_id;
+		enr->md.reliddesc = tdata->tg_transition_relid;
 		enr->md.tupdesc = NULL;
 		enr->md.enrtype = ENR_NAMED_TUPLESTORE;
 		enr->md.enrtuples = tuplestore_tuple_count(tdata->tg_oldtable);
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index d73969c8747..16bc7759944 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -32,6 +32,7 @@ typedef struct TriggerData
 	NodeTag		type;
 	TriggerEvent tg_event;
 	Relation	tg_relation;
+	Oid			tg_transition_relid;
 	HeapTuple	tg_trigtuple;
 	HeapTuple	tg_newtuple;
 	Trigger    *tg_trigger;
@@ -42,6 +43,22 @@ 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;
+	Oid			ttf_transition_relid;
+	TupleConversionMap *ttf_map;
+} TriggerTransitionFilter;
+
+/*
  * TriggerEvent bit flags
  *
  * Note that we assume different event types (INSERT/DELETE/UPDATE/TRUNCATE)
@@ -127,6 +144,10 @@ extern void RelationBuildTriggers(Relation relation);
 
 extern TriggerDesc *CopyTriggerDesc(TriggerDesc *trigdesc);
 
+extern TriggerTransitionFilter *MakeTriggerTransitionFilter(TriggerDesc *trigdesc);
+extern TriggerTransitionFilter *MergeTriggerTransitionFilter(TriggerTransitionFilter *filter,
+															 TriggerDesc *trigdesc);
+
 extern void FreeTriggerDesc(TriggerDesc *trigdesc);
 
 extern void ExecBSInsertTriggers(EState *estate,
@@ -139,7 +160,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 +177,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 +197,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 c300449f3aa..8efb199a6e2 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
 --
@@ -1883,3 +1858,104 @@ 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 row triggers on partition parent with transition
+-- tables, when the row trigger is installed on a child
+--
+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 different order
+create table child2 (b int, a text);
+insert into child2 values (42, 'BBB');
+alter table parent attach partition child2 for values in ('BBB');
+create trigger child2_row_trig
+  after insert or update or delete on child2
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+-- updating the parent should cause the transition tuples to be
+-- captured from child1 and child2, and child2_row_trig should see
+-- them and they should all be converted to the same format
+update parent set b = b + 1;
+NOTICE:  old table = [{"a":"AAA","b":42}, {"a":"BBB","b":42}], new table = [{"a":"AAA","b":43}, {"a":"BBB","b":43}]
+drop table child1, child2, 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;
+--
+-- Verify behavior of row triggers on inheritance child tables with
+-- transition tables
+--
+create table animal (a text);
+create table cat (whiskers text) inherits (animal);
+create table kangaroo (pouch text) inherits (animal);
+insert into cat values ('C', 'whiskers');
+insert into kangaroo values ('K', 'pouch');
+create trigger kangaroo_row_trig
+  after insert or update or delete on kangaroo
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+-- updating "animal" should fire kangaroo's row trigger, and it should
+-- see the "cat" and "kangaroo" rows but they should be sliced down to
+-- "animal" format, because that is the directly accessed relation and
+-- the common subset of columns
+update animal set a = a || 'x';
+NOTICE:  old table = [{"a":"C"}, {"a":"K"}], new table = [{"a":"Cx"}, {"a":"Kx"}]
+drop table cat, kangaroo, animal;
+drop function dump_transition_tables();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index e5dbcaeea36..50a52f51949 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
 --
@@ -1348,3 +1324,125 @@ 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 row triggers on partition parent with transition
+-- tables, when the row trigger is installed on a child
+--
+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 different order
+create table child2 (b int, a text);
+insert into child2 values (42, 'BBB');
+alter table parent attach partition child2 for values in ('BBB');
+
+create trigger child2_row_trig
+  after insert or update or delete on child2
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+
+-- updating the parent should cause the transition tuples to be
+-- captured from child1 and child2, and child2_row_trig should see
+-- them and they should all be converted to the same format
+update parent set b = b + 1;
+
+drop table child1, child2, 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;
+
+--
+-- Verify behavior of row triggers on inheritance child tables with
+-- transition tables
+--
+create table animal (a text);
+create table cat (whiskers text) inherits (animal);
+create table kangaroo (pouch text) inherits (animal);
+insert into cat values ('C', 'whiskers');
+insert into kangaroo values ('K', 'pouch');
+
+create trigger kangaroo_row_trig
+  after insert or update or delete on kangaroo
+  referencing old table as old_table new table as new_table
+  for each row
+  execute procedure dump_transition_tables();
+
+-- updating "animal" should fire kangaroo's row trigger, and it should
+-- see the "cat" and "kangaroo" rows but they should be sliced down to
+-- "animal" format, because that is the directly accessed relation and
+-- the common subset of columns
+update animal set a = a || 'x';
+
+drop table cat, kangaroo, animal;
+
+drop function dump_transition_tables();
#25Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Thomas Munro (#24)

On Wed, May 10, 2017 at 11:10 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

2. If you attach a row-level trigger with transition tables to any
inheritance child, it will see transition tuples from all tables in
the inheritance hierarchy at or below the directly named table that
were modified by the same statement, sliced so that they appear as
tuples from the directly named table.

Of course that's a bit crazy, not only for trigger authors to
understand and deal with, but also for plan caching: it just doesn't
really make sense to have a database object, even an ephemeral one,
whose type changes depending on how the trigger was invoked, because
the plans stick around. Perhaps you could modify NamedTuplestorescan
to convert on the fly to the TupleDesc of the table that the row-level
trigger is attached to, using NULL for missing columns, but that'd be
a slightly strange too, depending on how you did it.

Perhaps we should reject row-level triggers with transition tables on
tables that are part of an inheritance hierarchy, but allow them for
partitions.

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#26Robert Haas
robertmhaas@gmail.com
In reply to: Thomas Munro (#25)

On Wed, May 10, 2017 at 8:02 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Wed, May 10, 2017 at 11:10 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

2. If you attach a row-level trigger with transition tables to any
inheritance child, it will see transition tuples from all tables in
the inheritance hierarchy at or below the directly named table that
were modified by the same statement, sliced so that they appear as
tuples from the directly named table.

Of course that's a bit crazy, not only for trigger authors to
understand and deal with, but also for plan caching: it just doesn't
really make sense to have a database object, even an ephemeral one,
whose type changes depending on how the trigger was invoked, because
the plans stick around. Perhaps you could modify NamedTuplestorescan
to convert on the fly to the TupleDesc of the table that the row-level
trigger is attached to, using NULL for missing columns, but that'd be
a slightly strange too, depending on how you did it.

I don't think it's crazy from a user perspective, but the plan caching
thing sounds like a problem.

Perhaps we should reject row-level triggers with transition tables on
tables that are part of an inheritance hierarchy, but allow them for
partitions.

Sounds like a sensible solution.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#27Noah Misch
noah@leadboat.com
In reply to: Noah Misch (#13)

On Sat, May 06, 2017 at 06:54:37PM +0000, Noah Misch wrote:

On Mon, May 01, 2017 at 11:10:52AM -0500, Kevin Grittner wrote:

On Mon, May 1, 2017 at 10:01 AM, Robert Haas <robertmhaas@gmail.com> wrote:

It seems pretty clear to me that this is busted.

I don't think you actually tested anything that is dependent on any
of my patches there.

Adding this as an open item. Kevin?

It will take some time to establish what legacy behavior is and how
the new transition tables are impacted. My first reaction is that a
trigger on the parent should fire for any related action on a child
(unless maybe the trigger is defined with an ONLY keyword???) using
the TupleDesc of the parent. Note that the SQL spec mandates that
even in a AFTER EACH ROW trigger the transition tables must
represent all rows affected by the STATEMENT. I think that this
should be independent of triggers fired at the row level. I think
the rules should be similar for updateable views.

This will take some time to investigate, discuss and produce a
patch. I think best case is Friday.

[Action required within three days. This is a generic notification.]

The above-described topic is currently a PostgreSQL 10 open item. Kevin,
since you committed the patch believed to have created it, you own this open
item. If some other commit is more relevant or if this does not belong as a
v10 open item, please let us know. Otherwise, please observe the policy on
open item ownership[1] and send a status update within three calendar days of
this message. Include a date for your subsequent status update. Testers may
discover new open items at any time, and I want to plan to get them all fixed
well in advance of shipping v10. Consequently, I will appreciate your efforts
toward speedy resolution. Thanks.

[1] /messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

This PostgreSQL 10 open item is past due for your status update. Kindly send
a status update within 24 hours, and include a date for your subsequent status
update. Refer to the policy on open item ownership:
/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#28Robert Haas
robertmhaas@gmail.com
In reply to: Noah Misch (#27)

On Thu, May 11, 2017 at 3:38 AM, Noah Misch <noah@leadboat.com> wrote:

This PostgreSQL 10 open item is past due for your status update. Kindly send
a status update within 24 hours, and include a date for your subsequent status
update. Refer to the policy on open item ownership:
/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

Kevin has not posted to this mailing list since May 3rd. I don't know
whether he's gone on vacation or been without email access for some
other reason, but I think we'd better assume that he's not likely to
respond to emails demanding immediate action regardless of how many of
them we send.

I'm not prepared to write a patch for this issue, but it seems like
Thomas is on top of that. If nobody else steps up to the plate I
guess I'm willing to take responsibility for reviewing and committing
that patch once it's in final form, but at this point I don't think
it's going to be possible to get that done before Monday's planned
wrap.

In formal terms, if Kevin forfeits ownership of this item and nobody
else volunteers to adopt it, put me down as owner with a next-update
date of Friday, May 19th, the day after beta1 is expected to ship.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#29Kevin Grittner
kgrittn@gmail.com
In reply to: Thomas Munro (#16)

[Apologies to all for my recent absence from community lists, and
special thanks to Thomas and Robert for picking up the slack.]

On Tue, May 9, 2017 at 4:51 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Tue, May 9, 2017 at 10:29 PM, Thomas Munro <thomas.munro@enterprisedb.com> wrote:

Recall that transition tables can be specified for statement-level
triggers AND row-level triggers. If you specify them for row-level
triggers, then they can see all rows changed so far each time they
fire.

No, they see all rows from the statement, each time.

test=# create table t (c int not null);
CREATE TABLE
test=# create function t_func()
test-# returns trigger
test-# language plpgsql
test-# as $$
test$# begin
test$# raise notice '% / % = %',
test$# new.c,
test$# (select sum(c) from n),
test$# (select new.c::float / sum(n.c) from n);
test$# return null;
test$# end;
test$# $$;
CREATE FUNCTION
test=# create trigger t_trig
test-# after insert or update on t
test-# referencing new table as n
test-# for each row
test-# execute procedure t_func();
CREATE TRIGGER
test=# insert into t select generate_series(1,5);
NOTICE: 1 / 15 = 0.0666666666666667
NOTICE: 2 / 15 = 0.133333333333333
NOTICE: 3 / 15 = 0.2
NOTICE: 4 / 15 = 0.266666666666667
NOTICE: 5 / 15 = 0.333333333333333
INSERT 0 5

This behavior is required for this feature by the SQL standard.

Now our policy of firing the statement level triggers only for
the named relation but firing row-level triggers for all modified
relations leads to a tricky problem for the inheritance case: what
type of transition tuples should the child table's row-level triggers
see?

The record format for the object on which the trigger was declared, IMO.

Suppose you have an inheritance hierarchy like this:

animal
-> mammal
-> cat

You define a statement-level trigger on "animal" and another
statement-level trigger on "mammal". You define a row-level trigger
on "cat". When you update either "animal" or "mammal", the row
triggers on "cat" might run. Row-level triggers on "cat" see OLD and
NEW as "cat" tuples, of course, but if they are configured to see
transition tables, should they see "cat", "mammal" or "animal" tuples
in the transition tables? With my patch as it is, that depends on
which level of the hierarchy you explicitly updated!

I think that the ideal behavior would be that if you define a
trigger on "cat", you see rows in the "cat" format; if you define a
trigger on rows for "mammal", you see rows in the "mammal" format;
if you define a trigger on rows for "animal", you see rows in the
"animal" format. Also, the ideal would be that we support an ONLY
option for trigger declaration. If your statement is ONLY on the a
given level in the hierarchy, the row triggers for only that level
would fire. If you don't use ONLY, a row trigger at that level
would fire for operations at that level or any child level, but with
a record format matching the level of the trigger.

Now, that may be too ambitious for this release. If so, I suggest
we not implement anything that would be broken by the above, and
throw a "not implemented" error when necessary.

--
Kevin Grittner
VMware vCenter Server
https://www.vmware.com/

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#30Kevin Grittner
kgrittn@gmail.com
In reply to: Thomas Munro (#25)

On Wed, May 10, 2017 at 7:02 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Wed, May 10, 2017 at 11:10 PM, Thomas Munro <thomas.munro@enterprisedb.com> wrote:

2. If you attach a row-level trigger with transition tables to any
inheritance child, it will see transition tuples from all tables in
the inheritance hierarchy at or below the directly named table that
were modified by the same statement, sliced so that they appear as
tuples from the directly named table.

Of course that's a bit crazy, not only for trigger authors to
understand and deal with, but also for plan caching: it just doesn't
really make sense to have a database object, even an ephemeral one,
whose type changes depending on how the trigger was invoked, because
the plans stick around.

The patch to add transition tables changed caching of a trigger
function to key on the combination of the function and the target
relation, rather than having one cache entry regardless of the
target table.

--
Kevin Grittner
VMware vCenter Server
https://www.vmware.com/

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#31Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Kevin Grittner (#30)

On Wed, May 17, 2017 at 9:20 AM, Kevin Grittner <kgrittn@gmail.com> wrote:

On Wed, May 10, 2017 at 7:02 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Wed, May 10, 2017 at 11:10 PM, Thomas Munro <thomas.munro@enterprisedb.com> wrote:

2. If you attach a row-level trigger with transition tables to any
inheritance child, it will see transition tuples from all tables in
the inheritance hierarchy at or below the directly named table that
were modified by the same statement, sliced so that they appear as
tuples from the directly named table.

Of course that's a bit crazy, not only for trigger authors to
understand and deal with, but also for plan caching: it just doesn't
really make sense to have a database object, even an ephemeral one,
whose type changes depending on how the trigger was invoked, because
the plans stick around.

The patch to add transition tables changed caching of a trigger
function to key on the combination of the function and the target
relation, rather than having one cache entry regardless of the
target table.

Right. That works as long as triggers always see tuples in the format
of the relation that they're attached to, and I think that's the only
sensible choice. The problem I was thinking about was this: We have
only one pair of tuplestores and in the current design it holds tuples
of a uniform format, yet it may (eventually) need to be scanned by a
statement trigger attached to a the named relation AND any number of
row triggers attached to children with potentially different formats.
That implies some extra conversions are required at scan time. I had
a more ambitious patch that would deal with sone of that by tracking
storage format and scan format separately (next time your row trigger
is invoked the scan format will be the same but the storage format may
be different depending on which relation you named in a query), but
I'm putting that to one side for this release. That was a bit of a
rabbit hole, and there are some tricky design questions about tuple
conversions (to behave like DB2 with subtables may even require
tuplestore with per-tuple type information) and also the subset of
rows that each row trigger should see (which may require extra tuple
origin metadata or separate tuplestores).

I'm about to post a much simpler patch that collects uniform tuples
from children, addressing the reported bug, and simply rejects
transition tables on row-triggers on tables that are in either kind of
inheritance hierarchy. More soon...

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#32Kevin Grittner
kgrittn@gmail.com
In reply to: Thomas Munro (#31)

On Tue, May 16, 2017 at 4:50 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Wed, May 17, 2017 at 9:20 AM, Kevin Grittner <kgrittn@gmail.com> wrote:

On Wed, May 10, 2017 at 7:02 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Wed, May 10, 2017 at 11:10 PM, Thomas Munro <thomas.munro@enterprisedb.com> wrote:

2. If you attach a row-level trigger with transition tables to any
inheritance child, it will see transition tuples from all tables in
the inheritance hierarchy at or below the directly named table that
were modified by the same statement, sliced so that they appear as
tuples from the directly named table.

Of course that's a bit crazy, not only for trigger authors to
understand and deal with, but also for plan caching: it just doesn't
really make sense to have a database object, even an ephemeral one,
whose type changes depending on how the trigger was invoked, because
the plans stick around.

The patch to add transition tables changed caching of a trigger
function to key on the combination of the function and the target
relation, rather than having one cache entry regardless of the
target table.

Right. That works as long as triggers always see tuples in the format
of the relation that they're attached to, and I think that's the only
sensible choice. The problem I was thinking about was this: We have
only one pair of tuplestores and in the current design it holds tuples
of a uniform format, yet it may (eventually) need to be scanned by a
statement trigger attached to a the named relation AND any number of
row triggers attached to children with potentially different formats.
That implies some extra conversions are required at scan time. I had
a more ambitious patch that would deal with sone of that by tracking
storage format and scan format separately (next time your row trigger
is invoked the scan format will be the same but the storage format may
be different depending on which relation you named in a query), but
I'm putting that to one side for this release. That was a bit of a
rabbit hole, and there are some tricky design questions about tuple
conversions (to behave like DB2 with subtables may even require
tuplestore with per-tuple type information) and also the subset of
rows that each row trigger should see (which may require extra tuple
origin metadata or separate tuplestores).

Yeah, I wish this had surfaced far earlier, but I missed it and none
of the reviews prior to commit caught it, either. :-( It seems too
big to squeeze into v10 now. I just want to have that general
direction in mind and not paint ourselves into any corner that makes
it hard to get there in the next release.

I'm about to post a much simpler patch that collects uniform tuples
from children, addressing the reported bug, and simply rejects
transition tables on row-triggers on tables that are in either kind of
inheritance hierarchy. More soon...

I agree that we can safely go that far, but not farther. Thanks!

--
Kevin Grittner
VMware vCenter Server
https://www.vmware.com/

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#33Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Kevin Grittner (#32)
2 attachment(s)

On Wed, May 17, 2017 at 10:13 AM, Kevin Grittner <kgrittn@gmail.com> wrote:

On Tue, May 16, 2017 at 4:50 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

I'm about to post a much simpler patch that collects uniform tuples
from children, addressing the reported bug, and simply rejects
transition tables on row-triggers on tables that are in either kind of
inheritance hierarchy. More soon...

I agree that we can safely go that far, but not farther. Thanks!

Here is that patch. Thoughts?

On Wed, May 10, 2017 at 3:41 PM, Robert Haas <robertmhaas@gmail.com> wrote:

Hmm. What if the partitioning hierarchy contains foreign tables?

No tuples are collected from partitions that are foreign tables. See
the attached demonstration. I wasn't sure whether and if so where to
include that in the regression tests because it needs a contrib
module.

Robert and I discussed this off-list and came up with some options:
(1) document that as the current behaviour (where?), (2) figure out
how to prevent that situation from arising, (3) raise some kind of
runtime error if foreign transition tuples need to be collected.

Option 2 would seem to require us to lock the whole chain of ancestors
to check for statement-level triggers with transition tables, which
seems unpleasant, and option 3 is conceptually similar to the
execution time insertion failure. It's debatable wither 3 or 1 is
more surprising or inconvenient to users. I vote for option 1 as a
stop-gap measure (and I hope that someone will soon fix transition
tuple capture for foreign tables generally). However, it's a bit
inconsistent that we explicitly reject triggers with transition tables
on foreign tables directly, but let them silently fail to capture
anything when they're indirectly accessed via a parent relation.
Thoughts?

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tables-vs-foreign-partitions.sqlapplication/octet-stream; name=transition-tables-vs-foreign-partitions.sqlDownload
transition-tuples-from-child-tables-v4.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v4.patchDownload
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index 827ad2a850e..4c81b37d515 100644
--- a/src/backend/catalog/pg_inherits.c
+++ b/src/backend/catalog/pg_inherits.c
@@ -283,6 +283,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 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..52d2e4dffbe 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 (estate->es_num_root_result_relations > 0)
+	{
+		/* Partitioned table.  The named relation is the first root. */
+		targetRelInfo = &estate->es_root_result_relations[0];
+	}
+	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();
#34Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Thomas Munro (#33)

On 2017/05/17 11:22, Thomas Munro wrote:

On Wed, May 17, 2017 at 10:13 AM, Kevin Grittner <kgrittn@gmail.com> wrote:

On Tue, May 16, 2017 at 4:50 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

I'm about to post a much simpler patch that collects uniform tuples
from children, addressing the reported bug, and simply rejects
transition tables on row-triggers on tables that are in either kind of
inheritance hierarchy. More soon...

I agree that we can safely go that far, but not farther. Thanks!

Here is that patch. Thoughts?

I looked at the patch and noticed that there might be some confusion about
what's in the EState.es_root_result_relations array.

If you see the following block of code in ExecInitModifyTable():

/* If modifying a partitioned table, initialize the root table info */
if (node->rootResultRelIndex >= 0)
mtstate->rootResultRelInfo = estate->es_root_result_relations +
node->rootResultRelIndex;

You might be able to see that node->rootResultRelIndex is used as offset
into es_root_result_relations, which means the partitioned table root
being modified by this node. The entries in es_root_result_relations
correspond to the partitioned table roots referenced as targets in
different parts of the query (for example, a WITH query might have its own
target partitioned tables).

So, in ExecSetupTriggerTransitionState() added by the patch, the following
code needs to be changed:

/*
* Find the ResultRelInfo corresponding to the relation that was
* explicitly named in the statement.
*/
if (estate->es_num_root_result_relations > 0)
{
/* Partitioned table. The named relation is the first root. */
targetRelInfo = &estate->es_root_result_relations[0];
}

targetRelInfo should instead be set to mtstate->rootResultRelInfo that was
set in ExecInitModifyTable() as described above, IOW, as follows:

/* Partitioned table. */
if (mtstate->rootResultRelInfo != NULL)
targetRelInfo = mtstate->rootResultRelInfo;

I guess that's what you intend to do here.

Sorry if the comments that I wrote about es_root_result_relations in what
got committed as e180c8aa8c were not clear enough.

Thanks,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#35Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Amit Langote (#34)
1 attachment(s)

On Wed, May 17, 2017 at 6:04 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/05/17 11:22, Thomas Munro wrote:

Here is that patch. Thoughts?

I looked at the patch and noticed that there might be some confusion about
what's in the EState.es_root_result_relations array.

Thanks for looking at this!

...

targetRelInfo should instead be set to mtstate->rootResultRelInfo that was
set in ExecInitModifyTable() as described above, IOW, as follows:

/* Partitioned table. */
if (mtstate->rootResultRelInfo != NULL)
targetRelInfo = mtstate->rootResultRelInfo;

Ah, I see. Thank you. Fixed in the attached.

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables-v5.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v5.patchDownload
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();
#36Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Thomas Munro (#35)
1 attachment(s)

On Wed, May 17, 2017 at 7:42 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Wed, May 17, 2017 at 6:04 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

targetRelInfo should instead be set to mtstate->rootResultRelInfo that was
set in ExecInitModifyTable() as described above, IOW, as follows:

/* Partitioned table. */
if (mtstate->rootResultRelInfo != NULL)
targetRelInfo = mtstate->rootResultRelInfo;

Ah, I see. Thank you. Fixed in the attached.

Here's a post-pgindent rebase.

Also, I discovered a preexisting bug that is independent of all this
inheritance stuff. COPY in the batch optimisation case was failing to
capture transition tuples. I thought about sending a separate patch
but this patch already has a regression test that covers it so I've
included it here. It's this hunk:

@@ -2872,7 +2872,8 @@ CopyFromInsertBatch(CopyState cstate, EState
*estate, CommandId mycid,
         * anyway.
         */
        else if (resultRelInfo->ri_TrigDesc != NULL &&
-                        resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+                        (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+                         resultRelInfo->ri_TrigDesc->trig_insert_new_table))
        {
                for (i = 0; i < nBufferedTuples; i++)
                {

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables-v6.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v6.patchDownload
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index e5fb52cfbf8..207ae84f3df 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 84b1a54cb9b..a11c4dd4fad 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);
 		}
 	}
@@ -2851,14 +2872,15 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 	 * anyway.
 	 */
 	else if (resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+			 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
 	{
 		for (i = 0; i < nBufferedTuples; i++)
 		{
 			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 7319aa597e7..67c50312233 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 0271788bf99..80bca37407e 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);
 }
 
 
@@ -5091,7 +5178,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;
@@ -5121,10 +5209,47 @@ 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;
 
@@ -5132,12 +5257,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			old_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.old_tuplestores);
-			tuplestore_puttuple(old_tuplestore, oldtup);
+			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;
 
@@ -5145,13 +5276,24 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			new_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.new_tuplestores);
-			tuplestore_puttuple(new_tuplestore, newtup);
+			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) ||
-		(event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) ||
-		 (event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_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 4a899f1eb56..7c82cda1431 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 c6a66b6195f..0a98fbfdf54 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 cf555fe78d9..f504f7c2cab 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 d33392f3b55..666bb94112d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -946,6 +946,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..ef12c975e71 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 = (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>
+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();
#37Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Thomas Munro (#36)

On 2017/05/18 7:13, Thomas Munro wrote:

On Wed, May 17, 2017 at 7:42 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Wed, May 17, 2017 at 6:04 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

targetRelInfo should instead be set to mtstate->rootResultRelInfo that was
set in ExecInitModifyTable() as described above, IOW, as follows:

/* Partitioned table. */
if (mtstate->rootResultRelInfo != NULL)
targetRelInfo = mtstate->rootResultRelInfo;

Ah, I see. Thank you. Fixed in the attached.

Here's a post-pgindent rebase.

I read through the latest patch. Some comments:

Do we need to update documentation? Perhaps, some clarification on the
inheritance/partitioning behavior somewhere.

+typedef struct TriggerTransitionState
+{
...
+    bool        ttf_delete_old_table;

Just curious: why ttf_? TriggerTransition field?

-    Assert((enrmd->reliddesc == InvalidOid) != (enrmd->tupdesc == NULL));
+    Assert((enrmd->reliddesc == InvalidOid) !=
+           (enrmd->tupdesc == NULL));

Perhaps, unintentional change?

+ 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;

I'm not sure if it's significant for transition tables, but what if a
partition's BR trigger modified the tuple? Would we want to include the
modified version of the tuple in the transition table or the original as
the patch does? Same for the code in CopyFrom().

* '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)

Oh, thanks! :)

Other than the above minor nitpicks, patch looks good to me.

Thanks,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#38Kevin Grittner
kgrittn@gmail.com
In reply to: Amit Langote (#37)

On Thu, May 18, 2017 at 5:16 AM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

Do we need to update documentation? Perhaps, some clarification on the
inheritance/partitioning behavior somewhere.

Yeah, I think so.

-    Assert((enrmd->reliddesc == InvalidOid) != (enrmd->tupdesc == NULL));
+    Assert((enrmd->reliddesc == InvalidOid) !=
+           (enrmd->tupdesc == NULL));

Perhaps, unintentional change?

Agreed; line is not long enough to need to wrap.

I'm not sure if it's significant for transition tables, but what if a
partition's BR trigger modified the tuple? Would we want to include the
modified version of the tuple in the transition table or the original as
the patch does? Same for the code in CopyFrom().

Good spot! If the BR trigger on the child table modifies or
suppresses the action, I strongly feel that must be reflected in the
transition table. This needs to be fixed.

--
Kevin Grittner
VMware vCenter Server
https://www.vmware.com/

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#39Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Kevin Grittner (#38)
1 attachment(s)

On Fri, May 19, 2017 at 1:38 AM, Kevin Grittner <kgrittn@gmail.com> wrote:

On Thu, May 18, 2017 at 5:16 AM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

Do we need to update documentation? Perhaps, some clarification on the
inheritance/partitioning behavior somewhere.

Yeah, I think so.

Here is an attempt at documenting the situation in the CREATE TRIGGER
notes section.

-    Assert((enrmd->reliddesc == InvalidOid) != (enrmd->tupdesc == NULL));
+    Assert((enrmd->reliddesc == InvalidOid) !=
+           (enrmd->tupdesc == NULL));

Perhaps, unintentional change?

Agreed; line is not long enough to need to wrap.

Fixed.

I'm not sure if it's significant for transition tables, but what if a
partition's BR trigger modified the tuple? Would we want to include the
modified version of the tuple in the transition table or the original as
the patch does? Same for the code in CopyFrom().

Good spot! If the BR trigger on the child table modifies or
suppresses the action, I strongly feel that must be reflected in the
transition table. This needs to be fixed.

Gah. Right. In the attached version, there is a still an 'original
tuple' optimisation for insertions (avoiding parent -> child -> parent
conversion), but it's disabled if there are any BEFORE INSERT or
INSTEAD OF INSERT row-level triggers.

That's demonstrated by this part of the regression test, which
modifies the value inserted into the 'CCC' partition (and similar case
for COPY):

insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
NOTICE: trigger = parent_stmt_trig, old table = <NULL>, new table =
(AAA,42), (BBB,42), (CCC,1066)

On Thu, May 18, 2017 at 10:16 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

+typedef struct TriggerTransitionState
+{
...
+    bool        ttf_delete_old_table;

Just curious: why ttf_? TriggerTransition field?

Oops. Changed to "tts_". I had renamed this struct but not the members.

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables-v7.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v7.patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index c5f7c758381..18efe6a9ed7 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -459,6 +459,20 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
   </para>
 
   <para>
+    Modifying a partitioned table or a table with inheritance children fires
+    statement-level triggers directly attached to that table, but not
+    statement-level triggers for its partitions or child tables.  In contrast,
+    row-level triggers are fired for all affected partitions or child tables.
+    If a statement-level trigger has been defined with transition relations
+    named by a <literal>REFERENCING</literal> clause, then before and after
+    images of rows are visible from all affected partitions or child tables.
+    In the case of inheritance children, the row images include only columns
+    that are present in the table that the trigger is attached to.  Currently,
+    row-level triggers with transition relations cannot be defined on
+    partitions or inheritance child tables.
+  </para>
+
+  <para>
    In <productname>PostgreSQL</productname> versions before 7.3, it was
    necessary to declare trigger functions as returning the placeholder
    type <type>opaque</>, rather than <type>trigger</>.  To support loading
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index e5fb52cfbf8..207ae84f3df 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 84b1a54cb9b..c0c19adb7ff 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -171,6 +171,8 @@ typedef struct CopyStateData
 	ResultRelInfo *partitions;	/* Per partition result relation */
 	TupleConversionMap **partition_tupconv_maps;
 	TupleTableSlot *partition_tuple_slot;
+	TriggerTransitionState *transition_state;
+	TupleConversionMap **transition_tupconv_maps;
 
 	/*
 	 * These variables are used to reduce overhead in textual COPY FROM.
@@ -1436,6 +1438,35 @@ 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->transition_state = MakeTriggerTransitionState(rel->trigdesc);
+
+			/*
+			 * If we are capturing transition tuples, they may need to be
+			 * converted from partition format back to partitioned table
+			 * format (this is only ever necessary if a BEFORE trigger
+			 * modifies the tuple).
+			 */
+			if (cstate->transition_state != NULL)
+			{
+				int		i;
+
+				cstate->transition_tupconv_maps = (TupleConversionMap **)
+					palloc0(sizeof(TupleConversionMap *) *
+							cstate->num_partitions);
+				for (i = 0; i < cstate->num_partitions; ++i)
+				{
+					cstate->transition_tupconv_maps[i] =
+						convert_tuples_by_name(RelationGetDescr(cstate->partitions[i].ri_RelationDesc),
+											   RelationGetDescr(rel),
+											   gettext_noop("could not convert row type"));
+				}
+			}
 		}
 	}
 	else
@@ -2595,6 +2626,31 @@ CopyFrom(CopyState cstate)
 			 * We might need to convert from the parent rowtype to the
 			 * partition rowtype.
 			 */
+			if (cstate->transition_state != NULL)
+			{
+				if (resultRelInfo->ri_TrigDesc &&
+					(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+					 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+				{
+					/*
+					 * If there are any BEFORE or INSTEAD triggers on the
+					 * partition, we'll have to be ready to convert their
+					 * result back to tuplestore format.
+					 */
+					cstate->transition_state->tts_original_insert_tuple = NULL;
+					cstate->transition_state->tts_map =
+						cstate->transition_tupconv_maps[leaf_part_index];
+				}
+				else
+				{
+					/*
+					 * Otherwise, just remember the original unconverted
+					 * tuple, to avoid a needless round trip conversion.
+					 */
+					cstate->transition_state->tts_original_insert_tuple = tuple;
+					cstate->transition_state->tts_map = NULL;
+				}
+			}
 			map = cstate->partition_tupconv_maps[leaf_part_index];
 			if (map)
 			{
@@ -2688,7 +2744,7 @@ CopyFrom(CopyState cstate)
 
 					/* AFTER ROW INSERT Triggers */
 					ExecARInsertTriggers(estate, resultRelInfo, tuple,
-										 recheckIndexes);
+										 recheckIndexes, cstate->transition_state);
 
 					list_free(recheckIndexes);
 				}
@@ -2841,7 +2897,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 									  estate, false, NULL, NIL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
-								 recheckIndexes);
+								 recheckIndexes, NULL);
 			list_free(recheckIndexes);
 		}
 	}
@@ -2851,14 +2907,15 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 	 * anyway.
 	 */
 	else if (resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+			 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
 	{
 		for (i = 0; i < nBufferedTuples; i++)
 		{
 			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 7319aa597e7..67c50312233 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 0271788bf99..124217084a1 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 tts_map or tts_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->tts_delete_old_table = trigdesc->trig_delete_old_table;
+		state->tts_update_old_table = trigdesc->trig_update_old_table;
+		state->tts_update_new_table = trigdesc->trig_update_new_table;
+		state->tts_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->tts_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->tts_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->tts_update_old_table ||
+						 transitions->tts_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);
 }
 
 
@@ -5091,7 +5178,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;
@@ -5121,10 +5209,48 @@ 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.  As a small optimization, we may receive the
+			 * original tuple from an insertion into a partitioned table to
+			 * avoid a wasteful parent->child->parent round trip.
+			 */
+			delete_old_table = transitions->tts_delete_old_table;
+			update_old_table = transitions->tts_update_old_table;
+			update_new_table = transitions->tts_update_new_table;
+			insert_new_table = transitions->tts_insert_new_table;
+			map = transitions->tts_map;
+			original_insert_tuple = transitions->tts_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;
 
@@ -5132,12 +5258,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			old_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.old_tuplestores);
-			tuplestore_puttuple(old_tuplestore, oldtup);
+			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;
 
@@ -5145,13 +5277,24 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			new_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.new_tuplestores);
-			tuplestore_puttuple(new_tuplestore, newtup);
+			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) ||
-		(event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) ||
-		 (event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_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 4a899f1eb56..7c82cda1431 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 c6a66b6195f..0a98fbfdf54 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 cf555fe78d9..c88e3d5353b 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -317,6 +317,31 @@ ExecInsert(ModifyTableState *mtstate,
 		 * We might need to convert from the parent rowtype to the partition
 		 * rowtype.
 		 */
+		if (mtstate->mt_transition_state != NULL)
+		{
+			if (resultRelInfo->ri_TrigDesc &&
+				(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+				 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+			{
+				/*
+				 * If there are any BEFORE or INSTEAD triggers on the
+				 * partition, we'll have to be ready to convert their result
+				 * back to tuplestore format.
+				 */
+				mtstate->mt_transition_state->tts_original_insert_tuple = NULL;
+				mtstate->mt_transition_state->tts_map =
+					mtstate->mt_transition_tupconv_maps[leaf_part_index];
+			}
+			else
+			{
+				/*
+				 * Otherwise, just remember the original unconverted tuple, to
+				 * avoid a needless round trip conversion.
+				 */
+				mtstate->mt_transition_state->tts_original_insert_tuple = tuple;
+				mtstate->mt_transition_state->tts_map = NULL;
+			}
+		}
 		map = mtstate->mt_partition_tupconv_maps[leaf_part_index];
 		if (map)
 		{
@@ -571,7 +596,8 @@ ExecInsert(ModifyTableState *mtstate,
 	}
 
 	/* AFTER ROW INSERT Triggers */
-	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
+	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes,
+						 mtstate->mt_transition_state);
 
 	list_free(recheckIndexes);
 
@@ -619,7 +645,8 @@ ExecInsert(ModifyTableState *mtstate,
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecDelete(ItemPointer tupleid,
+ExecDelete(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
@@ -796,7 +823,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 +905,8 @@ ldelete:;
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecUpdate(ItemPointer tupleid,
+ExecUpdate(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
 		   TupleTableSlot *planSlot,
@@ -1105,7 +1134,8 @@ lreplace:;
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
-						 recheckIndexes);
+						 recheckIndexes,
+						 mtstate->mt_transition_state);
 
 	list_free(recheckIndexes);
 
@@ -1312,7 +1342,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);
@@ -1359,20 +1389,31 @@ fireBSTriggers(ModifyTableState *node)
 }
 
 /*
- * Process AFTER EACH STATEMENT triggers
+ * Return the ResultRelInfo for which we will fire AFTER STATEMENT triggers.
+ * This is also the relation into whose tuple format all captured transition
+ * tuples must be converted.
  */
-static void
-fireASTriggers(ModifyTableState *node)
+static ResultRelInfo *
+getASTriggerResultRelInfo(ModifyTableState *node)
 {
-	ResultRelInfo *resultRelInfo = node->resultRelInfo;
-
 	/*
 	 * If the node modifies a partitioned table, we must fire its triggers.
 	 * Note that in that case, node->resultRelInfo points to the first leaf
 	 * partition, not the root table.
 	 */
 	if (node->rootResultRelInfo != NULL)
-		resultRelInfo = node->rootResultRelInfo;
+		return node->rootResultRelInfo;
+	else
+		return node->resultRelInfo;
+}
+
+/*
+ * Process AFTER EACH STATEMENT triggers
+ */
+static void
+fireASTriggers(ModifyTableState *node)
+{
+	ResultRelInfo *resultRelInfo = getASTriggerResultRelInfo(node);
 
 	switch (node->operation)
 	{
@@ -1394,6 +1435,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 = getASTriggerResultRelInfo(mtstate);
+	int		i;
+
+	/* 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.
+	 */
+	if (mtstate->mt_transition_state != NULL)
+	{
+		ResultRelInfo *resultRelInfos;
+		int		numResultRelInfos;
+
+		/* Find the set of partitions so that we can find their TupleDescs. */
+		if (mtstate->mt_partition_dispatch_info != NULL)
+		{
+			/*
+			 * For INSERT via partitioned table, so we need TupleDescs based
+			 * on the partition routing table.
+			 */
+			resultRelInfos = mtstate->mt_partitions;
+			numResultRelInfos = mtstate->mt_num_partitions;
+		}
+		else
+		{
+			/* Otherwise we need the ResultRelInfo for each subplan. */
+			resultRelInfos = mtstate->resultRelInfo;
+			numResultRelInfos = mtstate->mt_nplans;
+		}
+
+		/*
+		 * 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 *) * numResultRelInfos);
+		for (i = 0; i < numResultRelInfos; ++i)
+		{
+			mtstate->mt_transition_tupconv_maps[i] =
+				convert_tuples_by_name(RelationGetDescr(resultRelInfos[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.  INSERT operations set it every time.
+		 */
+		mtstate->mt_transition_state->tts_map =
+			mtstate->mt_transition_tupconv_maps[0];
+	}
+}
 
 /* ----------------------------------------------------------------
  *	   ExecModifyTable
@@ -1492,6 +1599,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->tts_map =
+						node->mt_transition_tupconv_maps[node->mt_whichplan];
+				}
 				continue;
 			}
 			else
@@ -1602,11 +1716,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 +1902,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/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..e5758c64697 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		tts_delete_old_table;
+	bool		tts_update_old_table;
+	bool		tts_update_new_table;
+	bool		tts_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 *tts_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	tts_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 d33392f3b55..666bb94112d 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -946,6 +946,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..f38665c1450 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,248 @@ 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)
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (AAA,42), (BBB,42), (CCC,1066)
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (AAA,42), (BBB,42), (CCC,1234)
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+--
+-- 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 = (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>
+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..05e0c36179f 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,286 @@ 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
+\.
+
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+
+
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+AAA	42
+BBB	42
+CCC	234
+\.
+
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+
+--
+-- 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();
#40Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Thomas Munro (#39)

On 2017/05/19 14:01, Thomas Munro wrote:

On Fri, May 19, 2017 at 1:38 AM, Kevin Grittner <kgrittn@gmail.com> wrote:

On Thu, May 18, 2017 at 5:16 AM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

Do we need to update documentation? Perhaps, some clarification on the
inheritance/partitioning behavior somewhere.

Yeah, I think so.

Here is an attempt at documenting the situation in the CREATE TRIGGER
notes section.

Looks good, thanks.

I'm not sure if it's significant for transition tables, but what if a
partition's BR trigger modified the tuple? Would we want to include the
modified version of the tuple in the transition table or the original as
the patch does? Same for the code in CopyFrom().

Good spot! If the BR trigger on the child table modifies or
suppresses the action, I strongly feel that must be reflected in the
transition table. This needs to be fixed.

Gah. Right. In the attached version, there is a still an 'original
tuple' optimisation for insertions (avoiding parent -> child -> parent
conversion), but it's disabled if there are any BEFORE INSERT or
INSTEAD OF INSERT row-level triggers.

That's demonstrated by this part of the regression test, which
modifies the value inserted into the 'CCC' partition (and similar case
for COPY):

insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
NOTICE: trigger = parent_stmt_trig, old table = <NULL>, new table =
(AAA,42), (BBB,42), (CCC,1066)

Seems to work correctly.

I saw in the latest patch that now ExecSetupTriggerTransitionState() looks
at mtstate->mt_partition_dispatch_info when setting up the transition
conversion map. In the case where it's non-NULL, you may have realized
that mt_transition_tupconv_map will be an exact copy of
mt_partition_tupconv_maps that's already built. Would it perhaps be a
good idea to either share the same or make a copy using memcpy() instead
of doing the convert_tuples_by_name() calls again?

On Thu, May 18, 2017 at 10:16 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

+typedef struct TriggerTransitionState
+{
...
+    bool        ttf_delete_old_table;

Just curious: why ttf_? TriggerTransition field?

Oops. Changed to "tts_". I had renamed this struct but not the members.

Ah. BTW, maybe it's not a problem, but the existing TupleTableSlot's
member names are prefixed with tts_, too. :)

Thanks,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#41Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Amit Langote (#40)

On Fri, May 19, 2017 at 5:51 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

I saw in the latest patch that now ExecSetupTriggerTransitionState() looks
at mtstate->mt_partition_dispatch_info when setting up the transition
conversion map. In the case where it's non-NULL, you may have realized
that mt_transition_tupconv_map will be an exact copy of
mt_partition_tupconv_maps that's already built. Would it perhaps be a
good idea to either share the same or make a copy using memcpy() instead
of doing the convert_tuples_by_name() calls again?

Isn't it the opposite? mt_partition_tupconv_maps holds maps that
convert the parent format to the partition format.
mt_transition_tupconv_maps holds maps that convert the partition
format to the parent format (= transition tuplestore format).

On Thu, May 18, 2017 at 10:16 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

+typedef struct TriggerTransitionState
+{
...
+    bool        ttf_delete_old_table;

Just curious: why ttf_? TriggerTransition field?

Oops. Changed to "tts_". I had renamed this struct but not the members.

Ah. BTW, maybe it's not a problem, but the existing TupleTableSlot's
member names are prefixed with tts_, too. :)

Would TransitionCaptureState be a better name for this struct?

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#42Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Thomas Munro (#41)

On 2017/05/19 15:16, Thomas Munro wrote:

On Fri, May 19, 2017 at 5:51 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

I saw in the latest patch that now ExecSetupTriggerTransitionState() looks
at mtstate->mt_partition_dispatch_info when setting up the transition
conversion map. In the case where it's non-NULL, you may have realized
that mt_transition_tupconv_map will be an exact copy of
mt_partition_tupconv_maps that's already built. Would it perhaps be a
good idea to either share the same or make a copy using memcpy() instead
of doing the convert_tuples_by_name() calls again?

Isn't it the opposite? mt_partition_tupconv_maps holds maps that
convert the parent format to the partition format.
mt_transition_tupconv_maps holds maps that convert the partition
format to the parent format (= transition tuplestore format).

You're right, never mind.

On Thu, May 18, 2017 at 10:16 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

+typedef struct TriggerTransitionState
+{
...
+    bool        ttf_delete_old_table;

Just curious: why ttf_? TriggerTransition field?

Oops. Changed to "tts_". I had renamed this struct but not the members.

Ah. BTW, maybe it's not a problem, but the existing TupleTableSlot's
member names are prefixed with tts_, too. :)

Would TransitionCaptureState be a better name for this struct?

Yes. Although, losing the Trigger prefix might make it sound a bit
ambiguous though. Right above its definition, we have TriggerData. So,
maybe TriggerTransitionCaptureState or TriggerTransitionCaptureData or
TriggerTransitionData may be worth considering.

Thanks,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#43Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Amit Langote (#42)
1 attachment(s)

On Fri, May 19, 2017 at 6:35 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/05/19 15:16, Thomas Munro wrote:

Would TransitionCaptureState be a better name for this struct?

Yes. Although, losing the Trigger prefix might make it sound a bit
ambiguous though. Right above its definition, we have TriggerData. So,
maybe TriggerTransitionCaptureState or TriggerTransitionCaptureData or
TriggerTransitionData may be worth considering.

Ok, here's a version using TransitionCaptureState. Those other names
seem too long, and "TriggerTransition" is already in use so
"TriggerTransitionData" seems off the table. Having the word
"capture" in there seems good, since this is an object that controls
what we capture when we process a modify a set of tables. I hope
that's clear.

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables-v8.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v8.patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index c5f7c758381..18efe6a9ed7 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -459,6 +459,20 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
   </para>
 
   <para>
+    Modifying a partitioned table or a table with inheritance children fires
+    statement-level triggers directly attached to that table, but not
+    statement-level triggers for its partitions or child tables.  In contrast,
+    row-level triggers are fired for all affected partitions or child tables.
+    If a statement-level trigger has been defined with transition relations
+    named by a <literal>REFERENCING</literal> clause, then before and after
+    images of rows are visible from all affected partitions or child tables.
+    In the case of inheritance children, the row images include only columns
+    that are present in the table that the trigger is attached to.  Currently,
+    row-level triggers with transition relations cannot be defined on
+    partitions or inheritance child tables.
+  </para>
+
+  <para>
    In <productname>PostgreSQL</productname> versions before 7.3, it was
    necessary to declare trigger functions as returning the placeholder
    type <type>opaque</>, rather than <type>trigger</>.  To support loading
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index e5fb52cfbf8..207ae84f3df 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 84b1a54cb9b..ba50927c6b0 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -171,6 +171,8 @@ typedef struct CopyStateData
 	ResultRelInfo *partitions;	/* Per partition result relation */
 	TupleConversionMap **partition_tupconv_maps;
 	TupleTableSlot *partition_tuple_slot;
+	TransitionCaptureState *transition_state;
+	TupleConversionMap **transition_tupconv_maps;
 
 	/*
 	 * These variables are used to reduce overhead in textual COPY FROM.
@@ -1436,6 +1438,35 @@ 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->transition_state = MakeTransitionCaptureState(rel->trigdesc);
+
+			/*
+			 * If we are capturing transition tuples, they may need to be
+			 * converted from partition format back to partitioned table
+			 * format (this is only ever necessary if a BEFORE trigger
+			 * modifies the tuple).
+			 */
+			if (cstate->transition_state != NULL)
+			{
+				int		i;
+
+				cstate->transition_tupconv_maps = (TupleConversionMap **)
+					palloc0(sizeof(TupleConversionMap *) *
+							cstate->num_partitions);
+				for (i = 0; i < cstate->num_partitions; ++i)
+				{
+					cstate->transition_tupconv_maps[i] =
+						convert_tuples_by_name(RelationGetDescr(cstate->partitions[i].ri_RelationDesc),
+											   RelationGetDescr(rel),
+											   gettext_noop("could not convert row type"));
+				}
+			}
 		}
 	}
 	else
@@ -2595,6 +2626,31 @@ CopyFrom(CopyState cstate)
 			 * We might need to convert from the parent rowtype to the
 			 * partition rowtype.
 			 */
+			if (cstate->transition_state != NULL)
+			{
+				if (resultRelInfo->ri_TrigDesc &&
+					(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+					 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+				{
+					/*
+					 * If there are any BEFORE or INSTEAD triggers on the
+					 * partition, we'll have to be ready to convert their
+					 * result back to tuplestore format.
+					 */
+					cstate->transition_state->tcs_original_insert_tuple = NULL;
+					cstate->transition_state->tcs_map =
+						cstate->transition_tupconv_maps[leaf_part_index];
+				}
+				else
+				{
+					/*
+					 * Otherwise, just remember the original unconverted
+					 * tuple, to avoid a needless round trip conversion.
+					 */
+					cstate->transition_state->tcs_original_insert_tuple = tuple;
+					cstate->transition_state->tcs_map = NULL;
+				}
+			}
 			map = cstate->partition_tupconv_maps[leaf_part_index];
 			if (map)
 			{
@@ -2688,7 +2744,7 @@ CopyFrom(CopyState cstate)
 
 					/* AFTER ROW INSERT Triggers */
 					ExecARInsertTriggers(estate, resultRelInfo, tuple,
-										 recheckIndexes);
+										 recheckIndexes, cstate->transition_state);
 
 					list_free(recheckIndexes);
 				}
@@ -2841,7 +2897,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 									  estate, false, NULL, NIL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
-								 recheckIndexes);
+								 recheckIndexes, NULL);
 			list_free(recheckIndexes);
 		}
 	}
@@ -2851,14 +2907,15 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 	 * anyway.
 	 */
 	else if (resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+			 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
 	{
 		for (i = 0; i < nBufferedTuples; i++)
 		{
 			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 fb961e46c4a..99a02a741f7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10924,6 +10924,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
@@ -11005,6 +11006,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);
 
@@ -13376,6 +13390,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);
 
@@ -13505,6 +13520,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 0271788bf99..d6d504a9292 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,
+					  TransitionCaptureState *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 TransitionCaptureState 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 tcs_map or tcs_original_insert_tuple as appropriate when dealing
+ * with child tables.
+ */
+TransitionCaptureState *
+MakeTransitionCaptureState(TriggerDesc *trigdesc)
+{
+	TransitionCaptureState *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 = (TransitionCaptureState *)
+			palloc0(sizeof(TransitionCaptureState));
+		state->tcs_delete_old_table = trigdesc->trig_delete_old_table;
+		state->tcs_update_old_table = trigdesc->trig_update_old_table;
+		state->tcs_update_new_table = trigdesc->trig_update_new_table;
+		state->tcs_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,
+					 TransitionCaptureState *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->tcs_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,
+					 TransitionCaptureState *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->tcs_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,
+					 TransitionCaptureState *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->tcs_update_old_table ||
+						 transitions->tcs_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);
 }
 
 
@@ -5091,7 +5178,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,
+					  TransitionCaptureState *transitions)
 {
 	Relation	rel = relinfo->ri_RelationDesc;
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5121,10 +5209,48 @@ 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 TransitionCaptureState 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.  As a small optimization, we may receive the
+			 * original tuple from an insertion into a partitioned table to
+			 * avoid a wasteful parent->child->parent round trip.
+			 */
+			delete_old_table = transitions->tcs_delete_old_table;
+			update_old_table = transitions->tcs_update_old_table;
+			update_new_table = transitions->tcs_update_new_table;
+			insert_new_table = transitions->tcs_insert_new_table;
+			map = transitions->tcs_map;
+			original_insert_tuple = transitions->tcs_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
+			 * TriggerCaptureState 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;
 
@@ -5132,12 +5258,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			old_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.old_tuplestores);
-			tuplestore_puttuple(old_tuplestore, oldtup);
+			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;
 
@@ -5145,13 +5277,24 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			new_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.new_tuplestores);
-			tuplestore_puttuple(new_tuplestore, newtup);
+			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) ||
-		(event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) ||
-		 (event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_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 4a899f1eb56..7c82cda1431 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 c6a66b6195f..0a98fbfdf54 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 cf555fe78d9..7d695181e0c 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -317,6 +317,31 @@ ExecInsert(ModifyTableState *mtstate,
 		 * We might need to convert from the parent rowtype to the partition
 		 * rowtype.
 		 */
+		if (mtstate->mt_transition_state != NULL)
+		{
+			if (resultRelInfo->ri_TrigDesc &&
+				(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+				 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+			{
+				/*
+				 * If there are any BEFORE or INSTEAD triggers on the
+				 * partition, we'll have to be ready to convert their result
+				 * back to tuplestore format.
+				 */
+				mtstate->mt_transition_state->tcs_original_insert_tuple = NULL;
+				mtstate->mt_transition_state->tcs_map =
+					mtstate->mt_transition_tupconv_maps[leaf_part_index];
+			}
+			else
+			{
+				/*
+				 * Otherwise, just remember the original unconverted tuple, to
+				 * avoid a needless round trip conversion.
+				 */
+				mtstate->mt_transition_state->tcs_original_insert_tuple = tuple;
+				mtstate->mt_transition_state->tcs_map = NULL;
+			}
+		}
 		map = mtstate->mt_partition_tupconv_maps[leaf_part_index];
 		if (map)
 		{
@@ -571,7 +596,8 @@ ExecInsert(ModifyTableState *mtstate,
 	}
 
 	/* AFTER ROW INSERT Triggers */
-	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
+	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes,
+						 mtstate->mt_transition_state);
 
 	list_free(recheckIndexes);
 
@@ -619,7 +645,8 @@ ExecInsert(ModifyTableState *mtstate,
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecDelete(ItemPointer tupleid,
+ExecDelete(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
@@ -796,7 +823,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 +905,8 @@ ldelete:;
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecUpdate(ItemPointer tupleid,
+ExecUpdate(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
 		   TupleTableSlot *planSlot,
@@ -1105,7 +1134,8 @@ lreplace:;
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
-						 recheckIndexes);
+						 recheckIndexes,
+						 mtstate->mt_transition_state);
 
 	list_free(recheckIndexes);
 
@@ -1312,7 +1342,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);
@@ -1359,20 +1389,31 @@ fireBSTriggers(ModifyTableState *node)
 }
 
 /*
- * Process AFTER EACH STATEMENT triggers
+ * Return the ResultRelInfo for which we will fire AFTER STATEMENT triggers.
+ * This is also the relation into whose tuple format all captured transition
+ * tuples must be converted.
  */
-static void
-fireASTriggers(ModifyTableState *node)
+static ResultRelInfo *
+getASTriggerResultRelInfo(ModifyTableState *node)
 {
-	ResultRelInfo *resultRelInfo = node->resultRelInfo;
-
 	/*
 	 * If the node modifies a partitioned table, we must fire its triggers.
 	 * Note that in that case, node->resultRelInfo points to the first leaf
 	 * partition, not the root table.
 	 */
 	if (node->rootResultRelInfo != NULL)
-		resultRelInfo = node->rootResultRelInfo;
+		return node->rootResultRelInfo;
+	else
+		return node->resultRelInfo;
+}
+
+/*
+ * Process AFTER EACH STATEMENT triggers
+ */
+static void
+fireASTriggers(ModifyTableState *node)
+{
+	ResultRelInfo *resultRelInfo = getASTriggerResultRelInfo(node);
 
 	switch (node->operation)
 	{
@@ -1394,6 +1435,72 @@ fireASTriggers(ModifyTableState *node)
 	}
 }
 
+/*
+ * Set up the state needed for collecting transition tuples for AFTER
+ * triggers.
+ */
+static void
+ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate)
+{
+	ResultRelInfo *targetRelInfo = getASTriggerResultRelInfo(mtstate);
+	int		i;
+
+	/* Check for transition tables on the directly targeted relation. */
+	mtstate->mt_transition_state =
+		MakeTransitionCaptureState(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.
+	 */
+	if (mtstate->mt_transition_state != NULL)
+	{
+		ResultRelInfo *resultRelInfos;
+		int		numResultRelInfos;
+
+		/* Find the set of partitions so that we can find their TupleDescs. */
+		if (mtstate->mt_partition_dispatch_info != NULL)
+		{
+			/*
+			 * For INSERT via partitioned table, so we need TupleDescs based
+			 * on the partition routing table.
+			 */
+			resultRelInfos = mtstate->mt_partitions;
+			numResultRelInfos = mtstate->mt_num_partitions;
+		}
+		else
+		{
+			/* Otherwise we need the ResultRelInfo for each subplan. */
+			resultRelInfos = mtstate->resultRelInfo;
+			numResultRelInfos = mtstate->mt_nplans;
+		}
+
+		/*
+		 * 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 *) * numResultRelInfos);
+		for (i = 0; i < numResultRelInfos; ++i)
+		{
+			mtstate->mt_transition_tupconv_maps[i] =
+				convert_tuples_by_name(RelationGetDescr(resultRelInfos[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.  (INSERT operations set it every time.)
+		 */
+		mtstate->mt_transition_state->tcs_map =
+			mtstate->mt_transition_tupconv_maps[0];
+	}
+}
 
 /* ----------------------------------------------------------------
  *	   ExecModifyTable
@@ -1492,6 +1599,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->tcs_map =
+						node->mt_transition_tupconv_maps[node->mt_whichplan];
+				}
 				continue;
 			}
 			else
@@ -1602,11 +1716,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 +1902,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		mtstate->mt_partition_tuple_slot = partition_tuple_slot;
 	}
 
+	/* Build state for collecting transition tuples */
+	ExecSetupTransitionCaptureState(mtstate, estate);
+
 	/*
 	 * Initialize any WITH CHECK OPTION constraints if needed.
 	 */
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..e99561628ca 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 TransitionCaptureState
+{
+	/*
+	 * Is there at least one trigger specifying each transition relation on
+	 * the relation explicitly named in the DML statement or COPY command?
+	 */
+	bool		tcs_delete_old_table;
+	bool		tcs_update_old_table;
+	bool		tcs_update_new_table;
+	bool		tcs_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 *tcs_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	tcs_original_insert_tuple;
+} TransitionCaptureState;
+
+/*
  * 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 TransitionCaptureState *MakeTransitionCaptureState(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,
+					 TransitionCaptureState *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,
+					 TransitionCaptureState *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,
+					 TransitionCaptureState *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 d33392f3b55..c2df50ac223 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -946,6 +946,10 @@ typedef struct ModifyTableState
 	TupleConversionMap **mt_partition_tupconv_maps;
 	/* Per partition tuple conversion map */
 	TupleTableSlot *mt_partition_tuple_slot;
+	struct TransitionCaptureState *mt_transition_state;
+									/* controls transition table population */
+	TupleConversionMap **mt_transition_tupconv_maps;
+									/* Per plan/partition tuple conversion */
 } ModifyTableState;
 
 /* ----------------
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 0d560fb3eed..f38665c1450 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,248 @@ 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)
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (AAA,42), (BBB,42), (CCC,1066)
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (AAA,42), (BBB,42), (CCC,1234)
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+--
+-- 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 = (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>
+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..05e0c36179f 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,286 @@ 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
+\.
+
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+
+
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+AAA	42
+BBB	42
+CCC	234
+\.
+
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+
+--
+-- 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();
#44Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Thomas Munro (#43)
1 attachment(s)

On Sat, May 20, 2017 at 10:43 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Fri, May 19, 2017 at 6:35 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/05/19 15:16, Thomas Munro wrote:

Would TransitionCaptureState be a better name for this struct?

Yes. Although, losing the Trigger prefix might make it sound a bit
ambiguous though. Right above its definition, we have TriggerData. So,
maybe TriggerTransitionCaptureState or TriggerTransitionCaptureData or
TriggerTransitionData may be worth considering.

Ok, here's a version using TransitionCaptureState. Those other names
seem too long, and "TriggerTransition" is already in use so
"TriggerTransitionData" seems off the table. Having the word
"capture" in there seems good, since this is an object that controls
what we capture when we process a modify a set of tables. I hope
that's clear.

Sent too soon. Several variables should also be renamed to make clear
they refer to the transition capture state in effect, instead of vague
names like 'transitions'. Sorry for the version churn.

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables-v9.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v9.patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index c5f7c758381..18efe6a9ed7 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -459,6 +459,20 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
   </para>
 
   <para>
+    Modifying a partitioned table or a table with inheritance children fires
+    statement-level triggers directly attached to that table, but not
+    statement-level triggers for its partitions or child tables.  In contrast,
+    row-level triggers are fired for all affected partitions or child tables.
+    If a statement-level trigger has been defined with transition relations
+    named by a <literal>REFERENCING</literal> clause, then before and after
+    images of rows are visible from all affected partitions or child tables.
+    In the case of inheritance children, the row images include only columns
+    that are present in the table that the trigger is attached to.  Currently,
+    row-level triggers with transition relations cannot be defined on
+    partitions or inheritance child tables.
+  </para>
+
+  <para>
    In <productname>PostgreSQL</productname> versions before 7.3, it was
    necessary to declare trigger functions as returning the placeholder
    type <type>opaque</>, rather than <type>trigger</>.  To support loading
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index e5fb52cfbf8..207ae84f3df 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 84b1a54cb9b..7d5ec00e777 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -171,6 +171,8 @@ typedef struct CopyStateData
 	ResultRelInfo *partitions;	/* Per partition result relation */
 	TupleConversionMap **partition_tupconv_maps;
 	TupleTableSlot *partition_tuple_slot;
+	TransitionCaptureState *transition_capture;
+	TupleConversionMap **transition_tupconv_maps;
 
 	/*
 	 * These variables are used to reduce overhead in textual COPY FROM.
@@ -1436,6 +1438,36 @@ 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->transition_capture =
+				MakeTransitionCaptureState(rel->trigdesc);
+
+			/*
+			 * If we are capturing transition tuples, they may need to be
+			 * converted from partition format back to partitioned table
+			 * format (this is only ever necessary if a BEFORE trigger
+			 * modifies the tuple).
+			 */
+			if (cstate->transition_capture != NULL)
+			{
+				int		i;
+
+				cstate->transition_tupconv_maps = (TupleConversionMap **)
+					palloc0(sizeof(TupleConversionMap *) *
+							cstate->num_partitions);
+				for (i = 0; i < cstate->num_partitions; ++i)
+				{
+					cstate->transition_tupconv_maps[i] =
+						convert_tuples_by_name(RelationGetDescr(cstate->partitions[i].ri_RelationDesc),
+											   RelationGetDescr(rel),
+											   gettext_noop("could not convert row type"));
+				}
+			}
 		}
 	}
 	else
@@ -2595,6 +2627,31 @@ CopyFrom(CopyState cstate)
 			 * We might need to convert from the parent rowtype to the
 			 * partition rowtype.
 			 */
+			if (cstate->transition_capture != NULL)
+			{
+				if (resultRelInfo->ri_TrigDesc &&
+					(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+					 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+				{
+					/*
+					 * If there are any BEFORE or INSTEAD triggers on the
+					 * partition, we'll have to be ready to convert their
+					 * result back to tuplestore format.
+					 */
+					cstate->transition_capture->tcs_original_insert_tuple = NULL;
+					cstate->transition_capture->tcs_map =
+						cstate->transition_tupconv_maps[leaf_part_index];
+				}
+				else
+				{
+					/*
+					 * Otherwise, just remember the original unconverted
+					 * tuple, to avoid a needless round trip conversion.
+					 */
+					cstate->transition_capture->tcs_original_insert_tuple = tuple;
+					cstate->transition_capture->tcs_map = NULL;
+				}
+			}
 			map = cstate->partition_tupconv_maps[leaf_part_index];
 			if (map)
 			{
@@ -2688,7 +2745,7 @@ CopyFrom(CopyState cstate)
 
 					/* AFTER ROW INSERT Triggers */
 					ExecARInsertTriggers(estate, resultRelInfo, tuple,
-										 recheckIndexes);
+										 recheckIndexes, cstate->transition_capture);
 
 					list_free(recheckIndexes);
 				}
@@ -2841,7 +2898,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 									  estate, false, NULL, NIL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
-								 recheckIndexes);
+								 recheckIndexes, NULL);
 			list_free(recheckIndexes);
 		}
 	}
@@ -2851,14 +2908,15 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 	 * anyway.
 	 */
 	else if (resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+			 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
 	{
 		for (i = 0; i < nBufferedTuples; i++)
 		{
 			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 fb961e46c4a..99a02a741f7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10924,6 +10924,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
@@ -11005,6 +11006,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);
 
@@ -13376,6 +13390,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);
 
@@ -13505,6 +13520,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 0271788bf99..d90f0474fa5 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,
+					  TransitionCaptureState *transition_capture);
 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 TransitionCaptureState 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 tcs_map or tcs_original_insert_tuple as appropriate when dealing
+ * with child tables.
+ */
+TransitionCaptureState *
+MakeTransitionCaptureState(TriggerDesc *trigdesc)
+{
+	TransitionCaptureState *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 = (TransitionCaptureState *)
+			palloc0(sizeof(TransitionCaptureState));
+		state->tcs_delete_old_table = trigdesc->trig_delete_old_table;
+		state->tcs_update_old_table = trigdesc->trig_update_old_table;
+		state->tcs_update_new_table = trigdesc->trig_update_new_table;
+		state->tcs_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,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture && trigdesc->trig_insert_new_table) ||
+		(transition_capture && transition_capture->tcs_insert_new_table))
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
-							  true, NULL, trigtuple, recheckIndexes, NULL);
+							  true, NULL, trigtuple,
+							  recheckIndexes, NULL,
+							  transition_capture);
 }
 
 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,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture && trigdesc->trig_delete_old_table) ||
+		(transition_capture && transition_capture->tcs_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,
+							  transition_capture);
 		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,18 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
 					 HeapTuple fdw_trigtuple,
 					 HeapTuple newtuple,
-					 List *recheckIndexes)
+					 List *recheckIndexes,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture &&
+		 (trigdesc->trig_update_old_table ||
+		  trigdesc->trig_update_new_table)) ||
+		(transition_capture &&
+		 (transition_capture->tcs_update_old_table ||
+		  transition_capture->tcs_update_new_table)))
 	{
 		HeapTuple	trigtuple;
 
@@ -2757,7 +2845,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
 							  true, trigtuple, newtuple, recheckIndexes,
-							  GetUpdatedColumns(relinfo, estate));
+							  GetUpdatedColumns(relinfo, estate),
+							  transition_capture);
 		if (trigtuple != fdw_trigtuple)
 			heap_freetuple(trigtuple);
 	}
@@ -2888,7 +2977,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);
 }
 
 
@@ -5091,7 +5180,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,
+					  TransitionCaptureState *transition_capture)
 {
 	Relation	rel = relinfo->ri_RelationDesc;
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5121,10 +5211,49 @@ 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 (transition_capture != NULL)
+		{
+			/*
+			 * A TransitionCaptureState 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.  As a small optimization, we may receive the
+			 * original tuple from an insertion into a partitioned table to
+			 * avoid a wasteful parent->child->parent round trip.
+			 */
+			delete_old_table = transition_capture->tcs_delete_old_table;
+			update_old_table = transition_capture->tcs_update_old_table;
+			update_new_table = transition_capture->tcs_update_new_table;
+			insert_new_table = transition_capture->tcs_insert_new_table;
+			map = transition_capture->tcs_map;
+			original_insert_tuple =
+				transition_capture->tcs_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
+			 * TriggerCaptureState 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;
 
@@ -5132,12 +5261,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			old_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.old_tuplestores);
-			tuplestore_puttuple(old_tuplestore, oldtup);
+			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;
 
@@ -5145,13 +5280,24 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			new_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.new_tuplestores);
-			tuplestore_puttuple(new_tuplestore, newtup);
+			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) ||
-		(event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) ||
-		 (event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_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 4a899f1eb56..7c82cda1431 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 c6a66b6195f..0a98fbfdf54 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 cf555fe78d9..6eb979f17da 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -317,6 +317,31 @@ ExecInsert(ModifyTableState *mtstate,
 		 * We might need to convert from the parent rowtype to the partition
 		 * rowtype.
 		 */
+		if (mtstate->mt_transition_capture != NULL)
+		{
+			if (resultRelInfo->ri_TrigDesc &&
+				(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+				 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+			{
+				/*
+				 * If there are any BEFORE or INSTEAD triggers on the
+				 * partition, we'll have to be ready to convert their result
+				 * back to tuplestore format.
+				 */
+				mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL;
+				mtstate->mt_transition_capture->tcs_map =
+					mtstate->mt_transition_tupconv_maps[leaf_part_index];
+			}
+			else
+			{
+				/*
+				 * Otherwise, just remember the original unconverted tuple, to
+				 * avoid a needless round trip conversion.
+				 */
+				mtstate->mt_transition_capture->tcs_original_insert_tuple = tuple;
+				mtstate->mt_transition_capture->tcs_map = NULL;
+			}
+		}
 		map = mtstate->mt_partition_tupconv_maps[leaf_part_index];
 		if (map)
 		{
@@ -571,7 +596,8 @@ ExecInsert(ModifyTableState *mtstate,
 	}
 
 	/* AFTER ROW INSERT Triggers */
-	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
+	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes,
+						 mtstate->mt_transition_capture);
 
 	list_free(recheckIndexes);
 
@@ -619,7 +645,8 @@ ExecInsert(ModifyTableState *mtstate,
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecDelete(ItemPointer tupleid,
+ExecDelete(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
@@ -796,7 +823,8 @@ ldelete:;
 		(estate->es_processed)++;
 
 	/* AFTER ROW DELETE Triggers */
-	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple);
+	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+						 mtstate->mt_transition_capture);
 
 	/* Process RETURNING if present */
 	if (resultRelInfo->ri_projectReturning)
@@ -877,7 +905,8 @@ ldelete:;
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecUpdate(ItemPointer tupleid,
+ExecUpdate(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
 		   TupleTableSlot *planSlot,
@@ -1105,7 +1134,8 @@ lreplace:;
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
-						 recheckIndexes);
+						 recheckIndexes,
+						 mtstate->mt_transition_capture);
 
 	list_free(recheckIndexes);
 
@@ -1312,7 +1342,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);
@@ -1359,20 +1389,31 @@ fireBSTriggers(ModifyTableState *node)
 }
 
 /*
- * Process AFTER EACH STATEMENT triggers
+ * Return the ResultRelInfo for which we will fire AFTER STATEMENT triggers.
+ * This is also the relation into whose tuple format all captured transition
+ * tuples must be converted.
  */
-static void
-fireASTriggers(ModifyTableState *node)
+static ResultRelInfo *
+getASTriggerResultRelInfo(ModifyTableState *node)
 {
-	ResultRelInfo *resultRelInfo = node->resultRelInfo;
-
 	/*
 	 * If the node modifies a partitioned table, we must fire its triggers.
 	 * Note that in that case, node->resultRelInfo points to the first leaf
 	 * partition, not the root table.
 	 */
 	if (node->rootResultRelInfo != NULL)
-		resultRelInfo = node->rootResultRelInfo;
+		return node->rootResultRelInfo;
+	else
+		return node->resultRelInfo;
+}
+
+/*
+ * Process AFTER EACH STATEMENT triggers
+ */
+static void
+fireASTriggers(ModifyTableState *node)
+{
+	ResultRelInfo *resultRelInfo = getASTriggerResultRelInfo(node);
 
 	switch (node->operation)
 	{
@@ -1394,6 +1435,72 @@ fireASTriggers(ModifyTableState *node)
 	}
 }
 
+/*
+ * Set up the state needed for collecting transition tuples for AFTER
+ * triggers.
+ */
+static void
+ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate)
+{
+	ResultRelInfo *targetRelInfo = getASTriggerResultRelInfo(mtstate);
+	int		i;
+
+	/* Check for transition tables on the directly targeted relation. */
+	mtstate->mt_transition_capture =
+		MakeTransitionCaptureState(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.
+	 */
+	if (mtstate->mt_transition_capture != NULL)
+	{
+		ResultRelInfo *resultRelInfos;
+		int		numResultRelInfos;
+
+		/* Find the set of partitions so that we can find their TupleDescs. */
+		if (mtstate->mt_partition_dispatch_info != NULL)
+		{
+			/*
+			 * For INSERT via partitioned table, so we need TupleDescs based
+			 * on the partition routing table.
+			 */
+			resultRelInfos = mtstate->mt_partitions;
+			numResultRelInfos = mtstate->mt_num_partitions;
+		}
+		else
+		{
+			/* Otherwise we need the ResultRelInfo for each subplan. */
+			resultRelInfos = mtstate->resultRelInfo;
+			numResultRelInfos = mtstate->mt_nplans;
+		}
+
+		/*
+		 * 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 *) * numResultRelInfos);
+		for (i = 0; i < numResultRelInfos; ++i)
+		{
+			mtstate->mt_transition_tupconv_maps[i] =
+				convert_tuples_by_name(RelationGetDescr(resultRelInfos[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.  (INSERT operations set it every time.)
+		 */
+		mtstate->mt_transition_capture->tcs_map =
+			mtstate->mt_transition_tupconv_maps[0];
+	}
+}
 
 /* ----------------------------------------------------------------
  *	   ExecModifyTable
@@ -1492,6 +1599,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_capture != NULL)
+				{
+					/* Prepare to convert transition tuples from this child. */
+					Assert(node->mt_transition_tupconv_maps != NULL);
+					node->mt_transition_capture->tcs_map =
+						node->mt_transition_tupconv_maps[node->mt_whichplan];
+				}
 				continue;
 			}
 			else
@@ -1602,11 +1716,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 +1902,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		mtstate->mt_partition_tuple_slot = partition_tuple_slot;
 	}
 
+	/* Build state for collecting transition tuples */
+	ExecSetupTransitionCaptureState(mtstate, estate);
+
 	/*
 	 * Initialize any WITH CHECK OPTION constraints if needed.
 	 */
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..736f5de67dd 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 TransitionCaptureState
+{
+	/*
+	 * Is there at least one trigger specifying each transition relation on
+	 * the relation explicitly named in the DML statement or COPY command?
+	 */
+	bool		tcs_delete_old_table;
+	bool		tcs_update_old_table;
+	bool		tcs_update_new_table;
+	bool		tcs_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 *tcs_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	tcs_original_insert_tuple;
+} TransitionCaptureState;
+
+/*
  * 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 TransitionCaptureState *MakeTransitionCaptureState(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,
+					 TransitionCaptureState *transition_capture);
 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,
+					 TransitionCaptureState *transition_capture);
 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,
+					 TransitionCaptureState *transition_capture);
 extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index d33392f3b55..fe2b0abb8a3 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -946,6 +946,10 @@ typedef struct ModifyTableState
 	TupleConversionMap **mt_partition_tupconv_maps;
 	/* Per partition tuple conversion map */
 	TupleTableSlot *mt_partition_tuple_slot;
+	struct TransitionCaptureState *mt_transition_capture;
+									/* controls transition table population */
+	TupleConversionMap **mt_transition_tupconv_maps;
+									/* Per plan/partition tuple conversion */
 } ModifyTableState;
 
 /* ----------------
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 0d560fb3eed..f38665c1450 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,248 @@ 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)
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (AAA,42), (BBB,42), (CCC,1066)
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_stmt_trig, old table = <NULL>, new table = (AAA,42), (BBB,42), (CCC,1234)
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+--
+-- 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 = (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>
+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..05e0c36179f 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,286 @@ 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
+\.
+
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+
+
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+AAA	42
+BBB	42
+CCC	234
+\.
+
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+
+--
+-- 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();
#45Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Thomas Munro (#44)

On 2017/05/20 9:01, Thomas Munro wrote:

On Sat, May 20, 2017 at 10:43 AM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

On Fri, May 19, 2017 at 6:35 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/05/19 15:16, Thomas Munro wrote:

Would TransitionCaptureState be a better name for this struct?

Yes. Although, losing the Trigger prefix might make it sound a bit
ambiguous though. Right above its definition, we have TriggerData. So,
maybe TriggerTransitionCaptureState or TriggerTransitionCaptureData or
TriggerTransitionData may be worth considering.

Ok, here's a version using TransitionCaptureState. Those other names
seem too long, and "TriggerTransition" is already in use so
"TriggerTransitionData" seems off the table. Having the word
"capture" in there seems good, since this is an object that controls
what we capture when we process a modify a set of tables. I hope
that's clear.

I agree. TransitionCaptureState sounds good.

Sent too soon. Several variables should also be renamed to make clear
they refer to the transition capture state in effect, instead of vague
names like 'transitions'. Sorry for the version churn.

Ah, I was kind of getting distracted by it earlier too; thanks.

Anyway, the patch looks good to me.

Thanks,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#46Noah Misch
noah@leadboat.com
In reply to: Kevin Grittner (#38)

This PostgreSQL 10 open item is past due for your status update. Kindly send
a status update within 24 hours, and include a date for your subsequent status
update. Refer to the policy on open item ownership:
/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#47Robert Haas
robertmhaas@gmail.com
In reply to: Noah Misch (#46)

On Mon, May 29, 2017 at 9:34 PM, Noah Misch <noah@leadboat.com> wrote:

This PostgreSQL 10 open item is past due for your status update. Kindly send
a status update within 24 hours, and include a date for your subsequent status
update. Refer to the policy on open item ownership:
/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

I spoke with Kevin about this at PGCon and asked him to have a look at
it. He agreed to do so, but did not specify a time frame, which seems
important.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#48Noah Misch
noah@leadboat.com
In reply to: Noah Misch (#46)

On Tue, May 30, 2017 at 01:34:33AM +0000, Noah Misch wrote:

This PostgreSQL 10 open item is past due for your status update. Kindly send
a status update within 24 hours, and include a date for your subsequent status
update. Refer to the policy on open item ownership:
/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

IMMEDIATE ATTENTION REQUIRED. This PostgreSQL 10 open item is long past due
for your status update. Please reacquaint yourself with the policy on open
item ownership[1]/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com and then reply immediately. If I do not hear from you by
2017-06-01 07:00 UTC, I will transfer this item to release management team
ownership without further notice.

[1]: /messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#49Kevin Grittner
kgrittn@gmail.com
In reply to: Noah Misch (#48)

On Wed, May 31, 2017 at 1:44 AM, Noah Misch <noah@leadboat.com> wrote:

IMMEDIATE ATTENTION REQUIRED.

I should be able to complete review and testing by Friday. If there
are problems I might not take action until Monday; otherwise I
should be able to do so on Friday.

--
Kevin Grittner
VMware vCenter Server
https://www.vmware.com/

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#50Noah Misch
noah@leadboat.com
In reply to: Kevin Grittner (#49)

On Wed, May 31, 2017 at 09:14:17AM -0500, Kevin Grittner wrote:

On Wed, May 31, 2017 at 1:44 AM, Noah Misch <noah@leadboat.com> wrote:

IMMEDIATE ATTENTION REQUIRED.

I should be able to complete review and testing by Friday. If there
are problems I might not take action until Monday; otherwise I
should be able to do so on Friday.

IMMEDIATE ATTENTION REQUIRED. This PostgreSQL 10 open item is again long past
due for your status update. Please reacquaint yourself with the policy on
open item ownership[1]/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com and then reply immediately. If I do not hear from you
by 2017-06-08 08:00 UTC, I will transfer this item to release management team
ownership without further notice.

[1]: /messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#51Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Amit Langote (#45)
1 attachment(s)

On Mon, May 22, 2017 at 5:51 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

On 2017/05/20 9:01, Thomas Munro wrote:

Sent too soon. Several variables should also be renamed to make clear
they refer to the transition capture state in effect, instead of vague
names like 'transitions'. Sorry for the version churn.

Ah, I was kind of getting distracted by it earlier too; thanks.

Anyway, the patch looks good to me.

[Adding Andrew Gierth]

Here is a rebased version of the patch to fix transition tables with
inheritance. Fixes a typo in an error message ("not support on
partitions" -> "... supported ..."), and changes regression test
triggers to be single-event (only one of INSERT, UPDATE or DELETE),
because a later patch will not allow multi-event triggers with TTs.

This is patch 1 of a stack of 3 patches addressing currently known
problems with transition tables.

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables-v10.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v10.patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index c5f7c758381..18efe6a9ed7 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -459,6 +459,20 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
   </para>
 
   <para>
+    Modifying a partitioned table or a table with inheritance children fires
+    statement-level triggers directly attached to that table, but not
+    statement-level triggers for its partitions or child tables.  In contrast,
+    row-level triggers are fired for all affected partitions or child tables.
+    If a statement-level trigger has been defined with transition relations
+    named by a <literal>REFERENCING</literal> clause, then before and after
+    images of rows are visible from all affected partitions or child tables.
+    In the case of inheritance children, the row images include only columns
+    that are present in the table that the trigger is attached to.  Currently,
+    row-level triggers with transition relations cannot be defined on
+    partitions or inheritance child tables.
+  </para>
+
+  <para>
    In <productname>PostgreSQL</productname> versions before 7.3, it was
    necessary to declare trigger functions as returning the placeholder
    type <type>opaque</>, rather than <type>trigger</>.  To support loading
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index e5fb52cfbf8..207ae84f3df 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 0a33c40c17f..1458c929347 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -171,6 +171,8 @@ typedef struct CopyStateData
 	ResultRelInfo *partitions;	/* Per partition result relation */
 	TupleConversionMap **partition_tupconv_maps;
 	TupleTableSlot *partition_tuple_slot;
+	TransitionCaptureState *transition_capture;
+	TupleConversionMap **transition_tupconv_maps;
 
 	/*
 	 * These variables are used to reduce overhead in textual COPY FROM.
@@ -1436,6 +1438,36 @@ 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->transition_capture =
+				MakeTransitionCaptureState(rel->trigdesc);
+
+			/*
+			 * If we are capturing transition tuples, they may need to be
+			 * converted from partition format back to partitioned table
+			 * format (this is only ever necessary if a BEFORE trigger
+			 * modifies the tuple).
+			 */
+			if (cstate->transition_capture != NULL)
+			{
+				int		i;
+
+				cstate->transition_tupconv_maps = (TupleConversionMap **)
+					palloc0(sizeof(TupleConversionMap *) *
+							cstate->num_partitions);
+				for (i = 0; i < cstate->num_partitions; ++i)
+				{
+					cstate->transition_tupconv_maps[i] =
+						convert_tuples_by_name(RelationGetDescr(cstate->partitions[i].ri_RelationDesc),
+											   RelationGetDescr(rel),
+											   gettext_noop("could not convert row type"));
+				}
+			}
 		}
 	}
 	else
@@ -2595,6 +2627,31 @@ CopyFrom(CopyState cstate)
 			 * We might need to convert from the parent rowtype to the
 			 * partition rowtype.
 			 */
+			if (cstate->transition_capture != NULL)
+			{
+				if (resultRelInfo->ri_TrigDesc &&
+					(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+					 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+				{
+					/*
+					 * If there are any BEFORE or INSTEAD triggers on the
+					 * partition, we'll have to be ready to convert their
+					 * result back to tuplestore format.
+					 */
+					cstate->transition_capture->tcs_original_insert_tuple = NULL;
+					cstate->transition_capture->tcs_map =
+						cstate->transition_tupconv_maps[leaf_part_index];
+				}
+				else
+				{
+					/*
+					 * Otherwise, just remember the original unconverted
+					 * tuple, to avoid a needless round trip conversion.
+					 */
+					cstate->transition_capture->tcs_original_insert_tuple = tuple;
+					cstate->transition_capture->tcs_map = NULL;
+				}
+			}
 			map = cstate->partition_tupconv_maps[leaf_part_index];
 			if (map)
 			{
@@ -2703,7 +2760,7 @@ CopyFrom(CopyState cstate)
 
 					/* AFTER ROW INSERT Triggers */
 					ExecARInsertTriggers(estate, resultRelInfo, tuple,
-										 recheckIndexes);
+										 recheckIndexes, cstate->transition_capture);
 
 					list_free(recheckIndexes);
 				}
@@ -2856,7 +2913,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 									  estate, false, NULL, NIL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
-								 recheckIndexes);
+								 recheckIndexes, NULL);
 			list_free(recheckIndexes);
 		}
 	}
@@ -2866,14 +2923,15 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 	 * anyway.
 	 */
 	else if (resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+			 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
 	{
 		for (i = 0; i < nBufferedTuples; i++)
 		{
 			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 b61fda9909d..6bcdffb81c1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10935,6 +10935,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
@@ -11016,6 +11017,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);
 
@@ -13417,6 +13431,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);
 
@@ -13546,6 +13561,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 0271788bf99..ac75ff8f8e4 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,
+					  TransitionCaptureState *transition_capture);
 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 supported 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 TransitionCaptureState 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 tcs_map or tcs_original_insert_tuple as appropriate when dealing
+ * with child tables.
+ */
+TransitionCaptureState *
+MakeTransitionCaptureState(TriggerDesc *trigdesc)
+{
+	TransitionCaptureState *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 = (TransitionCaptureState *)
+			palloc0(sizeof(TransitionCaptureState));
+		state->tcs_delete_old_table = trigdesc->trig_delete_old_table;
+		state->tcs_update_old_table = trigdesc->trig_update_old_table;
+		state->tcs_update_new_table = trigdesc->trig_update_new_table;
+		state->tcs_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,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture && trigdesc->trig_insert_new_table) ||
+		(transition_capture && transition_capture->tcs_insert_new_table))
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
-							  true, NULL, trigtuple, recheckIndexes, NULL);
+							  true, NULL, trigtuple,
+							  recheckIndexes, NULL,
+							  transition_capture);
 }
 
 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,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture && trigdesc->trig_delete_old_table) ||
+		(transition_capture && transition_capture->tcs_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,
+							  transition_capture);
 		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,18 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
 					 HeapTuple fdw_trigtuple,
 					 HeapTuple newtuple,
-					 List *recheckIndexes)
+					 List *recheckIndexes,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture &&
+		 (trigdesc->trig_update_old_table ||
+		  trigdesc->trig_update_new_table)) ||
+		(transition_capture &&
+		 (transition_capture->tcs_update_old_table ||
+		  transition_capture->tcs_update_new_table)))
 	{
 		HeapTuple	trigtuple;
 
@@ -2757,7 +2845,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
 							  true, trigtuple, newtuple, recheckIndexes,
-							  GetUpdatedColumns(relinfo, estate));
+							  GetUpdatedColumns(relinfo, estate),
+							  transition_capture);
 		if (trigtuple != fdw_trigtuple)
 			heap_freetuple(trigtuple);
 	}
@@ -2888,7 +2977,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);
 }
 
 
@@ -5091,7 +5180,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,
+					  TransitionCaptureState *transition_capture)
 {
 	Relation	rel = relinfo->ri_RelationDesc;
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5121,10 +5211,49 @@ 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 (transition_capture != NULL)
+		{
+			/*
+			 * A TransitionCaptureState 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.  As a small optimization, we may receive the
+			 * original tuple from an insertion into a partitioned table to
+			 * avoid a wasteful parent->child->parent round trip.
+			 */
+			delete_old_table = transition_capture->tcs_delete_old_table;
+			update_old_table = transition_capture->tcs_update_old_table;
+			update_new_table = transition_capture->tcs_update_new_table;
+			insert_new_table = transition_capture->tcs_insert_new_table;
+			map = transition_capture->tcs_map;
+			original_insert_tuple =
+				transition_capture->tcs_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
+			 * TriggerCaptureState 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;
 
@@ -5132,12 +5261,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			old_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.old_tuplestores);
-			tuplestore_puttuple(old_tuplestore, oldtup);
+			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;
 
@@ -5145,13 +5280,24 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			new_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.new_tuplestores);
-			tuplestore_puttuple(new_tuplestore, newtup);
+			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) ||
-		(event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) ||
-		 (event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_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 3caeeac708c..ee940cdf14f 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -3198,7 +3198,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 c6a66b6195f..0a98fbfdf54 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 bf26488c510..6c86abca75d 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -317,6 +317,31 @@ ExecInsert(ModifyTableState *mtstate,
 		 * We might need to convert from the parent rowtype to the partition
 		 * rowtype.
 		 */
+		if (mtstate->mt_transition_capture != NULL)
+		{
+			if (resultRelInfo->ri_TrigDesc &&
+				(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+				 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+			{
+				/*
+				 * If there are any BEFORE or INSTEAD triggers on the
+				 * partition, we'll have to be ready to convert their result
+				 * back to tuplestore format.
+				 */
+				mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL;
+				mtstate->mt_transition_capture->tcs_map =
+					mtstate->mt_transition_tupconv_maps[leaf_part_index];
+			}
+			else
+			{
+				/*
+				 * Otherwise, just remember the original unconverted tuple, to
+				 * avoid a needless round trip conversion.
+				 */
+				mtstate->mt_transition_capture->tcs_original_insert_tuple = tuple;
+				mtstate->mt_transition_capture->tcs_map = NULL;
+			}
+		}
 		map = mtstate->mt_partition_tupconv_maps[leaf_part_index];
 		if (map)
 		{
@@ -588,7 +613,8 @@ ExecInsert(ModifyTableState *mtstate,
 	}
 
 	/* AFTER ROW INSERT Triggers */
-	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
+	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes,
+						 mtstate->mt_transition_capture);
 
 	list_free(recheckIndexes);
 
@@ -636,7 +662,8 @@ ExecInsert(ModifyTableState *mtstate,
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecDelete(ItemPointer tupleid,
+ExecDelete(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
@@ -813,7 +840,8 @@ ldelete:;
 		(estate->es_processed)++;
 
 	/* AFTER ROW DELETE Triggers */
-	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple);
+	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+						 mtstate->mt_transition_capture);
 
 	/* Process RETURNING if present */
 	if (resultRelInfo->ri_projectReturning)
@@ -894,7 +922,8 @@ ldelete:;
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecUpdate(ItemPointer tupleid,
+ExecUpdate(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
 		   TupleTableSlot *planSlot,
@@ -1122,7 +1151,8 @@ lreplace:;
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
-						 recheckIndexes);
+						 recheckIndexes,
+						 mtstate->mt_transition_capture);
 
 	list_free(recheckIndexes);
 
@@ -1329,7 +1359,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);
@@ -1376,20 +1406,31 @@ fireBSTriggers(ModifyTableState *node)
 }
 
 /*
- * Process AFTER EACH STATEMENT triggers
+ * Return the ResultRelInfo for which we will fire AFTER STATEMENT triggers.
+ * This is also the relation into whose tuple format all captured transition
+ * tuples must be converted.
  */
-static void
-fireASTriggers(ModifyTableState *node)
+static ResultRelInfo *
+getASTriggerResultRelInfo(ModifyTableState *node)
 {
-	ResultRelInfo *resultRelInfo = node->resultRelInfo;
-
 	/*
 	 * If the node modifies a partitioned table, we must fire its triggers.
 	 * Note that in that case, node->resultRelInfo points to the first leaf
 	 * partition, not the root table.
 	 */
 	if (node->rootResultRelInfo != NULL)
-		resultRelInfo = node->rootResultRelInfo;
+		return node->rootResultRelInfo;
+	else
+		return node->resultRelInfo;
+}
+
+/*
+ * Process AFTER EACH STATEMENT triggers
+ */
+static void
+fireASTriggers(ModifyTableState *node)
+{
+	ResultRelInfo *resultRelInfo = getASTriggerResultRelInfo(node);
 
 	switch (node->operation)
 	{
@@ -1411,6 +1452,72 @@ fireASTriggers(ModifyTableState *node)
 	}
 }
 
+/*
+ * Set up the state needed for collecting transition tuples for AFTER
+ * triggers.
+ */
+static void
+ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate)
+{
+	ResultRelInfo *targetRelInfo = getASTriggerResultRelInfo(mtstate);
+	int		i;
+
+	/* Check for transition tables on the directly targeted relation. */
+	mtstate->mt_transition_capture =
+		MakeTransitionCaptureState(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.
+	 */
+	if (mtstate->mt_transition_capture != NULL)
+	{
+		ResultRelInfo *resultRelInfos;
+		int		numResultRelInfos;
+
+		/* Find the set of partitions so that we can find their TupleDescs. */
+		if (mtstate->mt_partition_dispatch_info != NULL)
+		{
+			/*
+			 * For INSERT via partitioned table, so we need TupleDescs based
+			 * on the partition routing table.
+			 */
+			resultRelInfos = mtstate->mt_partitions;
+			numResultRelInfos = mtstate->mt_num_partitions;
+		}
+		else
+		{
+			/* Otherwise we need the ResultRelInfo for each subplan. */
+			resultRelInfos = mtstate->resultRelInfo;
+			numResultRelInfos = mtstate->mt_nplans;
+		}
+
+		/*
+		 * 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 *) * numResultRelInfos);
+		for (i = 0; i < numResultRelInfos; ++i)
+		{
+			mtstate->mt_transition_tupconv_maps[i] =
+				convert_tuples_by_name(RelationGetDescr(resultRelInfos[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.  (INSERT operations set it every time.)
+		 */
+		mtstate->mt_transition_capture->tcs_map =
+			mtstate->mt_transition_tupconv_maps[0];
+	}
+}
 
 /* ----------------------------------------------------------------
  *	   ExecModifyTable
@@ -1509,6 +1616,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_capture != NULL)
+				{
+					/* Prepare to convert transition tuples from this child. */
+					Assert(node->mt_transition_tupconv_maps != NULL);
+					node->mt_transition_capture->tcs_map =
+						node->mt_transition_tupconv_maps[node->mt_whichplan];
+				}
 				continue;
 			}
 			else
@@ -1619,11 +1733,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:
@@ -1805,6 +1919,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		mtstate->mt_partition_tuple_slot = partition_tuple_slot;
 	}
 
+	/* Build state for collecting transition tuples */
+	ExecSetupTransitionCaptureState(mtstate, estate);
+
 	/*
 	 * Initialize any WITH CHECK OPTION constraints if needed.
 	 */
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..736f5de67dd 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 TransitionCaptureState
+{
+	/*
+	 * Is there at least one trigger specifying each transition relation on
+	 * the relation explicitly named in the DML statement or COPY command?
+	 */
+	bool		tcs_delete_old_table;
+	bool		tcs_update_old_table;
+	bool		tcs_update_new_table;
+	bool		tcs_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 *tcs_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	tcs_original_insert_tuple;
+} TransitionCaptureState;
+
+/*
  * 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 TransitionCaptureState *MakeTransitionCaptureState(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,
+					 TransitionCaptureState *transition_capture);
 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,
+					 TransitionCaptureState *transition_capture);
 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,
+					 TransitionCaptureState *transition_capture);
 extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index d33392f3b55..fe2b0abb8a3 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -946,6 +946,10 @@ typedef struct ModifyTableState
 	TupleConversionMap **mt_partition_tupconv_maps;
 	/* Per partition tuple conversion map */
 	TupleTableSlot *mt_partition_tuple_slot;
+	struct TransitionCaptureState *mt_transition_capture;
+									/* controls transition table population */
+	TupleConversionMap **mt_transition_tupconv_maps;
+									/* Per plan/partition tuple conversion */
 } ModifyTableState;
 
 /* ----------------
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 29b8adf1e23..995410f1aae 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -1793,31 +1793,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
 --
@@ -1922,3 +1897,304 @@ 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_insert() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, new table = %',
+                 TG_NAME,
+                 (select string_agg(new_table::text, ', ' order by a) from new_table);
+    return null;
+  end;
+$$;
+create or replace function dump_update() 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;
+$$;
+create or replace function dump_delete() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, old table = %',
+                 TG_NAME,
+                 (select string_agg(old_table::text, ', ' order by a) from old_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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values ('BBB', 42);
+NOTICE:  trigger = child2_insert_trig, new table = (BBB,42)
+insert into child3 values (42, 'CCC');
+NOTICE:  trigger = child3_insert_trig, new table = (42,CCC)
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+NOTICE:  trigger = parent_update_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_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43)
+-- insert into parent sees parent-format tuples
+insert into parent values ('AAA', 42);
+NOTICE:  trigger = parent_insert_trig, new table = (AAA,42)
+insert into parent values ('BBB', 42);
+NOTICE:  trigger = parent_insert_trig, new table = (BBB,42)
+insert into parent values ('CCC', 42);
+NOTICE:  trigger = parent_insert_trig, new table = (CCC,42)
+-- delete from children sees respective child-format tuples
+delete from child1;
+NOTICE:  trigger = child1_delete_trig, old table = (AAA,42)
+delete from child2;
+NOTICE:  trigger = child2_delete_trig, old table = (BBB,42)
+delete from child3;
+NOTICE:  trigger = child3_delete_trig, old table = (42,CCC)
+-- copy into parent sees parent-format tuples
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_insert_trig, 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_trig on child3;
+delete from parent;
+NOTICE:  trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42)
+-- 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_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42)
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+NOTICE:  trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1066)
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1234)
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+--
+-- 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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+ERROR:  ROW triggers with transition tables are not supported on partitions
+-- detaching it first works
+alter table parent detach partition child;
+create trigger child_row_trig
+  after insert on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+-- 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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values (42, 'BBB');
+NOTICE:  trigger = child2_insert_trig, new table = (42,BBB)
+insert into child3 values ('CCC', 42, 'foo');
+NOTICE:  trigger = child3_insert_trig, new table = (CCC,42,foo)
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+NOTICE:  trigger = parent_update_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_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43)
+-- reinsert values into children for next test...
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values (42, 'BBB');
+NOTICE:  trigger = child2_insert_trig, new table = (42,BBB)
+insert into child3 values ('CCC', 42, 'foo');
+NOTICE:  trigger = child3_insert_trig, new table = (CCC,42,foo)
+-- delete from children sees respective child-format tuples
+delete from child1;
+NOTICE:  trigger = child1_delete_trig, old table = (AAA,42)
+delete from child2;
+NOTICE:  trigger = child2_delete_trig, old table = (42,BBB)
+delete from child3;
+NOTICE:  trigger = child3_delete_trig, old table = (CCC,42,foo)
+-- 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_insert_trig, 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_trig on child3;
+delete from parent;
+NOTICE:  trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42)
+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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+-- 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_insert();
+drop function dump_update();
+drop function dump_delete();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 9f2ed88f209..683a5f1e5c4 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1273,30 +1273,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
 --
@@ -1391,3 +1367,344 @@ 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_insert() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, new table = %',
+                 TG_NAME,
+                 (select string_agg(new_table::text, ', ' order by a) from new_table);
+    return null;
+  end;
+$$;
+
+create or replace function dump_update() 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;
+$$;
+
+create or replace function dump_delete() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, old table = %',
+                 TG_NAME,
+                 (select string_agg(old_table::text, ', ' order by a) from old_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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+-- 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_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
+\.
+
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+
+
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+AAA	42
+BBB	42
+CCC	234
+\.
+
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+
+--
+-- 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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- detaching it first works
+alter table parent detach partition child;
+
+create trigger child_row_trig
+  after insert on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- 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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+-- 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- disinheriting it first works
+alter table child no inherit parent;
+
+create trigger child_row_trig
+  after insert on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- 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_insert();
+drop function dump_update();
+drop function dump_delete();
#52Robert Haas
robertmhaas@gmail.com
In reply to: Thomas Munro (#51)

On Thu, Jun 8, 2017 at 11:56 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

[Adding Andrew Gierth]

Here is a rebased version of the patch to fix transition tables with
inheritance. Fixes a typo in an error message ("not support on
partitions" -> "... supported ..."), and changes regression test
triggers to be single-event (only one of INSERT, UPDATE or DELETE),
because a later patch will not allow multi-event triggers with TTs.

This is patch 1 of a stack of 3 patches addressing currently known
problems with transition tables.

So, Andrew, are you running with this, or should I keep looking into it?

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#53Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#52)

On Fri, Jun 9, 2017 at 12:19 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, Jun 8, 2017 at 11:56 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

[Adding Andrew Gierth]

Here is a rebased version of the patch to fix transition tables with
inheritance. Fixes a typo in an error message ("not support on
partitions" -> "... supported ..."), and changes regression test
triggers to be single-event (only one of INSERT, UPDATE or DELETE),
because a later patch will not allow multi-event triggers with TTs.

This is patch 1 of a stack of 3 patches addressing currently known
problems with transition tables.

So, Andrew, are you running with this, or should I keep looking into it?

I have spent some time now studying this patch. I might be missing
something, but to me this appears to be in great shape. A few minor
nitpicks:

-        if ((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))
+        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))

I suspect the whitespace changes will get reverted by pgindent, making
them pointless. But that's a trivial issue.

+        if (mtstate->mt_transition_capture != NULL)
+        {
+            if (resultRelInfo->ri_TrigDesc &&
+                (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+                 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+            {
+                /*
+                 * If there are any BEFORE or INSTEAD triggers on the
+                 * partition, we'll have to be ready to convert their result
+                 * back to tuplestore format.
+                 */
+
mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL;
+                mtstate->mt_transition_capture->tcs_map =
+                    mtstate->mt_transition_tupconv_maps[leaf_part_index];
+            }
+            else
+            {
+                /*
+                 * Otherwise, just remember the original unconverted tuple, to
+                 * avoid a needless round trip conversion.
+                 */
+
mtstate->mt_transition_capture->tcs_original_insert_tuple = tuple;
+                mtstate->mt_transition_capture->tcs_map = NULL;
+            }
+        }

This chunk of code gets inserted between a comment and the code to
which that comment refers. Maybe that's not the best idea.

Some other things I thought about:

* Does has_superclass have concurrency issues? After some
consideration, I decided that it's probably fine as long as you hold a
lock on the target relation sufficient to prevent it from concurrently
becoming an inheritance child - i.e. any lock at all. The caller is
CREATE TRIGGER, which certainly does.

* In AfterTriggerSaveEvent, is it a problem that the large new hunk of
code ignores trigdesc if transition_capture != NULL? If I understand
correctly, the trigdesc will be coming from the leaf relation actually
being updated, while the transition_capture will be coming from the
relation named in the query. Is the transition_capture object
guaranteed to have all the flags set, or do we also need to include
the ones from the trigdesc? This also seems to be fine, because of
the restriction that row-level triggers with tuplestores can't
participate in inheritance hierarchies. We can only need to capture
the tuples for the relation named in the query, not the leaf
partitions.

* The regression tests for this function are fairly lengthy. Given
the complexity of the behavior being tested, though, it seems like a
really good idea to have these. Otherwise, it's easy to imagine some
future patch breaking this again.

I also like the documentation update.

So, in short, +1 from me.

Regards,

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#54Andrew Gierth
andrew@tao11.riddles.org.uk
In reply to: Robert Haas (#52)
Re: transition table behavior with inheritance appears broken

"Robert" == Robert Haas <robertmhaas@gmail.com> writes:

Robert> So, Andrew, are you running with this, or should I keep looking
Robert> into it?

I have it; I will post a status update before 23:59 BST on 11 Jun.

--
Andrew (irc:RhodiumToad)

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#55Andrew Gierth
andrew@tao11.riddles.org.uk
In reply to: Andrew Gierth (#54)
Re: transition table behavior with inheritance appears broken

"Andrew" == Andrew Gierth <andrew@tao11.riddles.org.uk> writes:

Andrew> I have it; I will post a status update before 23:59 BST on 11
Andrew> Jun.

This is that status update. I am still studying Thomas' latest patch
set; as I mentioned in another message, I've confirmed a memory leak,
and I expect further work may be needed in some other areas as well, but
I think we're still making progress towards fixing it and I will work
with Thomas on it.

I will post a further status update before 23:59 BST on 14th Jun.

--
Andrew (irc:RhodiumToad)

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#56Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Robert Haas (#53)
1 attachment(s)

On Sat, Jun 10, 2017 at 6:08 AM, Robert Haas <robertmhaas@gmail.com> wrote:

I have spent some time now studying this patch. I might be missing
something, but to me this appears to be in great shape. A few minor
nitpicks:

-        if ((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))
+        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))

I suspect the whitespace changes will get reverted by pgindent, making
them pointless. But that's a trivial issue.

Not just a whitespace change: added "trigdesc == NULL" and put the
existing stuff into parens, necessarily changing indentation level.

+        if (mtstate->mt_transition_capture != NULL)
+        {
+            if (resultRelInfo->ri_TrigDesc &&
+                (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+                 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+            {
+                /*
+                 * If there are any BEFORE or INSTEAD triggers on the
+                 * partition, we'll have to be ready to convert their result
+                 * back to tuplestore format.
+                 */
+
mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL;
+                mtstate->mt_transition_capture->tcs_map =
+                    mtstate->mt_transition_tupconv_maps[leaf_part_index];
+            }
+            else
+            {
+                /*
+                 * Otherwise, just remember the original unconverted tuple, to
+                 * avoid a needless round trip conversion.
+                 */
+
mtstate->mt_transition_capture->tcs_original_insert_tuple = tuple;
+                mtstate->mt_transition_capture->tcs_map = NULL;
+            }
+        }

This chunk of code gets inserted between a comment and the code to
which that comment refers. Maybe that's not the best idea.

Fixed. Similar code existed in copy.c, so fixed there too.

* Does has_superclass have concurrency issues? After some
consideration, I decided that it's probably fine as long as you hold a
lock on the target relation sufficient to prevent it from concurrently
becoming an inheritance child - i.e. any lock at all. The caller is
CREATE TRIGGER, which certainly does.

I added a comment to make that clear.

* In AfterTriggerSaveEvent, is it a problem that the large new hunk of
code ignores trigdesc if transition_capture != NULL? If I understand
correctly, the trigdesc will be coming from the leaf relation actually
being updated, while the transition_capture will be coming from the
relation named in the query. Is the transition_capture object
guaranteed to have all the flags set, or do we also need to include
the ones from the trigdesc? This also seems to be fine, because of
the restriction that row-level triggers with tuplestores can't
participate in inheritance hierarchies. We can only need to capture
the tuples for the relation named in the query, not the leaf
partitions.

Yeah. I think that's right.

Note that in this patch I was trying to cater to execReplication.c so
that it wouldn't have to construct a TransitionCaptureState. It
doesn't actually support hierarchies (it doesn't operate on
partitioned tables, and doesn't have the smarts to direct updates to
traditional inheritance children), and doesn't fire statement
triggers.

In the #2 patch in the other thread about wCTEs, I changed this to
make a TransitionCaptureState object the *only* way to cause
transition tuple capture, because in that patch it owns the
tuplestores without which you can't capture anything. So in that
patch trigdesc is no longer relevant at all for the tuple capture.
Someone extending execReplication.c to support partitions and
statement triggers etc will need to think about creating a
TransitionCaptureState but I decided that was out of scope for the
transition table rescue project. In the new version of the #2 patch
that I'm about to post on the other there there is now a comment in
execReplication.c to explain.

So, in short, +1 from me.

Thanks for the review. New version of patch #1 attached.

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables-v11.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v11.patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index c5f7c758381..18efe6a9ed7 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -459,6 +459,20 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
   </para>
 
   <para>
+    Modifying a partitioned table or a table with inheritance children fires
+    statement-level triggers directly attached to that table, but not
+    statement-level triggers for its partitions or child tables.  In contrast,
+    row-level triggers are fired for all affected partitions or child tables.
+    If a statement-level trigger has been defined with transition relations
+    named by a <literal>REFERENCING</literal> clause, then before and after
+    images of rows are visible from all affected partitions or child tables.
+    In the case of inheritance children, the row images include only columns
+    that are present in the table that the trigger is attached to.  Currently,
+    row-level triggers with transition relations cannot be defined on
+    partitions or inheritance child tables.
+  </para>
+
+  <para>
    In <productname>PostgreSQL</productname> versions before 7.3, it was
    necessary to declare trigger functions as returning the placeholder
    type <type>opaque</>, rather than <type>trigger</>.  To support loading
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index e5fb52cfbf8..245a374fc91 100644
--- a/src/backend/catalog/pg_inherits.c
+++ b/src/backend/catalog/pg_inherits.c
@@ -273,6 +273,30 @@ has_subclass(Oid relationId)
 	return result;
 }
 
+/*
+ * has_superclass - does this relation inherit from another?  The caller
+ * should hold a lock on the given relation so that it can't be concurrently
+ * added to or removed from an inheritance hierarchy.
+ */
+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 0a33c40c17f..5e02b5feba5 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -171,6 +171,8 @@ typedef struct CopyStateData
 	ResultRelInfo *partitions;	/* Per partition result relation */
 	TupleConversionMap **partition_tupconv_maps;
 	TupleTableSlot *partition_tuple_slot;
+	TransitionCaptureState *transition_capture;
+	TupleConversionMap **transition_tupconv_maps;
 
 	/*
 	 * These variables are used to reduce overhead in textual COPY FROM.
@@ -1436,6 +1438,36 @@ 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->transition_capture =
+				MakeTransitionCaptureState(rel->trigdesc);
+
+			/*
+			 * If we are capturing transition tuples, they may need to be
+			 * converted from partition format back to partitioned table
+			 * format (this is only ever necessary if a BEFORE trigger
+			 * modifies the tuple).
+			 */
+			if (cstate->transition_capture != NULL)
+			{
+				int		i;
+
+				cstate->transition_tupconv_maps = (TupleConversionMap **)
+					palloc0(sizeof(TupleConversionMap *) *
+							cstate->num_partitions);
+				for (i = 0; i < cstate->num_partitions; ++i)
+				{
+					cstate->transition_tupconv_maps[i] =
+						convert_tuples_by_name(RelationGetDescr(cstate->partitions[i].ri_RelationDesc),
+											   RelationGetDescr(rel),
+											   gettext_noop("could not convert row type"));
+				}
+			}
 		}
 	}
 	else
@@ -2592,6 +2624,35 @@ CopyFrom(CopyState cstate)
 			estate->es_result_relation_info = resultRelInfo;
 
 			/*
+			 * If we're capturing transition tuples, we might need to convert
+			 * from the partition rowtype to parent rowtype.
+			 */
+			if (cstate->transition_capture != NULL)
+			{
+				if (resultRelInfo->ri_TrigDesc &&
+					(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+					 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+				{
+					/*
+					 * If there are any BEFORE or INSTEAD triggers on the
+					 * partition, we'll have to be ready to convert their
+					 * result back to tuplestore format.
+					 */
+					cstate->transition_capture->tcs_original_insert_tuple = NULL;
+					cstate->transition_capture->tcs_map =
+						cstate->transition_tupconv_maps[leaf_part_index];
+				}
+				else
+				{
+					/*
+					 * Otherwise, just remember the original unconverted
+					 * tuple, to avoid a needless round trip conversion.
+					 */
+					cstate->transition_capture->tcs_original_insert_tuple = tuple;
+					cstate->transition_capture->tcs_map = NULL;
+				}
+			}
+			/*
 			 * We might need to convert from the parent rowtype to the
 			 * partition rowtype.
 			 */
@@ -2703,7 +2764,7 @@ CopyFrom(CopyState cstate)
 
 					/* AFTER ROW INSERT Triggers */
 					ExecARInsertTriggers(estate, resultRelInfo, tuple,
-										 recheckIndexes);
+										 recheckIndexes, cstate->transition_capture);
 
 					list_free(recheckIndexes);
 				}
@@ -2856,7 +2917,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 									  estate, false, NULL, NIL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
-								 recheckIndexes);
+								 recheckIndexes, NULL);
 			list_free(recheckIndexes);
 		}
 	}
@@ -2866,14 +2927,15 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 	 * anyway.
 	 */
 	else if (resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+			 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
 	{
 		for (i = 0; i < nBufferedTuples; i++)
 		{
 			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 b61fda9909d..6bcdffb81c1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10935,6 +10935,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
@@ -11016,6 +11017,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);
 
@@ -13417,6 +13431,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);
 
@@ -13546,6 +13561,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 0271788bf99..ac75ff8f8e4 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,
+					  TransitionCaptureState *transition_capture);
 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 supported 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 TransitionCaptureState 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 tcs_map or tcs_original_insert_tuple as appropriate when dealing
+ * with child tables.
+ */
+TransitionCaptureState *
+MakeTransitionCaptureState(TriggerDesc *trigdesc)
+{
+	TransitionCaptureState *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 = (TransitionCaptureState *)
+			palloc0(sizeof(TransitionCaptureState));
+		state->tcs_delete_old_table = trigdesc->trig_delete_old_table;
+		state->tcs_update_old_table = trigdesc->trig_update_old_table;
+		state->tcs_update_new_table = trigdesc->trig_update_new_table;
+		state->tcs_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,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture && trigdesc->trig_insert_new_table) ||
+		(transition_capture && transition_capture->tcs_insert_new_table))
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
-							  true, NULL, trigtuple, recheckIndexes, NULL);
+							  true, NULL, trigtuple,
+							  recheckIndexes, NULL,
+							  transition_capture);
 }
 
 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,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture && trigdesc->trig_delete_old_table) ||
+		(transition_capture && transition_capture->tcs_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,
+							  transition_capture);
 		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,18 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
 					 HeapTuple fdw_trigtuple,
 					 HeapTuple newtuple,
-					 List *recheckIndexes)
+					 List *recheckIndexes,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture &&
+		 (trigdesc->trig_update_old_table ||
+		  trigdesc->trig_update_new_table)) ||
+		(transition_capture &&
+		 (transition_capture->tcs_update_old_table ||
+		  transition_capture->tcs_update_new_table)))
 	{
 		HeapTuple	trigtuple;
 
@@ -2757,7 +2845,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
 							  true, trigtuple, newtuple, recheckIndexes,
-							  GetUpdatedColumns(relinfo, estate));
+							  GetUpdatedColumns(relinfo, estate),
+							  transition_capture);
 		if (trigtuple != fdw_trigtuple)
 			heap_freetuple(trigtuple);
 	}
@@ -2888,7 +2977,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);
 }
 
 
@@ -5091,7 +5180,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,
+					  TransitionCaptureState *transition_capture)
 {
 	Relation	rel = relinfo->ri_RelationDesc;
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5121,10 +5211,49 @@ 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 (transition_capture != NULL)
+		{
+			/*
+			 * A TransitionCaptureState 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.  As a small optimization, we may receive the
+			 * original tuple from an insertion into a partitioned table to
+			 * avoid a wasteful parent->child->parent round trip.
+			 */
+			delete_old_table = transition_capture->tcs_delete_old_table;
+			update_old_table = transition_capture->tcs_update_old_table;
+			update_new_table = transition_capture->tcs_update_new_table;
+			insert_new_table = transition_capture->tcs_insert_new_table;
+			map = transition_capture->tcs_map;
+			original_insert_tuple =
+				transition_capture->tcs_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
+			 * TriggerCaptureState 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;
 
@@ -5132,12 +5261,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			old_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.old_tuplestores);
-			tuplestore_puttuple(old_tuplestore, oldtup);
+			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;
 
@@ -5145,13 +5280,24 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			new_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.new_tuplestores);
-			tuplestore_puttuple(new_tuplestore, newtup);
+			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) ||
-		(event == TRIGGER_EVENT_INSERT && !trigdesc->trig_insert_after_row) ||
-		 (event == TRIGGER_EVENT_UPDATE && !trigdesc->trig_update_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 3caeeac708c..ee940cdf14f 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -3198,7 +3198,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 c6a66b6195f..0a98fbfdf54 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 bf26488c510..ebfc8139978 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -314,6 +314,36 @@ ExecInsert(ModifyTableState *mtstate,
 		estate->es_result_relation_info = resultRelInfo;
 
 		/*
+		 * If we're capturing transition tuples, we might need to convert from
+		 * the partition rowtype to parent rowtype.
+		 */
+		if (mtstate->mt_transition_capture != NULL)
+		{
+			if (resultRelInfo->ri_TrigDesc &&
+				(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+				 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+			{
+				/*
+				 * If there are any BEFORE or INSTEAD triggers on the
+				 * partition, we'll have to be ready to convert their result
+				 * back to tuplestore format.
+				 */
+				mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL;
+				mtstate->mt_transition_capture->tcs_map =
+					mtstate->mt_transition_tupconv_maps[leaf_part_index];
+			}
+			else
+			{
+				/*
+				 * Otherwise, just remember the original unconverted tuple, to
+				 * avoid a needless round trip conversion.
+				 */
+				mtstate->mt_transition_capture->tcs_original_insert_tuple = tuple;
+				mtstate->mt_transition_capture->tcs_map = NULL;
+			}
+		}
+
+		/*
 		 * We might need to convert from the parent rowtype to the partition
 		 * rowtype.
 		 */
@@ -588,7 +618,8 @@ ExecInsert(ModifyTableState *mtstate,
 	}
 
 	/* AFTER ROW INSERT Triggers */
-	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
+	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes,
+						 mtstate->mt_transition_capture);
 
 	list_free(recheckIndexes);
 
@@ -636,7 +667,8 @@ ExecInsert(ModifyTableState *mtstate,
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecDelete(ItemPointer tupleid,
+ExecDelete(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
@@ -813,7 +845,8 @@ ldelete:;
 		(estate->es_processed)++;
 
 	/* AFTER ROW DELETE Triggers */
-	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple);
+	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+						 mtstate->mt_transition_capture);
 
 	/* Process RETURNING if present */
 	if (resultRelInfo->ri_projectReturning)
@@ -894,7 +927,8 @@ ldelete:;
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecUpdate(ItemPointer tupleid,
+ExecUpdate(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
 		   TupleTableSlot *planSlot,
@@ -1122,7 +1156,8 @@ lreplace:;
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
-						 recheckIndexes);
+						 recheckIndexes,
+						 mtstate->mt_transition_capture);
 
 	list_free(recheckIndexes);
 
@@ -1329,7 +1364,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);
@@ -1376,20 +1411,31 @@ fireBSTriggers(ModifyTableState *node)
 }
 
 /*
- * Process AFTER EACH STATEMENT triggers
+ * Return the ResultRelInfo for which we will fire AFTER STATEMENT triggers.
+ * This is also the relation into whose tuple format all captured transition
+ * tuples must be converted.
  */
-static void
-fireASTriggers(ModifyTableState *node)
+static ResultRelInfo *
+getASTriggerResultRelInfo(ModifyTableState *node)
 {
-	ResultRelInfo *resultRelInfo = node->resultRelInfo;
-
 	/*
 	 * If the node modifies a partitioned table, we must fire its triggers.
 	 * Note that in that case, node->resultRelInfo points to the first leaf
 	 * partition, not the root table.
 	 */
 	if (node->rootResultRelInfo != NULL)
-		resultRelInfo = node->rootResultRelInfo;
+		return node->rootResultRelInfo;
+	else
+		return node->resultRelInfo;
+}
+
+/*
+ * Process AFTER EACH STATEMENT triggers
+ */
+static void
+fireASTriggers(ModifyTableState *node)
+{
+	ResultRelInfo *resultRelInfo = getASTriggerResultRelInfo(node);
 
 	switch (node->operation)
 	{
@@ -1411,6 +1457,72 @@ fireASTriggers(ModifyTableState *node)
 	}
 }
 
+/*
+ * Set up the state needed for collecting transition tuples for AFTER
+ * triggers.
+ */
+static void
+ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate)
+{
+	ResultRelInfo *targetRelInfo = getASTriggerResultRelInfo(mtstate);
+	int		i;
+
+	/* Check for transition tables on the directly targeted relation. */
+	mtstate->mt_transition_capture =
+		MakeTransitionCaptureState(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.
+	 */
+	if (mtstate->mt_transition_capture != NULL)
+	{
+		ResultRelInfo *resultRelInfos;
+		int		numResultRelInfos;
+
+		/* Find the set of partitions so that we can find their TupleDescs. */
+		if (mtstate->mt_partition_dispatch_info != NULL)
+		{
+			/*
+			 * For INSERT via partitioned table, so we need TupleDescs based
+			 * on the partition routing table.
+			 */
+			resultRelInfos = mtstate->mt_partitions;
+			numResultRelInfos = mtstate->mt_num_partitions;
+		}
+		else
+		{
+			/* Otherwise we need the ResultRelInfo for each subplan. */
+			resultRelInfos = mtstate->resultRelInfo;
+			numResultRelInfos = mtstate->mt_nplans;
+		}
+
+		/*
+		 * 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 *) * numResultRelInfos);
+		for (i = 0; i < numResultRelInfos; ++i)
+		{
+			mtstate->mt_transition_tupconv_maps[i] =
+				convert_tuples_by_name(RelationGetDescr(resultRelInfos[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.  (INSERT operations set it every time.)
+		 */
+		mtstate->mt_transition_capture->tcs_map =
+			mtstate->mt_transition_tupconv_maps[0];
+	}
+}
 
 /* ----------------------------------------------------------------
  *	   ExecModifyTable
@@ -1509,6 +1621,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_capture != NULL)
+				{
+					/* Prepare to convert transition tuples from this child. */
+					Assert(node->mt_transition_tupconv_maps != NULL);
+					node->mt_transition_capture->tcs_map =
+						node->mt_transition_tupconv_maps[node->mt_whichplan];
+				}
 				continue;
 			}
 			else
@@ -1619,11 +1738,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:
@@ -1805,6 +1924,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		mtstate->mt_partition_tuple_slot = partition_tuple_slot;
 	}
 
+	/* Build state for collecting transition tuples */
+	ExecSetupTransitionCaptureState(mtstate, estate);
+
 	/*
 	 * Initialize any WITH CHECK OPTION constraints if needed.
 	 */
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..736f5de67dd 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 TransitionCaptureState
+{
+	/*
+	 * Is there at least one trigger specifying each transition relation on
+	 * the relation explicitly named in the DML statement or COPY command?
+	 */
+	bool		tcs_delete_old_table;
+	bool		tcs_update_old_table;
+	bool		tcs_update_new_table;
+	bool		tcs_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 *tcs_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	tcs_original_insert_tuple;
+} TransitionCaptureState;
+
+/*
  * 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 TransitionCaptureState *MakeTransitionCaptureState(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,
+					 TransitionCaptureState *transition_capture);
 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,
+					 TransitionCaptureState *transition_capture);
 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,
+					 TransitionCaptureState *transition_capture);
 extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index d33392f3b55..fe2b0abb8a3 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -946,6 +946,10 @@ typedef struct ModifyTableState
 	TupleConversionMap **mt_partition_tupconv_maps;
 	/* Per partition tuple conversion map */
 	TupleTableSlot *mt_partition_tuple_slot;
+	struct TransitionCaptureState *mt_transition_capture;
+									/* controls transition table population */
+	TupleConversionMap **mt_transition_tupconv_maps;
+									/* Per plan/partition tuple conversion */
 } ModifyTableState;
 
 /* ----------------
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 29b8adf1e23..995410f1aae 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -1793,31 +1793,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
 --
@@ -1922,3 +1897,304 @@ 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_insert() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, new table = %',
+                 TG_NAME,
+                 (select string_agg(new_table::text, ', ' order by a) from new_table);
+    return null;
+  end;
+$$;
+create or replace function dump_update() 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;
+$$;
+create or replace function dump_delete() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, old table = %',
+                 TG_NAME,
+                 (select string_agg(old_table::text, ', ' order by a) from old_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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values ('BBB', 42);
+NOTICE:  trigger = child2_insert_trig, new table = (BBB,42)
+insert into child3 values (42, 'CCC');
+NOTICE:  trigger = child3_insert_trig, new table = (42,CCC)
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+NOTICE:  trigger = parent_update_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_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43)
+-- insert into parent sees parent-format tuples
+insert into parent values ('AAA', 42);
+NOTICE:  trigger = parent_insert_trig, new table = (AAA,42)
+insert into parent values ('BBB', 42);
+NOTICE:  trigger = parent_insert_trig, new table = (BBB,42)
+insert into parent values ('CCC', 42);
+NOTICE:  trigger = parent_insert_trig, new table = (CCC,42)
+-- delete from children sees respective child-format tuples
+delete from child1;
+NOTICE:  trigger = child1_delete_trig, old table = (AAA,42)
+delete from child2;
+NOTICE:  trigger = child2_delete_trig, old table = (BBB,42)
+delete from child3;
+NOTICE:  trigger = child3_delete_trig, old table = (42,CCC)
+-- copy into parent sees parent-format tuples
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_insert_trig, 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_trig on child3;
+delete from parent;
+NOTICE:  trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42)
+-- 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_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42)
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+NOTICE:  trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1066)
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1234)
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+--
+-- 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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+ERROR:  ROW triggers with transition tables are not supported on partitions
+-- detaching it first works
+alter table parent detach partition child;
+create trigger child_row_trig
+  after insert on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+-- 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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values (42, 'BBB');
+NOTICE:  trigger = child2_insert_trig, new table = (42,BBB)
+insert into child3 values ('CCC', 42, 'foo');
+NOTICE:  trigger = child3_insert_trig, new table = (CCC,42,foo)
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+NOTICE:  trigger = parent_update_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_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43)
+-- reinsert values into children for next test...
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values (42, 'BBB');
+NOTICE:  trigger = child2_insert_trig, new table = (42,BBB)
+insert into child3 values ('CCC', 42, 'foo');
+NOTICE:  trigger = child3_insert_trig, new table = (CCC,42,foo)
+-- delete from children sees respective child-format tuples
+delete from child1;
+NOTICE:  trigger = child1_delete_trig, old table = (AAA,42)
+delete from child2;
+NOTICE:  trigger = child2_delete_trig, old table = (42,BBB)
+delete from child3;
+NOTICE:  trigger = child3_delete_trig, old table = (CCC,42,foo)
+-- 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_insert_trig, 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_trig on child3;
+delete from parent;
+NOTICE:  trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42)
+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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+-- 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_insert();
+drop function dump_update();
+drop function dump_delete();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 9f2ed88f209..683a5f1e5c4 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1273,30 +1273,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
 --
@@ -1391,3 +1367,344 @@ 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_insert() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, new table = %',
+                 TG_NAME,
+                 (select string_agg(new_table::text, ', ' order by a) from new_table);
+    return null;
+  end;
+$$;
+
+create or replace function dump_update() 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;
+$$;
+
+create or replace function dump_delete() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, old table = %',
+                 TG_NAME,
+                 (select string_agg(old_table::text, ', ' order by a) from old_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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+-- 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_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
+\.
+
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+
+
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+AAA	42
+BBB	42
+CCC	234
+\.
+
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+
+--
+-- 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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- detaching it first works
+alter table parent detach partition child;
+
+create trigger child_row_trig
+  after insert on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- 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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+-- 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- disinheriting it first works
+alter table child no inherit parent;
+
+create trigger child_row_trig
+  after insert on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- 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_insert();
+drop function dump_update();
+drop function dump_delete();
#57Andrew Gierth
andrew@tao11.riddles.org.uk
In reply to: Andrew Gierth (#55)
Re: transition table behavior with inheritance appears broken

"Andrew" == Andrew Gierth <andrew@tao11.riddles.org.uk> writes:

Andrew> I will post a further status update before 23:59 BST on 14th
Andrew> Jun.

Unfortunately I've been delayed over the past couple of days, but I have
Thomas' latest patchset in hand and will be working on it over the rest
of the week. Status update by 23:59 BST on Sun 18th, by which time I
hope to have everything finalized (all three issues, not just the
inheritance one).

--
Andrew (irc:RhodiumToad)

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#58Andrew Gierth
andrew@tao11.riddles.org.uk
In reply to: Andrew Gierth (#57)
Re: transition table behavior with inheritance appears broken

"Andrew" == Andrew Gierth <andrew@tao11.riddles.org.uk> writes:

Andrew> Unfortunately I've been delayed over the past couple of days,
Andrew> but I have Thomas' latest patchset in hand and will be working
Andrew> on it over the rest of the week. Status update by 23:59 BST on
Andrew> Sun 18th, by which time I hope to have everything finalized
Andrew> (all three issues, not just the inheritance one).

I have, I believe, completed my review of the patchset. My conclusion is
that the fix appears to be sound and I haven't been able to find any
further issues with it; so I think Thomas's patches should be committed
as-is. Unless anyone objects I will do this within the next few days.

(Any preferences for whether it should be one commit or 3 separate ones?)

--
Andrew (irc:RhodiumToad)

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#59Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Andrew Gierth (#58)
Re: transition table behavior with inheritance appears broken

On 2017/06/19 7:59, Andrew Gierth wrote:

"Andrew" == Andrew Gierth <andrew@tao11.riddles.org.uk> writes:

(Any preferences for whether it should be one commit or 3 separate ones?)

For my 2c, it would be nice to keep at least the inheritance one (or all
of them actually) separate.

Thanks,
Amit

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#60Robert Haas
robertmhaas@gmail.com
In reply to: Andrew Gierth (#58)
Re: transition table behavior with inheritance appears broken

On Sun, Jun 18, 2017 at 6:59 PM, Andrew Gierth
<andrew@tao11.riddles.org.uk> wrote:

(Any preferences for whether it should be one commit or 3 separate ones?)

If I were doing it, I would commit them separately.

But I'm not doing it, so I won't complain about what you decide to do.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#61Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Thomas Munro (#56)
1 attachment(s)

On Mon, Jun 12, 2017 at 2:03 PM, Thomas Munro
<thomas.munro@enterprisedb.com> wrote:

Thanks for the review. New version of patch #1 attached.

Here's a version rebased on top of the recently reindented master branch.

--
Thomas Munro
http://www.enterprisedb.com

Attachments:

transition-tuples-from-child-tables-v12.patchapplication/octet-stream; name=transition-tuples-from-child-tables-v12.patchDownload
diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index c5f7c758381..18efe6a9ed7 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -459,6 +459,20 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
   </para>
 
   <para>
+    Modifying a partitioned table or a table with inheritance children fires
+    statement-level triggers directly attached to that table, but not
+    statement-level triggers for its partitions or child tables.  In contrast,
+    row-level triggers are fired for all affected partitions or child tables.
+    If a statement-level trigger has been defined with transition relations
+    named by a <literal>REFERENCING</literal> clause, then before and after
+    images of rows are visible from all affected partitions or child tables.
+    In the case of inheritance children, the row images include only columns
+    that are present in the table that the trigger is attached to.  Currently,
+    row-level triggers with transition relations cannot be defined on
+    partitions or inheritance child tables.
+  </para>
+
+  <para>
    In <productname>PostgreSQL</productname> versions before 7.3, it was
    necessary to declare trigger functions as returning the placeholder
    type <type>opaque</>, rather than <type>trigger</>.  To support loading
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index e5fb52cfbf8..245a374fc91 100644
--- a/src/backend/catalog/pg_inherits.c
+++ b/src/backend/catalog/pg_inherits.c
@@ -273,6 +273,30 @@ has_subclass(Oid relationId)
 	return result;
 }
 
+/*
+ * has_superclass - does this relation inherit from another?  The caller
+ * should hold a lock on the given relation so that it can't be concurrently
+ * added to or removed from an inheritance hierarchy.
+ */
+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 3c399e29db5..a4c02e6b7c5 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -171,6 +171,8 @@ typedef struct CopyStateData
 	ResultRelInfo *partitions;	/* Per partition result relation */
 	TupleConversionMap **partition_tupconv_maps;
 	TupleTableSlot *partition_tuple_slot;
+	TransitionCaptureState *transition_capture;
+	TupleConversionMap **transition_tupconv_maps;
 
 	/*
 	 * These variables are used to reduce overhead in textual COPY FROM.
@@ -1436,6 +1438,36 @@ 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->transition_capture =
+				MakeTransitionCaptureState(rel->trigdesc);
+
+			/*
+			 * If we are capturing transition tuples, they may need to be
+			 * converted from partition format back to partitioned table
+			 * format (this is only ever necessary if a BEFORE trigger
+			 * modifies the tuple).
+			 */
+			if (cstate->transition_capture != NULL)
+			{
+				int		i;
+
+				cstate->transition_tupconv_maps = (TupleConversionMap **)
+					palloc0(sizeof(TupleConversionMap *) *
+							cstate->num_partitions);
+				for (i = 0; i < cstate->num_partitions; ++i)
+				{
+					cstate->transition_tupconv_maps[i] =
+						convert_tuples_by_name(RelationGetDescr(cstate->partitions[i].ri_RelationDesc),
+											   RelationGetDescr(rel),
+											   gettext_noop("could not convert row type"));
+				}
+			}
 		}
 	}
 	else
@@ -2592,6 +2624,35 @@ CopyFrom(CopyState cstate)
 			estate->es_result_relation_info = resultRelInfo;
 
 			/*
+			 * If we're capturing transition tuples, we might need to convert
+			 * from the partition rowtype to parent rowtype.
+			 */
+			if (cstate->transition_capture != NULL)
+			{
+				if (resultRelInfo->ri_TrigDesc &&
+					(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+					 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+				{
+					/*
+					 * If there are any BEFORE or INSTEAD triggers on the
+					 * partition, we'll have to be ready to convert their
+					 * result back to tuplestore format.
+					 */
+					cstate->transition_capture->tcs_original_insert_tuple = NULL;
+					cstate->transition_capture->tcs_map =
+						cstate->transition_tupconv_maps[leaf_part_index];
+				}
+				else
+				{
+					/*
+					 * Otherwise, just remember the original unconverted
+					 * tuple, to avoid a needless round trip conversion.
+					 */
+					cstate->transition_capture->tcs_original_insert_tuple = tuple;
+					cstate->transition_capture->tcs_map = NULL;
+				}
+			}
+			/*
 			 * We might need to convert from the parent rowtype to the
 			 * partition rowtype.
 			 */
@@ -2703,7 +2764,7 @@ CopyFrom(CopyState cstate)
 
 					/* AFTER ROW INSERT Triggers */
 					ExecARInsertTriggers(estate, resultRelInfo, tuple,
-										 recheckIndexes);
+										 recheckIndexes, cstate->transition_capture);
 
 					list_free(recheckIndexes);
 				}
@@ -2856,7 +2917,7 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 									  estate, false, NULL, NIL);
 			ExecARInsertTriggers(estate, resultRelInfo,
 								 bufferedTuples[i],
-								 recheckIndexes);
+								 recheckIndexes, NULL);
 			list_free(recheckIndexes);
 		}
 	}
@@ -2866,14 +2927,15 @@ CopyFromInsertBatch(CopyState cstate, EState *estate, CommandId mycid,
 	 * anyway.
 	 */
 	else if (resultRelInfo->ri_TrigDesc != NULL &&
-			 resultRelInfo->ri_TrigDesc->trig_insert_after_row)
+			 (resultRelInfo->ri_TrigDesc->trig_insert_after_row ||
+			  resultRelInfo->ri_TrigDesc->trig_insert_new_table))
 	{
 		for (i = 0; i < nBufferedTuples; i++)
 		{
 			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 7d9c769b062..bb00858ad13 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -10933,6 +10933,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
@@ -11014,6 +11015,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);
 
@@ -13418,6 +13432,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);
 
@@ -13547,6 +13562,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 45d1f515eb9..f902e0cdf5f 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,
+					  TransitionCaptureState *transition_capture);
 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 supported 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 TransitionCaptureState 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 tcs_map or tcs_original_insert_tuple as appropriate when dealing
+ * with child tables.
+ */
+TransitionCaptureState *
+MakeTransitionCaptureState(TriggerDesc *trigdesc)
+{
+	TransitionCaptureState *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 = (TransitionCaptureState *)
+			palloc0(sizeof(TransitionCaptureState));
+		state->tcs_delete_old_table = trigdesc->trig_delete_old_table;
+		state->tcs_update_old_table = trigdesc->trig_update_old_table;
+		state->tcs_update_new_table = trigdesc->trig_update_new_table;
+		state->tcs_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,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture && trigdesc->trig_insert_new_table) ||
+		(transition_capture && transition_capture->tcs_insert_new_table))
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_INSERT,
-							  true, NULL, trigtuple, recheckIndexes, NULL);
+							  true, NULL, trigtuple,
+							  recheckIndexes, NULL,
+							  transition_capture);
 }
 
 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,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture && trigdesc->trig_delete_old_table) ||
+		(transition_capture && transition_capture->tcs_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,
+							  transition_capture);
 		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,18 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 					 ItemPointer tupleid,
 					 HeapTuple fdw_trigtuple,
 					 HeapTuple newtuple,
-					 List *recheckIndexes)
+					 List *recheckIndexes,
+					 TransitionCaptureState *transition_capture)
 {
 	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 && !transition_capture &&
+		 (trigdesc->trig_update_old_table ||
+		  trigdesc->trig_update_new_table)) ||
+		(transition_capture &&
+		 (transition_capture->tcs_update_old_table ||
+		  transition_capture->tcs_update_new_table)))
 	{
 		HeapTuple	trigtuple;
 
@@ -2757,7 +2845,8 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
 
 		AfterTriggerSaveEvent(estate, relinfo, TRIGGER_EVENT_UPDATE,
 							  true, trigtuple, newtuple, recheckIndexes,
-							  GetUpdatedColumns(relinfo, estate));
+							  GetUpdatedColumns(relinfo, estate),
+							  transition_capture);
 		if (trigtuple != fdw_trigtuple)
 			heap_freetuple(trigtuple);
 	}
@@ -2888,7 +2977,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 +5179,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,
+					  TransitionCaptureState *transition_capture)
 {
 	Relation	rel = relinfo->ri_RelationDesc;
 	TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -5120,10 +5210,49 @@ 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 (transition_capture != NULL)
+		{
+			/*
+			 * A TransitionCaptureState 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.  As a small optimization, we may receive the
+			 * original tuple from an insertion into a partitioned table to
+			 * avoid a wasteful parent->child->parent round trip.
+			 */
+			delete_old_table = transition_capture->tcs_delete_old_table;
+			update_old_table = transition_capture->tcs_update_old_table;
+			update_new_table = transition_capture->tcs_update_new_table;
+			insert_new_table = transition_capture->tcs_insert_new_table;
+			map = transition_capture->tcs_map;
+			original_insert_tuple =
+				transition_capture->tcs_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
+			 * TriggerCaptureState 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;
 
@@ -5131,12 +5260,18 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			old_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.old_tuplestores);
-			tuplestore_puttuple(old_tuplestore, oldtup);
+			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;
 
@@ -5144,11 +5279,22 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
 			new_tuplestore =
 				GetTriggerTransitionTuplestore
 				(afterTriggers.new_tuplestores);
-			tuplestore_puttuple(new_tuplestore, newtup);
+			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 7f0d21f5166..0f08283f81f 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -3198,7 +3198,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 6dae79a8f00..d1edd38eccd 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 5e43a069426..f2534f20622 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -314,6 +314,36 @@ ExecInsert(ModifyTableState *mtstate,
 		estate->es_result_relation_info = resultRelInfo;
 
 		/*
+		 * If we're capturing transition tuples, we might need to convert from
+		 * the partition rowtype to parent rowtype.
+		 */
+		if (mtstate->mt_transition_capture != NULL)
+		{
+			if (resultRelInfo->ri_TrigDesc &&
+				(resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
+				 resultRelInfo->ri_TrigDesc->trig_insert_instead_row))
+			{
+				/*
+				 * If there are any BEFORE or INSTEAD triggers on the
+				 * partition, we'll have to be ready to convert their result
+				 * back to tuplestore format.
+				 */
+				mtstate->mt_transition_capture->tcs_original_insert_tuple = NULL;
+				mtstate->mt_transition_capture->tcs_map =
+					mtstate->mt_transition_tupconv_maps[leaf_part_index];
+			}
+			else
+			{
+				/*
+				 * Otherwise, just remember the original unconverted tuple, to
+				 * avoid a needless round trip conversion.
+				 */
+				mtstate->mt_transition_capture->tcs_original_insert_tuple = tuple;
+				mtstate->mt_transition_capture->tcs_map = NULL;
+			}
+		}
+
+		/*
 		 * We might need to convert from the parent rowtype to the partition
 		 * rowtype.
 		 */
@@ -588,7 +618,8 @@ ExecInsert(ModifyTableState *mtstate,
 	}
 
 	/* AFTER ROW INSERT Triggers */
-	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes);
+	ExecARInsertTriggers(estate, resultRelInfo, tuple, recheckIndexes,
+						 mtstate->mt_transition_capture);
 
 	list_free(recheckIndexes);
 
@@ -636,7 +667,8 @@ ExecInsert(ModifyTableState *mtstate,
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecDelete(ItemPointer tupleid,
+ExecDelete(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
@@ -813,7 +845,8 @@ ldelete:;
 		(estate->es_processed)++;
 
 	/* AFTER ROW DELETE Triggers */
-	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple);
+	ExecARDeleteTriggers(estate, resultRelInfo, tupleid, oldtuple,
+						 mtstate->mt_transition_capture);
 
 	/* Process RETURNING if present */
 	if (resultRelInfo->ri_projectReturning)
@@ -894,7 +927,8 @@ ldelete:;
  * ----------------------------------------------------------------
  */
 static TupleTableSlot *
-ExecUpdate(ItemPointer tupleid,
+ExecUpdate(ModifyTableState *mtstate,
+		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
 		   TupleTableSlot *planSlot,
@@ -1122,7 +1156,8 @@ lreplace:;
 
 	/* AFTER ROW UPDATE Triggers */
 	ExecARUpdateTriggers(estate, resultRelInfo, tupleid, oldtuple, tuple,
-						 recheckIndexes);
+						 recheckIndexes,
+						 mtstate->mt_transition_capture);
 
 	list_free(recheckIndexes);
 
@@ -1329,7 +1364,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);
@@ -1376,20 +1411,31 @@ fireBSTriggers(ModifyTableState *node)
 }
 
 /*
- * Process AFTER EACH STATEMENT triggers
+ * Return the ResultRelInfo for which we will fire AFTER STATEMENT triggers.
+ * This is also the relation into whose tuple format all captured transition
+ * tuples must be converted.
  */
-static void
-fireASTriggers(ModifyTableState *node)
+static ResultRelInfo *
+getASTriggerResultRelInfo(ModifyTableState *node)
 {
-	ResultRelInfo *resultRelInfo = node->resultRelInfo;
-
 	/*
 	 * If the node modifies a partitioned table, we must fire its triggers.
 	 * Note that in that case, node->resultRelInfo points to the first leaf
 	 * partition, not the root table.
 	 */
 	if (node->rootResultRelInfo != NULL)
-		resultRelInfo = node->rootResultRelInfo;
+		return node->rootResultRelInfo;
+	else
+		return node->resultRelInfo;
+}
+
+/*
+ * Process AFTER EACH STATEMENT triggers
+ */
+static void
+fireASTriggers(ModifyTableState *node)
+{
+	ResultRelInfo *resultRelInfo = getASTriggerResultRelInfo(node);
 
 	switch (node->operation)
 	{
@@ -1411,6 +1457,72 @@ fireASTriggers(ModifyTableState *node)
 	}
 }
 
+/*
+ * Set up the state needed for collecting transition tuples for AFTER
+ * triggers.
+ */
+static void
+ExecSetupTransitionCaptureState(ModifyTableState *mtstate, EState *estate)
+{
+	ResultRelInfo *targetRelInfo = getASTriggerResultRelInfo(mtstate);
+	int		i;
+
+	/* Check for transition tables on the directly targeted relation. */
+	mtstate->mt_transition_capture =
+		MakeTransitionCaptureState(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.
+	 */
+	if (mtstate->mt_transition_capture != NULL)
+	{
+		ResultRelInfo *resultRelInfos;
+		int		numResultRelInfos;
+
+		/* Find the set of partitions so that we can find their TupleDescs. */
+		if (mtstate->mt_partition_dispatch_info != NULL)
+		{
+			/*
+			 * For INSERT via partitioned table, so we need TupleDescs based
+			 * on the partition routing table.
+			 */
+			resultRelInfos = mtstate->mt_partitions;
+			numResultRelInfos = mtstate->mt_num_partitions;
+		}
+		else
+		{
+			/* Otherwise we need the ResultRelInfo for each subplan. */
+			resultRelInfos = mtstate->resultRelInfo;
+			numResultRelInfos = mtstate->mt_nplans;
+		}
+
+		/*
+		 * 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 *) * numResultRelInfos);
+		for (i = 0; i < numResultRelInfos; ++i)
+		{
+			mtstate->mt_transition_tupconv_maps[i] =
+				convert_tuples_by_name(RelationGetDescr(resultRelInfos[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.  (INSERT operations set it every time.)
+		 */
+		mtstate->mt_transition_capture->tcs_map =
+			mtstate->mt_transition_tupconv_maps[0];
+	}
+}
 
 /* ----------------------------------------------------------------
  *	   ExecModifyTable
@@ -1509,6 +1621,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_capture != NULL)
+				{
+					/* Prepare to convert transition tuples from this child. */
+					Assert(node->mt_transition_tupconv_maps != NULL);
+					node->mt_transition_capture->tcs_map =
+						node->mt_transition_tupconv_maps[node->mt_whichplan];
+				}
 				continue;
 			}
 			else
@@ -1618,11 +1737,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:
@@ -1804,6 +1923,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		mtstate->mt_partition_tuple_slot = partition_tuple_slot;
 	}
 
+	/* Build state for collecting transition tuples */
+	ExecSetupTransitionCaptureState(mtstate, estate);
+
 	/*
 	 * Initialize any WITH CHECK OPTION constraints if needed.
 	 */
diff --git a/src/include/catalog/pg_inherits_fn.h b/src/include/catalog/pg_inherits_fn.h
index abfa4766a12..77433888992 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 10ac724febf..51a25c8ddc2 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 TransitionCaptureState
+{
+	/*
+	 * Is there at least one trigger specifying each transition relation on
+	 * the relation explicitly named in the DML statement or COPY command?
+	 */
+	bool		tcs_delete_old_table;
+	bool		tcs_update_old_table;
+	bool		tcs_update_new_table;
+	bool		tcs_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 *tcs_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	tcs_original_insert_tuple;
+} TransitionCaptureState;
+
+/*
  * 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 TransitionCaptureState *MakeTransitionCaptureState(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,
+					 TransitionCaptureState *transition_capture);
 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,
+					 TransitionCaptureState *transition_capture);
 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,
+					 TransitionCaptureState *transition_capture);
 extern TupleTableSlot *ExecIRUpdateTriggers(EState *estate,
 					 ResultRelInfo *relinfo,
 					 HeapTuple trigtuple,
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 54c5cf5f95b..85fac8ab91b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -963,6 +963,10 @@ typedef struct ModifyTableState
 	TupleConversionMap **mt_partition_tupconv_maps;
 	/* Per partition tuple conversion map */
 	TupleTableSlot *mt_partition_tuple_slot;
+	struct TransitionCaptureState *mt_transition_capture;
+									/* controls transition table population */
+	TupleConversionMap **mt_transition_tupconv_maps;
+									/* Per plan/partition tuple conversion */
 } ModifyTableState;
 
 /* ----------------
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 29b8adf1e23..995410f1aae 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -1793,31 +1793,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
 --
@@ -1922,3 +1897,304 @@ 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_insert() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, new table = %',
+                 TG_NAME,
+                 (select string_agg(new_table::text, ', ' order by a) from new_table);
+    return null;
+  end;
+$$;
+create or replace function dump_update() 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;
+$$;
+create or replace function dump_delete() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, old table = %',
+                 TG_NAME,
+                 (select string_agg(old_table::text, ', ' order by a) from old_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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values ('BBB', 42);
+NOTICE:  trigger = child2_insert_trig, new table = (BBB,42)
+insert into child3 values (42, 'CCC');
+NOTICE:  trigger = child3_insert_trig, new table = (42,CCC)
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+NOTICE:  trigger = parent_update_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_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43)
+-- insert into parent sees parent-format tuples
+insert into parent values ('AAA', 42);
+NOTICE:  trigger = parent_insert_trig, new table = (AAA,42)
+insert into parent values ('BBB', 42);
+NOTICE:  trigger = parent_insert_trig, new table = (BBB,42)
+insert into parent values ('CCC', 42);
+NOTICE:  trigger = parent_insert_trig, new table = (CCC,42)
+-- delete from children sees respective child-format tuples
+delete from child1;
+NOTICE:  trigger = child1_delete_trig, old table = (AAA,42)
+delete from child2;
+NOTICE:  trigger = child2_delete_trig, old table = (BBB,42)
+delete from child3;
+NOTICE:  trigger = child3_delete_trig, old table = (42,CCC)
+-- copy into parent sees parent-format tuples
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_insert_trig, 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_trig on child3;
+delete from parent;
+NOTICE:  trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42)
+-- 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_insert_trig, new table = (AAA,42), (BBB,42), (CCC,42)
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+NOTICE:  trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1066)
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+NOTICE:  trigger = parent_insert_trig, new table = (AAA,42), (BBB,42), (CCC,1234)
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+--
+-- 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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+ERROR:  ROW triggers with transition tables are not supported on partitions
+-- detaching it first works
+alter table parent detach partition child;
+create trigger child_row_trig
+  after insert on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+-- 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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+-- insert directly into children sees respective child-format tuples
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values (42, 'BBB');
+NOTICE:  trigger = child2_insert_trig, new table = (42,BBB)
+insert into child3 values ('CCC', 42, 'foo');
+NOTICE:  trigger = child3_insert_trig, new table = (CCC,42,foo)
+-- update via parent sees parent-format tuples
+update parent set b = b + 1;
+NOTICE:  trigger = parent_update_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_delete_trig, old table = (AAA,43), (BBB,43), (CCC,43)
+-- reinsert values into children for next test...
+insert into child1 values ('AAA', 42);
+NOTICE:  trigger = child1_insert_trig, new table = (AAA,42)
+insert into child2 values (42, 'BBB');
+NOTICE:  trigger = child2_insert_trig, new table = (42,BBB)
+insert into child3 values ('CCC', 42, 'foo');
+NOTICE:  trigger = child3_insert_trig, new table = (CCC,42,foo)
+-- delete from children sees respective child-format tuples
+delete from child1;
+NOTICE:  trigger = child1_delete_trig, old table = (AAA,42)
+delete from child2;
+NOTICE:  trigger = child2_delete_trig, old table = (42,BBB)
+delete from child3;
+NOTICE:  trigger = child3_delete_trig, old table = (CCC,42,foo)
+-- 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_insert_trig, 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_trig on child3;
+delete from parent;
+NOTICE:  trigger = parent_delete_trig, old table = (AAA,42), (BBB,42), (CCC,42)
+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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+-- 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_insert();
+drop function dump_update();
+drop function dump_delete();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 9f2ed88f209..683a5f1e5c4 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1273,30 +1273,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
 --
@@ -1391,3 +1367,344 @@ 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_insert() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, new table = %',
+                 TG_NAME,
+                 (select string_agg(new_table::text, ', ' order by a) from new_table);
+    return null;
+  end;
+$$;
+
+create or replace function dump_update() 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;
+$$;
+
+create or replace function dump_delete() returns trigger language plpgsql as
+$$
+  begin
+    raise notice 'trigger = %, old table = %',
+                 TG_NAME,
+                 (select string_agg(old_table::text, ', ' order by a) from old_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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+-- 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_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
+\.
+
+-- insert into parent with a before trigger on a child tuple before
+-- insertion, and we capture the newly modified row in parent format
+create or replace function intercept_insert() returns trigger language plpgsql as
+$$
+  begin
+    new.b = new.b + 1000;
+    return new;
+  end;
+$$;
+
+create trigger intercept_insert_child3
+  before insert on child3
+  for each row execute procedure intercept_insert();
+
+
+-- insert, parent trigger sees post-modification parent-format tuple
+insert into parent values ('AAA', 42), ('BBB', 42), ('CCC', 66);
+
+-- copy, parent trigger sees post-modification parent-format tuple
+copy parent (a, b) from stdin;
+AAA	42
+BBB	42
+CCC	234
+\.
+
+drop table child1, child2, child3, parent;
+drop function intercept_insert();
+
+--
+-- 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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- detaching it first works
+alter table parent detach partition child;
+
+create trigger child_row_trig
+  after insert on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- 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_insert_trig
+  after insert on parent referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger parent_update_trig
+  after update on parent referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger parent_delete_trig
+  after delete on parent referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child1_insert_trig
+  after insert on child1 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child1_update_trig
+  after update on child1 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child1_delete_trig
+  after delete on child1 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child2_insert_trig
+  after insert on child2 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child2_update_trig
+  after update on child2 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child2_delete_trig
+  after delete on child2 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+create trigger child3_insert_trig
+  after insert on child3 referencing new table as new_table
+  for each statement execute procedure dump_insert();
+create trigger child3_update_trig
+  after update on child3 referencing old table as old_table new table as new_table
+  for each statement execute procedure dump_update();
+create trigger child3_delete_trig
+  after delete on child3 referencing old table as old_table
+  for each statement execute procedure dump_delete();
+
+-- 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_insert_trig on child1;
+drop trigger child1_update_trig on child1;
+drop trigger child1_delete_trig on child1;
+drop trigger child2_insert_trig on child2;
+drop trigger child2_update_trig on child2;
+drop trigger child2_delete_trig on child2;
+drop trigger child3_insert_trig on child3;
+drop trigger child3_update_trig on child3;
+drop trigger child3_delete_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 on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- disinheriting it first works
+alter table child no inherit parent;
+
+create trigger child_row_trig
+  after insert on child referencing new table as new_table
+  for each row execute procedure dump_insert();
+
+-- 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_insert();
+drop function dump_update();
+drop function dump_delete();
#62Noah Misch
noah@leadboat.com
In reply to: Andrew Gierth (#58)
Re: transition table behavior with inheritance appears broken

On Sun, Jun 18, 2017 at 11:59:43PM +0100, Andrew Gierth wrote:

"Andrew" == Andrew Gierth <andrew@tao11.riddles.org.uk> writes:

Andrew> Unfortunately I've been delayed over the past couple of days,
Andrew> but I have Thomas' latest patchset in hand and will be working
Andrew> on it over the rest of the week. Status update by 23:59 BST on
Andrew> Sun 18th, by which time I hope to have everything finalized
Andrew> (all three issues, not just the inheritance one).

I have, I believe, completed my review of the patchset. My conclusion is
that the fix appears to be sound and I haven't been able to find any
further issues with it; so I think Thomas's patches should be committed
as-is. Unless anyone objects I will do this within the next few days.

(Any preferences for whether it should be one commit or 3 separate ones?)

This PostgreSQL 10 open item is past due for your status update. Kindly send
a status update within 24 hours, and include a date for your subsequent status
update. Refer to the policy on open item ownership:
/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#63Andrew Gierth
andrew@tao11.riddles.org.uk
In reply to: Noah Misch (#62)
Re: transition table behavior with inheritance appears broken

"Noah" == Noah Misch <noah@leadboat.com> writes:

Noah> This PostgreSQL 10 open item is past due for your status update.
Noah> Kindly send a status update within 24 hours,

oops, sorry! I forgot to include a date in the last one, and in fact a
personal matter delayed things anyway. I expect to have this wrapped up
by 23:59 BST on the 24th.

--
Andrew.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#64Noah Misch
noah@leadboat.com
In reply to: Andrew Gierth (#63)
Re: transition table behavior with inheritance appears broken

On Fri, Jun 23, 2017 at 02:39:48PM +0100, Andrew Gierth wrote:

"Noah" == Noah Misch <noah@leadboat.com> writes:

Noah> This PostgreSQL 10 open item is past due for your status update.
Noah> Kindly send a status update within 24 hours,

oops, sorry! I forgot to include a date in the last one, and in fact a
personal matter delayed things anyway. I expect to have this wrapped up
by 23:59 BST on the 24th.

This PostgreSQL 10 open item is again past due for your status update. Kindly
send a status update within 24 hours, and include a date for your subsequent
status update.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#65Noah Misch
noah@leadboat.com
In reply to: Noah Misch (#64)
Re: transition table behavior with inheritance appears broken

On Sat, Jun 24, 2017 at 10:57:49PM -0700, Noah Misch wrote:

On Fri, Jun 23, 2017 at 02:39:48PM +0100, Andrew Gierth wrote:

"Noah" == Noah Misch <noah@leadboat.com> writes:

Noah> This PostgreSQL 10 open item is past due for your status update.
Noah> Kindly send a status update within 24 hours,

oops, sorry! I forgot to include a date in the last one, and in fact a
personal matter delayed things anyway. I expect to have this wrapped up
by 23:59 BST on the 24th.

This PostgreSQL 10 open item is again past due for your status update. Kindly
send a status update within 24 hours, and include a date for your subsequent
status update.

IMMEDIATE ATTENTION REQUIRED. This PostgreSQL 10 open item is long past due
for your status update. Please reacquaint yourself with the policy on open
item ownership[1]/messages/by-id/20170404140717.GA2675809@tornado.leadboat.com and then reply immediately. If I do not hear from you by
2017-06-28 06:00 UTC, I will transfer this item to release management team
ownership without further notice.

[1]: /messages/by-id/20170404140717.GA2675809@tornado.leadboat.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#66Andrew Gierth
andrew@tao11.riddles.org.uk
In reply to: Noah Misch (#65)
Re: transition table behavior with inheritance appears broken

"Noah" == Noah Misch <noah@leadboat.com> writes:

Noah> IMMEDIATE ATTENTION REQUIRED. This PostgreSQL 10 open item is
Noah> long past due for your status update. Please reacquaint yourself
Noah> with the policy on open item ownership[1] and then reply
Noah> immediately. If I do not hear from you by 2017-06-28 06:00 UTC,
Noah> I will transfer this item to release management team ownership
Noah> without further notice.

Sorry for the lack of updates. I need to sleep now, but I will send a
proper status update by 1800 UTC (1900 BST) today (28th).

--
Andrew.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#67Andrew Gierth
andrew@tao11.riddles.org.uk
In reply to: Noah Misch (#65)
Re: transition table behavior with inheritance appears broken

Commits pushed.

Unless I broke the buildfarm again (which I'll check up on later), or
some new issue arises with the fixes, this should close all 3 related
items for transition tables.

--
Andrew.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#68Thomas Munro
thomas.munro@enterprisedb.com
In reply to: Andrew Gierth (#67)
Re: transition table behavior with inheritance appears broken

On Thu, Jun 29, 2017 at 6:21 AM, Andrew Gierth
<andrew@tao11.riddles.org.uk> wrote:

Commits pushed.

Great news. Thanks for stepping up to get this committed. Thanks a
lot also to Marko, Amit L, Kevin, Robert, Noah and Peter G for the
problem reports, reviews and issue chasing.

--
Thomas Munro
http://www.enterprisedb.com

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers