*** a/contrib/file_fdw/output/file_fdw.source
--- b/contrib/file_fdw/output/file_fdw.source
***************
*** 315,321 **** SELECT tableoid::regclass, * FROM p2;
  (0 rows)
  
  COPY pt FROM '@abs_srcdir@/data/list2.bad' with (format 'csv', delimiter ','); -- ERROR
! ERROR:  cannot route inserted tuples to a foreign table
  CONTEXT:  COPY pt, line 2: "1,qux"
  COPY pt FROM '@abs_srcdir@/data/list2.csv' with (format 'csv', delimiter ',');
  SELECT tableoid::regclass, * FROM pt;
--- 315,321 ----
  (0 rows)
  
  COPY pt FROM '@abs_srcdir@/data/list2.bad' with (format 'csv', delimiter ','); -- ERROR
! ERROR:  cannot insert into foreign table "p1"
  CONTEXT:  COPY pt, line 2: "1,qux"
  COPY pt FROM '@abs_srcdir@/data/list2.csv' with (format 'csv', delimiter ',');
  SELECT tableoid::regclass, * FROM pt;
***************
*** 342,351 **** SELECT tableoid::regclass, * FROM p2;
  (2 rows)
  
  INSERT INTO pt VALUES (1, 'xyzzy'); -- ERROR
! ERROR:  cannot route inserted tuples to a foreign table
  INSERT INTO pt VALUES (2, 'xyzzy');
  UPDATE pt set a = 1 where a = 2; -- ERROR
! ERROR:  cannot route inserted tuples to a foreign table
  SELECT tableoid::regclass, * FROM pt;
   tableoid | a |   b   
  ----------+---+-------
--- 342,351 ----
  (2 rows)
  
  INSERT INTO pt VALUES (1, 'xyzzy'); -- ERROR
! ERROR:  cannot insert into foreign table "p1"
  INSERT INTO pt VALUES (2, 'xyzzy');
  UPDATE pt set a = 1 where a = 2; -- ERROR
! ERROR:  cannot insert into foreign table "p1"
  SELECT tableoid::regclass, * FROM pt;
   tableoid | a |   b   
  ----------+---+-------
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 7371,7376 **** NOTICE:  drop cascades to foreign table bar2
--- 7371,7710 ----
  drop table loct1;
  drop table loct2;
  -- ===================================================================
+ -- test tuple routing for foreign-table partitions
+ -- ===================================================================
+ -- Test insert tuple routing
+ create table itrtest (a int, b text) partition by list (a);
+ create table loct1 (a int check (a in (1)), b text);
+ create foreign table remp1 (a int check (a in (1)), b text) server loopback options (table_name 'loct1');
+ create table loct2 (a int check (a in (2)), b text);
+ create foreign table remp2 (b text, a int check (a in (2))) server loopback options (table_name 'loct2');
+ alter table itrtest attach partition remp1 for values in (1);
+ alter table itrtest attach partition remp2 for values in (2);
+ insert into itrtest values (1, 'foo');
+ insert into itrtest values (1, 'bar') returning *;
+  a |  b  
+ ---+-----
+  1 | bar
+ (1 row)
+ 
+ insert into itrtest values (2, 'baz');
+ insert into itrtest values (2, 'qux') returning *;
+  a |  b  
+ ---+-----
+  2 | qux
+ (1 row)
+ 
+ insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+  a |   b   
+ ---+-------
+  1 | test1
+  2 | test2
+ (2 rows)
+ 
+ select tableoid::regclass, * FROM itrtest;
+  tableoid | a |   b   
+ ----------+---+-------
+  remp1    | 1 | foo
+  remp1    | 1 | bar
+  remp1    | 1 | test1
+  remp2    | 2 | baz
+  remp2    | 2 | qux
+  remp2    | 2 | test2
+ (6 rows)
+ 
+ select tableoid::regclass, * FROM remp1;
+  tableoid | a |   b   
+ ----------+---+-------
+  remp1    | 1 | foo
+  remp1    | 1 | bar
+  remp1    | 1 | test1
+ (3 rows)
+ 
+ select tableoid::regclass, * FROM remp2;
+  tableoid |   b   | a 
+ ----------+-------+---
+  remp2    | baz   | 2
+  remp2    | qux   | 2
+  remp2    | test2 | 2
+ (3 rows)
+ 
+ delete from itrtest;
+ create unique index loct1_idx on loct1 (a);
+ -- DO NOTHING without an inference specification is supported
+ insert into itrtest values (1, 'foo') on conflict do nothing returning *;
+  a |  b  
+ ---+-----
+  1 | foo
+ (1 row)
+ 
+ insert into itrtest values (1, 'foo') on conflict do nothing returning *;
+  a | b 
+ ---+---
+ (0 rows)
+ 
+ -- But other cases are not supported
+ insert into itrtest values (1, 'bar') on conflict (a) do nothing;
+ ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+ insert into itrtest values (1, 'bar') on conflict (a) do update set b = excluded.b;
+ ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification
+ select tableoid::regclass, * FROM itrtest;
+  tableoid | a |  b  
+ ----------+---+-----
+  remp1    | 1 | foo
+ (1 row)
+ 
+ drop table itrtest;
+ drop table loct1;
+ drop table loct2;
+ -- Test update tuple routing
+ create table utrtest (a int, b text) partition by list (a);
+ create table loct (a int check (a in (1)), b text);
+ create foreign table remp (a int check (a in (1)), b text) server loopback options (table_name 'loct');
+ create table locp (a int check (a in (2)), b text);
+ alter table utrtest attach partition remp for values in (1);
+ alter table utrtest attach partition locp for values in (2);
+ insert into utrtest values (1, 'foo');
+ insert into utrtest values (2, 'qux');
+ select tableoid::regclass, * FROM utrtest;
+  tableoid | a |  b  
+ ----------+---+-----
+  remp     | 1 | foo
+  locp     | 2 | qux
+ (2 rows)
+ 
+ select tableoid::regclass, * FROM remp;
+  tableoid | a |  b  
+ ----------+---+-----
+  remp     | 1 | foo
+ (1 row)
+ 
+ select tableoid::regclass, * FROM locp;
+  tableoid | a |  b  
+ ----------+---+-----
+  locp     | 2 | qux
+ (1 row)
+ 
+ -- It's not allowed to move a row from a partition that is foreign to another
+ update utrtest set a = 2 where b = 'foo' returning *;
+ ERROR:  new row for relation "loct" violates check constraint "loct_a_check"
+ DETAIL:  Failing row contains (2, foo).
+ CONTEXT:  remote SQL command: UPDATE public.loct SET a = 2 WHERE ((b = 'foo'::text)) RETURNING a, b
+ -- But the reverse is allowed
+ update utrtest set a = 1 where b = 'qux' returning *;
+  a |  b  
+ ---+-----
+  1 | qux
+ (1 row)
+ 
+ select tableoid::regclass, * FROM utrtest;
+  tableoid | a |  b  
+ ----------+---+-----
+  remp     | 1 | foo
+  remp     | 1 | qux
+ (2 rows)
+ 
+ select tableoid::regclass, * FROM remp;
+  tableoid | a |  b  
+ ----------+---+-----
+  remp     | 1 | foo
+  remp     | 1 | qux
+ (2 rows)
+ 
+ select tableoid::regclass, * FROM locp;
+  tableoid | a | b 
+ ----------+---+---
+ (0 rows)
+ 
+ -- The executor should not let unexercised FDWs shut down
+ update utrtest set a = 1 where b = 'foo';
+ drop table utrtest;
+ drop table loct;
+ -- Test copy tuple routing
+ create table ctrtest (a int, b text) partition by list (a);
+ create table loct1 (a int check (a in (1)), b text);
+ create foreign table remp1 (a int check (a in (1)), b text) server loopback options (table_name 'loct1');
+ create table loct2 (a int check (a in (2)), b text);
+ create foreign table remp2 (b text, a int check (a in (2))) server loopback options (table_name 'loct2');
+ alter table ctrtest attach partition remp1 for values in (1);
+ alter table ctrtest attach partition remp2 for values in (2);
+ copy ctrtest from stdin;
+ select tableoid::regclass, * FROM ctrtest;
+  tableoid | a |  b  
+ ----------+---+-----
+  remp1    | 1 | foo
+  remp2    | 2 | qux
+ (2 rows)
+ 
+ select tableoid::regclass, * FROM remp1;
+  tableoid | a |  b  
+ ----------+---+-----
+  remp1    | 1 | foo
+ (1 row)
+ 
+ select tableoid::regclass, * FROM remp2;
+  tableoid |  b  | a 
+ ----------+-----+---
+  remp2    | qux | 2
+ (1 row)
+ 
+ -- Copying into foreign partitions directly should work as well
+ copy remp1 from stdin;
+ select tableoid::regclass, * FROM remp1;
+  tableoid | a |  b  
+ ----------+---+-----
+  remp1    | 1 | foo
+  remp1    | 1 | bar
+ (2 rows)
+ 
+ drop table ctrtest;
+ drop table loct1;
+ drop table loct2;
+ -- ===================================================================
+ -- test COPY FROM
+ -- ===================================================================
+ create table loc2 (f1 int, f2 text);
+ alter table loc2 set (autovacuum_enabled = 'false');
+ create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
+ -- Test basic functionality
+ copy rem2 from stdin;
+ select * from rem2;
+  f1 | f2  
+ ----+-----
+   1 | foo
+   2 | bar
+ (2 rows)
+ 
+ delete from rem2;
+ -- Test check constraints
+ alter table loc2 add constraint loc2_f1positive check (f1 >= 0);
+ alter foreign table rem2 add constraint rem2_f1positive check (f1 >= 0);
+ -- check constraint is enforced on the remote side, not locally
+ copy rem2 from stdin;
+ copy rem2 from stdin; -- ERROR
+ ERROR:  new row for relation "loc2" violates check constraint "loc2_f1positive"
+ DETAIL:  Failing row contains (-1, xyzzy).
+ CONTEXT:  remote SQL command: INSERT INTO public.loc2(f1, f2) VALUES ($1, $2)
+ COPY rem2, line 1: "-1	xyzzy"
+ select * from rem2;
+  f1 | f2  
+ ----+-----
+   1 | foo
+   2 | bar
+ (2 rows)
+ 
+ alter foreign table rem2 drop constraint rem2_f1positive;
+ alter table loc2 drop constraint loc2_f1positive;
+ delete from rem2;
+ -- Test local triggers
+ create trigger trig_stmt_before before insert on rem2
+ 	for each statement execute procedure trigger_func();
+ create trigger trig_stmt_after after insert on rem2
+ 	for each statement execute procedure trigger_func();
+ create trigger trig_row_before before insert on rem2
+ 	for each row execute procedure trigger_data(23,'skidoo');
+ create trigger trig_row_after after insert on rem2
+ 	for each row execute procedure trigger_data(23,'skidoo');
+ copy rem2 from stdin;
+ NOTICE:  trigger_func(<NULL>) called: action = INSERT, when = BEFORE, level = STATEMENT
+ NOTICE:  trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem2
+ NOTICE:  NEW: (1,foo)
+ NOTICE:  trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem2
+ NOTICE:  NEW: (2,bar)
+ NOTICE:  trig_row_after(23, skidoo) AFTER ROW INSERT ON rem2
+ NOTICE:  NEW: (1,foo)
+ NOTICE:  trig_row_after(23, skidoo) AFTER ROW INSERT ON rem2
+ NOTICE:  NEW: (2,bar)
+ NOTICE:  trigger_func(<NULL>) called: action = INSERT, when = AFTER, level = STATEMENT
+ select * from rem2;
+  f1 | f2  
+ ----+-----
+   1 | foo
+   2 | bar
+ (2 rows)
+ 
+ drop trigger trig_row_before on rem2;
+ drop trigger trig_row_after on rem2;
+ drop trigger trig_stmt_before on rem2;
+ drop trigger trig_stmt_after on rem2;
+ delete from rem2;
+ create trigger trig_row_before_insupdate before insert on rem2
+ 	for each row execute procedure trig_row_before_insupdate();
+ -- The new values are concatenated with ' triggered !'
+ copy rem2 from stdin;
+ select * from rem2;
+  f1 |       f2        
+ ----+-----------------
+   1 | foo triggered !
+   2 | bar triggered !
+ (2 rows)
+ 
+ drop trigger trig_row_before_insupdate on rem2;
+ delete from rem2;
+ create trigger trig_null before insert on rem2
+ 	for each row execute procedure trig_null();
+ -- Nothing happens
+ copy rem2 from stdin;
+ select * from rem2;
+  f1 | f2 
+ ----+----
+ (0 rows)
+ 
+ drop trigger trig_null on rem2;
+ delete from rem2;
+ -- Test remote triggers
+ create trigger trig_row_before_insupdate before insert on loc2
+ 	for each row execute procedure trig_row_before_insupdate();
+ -- The new values are concatenated with ' triggered !'
+ copy rem2 from stdin;
+ select * from rem2;
+  f1 |       f2        
+ ----+-----------------
+   1 | foo triggered !
+   2 | bar triggered !
+ (2 rows)
+ 
+ drop trigger trig_row_before_insupdate on loc2;
+ delete from rem2;
+ create trigger trig_null before insert on loc2
+ 	for each row execute procedure trig_null();
+ -- Nothing happens
+ copy rem2 from stdin;
+ select * from rem2;
+  f1 | f2 
+ ----+----
+ (0 rows)
+ 
+ drop trigger trig_null on loc2;
+ delete from rem2;
+ -- Test a combination of local and remote triggers
+ create trigger rem2_trig_row_before before insert on rem2
+ 	for each row execute procedure trigger_data(23,'skidoo');
+ create trigger rem2_trig_row_after after insert on rem2
+ 	for each row execute procedure trigger_data(23,'skidoo');
+ create trigger loc2_trig_row_before_insupdate before insert on loc2
+ 	for each row execute procedure trig_row_before_insupdate();
+ copy rem2 from stdin;
+ NOTICE:  rem2_trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem2
+ NOTICE:  NEW: (1,foo)
+ NOTICE:  rem2_trig_row_before(23, skidoo) BEFORE ROW INSERT ON rem2
+ NOTICE:  NEW: (2,bar)
+ NOTICE:  rem2_trig_row_after(23, skidoo) AFTER ROW INSERT ON rem2
+ NOTICE:  NEW: (1,"foo triggered !")
+ NOTICE:  rem2_trig_row_after(23, skidoo) AFTER ROW INSERT ON rem2
+ NOTICE:  NEW: (2,"bar triggered !")
+ select * from rem2;
+  f1 |       f2        
+ ----+-----------------
+   1 | foo triggered !
+   2 | bar triggered !
+ (2 rows)
+ 
+ drop trigger rem2_trig_row_before on rem2;
+ drop trigger rem2_trig_row_after on rem2;
+ drop trigger loc2_trig_row_before_insupdate on loc2;
+ delete from rem2;
+ -- ===================================================================
  -- test IMPORT FOREIGN SCHEMA
  -- ===================================================================
  CREATE SCHEMA import_source;
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 319,324 **** static TupleTableSlot *postgresExecForeignDelete(EState *estate,
--- 319,328 ----
  						  TupleTableSlot *planSlot);
  static void postgresEndForeignModify(EState *estate,
  						 ResultRelInfo *resultRelInfo);
+ static void postgresBeginForeignInsert(ModifyTableState *mtstate,
+ 						   ResultRelInfo *resultRelInfo);
+ static void postgresEndForeignInsert(EState *estate,
+ 						 ResultRelInfo *resultRelInfo);
  static int	postgresIsForeignRelUpdatable(Relation rel);
  static bool postgresPlanDirectModify(PlannerInfo *root,
  						 ModifyTable *plan,
***************
*** 473,478 **** postgres_fdw_handler(PG_FUNCTION_ARGS)
--- 477,484 ----
  	routine->ExecForeignUpdate = postgresExecForeignUpdate;
  	routine->ExecForeignDelete = postgresExecForeignDelete;
  	routine->EndForeignModify = postgresEndForeignModify;
+ 	routine->BeginForeignInsert = postgresBeginForeignInsert;
+ 	routine->EndForeignInsert = postgresEndForeignInsert;
  	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
  	routine->PlanDirectModify = postgresPlanDirectModify;
  	routine->BeginDirectModify = postgresBeginDirectModify;
***************
*** 1960,1965 **** postgresEndForeignModify(EState *estate,
--- 1966,2061 ----
  }
  
  /*
+  * postgresBeginForeignInsert
+  *		Begin an insert operation on a foreign table
+  */
+ static void
+ 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);
+ 	int			attnum;
+ 	StringInfoData sql;
+ 	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;
+ 
+ 	/* We transmit all columns that are defined in the foreign table. */
+ 	for (attnum = 1; attnum <= tupdesc->natts; attnum++)
+ 	{
+ 		Form_pg_attribute attr = TupleDescAttr(tupdesc, attnum - 1);
+ 
+ 		if (!attr->attisdropped)
+ 			targetAttrs = lappend_int(targetAttrs, attnum);
+ 	}
+ 
+ 	/* 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)
+ 			doNothing = true;
+ 		else if (onConflictAction != ONCONFLICT_NONE)
+ 			elog(ERROR, "unexpected ON CONFLICT specification: %d",
+ 				 (int) onConflictAction);
+ 	}
+ 
+ 	/* 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,
+ 									plan,
+ 									sql.data,
+ 									targetAttrs,
+ 									retrieved_attrs != NIL,
+ 									retrieved_attrs);
+ 
+ 	resultRelInfo->ri_FdwState = fmstate;
+ }
+ 
+ /*
+  * postgresEndForeignInsert
+  *		Finish an insert operation on a foreign table
+  */
+ static void
+ postgresEndForeignInsert(EState *estate,
+ 						 ResultRelInfo *resultRelInfo)
+ {
+ 	PgFdwModifyState *fmstate = (PgFdwModifyState *) resultRelInfo->ri_FdwState;
+ 
+ 	Assert(fmstate != NULL);
+ 
+ 	/* Destroy the execution state */
+ 	finish_foreign_modify(fmstate);
+ }
+ 
+ /*
   * postgresIsForeignRelUpdatable
   *		Determine whether a foreign table supports INSERT, UPDATE and/or
   *		DELETE.
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 1768,1773 **** drop table loct1;
--- 1768,2010 ----
  drop table loct2;
  
  -- ===================================================================
+ -- test tuple routing for foreign-table partitions
+ -- ===================================================================
+ 
+ -- Test insert tuple routing
+ create table itrtest (a int, b text) partition by list (a);
+ create table loct1 (a int check (a in (1)), b text);
+ create foreign table remp1 (a int check (a in (1)), b text) server loopback options (table_name 'loct1');
+ create table loct2 (a int check (a in (2)), b text);
+ create foreign table remp2 (b text, a int check (a in (2))) server loopback options (table_name 'loct2');
+ alter table itrtest attach partition remp1 for values in (1);
+ alter table itrtest attach partition remp2 for values in (2);
+ 
+ insert into itrtest values (1, 'foo');
+ insert into itrtest values (1, 'bar') returning *;
+ insert into itrtest values (2, 'baz');
+ insert into itrtest values (2, 'qux') returning *;
+ insert into itrtest values (1, 'test1'), (2, 'test2') returning *;
+ 
+ select tableoid::regclass, * FROM itrtest;
+ select tableoid::regclass, * FROM remp1;
+ select tableoid::regclass, * FROM remp2;
+ 
+ delete from itrtest;
+ 
+ create unique index loct1_idx on loct1 (a);
+ 
+ -- DO NOTHING without an inference specification is supported
+ insert into itrtest values (1, 'foo') on conflict do nothing returning *;
+ insert into itrtest values (1, 'foo') on conflict do nothing returning *;
+ 
+ -- But other cases are not supported
+ insert into itrtest values (1, 'bar') on conflict (a) do nothing;
+ insert into itrtest values (1, 'bar') on conflict (a) do update set b = excluded.b;
+ 
+ select tableoid::regclass, * FROM itrtest;
+ 
+ drop table itrtest;
+ drop table loct1;
+ drop table loct2;
+ 
+ -- Test update tuple routing
+ create table utrtest (a int, b text) partition by list (a);
+ create table loct (a int check (a in (1)), b text);
+ create foreign table remp (a int check (a in (1)), b text) server loopback options (table_name 'loct');
+ create table locp (a int check (a in (2)), b text);
+ alter table utrtest attach partition remp for values in (1);
+ alter table utrtest attach partition locp for values in (2);
+ 
+ insert into utrtest values (1, 'foo');
+ insert into utrtest values (2, 'qux');
+ 
+ select tableoid::regclass, * FROM utrtest;
+ select tableoid::regclass, * FROM remp;
+ select tableoid::regclass, * FROM locp;
+ 
+ -- It's not allowed to move a row from a partition that is foreign to another
+ update utrtest set a = 2 where b = 'foo' returning *;
+ 
+ -- But the reverse is allowed
+ update utrtest set a = 1 where b = 'qux' returning *;
+ 
+ select tableoid::regclass, * FROM utrtest;
+ select tableoid::regclass, * FROM remp;
+ select tableoid::regclass, * FROM locp;
+ 
+ -- The executor should not let unexercised FDWs shut down
+ update utrtest set a = 1 where b = 'foo';
+ 
+ drop table utrtest;
+ drop table loct;
+ 
+ -- Test copy tuple routing
+ create table ctrtest (a int, b text) partition by list (a);
+ create table loct1 (a int check (a in (1)), b text);
+ create foreign table remp1 (a int check (a in (1)), b text) server loopback options (table_name 'loct1');
+ create table loct2 (a int check (a in (2)), b text);
+ create foreign table remp2 (b text, a int check (a in (2))) server loopback options (table_name 'loct2');
+ alter table ctrtest attach partition remp1 for values in (1);
+ alter table ctrtest attach partition remp2 for values in (2);
+ 
+ copy ctrtest from stdin;
+ 1	foo
+ 2	qux
+ \.
+ 
+ select tableoid::regclass, * FROM ctrtest;
+ select tableoid::regclass, * FROM remp1;
+ select tableoid::regclass, * FROM remp2;
+ 
+ -- Copying into foreign partitions directly should work as well
+ copy remp1 from stdin;
+ 1	bar
+ \.
+ 
+ select tableoid::regclass, * FROM remp1;
+ 
+ drop table ctrtest;
+ drop table loct1;
+ drop table loct2;
+ 
+ -- ===================================================================
+ -- test COPY FROM
+ -- ===================================================================
+ 
+ create table loc2 (f1 int, f2 text);
+ alter table loc2 set (autovacuum_enabled = 'false');
+ create foreign table rem2 (f1 int, f2 text) server loopback options(table_name 'loc2');
+ 
+ -- Test basic functionality
+ copy rem2 from stdin;
+ 1	foo
+ 2	bar
+ \.
+ select * from rem2;
+ 
+ delete from rem2;
+ 
+ -- Test check constraints
+ alter table loc2 add constraint loc2_f1positive check (f1 >= 0);
+ alter foreign table rem2 add constraint rem2_f1positive check (f1 >= 0);
+ 
+ -- check constraint is enforced on the remote side, not locally
+ copy rem2 from stdin;
+ 1	foo
+ 2	bar
+ \.
+ copy rem2 from stdin; -- ERROR
+ -1	xyzzy
+ \.
+ select * from rem2;
+ 
+ alter foreign table rem2 drop constraint rem2_f1positive;
+ alter table loc2 drop constraint loc2_f1positive;
+ 
+ delete from rem2;
+ 
+ -- Test local triggers
+ create trigger trig_stmt_before before insert on rem2
+ 	for each statement execute procedure trigger_func();
+ create trigger trig_stmt_after after insert on rem2
+ 	for each statement execute procedure trigger_func();
+ create trigger trig_row_before before insert on rem2
+ 	for each row execute procedure trigger_data(23,'skidoo');
+ create trigger trig_row_after after insert on rem2
+ 	for each row execute procedure trigger_data(23,'skidoo');
+ 
+ copy rem2 from stdin;
+ 1	foo
+ 2	bar
+ \.
+ select * from rem2;
+ 
+ drop trigger trig_row_before on rem2;
+ drop trigger trig_row_after on rem2;
+ drop trigger trig_stmt_before on rem2;
+ drop trigger trig_stmt_after on rem2;
+ 
+ delete from rem2;
+ 
+ create trigger trig_row_before_insupdate before insert on rem2
+ 	for each row execute procedure trig_row_before_insupdate();
+ 
+ -- The new values are concatenated with ' triggered !'
+ copy rem2 from stdin;
+ 1	foo
+ 2	bar
+ \.
+ select * from rem2;
+ 
+ drop trigger trig_row_before_insupdate on rem2;
+ 
+ delete from rem2;
+ 
+ create trigger trig_null before insert on rem2
+ 	for each row execute procedure trig_null();
+ 
+ -- Nothing happens
+ copy rem2 from stdin;
+ 1	foo
+ 2	bar
+ \.
+ select * from rem2;
+ 
+ drop trigger trig_null on rem2;
+ 
+ delete from rem2;
+ 
+ -- Test remote triggers
+ create trigger trig_row_before_insupdate before insert on loc2
+ 	for each row execute procedure trig_row_before_insupdate();
+ 
+ -- The new values are concatenated with ' triggered !'
+ copy rem2 from stdin;
+ 1	foo
+ 2	bar
+ \.
+ select * from rem2;
+ 
+ drop trigger trig_row_before_insupdate on loc2;
+ 
+ delete from rem2;
+ 
+ create trigger trig_null before insert on loc2
+ 	for each row execute procedure trig_null();
+ 
+ -- Nothing happens
+ copy rem2 from stdin;
+ 1	foo
+ 2	bar
+ \.
+ select * from rem2;
+ 
+ drop trigger trig_null on loc2;
+ 
+ delete from rem2;
+ 
+ -- Test a combination of local and remote triggers
+ create trigger rem2_trig_row_before before insert on rem2
+ 	for each row execute procedure trigger_data(23,'skidoo');
+ create trigger rem2_trig_row_after after insert on rem2
+ 	for each row execute procedure trigger_data(23,'skidoo');
+ create trigger loc2_trig_row_before_insupdate before insert on loc2
+ 	for each row execute procedure trig_row_before_insupdate();
+ 
+ copy rem2 from stdin;
+ 1	foo
+ 2	bar
+ \.
+ select * from rem2;
+ 
+ drop trigger rem2_trig_row_before on rem2;
+ drop trigger rem2_trig_row_after on rem2;
+ drop trigger loc2_trig_row_before_insupdate on loc2;
+ 
+ delete from rem2;
+ 
+ -- ===================================================================
  -- test IMPORT FOREIGN SCHEMA
  -- ===================================================================
  
*** a/doc/src/sgml/ddl.sgml
--- b/doc/src/sgml/ddl.sgml
***************
*** 3037,3047 **** VALUES ('Albany', NULL, NULL, 'NY');
     </para>
  
     <para>
!     Partitions can also be foreign tables
!     (see <xref linkend="sql-createforeigntable"/>),
!     although these have some limitations that normal tables do not.  For
!     example, data inserted into the partitioned table is not routed to
!     foreign table partitions.
     </para>
  
     <para>
--- 3037,3045 ----
     </para>
  
     <para>
!     Partitions can also be foreign tables, although they have some limitations
!     that normal tables do not; see <xref linkend="sql-createforeigntable"> for
!     more information.
     </para>
  
     <para>
*** a/doc/src/sgml/fdwhandler.sgml
--- b/doc/src/sgml/fdwhandler.sgml
***************
*** 695,700 **** EndForeignModify(EState *estate,
--- 695,765 ----
      </para>
  
      <para>
+      Tuples inserted into a partitioned table are routed to partitions.  If an
+      FDW supports routable foreign-table partitions, it should also provide
+      the following callback functions.  These functions are also called when
+      <command>COPY FROM</command> is executed on a foreign table.
+     </para>
+ 
+     <para>
+ <programlisting>
+ void
+ BeginForeignInsert(ModifyTableState *mtstate,
+                    ResultRelInfo *rinfo);
+ </programlisting>
+ 
+      Begin executing an insert operation on a foreign table.  This routine is
+      called right before the first tuple is inserted into the foreign table
+      in both cases where it is the partition chosen for tuple routing and the
+      target specified in a <command>COPY FROM</command> command.  It should
+      perform any initialization needed prior to the actual insertion.
+      Subsequently, <function>ExecForeignInsert</function> will be called for
+      each tuple to be inserted into the foreign table.
+     </para>
+ 
+     <para>
+      <literal>mtstate</literal> is the overall state of the
+      <structname>ModifyTable</structname> plan node being executed; global data about
+      the plan and execution state is available via this structure.
+      <literal>rinfo</literal> is the <structname>ResultRelInfo</structname> struct describing
+      the target foreign table.  (The <structfield>ri_FdwState</structfield> field of
+      <structname>ResultRelInfo</structname> is available for the FDW to store any
+      private state it needs for this operation.)
+     </para>
+ 
+     <para>
+      When this is called by a <command>COPY FROM</command> command, the
+      plan-related global data in <literal>mtstate</literal> is not provided
+      and the <literal>planSlot</literal> parameter of
+      <function>ExecForeignInsert</function> subsequently called for each
+      inserted tuple is <literal>NULL</literal>, whether the foreign table is
+      the partition chosen for tuple routing or the target specified in the
+      command.
+     </para>
+ 
+     <para>
+      If the <function>BeginForeignInsert</function> pointer is set to
+      <literal>NULL</literal>, no action is taken for the initialization.
+     </para>
+ 
+     <para>
+ <programlisting>
+ void
+ EndForeignInsert(EState *estate,
+                  ResultRelInfo *rinfo);
+ </programlisting>
+ 
+      End the insert operation and release resources.  It is normally not important
+      to release palloc'd memory, but for example open files and connections
+      to remote servers should be cleaned up.
+     </para>
+ 
+     <para>
+      If the <function>EndForeignInsert</function> pointer is set to
+      <literal>NULL</literal>, no action is taken for the termination.
+     </para>
+ 
+     <para>
  <programlisting>
  int
  IsForeignRelUpdatable(Relation rel);
*** a/doc/src/sgml/ref/copy.sgml
--- b/doc/src/sgml/ref/copy.sgml
***************
*** 402,409 **** COPY <replaceable class="parameter">count</replaceable>
     </para>
  
     <para>
!     <command>COPY FROM</command> can be used with plain tables and with views
!     that have <literal>INSTEAD OF INSERT</literal> triggers.
     </para>
  
     <para>
--- 402,410 ----
     </para>
  
     <para>
!     <command>COPY FROM</command> can be used with plain, foreign, or
!     partitioned tables and with views that have
!     <literal>INSTEAD OF INSERT</literal> triggers.
     </para>
  
     <para>
*** a/doc/src/sgml/ref/update.sgml
--- b/doc/src/sgml/ref/update.sgml
***************
*** 291,296 **** UPDATE <replaceable class="parameter">count</replaceable>
--- 291,299 ----
     concurrent <command>UPDATE</command> or <command>DELETE</command> on the
     same row may miss this row. For details see the section
     <xref linkend="ddl-partitioning-declarative-limitations"/>.
+    Currently, it is not allowed to move a row from a partition that is a
+    foreign table to another, but the reverse is allowed if the foreign table
+    is routable.
    </para>
   </refsect1>
  
*** a/src/backend/commands/copy.c
--- b/src/backend/commands/copy.c
***************
*** 29,34 ****
--- 29,35 ----
  #include "commands/trigger.h"
  #include "executor/execPartition.h"
  #include "executor/executor.h"
+ #include "foreign/fdwapi.h"
  #include "libpq/libpq.h"
  #include "libpq/pqformat.h"
  #include "mb/pg_wchar.h"
***************
*** 2284,2289 **** CopyFrom(CopyState cstate)
--- 2285,2291 ----
  	ResultRelInfo *resultRelInfo;
  	ResultRelInfo *saved_resultRelInfo = NULL;
  	EState	   *estate = CreateExecutorState(); /* for ExecConstraints() */
+ 	ModifyTableState *mtstate;
  	ExprContext *econtext;
  	TupleTableSlot *myslot;
  	MemoryContext oldcontext = CurrentMemoryContext;
***************
*** 2305,2315 **** CopyFrom(CopyState cstate)
  	Assert(cstate->rel);
  
  	/*
! 	 * The target must be a plain relation or have an INSTEAD OF INSERT row
! 	 * trigger.  (Currently, such triggers are only allowed on views, so we
! 	 * only hint about them in the view case.)
  	 */
  	if (cstate->rel->rd_rel->relkind != RELKIND_RELATION &&
  		cstate->rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE &&
  		!(cstate->rel->trigdesc &&
  		  cstate->rel->trigdesc->trig_insert_instead_row))
--- 2307,2318 ----
  	Assert(cstate->rel);
  
  	/*
! 	 * The target must be a plain, foreign, or partitioned relation, or have
! 	 * an INSTEAD OF INSERT row trigger.  (Currently, such triggers are only
! 	 * allowed on views, so we only hint about them in the view case.)
  	 */
  	if (cstate->rel->rd_rel->relkind != RELKIND_RELATION &&
+ 		cstate->rel->rd_rel->relkind != RELKIND_FOREIGN_TABLE &&
  		cstate->rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE &&
  		!(cstate->rel->trigdesc &&
  		  cstate->rel->trigdesc->trig_insert_instead_row))
***************
*** 2325,2335 **** CopyFrom(CopyState cstate)
  					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
  					 errmsg("cannot copy to materialized view \"%s\"",
  							RelationGetRelationName(cstate->rel))));
- 		else if (cstate->rel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
- 			ereport(ERROR,
- 					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
- 					 errmsg("cannot copy to foreign table \"%s\"",
- 							RelationGetRelationName(cstate->rel))));
  		else if (cstate->rel->rd_rel->relkind == RELKIND_SEQUENCE)
  			ereport(ERROR,
  					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
--- 2328,2333 ----
***************
*** 2448,2453 **** CopyFrom(CopyState cstate)
--- 2446,2466 ----
  	/* Triggers might need a slot as well */
  	estate->es_trig_tuple_slot = ExecInitExtraTupleSlot(estate, NULL);
  
+ 	/*
+ 	 * Set up a ModifyTableState so we can let FDW(s) init themselves for
+ 	 * foreign-table result relation(s).
+ 	 */
+ 	mtstate = makeNode(ModifyTableState);
+ 	mtstate->ps.plan = NULL;
+ 	mtstate->ps.state = estate;
+ 	mtstate->operation = CMD_INSERT;
+ 	mtstate->resultRelInfo = estate->es_result_relations;
+ 
+ 	if (resultRelInfo->ri_FdwRoutine != NULL &&
+ 		resultRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+ 		resultRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate,
+ 														 resultRelInfo);
+ 
  	/* Prepare to catch AFTER triggers. */
  	AfterTriggerBeginQuery();
  
***************
*** 2489,2499 **** CopyFrom(CopyState cstate)
  	 * expressions. Such triggers or expressions might query the table we're
  	 * inserting to, and act differently if the tuples that have already been
  	 * processed and prepared for insertion are not there.  We also can't do
! 	 * it if the table is partitioned.
  	 */
  	if ((resultRelInfo->ri_TrigDesc != NULL &&
  		 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
  		  resultRelInfo->ri_TrigDesc->trig_insert_instead_row)) ||
  		cstate->partition_tuple_routing != NULL ||
  		cstate->volatile_defexprs)
  	{
--- 2502,2513 ----
  	 * expressions. Such triggers or expressions might query the table we're
  	 * inserting to, and act differently if the tuples that have already been
  	 * processed and prepared for insertion are not there.  We also can't do
! 	 * it if the table is foreign or partitioned.
  	 */
  	if ((resultRelInfo->ri_TrigDesc != NULL &&
  		 (resultRelInfo->ri_TrigDesc->trig_insert_before_row ||
  		  resultRelInfo->ri_TrigDesc->trig_insert_instead_row)) ||
+ 		resultRelInfo->ri_FdwRoutine != NULL ||
  		cstate->partition_tuple_routing != NULL ||
  		cstate->volatile_defexprs)
  	{
***************
*** 2608,2626 **** CopyFrom(CopyState cstate)
  			resultRelInfo = proute->partitions[leaf_part_index];
  			if (resultRelInfo == NULL)
  			{
! 				resultRelInfo = ExecInitPartitionInfo(NULL,
  													  saved_resultRelInfo,
  													  proute, estate,
  													  leaf_part_index);
  				Assert(resultRelInfo != NULL);
  			}
  
- 			/* We do not yet have a way to insert into a foreign partition */
- 			if (resultRelInfo->ri_FdwRoutine)
- 				ereport(ERROR,
- 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- 						 errmsg("cannot route inserted tuples to a foreign table")));
- 
  			/*
  			 * For ExecInsertIndexTuples() to work on the partition's indexes
  			 */
--- 2622,2634 ----
  			resultRelInfo = proute->partitions[leaf_part_index];
  			if (resultRelInfo == NULL)
  			{
! 				resultRelInfo = ExecInitPartitionInfo(mtstate,
  													  saved_resultRelInfo,
  													  proute, estate,
  													  leaf_part_index);
  				Assert(resultRelInfo != NULL);
  			}
  
  			/*
  			 * For ExecInsertIndexTuples() to work on the partition's indexes
  			 */
***************
*** 2708,2716 **** CopyFrom(CopyState cstate)
  					  resultRelInfo->ri_TrigDesc->trig_insert_before_row))
  					check_partition_constr = false;
  
! 				/* Check the constraints of the tuple */
! 				if (resultRelInfo->ri_RelationDesc->rd_att->constr ||
! 					check_partition_constr)
  					ExecConstraints(resultRelInfo, slot, estate, true);
  
  				if (useHeapMultiInsert)
--- 2716,2728 ----
  					  resultRelInfo->ri_TrigDesc->trig_insert_before_row))
  					check_partition_constr = false;
  
! 				/*
! 				 * If the target is a plain table, check the constraints of
! 				 * the tuple.
! 				 */
! 				if (resultRelInfo->ri_FdwRoutine == NULL &&
! 					(resultRelInfo->ri_RelationDesc->rd_att->constr ||
! 					 check_partition_constr))
  					ExecConstraints(resultRelInfo, slot, estate, true);
  
  				if (useHeapMultiInsert)
***************
*** 2742,2751 **** CopyFrom(CopyState cstate)
  				{
  					List	   *recheckIndexes = NIL;
  
! 					/* OK, store the tuple and create index entries for it */
! 					heap_insert(resultRelInfo->ri_RelationDesc, tuple, mycid,
! 								hi_options, bistate);
  
  					if (resultRelInfo->ri_NumIndices > 0)
  						recheckIndexes = ExecInsertIndexTuples(slot,
  															   &(tuple->t_self),
--- 2754,2785 ----
  				{
  					List	   *recheckIndexes = NIL;
  
! 					/* OK, store the tuple */
! 					if (resultRelInfo->ri_FdwRoutine != NULL)
! 					{
! 						slot = resultRelInfo->ri_FdwRoutine->ExecForeignInsert(estate,
! 																			   resultRelInfo,
! 																			   slot,
! 																			   NULL);
! 
! 						if (slot == NULL)		/* "do nothing" */
! 							goto next_tuple;
! 
! 						/* FDW might have changed tuple */
! 						tuple = ExecMaterializeSlot(slot);
! 
! 						/*
! 						 * AFTER ROW Triggers might reference the tableoid
! 						 * column, so initialize t_tableOid before evaluating
! 						 * them.
! 						 */
! 						tuple->t_tableOid = RelationGetRelid(resultRelInfo->ri_RelationDesc);
! 					}
! 					else
! 						heap_insert(resultRelInfo->ri_RelationDesc, tuple,
! 									mycid, hi_options, bistate);
  
+ 					/* And create index entries for it */
  					if (resultRelInfo->ri_NumIndices > 0)
  						recheckIndexes = ExecInsertIndexTuples(slot,
  															   &(tuple->t_self),
***************
*** 2763,2775 **** CopyFrom(CopyState cstate)
  			}
  
  			/*
! 			 * We count only tuples not suppressed by a BEFORE INSERT trigger;
! 			 * this is the same definition used by execMain.c for counting
! 			 * tuples inserted by an INSERT command.
  			 */
  			processed++;
  		}
  
  		/* Restore the saved ResultRelInfo */
  		if (saved_resultRelInfo)
  		{
--- 2797,2810 ----
  			}
  
  			/*
! 			 * We count only tuples not suppressed by a BEFORE INSERT trigger
! 			 * or FDW; this is the same definition used by nodeModifyTable.c
! 			 * for counting tuples inserted by an INSERT command.
  			 */
  			processed++;
  		}
  
+ next_tuple:
  		/* Restore the saved ResultRelInfo */
  		if (saved_resultRelInfo)
  		{
***************
*** 2810,2820 **** CopyFrom(CopyState cstate)
  
  	ExecResetTupleTable(estate->es_tupleTable, false);
  
  	ExecCloseIndices(resultRelInfo);
  
  	/* Close all the partitioned tables, leaf partitions, and their indices */
  	if (cstate->partition_tuple_routing)
! 		ExecCleanupTupleRouting(cstate->partition_tuple_routing);
  
  	/* Close any trigger target relations */
  	ExecCleanUpTriggerState(estate);
--- 2845,2861 ----
  
  	ExecResetTupleTable(estate->es_tupleTable, false);
  
+ 	/* Allow the FDW to shut down */
+ 	if (resultRelInfo->ri_FdwRoutine != NULL &&
+ 		resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+ 		resultRelInfo->ri_FdwRoutine->EndForeignInsert(estate,
+ 													   resultRelInfo);
+ 
  	ExecCloseIndices(resultRelInfo);
  
  	/* Close all the partitioned tables, leaf partitions, and their indices */
  	if (cstate->partition_tuple_routing)
! 		ExecCleanupTupleRouting(mtstate, cstate->partition_tuple_routing);
  
  	/* Close any trigger target relations */
  	ExecCleanUpTriggerState(estate);
*** a/src/backend/executor/execMain.c
--- b/src/backend/executor/execMain.c
***************
*** 1179,1191 **** CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation)
  			switch (operation)
  			{
  				case CMD_INSERT:
- 
- 					/*
- 					 * If foreign partition to do tuple-routing for, skip the
- 					 * check; it's disallowed elsewhere.
- 					 */
- 					if (resultRelInfo->ri_PartitionRoot)
- 						break;
  					if (fdwroutine->ExecForeignInsert == NULL)
  						ereport(ERROR,
  								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
--- 1179,1184 ----
***************
*** 1378,1383 **** InitResultRelInfo(ResultRelInfo *resultRelInfo,
--- 1371,1377 ----
  
  	resultRelInfo->ri_PartitionCheck = partition_check;
  	resultRelInfo->ri_PartitionRoot = partition_root;
+ 	resultRelInfo->ri_PartitionReadyForRouting = false;
  }
  
  /*
*** a/src/backend/executor/execPartition.c
--- b/src/backend/executor/execPartition.c
***************
*** 18,23 ****
--- 18,24 ----
  #include "catalog/pg_type.h"
  #include "executor/execPartition.h"
  #include "executor/executor.h"
+ #include "foreign/fdwapi.h"
  #include "mb/pg_wchar.h"
  #include "miscadmin.h"
  #include "nodes/makefuncs.h"
***************
*** 55,66 **** static List *adjust_partition_tlist(List *tlist, TupleConversionMap *map);
   * see ExecInitPartitionInfo.  However, if the function is invoked for update
   * tuple routing, caller would already have initialized ResultRelInfo's for
   * some of the partitions, which are reused and assigned to their respective
!  * slot in the aforementioned array.
   */
  PartitionTupleRouting *
  ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel)
  {
- 	TupleDesc	tupDesc = RelationGetDescr(rel);
  	List	   *leaf_parts;
  	ListCell   *cell;
  	int			i;
--- 56,68 ----
   * see ExecInitPartitionInfo.  However, if the function is invoked for update
   * tuple routing, caller would already have initialized ResultRelInfo's for
   * some of the partitions, which are reused and assigned to their respective
!  * slot in the aforementioned array.  For such partitions, we delay setting
!  * up objects such as TupleConversionMap until those are actually chosen as
!  * the partitions to route tuples to.  See ExecPrepareTupleRouting.
   */
  PartitionTupleRouting *
  ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel)
  {
  	List	   *leaf_parts;
  	ListCell   *cell;
  	int			i;
***************
*** 141,151 **** ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel)
  		if (update_rri_index < num_update_rri &&
  			RelationGetRelid(update_rri[update_rri_index].ri_RelationDesc) == leaf_oid)
  		{
- 			Relation	partrel;
- 			TupleDesc	part_tupdesc;
- 
  			leaf_part_rri = &update_rri[update_rri_index];
- 			partrel = leaf_part_rri->ri_RelationDesc;
  
  			/*
  			 * This is required in order to convert the partition's tuple to
--- 143,149 ----
***************
*** 159,181 **** ExecSetupPartitionTupleRouting(ModifyTableState *mtstate, Relation rel)
  			proute->subplan_partition_offsets[update_rri_index] = i;
  
  			update_rri_index++;
- 
- 			part_tupdesc = RelationGetDescr(partrel);
- 
- 			/*
- 			 * Save a tuple conversion map to convert a tuple routed to this
- 			 * partition from the parent's type to the partition's.
- 			 */
- 			proute->parent_child_tupconv_maps[i] =
- 				convert_tuples_by_name(tupDesc, part_tupdesc,
- 									   gettext_noop("could not convert row type"));
- 
- 			/*
- 			 * 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 required even when the operation is CMD_UPDATE.
- 			 */
- 			CheckValidResultRel(leaf_part_rri, CMD_INSERT);
  		}
  
  		proute->partitions[i] = leaf_part_rri;
--- 157,162 ----
***************
*** 342,351 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
  					  PartitionTupleRouting *proute,
  					  EState *estate, int partidx)
  {
  	Relation	rootrel = resultRelInfo->ri_RelationDesc,
  				partrel;
  	ResultRelInfo *leaf_part_rri;
- 	ModifyTable *node = mtstate ? (ModifyTable *) mtstate->ps.plan : NULL;
  	MemoryContext oldContext;
  
  	/*
--- 323,332 ----
  					  PartitionTupleRouting *proute,
  					  EState *estate, int partidx)
  {
+ 	ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
  	Relation	rootrel = resultRelInfo->ri_RelationDesc,
  				partrel;
  	ResultRelInfo *leaf_part_rri;
  	MemoryContext oldContext;
  
  	/*
***************
*** 369,379 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
  
  	leaf_part_rri->ri_PartitionLeafIndex = partidx;
  
! 	/*
! 	 * 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);
  
  	/*
--- 350,356 ----
  
  	leaf_part_rri->ri_PartitionLeafIndex = partidx;
  
! 	/* Verify the specified partition is a valid target for INSERT */
  	CheckValidResultRel(leaf_part_rri, CMD_INSERT);
  
  	/*
***************
*** 388,393 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 365,373 ----
  		lappend(estate->es_tuple_routing_result_relations,
  				leaf_part_rri);
  
+ 	/* Set up information for routing tuples to the specified 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
***************
*** 493,498 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 473,479 ----
  		returningList = map_partition_varattnos(returningList, firstVarno,
  												partrel, firstResultRel,
  												NULL);
+ 		leaf_part_rri->ri_returningList = returningList;
  
  		/*
  		 * Initialize the projection itself.
***************
*** 510,524 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
  	}
  
  	/*
- 	 * Save a tuple conversion map to convert a tuple routed to this partition
- 	 * from the parent's type to the partition's.
- 	 */
- 	proute->parent_child_tupconv_maps[partidx] =
- 		convert_tuples_by_name(RelationGetDescr(rootrel),
- 							   RelationGetDescr(partrel),
- 							   gettext_noop("could not convert row type"));
- 
- 	/*
  	 * If there is an ON CONFLICT clause, initialize state for it.
  	 */
  	if (node && node->onConflictAction != ONCONFLICT_NONE)
--- 491,496 ----
***************
*** 654,659 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 626,633 ----
  		}
  	}
  
+ 	leaf_part_rri->ri_PartitionReadyForRouting = true;
+ 
  	Assert(proute->partitions[partidx] == NULL);
  	proute->partitions[partidx] = leaf_part_rri;
  
***************
*** 747,752 **** ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 721,764 ----
  }
  
  /*
+  * ExecInitRoutingInfo
+  *		Prepare a tuple conversion map for the given partition, and if it is 
+  *		a foreign table, let the FDW init itself for the result relation.
+  */
+ void
+ ExecInitRoutingInfo(ModifyTableState *mtstate,
+ 					EState *estate,
+ 					PartitionTupleRouting *proute,
+ 					ResultRelInfo *partRelInfo,
+ 					int partidx)
+ {
+ 	MemoryContext oldContext;
+ 
+ 	/*
+ 	 * Switch into per-query memory context.
+ 	 */
+ 	oldContext = MemoryContextSwitchTo(estate->es_query_cxt);
+ 
+ 	/*
+ 	 * Set up a tuple conversion map to convert a tuple routed to the
+ 	 * partition from the parent's type to the partition's.
+ 	 */
+ 	proute->parent_child_tupconv_maps[partidx] =
+ 		convert_tuples_by_name(RelationGetDescr(partRelInfo->ri_PartitionRoot),
+ 							   RelationGetDescr(partRelInfo->ri_RelationDesc),
+ 							   gettext_noop("could not convert row type"));
+ 
+ 	/*
+ 	 * Let the FDW init itself for routing tuples to the partition.
+ 	 */
+ 	if (partRelInfo->ri_FdwRoutine != NULL &&
+ 		partRelInfo->ri_FdwRoutine->BeginForeignInsert != NULL)
+ 		partRelInfo->ri_FdwRoutine->BeginForeignInsert(mtstate, partRelInfo);
+ 
+ 	MemoryContextSwitchTo(oldContext);
+ }
+ 
+ /*
   * ExecSetupChildParentMapForLeaf -- Initialize the per-leaf-partition
   * child-to-root tuple conversion map array.
   *
***************
*** 848,854 **** ConvertPartitionTupleSlot(TupleConversionMap *map,
   * Close all the partitioned tables, leaf partitions, and their indices.
   */
  void
! ExecCleanupTupleRouting(PartitionTupleRouting *proute)
  {
  	int			i;
  	int			subplan_index = 0;
--- 860,867 ----
   * Close all the partitioned tables, leaf partitions, and their indices.
   */
  void
! ExecCleanupTupleRouting(ModifyTableState *mtstate,
! 						PartitionTupleRouting *proute)
  {
  	int			i;
  	int			subplan_index = 0;
***************
*** 876,881 **** ExecCleanupTupleRouting(PartitionTupleRouting *proute)
--- 889,901 ----
  		if (resultRelInfo == NULL)
  			continue;
  
+ 		/* Allow any FDWs to shut down if they've been exercised */
+ 		if (resultRelInfo->ri_PartitionReadyForRouting &&
+ 			resultRelInfo->ri_FdwRoutine != NULL &&
+ 			resultRelInfo->ri_FdwRoutine->EndForeignInsert != NULL)
+ 			resultRelInfo->ri_FdwRoutine->EndForeignInsert(mtstate->ps.state,
+ 														   resultRelInfo);
+ 
  		/*
  		 * If this result rel is one of the UPDATE subplan result rels, let
  		 * ExecEndPlan() close it. For INSERT or COPY,
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
***************
*** 1831,1841 **** ExecPrepareTupleRouting(ModifyTableState *mtstate,
  										proute, estate,
  										partidx);
  
! 	/* We do not yet have a way to insert into a foreign partition */
! 	if (partrel->ri_FdwRoutine)
! 		ereport(ERROR,
! 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
! 				 errmsg("cannot route inserted tuples to a foreign table")));
  
  	/*
  	 * Make it look like we are inserting into the partition.
--- 1831,1856 ----
  										proute, estate,
  										partidx);
  
! 	/*
! 	 * Verify the partition is a valid target for INSERT 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 move
! 	 * 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)
! 	{
! 		CheckValidResultRel(partrel, CMD_INSERT);
! 
! 		/* OK, set up information for routing tuples to the partition */
! 		ExecInitRoutingInfo(mtstate, estate, proute, partrel, partidx);
! 
! 		partrel->ri_PartitionReadyForRouting = true;
! 	}
  
  	/*
  	 * Make it look like we are inserting into the partition.
***************
*** 2536,2541 **** ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
--- 2551,2557 ----
  		{
  			List	   *rlist = (List *) lfirst(l);
  
+ 			resultRelInfo->ri_returningList = rlist;
  			resultRelInfo->ri_projectReturning =
  				ExecBuildProjectionInfo(rlist, econtext, slot, &mtstate->ps,
  										resultRelInfo->ri_RelationDesc->rd_att);
***************
*** 2931,2937 **** ExecEndModifyTable(ModifyTableState *node)
  
  	/* Close all the partitioned tables, leaf partitions, and their indices */
  	if (node->mt_partition_tuple_routing)
! 		ExecCleanupTupleRouting(node->mt_partition_tuple_routing);
  
  	/*
  	 * Free the exprcontext
--- 2947,2953 ----
  
  	/* Close all the partitioned tables, leaf partitions, and their indices */
  	if (node->mt_partition_tuple_routing)
! 		ExecCleanupTupleRouting(node, node->mt_partition_tuple_routing);
  
  	/*
  	 * Free the exprcontext
*** a/src/include/executor/execPartition.h
--- b/src/include/executor/execPartition.h
***************
*** 119,124 **** extern ResultRelInfo *ExecInitPartitionInfo(ModifyTableState *mtstate,
--- 119,129 ----
  					ResultRelInfo *resultRelInfo,
  					PartitionTupleRouting *proute,
  					EState *estate, int partidx);
+ extern void ExecInitRoutingInfo(ModifyTableState *mtstate,
+ 					EState *estate,
+ 					PartitionTupleRouting *proute,
+ 					ResultRelInfo *partRelInfo,
+ 					int partidx);
  extern void ExecSetupChildParentMapForLeaf(PartitionTupleRouting *proute);
  extern TupleConversionMap *TupConvMapForLeaf(PartitionTupleRouting *proute,
  				  ResultRelInfo *rootRelInfo, int leaf_index);
***************
*** 126,131 **** extern HeapTuple ConvertPartitionTupleSlot(TupleConversionMap *map,
  						  HeapTuple tuple,
  						  TupleTableSlot *new_slot,
  						  TupleTableSlot **p_my_slot);
! extern void ExecCleanupTupleRouting(PartitionTupleRouting *proute);
  
  #endif							/* EXECPARTITION_H */
--- 131,137 ----
  						  HeapTuple tuple,
  						  TupleTableSlot *new_slot,
  						  TupleTableSlot **p_my_slot);
! extern void ExecCleanupTupleRouting(ModifyTableState *mtstate,
! 						PartitionTupleRouting *proute);
  
  #endif							/* EXECPARTITION_H */
*** a/src/include/foreign/fdwapi.h
--- b/src/include/foreign/fdwapi.h
***************
*** 98,103 **** typedef TupleTableSlot *(*ExecForeignDelete_function) (EState *estate,
--- 98,109 ----
  typedef void (*EndForeignModify_function) (EState *estate,
  										   ResultRelInfo *rinfo);
  
+ typedef void (*BeginForeignInsert_function) (ModifyTableState *mtstate,
+ 											 ResultRelInfo *rinfo);
+ 
+ typedef void (*EndForeignInsert_function) (EState *estate,
+ 										   ResultRelInfo *rinfo);
+ 
  typedef int (*IsForeignRelUpdatable_function) (Relation rel);
  
  typedef bool (*PlanDirectModify_function) (PlannerInfo *root,
***************
*** 205,210 **** typedef struct FdwRoutine
--- 211,218 ----
  	ExecForeignUpdate_function ExecForeignUpdate;
  	ExecForeignDelete_function ExecForeignDelete;
  	EndForeignModify_function EndForeignModify;
+ 	BeginForeignInsert_function BeginForeignInsert;
+ 	EndForeignInsert_function EndForeignInsert;
  	IsForeignRelUpdatable_function IsForeignRelUpdatable;
  	PlanDirectModify_function PlanDirectModify;
  	BeginDirectModify_function BeginDirectModify;
*** a/src/include/nodes/execnodes.h
--- b/src/include/nodes/execnodes.h
***************
*** 444,449 **** typedef struct ResultRelInfo
--- 444,452 ----
  	/* for removing junk attributes from tuples */
  	JunkFilter *ri_junkFilter;
  
+ 	/* list of RETURNING expressions */
+ 	List	   *ri_returningList;
+ 
  	/* for computing a RETURNING list */
  	ProjectionInfo *ri_projectReturning;
  
***************
*** 462,467 **** typedef struct ResultRelInfo
--- 465,473 ----
  	/* relation descriptor for root partitioned table */
  	Relation	ri_PartitionRoot;
  
+ 	/* true if valid target for tuple routing */
+ 	bool		ri_PartitionReadyForRouting;
+ 
  	int			ri_PartitionLeafIndex;
  	/* for running MERGE on this result relation */
  	MergeState *ri_mergeState;
