Oddity in tuple routing for foreign partitions

Started by Etsuro Fujitaover 7 years ago45 messages
#1Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
1 attachment(s)

Hi,

While updating the fix-postgres_fdw-WCO-handling patch, I noticed that
the patch for tuple routing for foreign partitions doesn't work well
with remote triggers. Here is an example:

postgres=# create table loct1 (a int check (a in (1)), b text);
postgres=# create function br_insert_trigfunc() returns trigger as
$$begin new.b := new.b || ' triggered !'; return new; end$$ language
plpgsql;
postgres=# create trigger loct1_br_insert_trigger before insert on loct1
for each row execute procedure br_insert_trigfunc();
postgres=# create table itrtest (a int, b text) partition by list (a);
postgres=# create foreign table remp1 (a int check (a in (1)), b text)
server loopback options (table_name 'loct1');
postgres=# alter table itrtest attach partition remp1 for values in (1);

This behaves oddly:

postgres=# insert into itrtest values (1, 'foo') returning *;
a | b
---+-----
1 | foo
(1 row)

INSERT 0 1

The new value of b in the RETURNING result should be concatenated with '
triggered !', as shown below:

postgres=# select tableoid::regclass, * from itrtest ;
tableoid | a | b
----------+---+-----------------
remp1 | 1 | foo triggered !
(1 row)

The reason for that is: since that in ExecInitPartitionInfo, the core
calls BeginForeignInsert via ExecInitRoutingInfo before initializing
ri_returningList of ResultRelInfo for the partition, BeginForeignInsert
sees that the ri_returningList is NULL and incorrectly recognizes that
the local query doesn't have a RETURNING list. So, I moved the
ExecInitRoutingInfo call after building the ri_returningList (and before
handling ON CONFLICT because we reference the conversion map created by
that when handling that clause). The first version of the patch called
BeginForeignInsert that order, but I updated the patch incorrectly. :(
Also, I removed the CheckValidResultRel check from ExecInitRoutingInfo
and added that to ExecInitPartitionInfo, right after the
InitResultRelInfo call, because it would be better to abort the
operation as soon as we find the partition to be non-routable. Another
thing I noticed is the RT index of the foreign partition set to 1 in
postgresBeginForeignInsert to create a remote query; that would not work
for cases where the partitioned table has an RT index larger than 1 (eg,
the partitioned table is not the primary ModifyTable node); in which
cases, since the varno of Vars belonging to the foreign partition in the
RETURNING expressions is the same as the partitioned table, calling
deparseReturningList with the RT index set to 1 against the RETURNING
expressions would produce attrs_used to be NULL, leading to postgres_fdw
not retrieving actually inserted data from the remote, even if remote
triggers might change those data. So, I fixed this as well, by setting
the RT index accordingly to the partitioned table. Attached is a patch
for fixing these issues. I'll add this to the open items list for PG11.
(After addressing this, I'll post an updated version of the
fix-postgres_fdw-WCO-handling patch.)

Best regards,
Etsuro Fujita

Attachments:

fix-tuple-routing-for-foreign-partitions.patchtext/x-diff; name=fix-tuple-routing-for-foreign-partitions.patchDownload
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 7454,7459 **** select tableoid::regclass, * FROM itrtest;
--- 7454,7501 ----
   remp1    | 1 | foo
  (1 row)
  
+ delete from itrtest;
+ drop index loct1_idx;
+ -- Test that remote triggers work as expected
+ create function br_insert_trigfunc() returns trigger as $$
+ begin
+ 	new.b := new.b || ' triggered !';
+ 	return new;
+ end
+ $$ language plpgsql;
+ create trigger loct1_br_insert_trigger before insert on loct1
+ 	for each row execute procedure br_insert_trigfunc();
+ create trigger loct2_br_insert_trigger before insert on loct2
+ 	for each row execute procedure br_insert_trigfunc();
+ -- The new values are concatenated with ' triggered !'
+ insert into itrtest values (1, 'foo') returning *;
+  a |        b        
+ ---+-----------------
+  1 | foo triggered !
+ (1 row)
+ 
+ insert into itrtest values (2, 'qux') returning *;
+  a |        b        
+ ---+-----------------
+  2 | qux triggered !
+ (1 row)
+ 
+ insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+  a |         b         
+ ---+-------------------
+  1 | test1 triggered !
+  2 | test2 triggered !
+ (2 rows)
+ 
+ with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+  a |         b         
+ ---+-------------------
+  1 | test1 triggered !
+  2 | test2 triggered !
+ (2 rows)
+ 
+ drop trigger loct1_br_insert_trigger on loct1;
+ drop trigger loct2_br_insert_trigger on loct2;
  drop table itrtest;
  drop table loct1;
  drop table loct2;
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 1975,1982 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  {
  	PgFdwModifyState *fmstate;
  	Plan	   *plan = mtstate->ps.plan;
  	Relation	rel = resultRelInfo->ri_RelationDesc;
- 	RangeTblEntry *rte;
  	Query	   *query;
  	PlannerInfo *root;
  	TupleDesc	tupdesc = RelationGetDescr(rel);
--- 1975,1983 ----
  {
  	PgFdwModifyState *fmstate;
  	Plan	   *plan = mtstate->ps.plan;
+ 	EState	   *estate = mtstate->ps.state;
+ 	Index		resultRelation = resultRelInfo->ri_RangeTableIndex;
  	Relation	rel = resultRelInfo->ri_RelationDesc;
  	Query	   *query;
  	PlannerInfo *root;
  	TupleDesc	tupdesc = RelationGetDescr(rel);
***************
*** 1985,2002 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  	List	   *targetAttrs = NIL;
  	List	   *retrieved_attrs = NIL;
  	bool		doNothing = false;
  
  	initStringInfo(&sql);
  
  	/* Set up largely-dummy planner state. */
- 	rte = makeNode(RangeTblEntry);
- 	rte->rtekind = RTE_RELATION;
- 	rte->relid = RelationGetRelid(rel);
- 	rte->relkind = RELKIND_FOREIGN_TABLE;
  	query = makeNode(Query);
  	query->commandType = CMD_INSERT;
! 	query->resultRelation = 1;
! 	query->rtable = list_make1(rte);
  	root = makeNode(PlannerInfo);
  	root->parse = query;
  
--- 1986,2018 ----
  	List	   *targetAttrs = NIL;
  	List	   *retrieved_attrs = NIL;
  	bool		doNothing = false;
+ 	ListCell   *lc = NULL;
+ 	RangeTblEntry *parent_rte = NULL;
  
  	initStringInfo(&sql);
  
+ 	/*
+ 	 * If the foreign table is a partition, temporarily replace the parent's
+ 	 * RTE in the range table with a new target RTE describing the foreign
+ 	 * table for use by deparseInsertSql() and create_foreign_modify() below.
+ 	 */
+ 	if (resultRelInfo->ri_PartitionRoot)
+ 	{
+ 		RangeTblEntry *child_rte;
+ 
+ 		lc = list_nth_cell(estate->es_range_table, resultRelation - 1);
+ 		parent_rte = lfirst(lc);
+ 		child_rte = copyObject(parent_rte);
+ 		child_rte->relid = RelationGetRelid(rel);
+ 		child_rte->relkind = RELKIND_FOREIGN_TABLE;
+ 		lfirst(lc) = child_rte;
+ 	}
+ 
  	/* Set up largely-dummy planner state. */
  	query = makeNode(Query);
  	query->commandType = CMD_INSERT;
! 	query->resultRelation = resultRelation;
! 	query->rtable = estate->es_range_table;
  	root = makeNode(PlannerInfo);
  	root->parse = query;
  
***************
*** 2023,2033 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  	}
  
  	/* Construct the SQL command string. */
! 	deparseInsertSql(&sql, root, 1, rel, targetAttrs, doNothing,
  					 resultRelInfo->ri_returningList, &retrieved_attrs);
  
  	/* Construct an execution state. */
! 	fmstate = create_foreign_modify(mtstate->ps.state,
  									resultRelInfo,
  									CMD_INSERT,
  									NULL,
--- 2039,2049 ----
  	}
  
  	/* Construct the SQL command string. */
! 	deparseInsertSql(&sql, root, resultRelation, rel, targetAttrs, doNothing,
  					 resultRelInfo->ri_returningList, &retrieved_attrs);
  
  	/* Construct an execution state. */
! 	fmstate = create_foreign_modify(estate,
  									resultRelInfo,
  									CMD_INSERT,
  									NULL,
***************
*** 2036,2041 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
--- 2052,2061 ----
  									retrieved_attrs != NIL,
  									retrieved_attrs);
  
+ 	/* Restore the saved RTE. */
+ 	if (resultRelInfo->ri_PartitionRoot)
+ 		lfirst(lc) = parent_rte;
+ 
  	resultRelInfo->ri_FdwState = fmstate;
  }
  
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 1804,1809 **** insert into itrtest values (1, 'bar') on conflict (a) do update set b = excluded
--- 1804,1834 ----
  
  select tableoid::regclass, * FROM itrtest;
  
+ delete from itrtest;
+ 
+ drop index loct1_idx;
+ 
+ -- Test that remote triggers work as expected
+ create function br_insert_trigfunc() returns trigger as $$
+ begin
+ 	new.b := new.b || ' triggered !';
+ 	return new;
+ end
+ $$ language plpgsql;
+ create trigger loct1_br_insert_trigger before insert on loct1
+ 	for each row execute procedure br_insert_trigfunc();
+ create trigger loct2_br_insert_trigger before insert on loct2
+ 	for each row execute procedure br_insert_trigfunc();
+ 
+ -- The new values are concatenated with ' triggered !'
+ insert into itrtest values (1, 'foo') returning *;
+ insert into itrtest values (2, 'qux') returning *;
+ insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+ with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+ 
+ drop trigger loct1_br_insert_trigger on loct1;
+ drop trigger loct2_br_insert_trigger on loct2;
+ 
  drop table itrtest;
  drop table loct1;
  drop table loct2;
*** a/src/backend/executor/execPartition.c
--- b/src/backend/executor/execPartition.c
***************
*** 330,335 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 330,342 ----
  					  estate->es_instrument);
  
  	/*
+ 	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
+ 	 * partition-key becomes a DELETE+INSERT operation, so this check is still
+ 	 * required when the operation is CMD_UPDATE.
+ 	 */
+ 	CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+ 
+ 	/*
  	 * Since we've just initialized this ResultRelInfo, it's not in any list
  	 * attached to the estate as yet.  Add it, so that it can be found later.
  	 *
***************
*** 341,349 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
  		lappend(estate->es_tuple_routing_result_relations,
  				leaf_part_rri);
  
- 	/* Set up information needed for routing tuples to this partition. */
- 	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
- 
  	/*
  	 * Open partition indices.  The user may have asked to check for conflicts
  	 * within this leaf partition and do "nothing" instead of throwing an
--- 348,353 ----
***************
*** 466,471 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 470,478 ----
  									&mtstate->ps, RelationGetDescr(partrel));
  	}
  
+ 	/* Set up information needed for routing tuples to the partition. */
+ 	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
+ 
  	/*
  	 * If there is an ON CONFLICT clause, initialize state for it.
  	 */
***************
*** 612,619 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
  
  /*
   * ExecInitRoutingInfo
!  *		Set up information needed for routing tuples to a leaf partition if
!  *		routable; else abort the operation
   */
  void
  ExecInitRoutingInfo(ModifyTableState *mtstate,
--- 619,625 ----
  
  /*
   * ExecInitRoutingInfo
!  *		Set up information needed for routing tuples to a leaf partition
   */
  void
  ExecInitRoutingInfo(ModifyTableState *mtstate,
***************
*** 624,632 **** ExecInitRoutingInfo(ModifyTableState *mtstate,
  {
  	MemoryContext oldContext;
  
- 	/* Verify the partition is a valid target for INSERT */
- 	CheckValidResultRel(partRelInfo, CMD_INSERT);
- 
  	/*
  	 * Switch into per-query memory context.
  	 */
--- 630,635 ----
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
***************
*** 1700,1719 **** ExecPrepareTupleRouting(ModifyTableState *mtstate,
  										partidx);
  
  	/*
! 	 * Set up information needed for routing tuples to the partition if we
! 	 * didn't yet (ExecInitRoutingInfo would abort the operation if the
! 	 * partition isn't routable).
  	 *
  	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
! 	 * tuple to a new partition.  This setup would be needed for a subplan
  	 * partition of such an UPDATE that is chosen as the partition to route
! 	 * the tuple to.  The reason we do this setup here rather than in
  	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
  	 * unnecessarily due to non-routable subplan partitions that may not be
  	 * chosen for update tuple movement after all.
  	 */
  	if (!partrel->ri_PartitionReadyForRouting)
  		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
  
  	/*
  	 * Make it look like we are inserting into the partition.
--- 1700,1723 ----
  										partidx);
  
  	/*
! 	 * Check whether the partition is routable if we didn't yet
  	 *
  	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
! 	 * tuple to a new partition.  This check would be applied to a subplan
  	 * partition of such an UPDATE that is chosen as the partition to route
! 	 * the tuple to.  The reason we do this check here rather than in
  	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
  	 * unnecessarily due to non-routable subplan partitions that may not be
  	 * chosen for update tuple movement after all.
  	 */
  	if (!partrel->ri_PartitionReadyForRouting)
+ 	{
+ 		/* Verify the partition is a valid target for INSERT. */
+ 		CheckValidResultRel(partrel, CMD_INSERT);
+ 
+ 		/* Set up information needed for routing tuples to the partition. */
  		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
+ 	}
  
  	/*
  	 * Make it look like we are inserting into the partition.
#2Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Etsuro Fujita (#1)
Re: Oddity in tuple routing for foreign partitions

Fujita-san,

On 2018/04/16 20:25, Etsuro Fujita wrote:

Hi,

While updating the fix-postgres_fdw-WCO-handling patch, I noticed that
the patch for tuple routing for foreign partitions doesn't work well
with remote triggers. Here is an example:

postgres=# create table loct1 (a int check (a in (1)), b text);
postgres=# create function br_insert_trigfunc() returns trigger as
$$begin new.b := new.b || ' triggered !'; return new; end$$ language
plpgsql;
postgres=# create trigger loct1_br_insert_trigger before insert on loct1
for each row execute procedure br_insert_trigfunc();
postgres=# create table itrtest (a int, b text) partition by list (a);
postgres=# create foreign table remp1 (a int check (a in (1)), b text)
server loopback options (table_name 'loct1');
postgres=# alter table itrtest attach partition remp1 for values in (1);

This behaves oddly:

postgres=# insert into itrtest values (1, 'foo') returning *;
a | b
---+-----
1 | foo
(1 row)

INSERT 0 1

The new value of b in the RETURNING result should be concatenated with '
triggered !', as shown below:

postgres=# select tableoid::regclass, * from itrtest ;
tableoid | a | b
----------+---+-----------------
remp1 | 1 | foo triggered !
(1 row)

The reason for that is: since that in ExecInitPartitionInfo, the core
calls BeginForeignInsert via ExecInitRoutingInfo before initializing
ri_returningList of ResultRelInfo for the partition, BeginForeignInsert
sees that the ri_returningList is NULL and incorrectly recognizes that
the local query doesn't have a RETURNING list.

Good catch!

So, I moved the
ExecInitRoutingInfo call after building the ri_returningList (and before
handling ON CONFLICT because we reference the conversion map created by
that when handling that clause). The first version of the patch called
BeginForeignInsert that order, but I updated the patch incorrectly. :(

I didn't notice that when reviewing either. Actually, I was under the
impression that BeginForeignInsert consumes returningList from the
ModifyTable node itself, not the ResultRelInfo, but now I see that
ri_returningList was added exactly for BeginForeignInsert to consume.
Good thing about that is that we get a Var-translated version instead of
the original one that contains parent's attnos.

Also, I removed the CheckValidResultRel check from ExecInitRoutingInfo
and added that to ExecInitPartitionInfo right after the> InitResultRelInfo call, because it would be better to abort the
operation as soon as we find the partition to be non-routable.

Sounds fine.

Another
thing I noticed is the RT index of the foreign partition set to 1 in
postgresBeginForeignInsert to create a remote query; that would not work
for cases where the partitioned table has an RT index larger than 1 (eg,
the partitioned table is not the primary ModifyTable node); in which
cases, since the varno of Vars belonging to the foreign partition in the
RETURNING expressions is the same as the partitioned table, calling
deparseReturningList with the RT index set to 1 against the RETURNING
expressions would produce attrs_used to be NULL, leading to postgres_fdw
not retrieving actually inserted data from the remote, even if remote
triggers might change those data. So, I fixed this as well, by setting
the RT index accordingly to the partitioned table.

Check.

Attached is a patch
for fixing these issues. I'll add this to the open items list for PG11.
(After addressing this, I'll post an updated version of the
fix-postgres_fdw-WCO-handling patch.)

Your patch seems to fix the issue and code changes seem fine to me.

Thanks,
Amit

#3Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Amit Langote (#2)
Re: Oddity in tuple routing for foreign partitions

Hi Amit,

(2018/04/17 10:10), Amit Langote wrote:

On 2018/04/16 20:25, Etsuro Fujita wrote:

While updating the fix-postgres_fdw-WCO-handling patch, I noticed that
the patch for tuple routing for foreign partitions doesn't work well
with remote triggers. Here is an example:

postgres=# create table loct1 (a int check (a in (1)), b text);
postgres=# create function br_insert_trigfunc() returns trigger as
$$begin new.b := new.b || ' triggered !'; return new; end$$ language
plpgsql;
postgres=# create trigger loct1_br_insert_trigger before insert on loct1
for each row execute procedure br_insert_trigfunc();
postgres=# create table itrtest (a int, b text) partition by list (a);
postgres=# create foreign table remp1 (a int check (a in (1)), b text)
server loopback options (table_name 'loct1');
postgres=# alter table itrtest attach partition remp1 for values in (1);

This behaves oddly:

postgres=# insert into itrtest values (1, 'foo') returning *;
a | b
---+-----
1 | foo
(1 row)

INSERT 0 1

The new value of b in the RETURNING result should be concatenated with '
triggered !', as shown below:

postgres=# select tableoid::regclass, * from itrtest ;
tableoid | a | b
----------+---+-----------------
remp1 | 1 | foo triggered !
(1 row)

The reason for that is: since that in ExecInitPartitionInfo, the core
calls BeginForeignInsert via ExecInitRoutingInfo before initializing
ri_returningList of ResultRelInfo for the partition, BeginForeignInsert
sees that the ri_returningList is NULL and incorrectly recognizes that
the local query doesn't have a RETURNING list.

Good catch!

So, I moved the
ExecInitRoutingInfo call after building the ri_returningList (and before
handling ON CONFLICT because we reference the conversion map created by
that when handling that clause). The first version of the patch called
BeginForeignInsert that order, but I updated the patch incorrectly. :(

I didn't notice that when reviewing either. Actually, I was under the
impression that BeginForeignInsert consumes returningList from the
ModifyTable node itself, not the ResultRelInfo, but now I see that
ri_returningList was added exactly for BeginForeignInsert to consume.
Good thing about that is that we get a Var-translated version instead of
the original one that contains parent's attnos.

Also, I removed the CheckValidResultRel check from ExecInitRoutingInfo
and added that to ExecInitPartitionInfo right after the> InitResultRelInfo call, because it would be better to abort the
operation as soon as we find the partition to be non-routable.

Sounds fine.

Another
thing I noticed is the RT index of the foreign partition set to 1 in
postgresBeginForeignInsert to create a remote query; that would not work
for cases where the partitioned table has an RT index larger than 1 (eg,
the partitioned table is not the primary ModifyTable node); in which
cases, since the varno of Vars belonging to the foreign partition in the
RETURNING expressions is the same as the partitioned table, calling
deparseReturningList with the RT index set to 1 against the RETURNING
expressions would produce attrs_used to be NULL, leading to postgres_fdw
not retrieving actually inserted data from the remote, even if remote
triggers might change those data. So, I fixed this as well, by setting
the RT index accordingly to the partitioned table.

Check.

Attached is a patch
for fixing these issues. I'll add this to the open items list for PG11.
(After addressing this, I'll post an updated version of the
fix-postgres_fdw-WCO-handling patch.)

Your patch seems to fix the issue and code changes seem fine to me.

Thanks for the review!

Best regards,
Etsuro Fujita

#4Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: Amit Langote (#2)
Re: Oddity in tuple routing for foreign partitions

Hello.

At Tue, 17 Apr 2018 10:10:38 +0900, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote in <fefb9510-e304-27da-e984-a2b0ac77927a@lab.ntt.co.jp>

Fujita-san,

On 2018/04/16 20:25, Etsuro Fujita wrote:

Hi,

While updating the fix-postgres_fdw-WCO-handling patch, I noticed that
the patch for tuple routing for foreign partitions doesn't work well
with remote triggers. Here is an example:

postgres=# create table loct1 (a int check (a in (1)), b text);
postgres=# create function br_insert_trigfunc() returns trigger as
$$begin new.b := new.b || ' triggered !'; return new; end$$ language
plpgsql;
postgres=# create trigger loct1_br_insert_trigger before insert on loct1
for each row execute procedure br_insert_trigfunc();
postgres=# create table itrtest (a int, b text) partition by list (a);
postgres=# create foreign table remp1 (a int check (a in (1)), b text)
server loopback options (table_name 'loct1');
postgres=# alter table itrtest attach partition remp1 for values in (1);

This behaves oddly:

postgres=# insert into itrtest values (1, 'foo') returning *;
a | b
---+-----
1 | foo
(1 row)

INSERT 0 1

The new value of b in the RETURNING result should be concatenated with '
triggered !', as shown below:

postgres=# select tableoid::regclass, * from itrtest ;
tableoid | a | b
----------+---+-----------------
remp1 | 1 | foo triggered !
(1 row)

The reason for that is: since that in ExecInitPartitionInfo, the core
calls BeginForeignInsert via ExecInitRoutingInfo before initializing
ri_returningList of ResultRelInfo for the partition, BeginForeignInsert
sees that the ri_returningList is NULL and incorrectly recognizes that
the local query doesn't have a RETURNING list.

Good catch!

So, I moved the
ExecInitRoutingInfo call after building the ri_returningList (and before
handling ON CONFLICT because we reference the conversion map created by
that when handling that clause). The first version of the patch called
BeginForeignInsert that order, but I updated the patch incorrectly. :(

I didn't notice that when reviewing either. Actually, I was under the
impression that BeginForeignInsert consumes returningList from the
ModifyTable node itself, not the ResultRelInfo, but now I see that
ri_returningList was added exactly for BeginForeignInsert to consume.
Good thing about that is that we get a Var-translated version instead of
the original one that contains parent's attnos.

Also, I removed the CheckValidResultRel check from ExecInitRoutingInfo
and added that to ExecInitPartitionInfo right after the> InitResultRelInfo call, because it would be better to abort the
operation as soon as we find the partition to be non-routable.

Sounds fine.

If I'm reading this correctly, ExecInitParititionInfo calls
ExecInitRoutingInfo so currently CheckValidityResultRel is called
for the child when partrel is created in ExecPrepareTupleRouting.
But the move of CheckValidResultRel results in letting partrel
just created in ExecPrepareTupleRouting not be checked at all
since ri_ParititionReadyForRouting is always set true in
ExecInitPartitionInfo.

Another
thing I noticed is the RT index of the foreign partition set to 1 in
postgresBeginForeignInsert to create a remote query; that would not work
for cases where the partitioned table has an RT index larger than 1 (eg,
the partitioned table is not the primary ModifyTable node); in which
cases, since the varno of Vars belonging to the foreign partition in the
RETURNING expressions is the same as the partitioned table, calling
deparseReturningList with the RT index set to 1 against the RETURNING
expressions would produce attrs_used to be NULL, leading to postgres_fdw
not retrieving actually inserted data from the remote, even if remote
triggers might change those data. So, I fixed this as well, by setting
the RT index accordingly to the partitioned table.

Check.

I'm not sure but is it true that the parent is always at
resultRelation - 1?

Attached is a patch
for fixing these issues. I'll add this to the open items list for PG11.
(After addressing this, I'll post an updated version of the
fix-postgres_fdw-WCO-handling patch.)

Your patch seems to fix the issue and code changes seem fine to me.

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#5Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Kyotaro HORIGUCHI (#4)
Re: Oddity in tuple routing for foreign partitions

On 2018/04/17 11:13, Kyotaro HORIGUCHI wrote:

Also, I removed the CheckValidResultRel check from ExecInitRoutingInfo
and added that to ExecInitPartitionInfo right after the> InitResultRelInfo call,
because it would be better to abort the
operation as soon as we find the partition to be non-routable.

Sounds fine.

If I'm reading this correctly, ExecInitParititionInfo calls
ExecInitRoutingInfo so currently CheckValidityResultRel is called
for the child when partrel is created in ExecPrepareTupleRouting.
But the move of CheckValidResultRel results in letting partrel
just created in ExecPrepareTupleRouting not be checked at all
since ri_ParititionReadyForRouting is always set true in
ExecInitPartitionInfo.

I thought the same upon reading the email, but it seems that the patch
does add CheckValidResultRel() in ExecInitPartitionInfo() as well:

@@ -335,6 +335,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
estate->es_instrument);

     /*
+     * Verify result relation is a valid target for an INSERT.  An UPDATE
of a
+     * partition-key becomes a DELETE+INSERT operation, so this check is
still
+     * required when the operation is CMD_UPDATE.
+     */
+    CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+
+    /*
      * Since we've just initialized this ResultRelInfo, it's not in any list
      * attached to the estate as yet.  Add it, so that it can be found later.
      *

Another
thing I noticed is the RT index of the foreign partition set to 1 in
postgresBeginForeignInsert to create a remote query; that would not work
for cases where the partitioned table has an RT index larger than 1 (eg,
the partitioned table is not the primary ModifyTable node); in which
cases, since the varno of Vars belonging to the foreign partition in the
RETURNING expressions is the same as the partitioned table, calling
deparseReturningList with the RT index set to 1 against the RETURNING
expressions would produce attrs_used to be NULL, leading to postgres_fdw
not retrieving actually inserted data from the remote, even if remote
triggers might change those data. So, I fixed this as well, by setting
the RT index accordingly to the partitioned table.

Check.

I'm not sure but is it true that the parent is always at
resultRelation - 1?

Unless I'm missing something, EState.es_range_table contains *all* range
table entries that were created by the planner. So, if the planner
assigned resultRelation as the RT index for the parent, then it seems we
can rely on getting the same entry back with
list_nth_cell(estate->es_range_table, resultRelation - 1). That seems
true even if the parent wasn't the *primary* target relation. For
example, the following works with the patch:

-- in addition to the tables shown in Fujita-san's email, define one more
-- local partition
create table locp2 partition of itrtest for values in (2);

-- simple case
insert into itrtest values (1, 'foo') returning *;
a | b
---+-----------------
1 | foo triggered !
(1 row)

-- itrtest (the parent) appears as both the primary target relation and
-- otherwise. The latter in the form of being mentioned in the with query

with ins (a, b) as (insert into itrtest values (1, 'foo') returning *)
insert into itrtest select a + 1, b from ins returning *;
a | b
---+-----------------
2 | foo triggered !
(1 row)

But maybe I misunderstood your question.

Thanks,
Amit

#6Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Amit Langote (#5)
Re: Oddity in tuple routing for foreign partitions

(2018/04/17 16:10), Amit Langote wrote:

On 2018/04/17 11:13, Kyotaro HORIGUCHI wrote:

If I'm reading this correctly, ExecInitParititionInfo calls
ExecInitRoutingInfo so currently CheckValidityResultRel is called
for the child when partrel is created in ExecPrepareTupleRouting.
But the move of CheckValidResultRel results in letting partrel
just created in ExecPrepareTupleRouting not be checked at all
since ri_ParititionReadyForRouting is always set true in
ExecInitPartitionInfo.

I thought the same upon reading the email, but it seems that the patch
does add CheckValidResultRel() in ExecInitPartitionInfo() as well:

@@ -335,6 +335,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
estate->es_instrument);

/*
+     * Verify result relation is a valid target for an INSERT.  An UPDATE
of a
+     * partition-key becomes a DELETE+INSERT operation, so this check is
still
+     * required when the operation is CMD_UPDATE.
+     */
+    CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+
+    /*
* Since we've just initialized this ResultRelInfo, it's not in any list
* attached to the estate as yet.  Add it, so that it can be found later.
*

That's right. So, the validity check would be applied to a partition
created in ExecPrepareTupleRouting as well.

Another
thing I noticed is the RT index of the foreign partition set to 1 in
postgresBeginForeignInsert to create a remote query; that would not work
for cases where the partitioned table has an RT index larger than 1 (eg,
the partitioned table is not the primary ModifyTable node); in which
cases, since the varno of Vars belonging to the foreign partition in the
RETURNING expressions is the same as the partitioned table, calling
deparseReturningList with the RT index set to 1 against the RETURNING
expressions would produce attrs_used to be NULL, leading to postgres_fdw
not retrieving actually inserted data from the remote, even if remote
triggers might change those data. So, I fixed this as well, by setting
the RT index accordingly to the partitioned table.

Check.

I'm not sure but is it true that the parent is always at
resultRelation - 1?

Unless I'm missing something, EState.es_range_table contains *all* range
table entries that were created by the planner. So, if the planner
assigned resultRelation as the RT index for the parent, then it seems we
can rely on getting the same entry back with
list_nth_cell(estate->es_range_table, resultRelation - 1). That seems
true even if the parent wasn't the *primary* target relation. For
example, the following works with the patch:

-- in addition to the tables shown in Fujita-san's email, define one more
-- local partition
create table locp2 partition of itrtest for values in (2);

-- simple case
insert into itrtest values (1, 'foo') returning *;
a | b
---+-----------------
1 | foo triggered !
(1 row)

-- itrtest (the parent) appears as both the primary target relation and
-- otherwise. The latter in the form of being mentioned in the with query

with ins (a, b) as (insert into itrtest values (1, 'foo') returning *)
insert into itrtest select a + 1, b from ins returning *;
a | b
---+-----------------
2 | foo triggered !
(1 row)

I agree with that. Thanks for the explanation and the example, Amit!

Maybe my explanation (or the naming parent_rte/child_rte in the patch)
was not necessarily good, so I guess that that confused horiguchi-san.
Let me explain. In the INSERT/COPY-tuple-routing case, as explained by
Amit, the RTE at that position in the EState's range table is the one
for the partitioned table of a given partition, so the statement would
be true. BUT in the UPDATE-tuple-routing case, the RTE is the one for
the given partition, not for the partitioned table, so the statement
would not be true. In the latter case, we don't need to create a child
RTE and replace the original RTE with it, but I handled both cases the
same way for simplicity.

Thanks for the comments, Horiguchi-san!

Best regards,
Etsuro Fujita

#7Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Etsuro Fujita (#6)
Re: Oddity in tuple routing for foreign partitions

On 2018/04/17 16:41, Etsuro Fujita wrote:

In the INSERT/COPY-tuple-routing case, as explained by Amit, the
RTE at that position in the EState's range table is the one for the
partitioned table of a given partition, so the statement would be true. 
BUT in the UPDATE-tuple-routing case, the RTE is the one for the given
partition, not for the partitioned table, so the statement would not be
true.  In the latter case, we don't need to create a child RTE and replace
the original RTE with it, but I handled both cases the same way for
simplicity.

Oh, I didn't really consider this part carefully. That the resultRelInfo
received by BeginForeignInsert (when called by ExecInitRoutingInfo) could
be a reused UPDATE result relation. It might be possible to justify the
parent_rte/child_rte terminology by explaining it a bit better. I see
three cases that arise during tuple routing:

1. This is INSERT and so the resultRelation that's received in
BeginForeignInsert has been freshly created in ExecInitPartitionInfo
and it bears node->nominalRelation or 1 as its ri_RangeTableIndex

2. This is UPDATE and the resultRelInfo that's received in
BeginForeignInsert has been freshly created in ExecInitPartitionInfo
and it bears node->nominalRelation or 1 as its ri_RangeTableIndex

3. This is UPDATE and the resultRelInfo that's received in
BeginForeignInsert is a reused one, in which case, it bears the planner
assigned ri_RangeTableIndex

In all three cases, I think we can rely on using ri_RangeTableIndex to
fetch a valid "parent" RTE from es_range_table.

Do you think we need to clarify this in a comment?

Thanks,
Amit

#8Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Amit Langote (#7)
1 attachment(s)
Re: Oddity in tuple routing for foreign partitions

(2018/04/18 14:44), Amit Langote wrote:

On 2018/04/17 16:41, Etsuro Fujita wrote:

In the INSERT/COPY-tuple-routing case, as explained by Amit, the
RTE at that position in the EState's range table is the one for the
partitioned table of a given partition, so the statement would be true.
BUT in the UPDATE-tuple-routing case, the RTE is the one for the given
partition, not for the partitioned table, so the statement would not be
true. In the latter case, we don't need to create a child RTE and replace
the original RTE with it, but I handled both cases the same way for
simplicity.

Oh, I didn't really consider this part carefully. That the resultRelInfo
received by BeginForeignInsert (when called by ExecInitRoutingInfo) could
be a reused UPDATE result relation. It might be possible to justify the
parent_rte/child_rte terminology by explaining it a bit better.

Yeah, I think that terminology would be confusing, so I changed it to
old_rte/new_rte.

I see
three cases that arise during tuple routing:

1. This is INSERT and so the resultRelation that's received in
BeginForeignInsert has been freshly created in ExecInitPartitionInfo
and it bears node->nominalRelation or 1 as its ri_RangeTableIndex

Right.

2. This is UPDATE and the resultRelInfo that's received in
BeginForeignInsert has been freshly created in ExecInitPartitionInfo
and it bears node->nominalRelation or 1 as its ri_RangeTableIndex

In that case, we have a valid plan node, so it would only bear
node->nominalRelation.

3. This is UPDATE and the resultRelInfo that's received in
BeginForeignInsert is a reused one, in which case, it bears the planner
assigned ri_RangeTableIndex

Right.

In all three cases, I think we can rely on using ri_RangeTableIndex to
fetch a valid "parent" RTE from es_range_table.

I slept on this, I noticed there is another bug in case #2. Here is an
example with the previous patch:

postgres=# create table utrtest (a int, b text) partition by list (a);
postgres=# create table loct (a int check (a in (1)), b text);
postgres=# create foreign table remp (a int check (a in (1)), b text)
server loopback options (table_name 'loct');
postgres=# create table locp (a int check (a in (2)), b text);
postgres=# alter table utrtest attach partition remp for values in (1);
postgres=# alter table utrtest attach partition locp for values in (2);
postgres=# create trigger loct_br_insert_trigger before insert on loct
for each row execute procedure br_insert_trigfunc();

where the trigger function is the one defined in an earlier email.

postgres=# insert into utrtest values (2, 'qux');
INSERT 0 1
postgres=# update utrtest set a = 1 where a = 2 returning *;
a | b
---+-----
1 | qux
(1 row)

UPDATE 1

Wrong result! The result should be concatenated with ' triggered !'.

In case #2, since we initialize expressions for the partition's
ResultRelInfo including RETURNING by translating the attnos of the
corresponding expressions in the ResultRelInfo for the first subplan
target rel, I think we should replace the RTE for the first subplan
target rel, not the RTE for the nominal rel, with the new one created
for the foreign table. Attached is an updated version for fixing this
issue.

Do you think we need to clarify this in a comment?

Yeah, but I updated comments about this a little bit different way in
the attached. Does that make sense?

Thanks for the comments!

Best regards,
Etsuro Fujita

Attachments:

fix-tuple-routing-for-foreign-partitions-2.patchtext/x-diff; name=fix-tuple-routing-for-foreign-partitions-2.patchDownload
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 7454,7459 **** select tableoid::regclass, * FROM itrtest;
--- 7454,7501 ----
   remp1    | 1 | foo
  (1 row)
  
+ delete from itrtest;
+ drop index loct1_idx;
+ -- Test that remote triggers work with insert tuple routing
+ create function br_insert_trigfunc() returns trigger as $$
+ begin
+ 	new.b := new.b || ' triggered !';
+ 	return new;
+ end
+ $$ language plpgsql;
+ create trigger loct1_br_insert_trigger before insert on loct1
+ 	for each row execute procedure br_insert_trigfunc();
+ create trigger loct2_br_insert_trigger before insert on loct2
+ 	for each row execute procedure br_insert_trigfunc();
+ -- The new values are concatenated with ' triggered !'
+ insert into itrtest values (1, 'foo') returning *;
+  a |        b        
+ ---+-----------------
+  1 | foo triggered !
+ (1 row)
+ 
+ insert into itrtest values (2, 'qux') returning *;
+  a |        b        
+ ---+-----------------
+  2 | qux triggered !
+ (1 row)
+ 
+ insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+  a |         b         
+ ---+-------------------
+  1 | test1 triggered !
+  2 | test2 triggered !
+ (2 rows)
+ 
+ with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+  a |         b         
+ ---+-------------------
+  1 | test1 triggered !
+  2 | test2 triggered !
+ (2 rows)
+ 
+ drop trigger loct1_br_insert_trigger on loct1;
+ drop trigger loct2_br_insert_trigger on loct2;
  drop table itrtest;
  drop table loct1;
  drop table loct2;
***************
*** 7518,7523 **** select tableoid::regclass, * FROM locp;
--- 7560,7615 ----
  
  -- The executor should not let unexercised FDWs shut down
  update utrtest set a = 1 where b = 'foo';
+ -- Test that remote triggers work with update tuple routing
+ create trigger loct_br_insert_trigger before insert on loct
+ 	for each row execute procedure br_insert_trigfunc();
+ delete from utrtest;
+ insert into utrtest values (2, 'qux');
+ -- Check case where the foreign partition is a subplan target rel
+ explain (verbose, costs off)
+ update utrtest set a = 1 where a = 1 or a = 2 returning *;
+                                           QUERY PLAN                                          
+ ----------------------------------------------------------------------------------------------
+  Update on public.utrtest
+    Output: remp.a, remp.b
+    Foreign Update on public.remp
+    Update on public.locp
+    ->  Foreign Update on public.remp
+          Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b
+    ->  Seq Scan on public.locp
+          Output: 1, locp.b, locp.ctid
+          Filter: ((locp.a = 1) OR (locp.a = 2))
+ (9 rows)
+ 
+ -- The new values are concatenated with ' triggered !'
+ update utrtest set a = 1 where a = 1 or a = 2 returning *;
+  a |        b        
+ ---+-----------------
+  1 | qux triggered !
+ (1 row)
+ 
+ delete from utrtest;
+ insert into utrtest values (2, 'qux');
+ -- Check case where the foreign partition isn't a subplan target rel
+ explain (verbose, costs off)
+ update utrtest set a = 1 where a = 2 returning *;
+               QUERY PLAN              
+ --------------------------------------
+  Update on public.utrtest
+    Output: locp.a, locp.b
+    Update on public.locp
+    ->  Seq Scan on public.locp
+          Output: 1, locp.b, locp.ctid
+          Filter: (locp.a = 2)
+ (6 rows)
+ 
+ -- The new values are concatenated with ' triggered !'
+ update utrtest set a = 1 where a = 2 returning *;
+  a |        b        
+ ---+-----------------
+  1 | qux triggered !
+ (1 row)
+ 
  drop table utrtest;
  drop table loct;
  -- Test copy tuple routing
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 1974,1982 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  						   ResultRelInfo *resultRelInfo)
  {
  	PgFdwModifyState *fmstate;
! 	Plan	   *plan = mtstate->ps.plan;
  	Relation	rel = resultRelInfo->ri_RelationDesc;
- 	RangeTblEntry *rte;
  	Query	   *query;
  	PlannerInfo *root;
  	TupleDesc	tupdesc = RelationGetDescr(rel);
--- 1974,1983 ----
  						   ResultRelInfo *resultRelInfo)
  {
  	PgFdwModifyState *fmstate;
! 	ModifyTable *plan = (ModifyTable *) mtstate->ps.plan;
! 	EState	   *estate = mtstate->ps.state;
! 	Index		resultRelation = resultRelInfo->ri_RangeTableIndex;
  	Relation	rel = resultRelInfo->ri_RelationDesc;
  	Query	   *query;
  	PlannerInfo *root;
  	TupleDesc	tupdesc = RelationGetDescr(rel);
***************
*** 1985,2002 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  	List	   *targetAttrs = NIL;
  	List	   *retrieved_attrs = NIL;
  	bool		doNothing = false;
  
  	initStringInfo(&sql);
  
  	/* Set up largely-dummy planner state. */
- 	rte = makeNode(RangeTblEntry);
- 	rte->rtekind = RTE_RELATION;
- 	rte->relid = RelationGetRelid(rel);
- 	rte->relkind = RELKIND_FOREIGN_TABLE;
  	query = makeNode(Query);
  	query->commandType = CMD_INSERT;
! 	query->resultRelation = 1;
! 	query->rtable = list_make1(rte);
  	root = makeNode(PlannerInfo);
  	root->parse = query;
  
--- 1986,2047 ----
  	List	   *targetAttrs = NIL;
  	List	   *retrieved_attrs = NIL;
  	bool		doNothing = false;
+ 	bool		replace_rte = false;
+ 	ListCell   *lc = NULL;
+ 	RangeTblEntry *old_rte = NULL;
  
  	initStringInfo(&sql);
  
+ 	/*
+ 	 * If the foreign table is a partition, temporarily replace the parent's
+ 	 * RTE in the range table with a new target RTE describing the foreign
+ 	 * table for use by deparseInsertSql() and create_foreign_modify() below.
+ 	 */
+ 	if (resultRelInfo->ri_PartitionRoot)
+ 	{
+ 		replace_rte = true;
+ 
+ 		/*
+ 		 * If this is invoked by an UPDATE, we need special handling: if the
+ 		 * partition is one of the UPDATE subplan target rels, use the target
+ 		 * rel's RTE without replacement; else replace the RTE for the first
+ 		 * subplan target rel with the new target RTE as we have created
+ 		 * expressions in the partition's ResultRelInfo such as RETURNING by
+ 		 * translating the attnos of the corresponding expressions in the
+ 		 * ResultRelInfo for the first subplan target rel before we get here.
+ 		 */
+ 		if (plan && plan->operation == CMD_UPDATE)
+ 		{
+ 			if (resultRelation != plan->nominalRelation)
+ 			{
+ 				/* partition that is a subplan target rel */
+ 				replace_rte = false;
+ 			}
+ 			else
+ 			{
+ 				/* partition that isn't a subplan target rel */ 
+ 				resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+ 			}
+ 		}
+ 
+ 		if (replace_rte)
+ 		{
+ 			RangeTblEntry *new_rte;
+ 
+ 			lc = list_nth_cell(estate->es_range_table, resultRelation - 1);
+ 			old_rte = lfirst(lc);
+ 			new_rte = copyObject(old_rte);
+ 			new_rte->relid = RelationGetRelid(rel);
+ 			new_rte->relkind = RELKIND_FOREIGN_TABLE;
+ 			lfirst(lc) = new_rte;
+ 		}
+ 	}
+ 
  	/* Set up largely-dummy planner state. */
  	query = makeNode(Query);
  	query->commandType = CMD_INSERT;
! 	query->resultRelation = resultRelation;
! 	query->rtable = estate->es_range_table;
  	root = makeNode(PlannerInfo);
  	root->parse = query;
  
***************
*** 2012,2018 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  	/* Check if we add the ON CONFLICT clause to the remote query. */
  	if (plan)
  	{
! 		OnConflictAction onConflictAction = ((ModifyTable *) plan)->onConflictAction;
  
  		/* We only support DO NOTHING without an inference specification. */
  		if (onConflictAction == ONCONFLICT_NOTHING)
--- 2057,2063 ----
  	/* Check if we add the ON CONFLICT clause to the remote query. */
  	if (plan)
  	{
! 		OnConflictAction onConflictAction = plan->onConflictAction;
  
  		/* We only support DO NOTHING without an inference specification. */
  		if (onConflictAction == ONCONFLICT_NOTHING)
***************
*** 2023,2033 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  	}
  
  	/* Construct the SQL command string. */
! 	deparseInsertSql(&sql, root, 1, rel, targetAttrs, doNothing,
  					 resultRelInfo->ri_returningList, &retrieved_attrs);
  
  	/* Construct an execution state. */
! 	fmstate = create_foreign_modify(mtstate->ps.state,
  									resultRelInfo,
  									CMD_INSERT,
  									NULL,
--- 2068,2078 ----
  	}
  
  	/* Construct the SQL command string. */
! 	deparseInsertSql(&sql, root, resultRelation, rel, targetAttrs, doNothing,
  					 resultRelInfo->ri_returningList, &retrieved_attrs);
  
  	/* Construct an execution state. */
! 	fmstate = create_foreign_modify(estate,
  									resultRelInfo,
  									CMD_INSERT,
  									NULL,
***************
*** 2036,2041 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
--- 2081,2090 ----
  									retrieved_attrs != NIL,
  									retrieved_attrs);
  
+ 	/* Restore the saved RTE. */
+ 	if (replace_rte)
+ 		lfirst(lc) = old_rte;
+ 
  	resultRelInfo->ri_FdwState = fmstate;
  }
  
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 1804,1809 **** insert into itrtest values (1, 'bar') on conflict (a) do update set b = excluded
--- 1804,1834 ----
  
  select tableoid::regclass, * FROM itrtest;
  
+ delete from itrtest;
+ 
+ drop index loct1_idx;
+ 
+ -- Test that remote triggers work with insert tuple routing
+ create function br_insert_trigfunc() returns trigger as $$
+ begin
+ 	new.b := new.b || ' triggered !';
+ 	return new;
+ end
+ $$ language plpgsql;
+ create trigger loct1_br_insert_trigger before insert on loct1
+ 	for each row execute procedure br_insert_trigfunc();
+ create trigger loct2_br_insert_trigger before insert on loct2
+ 	for each row execute procedure br_insert_trigfunc();
+ 
+ -- The new values are concatenated with ' triggered !'
+ insert into itrtest values (1, 'foo') returning *;
+ insert into itrtest values (2, 'qux') returning *;
+ insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+ with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+ 
+ drop trigger loct1_br_insert_trigger on loct1;
+ drop trigger loct2_br_insert_trigger on loct2;
+ 
  drop table itrtest;
  drop table loct1;
  drop table loct2;
***************
*** 1836,1841 **** select tableoid::regclass, * FROM locp;
--- 1861,1888 ----
  -- The executor should not let unexercised FDWs shut down
  update utrtest set a = 1 where b = 'foo';
  
+ -- Test that remote triggers work with update tuple routing
+ create trigger loct_br_insert_trigger before insert on loct
+ 	for each row execute procedure br_insert_trigfunc();
+ 
+ delete from utrtest;
+ insert into utrtest values (2, 'qux');
+ 
+ -- Check case where the foreign partition is a subplan target rel
+ explain (verbose, costs off)
+ update utrtest set a = 1 where a = 1 or a = 2 returning *;
+ -- The new values are concatenated with ' triggered !'
+ update utrtest set a = 1 where a = 1 or a = 2 returning *;
+ 
+ delete from utrtest;
+ insert into utrtest values (2, 'qux');
+ 
+ -- Check case where the foreign partition isn't a subplan target rel
+ explain (verbose, costs off)
+ update utrtest set a = 1 where a = 2 returning *;
+ -- The new values are concatenated with ' triggered !'
+ update utrtest set a = 1 where a = 2 returning *;
+ 
  drop table utrtest;
  drop table loct;
  
*** a/src/backend/executor/execPartition.c
--- b/src/backend/executor/execPartition.c
***************
*** 330,335 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 330,342 ----
  					  estate->es_instrument);
  
  	/*
+ 	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
+ 	 * partition-key becomes a DELETE+INSERT operation, so this check is still
+ 	 * required when the operation is CMD_UPDATE.
+ 	 */
+ 	CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+ 
+ 	/*
  	 * Since we've just initialized this ResultRelInfo, it's not in any list
  	 * attached to the estate as yet.  Add it, so that it can be found later.
  	 *
***************
*** 341,349 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
  		lappend(estate->es_tuple_routing_result_relations,
  				leaf_part_rri);
  
- 	/* Set up information needed for routing tuples to this partition. */
- 	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
- 
  	/*
  	 * Open partition indices.  The user may have asked to check for conflicts
  	 * within this leaf partition and do "nothing" instead of throwing an
--- 348,353 ----
***************
*** 466,471 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 470,478 ----
  									&mtstate->ps, RelationGetDescr(partrel));
  	}
  
+ 	/* Set up information needed for routing tuples to the partition. */
+ 	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
+ 
  	/*
  	 * If there is an ON CONFLICT clause, initialize state for it.
  	 */
***************
*** 612,619 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
  
  /*
   * ExecInitRoutingInfo
!  *		Set up information needed for routing tuples to a leaf partition if
!  *		routable; else abort the operation
   */
  void
  ExecInitRoutingInfo(ModifyTableState *mtstate,
--- 619,625 ----
  
  /*
   * ExecInitRoutingInfo
!  *		Set up information needed for routing tuples to a leaf partition
   */
  void
  ExecInitRoutingInfo(ModifyTableState *mtstate,
***************
*** 624,632 **** ExecInitRoutingInfo(ModifyTableState *mtstate,
  {
  	MemoryContext oldContext;
  
- 	/* Verify the partition is a valid target for INSERT */
- 	CheckValidResultRel(partRelInfo, CMD_INSERT);
- 
  	/*
  	 * Switch into per-query memory context.
  	 */
--- 630,635 ----
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
***************
*** 1700,1719 **** ExecPrepareTupleRouting(ModifyTableState *mtstate,
  										partidx);
  
  	/*
! 	 * Set up information needed for routing tuples to the partition if we
! 	 * didn't yet (ExecInitRoutingInfo would abort the operation if the
! 	 * partition isn't routable).
  	 *
  	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
! 	 * tuple to a new partition.  This setup would be needed for a subplan
  	 * partition of such an UPDATE that is chosen as the partition to route
! 	 * the tuple to.  The reason we do this setup here rather than in
  	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
  	 * unnecessarily due to non-routable subplan partitions that may not be
  	 * chosen for update tuple movement after all.
  	 */
  	if (!partrel->ri_PartitionReadyForRouting)
  		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
  
  	/*
  	 * Make it look like we are inserting into the partition.
--- 1700,1723 ----
  										partidx);
  
  	/*
! 	 * Check whether the partition is routable if we didn't yet
  	 *
  	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
! 	 * tuple to a new partition.  This check would be applied to a subplan
  	 * partition of such an UPDATE that is chosen as the partition to route
! 	 * the tuple to.  The reason we do this check here rather than in
  	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
  	 * unnecessarily due to non-routable subplan partitions that may not be
  	 * chosen for update tuple movement after all.
  	 */
  	if (!partrel->ri_PartitionReadyForRouting)
+ 	{
+ 		/* Verify the partition is a valid target for INSERT. */
+ 		CheckValidResultRel(partrel, CMD_INSERT);
+ 
+ 		/* Set up information needed for routing tuples to the partition. */
  		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
+ 	}
  
  	/*
  	 * Make it look like we are inserting into the partition.
#9Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Etsuro Fujita (#8)
Re: Oddity in tuple routing for foreign partitions

On 2018/04/18 21:55, Etsuro Fujita wrote:

(2018/04/18 14:44), Amit Langote wrote:

Oh, I didn't really consider this part carefully.  That the resultRelInfo
received by BeginForeignInsert (when called by ExecInitRoutingInfo) could
be a reused UPDATE result relation.  It might be possible to justify the
parent_rte/child_rte terminology by explaining it a bit better.

2. This is UPDATE and the resultRelInfo that's received in
    BeginForeignInsert has been freshly created in ExecInitPartitionInfo
    and it bears node->nominalRelation or 1 as its ri_RangeTableIndex

[ ... ]

In all three cases, I think we can rely on using ri_RangeTableIndex to
fetch a valid "parent" RTE from es_range_table.

I slept on this, I noticed there is another bug in case #2.  Here is an
example with the previous patch:

postgres=# create table utrtest (a int, b text) partition by list (a);
postgres=# create table loct (a int check (a in (1)), b text);
postgres=# create foreign table remp (a int check (a in (1)), b text)
server loopback options (table_name 'loct');
postgres=# create table locp (a int check (a in (2)), b text);
postgres=# alter table utrtest attach partition remp for values in (1);
postgres=# alter table utrtest attach partition locp for values in (2);
postgres=# create trigger loct_br_insert_trigger before insert on loct for
each row execute procedure br_insert_trigfunc();

where the trigger function is the one defined in an earlier email.

postgres=# insert into utrtest values (2, 'qux');
INSERT 0 1
postgres=# update utrtest set a = 1 where a = 2 returning *;
 a |  b
---+-----
 1 | qux
(1 row)

UPDATE 1

Wrong result!  The result should be concatenated with ' triggered !'.

In case #2, since we initialize expressions for the partition's
ResultRelInfo including RETURNING by translating the attnos of the
corresponding expressions in the ResultRelInfo for the first subplan
target rel, I think we should replace the RTE for the first subplan target
rel, not the RTE for the nominal rel, with the new one created for the
foreign table.

Ah, so in case #2, we use firstVarno (per ExecInitPartitionInfo terms) as
varno throughout. So, we'd like to put the new RTE in that slot.

Would it be a good idea to explain *why* we need to replace the RTE in the
first place? Afaics, it's for deparseColumnRef() to find the correct
relation when it uses planner_rt_fetch() to get the RTE.

Attached is an updated version for fixing this issue.

Do you think we need to clarify this in a comment?

Yeah, but I updated comments about this a little bit different way in the
attached.  Does that make sense?

It looks generally good, although in the following:

+    /*
+     * If the foreign table is a partition, temporarily replace the parent's
+     * RTE in the range table with a new target RTE describing the foreign
+     * table for use by deparseInsertSql() and create_foreign_modify() below.
+     */

.. it could be mentioned that we don't switch either the RTE or the value
assigned to resultRelation if the RTE currently at resultRelation RT index
is the one created by the planner and refers to the same relation that
resultRelInfo does.

Beside that, I noticed that the patch has a stray white-space at the end
of the following line:

+ /* partition that isn't a subplan target rel */

Thanks,
Amit

#10Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#6)
Re: Oddity in tuple routing for foreign partitions

At Tue, 17 Apr 2018 16:41:31 +0900, Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp> wrote in <5AD5A52B.7050206@lab.ntt.co.jp>

(2018/04/17 16:10), Amit Langote wrote:

On 2018/04/17 11:13, Kyotaro HORIGUCHI wrote:

If I'm reading this correctly, ExecInitParititionInfo calls
ExecInitRoutingInfo so currently CheckValidityResultRel is called
for the child when partrel is created in ExecPrepareTupleRouting.
But the move of CheckValidResultRel results in letting partrel
just created in ExecPrepareTupleRouting not be checked at all
since ri_ParititionReadyForRouting is always set true in
ExecInitPartitionInfo.

I thought the same upon reading the email, but it seems that the patch
does add CheckValidResultRel() in ExecInitPartitionInfo() as well:

@@ -335,6 +335,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
estate->es_instrument);

/*
+ * Verify result relation is a valid target for an INSERT.  An UPDATE
of a
+ * partition-key becomes a DELETE+INSERT operation, so this check is
still
+     * required when the operation is CMD_UPDATE.
+     */
+    CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+
+    /*
* Since we've just initialized this ResultRelInfo, it's not in any
* list
* attached to the estate as yet.  Add it, so that it can be found
* later.
*

That's right. So, the validity check would be applied to a partition
created in ExecPrepareTupleRouting as well.

Yes, that's actually true but it seems quite bug-prone or at
least hard to understand. I understand that the
CheckValidResultRel(INSERT) is just a prerequisite for
ExecInitRoutingInfo but the relationship is obfucated after this
patch. If we have a strong reason to error-out as fast as
possible, the original code seems better to me..

Another
thing I noticed is the RT index of the foreign partition set to 1 in
postgresBeginForeignInsert to create a remote query; that would not
work
for cases where the partitioned table has an RT index larger than 1
(eg,
the partitioned table is not the primary ModifyTable node); in which
cases, since the varno of Vars belonging to the foreign partition in
the
RETURNING expressions is the same as the partitioned table, calling
deparseReturningList with the RT index set to 1 against the RETURNING
expressions would produce attrs_used to be NULL, leading to
postgres_fdw
not retrieving actually inserted data from the remote, even if remote
triggers might change those data. So, I fixed this as well, by
setting
the RT index accordingly to the partitioned table.

Check.

I'm not sure but is it true that the parent is always at
resultRelation - 1?

Unless I'm missing something, EState.es_range_table contains *all*
range
table entries that were created by the planner. So, if the planner
assigned resultRelation as the RT index for the parent, then it seems
we
can rely on getting the same entry back with
list_nth_cell(estate->es_range_table, resultRelation - 1). That seems
true even if the parent wasn't the *primary* target relation. For
example, the following works with the patch:

-- in addition to the tables shown in Fujita-san's email, define one more
-- local partition
create table locp2 partition of itrtest for values in (2);

-- simple case
insert into itrtest values (1, 'foo') returning *;
a | b
---+-----------------
1 | foo triggered !
(1 row)

-- itrtest (the parent) appears as both the primary target relation and
-- otherwise. The latter in the form of being mentioned in the with
-- query

with ins (a, b) as (insert into itrtest values (1, 'foo') returning *)
insert into itrtest select a + 1, b from ins returning *;
a | b
---+-----------------
2 | foo triggered !
(1 row)

I agree with that. Thanks for the explanation and the example, Amit!

Maybe my explanation (or the naming parent_rte/child_rte in the patch)
was not necessarily good, so I guess that that confused
horiguchi-san. Let me explain. In the INSERT/COPY-tuple-routing case,
as explained by Amit, the RTE at that position in the EState's range
table is the one for the partitioned table of a given partition, so
the statement would be true. BUT in the UPDATE-tuple-routing case,
the RTE is the one for the given partition, not for the partitioned
table, so the statement would not be true. In the latter case, we
don't need to create a child RTE and replace the original RTE with it,
but I handled both cases the same way for simplicity.

Thank you, your understanding of my concern is right. Thanks for
the explanation.

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#11Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Amit Langote (#9)
1 attachment(s)
Re: Oddity in tuple routing for foreign partitions

(2018/04/19 16:43), Amit Langote wrote:

On 2018/04/18 21:55, Etsuro Fujita wrote:

(2018/04/18 14:44), Amit Langote wrote:

That the resultRelInfo
received by BeginForeignInsert (when called by ExecInitRoutingInfo) could
be a reused UPDATE result relation.

2. This is UPDATE and the resultRelInfo that's received in
BeginForeignInsert has been freshly created in ExecInitPartitionInfo
and it bears node->nominalRelation or 1 as its ri_RangeTableIndex

In all three cases, I think we can rely on using ri_RangeTableIndex to
fetch a valid "parent" RTE from es_range_table.

I slept on this, I noticed there is another bug in case #2.

In case #2, since we initialize expressions for the partition's
ResultRelInfo including RETURNING by translating the attnos of the
corresponding expressions in the ResultRelInfo for the first subplan
target rel, I think we should replace the RTE for the first subplan target
rel, not the RTE for the nominal rel, with the new one created for the
foreign table.

Ah, so in case #2, we use firstVarno (per ExecInitPartitionInfo terms) as
varno throughout. So, we'd like to put the new RTE in that slot.

Would it be a good idea to explain *why* we need to replace the RTE in the
first place? Afaics, it's for deparseColumnRef() to find the correct
relation when it uses planner_rt_fetch() to get the RTE.

That might be a good idea, but one thing I'm concerned about is that
since we might use the RTE in different ways in future developments,
such a comment might be obsolete rather sooner. So, I just added *for
use by deparseInsertSql() and create_foreign_modify() below* to the
comments shown below. But I'd like to leave this to the committer.

Attached is an updated version for fixing this issue.

Do you think we need to clarify this in a comment?

Yeah, but I updated comments about this a little bit different way in the
attached. Does that make sense?

It looks generally good, although in the following:

+    /*
+     * If the foreign table is a partition, temporarily replace the parent's
+     * RTE in the range table with a new target RTE describing the foreign
+     * table for use by deparseInsertSql() and create_foreign_modify() below.
+     */

.. it could be mentioned that we don't switch either the RTE or the value
assigned to resultRelation if the RTE currently at resultRelation RT index
is the one created by the planner and refers to the same relation that
resultRelInfo does.

Done.

Beside that, I noticed that the patch has a stray white-space at the end
of the following line:

+ /* partition that isn't a subplan target rel */

Fixed.

Thanks for the review! Attached is a new version of the patch.

Best regards,
Etsuro Fujita

Attachments:

fix-tuple-routing-for-foreign-partitions-3.patchtext/x-diff; name=fix-tuple-routing-for-foreign-partitions-3.patchDownload
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 7454,7459 **** select tableoid::regclass, * FROM itrtest;
--- 7454,7501 ----
   remp1    | 1 | foo
  (1 row)
  
+ delete from itrtest;
+ drop index loct1_idx;
+ -- Test that remote triggers work with insert tuple routing
+ create function br_insert_trigfunc() returns trigger as $$
+ begin
+ 	new.b := new.b || ' triggered !';
+ 	return new;
+ end
+ $$ language plpgsql;
+ create trigger loct1_br_insert_trigger before insert on loct1
+ 	for each row execute procedure br_insert_trigfunc();
+ create trigger loct2_br_insert_trigger before insert on loct2
+ 	for each row execute procedure br_insert_trigfunc();
+ -- The new values are concatenated with ' triggered !'
+ insert into itrtest values (1, 'foo') returning *;
+  a |        b        
+ ---+-----------------
+  1 | foo triggered !
+ (1 row)
+ 
+ insert into itrtest values (2, 'qux') returning *;
+  a |        b        
+ ---+-----------------
+  2 | qux triggered !
+ (1 row)
+ 
+ insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+  a |         b         
+ ---+-------------------
+  1 | test1 triggered !
+  2 | test2 triggered !
+ (2 rows)
+ 
+ with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+  a |         b         
+ ---+-------------------
+  1 | test1 triggered !
+  2 | test2 triggered !
+ (2 rows)
+ 
+ drop trigger loct1_br_insert_trigger on loct1;
+ drop trigger loct2_br_insert_trigger on loct2;
  drop table itrtest;
  drop table loct1;
  drop table loct2;
***************
*** 7518,7523 **** select tableoid::regclass, * FROM locp;
--- 7560,7616 ----
  
  -- The executor should not let unexercised FDWs shut down
  update utrtest set a = 1 where b = 'foo';
+ -- Test that remote triggers work with update tuple routing
+ create trigger loct_br_insert_trigger before insert on loct
+ 	for each row execute procedure br_insert_trigfunc();
+ delete from utrtest;
+ insert into utrtest values (2, 'qux');
+ -- Check case where the foreign partition is a subplan target rel
+ explain (verbose, costs off)
+ update utrtest set a = 1 where a = 1 or a = 2 returning *;
+                                           QUERY PLAN                                          
+ ----------------------------------------------------------------------------------------------
+  Update on public.utrtest
+    Output: remp.a, remp.b
+    Foreign Update on public.remp
+    Update on public.locp
+    ->  Foreign Update on public.remp
+          Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b
+    ->  Seq Scan on public.locp
+          Output: 1, locp.b, locp.ctid
+          Filter: ((locp.a = 1) OR (locp.a = 2))
+ (9 rows)
+ 
+ -- The new values are concatenated with ' triggered !'
+ update utrtest set a = 1 where a = 1 or a = 2 returning *;
+  a |        b        
+ ---+-----------------
+  1 | qux triggered !
+ (1 row)
+ 
+ delete from utrtest;
+ insert into utrtest values (2, 'qux');
+ -- Check case where the foreign partition isn't a subplan target rel
+ explain (verbose, costs off)
+ update utrtest set a = 1 where a = 2 returning *;
+               QUERY PLAN              
+ --------------------------------------
+  Update on public.utrtest
+    Output: locp.a, locp.b
+    Update on public.locp
+    ->  Seq Scan on public.locp
+          Output: 1, locp.b, locp.ctid
+          Filter: (locp.a = 2)
+ (6 rows)
+ 
+ -- The new values are concatenated with ' triggered !'
+ update utrtest set a = 1 where a = 2 returning *;
+  a |        b        
+ ---+-----------------
+  1 | qux triggered !
+ (1 row)
+ 
+ drop trigger loct_br_insert_trigger on loct;
  drop table utrtest;
  drop table loct;
  -- Test copy tuple routing
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 1974,1982 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  						   ResultRelInfo *resultRelInfo)
  {
  	PgFdwModifyState *fmstate;
! 	Plan	   *plan = mtstate->ps.plan;
  	Relation	rel = resultRelInfo->ri_RelationDesc;
- 	RangeTblEntry *rte;
  	Query	   *query;
  	PlannerInfo *root;
  	TupleDesc	tupdesc = RelationGetDescr(rel);
--- 1974,1983 ----
  						   ResultRelInfo *resultRelInfo)
  {
  	PgFdwModifyState *fmstate;
! 	ModifyTable *plan = (ModifyTable *) mtstate->ps.plan;
! 	EState	   *estate = mtstate->ps.state;
! 	Index		resultRelation = resultRelInfo->ri_RangeTableIndex;
  	Relation	rel = resultRelInfo->ri_RelationDesc;
  	Query	   *query;
  	PlannerInfo *root;
  	TupleDesc	tupdesc = RelationGetDescr(rel);
***************
*** 1985,2002 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  	List	   *targetAttrs = NIL;
  	List	   *retrieved_attrs = NIL;
  	bool		doNothing = false;
  
  	initStringInfo(&sql);
  
  	/* Set up largely-dummy planner state. */
- 	rte = makeNode(RangeTblEntry);
- 	rte->rtekind = RTE_RELATION;
- 	rte->relid = RelationGetRelid(rel);
- 	rte->relkind = RELKIND_FOREIGN_TABLE;
  	query = makeNode(Query);
  	query->commandType = CMD_INSERT;
! 	query->resultRelation = 1;
! 	query->rtable = list_make1(rte);
  	root = makeNode(PlannerInfo);
  	root->parse = query;
  
--- 1986,2049 ----
  	List	   *targetAttrs = NIL;
  	List	   *retrieved_attrs = NIL;
  	bool		doNothing = false;
+ 	ListCell   *lc = NULL;
+ 	RangeTblEntry *old_rte = NULL;
+ 	bool		need_replace = false;
  
  	initStringInfo(&sql);
  
+ 	/*
+ 	 * If the foreign table is a partition, temporarily replace the parent's
+ 	 * RTE in the range table with a new target RTE describing the foreign
+ 	 * table for use by deparseInsertSql() and create_foreign_modify() below.
+ 	 * However, if this is invoked by UPDATE, we need special handling: if
+ 	 * the partition is one of the UPDATE subplan target rels, those can just
+ 	 * use the target rel's RTE as-is, so don't do anything; else replace the
+ 	 * RTE for the first subplan target rel with the new RTE since core code
+ 	 * would have built expressions for the partition such as RETURNING so
+ 	 * that the partition has the same varno as the first subplan target rel.
+ 	 */
+ 	if (resultRelInfo->ri_PartitionRoot)
+ 	{
+ 		need_replace = true;
+ 
+ 		if (plan && plan->operation == CMD_UPDATE)
+ 		{
+ 			if (resultRelation != plan->nominalRelation)
+ 			{
+ 				/*
+ 				 * partition that is a subplan target rel - don't do anything
+ 				 */
+ 				need_replace = false;
+ 			}
+ 			else
+ 			{
+ 				/*
+ 				 * partition that isn't a subplan target rel - replace the
+ 				 * the first subplan target rel's RTE with the new RTE
+ 				 */
+ 				resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+ 			}
+ 		}
+ 
+ 		if (need_replace)
+ 		{
+ 			RangeTblEntry *new_rte;
+ 
+ 			lc = list_nth_cell(estate->es_range_table, resultRelation - 1);
+ 			old_rte = lfirst(lc);
+ 			new_rte = copyObject(old_rte);
+ 			new_rte->relid = RelationGetRelid(rel);
+ 			new_rte->relkind = RELKIND_FOREIGN_TABLE;
+ 			lfirst(lc) = new_rte;
+ 		}
+ 	}
+ 
  	/* Set up largely-dummy planner state. */
  	query = makeNode(Query);
  	query->commandType = CMD_INSERT;
! 	query->resultRelation = resultRelation;
! 	query->rtable = estate->es_range_table;
  	root = makeNode(PlannerInfo);
  	root->parse = query;
  
***************
*** 2012,2018 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  	/* Check if we add the ON CONFLICT clause to the remote query. */
  	if (plan)
  	{
! 		OnConflictAction onConflictAction = ((ModifyTable *) plan)->onConflictAction;
  
  		/* We only support DO NOTHING without an inference specification. */
  		if (onConflictAction == ONCONFLICT_NOTHING)
--- 2059,2065 ----
  	/* Check if we add the ON CONFLICT clause to the remote query. */
  	if (plan)
  	{
! 		OnConflictAction onConflictAction = plan->onConflictAction;
  
  		/* We only support DO NOTHING without an inference specification. */
  		if (onConflictAction == ONCONFLICT_NOTHING)
***************
*** 2023,2033 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  	}
  
  	/* Construct the SQL command string. */
! 	deparseInsertSql(&sql, root, 1, rel, targetAttrs, doNothing,
  					 resultRelInfo->ri_returningList, &retrieved_attrs);
  
  	/* Construct an execution state. */
! 	fmstate = create_foreign_modify(mtstate->ps.state,
  									resultRelInfo,
  									CMD_INSERT,
  									NULL,
--- 2070,2080 ----
  	}
  
  	/* Construct the SQL command string. */
! 	deparseInsertSql(&sql, root, resultRelation, rel, targetAttrs, doNothing,
  					 resultRelInfo->ri_returningList, &retrieved_attrs);
  
  	/* Construct an execution state. */
! 	fmstate = create_foreign_modify(estate,
  									resultRelInfo,
  									CMD_INSERT,
  									NULL,
***************
*** 2036,2041 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
--- 2083,2092 ----
  									retrieved_attrs != NIL,
  									retrieved_attrs);
  
+ 	/* Restore the saved RTE. */
+ 	if (need_replace)
+ 		lfirst(lc) = old_rte;
+ 
  	resultRelInfo->ri_FdwState = fmstate;
  }
  
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 1804,1809 **** insert into itrtest values (1, 'bar') on conflict (a) do update set b = excluded
--- 1804,1834 ----
  
  select tableoid::regclass, * FROM itrtest;
  
+ delete from itrtest;
+ 
+ drop index loct1_idx;
+ 
+ -- Test that remote triggers work with insert tuple routing
+ create function br_insert_trigfunc() returns trigger as $$
+ begin
+ 	new.b := new.b || ' triggered !';
+ 	return new;
+ end
+ $$ language plpgsql;
+ create trigger loct1_br_insert_trigger before insert on loct1
+ 	for each row execute procedure br_insert_trigfunc();
+ create trigger loct2_br_insert_trigger before insert on loct2
+ 	for each row execute procedure br_insert_trigfunc();
+ 
+ -- The new values are concatenated with ' triggered !'
+ insert into itrtest values (1, 'foo') returning *;
+ insert into itrtest values (2, 'qux') returning *;
+ insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+ with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+ 
+ drop trigger loct1_br_insert_trigger on loct1;
+ drop trigger loct2_br_insert_trigger on loct2;
+ 
  drop table itrtest;
  drop table loct1;
  drop table loct2;
***************
*** 1836,1841 **** select tableoid::regclass, * FROM locp;
--- 1861,1890 ----
  -- The executor should not let unexercised FDWs shut down
  update utrtest set a = 1 where b = 'foo';
  
+ -- Test that remote triggers work with update tuple routing
+ create trigger loct_br_insert_trigger before insert on loct
+ 	for each row execute procedure br_insert_trigfunc();
+ 
+ delete from utrtest;
+ insert into utrtest values (2, 'qux');
+ 
+ -- Check case where the foreign partition is a subplan target rel
+ explain (verbose, costs off)
+ update utrtest set a = 1 where a = 1 or a = 2 returning *;
+ -- The new values are concatenated with ' triggered !'
+ update utrtest set a = 1 where a = 1 or a = 2 returning *;
+ 
+ delete from utrtest;
+ insert into utrtest values (2, 'qux');
+ 
+ -- Check case where the foreign partition isn't a subplan target rel
+ explain (verbose, costs off)
+ update utrtest set a = 1 where a = 2 returning *;
+ -- The new values are concatenated with ' triggered !'
+ update utrtest set a = 1 where a = 2 returning *;
+ 
+ drop trigger loct_br_insert_trigger on loct;
+ 
  drop table utrtest;
  drop table loct;
  
*** a/src/backend/executor/execPartition.c
--- b/src/backend/executor/execPartition.c
***************
*** 330,335 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 330,342 ----
  					  estate->es_instrument);
  
  	/*
+ 	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
+ 	 * partition-key becomes a DELETE+INSERT operation, so this check is still
+ 	 * required when the operation is CMD_UPDATE.
+ 	 */
+ 	CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+ 
+ 	/*
  	 * Since we've just initialized this ResultRelInfo, it's not in any list
  	 * attached to the estate as yet.  Add it, so that it can be found later.
  	 *
***************
*** 341,349 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
  		lappend(estate->es_tuple_routing_result_relations,
  				leaf_part_rri);
  
- 	/* Set up information needed for routing tuples to this partition. */
- 	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
- 
  	/*
  	 * Open partition indices.  The user may have asked to check for conflicts
  	 * within this leaf partition and do "nothing" instead of throwing an
--- 348,353 ----
***************
*** 466,471 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 470,478 ----
  									&mtstate->ps, RelationGetDescr(partrel));
  	}
  
+ 	/* Set up information needed for routing tuples to the partition. */
+ 	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
+ 
  	/*
  	 * If there is an ON CONFLICT clause, initialize state for it.
  	 */
***************
*** 612,619 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
  
  /*
   * ExecInitRoutingInfo
!  *		Set up information needed for routing tuples to a leaf partition if
!  *		routable; else abort the operation
   */
  void
  ExecInitRoutingInfo(ModifyTableState *mtstate,
--- 619,625 ----
  
  /*
   * ExecInitRoutingInfo
!  *		Set up information needed for routing tuples to a leaf partition
   */
  void
  ExecInitRoutingInfo(ModifyTableState *mtstate,
***************
*** 624,632 **** ExecInitRoutingInfo(ModifyTableState *mtstate,
  {
  	MemoryContext oldContext;
  
- 	/* Verify the partition is a valid target for INSERT */
- 	CheckValidResultRel(partRelInfo, CMD_INSERT);
- 
  	/*
  	 * Switch into per-query memory context.
  	 */
--- 630,635 ----
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
***************
*** 1700,1719 **** ExecPrepareTupleRouting(ModifyTableState *mtstate,
  										partidx);
  
  	/*
! 	 * Set up information needed for routing tuples to the partition if we
! 	 * didn't yet (ExecInitRoutingInfo would abort the operation if the
! 	 * partition isn't routable).
  	 *
  	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
! 	 * tuple to a new partition.  This setup would be needed for a subplan
  	 * partition of such an UPDATE that is chosen as the partition to route
! 	 * the tuple to.  The reason we do this setup here rather than in
  	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
  	 * unnecessarily due to non-routable subplan partitions that may not be
  	 * chosen for update tuple movement after all.
  	 */
  	if (!partrel->ri_PartitionReadyForRouting)
  		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
  
  	/*
  	 * Make it look like we are inserting into the partition.
--- 1700,1723 ----
  										partidx);
  
  	/*
! 	 * Check whether the partition is routable if we didn't yet
  	 *
  	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
! 	 * tuple to a new partition.  This check would be applied to a subplan
  	 * partition of such an UPDATE that is chosen as the partition to route
! 	 * the tuple to.  The reason we do this check here rather than in
  	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
  	 * unnecessarily due to non-routable subplan partitions that may not be
  	 * chosen for update tuple movement after all.
  	 */
  	if (!partrel->ri_PartitionReadyForRouting)
+ 	{
+ 		/* Verify the partition is a valid target for INSERT. */
+ 		CheckValidResultRel(partrel, CMD_INSERT);
+ 
+ 		/* Set up information needed for routing tuples to the partition. */
  		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
+ 	}
  
  	/*
  	 * Make it look like we are inserting into the partition.
#12Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Kyotaro HORIGUCHI (#10)
Re: Oddity in tuple routing for foreign partitions

(2018/04/19 19:03), Kyotaro HORIGUCHI wrote:

At Tue, 17 Apr 2018 16:41:31 +0900, Etsuro Fujita<fujita.etsuro@lab.ntt.co.jp> wrote in<5AD5A52B.7050206@lab.ntt.co.jp>

(2018/04/17 16:10), Amit Langote wrote:

On 2018/04/17 11:13, Kyotaro HORIGUCHI wrote:

If I'm reading this correctly, ExecInitParititionInfo calls
ExecInitRoutingInfo so currently CheckValidityResultRel is called
for the child when partrel is created in ExecPrepareTupleRouting.
But the move of CheckValidResultRel results in letting partrel
just created in ExecPrepareTupleRouting not be checked at all
since ri_ParititionReadyForRouting is always set true in
ExecInitPartitionInfo.

I thought the same upon reading the email, but it seems that the patch
does add CheckValidResultRel() in ExecInitPartitionInfo() as well:

@@ -335,6 +335,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
estate->es_instrument);

/*
+ * Verify result relation is a valid target for an INSERT.  An UPDATE
of a
+ * partition-key becomes a DELETE+INSERT operation, so this check is
still
+     * required when the operation is CMD_UPDATE.
+     */
+    CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+
+    /*
* Since we've just initialized this ResultRelInfo, it's not in any
* list
* attached to the estate as yet.  Add it, so that it can be found
* later.
*

That's right. So, the validity check would be applied to a partition
created in ExecPrepareTupleRouting as well.

Yes, that's actually true but it seems quite bug-prone or at
least hard to understand. I understand that the
CheckValidResultRel(INSERT) is just a prerequisite for
ExecInitRoutingInfo but the relationship is obfucated after this
patch. If we have a strong reason to error-out as fast as
possible, the original code seems better to me..

Actually, I think that change would make the initialization for a
partition more consistent with that for a main target rel in
ExecInitModifyTable, where we first perform the CheckValidResultRel
check against a target rel, and if valid, then open indices, initializes
the FDW, initialize expressions such as WITH CHECK OPTION and RETURNING,
and so on. That's reasonable, and I like consistency, so I'd vote for
that change.

Thanks for the review!

Best regards,
Etsuro Fujita

#13Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Etsuro Fujita (#11)
Re: Oddity in tuple routing for foreign partitions

Fujita-san,

Thanks for the updated patch.

On 2018/04/19 21:42, Etsuro Fujita wrote:

(2018/04/19 16:43), Amit Langote wrote:

Would it be a good idea to explain *why* we need to replace the RTE in the
first place?  Afaics, it's for deparseColumnRef() to find the correct
relation when it uses planner_rt_fetch() to get the RTE.

That might be a good idea, but one thing I'm concerned about is that since
we might use the RTE in different ways in future developments, such a
comment might be obsolete rather sooner.  So, I just added *for use by
deparseInsertSql() and create_foreign_modify() below* to the comments
shown below.  But I'd like to leave this to the committer.

OK, fine by me.

It looks generally good, although in the following:

+    /*
+     * If the foreign table is a partition, temporarily replace the
parent's
+     * RTE in the range table with a new target RTE describing the foreign
+     * table for use by deparseInsertSql() and create_foreign_modify()
below.
+     */

.. it could be mentioned that we don't switch either the RTE or the value
assigned to resultRelation if the RTE currently at resultRelation RT index
is the one created by the planner and refers to the same relation that
resultRelInfo does.

Done.

Beside that, I noticed that the patch has a stray white-space at the end
of the following line:

+                /* partition that isn't a subplan target rel */

Fixed.

Thanks for the review!  Attached is a new version of the patch.

Looks good.

Thanks,
Amit

#14Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Amit Langote (#13)
Re: Oddity in tuple routing for foreign partitions

(2018/04/20 9:48), Amit Langote wrote:

On 2018/04/19 21:42, Etsuro Fujita wrote:

(2018/04/19 16:43), Amit Langote wrote:

Would it be a good idea to explain *why* we need to replace the RTE in the
first place? Afaics, it's for deparseColumnRef() to find the correct
relation when it uses planner_rt_fetch() to get the RTE.

That might be a good idea, but one thing I'm concerned about is that since
we might use the RTE in different ways in future developments, such a
comment might be obsolete rather sooner. So, I just added *for use by
deparseInsertSql() and create_foreign_modify() below* to the comments
shown below. But I'd like to leave this to the committer.

OK, fine by me.

It looks generally good, although in the following:

+    /*
+     * If the foreign table is a partition, temporarily replace the
parent's
+     * RTE in the range table with a new target RTE describing the foreign
+     * table for use by deparseInsertSql() and create_foreign_modify()
below.
+     */

.. it could be mentioned that we don't switch either the RTE or the value
assigned to resultRelation if the RTE currently at resultRelation RT index
is the one created by the planner and refers to the same relation that
resultRelInfo does.

Done.

Beside that, I noticed that the patch has a stray white-space at the end
of the following line:

+ /* partition that isn't a subplan target rel */

Fixed.

Thanks for the review! Attached is a new version of the patch.

Looks good.

Thank you!

Best regards,
Etsuro Fujita

#15Alvaro Herrera
alvherre@alvh.no-ip.org
In reply to: Etsuro Fujita (#11)
Re: Oddity in tuple routing for foreign partitions

Robert, I think this is your turf, per 3d956d9562aa. Are you looking
into it?

Thanks,

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

#16Robert Haas
robertmhaas@gmail.com
In reply to: Alvaro Herrera (#15)
Re: Oddity in tuple routing for foreign partitions

On Mon, Apr 23, 2018 at 11:25 AM, Alvaro Herrera
<alvherre@alvh.no-ip.org> wrote:

Robert, I think this is your turf, per 3d956d9562aa. Are you looking
into it?

Thanks for the ping. I just had a look over the proposed patch and I
guess I don't like it very much. Temporarily modifying the range
table in place and then changing it back before we return seems ugly
and error-prone. I hope we can come up with a solution that doesn't
involve needing to do that.

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

#17Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#16)
1 attachment(s)
Re: Oddity in tuple routing for foreign partitions

On Tue, Apr 24, 2018 at 12:21 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Apr 23, 2018 at 11:25 AM, Alvaro Herrera
<alvherre@alvh.no-ip.org> wrote:

Robert, I think this is your turf, per 3d956d9562aa. Are you looking
into it?

Thanks for the ping. I just had a look over the proposed patch and I
guess I don't like it very much. Temporarily modifying the range
table in place and then changing it back before we return seems ugly
and error-prone. I hope we can come up with a solution that doesn't
involve needing to do that.

I have done some refactoring to avoid that. See attached.

I didn't really get beyond the refactoring stage with this today.
This version still seems to work, but I don't really understand the
logic in postgresBeginForeignInsert which decides whether to use the
RTE from the range table or create our own. We seem to need to do one
sometimes and the other sometimes, but I don't know why that is,
really. Nor do I understand why we need the other changes in the
patch. There's probably a good reason, but I haven't figured it out
yet.

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

Attachments:

fix-tuple-routing-for-foreign-partitions-4.patchapplication/octet-stream; name=fix-tuple-routing-for-foreign-partitions-4.patchDownload
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 6e2fa1420c..d272719ff4 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -125,7 +125,7 @@ static char *deparse_type_name(Oid type_oid, int32 typemod);
  * Functions to construct string representation of a node tree.
  */
 static void deparseTargetList(StringInfo buf,
-				  PlannerInfo *root,
+				  RangeTblEntry *rte,
 				  Index rtindex,
 				  Relation rel,
 				  bool is_returning,
@@ -137,13 +137,13 @@ static void deparseExplicitTargetList(List *tlist,
 						  List **retrieved_attrs,
 						  deparse_expr_cxt *context);
 static void deparseSubqueryTargetList(deparse_expr_cxt *context);
-static void deparseReturningList(StringInfo buf, PlannerInfo *root,
+static void deparseReturningList(StringInfo buf, RangeTblEntry *rte,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
 					 List **retrieved_attrs);
 static void deparseColumnRef(StringInfo buf, int varno, int varattno,
-				 PlannerInfo *root, bool qualify_col);
+				 RangeTblEntry *rte, bool qualify_col);
 static void deparseRelation(StringInfo buf, Relation rel);
 static void deparseExpr(Expr *expr, deparse_expr_cxt *context);
 static void deparseVar(Var *node, deparse_expr_cxt *context);
@@ -1050,7 +1050,7 @@ deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
 		 */
 		Relation	rel = heap_open(rte->relid, NoLock);
 
-		deparseTargetList(buf, root, foreignrel->relid, rel, false,
+		deparseTargetList(buf, rte, foreignrel->relid, rel, false,
 						  fpinfo->attrs_used, false, retrieved_attrs);
 		heap_close(rel, NoLock);
 	}
@@ -1099,7 +1099,7 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
  */
 static void
 deparseTargetList(StringInfo buf,
-				  PlannerInfo *root,
+				  RangeTblEntry *rte,
 				  Index rtindex,
 				  Relation rel,
 				  bool is_returning,
@@ -1137,7 +1137,7 @@ deparseTargetList(StringInfo buf,
 				appendStringInfoString(buf, " RETURNING ");
 			first = false;
 
-			deparseColumnRef(buf, rtindex, i, root, qualify_col);
+			deparseColumnRef(buf, rtindex, i, rte, qualify_col);
 
 			*retrieved_attrs = lappend_int(*retrieved_attrs, i);
 		}
@@ -1649,7 +1649,7 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  * to *retrieved_attrs.
  */
 void
-deparseInsertSql(StringInfo buf, PlannerInfo *root,
+deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing,
 				 List *returningList, List **retrieved_attrs)
@@ -1674,7 +1674,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
 				appendStringInfoString(buf, ", ");
 			first = false;
 
-			deparseColumnRef(buf, rtindex, attnum, root, false);
+			deparseColumnRef(buf, rtindex, attnum, rte, false);
 		}
 
 		appendStringInfoString(buf, ") VALUES (");
@@ -1699,7 +1699,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
 	if (doNothing)
 		appendStringInfoString(buf, " ON CONFLICT DO NOTHING");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_insert_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1712,7 +1712,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
  * to *retrieved_attrs.
  */
 void
-deparseUpdateSql(StringInfo buf, PlannerInfo *root,
+deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, List *returningList,
 				 List **retrieved_attrs)
@@ -1735,13 +1735,13 @@ deparseUpdateSql(StringInfo buf, PlannerInfo *root,
 			appendStringInfoString(buf, ", ");
 		first = false;
 
-		deparseColumnRef(buf, rtindex, attnum, root, false);
+		deparseColumnRef(buf, rtindex, attnum, rte, false);
 		appendStringInfo(buf, " = $%d", pindex);
 		pindex++;
 	}
 	appendStringInfoString(buf, " WHERE ctid = $1");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_update_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1777,6 +1777,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 	int			nestlevel;
 	bool		first;
 	ListCell   *lc;
+	RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
 
 	/* Set up context struct for recursion */
 	context.root = root;
@@ -1808,7 +1809,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 			appendStringInfoString(buf, ", ");
 		first = false;
 
-		deparseColumnRef(buf, rtindex, attnum, root, false);
+		deparseColumnRef(buf, rtindex, attnum, rte, false);
 		appendStringInfoString(buf, " = ");
 		deparseExpr((Expr *) tle->expr, &context);
 	}
@@ -1835,7 +1836,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 		deparseExplicitTargetList(returningList, true, retrieved_attrs,
 								  &context);
 	else
-		deparseReturningList(buf, root, rtindex, rel, false,
+		deparseReturningList(buf, rte, rtindex, rel, false,
 							 returningList, retrieved_attrs);
 }
 
@@ -1847,7 +1848,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  * to *retrieved_attrs.
  */
 void
-deparseDeleteSql(StringInfo buf, PlannerInfo *root,
+deparseDeleteSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *returningList,
 				 List **retrieved_attrs)
@@ -1856,7 +1857,7 @@ deparseDeleteSql(StringInfo buf, PlannerInfo *root,
 	deparseRelation(buf, rel);
 	appendStringInfoString(buf, " WHERE ctid = $1");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_delete_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1918,7 +1919,8 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
 		deparseExplicitTargetList(returningList, true, retrieved_attrs,
 								  &context);
 	else
-		deparseReturningList(buf, root, rtindex, rel, false,
+		deparseReturningList(buf, planner_rt_fetch(rtindex, root),
+							 rtindex, rel, false,
 							 returningList, retrieved_attrs);
 }
 
@@ -1926,7 +1928,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  * Add a RETURNING clause, if needed, to an INSERT/UPDATE/DELETE.
  */
 static void
-deparseReturningList(StringInfo buf, PlannerInfo *root,
+deparseReturningList(StringInfo buf, RangeTblEntry *rte,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
@@ -1952,7 +1954,7 @@ deparseReturningList(StringInfo buf, PlannerInfo *root,
 	}
 
 	if (attrs_used != NULL)
-		deparseTargetList(buf, root, rtindex, rel, true, attrs_used, false,
+		deparseTargetList(buf, rte, rtindex, rel, true, attrs_used, false,
 						  retrieved_attrs);
 	else
 		*retrieved_attrs = NIL;
@@ -2048,11 +2050,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
  * If qualify_col is true, qualify column name with the alias of relation.
  */
 static void
-deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
+deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
 				 bool qualify_col)
 {
-	RangeTblEntry *rte;
-
 	/* We support fetching the remote side's CTID and OID. */
 	if (varattno == SelfItemPointerAttributeNumber)
 	{
@@ -2077,10 +2077,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		Oid			fetchval = 0;
 
 		if (varattno == TableOidAttributeNumber)
-		{
-			rte = planner_rt_fetch(varno, root);
 			fetchval = rte->relid;
-		}
 
 		if (qualify_col)
 		{
@@ -2100,9 +2097,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		/* Required only to be passed down to deparseTargetList(). */
 		List	   *retrieved_attrs;
 
-		/* Get RangeTblEntry from array in PlannerInfo. */
-		rte = planner_rt_fetch(varno, root);
-
 		/*
 		 * The lock on the relation will be held by upper callers, so it's
 		 * fine to open it with no lock here.
@@ -2134,7 +2128,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		}
 
 		appendStringInfoString(buf, "ROW(");
-		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
+		deparseTargetList(buf, rte, varno, rel, false, attrs_used, qualify_col,
 						  &retrieved_attrs);
 		appendStringInfoChar(buf, ')');
 
@@ -2154,9 +2148,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		/* varno must not be any of OUTER_VAR, INNER_VAR and INDEX_VAR. */
 		Assert(!IS_SPECIAL_VARNO(varno));
 
-		/* Get RangeTblEntry from array in PlannerInfo. */
-		rte = planner_rt_fetch(varno, root);
-
 		/*
 		 * If it's a column of a foreign table, and it has the column_name FDW
 		 * option, use that value.
@@ -2354,7 +2345,8 @@ deparseVar(Var *node, deparse_expr_cxt *context)
 
 	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
 		deparseColumnRef(context->buf, node->varno, node->varattno,
-						 context->root, qualify_col);
+						 planner_rt_fetch(node->varno, context->root),
+						 qualify_col);
 	else
 	{
 		/* Treat like a Param */
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 53ed285875..bb6b1a8fdf 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7454,6 +7454,48 @@ select tableoid::regclass, * FROM itrtest;
  remp1    | 1 | foo
 (1 row)
 
+delete from itrtest;
+drop index loct1_idx;
+-- Test that remote triggers work with insert tuple routing
+create function br_insert_trigfunc() returns trigger as $$
+begin
+	new.b := new.b || ' triggered !';
+	return new;
+end
+$$ language plpgsql;
+create trigger loct1_br_insert_trigger before insert on loct1
+	for each row execute procedure br_insert_trigfunc();
+create trigger loct2_br_insert_trigger before insert on loct2
+	for each row execute procedure br_insert_trigfunc();
+-- The new values are concatenated with ' triggered !'
+insert into itrtest values (1, 'foo') returning *;
+ a |        b        
+---+-----------------
+ 1 | foo triggered !
+(1 row)
+
+insert into itrtest values (2, 'qux') returning *;
+ a |        b        
+---+-----------------
+ 2 | qux triggered !
+(1 row)
+
+insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+ a |         b         
+---+-------------------
+ 1 | test1 triggered !
+ 2 | test2 triggered !
+(2 rows)
+
+with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+ a |         b         
+---+-------------------
+ 1 | test1 triggered !
+ 2 | test2 triggered !
+(2 rows)
+
+drop trigger loct1_br_insert_trigger on loct1;
+drop trigger loct2_br_insert_trigger on loct2;
 drop table itrtest;
 drop table loct1;
 drop table loct2;
@@ -7518,6 +7560,57 @@ select tableoid::regclass, * FROM locp;
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
+-- Test that remote triggers work with update tuple routing
+create trigger loct_br_insert_trigger before insert on loct
+	for each row execute procedure br_insert_trigfunc();
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+-- Check case where the foreign partition is a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ Update on public.utrtest
+   Output: remp.a, remp.b
+   Foreign Update on public.remp
+   Update on public.locp
+   ->  Foreign Update on public.remp
+         Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b
+   ->  Seq Scan on public.locp
+         Output: 1, locp.b, locp.ctid
+         Filter: ((locp.a = 1) OR (locp.a = 2))
+(9 rows)
+
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+ a |        b        
+---+-----------------
+ 1 | qux triggered !
+(1 row)
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+-- Check case where the foreign partition isn't a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 2 returning *;
+              QUERY PLAN              
+--------------------------------------
+ Update on public.utrtest
+   Output: locp.a, locp.b
+   Update on public.locp
+   ->  Seq Scan on public.locp
+         Output: 1, locp.b, locp.ctid
+         Filter: (locp.a = 2)
+(6 rows)
+
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 2 returning *;
+ a |        b        
+---+-----------------
+ 1 | qux triggered !
+(1 row)
+
+drop trigger loct_br_insert_trigger on loct;
 drop table utrtest;
 drop table loct;
 -- Test copy tuple routing
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index a46160df7c..b9a7c95f70 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -381,6 +381,7 @@ static void create_cursor(ForeignScanState *node);
 static void fetch_more_data(ForeignScanState *node);
 static void close_cursor(PGconn *conn, unsigned int cursor_number);
 static PgFdwModifyState *create_foreign_modify(EState *estate,
+					  RangeTblEntry *rte,
 					  ResultRelInfo *resultRelInfo,
 					  CmdType operation,
 					  Plan *subplan,
@@ -1653,17 +1654,17 @@ postgresPlanForeignModify(PlannerInfo *root,
 	switch (operation)
 	{
 		case CMD_INSERT:
-			deparseInsertSql(&sql, root, resultRelation, rel,
+			deparseInsertSql(&sql, rte, resultRelation, rel,
 							 targetAttrs, doNothing, returningList,
 							 &retrieved_attrs);
 			break;
 		case CMD_UPDATE:
-			deparseUpdateSql(&sql, root, resultRelation, rel,
+			deparseUpdateSql(&sql, rte, resultRelation, rel,
 							 targetAttrs, returningList,
 							 &retrieved_attrs);
 			break;
 		case CMD_DELETE:
-			deparseDeleteSql(&sql, root, resultRelation, rel,
+			deparseDeleteSql(&sql, rte, resultRelation, rel,
 							 returningList,
 							 &retrieved_attrs);
 			break;
@@ -1700,6 +1701,7 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 	List	   *target_attrs;
 	bool		has_returning;
 	List	   *retrieved_attrs;
+	RangeTblEntry *rte;
 
 	/*
 	 * Do nothing in EXPLAIN (no ANALYZE) case.  resultRelInfo->ri_FdwState
@@ -1718,8 +1720,13 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 	retrieved_attrs = (List *) list_nth(fdw_private,
 										FdwModifyPrivateRetrievedAttrs);
 
+	/* Find RTE. */
+	rte = rt_fetch(resultRelInfo->ri_RangeTableIndex,
+				   mtstate->ps.state->es_range_table);
+
 	/* Construct an execution state. */
 	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
 									resultRelInfo,
 									mtstate->operation,
 									mtstate->mt_plans[subplan_index]->plan,
@@ -1974,11 +1981,11 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 						   ResultRelInfo *resultRelInfo)
 {
 	PgFdwModifyState *fmstate;
-	Plan	   *plan = mtstate->ps.plan;
+	ModifyTable *plan = castNode(ModifyTable, mtstate->ps.plan);
+	EState *estate = mtstate->ps.state;
+	Index		resultRelation = resultRelInfo->ri_RangeTableIndex;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
-	RangeTblEntry *rte;
-	Query	   *query;
-	PlannerInfo *root;
+	RangeTblEntry *rte = NULL;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
 	int			attnum;
 	StringInfoData sql;
@@ -1988,18 +1995,6 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 
 	initStringInfo(&sql);
 
-	/* Set up largely-dummy planner state. */
-	rte = makeNode(RangeTblEntry);
-	rte->rtekind = RTE_RELATION;
-	rte->relid = RelationGetRelid(rel);
-	rte->relkind = RELKIND_FOREIGN_TABLE;
-	query = makeNode(Query);
-	query->commandType = CMD_INSERT;
-	query->resultRelation = 1;
-	query->rtable = list_make1(rte);
-	root = makeNode(PlannerInfo);
-	root->parse = query;
-
 	/* We transmit all columns that are defined in the foreign table. */
 	for (attnum = 1; attnum <= tupdesc->natts; attnum++)
 	{
@@ -2022,12 +2017,39 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 				 (int) onConflictAction);
 	}
 
+	if (resultRelInfo->ri_PartitionRoot && plan)
+	{
+		bool dostuff = true;
+
+		if (resultRelation != plan->nominalRelation)
+			dostuff = false;
+		else
+			resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+
+		if (dostuff)
+		{
+			rte = list_nth(estate->es_range_table, resultRelation - 1);
+			rte = copyObject(rte);
+			rte->relid = RelationGetRelid(rel);
+			rte->relkind = RELKIND_FOREIGN_TABLE;
+		}
+	}
+
+	if (rte == NULL)
+	{
+		rte = makeNode(RangeTblEntry);
+		rte->rtekind = RTE_RELATION;
+		rte->relid = RelationGetRelid(rel);
+		rte->relkind = RELKIND_FOREIGN_TABLE;
+	}
+
 	/* Construct the SQL command string. */
-	deparseInsertSql(&sql, root, 1, rel, targetAttrs, doNothing,
+	deparseInsertSql(&sql, rte, resultRelation, rel, targetAttrs, doNothing,
 					 resultRelInfo->ri_returningList, &retrieved_attrs);
 
 	/* Construct an execution state. */
 	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
 									resultRelInfo,
 									CMD_INSERT,
 									NULL,
@@ -3255,6 +3277,7 @@ close_cursor(PGconn *conn, unsigned int cursor_number)
  */
 static PgFdwModifyState *
 create_foreign_modify(EState *estate,
+					  RangeTblEntry *rte,
 					  ResultRelInfo *resultRelInfo,
 					  CmdType operation,
 					  Plan *subplan,
@@ -3266,7 +3289,6 @@ create_foreign_modify(EState *estate,
 	PgFdwModifyState *fmstate;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
-	RangeTblEntry *rte;
 	Oid			userid;
 	ForeignTable *table;
 	UserMapping *user;
@@ -3283,7 +3305,6 @@ create_foreign_modify(EState *estate,
 	 * Identify which user to do the remote access as.  This should match what
 	 * ExecCheckRTEPerms() does.
 	 */
-	rte = rt_fetch(resultRelInfo->ri_RangeTableIndex, estate->es_range_table);
 	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
 
 	/* Get info about foreign table. */
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index d37cc88b6e..a5d4011e8d 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -140,11 +140,11 @@ extern void classifyConditions(PlannerInfo *root,
 extern bool is_foreign_expr(PlannerInfo *root,
 				RelOptInfo *baserel,
 				Expr *expr);
-extern void deparseInsertSql(StringInfo buf, PlannerInfo *root,
+extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing, List *returningList,
 				 List **retrieved_attrs);
-extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
+extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, List *returningList,
 				 List **retrieved_attrs);
@@ -157,7 +157,7 @@ extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 					   List **params_list,
 					   List *returningList,
 					   List **retrieved_attrs);
-extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
+extern void deparseDeleteSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *returningList,
 				 List **retrieved_attrs);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 633e9fecec..231b1e01a5 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1804,6 +1804,31 @@ insert into itrtest values (1, 'bar') on conflict (a) do update set b = excluded
 
 select tableoid::regclass, * FROM itrtest;
 
+delete from itrtest;
+
+drop index loct1_idx;
+
+-- Test that remote triggers work with insert tuple routing
+create function br_insert_trigfunc() returns trigger as $$
+begin
+	new.b := new.b || ' triggered !';
+	return new;
+end
+$$ language plpgsql;
+create trigger loct1_br_insert_trigger before insert on loct1
+	for each row execute procedure br_insert_trigfunc();
+create trigger loct2_br_insert_trigger before insert on loct2
+	for each row execute procedure br_insert_trigfunc();
+
+-- The new values are concatenated with ' triggered !'
+insert into itrtest values (1, 'foo') returning *;
+insert into itrtest values (2, 'qux') returning *;
+insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+
+drop trigger loct1_br_insert_trigger on loct1;
+drop trigger loct2_br_insert_trigger on loct2;
+
 drop table itrtest;
 drop table loct1;
 drop table loct2;
@@ -1836,6 +1861,30 @@ select tableoid::regclass, * FROM locp;
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
 
+-- Test that remote triggers work with update tuple routing
+create trigger loct_br_insert_trigger before insert on loct
+	for each row execute procedure br_insert_trigfunc();
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+
+-- Check case where the foreign partition is a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+
+-- Check case where the foreign partition isn't a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 2 returning *;
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 2 returning *;
+
+drop trigger loct_br_insert_trigger on loct;
+
 drop table utrtest;
 drop table loct;
 
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index f7bbb804aa..d7d427fc2d 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -333,6 +333,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 					  rootrel,
 					  estate->es_instrument);
 
+	/*
+	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
+	 * partition-key becomes a DELETE+INSERT operation, so this check is still
+	 * required when the operation is CMD_UPDATE.
+	 */
+	CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+
 	/*
 	 * Since we've just initialized this ResultRelInfo, it's not in any list
 	 * attached to the estate as yet.  Add it, so that it can be found later.
@@ -345,9 +352,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 		lappend(estate->es_tuple_routing_result_relations,
 				leaf_part_rri);
 
-	/* Set up information needed for routing tuples to this partition. */
-	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
-
 	/*
 	 * Open partition indices.  The user may have asked to check for conflicts
 	 * within this leaf partition and do "nothing" instead of throwing an
@@ -490,6 +494,9 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 									&mtstate->ps, RelationGetDescr(partrel));
 	}
 
+	/* Set up information needed for routing tuples to the partition. */
+	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
+
 	/*
 	 * If there is an ON CONFLICT clause, initialize state for it.
 	 */
@@ -656,8 +663,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 
 /*
  * ExecInitRoutingInfo
- *		Set up information needed for routing tuples to a leaf partition if
- *		routable; else abort the operation
+ *		Set up information needed for routing tuples to a leaf partition
  */
 void
 ExecInitRoutingInfo(ModifyTableState *mtstate,
@@ -668,9 +674,6 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 {
 	MemoryContext oldContext;
 
-	/* Verify the partition is a valid target for INSERT */
-	CheckValidResultRel(partRelInfo, CMD_INSERT);
-
 	/*
 	 * Switch into per-query memory context.
 	 */
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 7ec2c6bcaa..fc1e5385bb 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1700,20 +1700,24 @@ ExecPrepareTupleRouting(ModifyTableState *mtstate,
 										partidx);
 
 	/*
-	 * Set up information needed for routing tuples to the partition if we
-	 * didn't yet (ExecInitRoutingInfo would abort the operation if the
-	 * partition isn't routable).
+	 * Check whether the partition is routable if we didn't yet
 	 *
 	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
-	 * tuple to a new partition.  This setup would be needed for a subplan
+	 * tuple to a new partition.  This check would be applied to a subplan
 	 * partition of such an UPDATE that is chosen as the partition to route
-	 * the tuple to.  The reason we do this setup here rather than in
+	 * the tuple to.  The reason we do this check here rather than in
 	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
 	 * unnecessarily due to non-routable subplan partitions that may not be
 	 * chosen for update tuple movement after all.
 	 */
 	if (!partrel->ri_PartitionReadyForRouting)
+	{
+		/* Verify the partition is a valid target for INSERT. */
+		CheckValidResultRel(partrel, CMD_INSERT);
+
+		/* Set up information needed for routing tuples to the partition. */
 		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
+	}
 
 	/*
 	 * Make it look like we are inserting into the partition.
#18Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: Robert Haas (#17)
Re: Oddity in tuple routing for foreign partitions

Hello.

At Tue, 24 Apr 2018 15:49:20 -0400, Robert Haas <robertmhaas@gmail.com> wrote in <CA+Tgmob+CiHt+9JEUiXzNVmUcUf1zBb-bUeffVmbZWJHV0YVtw@mail.gmail.com>

On Tue, Apr 24, 2018 at 12:21 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Apr 23, 2018 at 11:25 AM, Alvaro Herrera
<alvherre@alvh.no-ip.org> wrote:

Robert, I think this is your turf, per 3d956d9562aa. Are you looking
into it?

Thanks for the ping. I just had a look over the proposed patch and I
guess I don't like it very much. Temporarily modifying the range
table in place and then changing it back before we return seems ugly
and error-prone. I hope we can come up with a solution that doesn't
involve needing to do that.

I have done some refactoring to avoid that. See attached.

I didn't really get beyond the refactoring stage with this today.
This version still seems to work, but I don't really understand the
logic in postgresBeginForeignInsert which decides whether to use the
RTE from the range table or create our own. We seem to need to do one
sometimes and the other sometimes, but I don't know why that is,
really. Nor do I understand why we need the other changes in the
patch. There's probably a good reason, but I haven't figured it out
yet.

FWIW, the refactoring drops the condition that the ForeignInsert
is a part of UPDATE. The code exists just for avoiding temprary
RTE generation in 2 cases out of the 3 possible cases mentioned
in [1]/messages/by-id/f970d875-9711-b8cb-f270-965fa3e40334@lab.ntt.co.jp. If it looks too complex for the gain, we can always
create an RTE for deparsing as Fujita-san's first patch in this
thread did. Anyway the condition for "dostuff" + "is update"
might be a bit too arbitrary.

[1]: /messages/by-id/f970d875-9711-b8cb-f270-965fa3e40334@lab.ntt.co.jp

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#19Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Robert Haas (#17)
Re: Oddity in tuple routing for foreign partitions

On 2018/04/25 4:49, Robert Haas wrote:

On Tue, Apr 24, 2018 at 12:21 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Apr 23, 2018 at 11:25 AM, Alvaro Herrera
<alvherre@alvh.no-ip.org> wrote:

Robert, I think this is your turf, per 3d956d9562aa. Are you looking
into it?

Thanks for the ping. I just had a look over the proposed patch and I
guess I don't like it very much. Temporarily modifying the range
table in place and then changing it back before we return seems ugly
and error-prone. I hope we can come up with a solution that doesn't
involve needing to do that.

I have done some refactoring to avoid that. See attached.

+1 for getting rid of the PlannerInfo argument of the many functions in
that code. I have long wondered if we couldn't rid of it and especially
thought of it when reviewing this patch.

Thanks,
Amit

#20Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: Amit Langote (#19)
Re: Oddity in tuple routing for foreign partitions

I forgot to mention that.

At Wed, 25 Apr 2018 10:17:02 +0900, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote in <f894af29-345f-678b-480a-9f6e8cc314bd@lab.ntt.co.jp>

On 2018/04/25 4:49, Robert Haas wrote:

On Tue, Apr 24, 2018 at 12:21 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Apr 23, 2018 at 11:25 AM, Alvaro Herrera
<alvherre@alvh.no-ip.org> wrote:

Robert, I think this is your turf, per 3d956d9562aa. Are you looking
into it?

Thanks for the ping. I just had a look over the proposed patch and I
guess I don't like it very much. Temporarily modifying the range
table in place and then changing it back before we return seems ugly
and error-prone. I hope we can come up with a solution that doesn't
involve needing to do that.

I have done some refactoring to avoid that. See attached.

+1 for getting rid of the PlannerInfo argument of the many functions in
that code. I have long wondered if we couldn't rid of it and especially
thought of it when reviewing this patch.

+1 from me. Thanks for making things simpler and easy to
understand. I feel the same as Amit:p

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#21Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Robert Haas (#17)
2 attachment(s)
Re: Oddity in tuple routing for foreign partitions

Oops, really meant to send the "+1 to the root -> rte refactoring" comment
and the rest in the same email.

On 2018/04/25 4:49, Robert Haas wrote:

I have done some refactoring to avoid that. See attached.

I didn't really get beyond the refactoring stage with this today.
This version still seems to work, but I don't really understand the
logic in postgresBeginForeignInsert which decides whether to use the
RTE from the range table or create our own. We seem to need to do one
sometimes and the other sometimes, but I don't know why that is,
really. Nor do I understand why we need the other changes in the
patch. There's probably a good reason, but I haven't figured it out
yet.

So, the main part of the patch that fixes the bug is the following per the
latest v4 patch.

+    if (resultRelInfo->ri_PartitionRoot && plan)
+    {
+        bool dostuff = true;
+
+        if (resultRelation != plan->nominalRelation)
+            dostuff = false;
+        else
+            resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+
+        if (dostuff)
+        {
+            rte = list_nth(estate->es_range_table, resultRelation - 1);
+            rte = copyObject(rte);
+            rte->relid = RelationGetRelid(rel);
+            rte->relkind = RELKIND_FOREIGN_TABLE;
+        }
+    }
+
+    if (rte == NULL)
+    {
+        rte = makeNode(RangeTblEntry);
+        rte->rtekind = RTE_RELATION;
+        rte->relid = RelationGetRelid(rel);
+        rte->relkind = RELKIND_FOREIGN_TABLE;
+    }

After the refactoring, it appears to me that we only need this much:

+    rte = makeNode(RangeTblEntry);
+    rte->rtekind = RTE_RELATION;
+    rte->relid = RelationGetRelid(rel);
+    rte->relkind = RELKIND_FOREIGN_TABLE;

that is, *after* teaching ExecInitPartitionInfo to correctly set the RT
index of the ResultRelInfo it creates. After the refactoring, the only
thing this patch needs to do is to choose the RT index of the result
relation correctly. We currently use this:

+ Index resultRelation = resultRelInfo->ri_RangeTableIndex;

And the rest of the code the patch adds is only to adjust it based on
where the partition ResultRelInfo seems to have originated from. If the
ri_RangeTableIndex was set correctly to begin with, we wouldn't need to
fiddle with that in the FDW code. Regular ResultRelInfo's already have it
set correctly, it's only the ones that ExecInitPartitionInfo creates that
seem be creating the problem.

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Thanks,
Amit

Attachments:

fix-tuple-routing-for-foreign-partitions-5.patchtext/plain; charset=UTF-8; name=fix-tuple-routing-for-foreign-partitions-5.patchDownload
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 6e2fa1420c..d272719ff4 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -125,7 +125,7 @@ static char *deparse_type_name(Oid type_oid, int32 typemod);
  * Functions to construct string representation of a node tree.
  */
 static void deparseTargetList(StringInfo buf,
-				  PlannerInfo *root,
+				  RangeTblEntry *rte,
 				  Index rtindex,
 				  Relation rel,
 				  bool is_returning,
@@ -137,13 +137,13 @@ static void deparseExplicitTargetList(List *tlist,
 						  List **retrieved_attrs,
 						  deparse_expr_cxt *context);
 static void deparseSubqueryTargetList(deparse_expr_cxt *context);
-static void deparseReturningList(StringInfo buf, PlannerInfo *root,
+static void deparseReturningList(StringInfo buf, RangeTblEntry *rte,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
 					 List **retrieved_attrs);
 static void deparseColumnRef(StringInfo buf, int varno, int varattno,
-				 PlannerInfo *root, bool qualify_col);
+				 RangeTblEntry *rte, bool qualify_col);
 static void deparseRelation(StringInfo buf, Relation rel);
 static void deparseExpr(Expr *expr, deparse_expr_cxt *context);
 static void deparseVar(Var *node, deparse_expr_cxt *context);
@@ -1050,7 +1050,7 @@ deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
 		 */
 		Relation	rel = heap_open(rte->relid, NoLock);
 
-		deparseTargetList(buf, root, foreignrel->relid, rel, false,
+		deparseTargetList(buf, rte, foreignrel->relid, rel, false,
 						  fpinfo->attrs_used, false, retrieved_attrs);
 		heap_close(rel, NoLock);
 	}
@@ -1099,7 +1099,7 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
  */
 static void
 deparseTargetList(StringInfo buf,
-				  PlannerInfo *root,
+				  RangeTblEntry *rte,
 				  Index rtindex,
 				  Relation rel,
 				  bool is_returning,
@@ -1137,7 +1137,7 @@ deparseTargetList(StringInfo buf,
 				appendStringInfoString(buf, " RETURNING ");
 			first = false;
 
-			deparseColumnRef(buf, rtindex, i, root, qualify_col);
+			deparseColumnRef(buf, rtindex, i, rte, qualify_col);
 
 			*retrieved_attrs = lappend_int(*retrieved_attrs, i);
 		}
@@ -1649,7 +1649,7 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  * to *retrieved_attrs.
  */
 void
-deparseInsertSql(StringInfo buf, PlannerInfo *root,
+deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing,
 				 List *returningList, List **retrieved_attrs)
@@ -1674,7 +1674,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
 				appendStringInfoString(buf, ", ");
 			first = false;
 
-			deparseColumnRef(buf, rtindex, attnum, root, false);
+			deparseColumnRef(buf, rtindex, attnum, rte, false);
 		}
 
 		appendStringInfoString(buf, ") VALUES (");
@@ -1699,7 +1699,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
 	if (doNothing)
 		appendStringInfoString(buf, " ON CONFLICT DO NOTHING");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_insert_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1712,7 +1712,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
  * to *retrieved_attrs.
  */
 void
-deparseUpdateSql(StringInfo buf, PlannerInfo *root,
+deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, List *returningList,
 				 List **retrieved_attrs)
@@ -1735,13 +1735,13 @@ deparseUpdateSql(StringInfo buf, PlannerInfo *root,
 			appendStringInfoString(buf, ", ");
 		first = false;
 
-		deparseColumnRef(buf, rtindex, attnum, root, false);
+		deparseColumnRef(buf, rtindex, attnum, rte, false);
 		appendStringInfo(buf, " = $%d", pindex);
 		pindex++;
 	}
 	appendStringInfoString(buf, " WHERE ctid = $1");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_update_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1777,6 +1777,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 	int			nestlevel;
 	bool		first;
 	ListCell   *lc;
+	RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
 
 	/* Set up context struct for recursion */
 	context.root = root;
@@ -1808,7 +1809,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 			appendStringInfoString(buf, ", ");
 		first = false;
 
-		deparseColumnRef(buf, rtindex, attnum, root, false);
+		deparseColumnRef(buf, rtindex, attnum, rte, false);
 		appendStringInfoString(buf, " = ");
 		deparseExpr((Expr *) tle->expr, &context);
 	}
@@ -1835,7 +1836,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 		deparseExplicitTargetList(returningList, true, retrieved_attrs,
 								  &context);
 	else
-		deparseReturningList(buf, root, rtindex, rel, false,
+		deparseReturningList(buf, rte, rtindex, rel, false,
 							 returningList, retrieved_attrs);
 }
 
@@ -1847,7 +1848,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  * to *retrieved_attrs.
  */
 void
-deparseDeleteSql(StringInfo buf, PlannerInfo *root,
+deparseDeleteSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *returningList,
 				 List **retrieved_attrs)
@@ -1856,7 +1857,7 @@ deparseDeleteSql(StringInfo buf, PlannerInfo *root,
 	deparseRelation(buf, rel);
 	appendStringInfoString(buf, " WHERE ctid = $1");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_delete_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1918,7 +1919,8 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
 		deparseExplicitTargetList(returningList, true, retrieved_attrs,
 								  &context);
 	else
-		deparseReturningList(buf, root, rtindex, rel, false,
+		deparseReturningList(buf, planner_rt_fetch(rtindex, root),
+							 rtindex, rel, false,
 							 returningList, retrieved_attrs);
 }
 
@@ -1926,7 +1928,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  * Add a RETURNING clause, if needed, to an INSERT/UPDATE/DELETE.
  */
 static void
-deparseReturningList(StringInfo buf, PlannerInfo *root,
+deparseReturningList(StringInfo buf, RangeTblEntry *rte,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
@@ -1952,7 +1954,7 @@ deparseReturningList(StringInfo buf, PlannerInfo *root,
 	}
 
 	if (attrs_used != NULL)
-		deparseTargetList(buf, root, rtindex, rel, true, attrs_used, false,
+		deparseTargetList(buf, rte, rtindex, rel, true, attrs_used, false,
 						  retrieved_attrs);
 	else
 		*retrieved_attrs = NIL;
@@ -2048,11 +2050,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
  * If qualify_col is true, qualify column name with the alias of relation.
  */
 static void
-deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
+deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
 				 bool qualify_col)
 {
-	RangeTblEntry *rte;
-
 	/* We support fetching the remote side's CTID and OID. */
 	if (varattno == SelfItemPointerAttributeNumber)
 	{
@@ -2077,10 +2077,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		Oid			fetchval = 0;
 
 		if (varattno == TableOidAttributeNumber)
-		{
-			rte = planner_rt_fetch(varno, root);
 			fetchval = rte->relid;
-		}
 
 		if (qualify_col)
 		{
@@ -2100,9 +2097,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		/* Required only to be passed down to deparseTargetList(). */
 		List	   *retrieved_attrs;
 
-		/* Get RangeTblEntry from array in PlannerInfo. */
-		rte = planner_rt_fetch(varno, root);
-
 		/*
 		 * The lock on the relation will be held by upper callers, so it's
 		 * fine to open it with no lock here.
@@ -2134,7 +2128,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		}
 
 		appendStringInfoString(buf, "ROW(");
-		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
+		deparseTargetList(buf, rte, varno, rel, false, attrs_used, qualify_col,
 						  &retrieved_attrs);
 		appendStringInfoChar(buf, ')');
 
@@ -2154,9 +2148,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		/* varno must not be any of OUTER_VAR, INNER_VAR and INDEX_VAR. */
 		Assert(!IS_SPECIAL_VARNO(varno));
 
-		/* Get RangeTblEntry from array in PlannerInfo. */
-		rte = planner_rt_fetch(varno, root);
-
 		/*
 		 * If it's a column of a foreign table, and it has the column_name FDW
 		 * option, use that value.
@@ -2354,7 +2345,8 @@ deparseVar(Var *node, deparse_expr_cxt *context)
 
 	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
 		deparseColumnRef(context->buf, node->varno, node->varattno,
-						 context->root, qualify_col);
+						 planner_rt_fetch(node->varno, context->root),
+						 qualify_col);
 	else
 	{
 		/* Treat like a Param */
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 53ed285875..bb6b1a8fdf 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7454,6 +7454,48 @@ select tableoid::regclass, * FROM itrtest;
  remp1    | 1 | foo
 (1 row)
 
+delete from itrtest;
+drop index loct1_idx;
+-- Test that remote triggers work with insert tuple routing
+create function br_insert_trigfunc() returns trigger as $$
+begin
+	new.b := new.b || ' triggered !';
+	return new;
+end
+$$ language plpgsql;
+create trigger loct1_br_insert_trigger before insert on loct1
+	for each row execute procedure br_insert_trigfunc();
+create trigger loct2_br_insert_trigger before insert on loct2
+	for each row execute procedure br_insert_trigfunc();
+-- The new values are concatenated with ' triggered !'
+insert into itrtest values (1, 'foo') returning *;
+ a |        b        
+---+-----------------
+ 1 | foo triggered !
+(1 row)
+
+insert into itrtest values (2, 'qux') returning *;
+ a |        b        
+---+-----------------
+ 2 | qux triggered !
+(1 row)
+
+insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+ a |         b         
+---+-------------------
+ 1 | test1 triggered !
+ 2 | test2 triggered !
+(2 rows)
+
+with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+ a |         b         
+---+-------------------
+ 1 | test1 triggered !
+ 2 | test2 triggered !
+(2 rows)
+
+drop trigger loct1_br_insert_trigger on loct1;
+drop trigger loct2_br_insert_trigger on loct2;
 drop table itrtest;
 drop table loct1;
 drop table loct2;
@@ -7518,6 +7560,57 @@ select tableoid::regclass, * FROM locp;
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
+-- Test that remote triggers work with update tuple routing
+create trigger loct_br_insert_trigger before insert on loct
+	for each row execute procedure br_insert_trigfunc();
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+-- Check case where the foreign partition is a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ Update on public.utrtest
+   Output: remp.a, remp.b
+   Foreign Update on public.remp
+   Update on public.locp
+   ->  Foreign Update on public.remp
+         Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b
+   ->  Seq Scan on public.locp
+         Output: 1, locp.b, locp.ctid
+         Filter: ((locp.a = 1) OR (locp.a = 2))
+(9 rows)
+
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+ a |        b        
+---+-----------------
+ 1 | qux triggered !
+(1 row)
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+-- Check case where the foreign partition isn't a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 2 returning *;
+              QUERY PLAN              
+--------------------------------------
+ Update on public.utrtest
+   Output: locp.a, locp.b
+   Update on public.locp
+   ->  Seq Scan on public.locp
+         Output: 1, locp.b, locp.ctid
+         Filter: (locp.a = 2)
+(6 rows)
+
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 2 returning *;
+ a |        b        
+---+-----------------
+ 1 | qux triggered !
+(1 row)
+
+drop trigger loct_br_insert_trigger on loct;
 drop table utrtest;
 drop table loct;
 -- Test copy tuple routing
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index a46160df7c..d74546ebbb 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -381,6 +381,7 @@ static void create_cursor(ForeignScanState *node);
 static void fetch_more_data(ForeignScanState *node);
 static void close_cursor(PGconn *conn, unsigned int cursor_number);
 static PgFdwModifyState *create_foreign_modify(EState *estate,
+					  RangeTblEntry *rte,
 					  ResultRelInfo *resultRelInfo,
 					  CmdType operation,
 					  Plan *subplan,
@@ -1653,17 +1654,17 @@ postgresPlanForeignModify(PlannerInfo *root,
 	switch (operation)
 	{
 		case CMD_INSERT:
-			deparseInsertSql(&sql, root, resultRelation, rel,
+			deparseInsertSql(&sql, rte, resultRelation, rel,
 							 targetAttrs, doNothing, returningList,
 							 &retrieved_attrs);
 			break;
 		case CMD_UPDATE:
-			deparseUpdateSql(&sql, root, resultRelation, rel,
+			deparseUpdateSql(&sql, rte, resultRelation, rel,
 							 targetAttrs, returningList,
 							 &retrieved_attrs);
 			break;
 		case CMD_DELETE:
-			deparseDeleteSql(&sql, root, resultRelation, rel,
+			deparseDeleteSql(&sql, rte, resultRelation, rel,
 							 returningList,
 							 &retrieved_attrs);
 			break;
@@ -1700,6 +1701,7 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 	List	   *target_attrs;
 	bool		has_returning;
 	List	   *retrieved_attrs;
+	RangeTblEntry *rte;
 
 	/*
 	 * Do nothing in EXPLAIN (no ANALYZE) case.  resultRelInfo->ri_FdwState
@@ -1718,8 +1720,13 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 	retrieved_attrs = (List *) list_nth(fdw_private,
 										FdwModifyPrivateRetrievedAttrs);
 
+	/* Find RTE. */
+	rte = rt_fetch(resultRelInfo->ri_RangeTableIndex,
+				   mtstate->ps.state->es_range_table);
+
 	/* Construct an execution state. */
 	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
 									resultRelInfo,
 									mtstate->operation,
 									mtstate->mt_plans[subplan_index]->plan,
@@ -1974,11 +1981,10 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 						   ResultRelInfo *resultRelInfo)
 {
 	PgFdwModifyState *fmstate;
-	Plan	   *plan = mtstate->ps.plan;
+	ModifyTable *plan = castNode(ModifyTable, mtstate->ps.plan);
+	Index		resultRelation = resultRelInfo->ri_RangeTableIndex;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	RangeTblEntry *rte;
-	Query	   *query;
-	PlannerInfo *root;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
 	int			attnum;
 	StringInfoData sql;
@@ -1988,18 +1994,6 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 
 	initStringInfo(&sql);
 
-	/* Set up largely-dummy planner state. */
-	rte = makeNode(RangeTblEntry);
-	rte->rtekind = RTE_RELATION;
-	rte->relid = RelationGetRelid(rel);
-	rte->relkind = RELKIND_FOREIGN_TABLE;
-	query = makeNode(Query);
-	query->commandType = CMD_INSERT;
-	query->resultRelation = 1;
-	query->rtable = list_make1(rte);
-	root = makeNode(PlannerInfo);
-	root->parse = query;
-
 	/* We transmit all columns that are defined in the foreign table. */
 	for (attnum = 1; attnum <= tupdesc->natts; attnum++)
 	{
@@ -2022,12 +2016,18 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 				 (int) onConflictAction);
 	}
 
+	rte = makeNode(RangeTblEntry);
+	rte->rtekind = RTE_RELATION;
+	rte->relid = RelationGetRelid(rel);
+	rte->relkind = RELKIND_FOREIGN_TABLE;
+
 	/* Construct the SQL command string. */
-	deparseInsertSql(&sql, root, 1, rel, targetAttrs, doNothing,
+	deparseInsertSql(&sql, rte, resultRelation, rel, targetAttrs, doNothing,
 					 resultRelInfo->ri_returningList, &retrieved_attrs);
 
 	/* Construct an execution state. */
 	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
 									resultRelInfo,
 									CMD_INSERT,
 									NULL,
@@ -3255,6 +3255,7 @@ close_cursor(PGconn *conn, unsigned int cursor_number)
  */
 static PgFdwModifyState *
 create_foreign_modify(EState *estate,
+					  RangeTblEntry *rte,
 					  ResultRelInfo *resultRelInfo,
 					  CmdType operation,
 					  Plan *subplan,
@@ -3266,7 +3267,6 @@ create_foreign_modify(EState *estate,
 	PgFdwModifyState *fmstate;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
-	RangeTblEntry *rte;
 	Oid			userid;
 	ForeignTable *table;
 	UserMapping *user;
@@ -3283,7 +3283,6 @@ create_foreign_modify(EState *estate,
 	 * Identify which user to do the remote access as.  This should match what
 	 * ExecCheckRTEPerms() does.
 	 */
-	rte = rt_fetch(resultRelInfo->ri_RangeTableIndex, estate->es_range_table);
 	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
 
 	/* Get info about foreign table. */
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index d37cc88b6e..a5d4011e8d 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -140,11 +140,11 @@ extern void classifyConditions(PlannerInfo *root,
 extern bool is_foreign_expr(PlannerInfo *root,
 				RelOptInfo *baserel,
 				Expr *expr);
-extern void deparseInsertSql(StringInfo buf, PlannerInfo *root,
+extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing, List *returningList,
 				 List **retrieved_attrs);
-extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
+extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, List *returningList,
 				 List **retrieved_attrs);
@@ -157,7 +157,7 @@ extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 					   List **params_list,
 					   List *returningList,
 					   List **retrieved_attrs);
-extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
+extern void deparseDeleteSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *returningList,
 				 List **retrieved_attrs);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 633e9fecec..231b1e01a5 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1804,6 +1804,31 @@ insert into itrtest values (1, 'bar') on conflict (a) do update set b = excluded
 
 select tableoid::regclass, * FROM itrtest;
 
+delete from itrtest;
+
+drop index loct1_idx;
+
+-- Test that remote triggers work with insert tuple routing
+create function br_insert_trigfunc() returns trigger as $$
+begin
+	new.b := new.b || ' triggered !';
+	return new;
+end
+$$ language plpgsql;
+create trigger loct1_br_insert_trigger before insert on loct1
+	for each row execute procedure br_insert_trigfunc();
+create trigger loct2_br_insert_trigger before insert on loct2
+	for each row execute procedure br_insert_trigfunc();
+
+-- The new values are concatenated with ' triggered !'
+insert into itrtest values (1, 'foo') returning *;
+insert into itrtest values (2, 'qux') returning *;
+insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+
+drop trigger loct1_br_insert_trigger on loct1;
+drop trigger loct2_br_insert_trigger on loct2;
+
 drop table itrtest;
 drop table loct1;
 drop table loct2;
@@ -1836,6 +1861,30 @@ select tableoid::regclass, * FROM locp;
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
 
+-- Test that remote triggers work with update tuple routing
+create trigger loct_br_insert_trigger before insert on loct
+	for each row execute procedure br_insert_trigfunc();
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+
+-- Check case where the foreign partition is a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+
+-- Check case where the foreign partition isn't a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 2 returning *;
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 2 returning *;
+
+drop trigger loct_br_insert_trigger on loct;
+
 drop table utrtest;
 drop table loct;
 
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index a378650df0..d1503d773e 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -335,6 +335,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 					  estate->es_instrument);
 
 	/*
+	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
+	 * partition-key becomes a DELETE+INSERT operation, so this check is still
+	 * required when the operation is CMD_UPDATE.
+	 */
+	CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+
+	/*
 	 * Since we've just initialized this ResultRelInfo, it's not in any list
 	 * attached to the estate as yet.  Add it, so that it can be found later.
 	 *
@@ -346,9 +353,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 		lappend(estate->es_tuple_routing_result_relations,
 				leaf_part_rri);
 
-	/* Set up information needed for routing tuples to this partition. */
-	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
-
 	/*
 	 * Open partition indices.  The user may have asked to check for conflicts
 	 * within this leaf partition and do "nothing" instead of throwing an
@@ -489,6 +493,9 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 									&mtstate->ps, RelationGetDescr(partrel));
 	}
 
+	/* Set up information needed for routing tuples to the partition. */
+	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
+
 	/*
 	 * If there is an ON CONFLICT clause, initialize state for it.
 	 */
@@ -654,8 +661,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 
 /*
  * ExecInitRoutingInfo
- *		Set up information needed for routing tuples to a leaf partition if
- *		routable; else abort the operation
+ *		Set up information needed for routing tuples to a leaf partition
  */
 void
 ExecInitRoutingInfo(ModifyTableState *mtstate,
@@ -666,9 +672,6 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 {
 	MemoryContext oldContext;
 
-	/* Verify the partition is a valid target for INSERT */
-	CheckValidResultRel(partRelInfo, CMD_INSERT);
-
 	/*
 	 * Switch into per-query memory context.
 	 */
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 7ec2c6bcaa..fc1e5385bb 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1700,20 +1700,24 @@ ExecPrepareTupleRouting(ModifyTableState *mtstate,
 										partidx);
 
 	/*
-	 * Set up information needed for routing tuples to the partition if we
-	 * didn't yet (ExecInitRoutingInfo would abort the operation if the
-	 * partition isn't routable).
+	 * Check whether the partition is routable if we didn't yet
 	 *
 	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
-	 * tuple to a new partition.  This setup would be needed for a subplan
+	 * tuple to a new partition.  This check would be applied to a subplan
 	 * partition of such an UPDATE that is chosen as the partition to route
-	 * the tuple to.  The reason we do this setup here rather than in
+	 * the tuple to.  The reason we do this check here rather than in
 	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
 	 * unnecessarily due to non-routable subplan partitions that may not be
 	 * chosen for update tuple movement after all.
 	 */
 	if (!partrel->ri_PartitionReadyForRouting)
+	{
+		/* Verify the partition is a valid target for INSERT. */
+		CheckValidResultRel(partrel, CMD_INSERT);
+
+		/* Set up information needed for routing tuples to the partition. */
 		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
+	}
 
 	/*
 	 * Make it look like we are inserting into the partition.
teach-ExecInitPartitionInfo-to-use-correct-RT-index.patchtext/plain; charset=UTF-8; name=teach-ExecInitPartitionInfo-to-use-correct-RT-index.patchDownload
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index f7418f64b1..a378650df0 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -309,6 +309,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 	Relation	rootrel = resultRelInfo->ri_RelationDesc,
 				partrel;
 	Relation	firstResultRel = mtstate->resultRelInfo[0].ri_RelationDesc;
+	Index		firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
 	ResultRelInfo *leaf_part_rri;
 	MemoryContext oldContext;
 	AttrNumber *part_attnos = NULL;
@@ -329,7 +330,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 	leaf_part_rri = makeNode(ResultRelInfo);
 	InitResultRelInfo(leaf_part_rri,
 					  partrel,
-					  node ? node->nominalRelation : 1,
+					  firstVarno,
 					  rootrel,
 					  estate->es_instrument);
 
@@ -372,7 +373,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 		List	   *wcoList;
 		List	   *wcoExprs = NIL;
 		ListCell   *ll;
-		int			firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
 
 		/*
 		 * In the case of INSERT on a partitioned table, there is only one
@@ -438,7 +438,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 		TupleTableSlot *slot;
 		ExprContext *econtext;
 		List	   *returningList;
-		int			firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
 
 		/* See the comment above for WCO lists. */
 		Assert((node->operation == CMD_INSERT &&
@@ -496,7 +495,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 	if (node && node->onConflictAction != ONCONFLICT_NONE)
 	{
 		TupleConversionMap *map = proute->parent_child_tupconv_maps[partidx];
-		int			firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
 		TupleDesc	partrelDesc = RelationGetDescr(partrel);
 		ExprContext *econtext = mtstate->ps.ps_ExprContext;
 		ListCell   *lc;
#22Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: Amit Langote (#21)
Re: Oddity in tuple routing for foreign partitions

At Wed, 25 Apr 2018 11:19:23 +0900, Amit Langote <Langote_Amit_f8@lab.ntt.co.jp> wrote in <573e60cc-beeb-b534-d89a-7622fae35585@lab.ntt.co.jp>

Oops, really meant to send the "+1 to the root -> rte refactoring" comment
and the rest in the same email.

On 2018/04/25 4:49, Robert Haas wrote:

I have done some refactoring to avoid that. See attached.

I didn't really get beyond the refactoring stage with this today.
This version still seems to work, but I don't really understand the
logic in postgresBeginForeignInsert which decides whether to use the
RTE from the range table or create our own. We seem to need to do one
sometimes and the other sometimes, but I don't know why that is,
really. Nor do I understand why we need the other changes in the
patch. There's probably a good reason, but I haven't figured it out
yet.

So, the main part of the patch that fixes the bug is the following per the
latest v4 patch.

+    if (resultRelInfo->ri_PartitionRoot && plan)
+    {
+        bool dostuff = true;
+
+        if (resultRelation != plan->nominalRelation)
+            dostuff = false;
+        else
+            resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+
+        if (dostuff)
+        {
+            rte = list_nth(estate->es_range_table, resultRelation - 1);
+            rte = copyObject(rte);
+            rte->relid = RelationGetRelid(rel);
+            rte->relkind = RELKIND_FOREIGN_TABLE;
+        }
+    }
+
+    if (rte == NULL)
+    {
+        rte = makeNode(RangeTblEntry);
+        rte->rtekind = RTE_RELATION;
+        rte->relid = RelationGetRelid(rel);
+        rte->relkind = RELKIND_FOREIGN_TABLE;
+    }

After the refactoring, it appears to me that we only need this much:

+    rte = makeNode(RangeTblEntry);
+    rte->rtekind = RTE_RELATION;
+    rte->relid = RelationGetRelid(rel);
+    rte->relkind = RELKIND_FOREIGN_TABLE;

Mmm.. That is, only relid is required to deparse (I don't mean
that it should be refactored so.). On the other hand
create_foreign_modify requires rte->checkAsUser as well. The
following query (probably) unexpectedly fails with the latest
patch. It succeeds with -3 patch.

=# create user usr1 login;
=# create view v1 as select * from itrtest;
=# revoke all ON itrtest FROM PUBLIC;
=# grant SELECT, INSERT ON v1 to usr1;
=> select * from v1; -- OK
=> insert into v1 (select n, 'foo' from (values (1), (2)) as t(n)) returning *;
ERROR: password is required
DETAIL: Non-superusers must provide a password in the user mapping.

We need to read it of the parent?

that is, *after* teaching ExecInitPartitionInfo to correctly set the RT
index of the ResultRelInfo it creates. After the refactoring, the only
thing this patch needs to do is to choose the RT index of the result
relation correctly. We currently use this:

+ Index resultRelation = resultRelInfo->ri_RangeTableIndex;

And the rest of the code the patch adds is only to adjust it based on
where the partition ResultRelInfo seems to have originated from. If the
ri_RangeTableIndex was set correctly to begin with, we wouldn't need to
fiddle with that in the FDW code. Regular ResultRelInfo's already have it
set correctly, it's only the ones that ExecInitPartitionInfo creates that
seem be creating the problem.

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Maybe, one reason that I feel uneasy is how the patch accesses
desired resultRelInfo.

+ Index firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;

Looking ExecInitModifyTable, just one resultRelInfo has been
passed to BeginForeignModify so it should not access as an
array. I will feel at easy if the line were in the following
shape. Does it make sense?

+ Index firstVarno = mtstate->resultRelInfo->ri_RangeTableIndex;

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#23Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Kyotaro HORIGUCHI (#22)
2 attachment(s)
Re: Oddity in tuple routing for foreign partitions

Horiguchi-san,

Thanks for taking a look at it.

On 2018/04/25 16:42, Kyotaro HORIGUCHI wrote:

At Wed, 25 Apr 2018 11:19:23 +0900, Amit Langote wrote:

After the refactoring, it appears to me that we only need this much:

+    rte = makeNode(RangeTblEntry);
+    rte->rtekind = RTE_RELATION;
+    rte->relid = RelationGetRelid(rel);
+    rte->relkind = RELKIND_FOREIGN_TABLE;

Mmm.. That is, only relid is required to deparse (I don't mean
that it should be refactored so.). On the other hand
create_foreign_modify requires rte->checkAsUser as well. The
following query (probably) unexpectedly fails with the latest
patch. It succeeds with -3 patch.

=# create user usr1 login;
=# create view v1 as select * from itrtest;
=# revoke all ON itrtest FROM PUBLIC;
=# grant SELECT, INSERT ON v1 to usr1;
=> select * from v1; -- OK
=> insert into v1 (select n, 'foo' from (values (1), (2)) as t(n)) returning *;
ERROR: password is required
DETAIL: Non-superusers must provide a password in the user mapping.

We need to read it of the parent?

Hmm, I missed that we do need information from a proper RTE as well. So,
I suppose we should be doing this instead of creating the RTE for foreign
partition from scratch.

+    rte = list_nth(estate->es_range_table, resultRelation - 1);
+    rte = copyObject(rte);
+    rte->relid = RelationGetRelid(rel);
+    rte->relkind = RELKIND_FOREIGN_TABLE;

If we apply the other patch I proposed, resultRelation always points to
the correct RTE.

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Maybe, one reason that I feel uneasy is how the patch accesses
desired resultRelInfo.

+ Index firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;

Looking ExecInitModifyTable, just one resultRelInfo has been
passed to BeginForeignModify so it should not access as an
array. I will feel at easy if the line were in the following
shape. Does it make sense?

+ Index firstVarno = mtstate->resultRelInfo->ri_RangeTableIndex;

This is the comment on
teach-ExecInitPartitionInfo-to-use-correct-RT-index.patch, right? I
haven't seen either ExecInitModifyTable or BeginForeignModify being
involved in this discussion, but I see your point. I see no problem with
doing it that way, I have updated that patch to do it that way. Also,
changed the line above it that is unrelated to this patch just to be
consistent.

Attached updated patches:

1. teach-ExecInitPartitionInfo-to-use-correct-RT-index-2.patch
2. fix-tuple-routing-for-foreign-partitions-6.patch

Thanks,
Amit

Attachments:

fix-tuple-routing-for-foreign-partitions-6.patchtext/plain; charset=UTF-8; name=fix-tuple-routing-for-foreign-partitions-6.patchDownload
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 6e2fa1420c..d272719ff4 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -125,7 +125,7 @@ static char *deparse_type_name(Oid type_oid, int32 typemod);
  * Functions to construct string representation of a node tree.
  */
 static void deparseTargetList(StringInfo buf,
-				  PlannerInfo *root,
+				  RangeTblEntry *rte,
 				  Index rtindex,
 				  Relation rel,
 				  bool is_returning,
@@ -137,13 +137,13 @@ static void deparseExplicitTargetList(List *tlist,
 						  List **retrieved_attrs,
 						  deparse_expr_cxt *context);
 static void deparseSubqueryTargetList(deparse_expr_cxt *context);
-static void deparseReturningList(StringInfo buf, PlannerInfo *root,
+static void deparseReturningList(StringInfo buf, RangeTblEntry *rte,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
 					 List **retrieved_attrs);
 static void deparseColumnRef(StringInfo buf, int varno, int varattno,
-				 PlannerInfo *root, bool qualify_col);
+				 RangeTblEntry *rte, bool qualify_col);
 static void deparseRelation(StringInfo buf, Relation rel);
 static void deparseExpr(Expr *expr, deparse_expr_cxt *context);
 static void deparseVar(Var *node, deparse_expr_cxt *context);
@@ -1050,7 +1050,7 @@ deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
 		 */
 		Relation	rel = heap_open(rte->relid, NoLock);
 
-		deparseTargetList(buf, root, foreignrel->relid, rel, false,
+		deparseTargetList(buf, rte, foreignrel->relid, rel, false,
 						  fpinfo->attrs_used, false, retrieved_attrs);
 		heap_close(rel, NoLock);
 	}
@@ -1099,7 +1099,7 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
  */
 static void
 deparseTargetList(StringInfo buf,
-				  PlannerInfo *root,
+				  RangeTblEntry *rte,
 				  Index rtindex,
 				  Relation rel,
 				  bool is_returning,
@@ -1137,7 +1137,7 @@ deparseTargetList(StringInfo buf,
 				appendStringInfoString(buf, " RETURNING ");
 			first = false;
 
-			deparseColumnRef(buf, rtindex, i, root, qualify_col);
+			deparseColumnRef(buf, rtindex, i, rte, qualify_col);
 
 			*retrieved_attrs = lappend_int(*retrieved_attrs, i);
 		}
@@ -1649,7 +1649,7 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  * to *retrieved_attrs.
  */
 void
-deparseInsertSql(StringInfo buf, PlannerInfo *root,
+deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing,
 				 List *returningList, List **retrieved_attrs)
@@ -1674,7 +1674,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
 				appendStringInfoString(buf, ", ");
 			first = false;
 
-			deparseColumnRef(buf, rtindex, attnum, root, false);
+			deparseColumnRef(buf, rtindex, attnum, rte, false);
 		}
 
 		appendStringInfoString(buf, ") VALUES (");
@@ -1699,7 +1699,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
 	if (doNothing)
 		appendStringInfoString(buf, " ON CONFLICT DO NOTHING");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_insert_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1712,7 +1712,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
  * to *retrieved_attrs.
  */
 void
-deparseUpdateSql(StringInfo buf, PlannerInfo *root,
+deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, List *returningList,
 				 List **retrieved_attrs)
@@ -1735,13 +1735,13 @@ deparseUpdateSql(StringInfo buf, PlannerInfo *root,
 			appendStringInfoString(buf, ", ");
 		first = false;
 
-		deparseColumnRef(buf, rtindex, attnum, root, false);
+		deparseColumnRef(buf, rtindex, attnum, rte, false);
 		appendStringInfo(buf, " = $%d", pindex);
 		pindex++;
 	}
 	appendStringInfoString(buf, " WHERE ctid = $1");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_update_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1777,6 +1777,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 	int			nestlevel;
 	bool		first;
 	ListCell   *lc;
+	RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
 
 	/* Set up context struct for recursion */
 	context.root = root;
@@ -1808,7 +1809,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 			appendStringInfoString(buf, ", ");
 		first = false;
 
-		deparseColumnRef(buf, rtindex, attnum, root, false);
+		deparseColumnRef(buf, rtindex, attnum, rte, false);
 		appendStringInfoString(buf, " = ");
 		deparseExpr((Expr *) tle->expr, &context);
 	}
@@ -1835,7 +1836,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 		deparseExplicitTargetList(returningList, true, retrieved_attrs,
 								  &context);
 	else
-		deparseReturningList(buf, root, rtindex, rel, false,
+		deparseReturningList(buf, rte, rtindex, rel, false,
 							 returningList, retrieved_attrs);
 }
 
@@ -1847,7 +1848,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  * to *retrieved_attrs.
  */
 void
-deparseDeleteSql(StringInfo buf, PlannerInfo *root,
+deparseDeleteSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *returningList,
 				 List **retrieved_attrs)
@@ -1856,7 +1857,7 @@ deparseDeleteSql(StringInfo buf, PlannerInfo *root,
 	deparseRelation(buf, rel);
 	appendStringInfoString(buf, " WHERE ctid = $1");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_delete_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1918,7 +1919,8 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
 		deparseExplicitTargetList(returningList, true, retrieved_attrs,
 								  &context);
 	else
-		deparseReturningList(buf, root, rtindex, rel, false,
+		deparseReturningList(buf, planner_rt_fetch(rtindex, root),
+							 rtindex, rel, false,
 							 returningList, retrieved_attrs);
 }
 
@@ -1926,7 +1928,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  * Add a RETURNING clause, if needed, to an INSERT/UPDATE/DELETE.
  */
 static void
-deparseReturningList(StringInfo buf, PlannerInfo *root,
+deparseReturningList(StringInfo buf, RangeTblEntry *rte,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
@@ -1952,7 +1954,7 @@ deparseReturningList(StringInfo buf, PlannerInfo *root,
 	}
 
 	if (attrs_used != NULL)
-		deparseTargetList(buf, root, rtindex, rel, true, attrs_used, false,
+		deparseTargetList(buf, rte, rtindex, rel, true, attrs_used, false,
 						  retrieved_attrs);
 	else
 		*retrieved_attrs = NIL;
@@ -2048,11 +2050,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
  * If qualify_col is true, qualify column name with the alias of relation.
  */
 static void
-deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
+deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
 				 bool qualify_col)
 {
-	RangeTblEntry *rte;
-
 	/* We support fetching the remote side's CTID and OID. */
 	if (varattno == SelfItemPointerAttributeNumber)
 	{
@@ -2077,10 +2077,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		Oid			fetchval = 0;
 
 		if (varattno == TableOidAttributeNumber)
-		{
-			rte = planner_rt_fetch(varno, root);
 			fetchval = rte->relid;
-		}
 
 		if (qualify_col)
 		{
@@ -2100,9 +2097,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		/* Required only to be passed down to deparseTargetList(). */
 		List	   *retrieved_attrs;
 
-		/* Get RangeTblEntry from array in PlannerInfo. */
-		rte = planner_rt_fetch(varno, root);
-
 		/*
 		 * The lock on the relation will be held by upper callers, so it's
 		 * fine to open it with no lock here.
@@ -2134,7 +2128,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		}
 
 		appendStringInfoString(buf, "ROW(");
-		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
+		deparseTargetList(buf, rte, varno, rel, false, attrs_used, qualify_col,
 						  &retrieved_attrs);
 		appendStringInfoChar(buf, ')');
 
@@ -2154,9 +2148,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		/* varno must not be any of OUTER_VAR, INNER_VAR and INDEX_VAR. */
 		Assert(!IS_SPECIAL_VARNO(varno));
 
-		/* Get RangeTblEntry from array in PlannerInfo. */
-		rte = planner_rt_fetch(varno, root);
-
 		/*
 		 * If it's a column of a foreign table, and it has the column_name FDW
 		 * option, use that value.
@@ -2354,7 +2345,8 @@ deparseVar(Var *node, deparse_expr_cxt *context)
 
 	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
 		deparseColumnRef(context->buf, node->varno, node->varattno,
-						 context->root, qualify_col);
+						 planner_rt_fetch(node->varno, context->root),
+						 qualify_col);
 	else
 	{
 		/* Treat like a Param */
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 53ed285875..bb6b1a8fdf 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7454,6 +7454,48 @@ select tableoid::regclass, * FROM itrtest;
  remp1    | 1 | foo
 (1 row)
 
+delete from itrtest;
+drop index loct1_idx;
+-- Test that remote triggers work with insert tuple routing
+create function br_insert_trigfunc() returns trigger as $$
+begin
+	new.b := new.b || ' triggered !';
+	return new;
+end
+$$ language plpgsql;
+create trigger loct1_br_insert_trigger before insert on loct1
+	for each row execute procedure br_insert_trigfunc();
+create trigger loct2_br_insert_trigger before insert on loct2
+	for each row execute procedure br_insert_trigfunc();
+-- The new values are concatenated with ' triggered !'
+insert into itrtest values (1, 'foo') returning *;
+ a |        b        
+---+-----------------
+ 1 | foo triggered !
+(1 row)
+
+insert into itrtest values (2, 'qux') returning *;
+ a |        b        
+---+-----------------
+ 2 | qux triggered !
+(1 row)
+
+insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+ a |         b         
+---+-------------------
+ 1 | test1 triggered !
+ 2 | test2 triggered !
+(2 rows)
+
+with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+ a |         b         
+---+-------------------
+ 1 | test1 triggered !
+ 2 | test2 triggered !
+(2 rows)
+
+drop trigger loct1_br_insert_trigger on loct1;
+drop trigger loct2_br_insert_trigger on loct2;
 drop table itrtest;
 drop table loct1;
 drop table loct2;
@@ -7518,6 +7560,57 @@ select tableoid::regclass, * FROM locp;
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
+-- Test that remote triggers work with update tuple routing
+create trigger loct_br_insert_trigger before insert on loct
+	for each row execute procedure br_insert_trigfunc();
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+-- Check case where the foreign partition is a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ Update on public.utrtest
+   Output: remp.a, remp.b
+   Foreign Update on public.remp
+   Update on public.locp
+   ->  Foreign Update on public.remp
+         Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b
+   ->  Seq Scan on public.locp
+         Output: 1, locp.b, locp.ctid
+         Filter: ((locp.a = 1) OR (locp.a = 2))
+(9 rows)
+
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+ a |        b        
+---+-----------------
+ 1 | qux triggered !
+(1 row)
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+-- Check case where the foreign partition isn't a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 2 returning *;
+              QUERY PLAN              
+--------------------------------------
+ Update on public.utrtest
+   Output: locp.a, locp.b
+   Update on public.locp
+   ->  Seq Scan on public.locp
+         Output: 1, locp.b, locp.ctid
+         Filter: (locp.a = 2)
+(6 rows)
+
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 2 returning *;
+ a |        b        
+---+-----------------
+ 1 | qux triggered !
+(1 row)
+
+drop trigger loct_br_insert_trigger on loct;
 drop table utrtest;
 drop table loct;
 -- Test copy tuple routing
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index a46160df7c..ac5521e971 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -381,6 +381,7 @@ static void create_cursor(ForeignScanState *node);
 static void fetch_more_data(ForeignScanState *node);
 static void close_cursor(PGconn *conn, unsigned int cursor_number);
 static PgFdwModifyState *create_foreign_modify(EState *estate,
+					  RangeTblEntry *rte,
 					  ResultRelInfo *resultRelInfo,
 					  CmdType operation,
 					  Plan *subplan,
@@ -1653,17 +1654,17 @@ postgresPlanForeignModify(PlannerInfo *root,
 	switch (operation)
 	{
 		case CMD_INSERT:
-			deparseInsertSql(&sql, root, resultRelation, rel,
+			deparseInsertSql(&sql, rte, resultRelation, rel,
 							 targetAttrs, doNothing, returningList,
 							 &retrieved_attrs);
 			break;
 		case CMD_UPDATE:
-			deparseUpdateSql(&sql, root, resultRelation, rel,
+			deparseUpdateSql(&sql, rte, resultRelation, rel,
 							 targetAttrs, returningList,
 							 &retrieved_attrs);
 			break;
 		case CMD_DELETE:
-			deparseDeleteSql(&sql, root, resultRelation, rel,
+			deparseDeleteSql(&sql, rte, resultRelation, rel,
 							 returningList,
 							 &retrieved_attrs);
 			break;
@@ -1700,6 +1701,7 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 	List	   *target_attrs;
 	bool		has_returning;
 	List	   *retrieved_attrs;
+	RangeTblEntry *rte;
 
 	/*
 	 * Do nothing in EXPLAIN (no ANALYZE) case.  resultRelInfo->ri_FdwState
@@ -1718,8 +1720,13 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 	retrieved_attrs = (List *) list_nth(fdw_private,
 										FdwModifyPrivateRetrievedAttrs);
 
+	/* Find RTE. */
+	rte = rt_fetch(resultRelInfo->ri_RangeTableIndex,
+				   mtstate->ps.state->es_range_table);
+
 	/* Construct an execution state. */
 	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
 									resultRelInfo,
 									mtstate->operation,
 									mtstate->mt_plans[subplan_index]->plan,
@@ -1974,11 +1981,11 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 						   ResultRelInfo *resultRelInfo)
 {
 	PgFdwModifyState *fmstate;
-	Plan	   *plan = mtstate->ps.plan;
+	ModifyTable *plan = castNode(ModifyTable, mtstate->ps.plan);
+	EState	   *estate = mtstate->ps.state;
+	Index		resultRelation = resultRelInfo->ri_RangeTableIndex;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	RangeTblEntry *rte;
-	Query	   *query;
-	PlannerInfo *root;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
 	int			attnum;
 	StringInfoData sql;
@@ -1988,18 +1995,6 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 
 	initStringInfo(&sql);
 
-	/* Set up largely-dummy planner state. */
-	rte = makeNode(RangeTblEntry);
-	rte->rtekind = RTE_RELATION;
-	rte->relid = RelationGetRelid(rel);
-	rte->relkind = RELKIND_FOREIGN_TABLE;
-	query = makeNode(Query);
-	query->commandType = CMD_INSERT;
-	query->resultRelation = 1;
-	query->rtable = list_make1(rte);
-	root = makeNode(PlannerInfo);
-	root->parse = query;
-
 	/* We transmit all columns that are defined in the foreign table. */
 	for (attnum = 1; attnum <= tupdesc->natts; attnum++)
 	{
@@ -2022,12 +2017,18 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 				 (int) onConflictAction);
 	}
 
+	rte = list_nth(estate->es_range_table, resultRelation - 1);
+	rte = copyObject(rte);
+	rte->relid = RelationGetRelid(rel);
+	rte->relkind = RELKIND_FOREIGN_TABLE;
+
 	/* Construct the SQL command string. */
-	deparseInsertSql(&sql, root, 1, rel, targetAttrs, doNothing,
+	deparseInsertSql(&sql, rte, resultRelation, rel, targetAttrs, doNothing,
 					 resultRelInfo->ri_returningList, &retrieved_attrs);
 
 	/* Construct an execution state. */
 	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
 									resultRelInfo,
 									CMD_INSERT,
 									NULL,
@@ -3255,6 +3256,7 @@ close_cursor(PGconn *conn, unsigned int cursor_number)
  */
 static PgFdwModifyState *
 create_foreign_modify(EState *estate,
+					  RangeTblEntry *rte,
 					  ResultRelInfo *resultRelInfo,
 					  CmdType operation,
 					  Plan *subplan,
@@ -3266,7 +3268,6 @@ create_foreign_modify(EState *estate,
 	PgFdwModifyState *fmstate;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
-	RangeTblEntry *rte;
 	Oid			userid;
 	ForeignTable *table;
 	UserMapping *user;
@@ -3283,7 +3284,6 @@ create_foreign_modify(EState *estate,
 	 * Identify which user to do the remote access as.  This should match what
 	 * ExecCheckRTEPerms() does.
 	 */
-	rte = rt_fetch(resultRelInfo->ri_RangeTableIndex, estate->es_range_table);
 	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
 
 	/* Get info about foreign table. */
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index d37cc88b6e..a5d4011e8d 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -140,11 +140,11 @@ extern void classifyConditions(PlannerInfo *root,
 extern bool is_foreign_expr(PlannerInfo *root,
 				RelOptInfo *baserel,
 				Expr *expr);
-extern void deparseInsertSql(StringInfo buf, PlannerInfo *root,
+extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing, List *returningList,
 				 List **retrieved_attrs);
-extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
+extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, List *returningList,
 				 List **retrieved_attrs);
@@ -157,7 +157,7 @@ extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 					   List **params_list,
 					   List *returningList,
 					   List **retrieved_attrs);
-extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
+extern void deparseDeleteSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *returningList,
 				 List **retrieved_attrs);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 633e9fecec..231b1e01a5 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1804,6 +1804,31 @@ insert into itrtest values (1, 'bar') on conflict (a) do update set b = excluded
 
 select tableoid::regclass, * FROM itrtest;
 
+delete from itrtest;
+
+drop index loct1_idx;
+
+-- Test that remote triggers work with insert tuple routing
+create function br_insert_trigfunc() returns trigger as $$
+begin
+	new.b := new.b || ' triggered !';
+	return new;
+end
+$$ language plpgsql;
+create trigger loct1_br_insert_trigger before insert on loct1
+	for each row execute procedure br_insert_trigfunc();
+create trigger loct2_br_insert_trigger before insert on loct2
+	for each row execute procedure br_insert_trigfunc();
+
+-- The new values are concatenated with ' triggered !'
+insert into itrtest values (1, 'foo') returning *;
+insert into itrtest values (2, 'qux') returning *;
+insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+
+drop trigger loct1_br_insert_trigger on loct1;
+drop trigger loct2_br_insert_trigger on loct2;
+
 drop table itrtest;
 drop table loct1;
 drop table loct2;
@@ -1836,6 +1861,30 @@ select tableoid::regclass, * FROM locp;
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
 
+-- Test that remote triggers work with update tuple routing
+create trigger loct_br_insert_trigger before insert on loct
+	for each row execute procedure br_insert_trigfunc();
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+
+-- Check case where the foreign partition is a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+
+-- Check case where the foreign partition isn't a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 2 returning *;
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 2 returning *;
+
+drop trigger loct_br_insert_trigger on loct;
+
 drop table utrtest;
 drop table loct;
 
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 233e3e9552..0e874857fc 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -334,6 +334,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 					  estate->es_instrument);
 
 	/*
+	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
+	 * partition-key becomes a DELETE+INSERT operation, so this check is still
+	 * required when the operation is CMD_UPDATE.
+	 */
+	CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+
+	/*
 	 * Since we've just initialized this ResultRelInfo, it's not in any list
 	 * attached to the estate as yet.  Add it, so that it can be found later.
 	 *
@@ -345,9 +352,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 		lappend(estate->es_tuple_routing_result_relations,
 				leaf_part_rri);
 
-	/* Set up information needed for routing tuples to this partition. */
-	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
-
 	/*
 	 * Open partition indices.  The user may have asked to check for conflicts
 	 * within this leaf partition and do "nothing" instead of throwing an
@@ -488,6 +492,9 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 									&mtstate->ps, RelationGetDescr(partrel));
 	}
 
+	/* Set up information needed for routing tuples to the partition. */
+	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
+
 	/*
 	 * If there is an ON CONFLICT clause, initialize state for it.
 	 */
@@ -653,8 +660,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 
 /*
  * ExecInitRoutingInfo
- *		Set up information needed for routing tuples to a leaf partition if
- *		routable; else abort the operation
+ *		Set up information needed for routing tuples to a leaf partition
  */
 void
 ExecInitRoutingInfo(ModifyTableState *mtstate,
@@ -665,9 +671,6 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 {
 	MemoryContext oldContext;
 
-	/* Verify the partition is a valid target for INSERT */
-	CheckValidResultRel(partRelInfo, CMD_INSERT);
-
 	/*
 	 * Switch into per-query memory context.
 	 */
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 7ec2c6bcaa..fc1e5385bb 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1700,20 +1700,24 @@ ExecPrepareTupleRouting(ModifyTableState *mtstate,
 										partidx);
 
 	/*
-	 * Set up information needed for routing tuples to the partition if we
-	 * didn't yet (ExecInitRoutingInfo would abort the operation if the
-	 * partition isn't routable).
+	 * Check whether the partition is routable if we didn't yet
 	 *
 	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
-	 * tuple to a new partition.  This setup would be needed for a subplan
+	 * tuple to a new partition.  This check would be applied to a subplan
 	 * partition of such an UPDATE that is chosen as the partition to route
-	 * the tuple to.  The reason we do this setup here rather than in
+	 * the tuple to.  The reason we do this check here rather than in
 	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
 	 * unnecessarily due to non-routable subplan partitions that may not be
 	 * chosen for update tuple movement after all.
 	 */
 	if (!partrel->ri_PartitionReadyForRouting)
+	{
+		/* Verify the partition is a valid target for INSERT. */
+		CheckValidResultRel(partrel, CMD_INSERT);
+
+		/* Set up information needed for routing tuples to the partition. */
 		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
+	}
 
 	/*
 	 * Make it look like we are inserting into the partition.
teach-ExecInitPartitionInfo-to-use-correct-RT-index-2.patchtext/plain; charset=UTF-8; name=teach-ExecInitPartitionInfo-to-use-correct-RT-index-2.patchDownload
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index b2ee92eb15..233e3e9552 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -307,7 +307,8 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
 	Relation	rootrel = resultRelInfo->ri_RelationDesc,
 				partrel;
-	Relation	firstResultRel = mtstate->resultRelInfo[0].ri_RelationDesc;
+	Relation	firstResultRel = mtstate->resultRelInfo->ri_RelationDesc;
+	Index		firstVarno = mtstate->resultRelInfo->ri_RangeTableIndex;
 	ResultRelInfo *leaf_part_rri;
 	MemoryContext oldContext;
 	AttrNumber *part_attnos = NULL;
@@ -328,7 +329,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 	leaf_part_rri = makeNode(ResultRelInfo);
 	InitResultRelInfo(leaf_part_rri,
 					  partrel,
-					  node ? node->nominalRelation : 1,
+					  firstVarno,
 					  rootrel,
 					  estate->es_instrument);
 
@@ -371,7 +372,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 		List	   *wcoList;
 		List	   *wcoExprs = NIL;
 		ListCell   *ll;
-		int			firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
 
 		/*
 		 * In the case of INSERT on a partitioned table, there is only one
@@ -437,7 +437,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 		TupleTableSlot *slot;
 		ExprContext *econtext;
 		List	   *returningList;
-		int			firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
 
 		/* See the comment above for WCO lists. */
 		Assert((node->operation == CMD_INSERT &&
@@ -495,7 +494,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 	if (node && node->onConflictAction != ONCONFLICT_NONE)
 	{
 		TupleConversionMap *map = proute->parent_child_tupconv_maps[partidx];
-		int			firstVarno = mtstate->resultRelInfo[0].ri_RangeTableIndex;
 		TupleDesc	partrelDesc = RelationGetDescr(partrel);
 		ExprContext *econtext = mtstate->ps.ps_ExprContext;
 		ListCell   *lc;
#24Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Robert Haas (#17)
Re: Oddity in tuple routing for foreign partitions

(2018/04/25 4:49), Robert Haas wrote:

On Tue, Apr 24, 2018 at 12:21 PM, Robert Haas<robertmhaas@gmail.com> wrote:

On Mon, Apr 23, 2018 at 11:25 AM, Alvaro Herrera
<alvherre@alvh.no-ip.org> wrote:

Robert, I think this is your turf, per 3d956d9562aa. Are you looking
into it?

Thanks for the ping. I just had a look over the proposed patch and I
guess I don't like it very much. Temporarily modifying the range
table in place and then changing it back before we return seems ugly
and error-prone. I hope we can come up with a solution that doesn't
involve needing to do that.

I see your concern.

I have done some refactoring to avoid that. See attached.

I like this refactoring.

I didn't really get beyond the refactoring stage with this today.
This version still seems to work, but I don't really understand the
logic in postgresBeginForeignInsert which decides whether to use the
RTE from the range table or create our own. We seem to need to do one
sometimes and the other sometimes, but I don't know why that is,
really. Nor do I understand why we need the other changes in the
patch. There's probably a good reason, but I haven't figured it out
yet.

To reduce the cost of creating RTEs, the original patch uses the RTE in
the range table if the partition is an UPDATE subplan partition. One
thing I'm concerned about is this change to postgresBeginForeignInsert:

+	if (resultRelInfo->ri_PartitionRoot && plan)
+	{
+		bool dostuff = true;
+
+		if (resultRelation != plan->nominalRelation)
+			dostuff = false;
+		else
+			resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+
+		if (dostuff)
+		{
+			rte = list_nth(estate->es_range_table, resultRelation - 1);
+			rte = copyObject(rte);
+			rte->relid = RelationGetRelid(rel);
+			rte->relkind = RELKIND_FOREIGN_TABLE;
+		}
+	}
+
+	if (rte == NULL)
+	{
+		rte = makeNode(RangeTblEntry);
+		rte->rtekind = RTE_RELATION;
+		rte->relid = RelationGetRelid(rel);
+		rte->relkind = RELKIND_FOREIGN_TABLE;
+	}

IIUC, I think this creates a new RTE for an UPDATE subplan partition by
makeNode, but I'm not sure if that would work well because we decide
which user to do the remote access as by looking at rte->checkAsUser, in
create_foreign_modify.

Thanks for working on this!

Best regards,
Etsuro Fujita

#25Robert Haas
robertmhaas@gmail.com
In reply to: Amit Langote (#21)
Re: Oddity in tuple routing for foreign partitions

On Tue, Apr 24, 2018 at 10:19 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Are you splitting this into 2 patches here because there are 2
separate issues? If so, what are those issues? There seem to be a
bunch of interrelated changes here but I haven't seen a clear
explanation of which ones are needed for which reasons.

I agree that fixing ExecInitPartitionInfo to use the right RT index in
the first place sounds like a better idea than trying to piece
together which RT index we should be using after-the-fact, but I need
to better understand what problems we're trying to fix here before I
can be sure if that's the strategy I want to endorse...

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

#26Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#24)
Re: Oddity in tuple routing for foreign partitions

(2018/04/25 17:51), Etsuro Fujita wrote:

(2018/04/25 4:49), Robert Haas wrote:

I didn't really get beyond the refactoring stage with this today.
This version still seems to work, but I don't really understand the
logic in postgresBeginForeignInsert which decides whether to use the
RTE from the range table or create our own. We seem to need to do one
sometimes and the other sometimes, but I don't know why that is,
really. Nor do I understand why we need the other changes in the
patch. There's probably a good reason, but I haven't figured it out
yet.

To reduce the cost of creating RTEs, the original patch uses the RTE in
the range table if the partition is an UPDATE subplan partition.

One thing to add: in addition to that case, the original patch uses the
RTE in the range table if the foreign table is the target specified in a
COPY command.

Best regards,
Etsuro Fujita

#27Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Amit Langote (#23)
Re: Oddity in tuple routing for foreign partitions

(2018/04/25 17:29), Amit Langote wrote:

On 2018/04/25 16:42, Kyotaro HORIGUCHI wrote:

At Wed, 25 Apr 2018 11:19:23 +0900, Amit Langote wrote:

After the refactoring, it appears to me that we only need this much:

+    rte = makeNode(RangeTblEntry);
+    rte->rtekind = RTE_RELATION;
+    rte->relid = RelationGetRelid(rel);
+    rte->relkind = RELKIND_FOREIGN_TABLE;

Mmm.. That is, only relid is required to deparse (I don't mean
that it should be refactored so.). On the other hand
create_foreign_modify requires rte->checkAsUser as well.

That's right. I took care of this in my version, but unfortuneately,
that was ignored in the updated versions. Maybe the comments I added to
the patch were not enough, though.

Hmm, I missed that we do need information from a proper RTE as well. So,
I suppose we should be doing this instead of creating the RTE for foreign
partition from scratch.

+    rte = list_nth(estate->es_range_table, resultRelation - 1);
+    rte = copyObject(rte);
+    rte->relid = RelationGetRelid(rel);
+    rte->relkind = RELKIND_FOREIGN_TABLE;

As I said upthread, we can use the RTE in the range table as-is if the
foreign table is one of the UPDATE subplan partitions or the target
specified in a COPY command. So, I created the patch that way because
that would save cycles. Why not just use that RTE in those cases?

If we apply the other patch I proposed, resultRelation always points to
the correct RTE.

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Actually, I also thought the former when creating the patch, but I left
that as-is because I'm not sure that would be really safe;
ExecConstraints looks at the RT index via
GetInsertedColumns/GetUpdatedColumns, so what I thought was: that might
cause unexpected behavior. Anyway, I think that the former is more like
an improvement rather than a fix, so it would be better to leave that
for another patch for PG12?

Best regards,
Etsuro Fujita

#28Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Robert Haas (#25)
Re: Oddity in tuple routing for foreign partitions

(2018/04/26 2:59), Robert Haas wrote:

On Tue, Apr 24, 2018 at 10:19 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Are you splitting this into 2 patches here because there are 2
separate issues? If so, what are those issues? There seem to be a
bunch of interrelated changes here but I haven't seen a clear
explanation of which ones are needed for which reasons.

As I said in an earlier email, I'm wondering if it would be better to
leave that for another patch for PG12.

Best regards,
Etsuro Fujita

#29Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#27)
1 attachment(s)
Re: Oddity in tuple routing for foreign partitions

(2018/04/26 12:43), Etsuro Fujita wrote:

(2018/04/25 17:29), Amit Langote wrote:

On 2018/04/25 16:42, Kyotaro HORIGUCHI wrote:

At Wed, 25 Apr 2018 11:19:23 +0900, Amit Langote wrote:

After the refactoring, it appears to me that we only need this much:

+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RELKIND_FOREIGN_TABLE;

Mmm.. That is, only relid is required to deparse (I don't mean
that it should be refactored so.). On the other hand
create_foreign_modify requires rte->checkAsUser as well.

That's right. I took care of this in my version, but unfortuneately,
that was ignored in the updated versions. Maybe the comments I added to
the patch were not enough, though.

Hmm, I missed that we do need information from a proper RTE as well. So,
I suppose we should be doing this instead of creating the RTE for foreign
partition from scratch.

+ rte = list_nth(estate->es_range_table, resultRelation - 1);
+ rte = copyObject(rte);
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RELKIND_FOREIGN_TABLE;

As I said upthread, we can use the RTE in the range table as-is if the
foreign table is one of the UPDATE subplan partitions or the target
specified in a COPY command. So, I created the patch that way because
that would save cycles. Why not just use that RTE in those cases?

If we apply the other patch I proposed, resultRelation always points to
the correct RTE.

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Actually, I also thought the former when creating the patch, but I left
that as-is because I'm not sure that would be really safe;
ExecConstraints looks at the RT index via
GetInsertedColumns/GetUpdatedColumns, so what I thought was: that might
cause unexpected behavior. Anyway, I think that the former is more like
an improvement rather than a fix, so it would be better to leave that
for another patch for PG12?

Here is a new version I'd like to propose for fixing this issue without
the former patch.

Thanks for working on this!

Best regards,
Etsuro Fujita

Attachments:

fix-tuple-routing-for-foreign-partitions-7.patchtext/x-diff; name=fix-tuple-routing-for-foreign-partitions-7.patchDownload
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 6e2fa14..d272719 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -125,7 +125,7 @@ static char *deparse_type_name(Oid type_oid, int32 typemod);
  * Functions to construct string representation of a node tree.
  */
 static void deparseTargetList(StringInfo buf,
-				  PlannerInfo *root,
+				  RangeTblEntry *rte,
 				  Index rtindex,
 				  Relation rel,
 				  bool is_returning,
@@ -137,13 +137,13 @@ static void deparseExplicitTargetList(List *tlist,
 						  List **retrieved_attrs,
 						  deparse_expr_cxt *context);
 static void deparseSubqueryTargetList(deparse_expr_cxt *context);
-static void deparseReturningList(StringInfo buf, PlannerInfo *root,
+static void deparseReturningList(StringInfo buf, RangeTblEntry *rte,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
 					 List **retrieved_attrs);
 static void deparseColumnRef(StringInfo buf, int varno, int varattno,
-				 PlannerInfo *root, bool qualify_col);
+				 RangeTblEntry *rte, bool qualify_col);
 static void deparseRelation(StringInfo buf, Relation rel);
 static void deparseExpr(Expr *expr, deparse_expr_cxt *context);
 static void deparseVar(Var *node, deparse_expr_cxt *context);
@@ -1050,7 +1050,7 @@ deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
 		 */
 		Relation	rel = heap_open(rte->relid, NoLock);
 
-		deparseTargetList(buf, root, foreignrel->relid, rel, false,
+		deparseTargetList(buf, rte, foreignrel->relid, rel, false,
 						  fpinfo->attrs_used, false, retrieved_attrs);
 		heap_close(rel, NoLock);
 	}
@@ -1099,7 +1099,7 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
  */
 static void
 deparseTargetList(StringInfo buf,
-				  PlannerInfo *root,
+				  RangeTblEntry *rte,
 				  Index rtindex,
 				  Relation rel,
 				  bool is_returning,
@@ -1137,7 +1137,7 @@ deparseTargetList(StringInfo buf,
 				appendStringInfoString(buf, " RETURNING ");
 			first = false;
 
-			deparseColumnRef(buf, rtindex, i, root, qualify_col);
+			deparseColumnRef(buf, rtindex, i, rte, qualify_col);
 
 			*retrieved_attrs = lappend_int(*retrieved_attrs, i);
 		}
@@ -1649,7 +1649,7 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  * to *retrieved_attrs.
  */
 void
-deparseInsertSql(StringInfo buf, PlannerInfo *root,
+deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing,
 				 List *returningList, List **retrieved_attrs)
@@ -1674,7 +1674,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
 				appendStringInfoString(buf, ", ");
 			first = false;
 
-			deparseColumnRef(buf, rtindex, attnum, root, false);
+			deparseColumnRef(buf, rtindex, attnum, rte, false);
 		}
 
 		appendStringInfoString(buf, ") VALUES (");
@@ -1699,7 +1699,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
 	if (doNothing)
 		appendStringInfoString(buf, " ON CONFLICT DO NOTHING");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_insert_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1712,7 +1712,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
  * to *retrieved_attrs.
  */
 void
-deparseUpdateSql(StringInfo buf, PlannerInfo *root,
+deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, List *returningList,
 				 List **retrieved_attrs)
@@ -1735,13 +1735,13 @@ deparseUpdateSql(StringInfo buf, PlannerInfo *root,
 			appendStringInfoString(buf, ", ");
 		first = false;
 
-		deparseColumnRef(buf, rtindex, attnum, root, false);
+		deparseColumnRef(buf, rtindex, attnum, rte, false);
 		appendStringInfo(buf, " = $%d", pindex);
 		pindex++;
 	}
 	appendStringInfoString(buf, " WHERE ctid = $1");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_update_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1777,6 +1777,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 	int			nestlevel;
 	bool		first;
 	ListCell   *lc;
+	RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
 
 	/* Set up context struct for recursion */
 	context.root = root;
@@ -1808,7 +1809,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 			appendStringInfoString(buf, ", ");
 		first = false;
 
-		deparseColumnRef(buf, rtindex, attnum, root, false);
+		deparseColumnRef(buf, rtindex, attnum, rte, false);
 		appendStringInfoString(buf, " = ");
 		deparseExpr((Expr *) tle->expr, &context);
 	}
@@ -1835,7 +1836,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 		deparseExplicitTargetList(returningList, true, retrieved_attrs,
 								  &context);
 	else
-		deparseReturningList(buf, root, rtindex, rel, false,
+		deparseReturningList(buf, rte, rtindex, rel, false,
 							 returningList, retrieved_attrs);
 }
 
@@ -1847,7 +1848,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  * to *retrieved_attrs.
  */
 void
-deparseDeleteSql(StringInfo buf, PlannerInfo *root,
+deparseDeleteSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *returningList,
 				 List **retrieved_attrs)
@@ -1856,7 +1857,7 @@ deparseDeleteSql(StringInfo buf, PlannerInfo *root,
 	deparseRelation(buf, rel);
 	appendStringInfoString(buf, " WHERE ctid = $1");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_delete_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1918,7 +1919,8 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
 		deparseExplicitTargetList(returningList, true, retrieved_attrs,
 								  &context);
 	else
-		deparseReturningList(buf, root, rtindex, rel, false,
+		deparseReturningList(buf, planner_rt_fetch(rtindex, root),
+							 rtindex, rel, false,
 							 returningList, retrieved_attrs);
 }
 
@@ -1926,7 +1928,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  * Add a RETURNING clause, if needed, to an INSERT/UPDATE/DELETE.
  */
 static void
-deparseReturningList(StringInfo buf, PlannerInfo *root,
+deparseReturningList(StringInfo buf, RangeTblEntry *rte,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
@@ -1952,7 +1954,7 @@ deparseReturningList(StringInfo buf, PlannerInfo *root,
 	}
 
 	if (attrs_used != NULL)
-		deparseTargetList(buf, root, rtindex, rel, true, attrs_used, false,
+		deparseTargetList(buf, rte, rtindex, rel, true, attrs_used, false,
 						  retrieved_attrs);
 	else
 		*retrieved_attrs = NIL;
@@ -2048,11 +2050,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
  * If qualify_col is true, qualify column name with the alias of relation.
  */
 static void
-deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
+deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
 				 bool qualify_col)
 {
-	RangeTblEntry *rte;
-
 	/* We support fetching the remote side's CTID and OID. */
 	if (varattno == SelfItemPointerAttributeNumber)
 	{
@@ -2077,10 +2077,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		Oid			fetchval = 0;
 
 		if (varattno == TableOidAttributeNumber)
-		{
-			rte = planner_rt_fetch(varno, root);
 			fetchval = rte->relid;
-		}
 
 		if (qualify_col)
 		{
@@ -2100,9 +2097,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		/* Required only to be passed down to deparseTargetList(). */
 		List	   *retrieved_attrs;
 
-		/* Get RangeTblEntry from array in PlannerInfo. */
-		rte = planner_rt_fetch(varno, root);
-
 		/*
 		 * The lock on the relation will be held by upper callers, so it's
 		 * fine to open it with no lock here.
@@ -2134,7 +2128,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		}
 
 		appendStringInfoString(buf, "ROW(");
-		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
+		deparseTargetList(buf, rte, varno, rel, false, attrs_used, qualify_col,
 						  &retrieved_attrs);
 		appendStringInfoChar(buf, ')');
 
@@ -2154,9 +2148,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		/* varno must not be any of OUTER_VAR, INNER_VAR and INDEX_VAR. */
 		Assert(!IS_SPECIAL_VARNO(varno));
 
-		/* Get RangeTblEntry from array in PlannerInfo. */
-		rte = planner_rt_fetch(varno, root);
-
 		/*
 		 * If it's a column of a foreign table, and it has the column_name FDW
 		 * option, use that value.
@@ -2354,7 +2345,8 @@ deparseVar(Var *node, deparse_expr_cxt *context)
 
 	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
 		deparseColumnRef(context->buf, node->varno, node->varattno,
-						 context->root, qualify_col);
+						 planner_rt_fetch(node->varno, context->root),
+						 qualify_col);
 	else
 	{
 		/* Treat like a Param */
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 53ed285..bb6b1a8 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7454,6 +7454,48 @@ select tableoid::regclass, * FROM itrtest;
  remp1    | 1 | foo
 (1 row)
 
+delete from itrtest;
+drop index loct1_idx;
+-- Test that remote triggers work with insert tuple routing
+create function br_insert_trigfunc() returns trigger as $$
+begin
+	new.b := new.b || ' triggered !';
+	return new;
+end
+$$ language plpgsql;
+create trigger loct1_br_insert_trigger before insert on loct1
+	for each row execute procedure br_insert_trigfunc();
+create trigger loct2_br_insert_trigger before insert on loct2
+	for each row execute procedure br_insert_trigfunc();
+-- The new values are concatenated with ' triggered !'
+insert into itrtest values (1, 'foo') returning *;
+ a |        b        
+---+-----------------
+ 1 | foo triggered !
+(1 row)
+
+insert into itrtest values (2, 'qux') returning *;
+ a |        b        
+---+-----------------
+ 2 | qux triggered !
+(1 row)
+
+insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+ a |         b         
+---+-------------------
+ 1 | test1 triggered !
+ 2 | test2 triggered !
+(2 rows)
+
+with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+ a |         b         
+---+-------------------
+ 1 | test1 triggered !
+ 2 | test2 triggered !
+(2 rows)
+
+drop trigger loct1_br_insert_trigger on loct1;
+drop trigger loct2_br_insert_trigger on loct2;
 drop table itrtest;
 drop table loct1;
 drop table loct2;
@@ -7518,6 +7560,57 @@ select tableoid::regclass, * FROM locp;
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
+-- Test that remote triggers work with update tuple routing
+create trigger loct_br_insert_trigger before insert on loct
+	for each row execute procedure br_insert_trigfunc();
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+-- Check case where the foreign partition is a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ Update on public.utrtest
+   Output: remp.a, remp.b
+   Foreign Update on public.remp
+   Update on public.locp
+   ->  Foreign Update on public.remp
+         Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b
+   ->  Seq Scan on public.locp
+         Output: 1, locp.b, locp.ctid
+         Filter: ((locp.a = 1) OR (locp.a = 2))
+(9 rows)
+
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+ a |        b        
+---+-----------------
+ 1 | qux triggered !
+(1 row)
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+-- Check case where the foreign partition isn't a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 2 returning *;
+              QUERY PLAN              
+--------------------------------------
+ Update on public.utrtest
+   Output: locp.a, locp.b
+   Update on public.locp
+   ->  Seq Scan on public.locp
+         Output: 1, locp.b, locp.ctid
+         Filter: (locp.a = 2)
+(6 rows)
+
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 2 returning *;
+ a |        b        
+---+-----------------
+ 1 | qux triggered !
+(1 row)
+
+drop trigger loct_br_insert_trigger on loct;
 drop table utrtest;
 drop table loct;
 -- Test copy tuple routing
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index a46160d..1285e39 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -381,6 +381,7 @@ static void create_cursor(ForeignScanState *node);
 static void fetch_more_data(ForeignScanState *node);
 static void close_cursor(PGconn *conn, unsigned int cursor_number);
 static PgFdwModifyState *create_foreign_modify(EState *estate,
+					  RangeTblEntry *rte,
 					  ResultRelInfo *resultRelInfo,
 					  CmdType operation,
 					  Plan *subplan,
@@ -1653,17 +1654,17 @@ postgresPlanForeignModify(PlannerInfo *root,
 	switch (operation)
 	{
 		case CMD_INSERT:
-			deparseInsertSql(&sql, root, resultRelation, rel,
+			deparseInsertSql(&sql, rte, resultRelation, rel,
 							 targetAttrs, doNothing, returningList,
 							 &retrieved_attrs);
 			break;
 		case CMD_UPDATE:
-			deparseUpdateSql(&sql, root, resultRelation, rel,
+			deparseUpdateSql(&sql, rte, resultRelation, rel,
 							 targetAttrs, returningList,
 							 &retrieved_attrs);
 			break;
 		case CMD_DELETE:
-			deparseDeleteSql(&sql, root, resultRelation, rel,
+			deparseDeleteSql(&sql, rte, resultRelation, rel,
 							 returningList,
 							 &retrieved_attrs);
 			break;
@@ -1700,6 +1701,7 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 	List	   *target_attrs;
 	bool		has_returning;
 	List	   *retrieved_attrs;
+	RangeTblEntry *rte;
 
 	/*
 	 * Do nothing in EXPLAIN (no ANALYZE) case.  resultRelInfo->ri_FdwState
@@ -1718,8 +1720,13 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 	retrieved_attrs = (List *) list_nth(fdw_private,
 										FdwModifyPrivateRetrievedAttrs);
 
+	/* Find RTE. */
+	rte = rt_fetch(resultRelInfo->ri_RangeTableIndex,
+				   mtstate->ps.state->es_range_table);
+
 	/* Construct an execution state. */
 	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
 									resultRelInfo,
 									mtstate->operation,
 									mtstate->mt_plans[subplan_index]->plan,
@@ -1974,32 +1981,21 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 						   ResultRelInfo *resultRelInfo)
 {
 	PgFdwModifyState *fmstate;
-	Plan	   *plan = mtstate->ps.plan;
+	ModifyTable *plan = castNode(ModifyTable, mtstate->ps.plan);
+	EState *estate = mtstate->ps.state;
+	Index		resultRelation = resultRelInfo->ri_RangeTableIndex;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
-	RangeTblEntry *rte;
-	Query	   *query;
-	PlannerInfo *root;
+	RangeTblEntry *rte = NULL;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
 	int			attnum;
 	StringInfoData sql;
 	List	   *targetAttrs = NIL;
 	List	   *retrieved_attrs = NIL;
 	bool		doNothing = false;
+	bool		dostuff = false;
 
 	initStringInfo(&sql);
 
-	/* Set up largely-dummy planner state. */
-	rte = makeNode(RangeTblEntry);
-	rte->rtekind = RTE_RELATION;
-	rte->relid = RelationGetRelid(rel);
-	rte->relkind = RELKIND_FOREIGN_TABLE;
-	query = makeNode(Query);
-	query->commandType = CMD_INSERT;
-	query->resultRelation = 1;
-	query->rtable = list_make1(rte);
-	root = makeNode(PlannerInfo);
-	root->parse = query;
-
 	/* We transmit all columns that are defined in the foreign table. */
 	for (attnum = 1; attnum <= tupdesc->natts; attnum++)
 	{
@@ -2012,7 +2008,7 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 	/* Check if we add the ON CONFLICT clause to the remote query. */
 	if (plan)
 	{
-		OnConflictAction onConflictAction = ((ModifyTable *) plan)->onConflictAction;
+		OnConflictAction onConflictAction = plan->onConflictAction;
 
 		/* We only support DO NOTHING without an inference specification. */
 		if (onConflictAction == ONCONFLICT_NOTHING)
@@ -2022,12 +2018,34 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 				 (int) onConflictAction);
 	}
 
+	/* Find or create the RTE for the foreign table. */
+	if (resultRelInfo->ri_PartitionRoot)
+	{
+		dostuff = true;
+
+		if (plan && plan->operation == CMD_UPDATE)
+		{
+			if (resultRelation != plan->nominalRelation)
+				dostuff = false;
+			else
+				resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+		}
+	}
+	rte = list_nth(estate->es_range_table, resultRelation - 1);
+	if (dostuff)
+	{
+		rte = copyObject(rte);
+		rte->relid = RelationGetRelid(rel);
+		rte->relkind = RELKIND_FOREIGN_TABLE;
+	}
+
 	/* Construct the SQL command string. */
-	deparseInsertSql(&sql, root, 1, rel, targetAttrs, doNothing,
+	deparseInsertSql(&sql, rte, resultRelation, rel, targetAttrs, doNothing,
 					 resultRelInfo->ri_returningList, &retrieved_attrs);
 
 	/* Construct an execution state. */
 	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
 									resultRelInfo,
 									CMD_INSERT,
 									NULL,
@@ -3255,6 +3273,7 @@ close_cursor(PGconn *conn, unsigned int cursor_number)
  */
 static PgFdwModifyState *
 create_foreign_modify(EState *estate,
+					  RangeTblEntry *rte,
 					  ResultRelInfo *resultRelInfo,
 					  CmdType operation,
 					  Plan *subplan,
@@ -3266,7 +3285,6 @@ create_foreign_modify(EState *estate,
 	PgFdwModifyState *fmstate;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
-	RangeTblEntry *rte;
 	Oid			userid;
 	ForeignTable *table;
 	UserMapping *user;
@@ -3283,7 +3301,6 @@ create_foreign_modify(EState *estate,
 	 * Identify which user to do the remote access as.  This should match what
 	 * ExecCheckRTEPerms() does.
 	 */
-	rte = rt_fetch(resultRelInfo->ri_RangeTableIndex, estate->es_range_table);
 	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
 
 	/* Get info about foreign table. */
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index d37cc88..a5d4011 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -140,11 +140,11 @@ extern void classifyConditions(PlannerInfo *root,
 extern bool is_foreign_expr(PlannerInfo *root,
 				RelOptInfo *baserel,
 				Expr *expr);
-extern void deparseInsertSql(StringInfo buf, PlannerInfo *root,
+extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing, List *returningList,
 				 List **retrieved_attrs);
-extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
+extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, List *returningList,
 				 List **retrieved_attrs);
@@ -157,7 +157,7 @@ extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 					   List **params_list,
 					   List *returningList,
 					   List **retrieved_attrs);
-extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
+extern void deparseDeleteSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *returningList,
 				 List **retrieved_attrs);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 633e9fe..231b1e0 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1804,6 +1804,31 @@ insert into itrtest values (1, 'bar') on conflict (a) do update set b = excluded
 
 select tableoid::regclass, * FROM itrtest;
 
+delete from itrtest;
+
+drop index loct1_idx;
+
+-- Test that remote triggers work with insert tuple routing
+create function br_insert_trigfunc() returns trigger as $$
+begin
+	new.b := new.b || ' triggered !';
+	return new;
+end
+$$ language plpgsql;
+create trigger loct1_br_insert_trigger before insert on loct1
+	for each row execute procedure br_insert_trigfunc();
+create trigger loct2_br_insert_trigger before insert on loct2
+	for each row execute procedure br_insert_trigfunc();
+
+-- The new values are concatenated with ' triggered !'
+insert into itrtest values (1, 'foo') returning *;
+insert into itrtest values (2, 'qux') returning *;
+insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+
+drop trigger loct1_br_insert_trigger on loct1;
+drop trigger loct2_br_insert_trigger on loct2;
+
 drop table itrtest;
 drop table loct1;
 drop table loct2;
@@ -1836,6 +1861,30 @@ select tableoid::regclass, * FROM locp;
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
 
+-- Test that remote triggers work with update tuple routing
+create trigger loct_br_insert_trigger before insert on loct
+	for each row execute procedure br_insert_trigfunc();
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+
+-- Check case where the foreign partition is a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+
+-- Check case where the foreign partition isn't a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 2 returning *;
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 2 returning *;
+
+drop trigger loct_br_insert_trigger on loct;
+
 drop table utrtest;
 drop table loct;
 
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index b2ee92e..954a96c 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -333,6 +333,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 					  estate->es_instrument);
 
 	/*
+	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
+	 * partition-key becomes a DELETE+INSERT operation, so this check is still
+	 * required when the operation is CMD_UPDATE.
+	 */
+	CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+
+	/*
 	 * Since we've just initialized this ResultRelInfo, it's not in any list
 	 * attached to the estate as yet.  Add it, so that it can be found later.
 	 *
@@ -344,9 +351,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 		lappend(estate->es_tuple_routing_result_relations,
 				leaf_part_rri);
 
-	/* Set up information needed for routing tuples to this partition. */
-	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
-
 	/*
 	 * Open partition indices.  The user may have asked to check for conflicts
 	 * within this leaf partition and do "nothing" instead of throwing an
@@ -489,6 +493,9 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 									&mtstate->ps, RelationGetDescr(partrel));
 	}
 
+	/* Set up information needed for routing tuples to the partition. */
+	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
+
 	/*
 	 * If there is an ON CONFLICT clause, initialize state for it.
 	 */
@@ -655,8 +662,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 
 /*
  * ExecInitRoutingInfo
- *		Set up information needed for routing tuples to a leaf partition if
- *		routable; else abort the operation
+ *		Set up information needed for routing tuples to a leaf partition
  */
 void
 ExecInitRoutingInfo(ModifyTableState *mtstate,
@@ -667,9 +673,6 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 {
 	MemoryContext oldContext;
 
-	/* Verify the partition is a valid target for INSERT */
-	CheckValidResultRel(partRelInfo, CMD_INSERT);
-
 	/*
 	 * Switch into per-query memory context.
 	 */
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 7ec2c6b..fc1e538 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1700,20 +1700,24 @@ ExecPrepareTupleRouting(ModifyTableState *mtstate,
 										partidx);
 
 	/*
-	 * Set up information needed for routing tuples to the partition if we
-	 * didn't yet (ExecInitRoutingInfo would abort the operation if the
-	 * partition isn't routable).
+	 * Check whether the partition is routable if we didn't yet
 	 *
 	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
-	 * tuple to a new partition.  This setup would be needed for a subplan
+	 * tuple to a new partition.  This check would be applied to a subplan
 	 * partition of such an UPDATE that is chosen as the partition to route
-	 * the tuple to.  The reason we do this setup here rather than in
+	 * the tuple to.  The reason we do this check here rather than in
 	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
 	 * unnecessarily due to non-routable subplan partitions that may not be
 	 * chosen for update tuple movement after all.
 	 */
 	if (!partrel->ri_PartitionReadyForRouting)
+	{
+		/* Verify the partition is a valid target for INSERT. */
+		CheckValidResultRel(partrel, CMD_INSERT);
+
+		/* Set up information needed for routing tuples to the partition. */
 		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
+	}
 
 	/*
 	 * Make it look like we are inserting into the partition.
#30Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Etsuro Fujita (#27)
1 attachment(s)
Re: Oddity in tuple routing for foreign partitions

On 2018/04/26 12:43, Etsuro Fujita wrote:

(2018/04/25 17:29), Amit Langote wrote:

Hmm, I missed that we do need information from a proper RTE as well.  So,
I suppose we should be doing this instead of creating the RTE for foreign
partition from scratch.

+    rte = list_nth(estate->es_range_table, resultRelation - 1);
+    rte = copyObject(rte);
+    rte->relid = RelationGetRelid(rel);
+    rte->relkind = RELKIND_FOREIGN_TABLE;

As I said upthread, we can use the RTE in the range table as-is if the
foreign table is one of the UPDATE subplan partitions or the target
specified in a COPY command.  So, I created the patch that way because
that would save cycles.  Why not just use that RTE in those cases?

Yes, I agree it's better to reuse the RTE in the cases we can. So, how
does this look:

+    /*
+     * If the foreign table is a partition, we need to create a new RTE
+     * describing the foreign table for use by deparseInsertSql and
+     * create_foreign_modify() below, after first copying the parent's
+     * RTE and modifying some fields to describe the foreign partition to
+     * work on. However, if this is invoked by UPDATE, the existing RTE
+     * may already correspond to this partition if it is one of the
+     * UPDATE subplan target rels; in that case, we can just use the
+     * existing RTE as-is.
+     */
+    rte = list_nth(estate->es_range_table, resultRelation - 1);
+    if (rte->relid != RelationGetRelid(rel))
+    {
+        rte = copyObject(rte);
+        rte->relid = RelationGetRelid(rel);
+        rte->relkind = RELKIND_FOREIGN_TABLE;
+
+        /*
+         * For UPDATE, we must use the RT index of the first subplan
+         * target rel's RTE, because the core code would have built
+         * expressions for the partition, such as RETURNING, using that
+         * RT index as varno of Vars contained in those expressions.
+         */
+        if (plan && plan->operation == CMD_UPDATE &&
+            resultRelation == plan->nominalRelation)
+            resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+    }

I added the block inside the if because I agree with your comment below
that the changes to ExecInitPartitionInfo are better kept for later to
think more carefully about the dependencies it might break.

If we apply the other patch I proposed, resultRelation always points to
the correct RTE.

I tried to do that.  So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
    InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Actually, I also thought the former when creating the patch, but I left
that as-is because I'm not sure that would be really safe; ExecConstraints
looks at the RT index via GetInsertedColumns/GetUpdatedColumns, so what I
thought was: that might cause unexpected behavior.

OK, I have to agree here that we better leave 1 to be looked at later.

After this change, GetInsertedColumns/GetUpdatedColumns will start
returning a different set of columns in some cases than it does now.
Currently, it *always* returns a set containing root table's attribute
numbers, even for UPDATE. But with this change, for UPDATE, it will start
returning the set containing the first subplan target rel's attribute
numbers, which could be any partition with arbitrarily different attribute
numbers.

Anyway, I think that
the former is more like an improvement rather than a fix, so it would be
better to leave that for another patch for PG12?

I agree, so I'm dropping the patch for 1.

See attached an updated version with changes as described above.

Thanks,
Amit

Attachments:

fix-tuple-routing-for-foreign-partitions-7.patchtext/plain; charset=UTF-8; name=fix-tuple-routing-for-foreign-partitions-7.patchDownload
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 6e2fa1420c..d272719ff4 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -125,7 +125,7 @@ static char *deparse_type_name(Oid type_oid, int32 typemod);
  * Functions to construct string representation of a node tree.
  */
 static void deparseTargetList(StringInfo buf,
-				  PlannerInfo *root,
+				  RangeTblEntry *rte,
 				  Index rtindex,
 				  Relation rel,
 				  bool is_returning,
@@ -137,13 +137,13 @@ static void deparseExplicitTargetList(List *tlist,
 						  List **retrieved_attrs,
 						  deparse_expr_cxt *context);
 static void deparseSubqueryTargetList(deparse_expr_cxt *context);
-static void deparseReturningList(StringInfo buf, PlannerInfo *root,
+static void deparseReturningList(StringInfo buf, RangeTblEntry *rte,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
 					 List **retrieved_attrs);
 static void deparseColumnRef(StringInfo buf, int varno, int varattno,
-				 PlannerInfo *root, bool qualify_col);
+				 RangeTblEntry *rte, bool qualify_col);
 static void deparseRelation(StringInfo buf, Relation rel);
 static void deparseExpr(Expr *expr, deparse_expr_cxt *context);
 static void deparseVar(Var *node, deparse_expr_cxt *context);
@@ -1050,7 +1050,7 @@ deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
 		 */
 		Relation	rel = heap_open(rte->relid, NoLock);
 
-		deparseTargetList(buf, root, foreignrel->relid, rel, false,
+		deparseTargetList(buf, rte, foreignrel->relid, rel, false,
 						  fpinfo->attrs_used, false, retrieved_attrs);
 		heap_close(rel, NoLock);
 	}
@@ -1099,7 +1099,7 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
  */
 static void
 deparseTargetList(StringInfo buf,
-				  PlannerInfo *root,
+				  RangeTblEntry *rte,
 				  Index rtindex,
 				  Relation rel,
 				  bool is_returning,
@@ -1137,7 +1137,7 @@ deparseTargetList(StringInfo buf,
 				appendStringInfoString(buf, " RETURNING ");
 			first = false;
 
-			deparseColumnRef(buf, rtindex, i, root, qualify_col);
+			deparseColumnRef(buf, rtindex, i, rte, qualify_col);
 
 			*retrieved_attrs = lappend_int(*retrieved_attrs, i);
 		}
@@ -1649,7 +1649,7 @@ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  * to *retrieved_attrs.
  */
 void
-deparseInsertSql(StringInfo buf, PlannerInfo *root,
+deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing,
 				 List *returningList, List **retrieved_attrs)
@@ -1674,7 +1674,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
 				appendStringInfoString(buf, ", ");
 			first = false;
 
-			deparseColumnRef(buf, rtindex, attnum, root, false);
+			deparseColumnRef(buf, rtindex, attnum, rte, false);
 		}
 
 		appendStringInfoString(buf, ") VALUES (");
@@ -1699,7 +1699,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
 	if (doNothing)
 		appendStringInfoString(buf, " ON CONFLICT DO NOTHING");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_insert_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1712,7 +1712,7 @@ deparseInsertSql(StringInfo buf, PlannerInfo *root,
  * to *retrieved_attrs.
  */
 void
-deparseUpdateSql(StringInfo buf, PlannerInfo *root,
+deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, List *returningList,
 				 List **retrieved_attrs)
@@ -1735,13 +1735,13 @@ deparseUpdateSql(StringInfo buf, PlannerInfo *root,
 			appendStringInfoString(buf, ", ");
 		first = false;
 
-		deparseColumnRef(buf, rtindex, attnum, root, false);
+		deparseColumnRef(buf, rtindex, attnum, rte, false);
 		appendStringInfo(buf, " = $%d", pindex);
 		pindex++;
 	}
 	appendStringInfoString(buf, " WHERE ctid = $1");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_update_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1777,6 +1777,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 	int			nestlevel;
 	bool		first;
 	ListCell   *lc;
+	RangeTblEntry *rte = planner_rt_fetch(rtindex, root);
 
 	/* Set up context struct for recursion */
 	context.root = root;
@@ -1808,7 +1809,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 			appendStringInfoString(buf, ", ");
 		first = false;
 
-		deparseColumnRef(buf, rtindex, attnum, root, false);
+		deparseColumnRef(buf, rtindex, attnum, rte, false);
 		appendStringInfoString(buf, " = ");
 		deparseExpr((Expr *) tle->expr, &context);
 	}
@@ -1835,7 +1836,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 		deparseExplicitTargetList(returningList, true, retrieved_attrs,
 								  &context);
 	else
-		deparseReturningList(buf, root, rtindex, rel, false,
+		deparseReturningList(buf, rte, rtindex, rel, false,
 							 returningList, retrieved_attrs);
 }
 
@@ -1847,7 +1848,7 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  * to *retrieved_attrs.
  */
 void
-deparseDeleteSql(StringInfo buf, PlannerInfo *root,
+deparseDeleteSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *returningList,
 				 List **retrieved_attrs)
@@ -1856,7 +1857,7 @@ deparseDeleteSql(StringInfo buf, PlannerInfo *root,
 	deparseRelation(buf, rel);
 	appendStringInfoString(buf, " WHERE ctid = $1");
 
-	deparseReturningList(buf, root, rtindex, rel,
+	deparseReturningList(buf, rte, rtindex, rel,
 						 rel->trigdesc && rel->trigdesc->trig_delete_after_row,
 						 returningList, retrieved_attrs);
 }
@@ -1918,7 +1919,8 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
 		deparseExplicitTargetList(returningList, true, retrieved_attrs,
 								  &context);
 	else
-		deparseReturningList(buf, root, rtindex, rel, false,
+		deparseReturningList(buf, planner_rt_fetch(rtindex, root),
+							 rtindex, rel, false,
 							 returningList, retrieved_attrs);
 }
 
@@ -1926,7 +1928,7 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  * Add a RETURNING clause, if needed, to an INSERT/UPDATE/DELETE.
  */
 static void
-deparseReturningList(StringInfo buf, PlannerInfo *root,
+deparseReturningList(StringInfo buf, RangeTblEntry *rte,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
@@ -1952,7 +1954,7 @@ deparseReturningList(StringInfo buf, PlannerInfo *root,
 	}
 
 	if (attrs_used != NULL)
-		deparseTargetList(buf, root, rtindex, rel, true, attrs_used, false,
+		deparseTargetList(buf, rte, rtindex, rel, true, attrs_used, false,
 						  retrieved_attrs);
 	else
 		*retrieved_attrs = NIL;
@@ -2048,11 +2050,9 @@ deparseAnalyzeSql(StringInfo buf, Relation rel, List **retrieved_attrs)
  * If qualify_col is true, qualify column name with the alias of relation.
  */
 static void
-deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
+deparseColumnRef(StringInfo buf, int varno, int varattno, RangeTblEntry *rte,
 				 bool qualify_col)
 {
-	RangeTblEntry *rte;
-
 	/* We support fetching the remote side's CTID and OID. */
 	if (varattno == SelfItemPointerAttributeNumber)
 	{
@@ -2077,10 +2077,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		Oid			fetchval = 0;
 
 		if (varattno == TableOidAttributeNumber)
-		{
-			rte = planner_rt_fetch(varno, root);
 			fetchval = rte->relid;
-		}
 
 		if (qualify_col)
 		{
@@ -2100,9 +2097,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		/* Required only to be passed down to deparseTargetList(). */
 		List	   *retrieved_attrs;
 
-		/* Get RangeTblEntry from array in PlannerInfo. */
-		rte = planner_rt_fetch(varno, root);
-
 		/*
 		 * The lock on the relation will be held by upper callers, so it's
 		 * fine to open it with no lock here.
@@ -2134,7 +2128,7 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		}
 
 		appendStringInfoString(buf, "ROW(");
-		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
+		deparseTargetList(buf, rte, varno, rel, false, attrs_used, qualify_col,
 						  &retrieved_attrs);
 		appendStringInfoChar(buf, ')');
 
@@ -2154,9 +2148,6 @@ deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
 		/* varno must not be any of OUTER_VAR, INNER_VAR and INDEX_VAR. */
 		Assert(!IS_SPECIAL_VARNO(varno));
 
-		/* Get RangeTblEntry from array in PlannerInfo. */
-		rte = planner_rt_fetch(varno, root);
-
 		/*
 		 * If it's a column of a foreign table, and it has the column_name FDW
 		 * option, use that value.
@@ -2354,7 +2345,8 @@ deparseVar(Var *node, deparse_expr_cxt *context)
 
 	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
 		deparseColumnRef(context->buf, node->varno, node->varattno,
-						 context->root, qualify_col);
+						 planner_rt_fetch(node->varno, context->root),
+						 qualify_col);
 	else
 	{
 		/* Treat like a Param */
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 53ed285875..bb6b1a8fdf 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7454,6 +7454,48 @@ select tableoid::regclass, * FROM itrtest;
  remp1    | 1 | foo
 (1 row)
 
+delete from itrtest;
+drop index loct1_idx;
+-- Test that remote triggers work with insert tuple routing
+create function br_insert_trigfunc() returns trigger as $$
+begin
+	new.b := new.b || ' triggered !';
+	return new;
+end
+$$ language plpgsql;
+create trigger loct1_br_insert_trigger before insert on loct1
+	for each row execute procedure br_insert_trigfunc();
+create trigger loct2_br_insert_trigger before insert on loct2
+	for each row execute procedure br_insert_trigfunc();
+-- The new values are concatenated with ' triggered !'
+insert into itrtest values (1, 'foo') returning *;
+ a |        b        
+---+-----------------
+ 1 | foo triggered !
+(1 row)
+
+insert into itrtest values (2, 'qux') returning *;
+ a |        b        
+---+-----------------
+ 2 | qux triggered !
+(1 row)
+
+insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+ a |         b         
+---+-------------------
+ 1 | test1 triggered !
+ 2 | test2 triggered !
+(2 rows)
+
+with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+ a |         b         
+---+-------------------
+ 1 | test1 triggered !
+ 2 | test2 triggered !
+(2 rows)
+
+drop trigger loct1_br_insert_trigger on loct1;
+drop trigger loct2_br_insert_trigger on loct2;
 drop table itrtest;
 drop table loct1;
 drop table loct2;
@@ -7518,6 +7560,57 @@ select tableoid::regclass, * FROM locp;
 
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
+-- Test that remote triggers work with update tuple routing
+create trigger loct_br_insert_trigger before insert on loct
+	for each row execute procedure br_insert_trigfunc();
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+-- Check case where the foreign partition is a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
+ Update on public.utrtest
+   Output: remp.a, remp.b
+   Foreign Update on public.remp
+   Update on public.locp
+   ->  Foreign Update on public.remp
+         Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b
+   ->  Seq Scan on public.locp
+         Output: 1, locp.b, locp.ctid
+         Filter: ((locp.a = 1) OR (locp.a = 2))
+(9 rows)
+
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+ a |        b        
+---+-----------------
+ 1 | qux triggered !
+(1 row)
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+-- Check case where the foreign partition isn't a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 2 returning *;
+              QUERY PLAN              
+--------------------------------------
+ Update on public.utrtest
+   Output: locp.a, locp.b
+   Update on public.locp
+   ->  Seq Scan on public.locp
+         Output: 1, locp.b, locp.ctid
+         Filter: (locp.a = 2)
+(6 rows)
+
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 2 returning *;
+ a |        b        
+---+-----------------
+ 1 | qux triggered !
+(1 row)
+
+drop trigger loct_br_insert_trigger on loct;
 drop table utrtest;
 drop table loct;
 -- Test copy tuple routing
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index a46160df7c..2ac947c37a 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -381,6 +381,7 @@ static void create_cursor(ForeignScanState *node);
 static void fetch_more_data(ForeignScanState *node);
 static void close_cursor(PGconn *conn, unsigned int cursor_number);
 static PgFdwModifyState *create_foreign_modify(EState *estate,
+					  RangeTblEntry *rte,
 					  ResultRelInfo *resultRelInfo,
 					  CmdType operation,
 					  Plan *subplan,
@@ -1653,17 +1654,17 @@ postgresPlanForeignModify(PlannerInfo *root,
 	switch (operation)
 	{
 		case CMD_INSERT:
-			deparseInsertSql(&sql, root, resultRelation, rel,
+			deparseInsertSql(&sql, rte, resultRelation, rel,
 							 targetAttrs, doNothing, returningList,
 							 &retrieved_attrs);
 			break;
 		case CMD_UPDATE:
-			deparseUpdateSql(&sql, root, resultRelation, rel,
+			deparseUpdateSql(&sql, rte, resultRelation, rel,
 							 targetAttrs, returningList,
 							 &retrieved_attrs);
 			break;
 		case CMD_DELETE:
-			deparseDeleteSql(&sql, root, resultRelation, rel,
+			deparseDeleteSql(&sql, rte, resultRelation, rel,
 							 returningList,
 							 &retrieved_attrs);
 			break;
@@ -1700,6 +1701,7 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 	List	   *target_attrs;
 	bool		has_returning;
 	List	   *retrieved_attrs;
+	RangeTblEntry *rte;
 
 	/*
 	 * Do nothing in EXPLAIN (no ANALYZE) case.  resultRelInfo->ri_FdwState
@@ -1718,8 +1720,13 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 	retrieved_attrs = (List *) list_nth(fdw_private,
 										FdwModifyPrivateRetrievedAttrs);
 
+	/* Find RTE. */
+	rte = rt_fetch(resultRelInfo->ri_RangeTableIndex,
+				   mtstate->ps.state->es_range_table);
+
 	/* Construct an execution state. */
 	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
 									resultRelInfo,
 									mtstate->operation,
 									mtstate->mt_plans[subplan_index]->plan,
@@ -1974,11 +1981,11 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 						   ResultRelInfo *resultRelInfo)
 {
 	PgFdwModifyState *fmstate;
-	Plan	   *plan = mtstate->ps.plan;
+	ModifyTable *plan = castNode(ModifyTable, mtstate->ps.plan);
+	EState	   *estate = mtstate->ps.state;
+	Index		resultRelation = resultRelInfo->ri_RangeTableIndex;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	RangeTblEntry *rte;
-	Query	   *query;
-	PlannerInfo *root;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
 	int			attnum;
 	StringInfoData sql;
@@ -1988,18 +1995,6 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 
 	initStringInfo(&sql);
 
-	/* Set up largely-dummy planner state. */
-	rte = makeNode(RangeTblEntry);
-	rte->rtekind = RTE_RELATION;
-	rte->relid = RelationGetRelid(rel);
-	rte->relkind = RELKIND_FOREIGN_TABLE;
-	query = makeNode(Query);
-	query->commandType = CMD_INSERT;
-	query->resultRelation = 1;
-	query->rtable = list_make1(rte);
-	root = makeNode(PlannerInfo);
-	root->parse = query;
-
 	/* We transmit all columns that are defined in the foreign table. */
 	for (attnum = 1; attnum <= tupdesc->natts; attnum++)
 	{
@@ -2022,12 +2017,41 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 				 (int) onConflictAction);
 	}
 
+	/*
+	 * If the foreign table is a partition, we need to create a new RTE
+	 * describing the foreign table for use by deparseInsertSql and
+	 * create_foreign_modify() below, after first copying the parent's
+	 * RTE and modifying some fields to describe the foreign partition to
+	 * work on. However, if this is invoked by UPDATE, the existing RTE
+	 * may already correspond to this partition if it is one of the
+	 * UPDATE subplan target rels; in that case, we can just use the
+	 * existing RTE as-is.
+	 */
+	rte = list_nth(estate->es_range_table, resultRelation - 1);
+	if (rte->relid != RelationGetRelid(rel))
+	{
+		rte = copyObject(rte);
+		rte->relid = RelationGetRelid(rel);
+		rte->relkind = RELKIND_FOREIGN_TABLE;
+
+		/*
+		 * For UPDATE, we must use the RT index of the first subplan
+		 * target rel's RTE, because the core code would have built
+		 * expressions for the partition, such as RETURNING, using that
+		 * RT index as varno of Vars contained in those expressions.
+		 */
+		if (plan && plan->operation == CMD_UPDATE &&
+			resultRelation == plan->nominalRelation)
+			resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+	}
+
 	/* Construct the SQL command string. */
-	deparseInsertSql(&sql, root, 1, rel, targetAttrs, doNothing,
+	deparseInsertSql(&sql, rte, resultRelation, rel, targetAttrs, doNothing,
 					 resultRelInfo->ri_returningList, &retrieved_attrs);
 
 	/* Construct an execution state. */
 	fmstate = create_foreign_modify(mtstate->ps.state,
+									rte,
 									resultRelInfo,
 									CMD_INSERT,
 									NULL,
@@ -3255,6 +3279,7 @@ close_cursor(PGconn *conn, unsigned int cursor_number)
  */
 static PgFdwModifyState *
 create_foreign_modify(EState *estate,
+					  RangeTblEntry *rte,
 					  ResultRelInfo *resultRelInfo,
 					  CmdType operation,
 					  Plan *subplan,
@@ -3266,7 +3291,6 @@ create_foreign_modify(EState *estate,
 	PgFdwModifyState *fmstate;
 	Relation	rel = resultRelInfo->ri_RelationDesc;
 	TupleDesc	tupdesc = RelationGetDescr(rel);
-	RangeTblEntry *rte;
 	Oid			userid;
 	ForeignTable *table;
 	UserMapping *user;
@@ -3283,7 +3307,6 @@ create_foreign_modify(EState *estate,
 	 * Identify which user to do the remote access as.  This should match what
 	 * ExecCheckRTEPerms() does.
 	 */
-	rte = rt_fetch(resultRelInfo->ri_RangeTableIndex, estate->es_range_table);
 	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
 
 	/* Get info about foreign table. */
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index d37cc88b6e..a5d4011e8d 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -140,11 +140,11 @@ extern void classifyConditions(PlannerInfo *root,
 extern bool is_foreign_expr(PlannerInfo *root,
 				RelOptInfo *baserel,
 				Expr *expr);
-extern void deparseInsertSql(StringInfo buf, PlannerInfo *root,
+extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing, List *returningList,
 				 List **retrieved_attrs);
-extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
+extern void deparseUpdateSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, List *returningList,
 				 List **retrieved_attrs);
@@ -157,7 +157,7 @@ extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 					   List **params_list,
 					   List *returningList,
 					   List **retrieved_attrs);
-extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
+extern void deparseDeleteSql(StringInfo buf, RangeTblEntry *rte,
 				 Index rtindex, Relation rel,
 				 List *returningList,
 				 List **retrieved_attrs);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 633e9fecec..231b1e01a5 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1804,6 +1804,31 @@ insert into itrtest values (1, 'bar') on conflict (a) do update set b = excluded
 
 select tableoid::regclass, * FROM itrtest;
 
+delete from itrtest;
+
+drop index loct1_idx;
+
+-- Test that remote triggers work with insert tuple routing
+create function br_insert_trigfunc() returns trigger as $$
+begin
+	new.b := new.b || ' triggered !';
+	return new;
+end
+$$ language plpgsql;
+create trigger loct1_br_insert_trigger before insert on loct1
+	for each row execute procedure br_insert_trigfunc();
+create trigger loct2_br_insert_trigger before insert on loct2
+	for each row execute procedure br_insert_trigfunc();
+
+-- The new values are concatenated with ' triggered !'
+insert into itrtest values (1, 'foo') returning *;
+insert into itrtest values (2, 'qux') returning *;
+insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+with result as (insert into itrtest values (1, 'test1'), (2, 'test2') returning *) select * from result;
+
+drop trigger loct1_br_insert_trigger on loct1;
+drop trigger loct2_br_insert_trigger on loct2;
+
 drop table itrtest;
 drop table loct1;
 drop table loct2;
@@ -1836,6 +1861,30 @@ select tableoid::regclass, * FROM locp;
 -- The executor should not let unexercised FDWs shut down
 update utrtest set a = 1 where b = 'foo';
 
+-- Test that remote triggers work with update tuple routing
+create trigger loct_br_insert_trigger before insert on loct
+	for each row execute procedure br_insert_trigfunc();
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+
+-- Check case where the foreign partition is a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 1 or a = 2 returning *;
+
+delete from utrtest;
+insert into utrtest values (2, 'qux');
+
+-- Check case where the foreign partition isn't a subplan target rel
+explain (verbose, costs off)
+update utrtest set a = 1 where a = 2 returning *;
+-- The new values are concatenated with ' triggered !'
+update utrtest set a = 1 where a = 2 returning *;
+
+drop trigger loct_br_insert_trigger on loct;
+
 drop table utrtest;
 drop table loct;
 
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index b2ee92eb15..954a96c696 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -333,6 +333,13 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 					  estate->es_instrument);
 
 	/*
+	 * Verify result relation is a valid target for an INSERT.  An UPDATE of a
+	 * partition-key becomes a DELETE+INSERT operation, so this check is still
+	 * required when the operation is CMD_UPDATE.
+	 */
+	CheckValidResultRel(leaf_part_rri, CMD_INSERT);
+
+	/*
 	 * Since we've just initialized this ResultRelInfo, it's not in any list
 	 * attached to the estate as yet.  Add it, so that it can be found later.
 	 *
@@ -344,9 +351,6 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 		lappend(estate->es_tuple_routing_result_relations,
 				leaf_part_rri);
 
-	/* Set up information needed for routing tuples to this partition. */
-	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
-
 	/*
 	 * Open partition indices.  The user may have asked to check for conflicts
 	 * within this leaf partition and do "nothing" instead of throwing an
@@ -489,6 +493,9 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 									&mtstate->ps, RelationGetDescr(partrel));
 	}
 
+	/* Set up information needed for routing tuples to the partition. */
+	ExecInitRoutingInfo(mtstate, estate, proute, leaf_part_rri, partidx);
+
 	/*
 	 * If there is an ON CONFLICT clause, initialize state for it.
 	 */
@@ -655,8 +662,7 @@ ExecInitPartitionInfo(ModifyTableState *mtstate,
 
 /*
  * ExecInitRoutingInfo
- *		Set up information needed for routing tuples to a leaf partition if
- *		routable; else abort the operation
+ *		Set up information needed for routing tuples to a leaf partition
  */
 void
 ExecInitRoutingInfo(ModifyTableState *mtstate,
@@ -667,9 +673,6 @@ ExecInitRoutingInfo(ModifyTableState *mtstate,
 {
 	MemoryContext oldContext;
 
-	/* Verify the partition is a valid target for INSERT */
-	CheckValidResultRel(partRelInfo, CMD_INSERT);
-
 	/*
 	 * Switch into per-query memory context.
 	 */
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 7ec2c6bcaa..fc1e5385bb 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1700,20 +1700,24 @@ ExecPrepareTupleRouting(ModifyTableState *mtstate,
 										partidx);
 
 	/*
-	 * Set up information needed for routing tuples to the partition if we
-	 * didn't yet (ExecInitRoutingInfo would abort the operation if the
-	 * partition isn't routable).
+	 * Check whether the partition is routable if we didn't yet
 	 *
 	 * Note: an UPDATE of a partition key invokes an INSERT that moves the
-	 * tuple to a new partition.  This setup would be needed for a subplan
+	 * tuple to a new partition.  This check would be applied to a subplan
 	 * partition of such an UPDATE that is chosen as the partition to route
-	 * the tuple to.  The reason we do this setup here rather than in
+	 * the tuple to.  The reason we do this check here rather than in
 	 * ExecSetupPartitionTupleRouting is to avoid aborting such an UPDATE
 	 * unnecessarily due to non-routable subplan partitions that may not be
 	 * chosen for update tuple movement after all.
 	 */
 	if (!partrel->ri_PartitionReadyForRouting)
+	{
+		/* Verify the partition is a valid target for INSERT. */
+		CheckValidResultRel(partrel, CMD_INSERT);
+
+		/* Set up information needed for routing tuples to the partition. */
 		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
+	}
 
 	/*
 	 * Make it look like we are inserting into the partition.
#31Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Etsuro Fujita (#29)
Re: Oddity in tuple routing for foreign partitions

On 2018/04/26 15:27, Etsuro Fujita wrote:

(2018/04/26 12:43), Etsuro Fujita wrote:

(2018/04/25 17:29), Amit Langote wrote:

On 2018/04/25 16:42, Kyotaro HORIGUCHI wrote:

At Wed, 25 Apr 2018 11:19:23 +0900, Amit Langote wrote:

After the refactoring, it appears to me that we only need this much:

+ rte = makeNode(RangeTblEntry);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RELKIND_FOREIGN_TABLE;

Mmm.. That is, only relid is required to deparse (I don't mean
that it should be refactored so.). On the other hand
create_foreign_modify requires rte->checkAsUser as well.

That's right. I took care of this in my version, but unfortuneately,
that was ignored in the updated versions. Maybe the comments I added to
the patch were not enough, though.

Hmm, I missed that we do need information from a proper RTE as well. So,
I suppose we should be doing this instead of creating the RTE for foreign
partition from scratch.

+ rte = list_nth(estate->es_range_table, resultRelation - 1);
+ rte = copyObject(rte);
+ rte->relid = RelationGetRelid(rel);
+ rte->relkind = RELKIND_FOREIGN_TABLE;

As I said upthread, we can use the RTE in the range table as-is if the
foreign table is one of the UPDATE subplan partitions or the target
specified in a COPY command. So, I created the patch that way because
that would save cycles. Why not just use that RTE in those cases?

If we apply the other patch I proposed, resultRelation always points to
the correct RTE.

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Actually, I also thought the former when creating the patch, but I left
that as-is because I'm not sure that would be really safe;
ExecConstraints looks at the RT index via
GetInsertedColumns/GetUpdatedColumns, so what I thought was: that might
cause unexpected behavior. Anyway, I think that the former is more like
an improvement rather than a fix, so it would be better to leave that
for another patch for PG12?

Here is a new version I'd like to propose for fixing this issue without
the former patch.

Thanks for working on this!

Sorry, didn't notice this before sending my patch, which I also marked v7.

It's a bit different than yours and has comments with excerpts from your
earlier versions. See if you find it helpful.

Thanks,
Amit

#32Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Amit Langote (#31)
Re: Oddity in tuple routing for foreign partitions

(2018/04/26 15:39), Amit Langote wrote:

On 2018/04/26 15:27, Etsuro Fujita wrote:

Here is a new version I'd like to propose for fixing this issue without
the former patch.

Sorry, didn't notice this before sending my patch, which I also marked v7.

It's a bit different than yours and has comments with excerpts from your
earlier versions. See if you find it helpful.

OK, will review your version.

Best regards,
Etsuro Fujita

#33Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Robert Haas (#25)
Re: Oddity in tuple routing for foreign partitions

On 2018/04/26 2:59, Robert Haas wrote:

On Tue, Apr 24, 2018 at 10:19 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Are you splitting this into 2 patches here because there are 2
separate issues? If so, what are those issues? There seem to be a
bunch of interrelated changes here but I haven't seen a clear
explanation of which ones are needed for which reasons.

I agree that fixing ExecInitPartitionInfo to use the right RT index in
the first place sounds like a better idea than trying to piece
together which RT index we should be using after-the-fact, but I need
to better understand what problems we're trying to fix here before I
can be sure if that's the strategy I want to endorse...

After considering Fujita-san's comment downthread, I think it might be
better to revisit 1 later, because it could break a certain things which
rely on ri_RangeTableIndex being set to the index of the root partitioned
table's RT entry. Fujita-san pointed out GetInsertedColumns et al that
return a set of attribute numbers after looking up the RT entry using
ri_RangeTableIndex of a given ResultRelInfo. They would start returning a
different set for partitions than previously in some cases due to this
change. Maybe, there are other things that rely on what a partition's
ri_RangeTableEntry has been set to.

For now, I think we'll have to stick with a solution to determine which RT
index to pass to deparseInsertSql that involves a bit of
reverse-engineering, whereby we base that on where the partition's
ResultRelInfo seems to have originated from. If it looks like it was
created by ExecInitModifyTable because the partition is one of the subplan
target rels, then use the RTE and the RT index as is. If not,
ExecInitPartitionInfo would have created it because the partition was
chosen as the tuple routing target. In that case, we have to freshly
create an RTE for the partition after copying most of the fields from the
parent's RTE. We're done if the partition was selected as routing target
for an INSERT. If for UPDATE, we also have to change the RT index to pass
to deparseInsertSql to that of the first subplan's target rel, because
that's what we use as varno of expressions contained in the partition's
ResultRelInfo. Phew!

Thanks,
Amit

#34Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Amit Langote (#30)
Re: Oddity in tuple routing for foreign partitions

(2018/04/26 15:35), Amit Langote wrote:

On 2018/04/26 12:43, Etsuro Fujita wrote:

(2018/04/25 17:29), Amit Langote wrote:

Hmm, I missed that we do need information from a proper RTE as well. So,
I suppose we should be doing this instead of creating the RTE for foreign
partition from scratch.

+    rte = list_nth(estate->es_range_table, resultRelation - 1);
+    rte = copyObject(rte);
+    rte->relid = RelationGetRelid(rel);
+    rte->relkind = RELKIND_FOREIGN_TABLE;

As I said upthread, we can use the RTE in the range table as-is if the
foreign table is one of the UPDATE subplan partitions or the target
specified in a COPY command. So, I created the patch that way because
that would save cycles. Why not just use that RTE in those cases?

Yes, I agree it's better to reuse the RTE in the cases we can. So, how
does this look:

+    /*
+     * If the foreign table is a partition, we need to create a new RTE
+     * describing the foreign table for use by deparseInsertSql and
+     * create_foreign_modify() below, after first copying the parent's
+     * RTE and modifying some fields to describe the foreign partition to
+     * work on. However, if this is invoked by UPDATE, the existing RTE
+     * may already correspond to this partition if it is one of the
+     * UPDATE subplan target rels; in that case, we can just use the
+     * existing RTE as-is.
+     */
+    rte = list_nth(estate->es_range_table, resultRelation - 1);
+    if (rte->relid != RelationGetRelid(rel))
+    {
+        rte = copyObject(rte);
+        rte->relid = RelationGetRelid(rel);
+        rte->relkind = RELKIND_FOREIGN_TABLE;
+
+        /*
+         * For UPDATE, we must use the RT index of the first subplan
+         * target rel's RTE, because the core code would have built
+         * expressions for the partition, such as RETURNING, using that
+         * RT index as varno of Vars contained in those expressions.
+         */
+        if (plan&&  plan->operation == CMD_UPDATE&&
+            resultRelation == plan->nominalRelation)
+            resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+    }

Seems like a better change than mine; because this simplifies the logic.

I added the block inside the if because I agree with your comment below
that the changes to ExecInitPartitionInfo are better kept for later to
think more carefully about the dependencies it might break.

OK

If we apply the other patch I proposed, resultRelation always points to
the correct RTE.

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Actually, I also thought the former when creating the patch, but I left
that as-is because I'm not sure that would be really safe; ExecConstraints
looks at the RT index via GetInsertedColumns/GetUpdatedColumns, so what I
thought was: that might cause unexpected behavior.

OK, I have to agree here that we better leave 1 to be looked at later.

After this change, GetInsertedColumns/GetUpdatedColumns will start
returning a different set of columns in some cases than it does now.
Currently, it *always* returns a set containing root table's attribute
numbers, even for UPDATE. But with this change, for UPDATE, it will start
returning the set containing the first subplan target rel's attribute
numbers, which could be any partition with arbitrarily different attribute
numbers.

Anyway, I think that
the former is more like an improvement rather than a fix, so it would be
better to leave that for another patch for PG12?

I agree, so I'm dropping the patch for 1.

OK, let's focus on #2!

See attached an updated version with changes as described above.

Looks good to me. Thanks for the updated version!

Best regards,
Etsuro Fujita

#35Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#34)
Re: Oddity in tuple routing for foreign partitions

It seems almost fine for me, but just one point..

At Thu, 26 Apr 2018 17:36:44 +0900, Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp> wrote in <5AE18F9C.6080805@lab.ntt.co.jp>

(2018/04/26 15:35), Amit Langote wrote:

On 2018/04/26 12:43, Etsuro Fujita wrote:
+            resultRelation == plan->nominalRelation)
+ resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+    }

Seems like a better change than mine; because this simplifies the
logic.

Please rewrite it to use not array reference, but pointer
reference if one mtstate logically holds just one resultRelInfo.

I added the block inside the if because I agree with your comment
below
that the changes to ExecInitPartitionInfo are better kept for later to
think more carefully about the dependencies it might break.

OK

If we apply the other patch I proposed, resultRelation always points
to
the correct RTE.

I tried to do that. So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Actually, I also thought the former when creating the patch, but I
left
that as-is because I'm not sure that would be really safe;
ExecConstraints
looks at the RT index via GetInsertedColumns/GetUpdatedColumns, so
what I
thought was: that might cause unexpected behavior.

OK, I have to agree here that we better leave 1 to be looked at later.

After this change, GetInsertedColumns/GetUpdatedColumns will start
returning a different set of columns in some cases than it does now.
Currently, it *always* returns a set containing root table's attribute
numbers, even for UPDATE. But with this change, for UPDATE, it will
start
returning the set containing the first subplan target rel's attribute
numbers, which could be any partition with arbitrarily different
attribute
numbers.

Anyway, I think that
the former is more like an improvement rather than a fix, so it would
be
better to leave that for another patch for PG12?

I agree, so I'm dropping the patch for 1.

OK, let's focus on #2!

See attached an updated version with changes as described above.

Looks good to me. Thanks for the updated version!

Agreed on all points above.

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#36Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Kyotaro HORIGUCHI (#35)
Re: Oddity in tuple routing for foreign partitions

(2018/04/26 20:06), Kyotaro HORIGUCHI wrote:

It seems almost fine for me, but just one point..

At Thu, 26 Apr 2018 17:36:44 +0900, Etsuro Fujita<fujita.etsuro@lab.ntt.co.jp> wrote in<5AE18F9C.6080805@lab.ntt.co.jp>

(2018/04/26 15:35), Amit Langote wrote:

On 2018/04/26 12:43, Etsuro Fujita wrote:
+            resultRelation == plan->nominalRelation)
+ resultRelation = mtstate->resultRelInfo[0].ri_RangeTableIndex;
+    }

Seems like a better change than mine; because this simplifies the
logic.

Please rewrite it to use not array reference, but pointer
reference if one mtstate logically holds just one resultRelInfo.

Maybe I don't understand your words correctky, but in that UPDATE case,
I think that mtstate can have multiple ResultRelInfo.

So, attached are two patches.

1. Fix for ExecInitPartitionInfo to use the right RT index to pass to
InitResultRelInfo

2. v5 of the patch to fix the bug of foreign partitions

Thoughts?

Actually, I also thought the former when creating the patch, but I
left
that as-is because I'm not sure that would be really safe;
ExecConstraints
looks at the RT index via GetInsertedColumns/GetUpdatedColumns, so
what I
thought was: that might cause unexpected behavior.

OK, I have to agree here that we better leave 1 to be looked at later.

After this change, GetInsertedColumns/GetUpdatedColumns will start
returning a different set of columns in some cases than it does now.
Currently, it *always* returns a set containing root table's attribute
numbers, even for UPDATE. But with this change, for UPDATE, it will
start
returning the set containing the first subplan target rel's attribute
numbers, which could be any partition with arbitrarily different
attribute
numbers.

Anyway, I think that
the former is more like an improvement rather than a fix, so it would
be
better to leave that for another patch for PG12?

I agree, so I'm dropping the patch for 1.

OK, let's focus on #2!

See attached an updated version with changes as described above.

Looks good to me. Thanks for the updated version!

Agreed on all points above.

Thanks for reviewing!

Best regards,
Etsuro Fujita

#37Kyotaro HORIGUCHI
horiguchi.kyotaro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#36)
Re: Oddity in tuple routing for foreign partitions

At Thu, 26 Apr 2018 21:16:38 +0900, Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp> wrote in <5AE1C326.6040201@lab.ntt.co.jp>

(2018/04/26 20:06), Kyotaro HORIGUCHI wrote:

Please rewrite it to use not array reference, but pointer
reference if one mtstate logically holds just one resultRelInfo.

Maybe I don't understand your words correctky, but in that UPDATE
case, I think that mtstate can have multiple ResultRelInfo.

Ah, mtstate has the same number of resultRelInfo with subplans
and it is *written* in the comment just above:( And it is exactly
for the UPDATE case. Sorry for the silly comment.

Anyway, I think that
the former is more like an improvement rather than a fix, so it would
be
better to leave that for another patch for PG12?

I agree, so I'm dropping the patch for 1.

OK, let's focus on #2!

See attached an updated version with changes as described above.

Looks good to me. Thanks for the updated version!

Agreed on all points above.

Thanks for reviewing!

I'm happy if it helps you.

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#38Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Kyotaro HORIGUCHI (#37)
Re: Oddity in tuple routing for foreign partitions

On 2018/04/27 10:01, Kyotaro HORIGUCHI wrote:

At Thu, 26 Apr 2018 21:16:38 +0900, Etsuro Fujita wrote:

(2018/04/26 20:06), Kyotaro HORIGUCHI wrote:

Agreed on all points above.

Thanks for reviewing!

I'm happy if it helps you.

Thank you both for reviewing.

Regards,
Amit

#39Robert Haas
robertmhaas@gmail.com
In reply to: Amit Langote (#38)
Re: Oddity in tuple routing for foreign partitions

On Thu, Apr 26, 2018 at 9:23 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

Thanks for reviewing!

I'm happy if it helps you.

Thank you both for reviewing.

Committed.

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

#40Andres Freund
andres@anarazel.de
In reply to: Robert Haas (#39)
Re: Oddity in tuple routing for foreign partitions

On 2018-05-01 13:41:32 -0400, Robert Haas wrote:

On Thu, Apr 26, 2018 at 9:23 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

Thanks for reviewing!

I'm happy if it helps you.

Thank you both for reviewing.

Committed.

There's still an open items entry for this thread - has that been
resolved by this commit

Greetings,

Andres Freund

#41Amit Langote
Langote_Amit_f8@lab.ntt.co.jp
In reply to: Andres Freund (#40)
Re: Oddity in tuple routing for foreign partitions

On 2018/05/02 6:09, Andres Freund wrote:

On 2018-05-01 13:41:32 -0400, Robert Haas wrote:

On Thu, Apr 26, 2018 at 9:23 PM, Amit Langote
<Langote_Amit_f8@lab.ntt.co.jp> wrote:

Thanks for reviewing!

I'm happy if it helps you.

Thank you both for reviewing.

Committed.

Thank you.

There's still an open items entry for this thread - has that been
resolved by this commit

Afaik, yes. So moved that to resolved items.

Regards,
Amit

#42Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Amit Langote (#41)
Re: Oddity in tuple routing for foreign partitions

(2018/05/02 10:10), Amit Langote wrote:

On 2018/05/02 6:09, Andres Freund wrote:

On 2018-05-01 13:41:32 -0400, Robert Haas wrote:

Committed.

Thank you.

Thanks for committing, Robert!

There's still an open items entry for this thread - has that been
resolved by this commit

Afaik, yes. So moved that to resolved items.

Yeah, thanks for that, Amit!

Best regards,
Etsuro Fujita

#43Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#42)
1 attachment(s)
Re: Oddity in tuple routing for foreign partitions

(2018/05/02 15:45), Etsuro Fujita wrote:

(2018/05/02 10:10), Amit Langote wrote:

On 2018/05/02 6:09, Andres Freund wrote:

On 2018-05-01 13:41:32 -0400, Robert Haas wrote:

Committed.

Here is a small patch to remove a no-longer-needed cast in
postgresBeginForeignInsert().

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-cleanup.patchtext/x-diff; name=postgres-fdw-cleanup.patchDownload
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 2007,2013 **** postgresBeginForeignInsert(ModifyTableState *mtstate,
  	/* Check if we add the ON CONFLICT clause to the remote query. */
  	if (plan)
  	{
! 		OnConflictAction onConflictAction = ((ModifyTable *) plan)->onConflictAction;
  
  		/* We only support DO NOTHING without an inference specification. */
  		if (onConflictAction == ONCONFLICT_NOTHING)
--- 2007,2013 ----
  	/* Check if we add the ON CONFLICT clause to the remote query. */
  	if (plan)
  	{
! 		OnConflictAction onConflictAction = plan->onConflictAction;
  
  		/* We only support DO NOTHING without an inference specification. */
  		if (onConflictAction == ONCONFLICT_NOTHING)
#44Robert Haas
robertmhaas@gmail.com
In reply to: Etsuro Fujita (#43)
Re: Oddity in tuple routing for foreign partitions

On Wed, May 2, 2018 at 7:06 AM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

Here is a small patch to remove a no-longer-needed cast in
postgresBeginForeignInsert().

Committed.

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

#45Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Robert Haas (#44)
Re: Oddity in tuple routing for foreign partitions

(2018/05/03 9:29), Robert Haas wrote:

On Wed, May 2, 2018 at 7:06 AM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

Here is a small patch to remove a no-longer-needed cast in
postgresBeginForeignInsert().

Committed.

Thanks!

Best regards,
Etsuro Fujita