Push down more UPDATEs/DELETEs in postgres_fdw

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

Hi,

Attached is a WIP patch extending the postgres_fdw DML pushdown in 9.6
so that it can perform an update/delete on a join remotely. An example
is shown below:

* without the patch:
postgres=# explain verbose delete from ft1 using ft2 where ft1.a = ft2.a;

QUERY PLAN

--------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------
Delete on public.ft1 (cost=100.00..102.04 rows=1 width=38)
Remote SQL: DELETE FROM public.t1 WHERE ctid = $1
-> Foreign Scan (cost=100.00..102.04 rows=1 width=38)
Output: ft1.ctid, ft2.*
Relations: (public.ft1) INNER JOIN (public.ft2)
Remote SQL: SELECT r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL
THEN ROW(r2.a, r2.b) END FROM (public.t1 r1 INNER JOIN public.t2 r2 ON
(((r1.a =
r2.a)))) FOR UPDATE OF r1
-> Nested Loop (cost=200.00..202.07 rows=1 width=38)
Output: ft1.ctid, ft2.*
Join Filter: (ft1.a = ft2.a)
-> Foreign Scan on public.ft1 (cost=100.00..101.03
rows=1 width=10)
Output: ft1.ctid, ft1.a
Remote SQL: SELECT a, ctid FROM public.t1 FOR UPDATE
-> Foreign Scan on public.ft2 (cost=100.00..101.03
rows=1 width=36)
Output: ft2.*, ft2.a
Remote SQL: SELECT a, b FROM public.t2
(15 rows)

* with the patch:
postgres=# explain verbose delete from ft1 using ft2 where ft1.a = ft2.a;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------
Delete on public.ft1 (cost=100.00..102.04 rows=1 width=38)
-> Foreign Delete (cost=100.00..102.04 rows=1 width=38)
Remote SQL: DELETE FROM public.t1 r1 USING (SELECT ROW(a, b),
a FROM public.t2) ss1(c1, c2) WHERE ((r1.a = ss1.c2))
(3 rows)

The WIP patch has been created on top of the join pushdown patch [1]/messages/by-id/1688885b-5fb1-8bfa-b1b8-c2758dbe0b38@lab.ntt.co.jp.
So, for testing, please apply the patch in [1]/messages/by-id/1688885b-5fb1-8bfa-b1b8-c2758dbe0b38@lab.ntt.co.jp first.

I'll add this to the the November commitfest.

Best regards,
Etsuro Fujita

[1]: /messages/by-id/1688885b-5fb1-8bfa-b1b8-c2758dbe0b38@lab.ntt.co.jp
/messages/by-id/1688885b-5fb1-8bfa-b1b8-c2758dbe0b38@lab.ntt.co.jp

Attachments:

postgres-fdw-more-update-pushdown-WIP.patchbinary/octet-stream; name=postgres-fdw-more-update-pushdown-WIP.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 140,152 **** static void deparseTargetList(StringInfo buf,
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 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);
  static void deparseRelation(StringInfo buf, Relation rel);
--- 140,159 ----
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(List *tlist,
! 						  bool is_returning,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 Index rtindex, Relation rel,
  					 bool trig_after_row,
  					 List *returningList,
  					 List **retrieved_attrs);
+ static void deparseExplicitReturningList(Index rtindex, Relation rel,
+ 							 List **fdw_scan_tlist,
+ 							 List *returningList,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context);
  static void deparseColumnRef(StringInfo buf, int varno, int varattno,
  				 PlannerInfo *root, bool qualify_col);
  static void deparseRelation(StringInfo buf, Relation rel);
***************
*** 178,189 **** static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
  static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 					  RelOptInfo *foreignrel, bool add_rel_alias,
! 					  AliasedExprs *aliased, List **params_list);
  static void deparseOperandRelation(StringInfo buf, PlannerInfo *root,
  					   RelOptInfo *foreignrel, JoinType jointype,
! 					   bool add_rel_alias, AliasedExprs *aliased,
! 					   List **params_list);
  static void appendSubselectAlias(List *exprs, deparse_expr_cxt *context);
  static void registerAliasedExpr(Expr *expr, int tabno, int colno, AliasedExprs *aliased);
  static bool is_aliased_expr(Expr *node, int *tabno, int *colno, deparse_expr_cxt *context);
--- 185,197 ----
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
  static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 					  RelOptInfo *foreignrel, Index ignore_rel,
! 					  bool add_rel_alias, AliasedExprs *aliased,
! 					  List **params_list);
  static void deparseOperandRelation(StringInfo buf, PlannerInfo *root,
  					   RelOptInfo *foreignrel, JoinType jointype,
! 					   Index ignore_rel, bool add_rel_alias,
! 					   AliasedExprs *aliased, List **params_list);
  static void appendSubselectAlias(List *exprs, deparse_expr_cxt *context);
  static void registerAliasedExpr(Expr *expr, int tabno, int colno, AliasedExprs *aliased);
  static bool is_aliased_expr(Expr *node, int *tabno, int *colno, deparse_expr_cxt *context);
***************
*** 934,940 **** deparseSelectSql(List *tlist,
  	 * Deparse a join tree expression in FROM clause first.
  	 */
  	initStringInfo(&jointree);
! 	deparseFromExprForRel(&jointree, root, foreignrel,
  						  (foreignrel->reloptkind == RELOPT_JOINREL),
  						  context->aliased, context->params_list);
  
--- 942,948 ----
  	 * Deparse a join tree expression in FROM clause first.
  	 */
  	initStringInfo(&jointree);
! 	deparseFromExprForRel(&jointree, root, foreignrel, (Index) 0,
  						  (foreignrel->reloptkind == RELOPT_JOINREL),
  						  context->aliased, context->params_list);
  
***************
*** 950,956 **** deparseSelectSql(List *tlist,
  	 */
  	if (tlist != NIL)
  	{
! 		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
  	{
--- 958,964 ----
  	 */
  	if (tlist != NIL)
  	{
! 		deparseExplicitTargetList(tlist, false, retrieved_attrs, context);
  	}
  	else
  	{
***************
*** 1250,1256 **** get_jointype_name(JoinType jointype)
   * from 1. It has same number of entries as tlist.
   */
  static void
! deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
--- 1258,1266 ----
   * from 1. It has same number of entries as tlist.
   */
  static void
! deparseExplicitTargetList(List *tlist,
! 						  bool is_returning,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
***************
*** 1275,1280 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
--- 1285,1293 ----
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
+ 		else if (is_returning)
+ 			appendStringInfoString(buf, " RETURNING ");
+ 
  		deparseExpr((Expr *) var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
***************
*** 1283,1289 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  	}
  
  	/* Don't generate bad syntax if no columns */
! 	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
  
--- 1296,1302 ----
  	}
  
  	/* Don't generate bad syntax if no columns */
! 	if (i == 0 && !is_returning)
  		appendStringInfoString(buf, "NULL");
  }
  
***************
*** 1294,1322 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
   * ((outer relation) <join type> (inner relation) ON (joinclauses))
   * For a base relation the function just adds the schema-qualified tablename,
   * with the appropriate alias if so requested.
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 					  RelOptInfo *foreignrel, bool add_rel_alias,
! 					  AliasedExprs *aliased, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
  		/* Begin the FROM clause entry. */
  		appendStringInfoChar(buf, '(');
  
  		/* Deparse outer relation */
  		deparseOperandRelation(buf, root, fpinfo->outerrel, fpinfo->jointype,
! 							   true, aliased, params_list);
  
  		/* Append join type */
  		appendStringInfo(buf, " %s JOIN ", get_jointype_name(fpinfo->jointype));
  
  		/* Deparse inner relation */
  		deparseOperandRelation(buf, root, fpinfo->innerrel, fpinfo->jointype,
! 							   true, aliased, params_list);
  
  		/* Append join conditions */
  		appendStringInfoString(buf, " ON ");
--- 1307,1375 ----
   * ((outer relation) <join type> (inner relation) ON (joinclauses))
   * For a base relation the function just adds the schema-qualified tablename,
   * with the appropriate alias if so requested.
+  *
+  * ignore_rel is either zero or the rangetable index of a target relation.
+  * In the latter case the function constructs a FROM clause of an UPDATE or
+  * a USING clause of a DELETE by deparsing a JOIN/ON clause with the
+  * ignore_rel target relation removed.  Note that we can do that safely
+  * because (1) any join to the target relation is an inner join and thus can
+  * be interchanged with higher-level joins (note: since we currently don't
+  * allow the target relation to appear on the nullable side of an outer join,
+  * this is true for the case where the higher-level join is an outer join,
+  * due to outer-join identity 1 shown in src/backend/optimizer/README), and
+  * (2) join conditions mentioning the target relation are already pulled up
+  * into the fpinfo->remote_conds of the given foreignrel, which are deparsed
+  * as WHERE conditions of the UPDATE/DELETE.
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 					  RelOptInfo *foreignrel, Index ignore_rel,
! 					  bool add_rel_alias, AliasedExprs *aliased,
! 					  List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
+ 		bool		deparse_outer = true;
+ 		bool		deparse_inner = true;
+ 
+ 		if (ignore_rel > 0)
+ 		{
+ 			int			varno;
+ 
+ 			if (bms_get_singleton_member(fpinfo->outerrel->relids, &varno) &&
+ 				varno == ignore_rel)
+ 				deparse_outer = false;
+ 			if (bms_get_singleton_member(fpinfo->innerrel->relids, &varno) &&
+ 				varno == ignore_rel)
+ 				deparse_inner = false;
+ 		}
+ 
+ 		if (deparse_outer && !deparse_inner)
+ 			return deparseOperandRelation(buf, root, fpinfo->outerrel,
+ 										  fpinfo->jointype, ignore_rel,
+ 										  true, aliased, params_list);
+ 		if (!deparse_outer && deparse_inner)
+ 			return deparseOperandRelation(buf, root, fpinfo->innerrel,
+ 										  fpinfo->jointype, ignore_rel,
+ 										  true, aliased, params_list);
+ 
+ 		Assert(deparse_outer && deparse_inner);
+ 
  		/* Begin the FROM clause entry. */
  		appendStringInfoChar(buf, '(');
  
  		/* Deparse outer relation */
  		deparseOperandRelation(buf, root, fpinfo->outerrel, fpinfo->jointype,
! 							   ignore_rel, true, aliased, params_list);
  
  		/* Append join type */
  		appendStringInfo(buf, " %s JOIN ", get_jointype_name(fpinfo->jointype));
  
  		/* Deparse inner relation */
  		deparseOperandRelation(buf, root, fpinfo->innerrel, fpinfo->jointype,
! 							   ignore_rel, true, aliased, params_list);
  
  		/* Append join conditions */
  		appendStringInfoString(buf, " ON ");
***************
*** 1379,1386 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
  static void
  deparseOperandRelation(StringInfo buf, PlannerInfo *root,
  					   RelOptInfo *foreignrel, JoinType jointype,
! 					   bool add_rel_alias, AliasedExprs *aliased,
! 					   List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
--- 1432,1439 ----
  static void
  deparseOperandRelation(StringInfo buf, PlannerInfo *root,
  					   RelOptInfo *foreignrel, JoinType jointype,
! 					   Index ignore_rel, bool add_rel_alias,
! 					   AliasedExprs *aliased, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
***************
*** 1411,1417 **** deparseOperandRelation(StringInfo buf, PlannerInfo *root,
  		appendSubselectAlias(foreignrel->reltarget->exprs, &context);
  	}
  	else
! 		deparseFromExprForRel(buf, root, foreignrel,
  							  true, aliased, params_list);
  }
  
--- 1464,1470 ----
  		appendSubselectAlias(foreignrel->reltarget->exprs, &context);
  	}
  	else
! 		deparseFromExprForRel(buf, root, foreignrel, ignore_rel,
  							  true, aliased, params_list);
  }
  
***************
*** 1653,1658 **** deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 1706,1713 ----
  void
  deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 1660,1680 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
! 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  	int			nestlevel;
  	bool		first;
  	ListCell   *lc;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
  	context.buf = buf;
! 	context.aliased = NULL;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
--- 1715,1754 ----
  					   List *returningList,
  					   List **retrieved_attrs)
  {
! 	AliasedExprs aliased;
! 	StringInfoData jointree;
  	deparse_expr_cxt context;
  	int			nestlevel;
  	bool		first;
  	ListCell   *lc;
  
+ 	/* Initialize fields of AliasedExprs */
+ 	aliased.exprs = NIL;
+ 	aliased.max_exprs = 32;
+ 	aliased.tabnos = (int *) palloc(aliased.max_exprs * sizeof(int));
+ 	aliased.colnos = (int *) palloc(aliased.max_exprs * sizeof(int));
+ 	aliased.next_tabno = 1;
+ 	aliased.relids = NULL;
+ 
+ 	/* Deparse a join tree expression in FROM if an UPDATE on a join */
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		initStringInfo(&jointree);
+ 		deparseFromExprForRel(&jointree, root, foreignrel, rtindex,
+ 							  true, &aliased, params_list);
+ 	}
+ 
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
  	context.buf = buf;
! 	context.aliased = &aliased;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
***************
*** 1701,1714 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  
  	reset_transmission_modes(nestlevel);
  
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1775,1796 ----
  
  	reset_transmission_modes(nestlevel);
  
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " FROM %s", jointree.data);
+ 
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 		deparseExplicitReturningList(rtindex, rel, fdw_scan_tlist,
! 									 returningList, retrieved_attrs,
! 									 &context);
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1743,1765 **** deparseDeleteSql(StringInfo buf, PlannerInfo *root,
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
! 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
  	context.buf = buf;
! 	context.aliased = NULL;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
  
  	if (remote_conds)
  	{
--- 1825,1871 ----
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
! 	AliasedExprs aliased;
! 	StringInfoData jointree;
  	deparse_expr_cxt context;
  
+ 	/* Initialize fields of AliasedExprs */
+ 	aliased.exprs = NIL;
+ 	aliased.max_exprs = 32;
+ 	aliased.tabnos = (int *) palloc(aliased.max_exprs * sizeof(int));
+ 	aliased.colnos = (int *) palloc(aliased.max_exprs * sizeof(int));
+ 	aliased.next_tabno = 1;
+ 	aliased.relids = NULL;
+ 
+ 	/* Deparse a join tree expression in USING if a DELETE on a join */
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		initStringInfo(&jointree);
+ 		deparseFromExprForRel(&jointree, root, foreignrel, rtindex,
+ 							  true, &aliased, params_list);
+ 	}
+ 
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
  	context.buf = buf;
! 	context.aliased = &aliased;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
+ 
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " USING %s", jointree.data);
  
  	if (remote_conds)
  	{
***************
*** 1767,1774 **** deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1873,1885 ----
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 	  deparseExplicitReturningList(rtindex, rel, fdw_scan_tlist,
! 								   returningList, retrieved_attrs,
! 								   &context);
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1808,1813 **** deparseReturningList(StringInfo buf, PlannerInfo *root,
--- 1919,2035 ----
  }
  
  /*
+  * Add a RETURNING clause, if needed, to an UPDATE/DELETE on a join.
+  */
+ static void
+ deparseExplicitReturningList(Index rtindex, Relation rel,
+ 							 List **fdw_scan_tlist,
+ 							 List *returningList,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context)
+ {
+ 	TupleDesc	tupdesc = RelationGetDescr(rel);
+ 	bool		have_wholerow = false;
+ 	List	   *tlist = NIL;
+ 	List	   *vars;
+ 	ListCell   *lc;
+ 
+ 	*retrieved_attrs = NIL;
+ 
+ 	if (returningList == NIL)
+ 		return;
+ 
+ 	vars = pull_var_clause((Node *) returningList, PVC_INCLUDE_PLACEHOLDERS);
+ 
+ 	/* Check that there's a whole-row reference to the result relation. */
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno == InvalidAttrNumber)
+ 		{
+ 			have_wholerow = true;
+ 			break;
+ 		}
+ 	}
+ 
+ 	/* If so, we'll need all non-system columns of the result relation. */
+ 	if (have_wholerow)
+ 	{
+ 		int			i;
+ 
+ 		for (i = 1; i <= tupdesc->natts; i++)
+ 		{
+ 			Form_pg_attribute attr = tupdesc->attrs[i - 1];
+ 			Var		   *var;
+ 
+ 			/* Ignore dropped attributes. */
+ 			if (attr->attisdropped)
+ 				continue;
+ 
+ 			var = makeVar(rtindex,
+ 						  i,
+ 						  attr->atttypid,
+ 						  attr->atttypmod,
+ 						  attr->attcollation,
+ 						  0);
+ 
+ 			tlist = lappend(tlist,
+ 							makeTargetEntry((Expr *) var,
+ 											i,
+ 											NULL,
+ 											false));
+ 		}
+ 	}
+ 
+ 	/* Now add any remaining columns. */ 
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno == InvalidAttrNumber)
+ 			continue;		/* don't need it */
+ 
+ 		if (tlist_member((Node *) var, tlist))
+ 			continue;		/* already got it */
+ 
+ 		tlist = lappend(tlist,
+ 						makeTargetEntry((Expr *) var,
+ 										list_length(tlist) + 1,
+ 										NULL,
+ 										false));
+ 	}
+ 
+ 	deparseExplicitTargetList(tlist, true, retrieved_attrs, context);
+ 
+ 	/* Finally, rewrite *fdw_scan_tlist. */
+ 	if (tlist != NIL)
+ 	{
+ 		foreach(lc, *fdw_scan_tlist)
+ 		{
+ 			TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 			Var		   *var = (Var *) tle->expr;
+ 
+ 			if (tlist_member((Node *) var, tlist))
+ 				continue;		/* already got it */
+ 
+ 			tlist = lappend(tlist,
+ 							makeTargetEntry((Expr *) var,
+ 											list_length(tlist) + 1,
+ 											NULL,
+ 											false));
+ 		}
+ 		*fdw_scan_tlist = tlist;
+ 	}
+ 
+ 	list_free(vars);
+ }
+ 
+ /*
   * Construct SELECT statement to acquire size in blocks of given relation.
   *
   * Note: we use local definition of block size, not remote definition.
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 2932,2958 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
!                                                                                                                                              QUERY PLAN                                                                                                                                             
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3, c7 = $4 WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.c1, (ft2.c2 + 500), NULL::integer, (ft2.c3 || '_update9'::text), ft2.c4, ft2.c5, ft2.c6, 'ft2       '::character(10), ft2.c8, ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c8, r1.ctid, ss1.c1 FROM ("S 1"."T 1" r1 INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))) ss1(c1, c2) ON (TRUE)) WHERE ((r1.c2 = ss1.c2)) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid
!                      Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c8, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))
! (17 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
--- 2932,2944 ----
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
!                                                                                                                                     QUERY PLAN                                                                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    ->  Foreign Update
!          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 500), c3 = (r1.c3 || '_update9'::text), c7 = 'ft2       '::character(10) FROM (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))) ss1(c1, c2) WHERE ((r1.c2 = ss1.c2))
! (3 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
***************
*** 3075,3101 **** DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
!                                                                                                                    QUERY PLAN                                                                                                                   
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1.ctid, ss1.c1 FROM ("S 1"."T 1" r1 INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))) ss1(c1, c2) ON (TRUE)) WHERE ((r1.c2 = ss1.c2)) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.ctid, ft2.c2
!                      Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))
! (17 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
--- 3061,3073 ----
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    ->  Foreign Delete
!          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))) ss1(c1, c2) WHERE ((r1.c2 = ss1.c2))
! (3 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 210,215 **** typedef struct PgFdwDirectModifyState
--- 210,221 ----
  	PGresult   *result;			/* result for query */
  	int			num_tuples;		/* # of result tuples */
  	int			next_tuple;		/* index of next one to return */
+ 	Relation	resultRel;		/* relcache entry for the target table */
+ 	TupleTableSlot *resultSlot;	/* slot for updated/deleted tuples */
+ 	AttrNumber *attnoMap;		/* array of resultRel attr numbers */
+ 	AttrNumber	ctidAttno;		/* attnum of ctid of result tuple */
+ 	AttrNumber	oidAttno;		/* attnum of oid of result tuple */
+ 	bool		hasSystemCols;	/* are there system columns of resultRel? */
  
  	/* working memory context */
  	MemoryContext temp_cxt;		/* context for per-tuple temporary data */
***************
*** 374,379 **** static void store_returning_result(PgFdwModifyState *fmstate,
--- 380,391 ----
  					   TupleTableSlot *slot, PGresult *res);
  static void execute_dml_stmt(ForeignScanState *node);
  static TupleTableSlot *get_returning_data(ForeignScanState *node);
+ static void init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate);
+ static TupleTableSlot *apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot);
  static void prepare_query_params(PlanState *node,
  					 List *fdw_exprs,
  					 int numParams,
***************
*** 2144,2149 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2156,2162 ----
  	Relation	rel;
  	StringInfoData sql;
  	ForeignScan *fscan;
+ 	RelOptInfo *foreignrel;
  	List	   *targetAttrs = NIL;
  	List	   *remote_conds;
  	List	   *params_list = NIL;
***************
*** 2175,2187 **** postgresPlanDirectModify(PlannerInfo *root,
  		return false;
  
  	/*
- 	 * We can't handle an UPDATE or DELETE on a foreign join for now.
- 	 */
- 	fscan = (ForeignScan *) subplan;
- 	if (fscan->scan.scanrelid == 0)
- 		return false;
- 
- 	/*
  	 * It's unsafe to update a foreign table directly, if any expressions to
  	 * assign to the target columns are unsafe to evaluate remotely.
  	 */
--- 2188,2193 ----
***************
*** 2220,2225 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2226,2233 ----
  	/*
  	 * Ok, rewrite subplan so as to modify the foreign table directly.
  	 */
+ 	fscan = (ForeignScan *) subplan;
+ 
  	initStringInfo(&sql);
  
  	/*
***************
*** 2229,2234 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2237,2254 ----
  	rel = heap_open(rte->relid, NoLock);
  
  	/*
+ 	 * Get a rel for this foreign table or join.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 	{
+ 		/* We should have a rel for this foreign join. */
+ 		foreignrel = find_join_rel(root, fscan->fs_relids);
+ 		Assert(foreignrel);
+ 	}
+ 	else
+ 		foreignrel = find_base_rel(root, resultRelation);
+ 
+ 	/*
  	 * Extract the baserestrictinfo clauses that can be evaluated remotely.
  	 */
  	remote_conds = (List *) list_nth(fscan->fdw_private,
***************
*** 2247,2252 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2267,2274 ----
  	{
  		case CMD_UPDATE:
  			deparseDirectUpdateSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
+ 								   &fscan->fdw_scan_tlist,
  								   ((Plan *) fscan)->targetlist,
  								   targetAttrs,
  								   remote_conds, &params_list,
***************
*** 2254,2259 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2276,2283 ----
  			break;
  		case CMD_DELETE:
  			deparseDirectDeleteSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
+ 								   &fscan->fdw_scan_tlist,
  								   remote_conds, &params_list,
  								   returningList, &retrieved_attrs);
  			break;
***************
*** 2281,2286 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2305,2316 ----
  									retrieved_attrs,
  									makeInteger(plan->canSetTag));
  
+ 	/*
+ 	 * We don't need the outer subplan.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 		fscan->scan.plan.lefttree = NULL;
+ 
  	heap_close(rel, NoLock);
  	return true;
  }
***************
*** 2295,2300 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2325,2331 ----
  	ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
  	EState	   *estate = node->ss.ps.state;
  	PgFdwDirectModifyState *dmstate;
+ 	Index		rtindex;
  	RangeTblEntry *rte;
  	Oid			userid;
  	ForeignTable *table;
***************
*** 2317,2327 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rte = rt_fetch(fsplan->scan.scanrelid, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	dmstate->rel = node->ss.ss_currentRelation;
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
--- 2348,2362 ----
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rtindex = estate->es_result_relation_info->ri_RangeTableIndex;
! 	rte = rt_fetch(rtindex, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	if (fsplan->scan.scanrelid > 0)
! 		dmstate->rel = node->ss.ss_currentRelation;
! 	else
! 		dmstate->rel = ExecOpenScanRelation(estate, rtindex, eflags);
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
***************
*** 2331,2336 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2366,2380 ----
  	 */
  	dmstate->conn = GetConnection(user, false);
  
+ 	if (fsplan->scan.scanrelid == 0)
+ 	{
+ 		/* Save info about target table. */
+ 		dmstate->resultRel = dmstate->rel;
+ 
+ 		/* rel should be NULL if foreign join. */
+ 		dmstate->rel = NULL;
+ 	}
+ 
  	/* Initialize state variable */
  	dmstate->num_tuples = -1;	/* -1 means not set yet */
  
***************
*** 2351,2357 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(RelationGetDescr(dmstate->rel));
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
--- 2395,2415 ----
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 	{
! 		TupleDesc	tupdesc;
! 
! 		if (fsplan->scan.scanrelid > 0)
! 			tupdesc = RelationGetDescr(dmstate->rel);
! 		else
! 			tupdesc = node->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
! 
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
! 
! 		/* Initialize a filter to extract an updated/deleted tuple. */
! 		if (fsplan->scan.scanrelid == 0)
! 			init_returning_filter(dmstate, fsplan->fdw_scan_tlist,
! 								  rtindex, estate);
! 	}
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
***************
*** 2432,2437 **** postgresEndDirectModify(ForeignScanState *node)
--- 2490,2499 ----
  	ReleaseConnection(dmstate->conn);
  	dmstate->conn = NULL;
  
+ 	/* close the result relation. */
+ 	if (dmstate->resultRel)
+ 		ExecCloseScanRelation(dmstate->resultRel);
+ 
  	/* MemoryContext will be deleted automatically. */
  }
  
***************
*** 3282,3287 **** get_returning_data(ForeignScanState *node)
--- 3344,3350 ----
  	EState	   *estate = node->ss.ps.state;
  	ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
  	TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ 	TupleTableSlot *resultSlot;
  
  	Assert(resultRelInfo->ri_projectReturning);
  
***************
*** 3299,3305 **** get_returning_data(ForeignScanState *node)
--- 3362,3371 ----
  	 * "UPDATE/DELETE .. RETURNING 1" for example.)
  	 */
  	if (!dmstate->has_returning)
+ 	{
  		ExecStoreAllNullTuple(slot);
+ 		resultSlot = slot;
+ 	}
  	else
  	{
  		/*
***************
*** 3315,3321 **** get_returning_data(ForeignScanState *node)
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												NULL,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
--- 3381,3387 ----
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												node,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
***************
*** 3326,3341 **** get_returning_data(ForeignScanState *node)
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = slot;
  
  	return slot;
  }
  
  /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
--- 3392,3579 ----
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
+ 
+ 		/* Get the updated/deleted tuple. */
+ 		if (dmstate->rel)
+ 			resultSlot = slot;
+ 		else
+ 			resultSlot = apply_returning_filter(dmstate, slot);
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = resultSlot;
  
  	return slot;
  }
  
  /*
+  * Initialize a filter to extract an updated/deleted tuple from a scan tuple.
+  */
+ static void
+ init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate)
+ {
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	ListCell   *lc;
+ 	int			i;
+ 
+ 	/* Make a new slot for storing an output tuple. */
+ 	dmstate->resultSlot = ExecInitExtraTupleSlot(estate);
+ 	ExecSetSlotDescriptor(dmstate->resultSlot, resultTupType);
+ 
+ 	/*
+ 	 * Calculate the mapping between the fdw_scan_tlist list's entries and
+ 	 * the updated/deleted tuple's attributes.
+ 	 *
+ 	 * The "map" is an array of the resultRel attribute numbers, i.e. one
+ 	 * entry for every attribute of the updated/deleted tuple.  The value of
+ 	 * this entry is the index of the corresponding entry in fdw_scan_tlist.
+ 	 * We store zero for any attributes that don't have the corresponding
+ 	 * entries in fdw_scan_tlist (i.e. attributes that won't be retrieved
+ 	 * from the remote server), marking that a NULL is needed in the output
+ 	 * tuple.
+ 	 *
+ 	 * Also get the index of the entry for ctid/oid of the updated/deleted
+ 	 * tuple if any.
+ 	 */
+ 	dmstate->attnoMap = (AttrNumber *) palloc0(resultTupType->natts * sizeof(AttrNumber));
+ 
+ 	dmstate->ctidAttno = dmstate->oidAttno = 0;
+ 
+ 	i = 1;
+ 	dmstate->hasSystemCols = false;
+ 	foreach(lc, fdw_scan_tlist)
+ 	{
+ 		if (list_member_int(dmstate->retrieved_attrs, i))
+ 		{
+ 			TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 			Var		   *var = (Var *) tle->expr;
+ 
+ 			if (IsA(var, Var) && var->varno == rtindex)
+ 			{
+ 				int			attrno = var->varattno;
+ 
+ 				if (attrno < 0)
+ 				{
+ 					if (attrno == SelfItemPointerAttributeNumber)
+ 						dmstate->ctidAttno = i;
+ 					else if (attrno == ObjectIdAttributeNumber)
+ 						dmstate->oidAttno = i;
+ 					dmstate->hasSystemCols = true;
+ 				}
+ 				else
+ 				{
+ 					Assert(attrno > 0);
+ 					dmstate->attnoMap[attrno - 1] = i;
+ 				}
+ 			}
+ 		}
+ 		i++;
+ 	}
+ }
+ 
+ /*
+  * Extract and return an updated/deleted tuple from a scan tuple.
+  */
+ static TupleTableSlot *
+ apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot)
+ {
+ 	TupleTableSlot *resultSlot = dmstate->resultSlot;
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	HeapTuple	resultTup;
+ 	Datum	   *values;
+ 	bool	   *isnull;
+ 	Datum	   *old_values;
+ 	bool	   *old_isnull;
+ 	int			i;
+ 
+ 	/*
+ 	 * Extract all the values of the scan tuple.
+ 	 */
+ 	slot_getallattrs(slot);
+ 	old_values = slot->tts_values;
+ 	old_isnull = slot->tts_isnull;
+ 
+ 	/*
+ 	 * Prepare to build a result tuple.
+ 	 */
+ 	ExecClearTuple(resultSlot);
+ 	values = resultSlot->tts_values;
+ 	isnull = resultSlot->tts_isnull;
+ 
+ 	/*
+ 	 * Transpose data into proper fields of the result tuple.
+ 	 */
+ 	for (i = 0; i < resultTupType->natts; i++)
+ 	{
+ 		int			j = dmstate->attnoMap[i];
+ 
+ 		if (j == 0)
+ 		{
+ 			values[i] = (Datum) 0;
+ 			isnull[i] = true;
+ 		}
+ 		else
+ 		{
+ 			values[i] = old_values[j - 1];
+ 			isnull[i] = old_isnull[j - 1];
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * Build the virtual result tuple.
+ 	 */
+ 	ExecStoreVirtualTuple(resultSlot);
+ 
+ 	/*
+ 	 * If we have any system columns to return, install it in t_self.
+ 	 */
+ 	if (dmstate->hasSystemCols)
+ 	{
+ 		resultTup = ExecMaterializeSlot(resultSlot);
+ 
+ 		/*
+ 		 * xmin, xmax, cmin, and tableoid
+ 		 *
+ 		 * Note: we currently don't allow the result relation to appear on
+ 		 * the nullable side of an outer join.
+ 		 */
+ 		HeapTupleHeaderSetXmin(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetXmax(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetCmin(resultTup->t_data, InvalidTransactionId);
+ 
+ 		resultTup->t_tableOid = RelationGetRelid(dmstate->resultRel);
+ 
+ 		/* ctid */
+ 		if (dmstate->ctidAttno)
+ 		{
+ 			ItemPointer ctid = NULL;
+ 
+ 			ctid = (ItemPointer) DatumGetPointer(old_values[dmstate->ctidAttno - 1]);
+ 			resultTup->t_self = *ctid;
+ 		}
+ 
+ 		/* oid */
+ 		if (dmstate->oidAttno)
+ 		{
+ 			Oid			oid = InvalidOid;
+ 
+ 			oid = DatumGetObjectId(old_values[dmstate->oidAttno - 1]);
+ 			HeapTupleSetOid(resultTup, oid);
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * And return the result tuple.
+ 	 */
+ 	return resultSlot;
+ }
+ 
+ /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
***************
*** 4455,4465 **** make_tuple_from_result_row(PGresult *res,
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
- 		PgFdwScanState *fdw_sstate;
- 
  		Assert(fsstate);
! 		fdw_sstate = (PgFdwScanState *) fsstate->fdw_state;
! 		tupdesc = fdw_sstate->tupdesc;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
--- 4693,4700 ----
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
  		Assert(fsstate);
! 		tupdesc = fsstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 143,148 **** extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 143,150 ----
  				 List **retrieved_attrs);
  extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 155,160 **** extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
--- 157,164 ----
  				 List **retrieved_attrs);
  extern void deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 685,698 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
--- 685,698 ----
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
#2Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#1)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

Thanks Fujita-san for working on this.

* with the patch:
postgres=# explain verbose delete from ft1 using ft2 where ft1.a = ft2.a;
QUERY PLAN
------------------------------------------------------------
-----------------------------------------------------------------
Delete on public.ft1 (cost=100.00..102.04 rows=1 width=38)
-> Foreign Delete (cost=100.00..102.04 rows=1 width=38)
Remote SQL: DELETE FROM public.t1 r1 USING (SELECT ROW(a, b), a
FROM public.t2) ss1(c1, c2) WHERE ((r1.a = ss1.c2))
(3 rows)

The WIP patch has been created on top of the join pushdown patch [1]. So,
for testing, please apply the patch in [1] first.

The underlying scan on t2 requires ROW(a,b) for locking the row for
update/share. But clearly it's not required if the full query is being
pushed down. Is there a way we can detect that ROW(a,b) is useless column
(not used anywhere in the other parts of the query like RETURNING, DELETE
clause etc.) and eliminate it? Similarly for a, it's part of the targetlist
of the underlying scan so that the WHERE clause can be applied on it. But
it's not needed if we are pushing down the query. If we eliminate the
targetlist of the query, we could construct a remote query without having
subquery in it, making it more readable.

--
Best Wishes,
Ashutosh Bapat
EnterpriseDB Corporation
The Postgres Database Company

#3Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#2)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 2016/09/07 13:21, Ashutosh Bapat wrote:

* with the patch:
postgres=# explain verbose delete from ft1 using ft2 where ft1.a =
ft2.a;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------
Delete on public.ft1 (cost=100.00..102.04 rows=1 width=38)
-> Foreign Delete (cost=100.00..102.04 rows=1 width=38)
Remote SQL: DELETE FROM public.t1 r1 USING (SELECT ROW(a,
b), a FROM public.t2) ss1(c1, c2) WHERE ((r1.a = ss1.c2))
(3 rows)

The underlying scan on t2 requires ROW(a,b) for locking the row for
update/share. But clearly it's not required if the full query is being
pushed down.

Exactly.

Is there a way we can detect that ROW(a,b) is useless
column (not used anywhere in the other parts of the query like
RETURNING, DELETE clause etc.) and eliminate it?

I don't have a clear solution for that yet, but I'll try to remove that
in the next version.

Similarly for a, it's
part of the targetlist of the underlying scan so that the WHERE clause
can be applied on it. But it's not needed if we are pushing down the
query. If we eliminate the targetlist of the query, we could construct a
remote query without having subquery in it, making it more readable.

Will try to do so also.

Thanks for the comments!

Best regards,
Etsuro Fujita

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

#4Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#3)
1 attachment(s)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 2016/09/08 19:55, Etsuro Fujita wrote:

On 2016/09/07 13:21, Ashutosh Bapat wrote:

* with the patch:
postgres=# explain verbose delete from ft1 using ft2 where ft1.a =
ft2.a;
QUERY PLAN

-----------------------------------------------------------------------------------------------------------------------------

Delete on public.ft1 (cost=100.00..102.04 rows=1 width=38)
-> Foreign Delete (cost=100.00..102.04 rows=1 width=38)
Remote SQL: DELETE FROM public.t1 r1 USING (SELECT ROW(a,
b), a FROM public.t2) ss1(c1, c2) WHERE ((r1.a = ss1.c2))
(3 rows)

The underlying scan on t2 requires ROW(a,b) for locking the row for
update/share. But clearly it's not required if the full query is being
pushed down.

Is there a way we can detect that ROW(a,b) is useless
column (not used anywhere in the other parts of the query like
RETURNING, DELETE clause etc.) and eliminate it?

I don't have a clear solution for that yet, but I'll try to remove that
in the next version.

Similarly for a, it's
part of the targetlist of the underlying scan so that the WHERE clause
can be applied on it. But it's not needed if we are pushing down the
query. If we eliminate the targetlist of the query, we could construct a
remote query without having subquery in it, making it more readable.

Will try to do so also.

I addressed this by improving the deparse logic so that a remote query
for performing an UPDATE/DELETE on a join directly on the remote can be
created as proposed if possible. Attached is an updated version of the
patch, which is created on top of the patch set [1]/messages/by-id/11eafd10-d3f8-ac8a-b642-b0e65037c76b@lab.ntt.co.jp. The patch is still
WIP (ie, needs more comments and regression tests, at least), but any
comments would be gratefully appreciated.

Best regards,
Etsuro Fujita

[1]: /messages/by-id/11eafd10-d3f8-ac8a-b642-b0e65037c76b@lab.ntt.co.jp
/messages/by-id/11eafd10-d3f8-ac8a-b642-b0e65037c76b@lab.ntt.co.jp

Attachments:

postgres-fdw-more-update-pushdown-WIP-2.patchapplication/x-patch; name=postgres-fdw-more-update-pushdown-WIP-2.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 133,145 **** static void deparseTargetList(StringInfo buf,
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 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);
  static void deparseRelation(StringInfo buf, Relation rel);
--- 133,164 ----
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(bool is_returning,
! 						  List *tlist,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 Index rtindex, Relation rel,
  					 bool trig_after_row,
  					 List *returningList,
  					 List **retrieved_attrs);
+ static void deparseExplicitReturningList(List *rlist,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context);
+ static List *makeExplicitReturningList(Index rtindex, Relation rel,
+ 						  List *returningList,
+ 						  List **fdw_scan_tlist,
+ 						  Relids *wholerows);
+ static void rewriteFromExprForRel(PlannerInfo *root, RelOptInfo *foreignrel,
+ 					  Index target_rel, List **target_conds, Relids wholerows);
+ static List *pullUpTargetConds(RelOptInfo *foreignrel, List *joinclauses,
+ 				  Index target_rel, List **target_conds);
+ static bool pullUpSubquery(PlannerInfo *root, RelOptInfo *foreignrel, JoinType jointype,
+ 			   Index target_rel, List **target_conds, Relids wholerows);
+ static bool isSimpleSubquery(PlannerInfo *root, RelOptInfo *baserel, JoinType jointype,
+ 				 Relids wholerows);
+ static List *getCleanSubqueryExprs(PlannerInfo *root, RelOptInfo *foreignrel,
+ 					  List *subquery_exprs, Relids wholerows);
  static void deparseColumnRef(StringInfo buf, int varno, int varattno,
  				 PlannerInfo *root, bool qualify_col);
  static void deparseRelation(StringInfo buf, Relation rel);
***************
*** 168,178 **** static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
! static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 					RelOptInfo *joinrel, bool use_alias, List **params_list);
  static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 				   bool make_subquery, List **params_list);
  static void appendSubselectAlias(StringInfo buf, int tabno, int ncols);
  static void getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
  					  int *tabno, int *colno);
--- 187,197 ----
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
! static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, Index target_rel, List **params_list);
  static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 				   bool make_subquery, Index target_rel, List **params_list);
  static void appendSubselectAlias(StringInfo buf, int tabno, int ncols);
  static void getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
  					  int *tabno, int *colno);
***************
*** 1088,1094 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  		tlist != NIL)
  	{
  		/* Use the input tlist */
! 		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
  	{
--- 1107,1113 ----
  		tlist != NIL)
  	{
  		/* Use the input tlist */
! 		deparseExplicitTargetList(false, tlist, retrieved_attrs, context);
  	}
  	else
  	{
***************
*** 1131,1137 **** deparseFromExpr(List *quals, deparse_expr_cxt *context)
  	appendStringInfoString(buf, " FROM ");
  	deparseFromExprForRel(buf, context->root, scanrel,
  						  (bms_num_members(scanrel->relids) > 1),
! 						  context->params_list);
  
  	/* Construct WHERE clause */
  	if (quals != NIL)
--- 1150,1156 ----
  	appendStringInfoString(buf, " FROM ");
  	deparseFromExprForRel(buf, context->root, scanrel,
  						  (bms_num_members(scanrel->relids) > 1),
! 						  (Index) 0, context->params_list);
  
  	/* Construct WHERE clause */
  	if (quals != NIL)
***************
*** 1407,1413 **** get_jointype_name(JoinType jointype)
   * from 1. It has same number of entries as tlist.
   */
  static void
! deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
--- 1426,1434 ----
   * from 1. It has same number of entries as tlist.
   */
  static void
! deparseExplicitTargetList(bool is_returning,
! 						  List *tlist,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
***************
*** 1425,1437 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
  		deparseExpr((Expr *) tle->expr, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  		i++;
  	}
  
! 	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
  
--- 1446,1461 ----
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
+ 		else if (is_returning)
+ 			appendStringInfoString(buf, " RETURNING ");
+ 
  		deparseExpr((Expr *) tle->expr, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  		i++;
  	}
  
! 	if (i == 0 && !is_returning)
  		appendStringInfoString(buf, "NULL");
  }
  
***************
*** 1444,1450 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
--- 1468,1474 ----
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, Index target_rel, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
***************
*** 1452,1471 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  	{
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
  
  		/* Deparse outer relation */
! 		initStringInfo(&join_sql_o);
! 		deparseRangeTblRef(&join_sql_o, root,
! 						   fpinfo->outerrel,
! 						   fpinfo->make_outerrel_subquery,
! 						   params_list);
  
  		/* Deparse inner relation */
! 		initStringInfo(&join_sql_i);
! 		deparseRangeTblRef(&join_sql_i, root,
! 						   fpinfo->innerrel,
! 						   fpinfo->make_innerrel_subquery,
! 						   params_list);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
--- 1476,1527 ----
  	{
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
+ 		bool		deparse_outerrel = true;
+ 		bool		deparse_innerrel = true;
+ 
+ 		if (target_rel > 0)
+ 		{
+ 			if (fpinfo->outerrel->reloptkind == RELOPT_BASEREL &&
+ 				fpinfo->outerrel->relid == target_rel)
+ 				deparse_outerrel = false;
+ 			if (fpinfo->innerrel->reloptkind == RELOPT_BASEREL &&
+ 				fpinfo->innerrel->relid == target_rel)
+ 				deparse_innerrel = false;
+ 		}
  
  		/* Deparse outer relation */
! 		if (deparse_outerrel)
! 		{
! 			initStringInfo(&join_sql_o);
! 			deparseRangeTblRef(&join_sql_o, root,
! 							   fpinfo->outerrel,
! 							   fpinfo->make_outerrel_subquery,
! 							   target_rel,
! 							   params_list);
! 		}
! 		if (!deparse_innerrel)
! 		{
! 			Assert(deparse_outerrel);
! 			appendStringInfo(buf, "%s", join_sql_o.data);
! 			return;
! 		}
  
  		/* Deparse inner relation */
! 		if (deparse_innerrel)
! 		{
! 			initStringInfo(&join_sql_i);
! 			deparseRangeTblRef(&join_sql_i, root,
! 							   fpinfo->innerrel,
! 							   fpinfo->make_innerrel_subquery,
! 							   target_rel,
! 							   params_list);
! 		}
! 		if (!deparse_outerrel)
! 		{
! 			Assert(deparse_innerrel);
! 			appendStringInfo(buf, "%s", join_sql_i.data);
! 			return;
! 		}
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
***************
*** 1525,1531 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
   */
  static void
  deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 				   bool make_subquery, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
--- 1581,1587 ----
   */
  static void
  deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 				   bool make_subquery, Index target_rel, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
***************
*** 1535,1555 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (make_subquery)
  	{
! 		List	   *tlist;
  		List	   *retrieved_attrs;
  
! 		tlist = make_tlist_from_pathtarget(foreignrel->reltarget);
  		Assert(tlist != NIL);
  		appendStringInfoChar(buf, '(');
  		deparseSelectStmtForRel(buf, root, foreignrel, tlist,
  								fpinfo->remote_conds, NIL,
  								&retrieved_attrs, params_list);
  		appendStringInfoChar(buf, ')');
  		appendSubselectAlias(buf, fpinfo->relation_index,
! 							 list_length(foreignrel->reltarget->exprs));
  	}
  	else
! 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
  }
  
  /*
--- 1591,1613 ----
  
  	if (make_subquery)
  	{
! 		List	   *tlist = NIL;
  		List	   *retrieved_attrs;
  
! 		tlist = add_to_flat_tlist(tlist, fpinfo->subquery_exprs);
  		Assert(tlist != NIL);
+ 		Assert(list_length(tlist) == list_length(fpinfo->subquery_exprs));
  		appendStringInfoChar(buf, '(');
  		deparseSelectStmtForRel(buf, root, foreignrel, tlist,
  								fpinfo->remote_conds, NIL,
  								&retrieved_attrs, params_list);
  		appendStringInfoChar(buf, ')');
  		appendSubselectAlias(buf, fpinfo->relation_index,
! 							 list_length(tlist));
  	}
  	else
! 		deparseFromExprForRel(buf, root, foreignrel, true, target_rel,
! 							  params_list);
  }
  
  /*
***************
*** 1598,1604 **** getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
  
  	/* Get the column number */
  	i = 1;
! 	foreach(lc, foreignrel->reltarget->exprs)
  	{
  		if (equal(lfirst(lc), (Node *) node))
  		{
--- 1656,1662 ----
  
  	/* Get the column number */
  	i = 1;
! 	foreach(lc, fpinfo->subquery_exprs)
  	{
  		if (equal(lfirst(lc), (Node *) node))
  		{
***************
*** 1805,1810 **** deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 1863,1870 ----
  void
  deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 1812,1832 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
- 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  	int			nestlevel;
  	bool		first;
  	ListCell   *lc;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
! 	context.scanrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
--- 1872,1906 ----
  					   List *returningList,
  					   List **retrieved_attrs)
  {
  	deparse_expr_cxt context;
+ 	List	   *rlist = NIL;
+ 	List	   *target_conds = NIL;
+ 	Relids		wholerows = NULL;
  	int			nestlevel;
  	bool		first;
  	ListCell   *lc;
  
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		rlist = makeExplicitReturningList(rtindex, rel,
+ 										  returningList,
+ 										  fdw_scan_tlist,
+ 										  &wholerows);
+ 		rewriteFromExprForRel(root, foreignrel, rtindex,
+ 							  &target_conds, wholerows);
+ 	}
+ 
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = foreignrel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
***************
*** 1853,1866 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  
  	reset_transmission_modes(nestlevel);
  
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1927,1951 ----
  
  	reset_transmission_modes(nestlevel);
  
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		appendStringInfo(buf, " FROM ");
+ 		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+ 							  params_list);
+ 		remote_conds = list_concat(list_copy(remote_conds), target_conds);
+ 	}
+ 
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 		deparseExplicitReturningList(rlist, retrieved_attrs, &context);
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1895,1917 **** deparseDeleteSql(StringInfo buf, PlannerInfo *root,
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
- 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
! 	context.scanrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
  
  	if (remote_conds)
  	{
--- 1980,2026 ----
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
  	deparse_expr_cxt context;
+ 	List	   *rlist = NIL;
+ 	List	   *target_conds = NIL;
+ 	Relids		wholerows = NULL;
+ 
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		rlist = makeExplicitReturningList(rtindex, rel,
+ 										  returningList,
+ 										  fdw_scan_tlist,
+ 										  &wholerows);
+ 		rewriteFromExprForRel(root, foreignrel, rtindex,
+ 							  &target_conds, wholerows);
+ 	}
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = foreignrel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
+ 
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		appendStringInfo(buf, " USING ");
+ 		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+ 							  params_list);
+ 		remote_conds = list_concat(list_copy(remote_conds), target_conds);
+ 	}
  
  	if (remote_conds)
  	{
***************
*** 1919,1926 **** deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 2028,2038 ----
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 		deparseExplicitReturningList(rlist, retrieved_attrs, &context);
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1960,1965 **** deparseReturningList(StringInfo buf, PlannerInfo *root,
--- 2072,2403 ----
  }
  
  /*
+  * Add a RETURNING clause, if needed, to an UPDATE/DELETE on a join.
+  */
+ static void
+ deparseExplicitReturningList(List *rlist,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context)
+ {
+ 	deparseExplicitTargetList(true, rlist, retrieved_attrs, context);
+ }
+ 
+ /*
+  * Create a RETURNING list, if executing an UPDATE/DELETE on a join remotely.
+  */
+ static List *
+ makeExplicitReturningList(Index rtindex, Relation rel,
+ 						  List *returningList,
+ 						  List **fdw_scan_tlist,
+ 						  Relids *wholerows)
+ {
+ 	TupleDesc	tupdesc = RelationGetDescr(rel);
+ 	List	   *rlist;
+ 	List	   *vars;
+ 	ListCell   *lc;
+ 
+ 	*wholerows = NULL;
+ 
+ 	if (returningList == NIL)
+ 		return NIL;
+ 
+ 	vars = pull_var_clause((Node *) returningList, PVC_INCLUDE_PLACEHOLDERS);
+ 
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		if (IsA(var, Var) &&
+ 			var->varattno == InvalidAttrNumber)
+ 			*wholerows = bms_add_member(*wholerows, var->varno);
+ 	}
+ 
+ 	/* If so, we'll need all non-system columns of the result relation. */
+ 	rlist = NIL;
+ 	if (bms_is_member(rtindex, *wholerows))
+ 	{
+ 		int			i;
+ 
+ 		for (i = 1; i <= tupdesc->natts; i++)
+ 		{
+ 			Form_pg_attribute attr = tupdesc->attrs[i - 1];
+ 			Var		   *var;
+ 
+ 			/* Ignore dropped attributes. */
+ 			if (attr->attisdropped)
+ 				continue;
+ 
+ 			var = makeVar(rtindex,
+ 						  i,
+ 						  attr->atttypid,
+ 						  attr->atttypmod,
+ 						  attr->attcollation,
+ 						  0);
+ 
+ 			rlist = lappend(rlist,
+ 							makeTargetEntry((Expr *) var,
+ 											list_length(rlist) + 1,
+ 											NULL,
+ 											false));
+ 		}
+ 	}
+ 
+ 	/* Now add any remaining columns. */ 
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno == InvalidAttrNumber)
+ 			continue;		/* don't need it */
+ 
+ 		if (tlist_member((Node *) var, rlist))
+ 			continue;		/* already got it */
+ 
+ 		rlist = lappend(rlist,
+ 						makeTargetEntry((Expr *) var,
+ 										list_length(rlist) + 1,
+ 										NULL,
+ 										false));
+ 	}
+ 
+ 	/* rewrite *fdw_scan_tlist */
+ 	if (rlist != NIL)
+ 	{
+ 		List	   *tlist = list_copy(rlist);
+ 
+ 		foreach(lc, *fdw_scan_tlist)
+ 		{
+ 			TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 
+ 			if (tlist_member((Node *) tle->expr, tlist))
+ 				continue;		/* already got it */
+ 
+ 			tlist = lappend(tlist,
+ 							makeTargetEntry(tle->expr,
+ 											list_length(tlist) + 1,
+ 											NULL,
+ 											false));
+ 		}
+ 
+ 		*fdw_scan_tlist = tlist;
+ 	}
+ 
+ 	list_free(vars);
+ 
+ 	return rlist;
+ }
+ 
+ static void
+ rewriteFromExprForRel(PlannerInfo *root, RelOptInfo *foreignrel,
+ 					  Index target_rel, List **target_conds, Relids wholerows)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 	JoinType	jointype = fpinfo->jointype;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_JOINREL);
+ 
+ 	if (jointype == JOIN_INNER)
+ 	{
+ 		Assert(fpinfo->remote_conds == NIL);
+ 
+ 		fpinfo->joinclauses = pullUpTargetConds(foreignrel,
+ 												fpinfo->joinclauses,
+ 												target_rel,
+ 												target_conds);
+ 	}
+ 
+ 	if (fpinfo->make_outerrel_subquery)
+ 	{
+ 		fpinfo->make_outerrel_subquery = !pullUpSubquery(root,
+ 														 outerrel,
+ 														 jointype,
+ 														 target_rel,
+ 														 target_conds,
+ 														 wholerows);
+ 	}
+ 	if (fpinfo->make_innerrel_subquery)
+ 	{
+ 		fpinfo->make_innerrel_subquery = !pullUpSubquery(root,
+ 														 innerrel,
+ 														 jointype,
+ 														 target_rel,
+ 														 target_conds,
+ 														 wholerows);
+ 	}
+ 
+ 	if (outerrel->reloptkind == RELOPT_JOINREL)
+ 		rewriteFromExprForRel(root, outerrel, target_rel, target_conds,
+ 							  wholerows);
+ 	if (innerrel->reloptkind == RELOPT_JOINREL)
+ 		rewriteFromExprForRel(root, innerrel, target_rel, target_conds,
+ 							  wholerows);
+ }
+ 
+ static List *
+ pullUpTargetConds(RelOptInfo *foreignrel, List *joinclauses,
+ 				  Index target_rel, List **target_conds)
+ {
+ 	List	   *result = NIL;
+ 	ListCell   *lc;
+ 
+ 	if (!bms_is_member(target_rel, foreignrel->relids))
+ 		return joinclauses;
+ 
+ 	foreach(lc, joinclauses)
+ 	{
+ 		Node	   *clause = (Node *) lfirst(lc);
+ 		Relids		relids;
+ 
+ 		if (IsA(clause, RestrictInfo))
+ 		{
+ 			RestrictInfo *ri = (RestrictInfo *) clause;
+ 
+ 			clause = (Node *) ri->clause;
+ 		}
+ 
+ 		relids = pull_varnos(clause);
+ 
+ 		if (bms_is_member(target_rel, relids))
+ 			*target_conds = lappend(*target_conds, clause);
+ 		else
+ 			result = lappend(result, clause);
+ 	}
+ 	return result;
+ }
+ 
+ static bool
+ pullUpSubquery(PlannerInfo *root, RelOptInfo *foreignrel, JoinType jointype,
+ 			   Index target_rel, List **target_conds, Relids wholerows)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		Assert(!bms_is_member(target_rel, foreignrel->relids));
+ 		fpinfo->subquery_exprs = getCleanSubqueryExprs(root,
+ 													   foreignrel,
+ 													   fpinfo->subquery_exprs,
+ 													   wholerows);
+ 		return false;
+ 	}
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL);
+ 
+ 	if (foreignrel->relid != target_rel)
+ 	{
+ 		if (isSimpleSubquery(root, foreignrel, jointype, wholerows))
+ 		{
+ 			fpinfo->subquery_exprs = NIL;
+ 			*target_conds = list_concat(*target_conds,
+ 										list_copy(fpinfo->remote_conds));
+ 			return true;
+ 		}
+ 
+ 		Assert(!bms_is_member(target_rel, foreignrel->relids));
+ 		fpinfo->subquery_exprs = getCleanSubqueryExprs(root,
+ 													   foreignrel,
+ 													   fpinfo->subquery_exprs,
+ 													   wholerows);
+ 		return false;
+ 	}
+ 
+ 	fpinfo->subquery_exprs = NIL;
+ 	*target_conds = list_concat(*target_conds,
+ 								list_copy(fpinfo->remote_conds));
+ 	return true;
+ }
+ 
+ static bool
+ isSimpleSubquery(PlannerInfo *root, RelOptInfo *baserel, JoinType jointype,
+ 				 Relids wholerows)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) baserel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	if (jointype == JOIN_FULL && fpinfo->remote_conds)
+ 		return false;
+ 
+ 	if (bms_is_member(baserel->relid, wholerows))
+ 		return false;
+ 
+ 	for (i = baserel->min_attr; i <= 0; i++)
+ 	{
+ 		Relids		relids;
+ 
+ 		if (i == SelfItemPointerAttributeNumber ||
+ 			i == ObjectIdAttributeNumber)
+ 			continue;
+ 
+ 		relids = baserel->attr_needed[i - baserel->min_attr];
+ 
+ 		if (i != 0)
+ 		{
+ 			if (!bms_is_empty(relids))
+ 				return false;
+ 		}
+ 		else
+ 		{
+ 			Assert(bms_is_member(0, relids));
+ 			relids = bms_copy(relids);
+ 			relids = bms_del_member(relids, 0);
+ 
+ 			if (bms_nonempty_difference(relids, baserel->relids))
+ 				return false;
+ 		}
+ 	}
+ 
+ 	foreach(lc, root->placeholder_list)
+ 	{
+ 		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc);
+ 
+ 		if (bms_nonempty_difference(phinfo->ph_needed, baserel->relids) &&
+ 			bms_is_subset(phinfo->ph_eval_at, baserel->relids))
+ 			return false;
+ 	}
+ 
+ 	return true;
+ }
+ 
+ static List *
+ getCleanSubqueryExprs(PlannerInfo *root, RelOptInfo *foreignrel,
+ 					  List *subquery_exprs, Relids wholerows)
+ {
+ 	List	   *result = NIL;
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, subquery_exprs)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		if (IsA(var, Var) && var->varattno == 0)
+ 		{
+ 			if (bms_is_member(var->varno, wholerows))		
+ 				result = lappend(result, var);
+ 			else
+ 			{
+ 				RelOptInfo *baserel = root->simple_rel_array[var->varno];
+ 				Relids		relids = baserel->attr_needed[0 - baserel->min_attr];
+ 
+ 				Assert(bms_is_member(0, relids));
+ 				relids = bms_copy(relids);
+ 			    relids = bms_del_member(relids, 0);
+ 
+ 				if (bms_nonempty_difference(relids, foreignrel->relids))
+ 					result = lappend(result, var);
+ 			}
+ 		}
+ 		else
+ 			result = lappend(result, var);
+ 	}
+ 	return result;
+ }
+ 
+ /*
   * Construct SELECT statement to acquire size in blocks of given relation.
   *
   * Note: we use local definition of block size, not remote definition.
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 4110,4136 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
!                                                                                                                                       QUERY PLAN                                                                                                                                      
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3, c7 = $4 WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.c1, (ft2.c2 + 500), NULL::integer, (ft2.c3 || '_update9'::text), ft2.c4, ft2.c5, ft2.c6, 'ft2       '::character(10), ft2.c8, ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c8, r1.ctid, s2.c1 FROM ("S 1"."T 1" r1 INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))) s2(c1, c2) ON (((r1.c2 = s2.c2)))) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid
!                      Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c8, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))
! (17 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
--- 4110,4122 ----
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
!                                                                                                    QUERY PLAN                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    ->  Foreign Update
!          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 500), c3 = (r1.c3 || '_update9'::text), c7 = 'ft2       '::character(10) FROM "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9))
! (3 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
***************
*** 4253,4279 **** DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
!                                                                                                             QUERY PLAN                                                                                                            
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1.ctid, s2.c1 FROM ("S 1"."T 1" r1 INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))) s2(c1, c2) ON (((r1.c2 = s2.c2)))) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.ctid, ft2.c2
!                      Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))
! (17 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
--- 4239,4251 ----
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
!                                                          QUERY PLAN                                                         
! ----------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    ->  Foreign Delete
!          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2))
! (3 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 212,217 **** typedef struct PgFdwDirectModifyState
--- 212,223 ----
  	PGresult   *result;			/* result for query */
  	int			num_tuples;		/* # of result tuples */
  	int			next_tuple;		/* index of next one to return */
+ 	Relation	resultRel;		/* relcache entry for the target table */
+ 	TupleTableSlot *resultSlot;	/* slot for updated/deleted tuples */
+ 	AttrNumber *attnoMap;		/* array of resultRel attr numbers */
+ 	AttrNumber	ctidAttno;		/* attnum of ctid of result tuple */
+ 	AttrNumber	oidAttno;		/* attnum of oid of result tuple */
+ 	bool		hasSystemCols;	/* are there system columns of resultRel? */
  
  	/* working memory context */
  	MemoryContext temp_cxt;		/* context for per-tuple temporary data */
***************
*** 380,385 **** static void store_returning_result(PgFdwModifyState *fmstate,
--- 386,397 ----
  					   TupleTableSlot *slot, PGresult *res);
  static void execute_dml_stmt(ForeignScanState *node);
  static TupleTableSlot *get_returning_data(ForeignScanState *node);
+ static void init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate);
+ static TupleTableSlot *apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot);
  static void prepare_query_params(PlanState *node,
  					 List *fdw_exprs,
  					 int numParams,
***************
*** 722,727 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 734,742 ----
  
  	/* Set the relation index */
  	fpinfo->relation_index = baserel->relid;
+ 
+ 	/* Initialize the subquery output columns */
+ 	fpinfo->subquery_exprs = NIL;
  }
  
  /*
***************
*** 2162,2167 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2177,2183 ----
  	Relation	rel;
  	StringInfoData sql;
  	ForeignScan *fscan;
+ 	RelOptInfo *foreignrel;
  	List	   *targetAttrs = NIL;
  	List	   *remote_conds;
  	List	   *params_list = NIL;
***************
*** 2193,2205 **** postgresPlanDirectModify(PlannerInfo *root,
  		return false;
  
  	/*
- 	 * We can't handle an UPDATE or DELETE on a foreign join for now.
- 	 */
- 	fscan = (ForeignScan *) subplan;
- 	if (fscan->scan.scanrelid == 0)
- 		return false;
- 
- 	/*
  	 * It's unsafe to update a foreign table directly, if any expressions to
  	 * assign to the target columns are unsafe to evaluate remotely.
  	 */
--- 2209,2214 ----
***************
*** 2238,2243 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2247,2254 ----
  	/*
  	 * Ok, rewrite subplan so as to modify the foreign table directly.
  	 */
+ 	fscan = (ForeignScan *) subplan;
+ 
  	initStringInfo(&sql);
  
  	/*
***************
*** 2247,2252 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2258,2275 ----
  	rel = heap_open(rte->relid, NoLock);
  
  	/*
+ 	 * Get a rel for this foreign table or join.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 	{
+ 		/* We should have a rel for this foreign join. */
+ 		foreignrel = find_join_rel(root, fscan->fs_relids);
+ 		Assert(foreignrel);
+ 	}
+ 	else
+ 		foreignrel = find_base_rel(root, resultRelation);
+ 
+ 	/*
  	 * Extract the baserestrictinfo clauses that can be evaluated remotely.
  	 */
  	remote_conds = (List *) list_nth(fscan->fdw_private,
***************
*** 2265,2270 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2288,2295 ----
  	{
  		case CMD_UPDATE:
  			deparseDirectUpdateSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
+ 								   &fscan->fdw_scan_tlist,
  								   ((Plan *) fscan)->targetlist,
  								   targetAttrs,
  								   remote_conds, &params_list,
***************
*** 2272,2277 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2297,2304 ----
  			break;
  		case CMD_DELETE:
  			deparseDirectDeleteSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
+ 								   &fscan->fdw_scan_tlist,
  								   remote_conds, &params_list,
  								   returningList, &retrieved_attrs);
  			break;
***************
*** 2299,2304 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2326,2337 ----
  									retrieved_attrs,
  									makeInteger(plan->canSetTag));
  
+ 	/*
+ 	 * We don't need the outer subplan.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 		fscan->scan.plan.lefttree = NULL;
+ 
  	heap_close(rel, NoLock);
  	return true;
  }
***************
*** 2313,2318 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2346,2352 ----
  	ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
  	EState	   *estate = node->ss.ps.state;
  	PgFdwDirectModifyState *dmstate;
+ 	Index		rtindex;
  	RangeTblEntry *rte;
  	Oid			userid;
  	ForeignTable *table;
***************
*** 2335,2345 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rte = rt_fetch(fsplan->scan.scanrelid, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	dmstate->rel = node->ss.ss_currentRelation;
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
--- 2369,2383 ----
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rtindex = estate->es_result_relation_info->ri_RangeTableIndex;
! 	rte = rt_fetch(rtindex, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	if (fsplan->scan.scanrelid > 0)
! 		dmstate->rel = node->ss.ss_currentRelation;
! 	else
! 		dmstate->rel = ExecOpenScanRelation(estate, rtindex, eflags);
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
***************
*** 2349,2354 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2387,2401 ----
  	 */
  	dmstate->conn = GetConnection(user, false);
  
+ 	if (fsplan->scan.scanrelid == 0)
+ 	{
+ 		/* Save info about target table. */
+ 		dmstate->resultRel = dmstate->rel;
+ 
+ 		/* rel should be NULL if foreign join. */
+ 		dmstate->rel = NULL;
+ 	}
+ 
  	/* Initialize state variable */
  	dmstate->num_tuples = -1;	/* -1 means not set yet */
  
***************
*** 2369,2375 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(RelationGetDescr(dmstate->rel));
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
--- 2416,2436 ----
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 	{
! 		TupleDesc	tupdesc;
! 
! 		if (fsplan->scan.scanrelid > 0)
! 			tupdesc = RelationGetDescr(dmstate->rel);
! 		else
! 			tupdesc = node->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
! 
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
! 
! 		/* Initialize a filter to extract an updated/deleted tuple. */
! 		if (fsplan->scan.scanrelid == 0)
! 			init_returning_filter(dmstate, fsplan->fdw_scan_tlist,
! 								  rtindex, estate);
! 	}
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
***************
*** 2450,2455 **** postgresEndDirectModify(ForeignScanState *node)
--- 2511,2520 ----
  	ReleaseConnection(dmstate->conn);
  	dmstate->conn = NULL;
  
+ 	/* close the result relation. */
+ 	if (dmstate->resultRel)
+ 		ExecCloseScanRelation(dmstate->resultRel);
+ 
  	/* MemoryContext will be deleted automatically. */
  }
  
***************
*** 3377,3382 **** get_returning_data(ForeignScanState *node)
--- 3442,3448 ----
  	EState	   *estate = node->ss.ps.state;
  	ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
  	TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ 	TupleTableSlot *resultSlot;
  
  	Assert(resultRelInfo->ri_projectReturning);
  
***************
*** 3394,3400 **** get_returning_data(ForeignScanState *node)
--- 3460,3469 ----
  	 * "UPDATE/DELETE .. RETURNING 1" for example.)
  	 */
  	if (!dmstate->has_returning)
+ 	{
  		ExecStoreAllNullTuple(slot);
+ 		resultSlot = slot;
+ 	}
  	else
  	{
  		/*
***************
*** 3410,3416 **** get_returning_data(ForeignScanState *node)
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												NULL,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
--- 3479,3485 ----
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												node,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
***************
*** 3421,3436 **** get_returning_data(ForeignScanState *node)
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = slot;
  
  	return slot;
  }
  
  /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
--- 3490,3677 ----
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
+ 
+ 		/* Get the updated/deleted tuple. */
+ 		if (dmstate->rel)
+ 			resultSlot = slot;
+ 		else
+ 			resultSlot = apply_returning_filter(dmstate, slot);
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = resultSlot;
  
  	return slot;
  }
  
  /*
+  * Initialize a filter to extract an updated/deleted tuple from a scan tuple.
+  */
+ static void
+ init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate)
+ {
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	ListCell   *lc;
+ 	int			i;
+ 
+ 	/* Make a new slot for storing an output tuple. */
+ 	dmstate->resultSlot = ExecInitExtraTupleSlot(estate);
+ 	ExecSetSlotDescriptor(dmstate->resultSlot, resultTupType);
+ 
+ 	/*
+ 	 * Calculate the mapping between the fdw_scan_tlist list's entries and
+ 	 * the updated/deleted tuple's attributes.
+ 	 *
+ 	 * The "map" is an array of the resultRel attribute numbers, i.e. one
+ 	 * entry for every attribute of the updated/deleted tuple.  The value of
+ 	 * this entry is the index of the corresponding entry in fdw_scan_tlist.
+ 	 * We store zero for any attributes that don't have the corresponding
+ 	 * entries in fdw_scan_tlist (i.e. attributes that won't be retrieved
+ 	 * from the remote server), marking that a NULL is needed in the output
+ 	 * tuple.
+ 	 *
+ 	 * Also get the index of the entry for ctid/oid of the updated/deleted
+ 	 * tuple if any.
+ 	 */
+ 	dmstate->attnoMap = (AttrNumber *) palloc0(resultTupType->natts * sizeof(AttrNumber));
+ 
+ 	dmstate->ctidAttno = dmstate->oidAttno = 0;
+ 
+ 	i = 1;
+ 	dmstate->hasSystemCols = false;
+ 	foreach(lc, fdw_scan_tlist)
+ 	{
+ 		if (list_member_int(dmstate->retrieved_attrs, i))
+ 		{
+ 			TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 			Var		   *var = (Var *) tle->expr;
+ 
+ 			if (IsA(var, Var) && var->varno == rtindex)
+ 			{
+ 				int			attrno = var->varattno;
+ 
+ 				if (attrno < 0)
+ 				{
+ 					if (attrno == SelfItemPointerAttributeNumber)
+ 						dmstate->ctidAttno = i;
+ 					else if (attrno == ObjectIdAttributeNumber)
+ 						dmstate->oidAttno = i;
+ 					dmstate->hasSystemCols = true;
+ 				}
+ 				else
+ 				{
+ 					Assert(attrno > 0);
+ 					dmstate->attnoMap[attrno - 1] = i;
+ 				}
+ 			}
+ 		}
+ 		i++;
+ 	}
+ }
+ 
+ /*
+  * Extract and return an updated/deleted tuple from a scan tuple.
+  */
+ static TupleTableSlot *
+ apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot)
+ {
+ 	TupleTableSlot *resultSlot = dmstate->resultSlot;
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	HeapTuple	resultTup;
+ 	Datum	   *values;
+ 	bool	   *isnull;
+ 	Datum	   *old_values;
+ 	bool	   *old_isnull;
+ 	int			i;
+ 
+ 	/*
+ 	 * Extract all the values of the scan tuple.
+ 	 */
+ 	slot_getallattrs(slot);
+ 	old_values = slot->tts_values;
+ 	old_isnull = slot->tts_isnull;
+ 
+ 	/*
+ 	 * Prepare to build a result tuple.
+ 	 */
+ 	ExecClearTuple(resultSlot);
+ 	values = resultSlot->tts_values;
+ 	isnull = resultSlot->tts_isnull;
+ 
+ 	/*
+ 	 * Transpose data into proper fields of the result tuple.
+ 	 */
+ 	for (i = 0; i < resultTupType->natts; i++)
+ 	{
+ 		int			j = dmstate->attnoMap[i];
+ 
+ 		if (j == 0)
+ 		{
+ 			values[i] = (Datum) 0;
+ 			isnull[i] = true;
+ 		}
+ 		else
+ 		{
+ 			values[i] = old_values[j - 1];
+ 			isnull[i] = old_isnull[j - 1];
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * Build the virtual result tuple.
+ 	 */
+ 	ExecStoreVirtualTuple(resultSlot);
+ 
+ 	/*
+ 	 * If we have any system columns to return, install it in t_self.
+ 	 */
+ 	if (dmstate->hasSystemCols)
+ 	{
+ 		resultTup = ExecMaterializeSlot(resultSlot);
+ 
+ 		/*
+ 		 * xmin, xmax, cmin, and tableoid
+ 		 *
+ 		 * Note: we currently don't allow the result relation to appear on
+ 		 * the nullable side of an outer join.
+ 		 */
+ 		HeapTupleHeaderSetXmin(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetXmax(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetCmin(resultTup->t_data, InvalidTransactionId);
+ 
+ 		resultTup->t_tableOid = RelationGetRelid(dmstate->resultRel);
+ 
+ 		/* ctid */
+ 		if (dmstate->ctidAttno)
+ 		{
+ 			ItemPointer ctid = NULL;
+ 
+ 			ctid = (ItemPointer) DatumGetPointer(old_values[dmstate->ctidAttno - 1]);
+ 			resultTup->t_self = *ctid;
+ 		}
+ 
+ 		/* oid */
+ 		if (dmstate->oidAttno)
+ 		{
+ 			Oid			oid = InvalidOid;
+ 
+ 			oid = DatumGetObjectId(old_values[dmstate->oidAttno - 1]);
+ 			HeapTupleSetOid(resultTup, oid);
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * And return the result tuple.
+ 	 */
+ 	return resultSlot;
+ }
+ 
+ /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
***************
*** 4293,4303 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4534,4550 ----
  	 * that relation covered by the subquery for later use of deparser.
  	 */
  	if (fpinfo->make_outerrel_subquery)
+ 	{
  		fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
  												outerrel->relids);
+ 		fpinfo_o->subquery_exprs = outerrel->reltarget->exprs;
+ 	}
  	if (fpinfo->make_innerrel_subquery)
+ 	{
  		fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
  												innerrel->relids);
+ 		fpinfo_i->subquery_exprs = innerrel->reltarget->exprs;
+ 	}
  
  	/*
  	 * For an inner join, all restrictions can be treated alike. Treating the
***************
*** 4420,4425 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4667,4675 ----
  	fpinfo->relation_index =
  		list_length(root->parse->rtable) + list_length(root->join_rel_list);
  
+ 	/* Initialize the subquery output columns */
+ 	fpinfo->subquery_exprs = NIL;
+ 
  	return true;
  }
  
***************
*** 4943,4953 **** make_tuple_from_result_row(PGresult *res,
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
- 		PgFdwScanState *fdw_sstate;
- 
  		Assert(fsstate);
! 		fdw_sstate = (PgFdwScanState *) fsstate->fdw_state;
! 		tupdesc = fdw_sstate->tupdesc;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
--- 5193,5200 ----
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
  		Assert(fsstate);
! 		tupdesc = fsstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 131,136 **** typedef struct PgFdwRelationInfo
--- 131,139 ----
  	 * deparsing the relation as a subquery.
  	 */
  	int			relation_index;
+ 
+ 	/* output columns of the subquery corresponding to the relation */
+ 	List	   *subquery_exprs;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
***************
*** 173,178 **** extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 176,183 ----
  				 List **retrieved_attrs);
  extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 185,190 **** extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
--- 190,197 ----
  				 List **retrieved_attrs);
  extern void deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 1008,1021 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
--- 1008,1021 ----
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
#5Rushabh Lathia
rushabh.lathia@gmail.com
In reply to: Etsuro Fujita (#4)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

Thanks Fujita-san for working on this. I've signed up to review this patch.

Your latest patch doesn't not get apply cleanly apply on master branch.

patching file contrib/postgres_fdw/deparse.c
6 out of 17 hunks FAILED -- saving rejects to file
contrib/postgres_fdw/deparse.c.rej
patching file contrib/postgres_fdw/expected/postgres_fdw.out
2 out of 2 hunks FAILED -- saving rejects to file
contrib/postgres_fdw/expected/postgres_fdw.out.rej
patching file contrib/postgres_fdw/postgres_fdw.c
2 out of 22 hunks FAILED -- saving rejects to file
contrib/postgres_fdw/postgres_fdw.c.rej
patching file contrib/postgres_fdw/postgres_fdw.h
1 out of 3 hunks FAILED -- saving rejects to file
contrib/postgres_fdw/postgres_fdw.h.rej

Please share the patch which get apply clean on master branch.

On Fri, Nov 11, 2016 at 5:00 PM, Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp>
wrote:

On 2016/09/08 19:55, Etsuro Fujita wrote:

On 2016/09/07 13:21, Ashutosh Bapat wrote:

* with the patch:
postgres=# explain verbose delete from ft1 using ft2 where ft1.a =
ft2.a;
QUERY PLAN

------------------------------------------------------------
-----------------------------------------------------------------

Delete on public.ft1 (cost=100.00..102.04 rows=1 width=38)
-> Foreign Delete (cost=100.00..102.04 rows=1 width=38)
Remote SQL: DELETE FROM public.t1 r1 USING (SELECT ROW(a,
b), a FROM public.t2) ss1(c1, c2) WHERE ((r1.a = ss1.c2))
(3 rows)

The underlying scan on t2 requires ROW(a,b) for locking the row for

update/share. But clearly it's not required if the full query is being
pushed down.

Is there a way we can detect that ROW(a,b) is useless

column (not used anywhere in the other parts of the query like
RETURNING, DELETE clause etc.) and eliminate it?

I don't have a clear solution for that yet, but I'll try to remove that

in the next version.

Similarly for a, it's

part of the targetlist of the underlying scan so that the WHERE clause
can be applied on it. But it's not needed if we are pushing down the
query. If we eliminate the targetlist of the query, we could construct a
remote query without having subquery in it, making it more readable.

Will try to do so also.

I addressed this by improving the deparse logic so that a remote query for
performing an UPDATE/DELETE on a join directly on the remote can be created
as proposed if possible. Attached is an updated version of the patch,
which is created on top of the patch set [1]. The patch is still WIP (ie,
needs more comments and regression tests, at least), but any comments would
be gratefully appreciated.

Best regards,
Etsuro Fujita

[1] /messages/by-id/11eafd10-d3f8-ac8a-b64
2-b0e65037c76b%40lab.ntt.co.jp

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

--
Rushabh Lathia

#6Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Rushabh Lathia (#5)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 2016/11/15 19:04, Rushabh Lathia wrote:

Thanks Fujita-san for working on this. I've signed up to review this patch.

Thank you for reviewing the patch!

Your latest patch doesn't not get apply cleanly apply on master branch.

Did you apply the patch set in [1]/messages/by-id/11eafd10-d3f8-ac8a-b642-b0e65037c76b@lab.ntt.co.jp
(postgres-fdw-subquery-support-v4.patch and
postgres-fdw-phv-pushdown-v4.patch in this order) before applying the
latest patch?

Best regards,
Etsuro Fujita

[1]: /messages/by-id/11eafd10-d3f8-ac8a-b642-b0e65037c76b@lab.ntt.co.jp
/messages/by-id/11eafd10-d3f8-ac8a-b642-b0e65037c76b@lab.ntt.co.jp

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

#7Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#6)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On Wed, Nov 16, 2016 at 8:25 AM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/11/15 19:04, Rushabh Lathia wrote:

Thanks Fujita-san for working on this. I've signed up to review this
patch.

Thank you for reviewing the patch!

Your latest patch doesn't not get apply cleanly apply on master branch.

Did you apply the patch set in [1] (postgres-fdw-subquery-support-v4.patch
and postgres-fdw-phv-pushdown-v4.patch in this order) before applying the
latest patch?

I don't see any reason why DML/UPDATE pushdown should depend upon
subquery deparsing or least PHV patch. Combined together they can help
in more cases, but without those patches, we will be able to push-down
more stuff. Probably, we should just restrict push-down only for the
cases when above patches are not needed. That makes reviews easy. Once
those patches get committed, we may add more functionality depending
upon the status of this patch. Does that make sense?

--
Best Wishes,
Ashutosh Bapat
EnterpriseDB Corporation
The Postgres Database Company

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

#8Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#7)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 2016/11/16 13:10, Ashutosh Bapat wrote:

On Wed, Nov 16, 2016 at 8:25 AM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/11/15 19:04, Rushabh Lathia wrote:

Your latest patch doesn't not get apply cleanly apply on master branch.

Did you apply the patch set in [1] (postgres-fdw-subquery-support-v4.patch
and postgres-fdw-phv-pushdown-v4.patch in this order) before applying the
latest patch?

I don't see any reason why DML/UPDATE pushdown should depend upon
subquery deparsing or least PHV patch. Combined together they can help
in more cases, but without those patches, we will be able to push-down
more stuff. Probably, we should just restrict push-down only for the
cases when above patches are not needed. That makes reviews easy. Once
those patches get committed, we may add more functionality depending
upon the status of this patch. Does that make sense?

OK, I'll extract from the patch the minimal part that wouldn't depend on
the two patches.

Best regards,
Etsuro Fujita

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

#9Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#8)
1 attachment(s)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 2016/11/16 16:38, Etsuro Fujita wrote:

On 2016/11/16 13:10, Ashutosh Bapat wrote:

I don't see any reason why DML/UPDATE pushdown should depend upon
subquery deparsing or least PHV patch. Combined together they can help
in more cases, but without those patches, we will be able to push-down
more stuff. Probably, we should just restrict push-down only for the
cases when above patches are not needed. That makes reviews easy. Once
those patches get committed, we may add more functionality depending
upon the status of this patch. Does that make sense?

OK, I'll extract from the patch the minimal part that wouldn't depend on
the two patches.

Here is a patch for that. Todo items are: (1) add more comments and (2)
add more regression tests. I'll do that in the next version.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-more-update-pushdown-v1.patchapplication/x-patch; name=postgres-fdw-more-update-pushdown-v1.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 130,142 **** static void deparseTargetList(StringInfo buf,
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 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);
  static void deparseRelation(StringInfo buf, Relation rel);
--- 130,154 ----
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(bool is_returning,
! 						  List *tlist,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 Index rtindex, Relation rel,
  					 bool trig_after_row,
  					 List *returningList,
  					 List **retrieved_attrs);
+ static void deparseExplicitReturningList(List *rlist,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context);
+ static List *make_explicit_returning_list(Index rtindex, Relation rel,
+ 							 List *returningList,
+ 							 List **fdw_scan_tlist);
+ static void pull_up_target_conditions(PlannerInfo *root, RelOptInfo *foreignrel,
+ 						  Index target_rel, List **target_conds);
+ static void extract_target_conditions(List **joinclauses, Index target_rel,
+ 						  List **target_conds);
  static void deparseColumnRef(StringInfo buf, int varno, int varattno,
  				 PlannerInfo *root, bool qualify_col);
  static void deparseRelation(StringInfo buf, Relation rel);
***************
*** 164,171 **** static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
! static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 					RelOptInfo *joinrel, bool use_alias, List **params_list);
  static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
  static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
--- 176,183 ----
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
! static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, Index target_rel, List **params_list);
  static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
  static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
***************
*** 994,1000 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  		foreignrel->reloptkind == RELOPT_UPPER_REL)
  	{
  		/* For a join relation use the input tlist */
! 		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
  	{
--- 1006,1012 ----
  		foreignrel->reloptkind == RELOPT_UPPER_REL)
  	{
  		/* For a join relation use the input tlist */
! 		deparseExplicitTargetList(false, tlist, retrieved_attrs, context);
  	}
  	else
  	{
***************
*** 1037,1043 **** deparseFromExpr(List *quals, deparse_expr_cxt *context)
  	appendStringInfoString(buf, " FROM ");
  	deparseFromExprForRel(buf, context->root, scanrel,
  						  (bms_num_members(scanrel->relids) > 1),
! 						  context->params_list);
  
  	/* Construct WHERE clause */
  	if (quals != NIL)
--- 1049,1055 ----
  	appendStringInfoString(buf, " FROM ");
  	deparseFromExprForRel(buf, context->root, scanrel,
  						  (bms_num_members(scanrel->relids) > 1),
! 						  (Index) 0, context->params_list);
  
  	/* Construct WHERE clause */
  	if (quals != NIL)
***************
*** 1304,1310 **** get_jointype_name(JoinType jointype)
   * from 1. It has same number of entries as tlist.
   */
  static void
! deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
--- 1316,1324 ----
   * from 1. It has same number of entries as tlist.
   */
  static void
! deparseExplicitTargetList(bool is_returning,
! 						  List *tlist,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
***************
*** 1322,1334 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
  		deparseExpr((Expr *) tle->expr, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  		i++;
  	}
  
! 	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
  
--- 1336,1351 ----
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
+ 		else if (is_returning)
+ 			appendStringInfoString(buf, " RETURNING ");
+ 
  		deparseExpr((Expr *) tle->expr, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  		i++;
  	}
  
! 	if (i == 0 && !is_returning)
  		appendStringInfoString(buf, "NULL");
  }
  
***************
*** 1341,1347 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
--- 1358,1364 ----
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, Index target_rel, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
***************
*** 1351,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		RelOptInfo *rel_i = fpinfo->innerrel;
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
  
! 		/* Deparse outer relation */
! 		initStringInfo(&join_sql_o);
! 		deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
  
! 		/* Deparse inner relation */
! 		initStringInfo(&join_sql_i);
! 		deparseFromExprForRel(&join_sql_i, root, rel_i, true, params_list);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
--- 1368,1419 ----
  		RelOptInfo *rel_i = fpinfo->innerrel;
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
+ 		bool		outer_is_targetrel = false;
+ 		bool		inner_is_targetrel = false;
  
! 		/* Check to see if either input relation is the target relation. */
! 		if (target_rel > 0 && bms_is_member(target_rel, foreignrel->relids))
! 		{
! 			if (fpinfo->outerrel->reloptkind == RELOPT_BASEREL &&
! 				fpinfo->outerrel->relid == target_rel)
! 				outer_is_targetrel = true;
! 			if (fpinfo->innerrel->reloptkind == RELOPT_BASEREL &&
! 				fpinfo->innerrel->relid == target_rel)
! 				inner_is_targetrel = true;
! 		}
! 
! 		/* Deparse outer relation if not the target relation. */
! 		if (!outer_is_targetrel)
! 		{
! 			initStringInfo(&join_sql_o);
! 			deparseFromExprForRel(&join_sql_o, root, rel_o, true, target_rel,
! 								  params_list);
! 
! 			/* If inner relation is the target relation, we are done. */
! 			if (inner_is_targetrel)
! 			{
! 				appendStringInfo(buf, "%s", join_sql_o.data);
! 				return;
! 			}
! 		}
! 
! 		/* Deparse inner relation if not the target relation. */
! 		if (!inner_is_targetrel)
! 		{
! 			initStringInfo(&join_sql_i);
! 			deparseFromExprForRel(&join_sql_i, root, rel_i, true, target_rel,
! 								  params_list);
! 
! 			/* If outer relation is the target relation, we are done. */
! 			if (outer_is_targetrel)
! 			{
! 				appendStringInfo(buf, "%s", join_sql_i.data);
! 				return;
! 			}
! 		}
  
! 		/* Neither of the relations is the target relation. */
! 		Assert(!outer_is_targetrel && !inner_is_targetrel);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
***************
*** 1528,1533 **** deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 1583,1590 ----
  void
  deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 1535,1541 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
- 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  	int			nestlevel;
  	bool		first;
--- 1592,1597 ----
***************
*** 1543,1555 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
! 	context.scanrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
--- 1599,1613 ----
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = foreignrel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
***************
*** 1576,1589 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  
  	reset_transmission_modes(nestlevel);
  
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1634,1672 ----
  
  	reset_transmission_modes(nestlevel);
  
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		List	   *target_conds = NIL;
+ 
+ 		/* Pull up the target relation's conditions into the WHERE clause */
+ 		pull_up_target_conditions(root, foreignrel, rtindex, &target_conds);
+ 		remote_conds = list_concat(target_conds, remote_conds);
+ 
+ 		appendStringInfo(buf, " FROM ");
+ 		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+ 							  params_list);
+ 	}
+ 
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 	{
! 		List	   *rlist = NIL;
! 
! 		/* Create a RETURNING list */
! 		rlist = make_explicit_returning_list(rtindex, rel,
! 											 returningList,
! 											 fdw_scan_tlist);
! 
! 		deparseExplicitReturningList(rlist, retrieved_attrs, &context);
! 	}
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1618,1640 **** deparseDeleteSql(StringInfo buf, PlannerInfo *root,
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
- 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
! 	context.scanrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
  
  	if (remote_conds)
  	{
--- 1701,1739 ----
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
  	deparse_expr_cxt context;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = foreignrel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
+ 
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		List	   *target_conds = NIL;
+ 
+ 		/* Pull up the target relation's conditions into the WHERE clause */
+ 		pull_up_target_conditions(root, foreignrel, rtindex, &target_conds);
+ 		remote_conds = list_concat(target_conds, remote_conds);
+ 
+ 		appendStringInfo(buf, " USING ");
+ 		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+ 							  params_list);
+ 	}
  
  	if (remote_conds)
  	{
***************
*** 1642,1649 **** deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1741,1760 ----
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 	{
! 		List	   *rlist = NIL;
! 
! 		/* Create a RETURNING list */
! 		rlist = make_explicit_returning_list(rtindex, rel,
! 											 returningList,
! 											 fdw_scan_tlist);
! 
! 		deparseExplicitReturningList(rlist, retrieved_attrs, &context);
! 	}
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1683,1688 **** deparseReturningList(StringInfo buf, PlannerInfo *root,
--- 1794,2011 ----
  }
  
  /*
+  * Add a RETURNING clause, if needed, to an UPDATE/DELETE on a join.
+  */
+ static void
+ deparseExplicitReturningList(List *rlist,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context)
+ {
+ 	deparseExplicitTargetList(true, rlist, retrieved_attrs, context);
+ }
+ 
+ /*
+  * Create a RETURNING list for executing an UPDATE/DELETE on a join remotely,
+  * if needed.
+  */
+ static List *
+ make_explicit_returning_list(Index rtindex, Relation rel,
+ 							 List *returningList,
+ 							 List **fdw_scan_tlist)
+ {
+ 	TupleDesc	tupdesc = RelationGetDescr(rel);
+ 	bool		have_wholerow = false;
+ 	List	   *rlist = NIL;
+ 	List	   *vars;
+ 	ListCell   *lc;
+ 
+ 	if (returningList == NIL)
+ 		return NIL;
+ 
+ 	vars = pull_var_clause((Node *) returningList, PVC_INCLUDE_PLACEHOLDERS);
+ 
+ 	/*
+ 	 * If there's a whole-row reference to the target relation, then we'll need
+ 	 * all the columns of the relation.
+ 	 */
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno == InvalidAttrNumber)
+ 		{
+ 			have_wholerow = true;
+ 			break;
+ 		}
+ 	}
+ 
+ 	if (have_wholerow)
+ 	{
+ 		int			i;
+ 
+ 		for (i = 1; i <= tupdesc->natts; i++)
+ 		{
+ 			Form_pg_attribute attr = tupdesc->attrs[i - 1];
+ 			Var		   *var;
+ 
+ 			/* Ignore dropped attributes. */
+ 			if (attr->attisdropped)
+ 				continue;
+ 
+ 			var = makeVar(rtindex,
+ 						  i,
+ 						  attr->atttypid,
+ 						  attr->atttypmod,
+ 						  attr->attcollation,
+ 						  0);
+ 
+ 			rlist = lappend(rlist,
+ 							makeTargetEntry((Expr *) var,
+ 											list_length(rlist) + 1,
+ 											NULL,
+ 											false));
+ 		}
+ 	}
+ 
+ 	/* Now add any remaining columns to rlist. */
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		/*
+ 		 * No need for system columns other than ctid and oid or whole-row
+ 		 * Vars of the target relation, since those are set locally.
+ 		 */
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno <= InvalidAttrNumber &&
+ 			var->varattno != SelfItemPointerAttributeNumber &&
+ 			var->varattno != ObjectIdAttributeNumber)
+ 			continue;		/* don't need it */
+ 
+ 		if (tlist_member((Node *) var, rlist))
+ 			continue;		/* already got it */
+ 
+ 		rlist = lappend(rlist,
+ 						makeTargetEntry((Expr *) var,
+ 										list_length(rlist) + 1,
+ 										NULL,
+ 										false));
+ 	}
+ 
+ 	/* Finally, rewrite *fdw_scan_tlist, if needed. */
+ 	if (rlist != NIL)
+ 	{
+ 		List	   *tlist = list_copy(rlist);
+ 
+ 		foreach(lc, *fdw_scan_tlist)
+ 		{
+ 			TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 
+ 			if (tlist_member((Node *) tle->expr, tlist))
+ 				continue;		/* already got it */
+ 
+ 			tlist = lappend(tlist,
+ 							makeTargetEntry(tle->expr,
+ 											list_length(tlist) + 1,
+ 											NULL,
+ 											false));
+ 		}
+ 		*fdw_scan_tlist = tlist;
+ 	}
+ 
+ 	list_free(vars);
+ 
+ 	return rlist;
+ }
+ 
+ /*
+  * Look for conditions mentioning the target relation in the given join tree,
+  * which will be pulled up into the WHERE clause.
+  */
+ static void
+ pull_up_target_conditions(PlannerInfo *root, RelOptInfo *foreignrel,
+ 						  Index target_rel, List **target_conds)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(bms_is_member(target_rel, foreignrel->relids));
+ 
+ 	/* No work if not an inner join. */
+ 	if (fpinfo->jointype == JOIN_INNER)
+ 	{
+ 		/* The remote_conds should be empty. */
+ 		Assert(fpinfo->remote_conds == NIL);
+ 
+ 		/* Extract conditions from the joinclauses. */
+ 		if ((outerrel->reloptkind == RELOPT_BASEREL &&
+ 			 outerrel->relid == target_rel) ||
+ 			(innerrel->reloptkind == RELOPT_BASEREL &&
+ 			 innerrel->relid == target_rel))
+ 		{
+ 			*target_conds = list_concat(*target_conds,
+ 										list_copy(fpinfo->joinclauses));
+ 			fpinfo->joinclauses = NIL;
+ 		}
+ 		else
+ 			extract_target_conditions(&fpinfo->joinclauses,
+ 									  target_rel, target_conds);
+ 	}
+ 
+ 	/* Recurse into either input relation. */
+ 	if (outerrel->reloptkind == RELOPT_JOINREL &&
+ 		bms_is_member(target_rel, outerrel->relids))
+ 		pull_up_target_conditions(root, outerrel, target_rel, target_conds);
+ 	if (innerrel->reloptkind == RELOPT_JOINREL &&
+ 		bms_is_member(target_rel, innerrel->relids))
+ 		pull_up_target_conditions(root, innerrel, target_rel, target_conds);
+ }
+ 
+ /*
+  * Extract conditions from *joinclauses, separating those that mention
+  * the given relation from those that don't.
+  */
+ static void
+ extract_target_conditions(List **joinclauses,	/* in/out parameters */
+ 						  Index target_rel,
+ 						  List **target_conds	/* output parameters */)
+ {
+ 	List	   *other_conds = NIL;
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, *joinclauses)
+ 	{
+ 		Node	   *clause = (Node *) lfirst(lc);
+ 		Relids		relids;
+ 
+ 		/* Extract clause from RestrictInfo, if needed. */
+ 		if (IsA(clause, RestrictInfo))
+ 		{
+ 			RestrictInfo *ri = (RestrictInfo *) clause;
+ 
+ 			clause = (Node *) ri->clause;
+ 		}
+ 
+ 		/* Retrieve all relids mentioned within the clause. */
+ 		relids = pull_varnos(clause);
+ 
+ 		/* Classify the clause as mentioning the given relation or not. */
+ 		if (bms_is_member(target_rel, relids))
+ 			*target_conds = lappend(*target_conds, clause);
+ 		else
+ 			other_conds = lappend(other_conds, clause);
+ 	}
+ 
+ 	/* Replace *joinclauses. */
+ 	*joinclauses = other_conds;
+ }
+ 
+ /*
   * Construct SELECT statement to acquire size in blocks of given relation.
   *
   * Note: we use local definition of block size, not remote definition.
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 4007,4033 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
!                                                                                                                                                         QUERY PLAN                                                                                                                                                         
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3, c7 = $4 WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.c1, (ft2.c2 + 500), NULL::integer, (ft2.c3 || '_update9'::text), ft2.c4, ft2.c5, ft2.c6, 'ft2       '::character(10), ft2.c8, ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c8, r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9)))) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid
!                      Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c8, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))
! (17 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
--- 4007,4019 ----
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
!                                                                                                    QUERY PLAN                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    ->  Foreign Update
!          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 500), c3 = (r1.c3 || '_update9'::text), c7 = 'ft2       '::character(10) FROM "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9))
! (3 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
***************
*** 4150,4176 **** DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
!                                                                                                                               QUERY PLAN                                                                                                                               
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2)))) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.ctid, ft2.c2
!                      Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))
! (17 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
--- 4136,4148 ----
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
!                                                          QUERY PLAN                                                         
! ----------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    ->  Foreign Delete
!          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2))
! (3 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 211,216 **** typedef struct PgFdwDirectModifyState
--- 211,222 ----
  	PGresult   *result;			/* result for query */
  	int			num_tuples;		/* # of result tuples */
  	int			next_tuple;		/* index of next one to return */
+ 	Relation	resultRel;		/* relcache entry for the target table */
+ 	TupleTableSlot *resultSlot;	/* slot for updated/deleted tuples */
+ 	AttrNumber *attnoMap;		/* array of resultRel attr numbers */
+ 	AttrNumber	ctidAttno;		/* attnum of ctid of result tuple */
+ 	AttrNumber	oidAttno;		/* attnum of oid of result tuple */
+ 	bool		hasSystemCols;	/* are there system columns of resultRel? */
  
  	/* working memory context */
  	MemoryContext temp_cxt;		/* context for per-tuple temporary data */
***************
*** 379,384 **** static void store_returning_result(PgFdwModifyState *fmstate,
--- 385,396 ----
  					   TupleTableSlot *slot, PGresult *res);
  static void execute_dml_stmt(ForeignScanState *node);
  static TupleTableSlot *get_returning_data(ForeignScanState *node);
+ static void init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate);
+ static TupleTableSlot *apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot);
  static void prepare_query_params(PlanState *node,
  					 List *fdw_exprs,
  					 int numParams,
***************
*** 2108,2113 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2120,2126 ----
  	Relation	rel;
  	StringInfoData sql;
  	ForeignScan *fscan;
+ 	RelOptInfo *foreignrel;
  	List	   *targetAttrs = NIL;
  	List	   *remote_conds;
  	List	   *params_list = NIL;
***************
*** 2139,2151 **** postgresPlanDirectModify(PlannerInfo *root,
  		return false;
  
  	/*
- 	 * We can't handle an UPDATE or DELETE on a foreign join for now.
- 	 */
- 	fscan = (ForeignScan *) subplan;
- 	if (fscan->scan.scanrelid == 0)
- 		return false;
- 
- 	/*
  	 * It's unsafe to update a foreign table directly, if any expressions to
  	 * assign to the target columns are unsafe to evaluate remotely.
  	 */
--- 2152,2157 ----
***************
*** 2184,2189 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2190,2197 ----
  	/*
  	 * Ok, rewrite subplan so as to modify the foreign table directly.
  	 */
+ 	fscan = (ForeignScan *) subplan;
+ 
  	initStringInfo(&sql);
  
  	/*
***************
*** 2193,2198 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2201,2218 ----
  	rel = heap_open(rte->relid, NoLock);
  
  	/*
+ 	 * Get a rel for this foreign table or join.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 	{
+ 		/* We should have a rel for this foreign join. */
+ 		foreignrel = find_join_rel(root, fscan->fs_relids);
+ 		Assert(foreignrel);
+ 	}
+ 	else
+ 		foreignrel = find_base_rel(root, resultRelation);
+ 
+ 	/*
  	 * Extract the baserestrictinfo clauses that can be evaluated remotely.
  	 */
  	remote_conds = (List *) list_nth(fscan->fdw_private,
***************
*** 2211,2216 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2231,2238 ----
  	{
  		case CMD_UPDATE:
  			deparseDirectUpdateSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
+ 								   &fscan->fdw_scan_tlist,
  								   ((Plan *) fscan)->targetlist,
  								   targetAttrs,
  								   remote_conds, &params_list,
***************
*** 2218,2223 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2240,2247 ----
  			break;
  		case CMD_DELETE:
  			deparseDirectDeleteSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
+ 								   &fscan->fdw_scan_tlist,
  								   remote_conds, &params_list,
  								   returningList, &retrieved_attrs);
  			break;
***************
*** 2245,2250 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2269,2280 ----
  									retrieved_attrs,
  									makeInteger(plan->canSetTag));
  
+ 	/*
+ 	 * We don't need the outer subplan.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 		fscan->scan.plan.lefttree = NULL;
+ 
  	heap_close(rel, NoLock);
  	return true;
  }
***************
*** 2259,2264 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2289,2295 ----
  	ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
  	EState	   *estate = node->ss.ps.state;
  	PgFdwDirectModifyState *dmstate;
+ 	Index		rtindex;
  	RangeTblEntry *rte;
  	Oid			userid;
  	ForeignTable *table;
***************
*** 2281,2291 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rte = rt_fetch(fsplan->scan.scanrelid, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	dmstate->rel = node->ss.ss_currentRelation;
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
--- 2312,2326 ----
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rtindex = estate->es_result_relation_info->ri_RangeTableIndex;
! 	rte = rt_fetch(rtindex, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	if (fsplan->scan.scanrelid > 0)
! 		dmstate->rel = node->ss.ss_currentRelation;
! 	else
! 		dmstate->rel = ExecOpenScanRelation(estate, rtindex, eflags);
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
***************
*** 2295,2300 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2330,2344 ----
  	 */
  	dmstate->conn = GetConnection(user, false);
  
+ 	if (fsplan->scan.scanrelid == 0)
+ 	{
+ 		/* Save info about target table. */
+ 		dmstate->resultRel = dmstate->rel;
+ 
+ 		/* rel should be NULL if foreign join. */
+ 		dmstate->rel = NULL;
+ 	}
+ 
  	/* Initialize state variable */
  	dmstate->num_tuples = -1;	/* -1 means not set yet */
  
***************
*** 2315,2321 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(RelationGetDescr(dmstate->rel));
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
--- 2359,2379 ----
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 	{
! 		TupleDesc	tupdesc;
! 
! 		if (fsplan->scan.scanrelid > 0)
! 			tupdesc = RelationGetDescr(dmstate->rel);
! 		else
! 			tupdesc = node->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
! 
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
! 
! 		/* Initialize a filter to extract an updated/deleted tuple. */
! 		if (fsplan->scan.scanrelid == 0)
! 			init_returning_filter(dmstate, fsplan->fdw_scan_tlist,
! 								  rtindex, estate);
! 	}
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
***************
*** 2396,2401 **** postgresEndDirectModify(ForeignScanState *node)
--- 2454,2463 ----
  	ReleaseConnection(dmstate->conn);
  	dmstate->conn = NULL;
  
+ 	/* close the result relation. */
+ 	if (dmstate->resultRel)
+ 		ExecCloseScanRelation(dmstate->resultRel);
+ 
  	/* MemoryContext will be deleted automatically. */
  }
  
***************
*** 3323,3328 **** get_returning_data(ForeignScanState *node)
--- 3385,3391 ----
  	EState	   *estate = node->ss.ps.state;
  	ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
  	TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ 	TupleTableSlot *resultSlot;
  
  	Assert(resultRelInfo->ri_projectReturning);
  
***************
*** 3340,3346 **** get_returning_data(ForeignScanState *node)
--- 3403,3412 ----
  	 * "UPDATE/DELETE .. RETURNING 1" for example.)
  	 */
  	if (!dmstate->has_returning)
+ 	{
  		ExecStoreAllNullTuple(slot);
+ 		resultSlot = slot;
+ 	}
  	else
  	{
  		/*
***************
*** 3356,3362 **** get_returning_data(ForeignScanState *node)
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												NULL,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
--- 3422,3428 ----
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												node,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
***************
*** 3367,3382 **** get_returning_data(ForeignScanState *node)
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = slot;
  
  	return slot;
  }
  
  /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
--- 3433,3620 ----
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
+ 
+ 		/* Get the updated/deleted tuple. */
+ 		if (dmstate->rel)
+ 			resultSlot = slot;
+ 		else
+ 			resultSlot = apply_returning_filter(dmstate, slot);
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = resultSlot;
  
  	return slot;
  }
  
  /*
+  * Initialize a filter to extract an updated/deleted tuple from a scan tuple.
+  */
+ static void
+ init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate)
+ {
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	ListCell   *lc;
+ 	int			i;
+ 
+ 	/* Make a new slot for storing an output tuple. */
+ 	dmstate->resultSlot = ExecInitExtraTupleSlot(estate);
+ 
+ 	ExecSetSlotDescriptor(dmstate->resultSlot, resultTupType);
+ 
+ 	/*
+ 	 * Calculate the mapping between the fdw_scan_tlist list's entries and
+ 	 * the updated/deleted tuple's attributes.
+ 	 *
+ 	 * The "map" is an array of the resultRel attribute numbers, i.e. one
+ 	 * entry for every attribute of the updated/deleted tuple.  The value of
+ 	 * this entry is the index of the corresponding entry in fdw_scan_tlist.
+ 	 * We store zero for any attributes that don't have the corresponding
+ 	 * entries in fdw_scan_tlist (i.e. attributes that won't be retrieved
+ 	 * from the remote server), marking that a NULL is needed in the output
+ 	 * tuple.
+ 	 *
+ 	 * Also get the index of the entry for ctid/oid of the updated/deleted
+ 	 * tuple if any.
+ 	 */
+ 	dmstate->attnoMap = (AttrNumber *) palloc0(resultTupType->natts * sizeof(AttrNumber));
+ 
+ 	dmstate->ctidAttno = dmstate->oidAttno = 0;
+ 
+ 	i = 1;
+ 	dmstate->hasSystemCols = false;
+ 	foreach(lc, fdw_scan_tlist)
+ 	{
+ 		if (list_member_int(dmstate->retrieved_attrs, i))
+ 		{
+ 			TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 			Var		   *var = (Var *) tle->expr;
+ 
+ 			if (IsA(var, Var) && var->varno == rtindex)
+ 			{
+ 				int			attrno = var->varattno;
+ 
+ 				if (attrno < 0)
+ 				{
+ 					if (attrno == SelfItemPointerAttributeNumber)
+ 						dmstate->ctidAttno = i;
+ 					else if (attrno == ObjectIdAttributeNumber)
+ 						dmstate->oidAttno = i;
+ 					dmstate->hasSystemCols = true;
+ 				}
+ 				else
+ 				{
+ 					Assert(attrno > 0);
+ 					dmstate->attnoMap[attrno - 1] = i;
+ 				}
+ 			}
+ 		}
+ 		i++;
+ 	}
+ }
+ 
+ /*
+  * Extract and return an updated/deleted tuple from a scan tuple.
+  */
+ static TupleTableSlot *
+ apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot)
+ {
+ 	TupleTableSlot *resultSlot = dmstate->resultSlot;
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	HeapTuple	resultTup;
+ 	Datum	   *values;
+ 	bool	   *isnull;
+ 	Datum	   *old_values;
+ 	bool	   *old_isnull;
+ 	int			i;
+ 
+ 	/*
+ 	 * Extract all the values of the scan tuple.
+ 	 */
+ 	slot_getallattrs(slot);
+ 	old_values = slot->tts_values;
+ 	old_isnull = slot->tts_isnull;
+ 
+ 	/*
+ 	 * Prepare to build a result tuple.
+ 	 */
+ 	ExecClearTuple(resultSlot);
+ 	values = resultSlot->tts_values;
+ 	isnull = resultSlot->tts_isnull;
+ 
+ 	/*
+ 	 * Transpose data into proper fields of the result tuple.
+ 	 */
+ 	for (i = 0; i < resultTupType->natts; i++)
+ 	{
+ 		int			j = dmstate->attnoMap[i];
+ 
+ 		if (j == 0)
+ 		{
+ 			values[i] = (Datum) 0;
+ 			isnull[i] = true;
+ 		}
+ 		else
+ 		{
+ 			values[i] = old_values[j - 1];
+ 			isnull[i] = old_isnull[j - 1];
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * Build the virtual result tuple.
+ 	 */
+ 	ExecStoreVirtualTuple(resultSlot);
+ 
+ 	/*
+ 	 * If we have any system columns to return, install it in t_self.
+ 	 */
+ 	if (dmstate->hasSystemCols)
+ 	{
+ 		resultTup = ExecMaterializeSlot(resultSlot);
+ 
+ 		/* ctid */
+ 		if (dmstate->ctidAttno)
+ 		{
+ 			ItemPointer ctid = NULL;
+ 
+ 			ctid = (ItemPointer) DatumGetPointer(old_values[dmstate->ctidAttno - 1]);
+ 			resultTup->t_self = *ctid;
+ 		}
+ 
+ 		/* oid */
+ 		if (dmstate->oidAttno)
+ 		{
+ 			Oid			oid = InvalidOid;
+ 
+ 			oid = DatumGetObjectId(old_values[dmstate->oidAttno - 1]);
+ 			HeapTupleSetOid(resultTup, oid);
+ 		}
+ 
+ 		/*
+ 		 * xmin, xmax, cmin, and tableoid  (Note that since we currently
+ 		 * don't allow the result relation to appear on the nullable side
+ 		 * of an outer join, they can't go to NULL.)
+ 		 */
+ 		HeapTupleHeaderSetXmin(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetXmax(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetCmin(resultTup->t_data, InvalidTransactionId);
+ 
+ 		resultTup->t_tableOid = RelationGetRelid(dmstate->resultRel);
+ 	}
+ 
+ 	/*
+ 	 * And return the result tuple.
+ 	 */
+ 	return resultSlot;
+ }
+ 
+ /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
***************
*** 4796,4806 **** make_tuple_from_result_row(PGresult *res,
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
- 		PgFdwScanState *fdw_sstate;
- 
  		Assert(fsstate);
! 		fdw_sstate = (PgFdwScanState *) fsstate->fdw_state;
! 		tupdesc = fdw_sstate->tupdesc;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
--- 5034,5041 ----
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
  		Assert(fsstate);
! 		tupdesc = fsstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 137,142 **** extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 137,144 ----
  				 List **retrieved_attrs);
  extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 149,154 **** extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
--- 151,158 ----
  				 List **retrieved_attrs);
  extern void deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
+ 					   List **fdw_scan_tlist,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 987,1000 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
--- 987,1000 ----
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
#10Rushabh Lathia
rushabh.lathia@gmail.com
In reply to: Etsuro Fujita (#9)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

I started reviewing the patch and here are few initial review points and
questions for you.

1)
-static void deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
+static void deparseExplicitTargetList(bool is_returning,
+                          List *tlist,
+                          List **retrieved_attrs,
                           deparse_expr_cxt *context);

Any particular reason of inserting new argument as 1st argunment? In general
we add new flags at the end or before the out param for the existing
function.

2)
-static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
-                    RelOptInfo *joinrel, bool use_alias, List
**params_list);
+static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel,
+                      bool use_alias, Index target_rel, List
**params_list);

Going beyond 80 character length

3)
 static void
-deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
+deparseExplicitTargetList(bool is_returning,
+                          List *tlist,
+                          List **retrieved_attrs,
                           deparse_expr_cxt *context)

This looks bit wired to me - as comment for the deparseExplicitTargetList()
function name and comments says different then what its trying to do when
is_returning flag is passed. Either function comment need to be change or
may be separate function fo deparse returning list for join relation?

Is it possible to teach deparseReturningList() to deparse the returning
list for the joinrel?

4)

+    if (foreignrel->reloptkind == RELOPT_JOINREL)
+    {
+        List       *rlist = NIL;
+
+        /* Create a RETURNING list */
+        rlist = make_explicit_returning_list(rtindex, rel,
+                                             returningList,
+                                             fdw_scan_tlist);
+
+        deparseExplicitReturningList(rlist, retrieved_attrs, &context);
+    }
+    else
+        deparseReturningList(buf, root, rtindex, rel, false,
+                             returningList, retrieved_attrs);

The code will be more centralized if we add the logic of extracting
returninglist
for JOINREL inside deparseReturningList() only, isn't it?

5) make_explicit_returning_list() pull the var list from the returningList
and
build the targetentry for the returning list and at the end rewrites the
fdw_scan_tlist.

AFAIK, in case of DML - which is going to pushdown to the remote server
ideally fdw_scan_tlist should be same as returning list, as final output
for the query is query will be RETUNING list only. isn't that true?

If yes, then why can't we directly replace the fdw_scan_tlist with the
returning
list, rather then make_explicit_returning_list()?

Also I think rewriting the fdw_scan_tlist should happen into postgres_fdw.c
-
rather then deparse.c.

6) Test coverage for the returning list is missing.

On Fri, Nov 18, 2016 at 8:56 AM, Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp>
wrote:

On 2016/11/16 16:38, Etsuro Fujita wrote:

On 2016/11/16 13:10, Ashutosh Bapat wrote:

I don't see any reason why DML/UPDATE pushdown should depend upon
subquery deparsing or least PHV patch. Combined together they can help
in more cases, but without those patches, we will be able to push-down
more stuff. Probably, we should just restrict push-down only for the
cases when above patches are not needed. That makes reviews easy. Once
those patches get committed, we may add more functionality depending
upon the status of this patch. Does that make sense?

OK, I'll extract from the patch the minimal part that wouldn't depend on

the two patches.

Here is a patch for that. Todo items are: (1) add more comments and (2)
add more regression tests. I'll do that in the next version.

Best regards,
Etsuro Fujita

--
Rushabh Lathia

#11Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Rushabh Lathia (#10)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

Hi Rushabh,

On 2016/11/22 19:05, Rushabh Lathia wrote:

I started reviewing the patch and here are few initial review points and
questions for you.

Thanks for the review!

1)
-static void deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
+static void deparseExplicitTargetList(bool is_returning,
+                          List *tlist,
+                          List **retrieved_attrs,
deparse_expr_cxt *context);

Any particular reason of inserting new argument as 1st argunment? In general
we add new flags at the end or before the out param for the existing
function.

No, I don't. I think the *context should be the last argument as in
other functions in deparse.c. How about inserting that before the out
param **retrieved_attrs, like this?

static void
deparseExplicitTargetList(List *tlist,
bool is_returning,
List **retrieved_attrs,
deparse_expr_cxt *context);

2)
-static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
-                    RelOptInfo *joinrel, bool use_alias, List
**params_list);
+static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel,
+                      bool use_alias, Index target_rel, List
**params_list);

Going beyond 80 character length

Will fix.

3)
static void
-deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
+deparseExplicitTargetList(bool is_returning,
+                          List *tlist,
+                          List **retrieved_attrs,
deparse_expr_cxt *context)

This looks bit wired to me - as comment for the deparseExplicitTargetList()
function name and comments says different then what its trying to do when
is_returning flag is passed. Either function comment need to be change or
may be separate function fo deparse returning list for join relation?

Is it possible to teach deparseReturningList() to deparse the returning
list for the joinrel?

I don't think it's possible to do that because deparseReturningList uses
deparseTargetList internally, which only allows us to emit a target list
for a baserel. For example, deparseReturningList can't handle a
returning list that contains join outputs like this:

postgres=# explain verbose delete from ft1 using ft2 where ft1.a = ft2.a
returning ft1.*, ft2.*;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
Delete on public.ft1 (cost=100.00..102.06 rows=1 width=46)
Output: ft1.a, ft1.b, ft2.a, ft2.b
-> Foreign Delete (cost=100.00..102.06 rows=1 width=46)
Remote SQL: DELETE FROM public.t1 r1 USING public.t2 r2 WHERE
((r1.a = r2.a)) RETURNING r1.a, r1.b, r2.a, r2.b
(4 rows)

I'd like to change the comment for deparseExplicitTargetList.

4)

+    if (foreignrel->reloptkind == RELOPT_JOINREL)
+    {
+        List       *rlist = NIL;
+
+        /* Create a RETURNING list */
+        rlist = make_explicit_returning_list(rtindex, rel,
+                                             returningList,
+                                             fdw_scan_tlist);
+
+        deparseExplicitReturningList(rlist, retrieved_attrs, &context);
+    }
+    else
+        deparseReturningList(buf, root, rtindex, rel, false,
+                             returningList, retrieved_attrs);

The code will be more centralized if we add the logic of extracting
returninglist
for JOINREL inside deparseReturningList() only, isn't it?

You are right. Will do.

5) make_explicit_returning_list() pull the var list from the
returningList and
build the targetentry for the returning list and at the end rewrites the
fdw_scan_tlist.

AFAIK, in case of DML - which is going to pushdown to the remote server
ideally fdw_scan_tlist should be same as returning list, as final output
for the query is query will be RETUNING list only. isn't that true?

That would be true. But the fdw_scan_tlist passed from the core would
contain junk columns inserted by the rewriter and planner work, such as
CTID for the target table and whole-row Vars for other tables specified
in the FROM clause of an UPDATE or the USING clause of a DELETE. So, I
created the patch so that the pushed-down UPDATE/DELETE retrieves only
the data needed for the RETURNING calculation from the remote server,
not the whole fdw_scan_tlist data.

If yes, then why can't we directly replace the fdw_scan_tlist with the
returning
list, rather then make_explicit_returning_list()?

I think we could do that if we modified the core (maybe the executor
part). I'm not sure that's a good idea, though.

Also I think rewriting the fdw_scan_tlist should happen into
postgres_fdw.c -
rather then deparse.c.

Will try.

6) Test coverage for the returning list is missing.

Will add.

Best regards,
Etsuro Fujita

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

#12Rushabh Lathia
rushabh.lathia@gmail.com
In reply to: Etsuro Fujita (#11)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On Tue, Nov 22, 2016 at 6:24 PM, Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp>
wrote:

Hi Rushabh,

On 2016/11/22 19:05, Rushabh Lathia wrote:

I started reviewing the patch and here are few initial review points and
questions for you.

Thanks for the review!

1)

-static void deparseExplicitTargetList(List *tlist, List
**retrieved_attrs,
+static void deparseExplicitTargetList(bool is_returning,
+                          List *tlist,
+                          List **retrieved_attrs,
deparse_expr_cxt *context);

Any particular reason of inserting new argument as 1st argunment? In
general
we add new flags at the end or before the out param for the existing
function.

No, I don't. I think the *context should be the last argument as in other
functions in deparse.c. How about inserting that before the out param
**retrieved_attrs, like this?

static void
deparseExplicitTargetList(List *tlist,
bool is_returning,
List **retrieved_attrs,
deparse_expr_cxt *context);

Yes, adding it before retrieved_attrs would be good.

2)

-static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
-                    RelOptInfo *joinrel, bool use_alias, List
**params_list);
+static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
RelOptInfo *foreignrel,
+                      bool use_alias, Index target_rel, List
**params_list);

Going beyond 80 character length

Will fix.

3)

static void
-deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
+deparseExplicitTargetList(bool is_returning,
+                          List *tlist,
+                          List **retrieved_attrs,
deparse_expr_cxt *context)

This looks bit wired to me - as comment for the
deparseExplicitTargetList()
function name and comments says different then what its trying to do when
is_returning flag is passed. Either function comment need to be change or
may be separate function fo deparse returning list for join relation?

Is it possible to teach deparseReturningList() to deparse the returning
list for the joinrel?

I don't think it's possible to do that because deparseReturningList uses
deparseTargetList internally, which only allows us to emit a target list
for a baserel. For example, deparseReturningList can't handle a returning
list that contains join outputs like this:

postgres=# explain verbose delete from ft1 using ft2 where ft1.a = ft2.a
returning ft1.*, ft2.*;
QUERY PLAN
------------------------------------------------------------
------------------------------------------------------------
Delete on public.ft1 (cost=100.00..102.06 rows=1 width=46)
Output: ft1.a, ft1.b, ft2.a, ft2.b
-> Foreign Delete (cost=100.00..102.06 rows=1 width=46)
Remote SQL: DELETE FROM public.t1 r1 USING public.t2 r2 WHERE
((r1.a = r2.a)) RETURNING r1.a, r1.b, r2.a, r2.b
(4 rows)

I'd like to change the comment for deparseExplicitTargetList.

4)

+    if (foreignrel->reloptkind == RELOPT_JOINREL)
+    {
+        List       *rlist = NIL;
+
+        /* Create a RETURNING list */
+        rlist = make_explicit_returning_list(rtindex, rel,
+                                             returningList,
+                                             fdw_scan_tlist);
+
+        deparseExplicitReturningList(rlist, retrieved_attrs, &context);
+    }
+    else
+        deparseReturningList(buf, root, rtindex, rel, false,
+                             returningList, retrieved_attrs);

The code will be more centralized if we add the logic of extracting
returninglist
for JOINREL inside deparseReturningList() only, isn't it?

You are right. Will do.

5) make_explicit_returning_list() pull the var list from the

returningList and
build the targetentry for the returning list and at the end rewrites the
fdw_scan_tlist.

AFAIK, in case of DML - which is going to pushdown to the remote server
ideally fdw_scan_tlist should be same as returning list, as final output
for the query is query will be RETUNING list only. isn't that true?

That would be true. But the fdw_scan_tlist passed from the core would
contain junk columns inserted by the rewriter and planner work, such as
CTID for the target table and whole-row Vars for other tables specified in
the FROM clause of an UPDATE or the USING clause of a DELETE. So, I
created the patch so that the pushed-down UPDATE/DELETE retrieves only the
data needed for the RETURNING calculation from the remote server, not the
whole fdw_scan_tlist data.

Yes, I noticed that fdw_scan_tlist have CTID for the target table and
whole-raw vars for
other tables specified in the FROM clause of the DML. But I was thinking
isn't it possible
to create new fdw_scan_tlist once we found that DML is direct pushable to
foreign server?
I tried quickly doing that - but later its was throwing "variable not found
in subplan target list"
from set_foreignscan_references().

If yes, then why can't we directly replace the fdw_scan_tlist with the

returning
list, rather then make_explicit_returning_list()?

I think we could do that if we modified the core (maybe the executor
part). I'm not sure that's a good idea, though.

Yes modifying core is not good idea. But just want to understand why you
think that this need need to modify core?

Also I think rewriting the fdw_scan_tlist should happen into

postgres_fdw.c -
rather then deparse.c.

Will try.

That would be good.

6) Test coverage for the returning list is missing.

Will add.

Thanks.

Best regards,
Etsuro Fujita

--
Rushabh Lathia

#13Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Rushabh Lathia (#12)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 2016/11/23 20:28, Rushabh Lathia wrote:

On Tue, Nov 22, 2016 at 6:24 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp <mailto:fujita.etsuro@lab.ntt.co.jp>> wrote:

1)
-static void deparseExplicitTargetList(List *tlist, List
**retrieved_attrs,
+static void deparseExplicitTargetList(bool is_returning,
+                          List *tlist,
+                          List **retrieved_attrs,
deparse_expr_cxt *context);

Any particular reason of inserting new argument as 1st
argunment? In general
we add new flags at the end or before the out param for the existing
function.

No, I don't. I think the *context should be the last argument as in
other functions in deparse.c. How about inserting that before the
out param **retrieved_attrs, like this?

static void
deparseExplicitTargetList(List *tlist,
bool is_returning,
List **retrieved_attrs,
deparse_expr_cxt *context);

Yes, adding it before retrieved_attrs would be good.

OK, will do.

5) make_explicit_returning_list() pull the var list from the
returningList and
build the targetentry for the returning list and at the end
rewrites the
fdw_scan_tlist.

AFAIK, in case of DML - which is going to pushdown to the remote
server
ideally fdw_scan_tlist should be same as returning list, as
final output
for the query is query will be RETUNING list only. isn't that true?

That would be true. But the fdw_scan_tlist passed from the core
would contain junk columns inserted by the rewriter and planner
work, such as CTID for the target table and whole-row Vars for other
tables specified in the FROM clause of an UPDATE or the USING clause
of a DELETE. So, I created the patch so that the pushed-down
UPDATE/DELETE retrieves only the data needed for the RETURNING
calculation from the remote server, not the whole fdw_scan_tlist data.

Yes, I noticed that fdw_scan_tlist have CTID for the target table and
whole-raw vars for
other tables specified in the FROM clause of the DML. But I was thinking
isn't it possible
to create new fdw_scan_tlist once we found that DML is direct pushable
to foreign server?
I tried quickly doing that - but later its was throwing "variable not
found in subplan target list"
from set_foreignscan_references().

If yes, then why can't we directly replace the fdw_scan_tlist
with the
returning
list, rather then make_explicit_returning_list()?

I think we could do that if we modified the core (maybe the executor
part). I'm not sure that's a good idea, though.

Yes modifying core is not good idea. But just want to understand why you
think that this need need to modify core?

Sorry, I don't remember that. Will check.

I'd like to move this to the next CF.

Thank you for the comments!

Best regards,
Etsuro Fujita

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

#14Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#13)
1 attachment(s)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 2016/11/30 17:29, Etsuro Fujita wrote:

On 2016/11/23 20:28, Rushabh Lathia wrote:

I wrote:

How about inserting that before the
out param **retrieved_attrs, like this?

static void
deparseExplicitTargetList(List *tlist,
bool is_returning,
List **retrieved_attrs,
deparse_expr_cxt *context);

Yes, adding it before retrieved_attrs would be good.

OK, will do.

Done.

You wrote:

5) make_explicit_returning_list() pull the var list from the
returningList and
build the targetentry for the returning list and at the end
rewrites the
fdw_scan_tlist.

AFAIK, in case of DML - which is going to pushdown to the remote
server
ideally fdw_scan_tlist should be same as returning list, as
final output
for the query is query will be RETUNING list only. isn't that
true?

I wrote:

That would be true. But the fdw_scan_tlist passed from the core
would contain junk columns inserted by the rewriter and planner
work, such as CTID for the target table and whole-row Vars for other
tables specified in the FROM clause of an UPDATE or the USING clause
of a DELETE. So, I created the patch so that the pushed-down
UPDATE/DELETE retrieves only the data needed for the RETURNING
calculation from the remote server, not the whole fdw_scan_tlist
data.

Yes, I noticed that fdw_scan_tlist have CTID for the target table and
whole-raw vars for
other tables specified in the FROM clause of the DML. But I was thinking
isn't it possible
to create new fdw_scan_tlist once we found that DML is direct pushable
to foreign server?
I tried quickly doing that - but later its was throwing "variable not
found in subplan target list"
from set_foreignscan_references().

We could probably avoid that error by replacing the targetlist of the
subplan with fdw_scan_tlist, but that wouldn't be enough ...

You wrote:

If yes, then why can't we directly replace the fdw_scan_tlist
with the
returning
list, rather then make_explicit_returning_list()?

I wrote:

I think we could do that if we modified the core (maybe the executor
part). I'm not sure that's a good idea, though.

Yes modifying core is not good idea. But just want to understand why you
think that this need need to modify core?

Sorry, I don't remember that. Will check.

The reason why I think so is that the ModifyTable node on top of the
ForeignScan node requires that the targetlist of the ForeignScan has (1)
junk whole-row Vars for secondary relations in UPDATE/DELETE and (2) all
attributes of the target relation to produce the new tuple for UPDATE.
(So, it wouldn't be enough to just replace the ForeignScan's targetlist
with fdw_scan_tlist!) For #1, see this (and the following code) in
ExecInitModifyTable:

/*
* If we have any secondary relations in an UPDATE or DELETE, they
need to
* be treated like non-locked relations in SELECT FOR UPDATE, ie, the
* EvalPlanQual mechanism needs to be told about them. Locate the
* relevant ExecRowMarks.
*/

And for #2, see this (and the following code, especially where calling
ExecCheckPlanOutput) in the same function:

* This section of code is also a convenient place to verify that the
* output of an INSERT or UPDATE matches the target table(s).

What you proposed would be a good idea because the FDW could calculate
the user-query RETURNING list more efficiently in some cases, but I'd
like to leave that for future work.

Attached is the new version of the patch. I also addressed other
comments from you: moved rewriting the fdw_scan_tlist to postgres_fdw.c,
added/revised comments, and added regression tests for the case where a
pushed down UPDATE/DELETE on a join has RETURNING.

My apologies for having been late to work on this.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-more-update-pushdown-v2.patchtext/x-patch; name=postgres-fdw-more-update-pushdown-v2.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 130,142 **** static void deparseTargetList(StringInfo buf,
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 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);
  static void deparseRelation(StringInfo buf, Relation rel);
--- 130,151 ----
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(List *tlist,
! 						  bool is_returning,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 Index rtindex, Relation rel,
  					 bool trig_after_row,
  					 List *returningList,
  					 List **retrieved_attrs);
+ static void deparseExplicitReturningList(List *rlist,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context);
+ static void pull_up_target_conditions(PlannerInfo *root, RelOptInfo *foreignrel,
+ 						  Index target_rel, List **target_conds);
+ static void extract_target_conditions(List **joinclauses, Index target_rel,
+ 						  List **target_conds);
  static void deparseColumnRef(StringInfo buf, int varno, int varattno,
  				 PlannerInfo *root, bool qualify_col);
  static void deparseRelation(StringInfo buf, Relation rel);
***************
*** 165,171 **** static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
  static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 					RelOptInfo *joinrel, bool use_alias, List **params_list);
  static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
  static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
--- 174,181 ----
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
  static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 								  RelOptInfo *foreignrel, bool use_alias,
! 								  Index target_rel, List **params_list);
  static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
  static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
***************
*** 994,1000 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  		foreignrel->reloptkind == RELOPT_UPPER_REL)
  	{
  		/* For a join relation use the input tlist */
! 		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
  	{
--- 1004,1010 ----
  		foreignrel->reloptkind == RELOPT_UPPER_REL)
  	{
  		/* For a join relation use the input tlist */
! 		deparseExplicitTargetList(tlist, false, retrieved_attrs, context);
  	}
  	else
  	{
***************
*** 1037,1043 **** deparseFromExpr(List *quals, deparse_expr_cxt *context)
  	appendStringInfoString(buf, " FROM ");
  	deparseFromExprForRel(buf, context->root, scanrel,
  						  (bms_num_members(scanrel->relids) > 1),
! 						  context->params_list);
  
  	/* Construct WHERE clause */
  	if (quals != NIL)
--- 1047,1053 ----
  	appendStringInfoString(buf, " FROM ");
  	deparseFromExprForRel(buf, context->root, scanrel,
  						  (bms_num_members(scanrel->relids) > 1),
! 						  (Index) 0, context->params_list);
  
  	/* Construct WHERE clause */
  	if (quals != NIL)
***************
*** 1302,1310 **** get_jointype_name(JoinType jointype)
   *
   * retrieved_attrs is the list of continuously increasing integers starting
   * from 1. It has same number of entries as tlist.
   */
  static void
! deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
--- 1312,1325 ----
   *
   * retrieved_attrs is the list of continuously increasing integers starting
   * from 1. It has same number of entries as tlist.
+  *
+  * This is used for both SELECT and RETURNING targetlists; the is_returning
+  * parameter is true only for a RETURNING targetlist.
   */
  static void
! deparseExplicitTargetList(List *tlist,
! 						  bool is_returning,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
***************
*** 1322,1334 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
  		deparseExpr((Expr *) tle->expr, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  		i++;
  	}
  
! 	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
  
--- 1337,1352 ----
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
+ 		else if (is_returning)
+ 			appendStringInfoString(buf, " RETURNING ");
+ 
  		deparseExpr((Expr *) tle->expr, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  		i++;
  	}
  
! 	if (i == 0 && !is_returning)
  		appendStringInfoString(buf, "NULL");
  }
  
***************
*** 1338,1347 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
   * The function constructs ... JOIN ... ON ... for join relation. For a base
   * relation it just returns schema-qualified tablename, with the appropriate
   * alias if so requested.
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
--- 1356,1372 ----
   * The function constructs ... JOIN ... ON ... for join relation. For a base
   * relation it just returns schema-qualified tablename, with the appropriate
   * alias if so requested.
+  *
+  * 'target_rel' is either zero or the rangetable index of a target relation.
+  * In the latter case this construncts FROM clause of UPDATE or USING clause
+  * of DELETE by simply ignoring the target relation while deparsing the given
+  * join tree.  Note that it's safe to do that because the join of the target
+  * relation with any other relation should be an inner join and hence can be
+  * interchanged with higher-level joins.
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, Index target_rel, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
***************
*** 1351,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		RelOptInfo *rel_i = fpinfo->innerrel;
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
  
! 		/* Deparse outer relation */
! 		initStringInfo(&join_sql_o);
! 		deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
  
! 		/* Deparse inner relation */
! 		initStringInfo(&join_sql_i);
! 		deparseFromExprForRel(&join_sql_i, root, rel_i, true, params_list);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
--- 1376,1427 ----
  		RelOptInfo *rel_i = fpinfo->innerrel;
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
+ 		bool		outer_is_targetrel = false;
+ 		bool		inner_is_targetrel = false;
  
! 		/* Check to see if either input relation is the target relation. */
! 		if (target_rel > 0 && bms_is_member(target_rel, foreignrel->relids))
! 		{
! 			if (fpinfo->outerrel->reloptkind == RELOPT_BASEREL &&
! 				fpinfo->outerrel->relid == target_rel)
! 				outer_is_targetrel = true;
! 			else if (fpinfo->innerrel->reloptkind == RELOPT_BASEREL &&
! 					 fpinfo->innerrel->relid == target_rel)
! 				inner_is_targetrel = true;
! 		}
! 
! 		/* Deparse outer relation if not the target relation. */
! 		if (!outer_is_targetrel)
! 		{
! 			initStringInfo(&join_sql_o);
! 			deparseFromExprForRel(&join_sql_o, root, rel_o, true, target_rel,
! 								  params_list);
! 
! 			/* If inner relation is the target relation, we are done. */
! 			if (inner_is_targetrel)
! 			{
! 				appendStringInfo(buf, "%s", join_sql_o.data);
! 				return;
! 			}
! 		}
! 
! 		/* Deparse inner relation if not the target relation. */
! 		if (!inner_is_targetrel)
! 		{
! 			initStringInfo(&join_sql_i);
! 			deparseFromExprForRel(&join_sql_i, root, rel_i, true, target_rel,
! 								  params_list);
! 
! 			/* If outer relation is the target relation, we are done. */
! 			if (outer_is_targetrel)
! 			{
! 				appendStringInfo(buf, "%s", join_sql_i.data);
! 				return;
! 			}
! 		}
  
! 		/* Neither of the relations is the target relation. */
! 		Assert(!outer_is_targetrel && !inner_is_targetrel);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
***************
*** 1528,1533 **** deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 1591,1597 ----
  void
  deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 1535,1541 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
- 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  	int			nestlevel;
  	bool		first;
--- 1599,1604 ----
***************
*** 1543,1555 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
! 	context.scanrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
--- 1606,1620 ----
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = foreignrel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
***************
*** 1576,1589 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  
  	reset_transmission_modes(nestlevel);
  
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1641,1670 ----
  
  	reset_transmission_modes(nestlevel);
  
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		List	   *target_conds = NIL;
+ 
+ 		/* Pull up the target relation's conditions into the WHERE clause */
+ 		pull_up_target_conditions(root, foreignrel, rtindex, &target_conds);
+ 		remote_conds = list_concat(target_conds, remote_conds);
+ 
+ 		appendStringInfo(buf, " FROM ");
+ 		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+ 							  params_list);
+ 	}
+ 
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 		deparseExplicitReturningList(returningList, retrieved_attrs, &context);
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1618,1640 **** deparseDeleteSql(StringInfo buf, PlannerInfo *root,
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
- 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
! 	context.scanrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
  
  	if (remote_conds)
  	{
--- 1699,1736 ----
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
  	deparse_expr_cxt context;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = foreignrel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
+ 
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		List	   *target_conds = NIL;
+ 
+ 		/* Pull up the target relation's conditions into the WHERE clause */
+ 		pull_up_target_conditions(root, foreignrel, rtindex, &target_conds);
+ 		remote_conds = list_concat(target_conds, remote_conds);
+ 
+ 		appendStringInfo(buf, " USING ");
+ 		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+ 							  params_list);
+ 	}
  
  	if (remote_conds)
  	{
***************
*** 1642,1649 **** deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1738,1748 ----
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 		deparseExplicitReturningList(returningList, retrieved_attrs, &context);
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1683,1688 **** deparseReturningList(StringInfo buf, PlannerInfo *root,
--- 1782,1887 ----
  }
  
  /*
+  * Add a RETURNING clause, if needed, to an UPDATE/DELETE on a join.
+  */
+ static void
+ deparseExplicitReturningList(List *rlist,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context)
+ {
+ 	deparseExplicitTargetList(rlist, true, retrieved_attrs, context);
+ }
+ 
+ /*
+  * Look for conditions mentioning the target relation in the given join tree,
+  * which will be pulled up into the WHERE clause.  Note that this is safe due
+  * to the same reason stated in comments in deparseFromExprForRel.
+  */
+ static void
+ pull_up_target_conditions(PlannerInfo *root, RelOptInfo *foreignrel,
+ 						  Index target_rel, List **target_conds)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(bms_is_member(target_rel, foreignrel->relids));
+ 
+ 	/* No work if not an inner join. */
+ 	if (fpinfo->jointype == JOIN_INNER)
+ 	{
+ 		/* The remote_conds should be empty (see foreign_join_ok). */
+ 		Assert(fpinfo->remote_conds == NIL);
+ 
+ 		/*
+ 		 * If either input is the target relation, get all the joinclauses.
+ 		 * Otherwise extract conditions mentioning the target relation from
+ 		 * the joinclauses.
+ 		 */
+ 		if ((outerrel->reloptkind == RELOPT_BASEREL &&
+ 			 outerrel->relid == target_rel) ||
+ 			(innerrel->reloptkind == RELOPT_BASEREL &&
+ 			 innerrel->relid == target_rel))
+ 		{
+ 			*target_conds = list_concat(*target_conds,
+ 										list_copy(fpinfo->joinclauses));
+ 			fpinfo->joinclauses = NIL;
+ 		}
+ 		else
+ 			extract_target_conditions(&fpinfo->joinclauses,
+ 									  target_rel, target_conds);
+ 	}
+ 
+ 	/* Recurse into either input relation. */
+ 	if (outerrel->reloptkind == RELOPT_JOINREL &&
+ 		bms_is_member(target_rel, outerrel->relids))
+ 		pull_up_target_conditions(root, outerrel, target_rel, target_conds);
+ 	else if (innerrel->reloptkind == RELOPT_JOINREL &&
+ 			 bms_is_member(target_rel, innerrel->relids))
+ 		pull_up_target_conditions(root, innerrel, target_rel, target_conds);
+ }
+ 
+ /*
+  * Extract conditions from *joinclauses, separating those that mention
+  * the given relation from those that don't.
+  */
+ static void
+ extract_target_conditions(List **joinclauses,	/* in/out parameters */
+ 						  Index target_rel,
+ 						  List **target_conds	/* output parameters */)
+ {
+ 	List	   *other_conds = NIL;
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, *joinclauses)
+ 	{
+ 		Node	   *clause = (Node *) lfirst(lc);
+ 		Relids		relids;
+ 
+ 		/* Extract clause from RestrictInfo, if needed. */
+ 		if (IsA(clause, RestrictInfo))
+ 		{
+ 			RestrictInfo *ri = (RestrictInfo *) clause;
+ 
+ 			clause = (Node *) ri->clause;
+ 		}
+ 
+ 		/* Retrieve all relids mentioned within the clause. */
+ 		relids = pull_varnos(clause);
+ 
+ 		/* Classify the clause as mentioning the given relation or not. */
+ 		if (bms_is_member(target_rel, relids))
+ 			*target_conds = lappend(*target_conds, clause);
+ 		else
+ 			other_conds = lappend(other_conds, clause);
+ 	}
+ 
+ 	/* Replace *joinclauses. */
+ 	*joinclauses = other_conds;
+ }
+ 
+ /*
   * Construct SELECT statement to acquire size in blocks of given relation.
   *
   * Note: we use local definition of block size, not remote definition.
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 4007,4033 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
!                                                                                                                                                         QUERY PLAN                                                                                                                                                         
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3, c7 = $4 WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.c1, (ft2.c2 + 500), NULL::integer, (ft2.c3 || '_update9'::text), ft2.c4, ft2.c5, ft2.c6, 'ft2       '::character(10), ft2.c8, ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c8, r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9)))) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid
!                      Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c8, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))
! (17 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
--- 4007,4019 ----
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
!                                                                                                    QUERY PLAN                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    ->  Foreign Update
!          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 500), c3 = (r1.c3 || '_update9'::text), c7 = 'ft2       '::character(10) FROM "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9))
! (3 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
***************
*** 4150,4176 **** DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
!                                                                                                                               QUERY PLAN                                                                                                                               
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2)))) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.ctid, ft2.c2
!                      Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))
! (17 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
--- 4136,4148 ----
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
!                                                          QUERY PLAN                                                         
! ----------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    ->  Foreign Delete
!          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2))
! (3 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
***************
*** 5046,5051 **** DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;
--- 5018,5192 ----
   ft2
  (1 row)
  
+ INSERT INTO ft2 (c1,c2,c3)
+   SELECT id, id % 10, to_char(id, 'FM00000') FROM generate_series(10001, 10100) id;
+ CREATE TABLE j1_tbl (c1 int NOT NULL, c2 text);
+ CREATE TABLE j2_tbl (c1 int NOT NULL, c2 text);
+ INSERT INTO j1_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ INSERT INTO j2_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ DELETE FROM j2_tbl WHERE c1 % 2 != 0;
+ CREATE FOREIGN TABLE j1_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j1_tbl');
+ CREATE FOREIGN TABLE j2_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j2_tbl');
+ ANALYZE j1_ftbl;
+ ANALYZE j2_ftbl;
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+                                                                                                                                                      QUERY PLAN                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Update on public.ft2
+    Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8
+    ->  Foreign Update
+          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 200), c3 = (r1.c3 || '_update2'::text) FROM (public.j1_tbl r2 INNER JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 2)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8
+ (4 rows)
+ 
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+   c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 
+ -------+-----+---------------+----+----+----+------------+----
+  10002 | 202 | 10002_update2 |    |    |    | ft2        | 
+  10012 | 202 | 10012_update2 |    |    |    | ft2        | 
+  10022 | 202 | 10022_update2 |    |    |    | ft2        | 
+  10032 | 202 | 10032_update2 |    |    |    | ft2        | 
+  10042 | 202 | 10042_update2 |    |    |    | ft2        | 
+  10052 | 202 | 10052_update2 |    |    |    | ft2        | 
+  10062 | 202 | 10062_update2 |    |    |    | ft2        | 
+  10072 | 202 | 10072_update2 |    |    |    | ft2        | 
+  10082 | 202 | 10082_update2 |    |    |    | ft2        | 
+  10092 | 202 | 10092_update2 |    |    |    | ft2        | 
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                               
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Update on public.ft2
+    Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.c1, j2_ftbl.c2
+    ->  Foreign Update
+          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 800), c3 = (r1.c3 || '_update8'::text) FROM (("S 1"."T 1" r2 INNER JOIN public.j1_tbl r3 ON (((r2."C 1" = (r3.c1 + 10000))))) LEFT JOIN public.j2_tbl r4 ON (((r3.c1 = r4.c1)))) WHERE ((r1."C 1" = r2."C 1")) AND (((r1."C 1" % 10) = 8)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r3.c1, r3.c2, r4.c1, r4.c2
+ (4 rows)
+ 
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+   c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 | c1 |  c2   | c1 |  c2   
+ -------+-----+---------------+----+----+----+------------+----+----+-------+----+-------
+  10008 | 808 | 10008_update8 |    |    |    | ft2        |    |  8 | 00008 |  8 | 00008
+  10018 | 808 | 10018_update8 |    |    |    | ft2        |    | 18 | 00018 | 18 | 00018
+  10028 | 808 | 10028_update8 |    |    |    | ft2        |    | 28 | 00028 | 28 | 00028
+  10038 | 808 | 10038_update8 |    |    |    | ft2        |    | 38 | 00038 | 38 | 00038
+  10048 | 808 | 10048_update8 |    |    |    | ft2        |    | 48 | 00048 | 48 | 00048
+  10058 | 808 | 10058_update8 |    |    |    | ft2        |    | 58 | 00058 | 58 | 00058
+  10068 | 808 | 10068_update8 |    |    |    | ft2        |    | 68 | 00068 | 68 | 00068
+  10078 | 808 | 10078_update8 |    |    |    | ft2        |    | 78 | 00078 | 78 | 00078
+  10088 | 808 | 10088_update8 |    |    |    | ft2        |    | 88 | 00088 | 88 | 00088
+  10098 | 808 | 10098_update8 |    |    |    | ft2        |    | 98 | 00098 | 98 | 00098
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+                                                                                          QUERY PLAN                                                                                          
+ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Delete on public.ft2
+    Output: 100
+    ->  Foreign Delete
+          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (public.j1_tbl r2 INNER JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 2))
+ (4 rows)
+ 
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+  ?column? 
+ ----------
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+                                                                                                                                      QUERY PLAN                                                                                                                                     
+ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Delete on public.ft2
+    Output: ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.ctid, j2_ftbl.c1, j2_ftbl.c2
+    ->  Foreign Delete
+          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (public.j1_tbl r2 LEFT JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 0)) RETURNING r1.ctid, r1."C 1", r1.c2, r1.c3, r2.ctid, r2.c1, r2.c2, r3.ctid, r3.c1, r3.c2
+ (4 rows)
+ 
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+    ctid   |  c1   | c2 |  c3   |  ctid   | c1  |  c2   |  ctid   | c1  |  c2   
+ ----------+-------+----+-------+---------+-----+-------+---------+-----+-------
+  (12,11)  | 10010 |  0 | 10010 | (0,10)  |  10 | 00010 | (0,10)  |  10 | 00010
+  (12,21)  | 10020 |  0 | 10020 | (0,20)  |  20 | 00020 | (0,20)  |  20 | 00020
+  (12,31)  | 10030 |  0 | 10030 | (0,30)  |  30 | 00030 | (0,30)  |  30 | 00030
+  (12,41)  | 10040 |  0 | 10040 | (0,40)  |  40 | 00040 | (0,40)  |  40 | 00040
+  (12,51)  | 10050 |  0 | 10050 | (0,50)  |  50 | 00050 | (0,50)  |  50 | 00050
+  (12,61)  | 10060 |  0 | 10060 | (0,60)  |  60 | 00060 | (0,60)  |  60 | 00060
+  (12,71)  | 10070 |  0 | 10070 | (0,70)  |  70 | 00070 | (0,70)  |  70 | 00070
+  (12,81)  | 10080 |  0 | 10080 | (0,80)  |  80 | 00080 | (0,80)  |  80 | 00080
+  (12,91)  | 10090 |  0 | 10090 | (0,90)  |  90 | 00090 | (0,90)  |  90 | 00090
+  (12,101) | 10100 |  0 | 10100 | (0,100) | 100 | 00100 | (0,100) | 100 | 00100
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+                                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                                     
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Delete on public.ft2
+    Output: ft2.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, j1_ftbl.*, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.*, j2_ftbl.c1, j2_ftbl.c2
+    ->  Foreign Delete
+          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (("S 1"."T 1" r2 INNER JOIN public.j1_tbl r3 ON (((r2."C 1" = (r3.c1 + 10000))))) LEFT JOIN public.j2_tbl r4 ON (((r3.c1 = r4.c1)))) WHERE ((r1."C 1" = r2."C 1")) AND (((r1."C 1" % 10) = 8)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2) END, r3.c1, r3.c2, CASE WHEN (r4.*)::text IS NOT NULL THEN ROW(r4.c1, r4.c2) END, r4.c1, r4.c2
+ (4 rows)
+ 
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+                     ft2                     |  c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 |  j1_ftbl   | c1 |  c2   |  j2_ftbl   | c1 |  c2   
+ --------------------------------------------+-------+-----+---------------+----+----+----+------------+----+------------+----+-------+------------+----+-------
+  (10008,808,10008_update8,,,,"ft2       ",) | 10008 | 808 | 10008_update8 |    |    |    | ft2        |    | (8,00008)  |  8 | 00008 | (8,00008)  |  8 | 00008
+  (10018,808,10018_update8,,,,"ft2       ",) | 10018 | 808 | 10018_update8 |    |    |    | ft2        |    | (18,00018) | 18 | 00018 | (18,00018) | 18 | 00018
+  (10028,808,10028_update8,,,,"ft2       ",) | 10028 | 808 | 10028_update8 |    |    |    | ft2        |    | (28,00028) | 28 | 00028 | (28,00028) | 28 | 00028
+  (10038,808,10038_update8,,,,"ft2       ",) | 10038 | 808 | 10038_update8 |    |    |    | ft2        |    | (38,00038) | 38 | 00038 | (38,00038) | 38 | 00038
+  (10048,808,10048_update8,,,,"ft2       ",) | 10048 | 808 | 10048_update8 |    |    |    | ft2        |    | (48,00048) | 48 | 00048 | (48,00048) | 48 | 00048
+  (10058,808,10058_update8,,,,"ft2       ",) | 10058 | 808 | 10058_update8 |    |    |    | ft2        |    | (58,00058) | 58 | 00058 | (58,00058) | 58 | 00058
+  (10068,808,10068_update8,,,,"ft2       ",) | 10068 | 808 | 10068_update8 |    |    |    | ft2        |    | (68,00068) | 68 | 00068 | (68,00068) | 68 | 00068
+  (10078,808,10078_update8,,,,"ft2       ",) | 10078 | 808 | 10078_update8 |    |    |    | ft2        |    | (78,00078) | 78 | 00078 | (78,00078) | 78 | 00078
+  (10088,808,10088_update8,,,,"ft2       ",) | 10088 | 808 | 10088_update8 |    |    |    | ft2        |    | (88,00088) | 88 | 00088 | (88,00088) | 88 | 00088
+  (10098,808,10098_update8,,,,"ft2       ",) | 10098 | 808 | 10098_update8 |    |    |    | ft2        |    | (98,00098) | 98 | 00098 | (98,00098) | 98 | 00098
+ (10 rows)
+ 
+ DELETE FROM ft2 WHERE ft2.c1 > 10000;
  -- Test that trigger on remote table works as expected
  CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
  BEGIN
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 211,216 **** typedef struct PgFdwDirectModifyState
--- 211,222 ----
  	PGresult   *result;			/* result for query */
  	int			num_tuples;		/* # of result tuples */
  	int			next_tuple;		/* index of next one to return */
+ 	Relation	resultRel;		/* relcache entry for the target table */
+ 	TupleTableSlot *resultSlot;	/* slot for updated/deleted tuple */
+ 	AttrNumber *attnoMap;		/* array of attnums of input user columns */
+ 	AttrNumber	ctidAttno;		/* attnum of input ctid column */
+ 	AttrNumber	oidAttno;		/* attnum of input oid column */
+ 	bool		hasSystemCols;	/* are there system columns of resultRel? */
  
  	/* working memory context */
  	MemoryContext temp_cxt;		/* context for per-tuple temporary data */
***************
*** 377,384 **** static const char **convert_prep_stmt_params(PgFdwModifyState *fmstate,
--- 383,400 ----
  						 TupleTableSlot *slot);
  static void store_returning_result(PgFdwModifyState *fmstate,
  					   TupleTableSlot *slot, PGresult *res);
+ static List *make_explicit_returning_list(Index rtindex, Relation rel,
+ 										  List *returningList);
+ static List *rewrite_fdw_scan_tlist(List *fdw_scan_tlist,  Index rtindex,
+ 					   List *returningList);
  static void execute_dml_stmt(ForeignScanState *node);
  static TupleTableSlot *get_returning_data(ForeignScanState *node);
+ static void init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate);
+ static TupleTableSlot *apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot);
  static void prepare_query_params(PlanState *node,
  					 List *fdw_exprs,
  					 int numParams,
***************
*** 2108,2113 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2124,2130 ----
  	Relation	rel;
  	StringInfoData sql;
  	ForeignScan *fscan;
+ 	RelOptInfo *foreignrel;
  	List	   *targetAttrs = NIL;
  	List	   *remote_conds;
  	List	   *params_list = NIL;
***************
*** 2139,2151 **** postgresPlanDirectModify(PlannerInfo *root,
  		return false;
  
  	/*
- 	 * We can't handle an UPDATE or DELETE on a foreign join for now.
- 	 */
- 	fscan = (ForeignScan *) subplan;
- 	if (fscan->scan.scanrelid == 0)
- 		return false;
- 
- 	/*
  	 * It's unsafe to update a foreign table directly, if any expressions to
  	 * assign to the target columns are unsafe to evaluate remotely.
  	 */
--- 2156,2161 ----
***************
*** 2184,2189 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2194,2201 ----
  	/*
  	 * Ok, rewrite subplan so as to modify the foreign table directly.
  	 */
+ 	fscan = (ForeignScan *) subplan;
+ 
  	initStringInfo(&sql);
  
  	/*
***************
*** 2193,2198 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2205,2222 ----
  	rel = heap_open(rte->relid, NoLock);
  
  	/*
+ 	 * Get a rel for this foreign table or join.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 	{
+ 		foreignrel = find_join_rel(root, fscan->fs_relids);
+ 		/* We should have a rel for this foreign join. */
+ 		Assert(foreignrel);
+ 	}
+ 	else
+ 		foreignrel = find_base_rel(root, resultRelation);
+ 
+ 	/*
  	 * Extract the baserestrictinfo clauses that can be evaluated remotely.
  	 */
  	remote_conds = (List *) list_nth(fscan->fdw_private,
***************
*** 2205,2216 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2229,2249 ----
  		returningList = (List *) list_nth(plan->returningLists, subplan_index);
  
  	/*
+ 	 * If UPDATE/DELETE on a join, create a RETURINING list used in the remote
+ 	 * query.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 		returningList = make_explicit_returning_list(resultRelation, rel,
+ 													 returningList);
+ 
+ 	/*
  	 * Construct the SQL command string.
  	 */
  	switch (operation)
  	{
  		case CMD_UPDATE:
  			deparseDirectUpdateSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
  								   ((Plan *) fscan)->targetlist,
  								   targetAttrs,
  								   remote_conds, &params_list,
***************
*** 2218,2223 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2251,2257 ----
  			break;
  		case CMD_DELETE:
  			deparseDirectDeleteSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
  								   remote_conds, &params_list,
  								   returningList, &retrieved_attrs);
  			break;
***************
*** 2245,2250 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2279,2299 ----
  									retrieved_attrs,
  									makeInteger(plan->canSetTag));
  
+ 	/*
+ 	 * Update the foreign-join-related fields.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 	{
+ 		/* No need for the outer subplan. */
+ 		fscan->scan.plan.lefttree = NULL;
+ 
+ 		/* If having RETURNING, rewrite fdw_scan_tlist to include it. */
+ 		if (returningList)
+ 			fscan->fdw_scan_tlist =
+ 				rewrite_fdw_scan_tlist(fscan->fdw_scan_tlist, resultRelation,
+ 									   returningList);
+ 	}
+ 
  	heap_close(rel, NoLock);
  	return true;
  }
***************
*** 2259,2264 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2308,2314 ----
  	ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
  	EState	   *estate = node->ss.ps.state;
  	PgFdwDirectModifyState *dmstate;
+ 	Index		rtindex;
  	RangeTblEntry *rte;
  	Oid			userid;
  	ForeignTable *table;
***************
*** 2281,2291 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rte = rt_fetch(fsplan->scan.scanrelid, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	dmstate->rel = node->ss.ss_currentRelation;
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
--- 2331,2345 ----
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rtindex = estate->es_result_relation_info->ri_RangeTableIndex;
! 	rte = rt_fetch(rtindex, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	if (fsplan->scan.scanrelid > 0)
! 		dmstate->rel = node->ss.ss_currentRelation;
! 	else
! 		dmstate->rel = ExecOpenScanRelation(estate, rtindex, eflags);
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
***************
*** 2295,2300 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2349,2364 ----
  	 */
  	dmstate->conn = GetConnection(user, false);
  
+ 	/* Update the foreign-join-related fields. */
+ 	if (fsplan->scan.scanrelid == 0)
+ 	{
+ 		/* Save info about target table. */
+ 		dmstate->resultRel = dmstate->rel;
+ 
+ 		/* rel should be NULL. */
+ 		dmstate->rel = NULL;
+ 	}
+ 
  	/* Initialize state variable */
  	dmstate->num_tuples = -1;	/* -1 means not set yet */
  
***************
*** 2315,2321 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(RelationGetDescr(dmstate->rel));
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
--- 2379,2402 ----
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 	{
! 		TupleDesc	tupdesc;
! 
! 		if (fsplan->scan.scanrelid > 0)
! 			tupdesc = RelationGetDescr(dmstate->rel);
! 		else
! 			tupdesc = node->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
! 
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
! 
! 		/*
! 		 * If UPDATE/DELETE on a join, initialize a filter to extract a result
! 		 * tuple from a scan tuple.
! 		 */
! 		if (fsplan->scan.scanrelid == 0)
! 			init_returning_filter(dmstate, fsplan->fdw_scan_tlist,
! 								  rtindex, estate);
! 	}
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
***************
*** 2396,2401 **** postgresEndDirectModify(ForeignScanState *node)
--- 2477,2486 ----
  	ReleaseConnection(dmstate->conn);
  	dmstate->conn = NULL;
  
+ 	/* close the result relation. */
+ 	if (dmstate->resultRel)
+ 		ExecCloseScanRelation(dmstate->resultRel);
+ 
  	/* MemoryContext will be deleted automatically. */
  }
  
***************
*** 3264,3269 **** store_returning_result(PgFdwModifyState *fmstate,
--- 3349,3476 ----
  }
  
  /*
+  * Create an explicit RETURNING list used in the remote query.
+  */
+ static List *
+ make_explicit_returning_list(Index rtindex, Relation rel, List *returningList)
+ {
+ 	TupleDesc	tupdesc = RelationGetDescr(rel);
+ 	bool		have_wholerow = false;
+ 	List	   *rlist = NIL;
+ 	List	   *vars;
+ 	ListCell   *lc;
+ 
+ 	if (returningList == NIL)
+ 		return NIL;
+ 
+ 	vars = pull_var_clause((Node *) returningList, PVC_INCLUDE_PLACEHOLDERS);
+ 
+ 	/*
+ 	 * If there's a whole-row reference to the target relation, then we'll need
+ 	 * all the columns of the relation.
+ 	 */
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno == InvalidAttrNumber)
+ 		{
+ 			have_wholerow = true;
+ 			break;
+ 		}
+ 	}
+ 
+ 	if (have_wholerow)
+ 	{
+ 		int			i;
+ 
+ 		for (i = 1; i <= tupdesc->natts; i++)
+ 		{
+ 			Form_pg_attribute attr = tupdesc->attrs[i - 1];
+ 			Var		   *var;
+ 
+ 			/* Ignore dropped attributes. */
+ 			if (attr->attisdropped)
+ 				continue;
+ 
+ 			var = makeVar(rtindex,
+ 						  i,
+ 						  attr->atttypid,
+ 						  attr->atttypmod,
+ 						  attr->attcollation,
+ 						  0);
+ 
+ 			rlist = lappend(rlist,
+ 							makeTargetEntry((Expr *) var,
+ 											list_length(rlist) + 1,
+ 											NULL,
+ 											false));
+ 		}
+ 	}
+ 
+ 	/* Now add any remaining columns to rlist. */
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		/*
+ 		 * No need for whole-row references to the target relation.  We don't
+ 		 * need system columns other than ctid and oid either, since those are
+ 		 * set locally.
+ 		 */
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno <= InvalidAttrNumber &&
+ 			var->varattno != SelfItemPointerAttributeNumber &&
+ 			var->varattno != ObjectIdAttributeNumber)
+ 			continue;		/* don't need it */
+ 
+ 		if (tlist_member((Node *) var, rlist))
+ 			continue;		/* already got it */
+ 
+ 		rlist = lappend(rlist,
+ 						makeTargetEntry((Expr *) var,
+ 										list_length(rlist) + 1,
+ 										NULL,
+ 										false));
+ 	}
+ 
+ 	list_free(vars);
+ 
+ 	return rlist;
+ }
+ 
+ /*
+  * Rewrite the given fdw_scan_tlist so it contains all the expressions
+  * specified in the RETURNING list.
+  */
+ static List *
+ rewrite_fdw_scan_tlist(List *fdw_scan_tlist,  Index rtindex,
+ 					   List *returningList)
+ {
+ 	List	   *tlist = list_copy(returningList);
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, fdw_scan_tlist)
+ 	{
+ 		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 
+ 		if (tlist_member((Node *) tle->expr, tlist))
+ 			continue;		/* already got it */
+ 
+ 		tlist = lappend(tlist,
+ 						makeTargetEntry(tle->expr,
+ 										list_length(tlist) + 1,
+ 										NULL,
+ 										false));
+ 	}
+ 
+ 	return tlist;
+ }
+ 
+ /*
   * Execute a direct UPDATE/DELETE statement.
   */
  static void
***************
*** 3323,3328 **** get_returning_data(ForeignScanState *node)
--- 3530,3536 ----
  	EState	   *estate = node->ss.ps.state;
  	ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
  	TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ 	TupleTableSlot *resultSlot;
  
  	Assert(resultRelInfo->ri_projectReturning);
  
***************
*** 3340,3346 **** get_returning_data(ForeignScanState *node)
--- 3548,3557 ----
  	 * "UPDATE/DELETE .. RETURNING 1" for example.)
  	 */
  	if (!dmstate->has_returning)
+ 	{
  		ExecStoreAllNullTuple(slot);
+ 		resultSlot = slot;
+ 	}
  	else
  	{
  		/*
***************
*** 3356,3362 **** get_returning_data(ForeignScanState *node)
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												NULL,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
--- 3567,3573 ----
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												node,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
***************
*** 3367,3382 **** get_returning_data(ForeignScanState *node)
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = slot;
  
  	return slot;
  }
  
  /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
--- 3578,3772 ----
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
+ 
+ 		/* Get the updated/deleted tuple. */
+ 		if (dmstate->rel)
+ 			resultSlot = slot;
+ 		else
+ 			resultSlot = apply_returning_filter(dmstate, slot);
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = resultSlot;
  
  	return slot;
  }
  
  /*
+  * Initialize a filter to extract an updated/deleted tuple from a scan tuple.
+  */
+ static void
+ init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate)
+ {
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	ListCell   *lc;
+ 	int			i;
+ 
+ 	/* Make a new slot for storing the result tuple. */
+ 	dmstate->resultSlot = ExecInitExtraTupleSlot(estate);
+ 
+ 	ExecSetSlotDescriptor(dmstate->resultSlot, resultTupType);
+ 
+ 	/*
+ 	 * Calculate the mapping between the fdw_scan_tlist's entries and the
+ 	 * result tuple's attributes.
+ 	 *
+ 	 * The "map" is an array of indexes of the result tuple's attributes in
+ 	 * fdw_scan_tlist, i.e., one entry for every attribute of the result
+ 	 * tuple.  We store zero for any attributes that don't have the
+ 	 * corresponding entries in that list, marking that a NULL is needed in
+ 	 * the result tuple.
+ 	 *
+ 	 * Also get the indexes of the entries for ctid and oid if any.
+ 	 */
+ 	dmstate->attnoMap = (AttrNumber *) palloc0(resultTupType->natts * sizeof(AttrNumber));
+ 
+ 	dmstate->ctidAttno = dmstate->oidAttno = 0;
+ 
+ 	i = 1;
+ 	dmstate->hasSystemCols = false;
+ 	foreach(lc, fdw_scan_tlist)
+ 	{
+ 		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 		Var		   *var = (Var *) tle->expr;
+ 
+ 		if (list_member_int(dmstate->retrieved_attrs, i))
+ 		{
+ 			if (IsA(var, Var) && var->varno == rtindex)
+ 			{
+ 				int			attrno = var->varattno;
+ 
+ 				if (attrno < 0)
+ 				{
+ 				  	/*
+ 					 * We don't retrieve system columns other than ctid and
+ 					 * oid.
+ 					 */
+ 					if (attrno == SelfItemPointerAttributeNumber)
+ 						dmstate->ctidAttno = i;
+ 					else if (attrno == ObjectIdAttributeNumber)
+ 						dmstate->oidAttno = i;
+ 					else
+ 						Assert(false);
+ 					dmstate->hasSystemCols = true;
+ 				}
+ 				else
+ 				{
+ 					/*
+ 					 * We don't retrieve whole-row references to the result
+ 					 * relation either.
+ 					 */
+ 					Assert(attrno > 0);
+ 					dmstate->attnoMap[attrno - 1] = i;
+ 				}
+ 			}
+ 		}
+ 		i++;
+ 	}
+ }
+ 
+ /*
+  * Extract and return an updated/deleted tuple from a scan tuple.
+  */
+ static TupleTableSlot *
+ apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot)
+ {
+ 	TupleTableSlot *resultSlot = dmstate->resultSlot;
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	HeapTuple	resultTup;
+ 	Datum	   *values;
+ 	bool	   *isnull;
+ 	Datum	   *old_values;
+ 	bool	   *old_isnull;
+ 	int			i;
+ 
+ 	/*
+ 	 * Extract all the values of the scan tuple.
+ 	 */
+ 	slot_getallattrs(slot);
+ 	old_values = slot->tts_values;
+ 	old_isnull = slot->tts_isnull;
+ 
+ 	/*
+ 	 * Prepare to build the result tuple.
+ 	 */
+ 	ExecClearTuple(resultSlot);
+ 	values = resultSlot->tts_values;
+ 	isnull = resultSlot->tts_isnull;
+ 
+ 	/*
+ 	 * Transpose data into proper fields of the result tuple.
+ 	 */
+ 	for (i = 0; i < resultTupType->natts; i++)
+ 	{
+ 		int			j = dmstate->attnoMap[i];
+ 
+ 		if (j == 0)
+ 		{
+ 			values[i] = (Datum) 0;
+ 			isnull[i] = true;
+ 		}
+ 		else
+ 		{
+ 			values[i] = old_values[j - 1];
+ 			isnull[i] = old_isnull[j - 1];
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * Build the virtual tuple.
+ 	 */
+ 	ExecStoreVirtualTuple(resultSlot);
+ 
+ 	/*
+ 	 * If we have any system columns to return, install them.
+ 	 */
+ 	if (dmstate->hasSystemCols)
+ 	{
+ 		resultTup = ExecMaterializeSlot(resultSlot);
+ 
+ 		/* ctid */
+ 		if (dmstate->ctidAttno)
+ 		{
+ 			ItemPointer ctid = NULL;
+ 
+ 			ctid = (ItemPointer) DatumGetPointer(old_values[dmstate->ctidAttno - 1]);
+ 			resultTup->t_self = *ctid;
+ 		}
+ 
+ 		/* oid */
+ 		if (dmstate->oidAttno)
+ 		{
+ 			Oid			oid = InvalidOid;
+ 
+ 			oid = DatumGetObjectId(old_values[dmstate->oidAttno - 1]);
+ 			HeapTupleSetOid(resultTup, oid);
+ 		}
+ 
+ 		/*
+ 		 * remaining columns (note that system columns should not go to NULL
+ 		 * because we currently don't allow the result relation to appear on
+ 		 * on the nullable side of an outer join.)
+ 		 */
+ 		HeapTupleHeaderSetXmin(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetXmax(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetCmin(resultTup->t_data, InvalidTransactionId);
+ 
+ 		resultTup->t_tableOid = RelationGetRelid(dmstate->resultRel);
+ 	}
+ 
+ 	/*
+ 	 * And return the result tuple.
+ 	 */
+ 	return resultSlot;
+ }
+ 
+ /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
***************
*** 4796,4806 **** make_tuple_from_result_row(PGresult *res,
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
- 		PgFdwScanState *fdw_sstate;
- 
  		Assert(fsstate);
! 		fdw_sstate = (PgFdwScanState *) fsstate->fdw_state;
! 		tupdesc = fdw_sstate->tupdesc;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
--- 5186,5193 ----
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
  		Assert(fsstate);
! 		tupdesc = fsstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 137,142 **** extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 137,143 ----
  				 List **retrieved_attrs);
  extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 149,154 **** extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
--- 150,156 ----
  				 List **retrieved_attrs);
  extern void deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 987,1000 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
--- 987,1000 ----
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
***************
*** 1007,1012 **** EXPLAIN (verbose, costs off)
--- 1007,1072 ----
  DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;                       -- can be pushed down
  DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;
  
+ INSERT INTO ft2 (c1,c2,c3)
+   SELECT id, id % 10, to_char(id, 'FM00000') FROM generate_series(10001, 10100) id;
+ CREATE TABLE j1_tbl (c1 int NOT NULL, c2 text);
+ CREATE TABLE j2_tbl (c1 int NOT NULL, c2 text);
+ INSERT INTO j1_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ INSERT INTO j2_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ DELETE FROM j2_tbl WHERE c1 % 2 != 0;
+ CREATE FOREIGN TABLE j1_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j1_tbl');
+ CREATE FOREIGN TABLE j2_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j2_tbl');
+ ANALYZE j1_ftbl;
+ ANALYZE j2_ftbl;
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+ DELETE FROM ft2 WHERE ft2.c1 > 10000;
+ 
  -- Test that trigger on remote table works as expected
  CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
  BEGIN
#15Michael Paquier
michael.paquier@gmail.com
In reply to: Etsuro Fujita (#14)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On Wed, Jan 25, 2017 at 7:20 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

Attached is the new version of the patch. I also addressed other comments
from you: moved rewriting the fdw_scan_tlist to postgres_fdw.c,
added/revised comments, and added regression tests for the case where a
pushed down UPDATE/DELETE on a join has RETURNING.

My apologies for having been late to work on this.

Moved to CF 2017-03.
--
Michael

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

#16Rushabh Lathia
rushabh.lathia@gmail.com
In reply to: Michael Paquier (#15)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

Sorry for delay in the review.

I started reviewing the patch again. Patch applied cleanly on latest source
as well as regression pass through with the patch. I also performed
few manual testing and haven't found any regression. Patch look
much cleaner the earlier version, and don't have any major concern as
such. Here are few comments:

1)

@@ -211,6 +211,12 @@ typedef struct PgFdwDirectModifyState
     PGresult   *result;            /* result for query */
     int            num_tuples;        /* # of result tuples */
     int            next_tuple;        /* index of next one to return */
+    Relation    resultRel;        /* relcache entry for the target table */

Why we need resultRel? Can't we directly use dmstate->rel ?

2) In the patch somewhere scanrelid condition being used as
fscan->scan.scanrelid == 0 where as some place its been used as
fsplan->scan.scanrelid > 0. Infact in the same function its been used
differently example postgresBeginDirectModify. Can make this consistent.

3)

+     * If UPDATE/DELETE on a join, create a RETURINING list used in the
remote
+     * query.
+     */
+    if (fscan->scan.scanrelid == 0)
+        returningList = make_explicit_returning_list(resultRelation, rel,
+                                                     returningList);
+

Above block can be moved inside the if (plan->returningLists) condition
above
the block. Like this:

/*
* Extract the relevant RETURNING list if any.
*/
if (plan->returningLists)
{
returningList = (List *) list_nth(plan->returningLists,
subplan_index);

/*
* If UPDATE/DELETE on a join, create a RETURINING list used in the
remote
* query.
*/
if (fscan->scan.scanrelid == 0)
returningList = make_explicit_returning_list(resultRelation,
rel,
returningList);
}

I am still doing few more testing with the patch, if I will found anything
apart from
this I will raise that into another mail.

Thanks,

On Wed, Feb 1, 2017 at 11:50 AM, Michael Paquier <michael.paquier@gmail.com>
wrote:

On Wed, Jan 25, 2017 at 7:20 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

Attached is the new version of the patch. I also addressed other

comments

from you: moved rewriting the fdw_scan_tlist to postgres_fdw.c,
added/revised comments, and added regression tests for the case where a
pushed down UPDATE/DELETE on a join has RETURNING.

My apologies for having been late to work on this.

Moved to CF 2017-03.
--
Michael

--
Rushabh Lathia

#17Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Rushabh Lathia (#16)
1 attachment(s)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 2017/02/13 18:24, Rushabh Lathia wrote:

I started reviewing the patch again. Patch applied cleanly on latest source
as well as regression pass through with the patch. I also performed
few manual testing and haven't found any regression. Patch look
much cleaner the earlier version, and don't have any major concern as
such.

Thanks for the review!

Here are few comments:

1)

@@ -211,6 +211,12 @@ typedef struct PgFdwDirectModifyState
PGresult   *result;            /* result for query */
int            num_tuples;        /* # of result tuples */
int            next_tuple;        /* index of next one to return */
+    Relation    resultRel;        /* relcache entry for the target table */

Why we need resultRel? Can't we directly use dmstate->rel ?

The reason why we need that is because in get_returning_data, we pass
dmstate->rel to make_tuple_from_result_row, which requires that
dmstate->rel be NULL when the scan tuple is described by fdw_scan_tlist.
So in that case we set dmstate->rel to NULL and have
dmstate->resultRel that is the relcache entry for the target relation in
postgresBeginDirectModify.

2) In the patch somewhere scanrelid condition being used as
fscan->scan.scanrelid == 0 where as some place its been used as
fsplan->scan.scanrelid > 0. Infact in the same function its been used
differently example postgresBeginDirectModify. Can make this consistent.

Ok, done.

3)

+     * If UPDATE/DELETE on a join, create a RETURINING list used in the
remote
+     * query.
+     */
+    if (fscan->scan.scanrelid == 0)
+        returningList = make_explicit_returning_list(resultRelation, rel,
+                                                     returningList);
+

Above block can be moved inside the if (plan->returningLists) condition
above
the block. Like this:

/*
* Extract the relevant RETURNING list if any.
*/
if (plan->returningLists)
{
returningList = (List *) list_nth(plan->returningLists,
subplan_index);

/*
* If UPDATE/DELETE on a join, create a RETURINING list used in
the remote
* query.
*/
if (fscan->scan.scanrelid == 0)
returningList = make_explicit_returning_list(resultRelation,
rel,
returningList);
}

Done that way.

Another thing I noticed is duplicate work in apply_returning_filter; it
initializes tableoid of an updated/deleted tuple if needed, but the core
will do that (see ExecProcessReturning). I removed that work from
apply_returning_filter.

I am still doing few more testing with the patch, if I will found
anything apart from
this I will raise that into another mail.

Thanks again!

Attached is an updated version of the patch.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-more-update-pushdown-v3.patchtext/x-diff; name=postgres-fdw-more-update-pushdown-v3.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 130,142 **** static void deparseTargetList(StringInfo buf,
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 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);
  static void deparseRelation(StringInfo buf, Relation rel);
--- 130,151 ----
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(List *tlist,
! 						  bool is_returning,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 Index rtindex, Relation rel,
  					 bool trig_after_row,
  					 List *returningList,
  					 List **retrieved_attrs);
+ static void deparseExplicitReturningList(List *rlist,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context);
+ static void pull_up_target_conditions(PlannerInfo *root, RelOptInfo *foreignrel,
+ 						  Index target_rel, List **target_conds);
+ static void extract_target_conditions(List **joinclauses, Index target_rel,
+ 						  List **target_conds);
  static void deparseColumnRef(StringInfo buf, int varno, int varattno,
  				 PlannerInfo *root, bool qualify_col);
  static void deparseRelation(StringInfo buf, Relation rel);
***************
*** 165,171 **** static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
  static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 					RelOptInfo *joinrel, bool use_alias, List **params_list);
  static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
  static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
--- 174,181 ----
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
  static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 								  RelOptInfo *foreignrel, bool use_alias,
! 								  Index target_rel, List **params_list);
  static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
  static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
***************
*** 994,1000 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  		foreignrel->reloptkind == RELOPT_UPPER_REL)
  	{
  		/* For a join relation use the input tlist */
! 		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
  	{
--- 1004,1010 ----
  		foreignrel->reloptkind == RELOPT_UPPER_REL)
  	{
  		/* For a join relation use the input tlist */
! 		deparseExplicitTargetList(tlist, false, retrieved_attrs, context);
  	}
  	else
  	{
***************
*** 1037,1043 **** deparseFromExpr(List *quals, deparse_expr_cxt *context)
  	appendStringInfoString(buf, " FROM ");
  	deparseFromExprForRel(buf, context->root, scanrel,
  						  (bms_num_members(scanrel->relids) > 1),
! 						  context->params_list);
  
  	/* Construct WHERE clause */
  	if (quals != NIL)
--- 1047,1053 ----
  	appendStringInfoString(buf, " FROM ");
  	deparseFromExprForRel(buf, context->root, scanrel,
  						  (bms_num_members(scanrel->relids) > 1),
! 						  (Index) 0, context->params_list);
  
  	/* Construct WHERE clause */
  	if (quals != NIL)
***************
*** 1302,1310 **** get_jointype_name(JoinType jointype)
   *
   * retrieved_attrs is the list of continuously increasing integers starting
   * from 1. It has same number of entries as tlist.
   */
  static void
! deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
--- 1312,1325 ----
   *
   * retrieved_attrs is the list of continuously increasing integers starting
   * from 1. It has same number of entries as tlist.
+  *
+  * This is used for both SELECT and RETURNING targetlists; the is_returning
+  * parameter is true only for a RETURNING targetlist.
   */
  static void
! deparseExplicitTargetList(List *tlist,
! 						  bool is_returning,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
***************
*** 1322,1334 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
  		deparseExpr((Expr *) tle->expr, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  		i++;
  	}
  
! 	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
  
--- 1337,1352 ----
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
+ 		else if (is_returning)
+ 			appendStringInfoString(buf, " RETURNING ");
+ 
  		deparseExpr((Expr *) tle->expr, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  		i++;
  	}
  
! 	if (i == 0 && !is_returning)
  		appendStringInfoString(buf, "NULL");
  }
  
***************
*** 1338,1347 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
   * The function constructs ... JOIN ... ON ... for join relation. For a base
   * relation it just returns schema-qualified tablename, with the appropriate
   * alias if so requested.
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
--- 1356,1372 ----
   * The function constructs ... JOIN ... ON ... for join relation. For a base
   * relation it just returns schema-qualified tablename, with the appropriate
   * alias if so requested.
+  *
+  * 'target_rel' is either zero or the rangetable index of a target relation.
+  * In the latter case this construncts FROM clause of UPDATE or USING clause
+  * of DELETE by simply ignoring the target relation while deparsing the given
+  * join tree.  Note that it's safe to do that because the join of the target
+  * relation with any other relation is an inner join and can be interchanged
+  * with higher-level joins.
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, Index target_rel, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
***************
*** 1351,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		RelOptInfo *rel_i = fpinfo->innerrel;
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
  
! 		/* Deparse outer relation */
! 		initStringInfo(&join_sql_o);
! 		deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
  
! 		/* Deparse inner relation */
! 		initStringInfo(&join_sql_i);
! 		deparseFromExprForRel(&join_sql_i, root, rel_i, true, params_list);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
--- 1376,1427 ----
  		RelOptInfo *rel_i = fpinfo->innerrel;
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
+ 		bool		outer_is_targetrel = false;
+ 		bool		inner_is_targetrel = false;
  
! 		/* Check to see if either input relation is the target relation. */
! 		if (target_rel > 0 && bms_is_member(target_rel, foreignrel->relids))
! 		{
! 			if (fpinfo->outerrel->reloptkind == RELOPT_BASEREL &&
! 				fpinfo->outerrel->relid == target_rel)
! 				outer_is_targetrel = true;
! 			else if (fpinfo->innerrel->reloptkind == RELOPT_BASEREL &&
! 					 fpinfo->innerrel->relid == target_rel)
! 				inner_is_targetrel = true;
! 		}
! 
! 		/* Deparse outer relation if not the target relation. */
! 		if (!outer_is_targetrel)
! 		{
! 			initStringInfo(&join_sql_o);
! 			deparseFromExprForRel(&join_sql_o, root, rel_o, true, target_rel,
! 								  params_list);
! 
! 			/* If inner relation is the target relation, we are done. */
! 			if (inner_is_targetrel)
! 			{
! 				appendStringInfo(buf, "%s", join_sql_o.data);
! 				return;
! 			}
! 		}
! 
! 		/* Deparse inner relation if not the target relation. */
! 		if (!inner_is_targetrel)
! 		{
! 			initStringInfo(&join_sql_i);
! 			deparseFromExprForRel(&join_sql_i, root, rel_i, true, target_rel,
! 								  params_list);
! 
! 			/* If outer relation is the target relation, we are done. */
! 			if (outer_is_targetrel)
! 			{
! 				appendStringInfo(buf, "%s", join_sql_i.data);
! 				return;
! 			}
! 		}
  
! 		/* Neither of the relations is the target relation. */
! 		Assert(!outer_is_targetrel && !inner_is_targetrel);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
***************
*** 1528,1533 **** deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 1591,1597 ----
  void
  deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 1535,1541 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
- 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  	int			nestlevel;
  	bool		first;
--- 1599,1604 ----
***************
*** 1543,1555 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
! 	context.scanrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
--- 1606,1620 ----
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = foreignrel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
***************
*** 1576,1589 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  
  	reset_transmission_modes(nestlevel);
  
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1641,1670 ----
  
  	reset_transmission_modes(nestlevel);
  
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		List	   *target_conds = NIL;
+ 
+ 		/* Pull up the target relation's conditions into the WHERE clause */
+ 		pull_up_target_conditions(root, foreignrel, rtindex, &target_conds);
+ 		remote_conds = list_concat(target_conds, remote_conds);
+ 
+ 		appendStringInfo(buf, " FROM ");
+ 		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+ 							  params_list);
+ 	}
+ 
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 		deparseExplicitReturningList(returningList, retrieved_attrs, &context);
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1618,1640 **** deparseDeleteSql(StringInfo buf, PlannerInfo *root,
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
- 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
! 	context.scanrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
  
  	if (remote_conds)
  	{
--- 1699,1736 ----
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
  	deparse_expr_cxt context;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = foreignrel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
+ 
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		List	   *target_conds = NIL;
+ 
+ 		/* Pull up the target relation's conditions into the WHERE clause */
+ 		pull_up_target_conditions(root, foreignrel, rtindex, &target_conds);
+ 		remote_conds = list_concat(target_conds, remote_conds);
+ 
+ 		appendStringInfo(buf, " USING ");
+ 		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+ 							  params_list);
+ 	}
  
  	if (remote_conds)
  	{
***************
*** 1642,1649 **** deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1738,1748 ----
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 		deparseExplicitReturningList(returningList, retrieved_attrs, &context);
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1683,1688 **** deparseReturningList(StringInfo buf, PlannerInfo *root,
--- 1782,1887 ----
  }
  
  /*
+  * Add a RETURNING clause, if needed, to an UPDATE/DELETE on a join.
+  */
+ static void
+ deparseExplicitReturningList(List *rlist,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context)
+ {
+ 	deparseExplicitTargetList(rlist, true, retrieved_attrs, context);
+ }
+ 
+ /*
+  * Look for conditions mentioning the target relation in the given join tree,
+  * which will be pulled up into the WHERE clause.  Note that this is safe due
+  * to the same reason stated in comments in deparseFromExprForRel.
+  */
+ static void
+ pull_up_target_conditions(PlannerInfo *root, RelOptInfo *foreignrel,
+ 						  Index target_rel, List **target_conds)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(bms_is_member(target_rel, foreignrel->relids));
+ 
+ 	/* No work if not an inner join. */
+ 	if (fpinfo->jointype == JOIN_INNER)
+ 	{
+ 		/* The remote_conds should be empty (see foreign_join_ok). */
+ 		Assert(fpinfo->remote_conds == NIL);
+ 
+ 		/*
+ 		 * If either input is the target relation, get all the joinclauses.
+ 		 * Otherwise extract conditions mentioning the target relation from
+ 		 * the joinclauses.
+ 		 */
+ 		if ((outerrel->reloptkind == RELOPT_BASEREL &&
+ 			 outerrel->relid == target_rel) ||
+ 			(innerrel->reloptkind == RELOPT_BASEREL &&
+ 			 innerrel->relid == target_rel))
+ 		{
+ 			*target_conds = list_concat(*target_conds,
+ 										list_copy(fpinfo->joinclauses));
+ 			fpinfo->joinclauses = NIL;
+ 		}
+ 		else
+ 			extract_target_conditions(&fpinfo->joinclauses,
+ 									  target_rel, target_conds);
+ 	}
+ 
+ 	/* Recurse into either input relation. */
+ 	if (outerrel->reloptkind == RELOPT_JOINREL &&
+ 		bms_is_member(target_rel, outerrel->relids))
+ 		pull_up_target_conditions(root, outerrel, target_rel, target_conds);
+ 	else if (innerrel->reloptkind == RELOPT_JOINREL &&
+ 			 bms_is_member(target_rel, innerrel->relids))
+ 		pull_up_target_conditions(root, innerrel, target_rel, target_conds);
+ }
+ 
+ /*
+  * Extract conditions from *joinclauses, separating those that mention
+  * the given relation from those that don't.
+  */
+ static void
+ extract_target_conditions(List **joinclauses,	/* in/out parameters */
+ 						  Index target_rel,
+ 						  List **target_conds	/* output parameters */)
+ {
+ 	List	   *other_conds = NIL;
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, *joinclauses)
+ 	{
+ 		Node	   *clause = (Node *) lfirst(lc);
+ 		Relids		relids;
+ 
+ 		/* Extract clause from RestrictInfo, if needed. */
+ 		if (IsA(clause, RestrictInfo))
+ 		{
+ 			RestrictInfo *ri = (RestrictInfo *) clause;
+ 
+ 			clause = (Node *) ri->clause;
+ 		}
+ 
+ 		/* Retrieve all relids mentioned within the clause. */
+ 		relids = pull_varnos(clause);
+ 
+ 		/* Classify the clause as mentioning the given relation or not. */
+ 		if (bms_is_member(target_rel, relids))
+ 			*target_conds = lappend(*target_conds, clause);
+ 		else
+ 			other_conds = lappend(other_conds, clause);
+ 	}
+ 
+ 	/* Replace *joinclauses. */
+ 	*joinclauses = other_conds;
+ }
+ 
+ /*
   * Construct SELECT statement to acquire size in blocks of given relation.
   *
   * Note: we use local definition of block size, not remote definition.
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 4007,4033 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
!                                                                                                                                                         QUERY PLAN                                                                                                                                                         
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3, c7 = $4 WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.c1, (ft2.c2 + 500), NULL::integer, (ft2.c3 || '_update9'::text), ft2.c4, ft2.c5, ft2.c6, 'ft2       '::character(10), ft2.c8, ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c8, r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9)))) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid
!                      Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c8, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))
! (17 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
--- 4007,4019 ----
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
!                                                                                                    QUERY PLAN                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    ->  Foreign Update
!          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 500), c3 = (r1.c3 || '_update9'::text), c7 = 'ft2       '::character(10) FROM "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9))
! (3 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
***************
*** 4150,4176 **** DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
!                                                                                                                               QUERY PLAN                                                                                                                               
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2)))) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.ctid, ft2.c2
!                      Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))
! (17 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
--- 4136,4148 ----
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
!                                                          QUERY PLAN                                                         
! ----------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    ->  Foreign Delete
!          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2))
! (3 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
***************
*** 5046,5051 **** DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;
--- 5018,5192 ----
   ft2
  (1 row)
  
+ INSERT INTO ft2 (c1,c2,c3)
+   SELECT id, id % 10, to_char(id, 'FM00000') FROM generate_series(10001, 10100) id;
+ CREATE TABLE j1_tbl (c1 int NOT NULL, c2 text);
+ CREATE TABLE j2_tbl (c1 int NOT NULL, c2 text);
+ INSERT INTO j1_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ INSERT INTO j2_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ DELETE FROM j2_tbl WHERE c1 % 2 != 0;
+ CREATE FOREIGN TABLE j1_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j1_tbl');
+ CREATE FOREIGN TABLE j2_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j2_tbl');
+ ANALYZE j1_ftbl;
+ ANALYZE j2_ftbl;
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+                                                                                                                                                      QUERY PLAN                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Update on public.ft2
+    Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8
+    ->  Foreign Update
+          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 200), c3 = (r1.c3 || '_update2'::text) FROM (public.j1_tbl r2 INNER JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 2)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8
+ (4 rows)
+ 
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+   c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 
+ -------+-----+---------------+----+----+----+------------+----
+  10002 | 202 | 10002_update2 |    |    |    | ft2        | 
+  10012 | 202 | 10012_update2 |    |    |    | ft2        | 
+  10022 | 202 | 10022_update2 |    |    |    | ft2        | 
+  10032 | 202 | 10032_update2 |    |    |    | ft2        | 
+  10042 | 202 | 10042_update2 |    |    |    | ft2        | 
+  10052 | 202 | 10052_update2 |    |    |    | ft2        | 
+  10062 | 202 | 10062_update2 |    |    |    | ft2        | 
+  10072 | 202 | 10072_update2 |    |    |    | ft2        | 
+  10082 | 202 | 10082_update2 |    |    |    | ft2        | 
+  10092 | 202 | 10092_update2 |    |    |    | ft2        | 
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                               
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Update on public.ft2
+    Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.c1, j2_ftbl.c2
+    ->  Foreign Update
+          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 800), c3 = (r1.c3 || '_update8'::text) FROM (("S 1"."T 1" r2 INNER JOIN public.j1_tbl r3 ON (((r2."C 1" = (r3.c1 + 10000))))) LEFT JOIN public.j2_tbl r4 ON (((r3.c1 = r4.c1)))) WHERE ((r1."C 1" = r2."C 1")) AND (((r1."C 1" % 10) = 8)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r3.c1, r3.c2, r4.c1, r4.c2
+ (4 rows)
+ 
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+   c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 | c1 |  c2   | c1 |  c2   
+ -------+-----+---------------+----+----+----+------------+----+----+-------+----+-------
+  10008 | 808 | 10008_update8 |    |    |    | ft2        |    |  8 | 00008 |  8 | 00008
+  10018 | 808 | 10018_update8 |    |    |    | ft2        |    | 18 | 00018 | 18 | 00018
+  10028 | 808 | 10028_update8 |    |    |    | ft2        |    | 28 | 00028 | 28 | 00028
+  10038 | 808 | 10038_update8 |    |    |    | ft2        |    | 38 | 00038 | 38 | 00038
+  10048 | 808 | 10048_update8 |    |    |    | ft2        |    | 48 | 00048 | 48 | 00048
+  10058 | 808 | 10058_update8 |    |    |    | ft2        |    | 58 | 00058 | 58 | 00058
+  10068 | 808 | 10068_update8 |    |    |    | ft2        |    | 68 | 00068 | 68 | 00068
+  10078 | 808 | 10078_update8 |    |    |    | ft2        |    | 78 | 00078 | 78 | 00078
+  10088 | 808 | 10088_update8 |    |    |    | ft2        |    | 88 | 00088 | 88 | 00088
+  10098 | 808 | 10098_update8 |    |    |    | ft2        |    | 98 | 00098 | 98 | 00098
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+                                                                                          QUERY PLAN                                                                                          
+ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Delete on public.ft2
+    Output: 100
+    ->  Foreign Delete
+          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (public.j1_tbl r2 INNER JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 2))
+ (4 rows)
+ 
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+  ?column? 
+ ----------
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+                                                                                                                                      QUERY PLAN                                                                                                                                     
+ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Delete on public.ft2
+    Output: ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.ctid, j2_ftbl.c1, j2_ftbl.c2
+    ->  Foreign Delete
+          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (public.j1_tbl r2 LEFT JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 0)) RETURNING r1.ctid, r1."C 1", r1.c2, r1.c3, r2.ctid, r2.c1, r2.c2, r3.ctid, r3.c1, r3.c2
+ (4 rows)
+ 
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+    ctid   |  c1   | c2 |  c3   |  ctid   | c1  |  c2   |  ctid   | c1  |  c2   
+ ----------+-------+----+-------+---------+-----+-------+---------+-----+-------
+  (12,11)  | 10010 |  0 | 10010 | (0,10)  |  10 | 00010 | (0,10)  |  10 | 00010
+  (12,21)  | 10020 |  0 | 10020 | (0,20)  |  20 | 00020 | (0,20)  |  20 | 00020
+  (12,31)  | 10030 |  0 | 10030 | (0,30)  |  30 | 00030 | (0,30)  |  30 | 00030
+  (12,41)  | 10040 |  0 | 10040 | (0,40)  |  40 | 00040 | (0,40)  |  40 | 00040
+  (12,51)  | 10050 |  0 | 10050 | (0,50)  |  50 | 00050 | (0,50)  |  50 | 00050
+  (12,61)  | 10060 |  0 | 10060 | (0,60)  |  60 | 00060 | (0,60)  |  60 | 00060
+  (12,71)  | 10070 |  0 | 10070 | (0,70)  |  70 | 00070 | (0,70)  |  70 | 00070
+  (12,81)  | 10080 |  0 | 10080 | (0,80)  |  80 | 00080 | (0,80)  |  80 | 00080
+  (12,91)  | 10090 |  0 | 10090 | (0,90)  |  90 | 00090 | (0,90)  |  90 | 00090
+  (12,101) | 10100 |  0 | 10100 | (0,100) | 100 | 00100 | (0,100) | 100 | 00100
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+                                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                                     
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Delete on public.ft2
+    Output: ft2.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, j1_ftbl.*, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.*, j2_ftbl.c1, j2_ftbl.c2
+    ->  Foreign Delete
+          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (("S 1"."T 1" r2 INNER JOIN public.j1_tbl r3 ON (((r2."C 1" = (r3.c1 + 10000))))) LEFT JOIN public.j2_tbl r4 ON (((r3.c1 = r4.c1)))) WHERE ((r1."C 1" = r2."C 1")) AND (((r1."C 1" % 10) = 8)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2) END, r3.c1, r3.c2, CASE WHEN (r4.*)::text IS NOT NULL THEN ROW(r4.c1, r4.c2) END, r4.c1, r4.c2
+ (4 rows)
+ 
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+                     ft2                     |  c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 |  j1_ftbl   | c1 |  c2   |  j2_ftbl   | c1 |  c2   
+ --------------------------------------------+-------+-----+---------------+----+----+----+------------+----+------------+----+-------+------------+----+-------
+  (10008,808,10008_update8,,,,"ft2       ",) | 10008 | 808 | 10008_update8 |    |    |    | ft2        |    | (8,00008)  |  8 | 00008 | (8,00008)  |  8 | 00008
+  (10018,808,10018_update8,,,,"ft2       ",) | 10018 | 808 | 10018_update8 |    |    |    | ft2        |    | (18,00018) | 18 | 00018 | (18,00018) | 18 | 00018
+  (10028,808,10028_update8,,,,"ft2       ",) | 10028 | 808 | 10028_update8 |    |    |    | ft2        |    | (28,00028) | 28 | 00028 | (28,00028) | 28 | 00028
+  (10038,808,10038_update8,,,,"ft2       ",) | 10038 | 808 | 10038_update8 |    |    |    | ft2        |    | (38,00038) | 38 | 00038 | (38,00038) | 38 | 00038
+  (10048,808,10048_update8,,,,"ft2       ",) | 10048 | 808 | 10048_update8 |    |    |    | ft2        |    | (48,00048) | 48 | 00048 | (48,00048) | 48 | 00048
+  (10058,808,10058_update8,,,,"ft2       ",) | 10058 | 808 | 10058_update8 |    |    |    | ft2        |    | (58,00058) | 58 | 00058 | (58,00058) | 58 | 00058
+  (10068,808,10068_update8,,,,"ft2       ",) | 10068 | 808 | 10068_update8 |    |    |    | ft2        |    | (68,00068) | 68 | 00068 | (68,00068) | 68 | 00068
+  (10078,808,10078_update8,,,,"ft2       ",) | 10078 | 808 | 10078_update8 |    |    |    | ft2        |    | (78,00078) | 78 | 00078 | (78,00078) | 78 | 00078
+  (10088,808,10088_update8,,,,"ft2       ",) | 10088 | 808 | 10088_update8 |    |    |    | ft2        |    | (88,00088) | 88 | 00088 | (88,00088) | 88 | 00088
+  (10098,808,10098_update8,,,,"ft2       ",) | 10098 | 808 | 10098_update8 |    |    |    | ft2        |    | (98,00098) | 98 | 00098 | (98,00098) | 98 | 00098
+ (10 rows)
+ 
+ DELETE FROM ft2 WHERE ft2.c1 > 10000;
  -- Test that trigger on remote table works as expected
  CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
  BEGIN
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 211,216 **** typedef struct PgFdwDirectModifyState
--- 211,222 ----
  	PGresult   *result;			/* result for query */
  	int			num_tuples;		/* # of result tuples */
  	int			next_tuple;		/* index of next one to return */
+ 	Relation	resultRel;		/* relcache entry for the target table */
+ 	TupleTableSlot *resultSlot;	/* slot for updated/deleted tuple */
+ 	AttrNumber *attnoMap;		/* array of attnums of input user columns */
+ 	AttrNumber	ctidAttno;		/* attnum of input ctid column */
+ 	AttrNumber	oidAttno;		/* attnum of input oid column */
+ 	bool		hasSystemCols;	/* are there system columns of resultRel? */
  
  	/* working memory context */
  	MemoryContext temp_cxt;		/* context for per-tuple temporary data */
***************
*** 377,384 **** static const char **convert_prep_stmt_params(PgFdwModifyState *fmstate,
--- 383,400 ----
  						 TupleTableSlot *slot);
  static void store_returning_result(PgFdwModifyState *fmstate,
  					   TupleTableSlot *slot, PGresult *res);
+ static List *make_explicit_returning_list(Index rtindex, Relation rel,
+ 										  List *returningList);
+ static List *rewrite_fdw_scan_tlist(List *fdw_scan_tlist,  Index rtindex,
+ 					   List *returningList);
  static void execute_dml_stmt(ForeignScanState *node);
  static TupleTableSlot *get_returning_data(ForeignScanState *node);
+ static void init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate);
+ static TupleTableSlot *apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot);
  static void prepare_query_params(PlanState *node,
  					 List *fdw_exprs,
  					 int numParams,
***************
*** 2108,2113 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2124,2130 ----
  	Relation	rel;
  	StringInfoData sql;
  	ForeignScan *fscan;
+ 	RelOptInfo *foreignrel;
  	List	   *targetAttrs = NIL;
  	List	   *remote_conds;
  	List	   *params_list = NIL;
***************
*** 2139,2151 **** postgresPlanDirectModify(PlannerInfo *root,
  		return false;
  
  	/*
- 	 * We can't handle an UPDATE or DELETE on a foreign join for now.
- 	 */
- 	fscan = (ForeignScan *) subplan;
- 	if (fscan->scan.scanrelid == 0)
- 		return false;
- 
- 	/*
  	 * It's unsafe to update a foreign table directly, if any expressions to
  	 * assign to the target columns are unsafe to evaluate remotely.
  	 */
--- 2156,2161 ----
***************
*** 2184,2189 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2194,2201 ----
  	/*
  	 * Ok, rewrite subplan so as to modify the foreign table directly.
  	 */
+ 	fscan = (ForeignScan *) subplan;
+ 
  	initStringInfo(&sql);
  
  	/*
***************
*** 2193,2198 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2205,2222 ----
  	rel = heap_open(rte->relid, NoLock);
  
  	/*
+ 	 * Get a rel for this foreign table or join.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 	{
+ 		foreignrel = find_join_rel(root, fscan->fs_relids);
+ 		/* We should have a rel for this foreign join. */
+ 		Assert(foreignrel);
+ 	}
+ 	else
+ 		foreignrel = find_base_rel(root, resultRelation);
+ 
+ 	/*
  	 * Extract the baserestrictinfo clauses that can be evaluated remotely.
  	 */
  	remote_conds = (List *) list_nth(fscan->fdw_private,
***************
*** 2202,2209 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2226,2244 ----
  	 * Extract the relevant RETURNING list if any.
  	 */
  	if (plan->returningLists)
+ 	{
  		returningList = (List *) list_nth(plan->returningLists, subplan_index);
  
+ 		/*
+ 		 * If UPDATE/DELETE on a join, create a RETURINING list used in the
+ 		 * remote query.
+ 		 */
+ 		if (fscan->scan.scanrelid == 0)
+ 			returningList = make_explicit_returning_list(resultRelation,
+ 														 rel,
+ 														 returningList);
+ 	}
+ 
  	/*
  	 * Construct the SQL command string.
  	 */
***************
*** 2211,2216 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2246,2252 ----
  	{
  		case CMD_UPDATE:
  			deparseDirectUpdateSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
  								   ((Plan *) fscan)->targetlist,
  								   targetAttrs,
  								   remote_conds, &params_list,
***************
*** 2218,2223 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2254,2260 ----
  			break;
  		case CMD_DELETE:
  			deparseDirectDeleteSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
  								   remote_conds, &params_list,
  								   returningList, &retrieved_attrs);
  			break;
***************
*** 2245,2250 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2282,2302 ----
  									retrieved_attrs,
  									makeInteger(plan->canSetTag));
  
+ 	/*
+ 	 * Update the foreign-join-related fields.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 	{
+ 		/* No need for the outer subplan. */
+ 		fscan->scan.plan.lefttree = NULL;
+ 
+ 		/* If having RETURNING, rewrite fdw_scan_tlist to include it. */
+ 		if (returningList)
+ 			fscan->fdw_scan_tlist =
+ 				rewrite_fdw_scan_tlist(fscan->fdw_scan_tlist, resultRelation,
+ 									   returningList);
+ 	}
+ 
  	heap_close(rel, NoLock);
  	return true;
  }
***************
*** 2259,2264 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2311,2317 ----
  	ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
  	EState	   *estate = node->ss.ps.state;
  	PgFdwDirectModifyState *dmstate;
+ 	Index		rtindex;
  	RangeTblEntry *rte;
  	Oid			userid;
  	ForeignTable *table;
***************
*** 2281,2291 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rte = rt_fetch(fsplan->scan.scanrelid, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	dmstate->rel = node->ss.ss_currentRelation;
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
--- 2334,2348 ----
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rtindex = estate->es_result_relation_info->ri_RangeTableIndex;
! 	rte = rt_fetch(rtindex, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	if (fsplan->scan.scanrelid == 0)
! 		dmstate->rel = ExecOpenScanRelation(estate, rtindex, eflags);
! 	else
! 		dmstate->rel = node->ss.ss_currentRelation;
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
***************
*** 2295,2300 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2352,2367 ----
  	 */
  	dmstate->conn = GetConnection(user, false);
  
+ 	/* Update the foreign-join-related fields. */
+ 	if (fsplan->scan.scanrelid == 0)
+ 	{
+ 		/* Save info about target table. */
+ 		dmstate->resultRel = dmstate->rel;
+ 
+ 		/* rel should be NULL. */
+ 		dmstate->rel = NULL;
+ 	}
+ 
  	/* Initialize state variable */
  	dmstate->num_tuples = -1;	/* -1 means not set yet */
  
***************
*** 2315,2321 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(RelationGetDescr(dmstate->rel));
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
--- 2382,2405 ----
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 	{
! 		TupleDesc	tupdesc;
! 
! 		if (fsplan->scan.scanrelid == 0)
! 			tupdesc = node->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
! 		else
! 			tupdesc = RelationGetDescr(dmstate->rel);
! 
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
! 
! 		/*
! 		 * If UPDATE/DELETE on a join, initialize a filter to extract a result
! 		 * tuple from a scan tuple.
! 		 */
! 		if (fsplan->scan.scanrelid == 0)
! 			init_returning_filter(dmstate, fsplan->fdw_scan_tlist,
! 								  rtindex, estate);
! 	}
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
***************
*** 2396,2401 **** postgresEndDirectModify(ForeignScanState *node)
--- 2480,2489 ----
  	ReleaseConnection(dmstate->conn);
  	dmstate->conn = NULL;
  
+ 	/* close the result relation. */
+ 	if (dmstate->resultRel)
+ 		ExecCloseScanRelation(dmstate->resultRel);
+ 
  	/* MemoryContext will be deleted automatically. */
  }
  
***************
*** 3264,3269 **** store_returning_result(PgFdwModifyState *fmstate,
--- 3352,3479 ----
  }
  
  /*
+  * Create an explicit RETURNING list used in the remote query.
+  */
+ static List *
+ make_explicit_returning_list(Index rtindex, Relation rel, List *returningList)
+ {
+ 	TupleDesc	tupdesc = RelationGetDescr(rel);
+ 	bool		have_wholerow = false;
+ 	List	   *rlist = NIL;
+ 	List	   *vars;
+ 	ListCell   *lc;
+ 
+ 	if (returningList == NIL)
+ 		return NIL;
+ 
+ 	vars = pull_var_clause((Node *) returningList, PVC_INCLUDE_PLACEHOLDERS);
+ 
+ 	/*
+ 	 * If there's a whole-row reference to the target relation, then we'll need
+ 	 * all the columns of the relation.
+ 	 */
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno == InvalidAttrNumber)
+ 		{
+ 			have_wholerow = true;
+ 			break;
+ 		}
+ 	}
+ 
+ 	if (have_wholerow)
+ 	{
+ 		int			i;
+ 
+ 		for (i = 1; i <= tupdesc->natts; i++)
+ 		{
+ 			Form_pg_attribute attr = tupdesc->attrs[i - 1];
+ 			Var		   *var;
+ 
+ 			/* Ignore dropped attributes. */
+ 			if (attr->attisdropped)
+ 				continue;
+ 
+ 			var = makeVar(rtindex,
+ 						  i,
+ 						  attr->atttypid,
+ 						  attr->atttypmod,
+ 						  attr->attcollation,
+ 						  0);
+ 
+ 			rlist = lappend(rlist,
+ 							makeTargetEntry((Expr *) var,
+ 											list_length(rlist) + 1,
+ 											NULL,
+ 											false));
+ 		}
+ 	}
+ 
+ 	/* Now add any remaining columns to rlist. */
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		/*
+ 		 * No need for whole-row references to the target relation.  We don't
+ 		 * need system columns other than ctid and oid either, since those are
+ 		 * set locally.
+ 		 */
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno <= InvalidAttrNumber &&
+ 			var->varattno != SelfItemPointerAttributeNumber &&
+ 			var->varattno != ObjectIdAttributeNumber)
+ 			continue;		/* don't need it */
+ 
+ 		if (tlist_member((Node *) var, rlist))
+ 			continue;		/* already got it */
+ 
+ 		rlist = lappend(rlist,
+ 						makeTargetEntry((Expr *) var,
+ 										list_length(rlist) + 1,
+ 										NULL,
+ 										false));
+ 	}
+ 
+ 	list_free(vars);
+ 
+ 	return rlist;
+ }
+ 
+ /*
+  * Rewrite the given fdw_scan_tlist so it contains all the expressions
+  * specified in the RETURNING list.
+  */
+ static List *
+ rewrite_fdw_scan_tlist(List *fdw_scan_tlist,  Index rtindex,
+ 					   List *returningList)
+ {
+ 	List	   *tlist = list_copy(returningList);
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, fdw_scan_tlist)
+ 	{
+ 		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 
+ 		if (tlist_member((Node *) tle->expr, tlist))
+ 			continue;		/* already got it */
+ 
+ 		tlist = lappend(tlist,
+ 						makeTargetEntry(tle->expr,
+ 										list_length(tlist) + 1,
+ 										NULL,
+ 										false));
+ 	}
+ 
+ 	return tlist;
+ }
+ 
+ /*
   * Execute a direct UPDATE/DELETE statement.
   */
  static void
***************
*** 3323,3328 **** get_returning_data(ForeignScanState *node)
--- 3533,3539 ----
  	EState	   *estate = node->ss.ps.state;
  	ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
  	TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ 	TupleTableSlot *resultSlot;
  
  	Assert(resultRelInfo->ri_projectReturning);
  
***************
*** 3340,3346 **** get_returning_data(ForeignScanState *node)
--- 3551,3560 ----
  	 * "UPDATE/DELETE .. RETURNING 1" for example.)
  	 */
  	if (!dmstate->has_returning)
+ 	{
  		ExecStoreAllNullTuple(slot);
+ 		resultSlot = slot;
+ 	}
  	else
  	{
  		/*
***************
*** 3356,3362 **** get_returning_data(ForeignScanState *node)
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												NULL,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
--- 3570,3576 ----
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												node,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
***************
*** 3367,3382 **** get_returning_data(ForeignScanState *node)
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = slot;
  
  	return slot;
  }
  
  /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
--- 3581,3777 ----
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
+ 
+ 		/* Get the updated/deleted tuple. */
+ 		if (dmstate->rel)
+ 			resultSlot = slot;
+ 		else
+ 			resultSlot = apply_returning_filter(dmstate, slot);
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = resultSlot;
  
  	return slot;
  }
  
  /*
+  * Initialize a filter to extract an updated/deleted tuple from a scan tuple.
+  */
+ static void
+ init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate)
+ {
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	ListCell   *lc;
+ 	int			i;
+ 
+ 	/* Make a new slot for storing the result tuple. */
+ 	dmstate->resultSlot = ExecInitExtraTupleSlot(estate);
+ 
+ 	ExecSetSlotDescriptor(dmstate->resultSlot, resultTupType);
+ 
+ 	/*
+ 	 * Calculate the mapping between the fdw_scan_tlist's entries and the
+ 	 * result tuple's attributes.
+ 	 *
+ 	 * The "map" is an array of indexes of the result tuple's attributes in
+ 	 * fdw_scan_tlist, i.e., one entry for every attribute of the result
+ 	 * tuple.  We store zero for any attributes that don't have the
+ 	 * corresponding entries in that list, marking that a NULL is needed in
+ 	 * the result tuple.
+ 	 *
+ 	 * Also get the indexes of the entries for ctid and oid if any.
+ 	 */
+ 	dmstate->attnoMap = (AttrNumber *) palloc0(resultTupType->natts * sizeof(AttrNumber));
+ 
+ 	dmstate->ctidAttno = dmstate->oidAttno = 0;
+ 
+ 	i = 1;
+ 	dmstate->hasSystemCols = false;
+ 	foreach(lc, fdw_scan_tlist)
+ 	{
+ 		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 		Var		   *var = (Var *) tle->expr;
+ 
+ 		if (list_member_int(dmstate->retrieved_attrs, i))
+ 		{
+ 			if (IsA(var, Var) && var->varno == rtindex)
+ 			{
+ 				int			attrno = var->varattno;
+ 
+ 				if (attrno < 0)
+ 				{
+ 				  	/*
+ 					 * We don't retrieve system columns other than ctid and
+ 					 * oid.
+ 					 */
+ 					if (attrno == SelfItemPointerAttributeNumber)
+ 						dmstate->ctidAttno = i;
+ 					else if (attrno == ObjectIdAttributeNumber)
+ 						dmstate->oidAttno = i;
+ 					else
+ 						Assert(false);
+ 					dmstate->hasSystemCols = true;
+ 				}
+ 				else
+ 				{
+ 					/*
+ 					 * We don't retrieve whole-row references to the result
+ 					 * relation either.
+ 					 */
+ 					Assert(attrno > 0);
+ 					dmstate->attnoMap[attrno - 1] = i;
+ 				}
+ 			}
+ 		}
+ 		i++;
+ 	}
+ }
+ 
+ /*
+  * Extract and return an updated/deleted tuple from a scan tuple.
+  */
+ static TupleTableSlot *
+ apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot)
+ {
+ 	TupleTableSlot *resultSlot = dmstate->resultSlot;
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	Datum	   *values;
+ 	bool	   *isnull;
+ 	Datum	   *old_values;
+ 	bool	   *old_isnull;
+ 	int			i;
+ 
+ 	/*
+ 	 * Extract all the values of the scan tuple.
+ 	 */
+ 	slot_getallattrs(slot);
+ 	old_values = slot->tts_values;
+ 	old_isnull = slot->tts_isnull;
+ 
+ 	/*
+ 	 * Prepare to build the result tuple.
+ 	 */
+ 	ExecClearTuple(resultSlot);
+ 	values = resultSlot->tts_values;
+ 	isnull = resultSlot->tts_isnull;
+ 
+ 	/*
+ 	 * Transpose data into proper fields of the result tuple.
+ 	 */
+ 	for (i = 0; i < resultTupType->natts; i++)
+ 	{
+ 		int			j = dmstate->attnoMap[i];
+ 
+ 		if (j == 0)
+ 		{
+ 			values[i] = (Datum) 0;
+ 			isnull[i] = true;
+ 		}
+ 		else
+ 		{
+ 			values[i] = old_values[j - 1];
+ 			isnull[i] = old_isnull[j - 1];
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * Build the virtual tuple.
+ 	 */
+ 	ExecStoreVirtualTuple(resultSlot);
+ 
+ 	/*
+ 	 * If we have any system columns to return, install them.
+ 	 */
+ 	if (dmstate->hasSystemCols)
+ 	{
+ 		HeapTuple	resultTup = ExecMaterializeSlot(resultSlot);
+ 
+ 		/* ctid */
+ 		if (dmstate->ctidAttno)
+ 		{
+ 			ItemPointer ctid = NULL;
+ 
+ 			ctid = (ItemPointer) DatumGetPointer(old_values[dmstate->ctidAttno - 1]);
+ 			resultTup->t_self = *ctid;
+ 		}
+ 
+ 		/* oid */
+ 		if (dmstate->oidAttno)
+ 		{
+ 			Oid			oid = InvalidOid;
+ 
+ 			oid = DatumGetObjectId(old_values[dmstate->oidAttno - 1]);
+ 			HeapTupleSetOid(resultTup, oid);
+ 		}
+ 
+ 		/*
+ 		 * And remaining columns
+ 		 *
+ 		 * Note: since we currently don't allow the result relation to appear
+ 		 * on the nullable side of an outer join, any system columns wouldn't
+ 		 * go to NULL.
+ 		 *
+ 		 * Note: no need to care about tableoid here because it will be
+ 		 * initialized in ExecProcessReturning().
+ 		 */
+ 		HeapTupleHeaderSetXmin(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetXmax(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetCmin(resultTup->t_data, InvalidTransactionId);
+ 	}
+ 
+ 	/*
+ 	 * And return the result tuple.
+ 	 */
+ 	return resultSlot;
+ }
+ 
+ /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
***************
*** 4796,4806 **** make_tuple_from_result_row(PGresult *res,
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
- 		PgFdwScanState *fdw_sstate;
- 
  		Assert(fsstate);
! 		fdw_sstate = (PgFdwScanState *) fsstate->fdw_state;
! 		tupdesc = fdw_sstate->tupdesc;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
--- 5191,5198 ----
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
  		Assert(fsstate);
! 		tupdesc = fsstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 137,142 **** extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 137,143 ----
  				 List **retrieved_attrs);
  extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 149,154 **** extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
--- 150,156 ----
  				 List **retrieved_attrs);
  extern void deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 987,1000 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
--- 987,1000 ----
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
***************
*** 1007,1012 **** EXPLAIN (verbose, costs off)
--- 1007,1072 ----
  DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;                       -- can be pushed down
  DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;
  
+ INSERT INTO ft2 (c1,c2,c3)
+   SELECT id, id % 10, to_char(id, 'FM00000') FROM generate_series(10001, 10100) id;
+ CREATE TABLE j1_tbl (c1 int NOT NULL, c2 text);
+ CREATE TABLE j2_tbl (c1 int NOT NULL, c2 text);
+ INSERT INTO j1_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ INSERT INTO j2_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ DELETE FROM j2_tbl WHERE c1 % 2 != 0;
+ CREATE FOREIGN TABLE j1_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j1_tbl');
+ CREATE FOREIGN TABLE j2_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j2_tbl');
+ ANALYZE j1_ftbl;
+ ANALYZE j2_ftbl;
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+ DELETE FROM ft2 WHERE ft2.c1 > 10000;
+ 
  -- Test that trigger on remote table works as expected
  CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
  BEGIN
#18Rushabh Lathia
rushabh.lathia@gmail.com
In reply to: Etsuro Fujita (#17)
1 attachment(s)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On Mon, Feb 20, 2017 at 1:41 PM, Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp>
wrote:

On 2017/02/13 18:24, Rushabh Lathia wrote:

I started reviewing the patch again. Patch applied cleanly on latest
source
as well as regression pass through with the patch. I also performed
few manual testing and haven't found any regression. Patch look
much cleaner the earlier version, and don't have any major concern as
such.

Thanks for the review!

Here are few comments:

1)

@@ -211,6 +211,12 @@ typedef struct PgFdwDirectModifyState
PGresult   *result;            /* result for query */
int            num_tuples;        /* # of result tuples */
int            next_tuple;        /* index of next one to return */
+    Relation    resultRel;        /* relcache entry for the target table
*/

Why we need resultRel? Can't we directly use dmstate->rel ?

The reason why we need that is because in get_returning_data, we pass
dmstate->rel to make_tuple_from_result_row, which requires that
dmstate->rel be NULL when the scan tuple is described by fdw_scan_tlist.
So in that case we set dmstate->rel to NULL and have dmstate->resultRel
that is the relcache entry for the target relation in
postgresBeginDirectModify.

Thanks for the explanation. We might do something here by using
fdw_scan_tlist or changing the assumption of make_tuple_from_result_row(),
and that way we can avoid two similar variable pointer in the
PgFdwDirectModifyState.

I am okay with currently also, but it adding a note somewhere about this
would be great. Also let keep this point open for the committer, if
committer feel this is good then lets go ahead with this.

Here are few other cosmetic changes:

1)

+ *
+ * 'target_rel' is either zero or the rangetable index of a target
relation.
+ * In the latter case this construncts FROM clause of UPDATE or USING
clause
+ * of DELETE by simply ignoring the target relation while deparsing the
given

Spell correction: - construncts

2)

+        /*
+         * If either input is the target relation, get all the joinclauses.
+         * Otherwise extract conditions mentioning the target relation from
+         * the joinclauses.
+         */

space between joinclauses needed.

3)

+        /*
+         * If UPDATE/DELETE on a join, create a RETURINING list used in the
+         * remote query.
+         */
+        if (fscan->scan.scanrelid == 0)
+            returningList = make_explicit_returning_list(resultRelation,
+                                                         rel,
+                                                         returningList);

Spell correction: RETURINING

I did above changes in the attached patch. Please have a look once and
then I feel like this patch is ready for committer.

Thanks,
Rushabh Lathia

Attachments:

postgres-fdw-more-update-pushdown-v3_cosmetic_changes.patchapplication/x-download; name=postgres-fdw-more-update-pushdown-v3_cosmetic_changes.patchDownload
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index d2b94aa..e0f4fd7 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -130,13 +130,22 @@ static void deparseTargetList(StringInfo buf,
 				  Bitmapset *attrs_used,
 				  bool qualify_col,
 				  List **retrieved_attrs);
-static void deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
+static void deparseExplicitTargetList(List *tlist,
+						  bool is_returning,
+						  List **retrieved_attrs,
 						  deparse_expr_cxt *context);
 static void deparseReturningList(StringInfo buf, PlannerInfo *root,
 					 Index rtindex, Relation rel,
 					 bool trig_after_row,
 					 List *returningList,
 					 List **retrieved_attrs);
+static void deparseExplicitReturningList(List *rlist,
+							 List **retrieved_attrs,
+							 deparse_expr_cxt *context);
+static void pull_up_target_conditions(PlannerInfo *root, RelOptInfo *foreignrel,
+						  Index target_rel, List **target_conds);
+static void extract_target_conditions(List **joinclauses, Index target_rel,
+						  List **target_conds);
 static void deparseColumnRef(StringInfo buf, int varno, int varattno,
 				 PlannerInfo *root, bool qualify_col);
 static void deparseRelation(StringInfo buf, Relation rel);
@@ -165,7 +174,8 @@ static void deparseLockingClause(deparse_expr_cxt *context);
 static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
 static void appendConditions(List *exprs, deparse_expr_cxt *context);
 static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
-					RelOptInfo *joinrel, bool use_alias, List **params_list);
+								  RelOptInfo *foreignrel, bool use_alias,
+								  Index target_rel, List **params_list);
 static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
 static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
 static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
@@ -994,7 +1004,7 @@ deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
 		foreignrel->reloptkind == RELOPT_UPPER_REL)
 	{
 		/* For a join relation use the input tlist */
-		deparseExplicitTargetList(tlist, retrieved_attrs, context);
+		deparseExplicitTargetList(tlist, false, retrieved_attrs, context);
 	}
 	else
 	{
@@ -1037,7 +1047,7 @@ deparseFromExpr(List *quals, deparse_expr_cxt *context)
 	appendStringInfoString(buf, " FROM ");
 	deparseFromExprForRel(buf, context->root, scanrel,
 						  (bms_num_members(scanrel->relids) > 1),
-						  context->params_list);
+						  (Index) 0, context->params_list);
 
 	/* Construct WHERE clause */
 	if (quals != NIL)
@@ -1302,9 +1312,14 @@ get_jointype_name(JoinType jointype)
  *
  * retrieved_attrs is the list of continuously increasing integers starting
  * from 1. It has same number of entries as tlist.
+ *
+ * This is used for both SELECT and RETURNING targetlists; the is_returning
+ * parameter is true only for a RETURNING targetlist.
  */
 static void
-deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
+deparseExplicitTargetList(List *tlist,
+						  bool is_returning,
+						  List **retrieved_attrs,
 						  deparse_expr_cxt *context)
 {
 	ListCell   *lc;
@@ -1319,13 +1334,16 @@ deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
 
 		if (i > 0)
 			appendStringInfoString(buf, ", ");
+		else if (is_returning)
+			appendStringInfoString(buf, " RETURNING ");
+
 		deparseExpr((Expr *) tle->expr, context);
 
 		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
 		i++;
 	}
 
-	if (i == 0)
+	if (i == 0 && !is_returning)
 		appendStringInfoString(buf, "NULL");
 }
 
@@ -1335,10 +1353,17 @@ deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  * The function constructs ... JOIN ... ON ... for join relation. For a base
  * relation it just returns schema-qualified tablename, with the appropriate
  * alias if so requested.
+ *
+ * 'target_rel' is either zero or the rangetable index of a target relation.
+ * In the latter case this constructs FROM clause of UPDATE or USING clause
+ * of DELETE by simply ignoring the target relation while deparsing the given
+ * join tree.  Note that it's safe to do that because the join of the target
+ * relation with any other relation is an inner join and can be interchanged
+ * with higher-level joins.
  */
 static void
 deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
-					  bool use_alias, List **params_list)
+					  bool use_alias, Index target_rel, List **params_list)
 {
 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
 
@@ -1348,14 +1373,52 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
 		RelOptInfo *rel_i = fpinfo->innerrel;
 		StringInfoData join_sql_o;
 		StringInfoData join_sql_i;
+		bool		outer_is_targetrel = false;
+		bool		inner_is_targetrel = false;
 
-		/* Deparse outer relation */
-		initStringInfo(&join_sql_o);
-		deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
+		/* Check to see if either input relation is the target relation. */
+		if (target_rel > 0 && bms_is_member(target_rel, foreignrel->relids))
+		{
+			if (fpinfo->outerrel->reloptkind == RELOPT_BASEREL &&
+				fpinfo->outerrel->relid == target_rel)
+				outer_is_targetrel = true;
+			else if (fpinfo->innerrel->reloptkind == RELOPT_BASEREL &&
+					 fpinfo->innerrel->relid == target_rel)
+				inner_is_targetrel = true;
+		}
+
+		/* Deparse outer relation if not the target relation. */
+		if (!outer_is_targetrel)
+		{
+			initStringInfo(&join_sql_o);
+			deparseFromExprForRel(&join_sql_o, root, rel_o, true, target_rel,
+								  params_list);
+
+			/* If inner relation is the target relation, we are done. */
+			if (inner_is_targetrel)
+			{
+				appendStringInfo(buf, "%s", join_sql_o.data);
+				return;
+			}
+		}
+
+		/* Deparse inner relation if not the target relation. */
+		if (!inner_is_targetrel)
+		{
+			initStringInfo(&join_sql_i);
+			deparseFromExprForRel(&join_sql_i, root, rel_i, true, target_rel,
+								  params_list);
+
+			/* If outer relation is the target relation, we are done. */
+			if (outer_is_targetrel)
+			{
+				appendStringInfo(buf, "%s", join_sql_i.data);
+				return;
+			}
+		}
 
-		/* Deparse inner relation */
-		initStringInfo(&join_sql_i);
-		deparseFromExprForRel(&join_sql_i, root, rel_i, true, params_list);
+		/* Neither of the relations is the target relation. */
+		Assert(!outer_is_targetrel && !inner_is_targetrel);
 
 		/*
 		 * For a join relation FROM clause entry is deparsed as
@@ -1525,6 +1588,7 @@ deparseUpdateSql(StringInfo buf, PlannerInfo *root,
 void
 deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 					   Index rtindex, Relation rel,
+					   RelOptInfo *foreignrel,
 					   List *targetlist,
 					   List *targetAttrs,
 					   List *remote_conds,
@@ -1532,7 +1596,6 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 					   List *returningList,
 					   List **retrieved_attrs)
 {
-	RelOptInfo *baserel = root->simple_rel_array[rtindex];
 	deparse_expr_cxt context;
 	int			nestlevel;
 	bool		first;
@@ -1540,13 +1603,15 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 
 	/* Set up context struct for recursion */
 	context.root = root;
-	context.foreignrel = baserel;
-	context.scanrel = baserel;
+	context.foreignrel = foreignrel;
+	context.scanrel = foreignrel;
 	context.buf = buf;
 	context.params_list = params_list;
 
 	appendStringInfoString(buf, "UPDATE ");
 	deparseRelation(buf, rel);
+	if (foreignrel->reloptkind == RELOPT_JOINREL)
+		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
 	appendStringInfoString(buf, " SET ");
 
 	/* Make sure any constants in the exprs are printed portably */
@@ -1573,14 +1638,30 @@ deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 
 	reset_transmission_modes(nestlevel);
 
+	if (foreignrel->reloptkind == RELOPT_JOINREL)
+	{
+		List	   *target_conds = NIL;
+
+		/* Pull up the target relation's conditions into the WHERE clause */
+		pull_up_target_conditions(root, foreignrel, rtindex, &target_conds);
+		remote_conds = list_concat(target_conds, remote_conds);
+
+		appendStringInfo(buf, " FROM ");
+		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+							  params_list);
+	}
+
 	if (remote_conds)
 	{
 		appendStringInfo(buf, " WHERE ");
 		appendConditions(remote_conds, &context);
 	}
 
-	deparseReturningList(buf, root, rtindex, rel, false,
-						 returningList, retrieved_attrs);
+	if (foreignrel->reloptkind == RELOPT_JOINREL)
+		deparseExplicitReturningList(returningList, retrieved_attrs, &context);
+	else
+		deparseReturningList(buf, root, rtindex, rel, false,
+							 returningList, retrieved_attrs);
 }
 
 /*
@@ -1615,23 +1696,38 @@ deparseDeleteSql(StringInfo buf, PlannerInfo *root,
 void
 deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
 					   Index rtindex, Relation rel,
+					   RelOptInfo *foreignrel,
 					   List *remote_conds,
 					   List **params_list,
 					   List *returningList,
 					   List **retrieved_attrs)
 {
-	RelOptInfo *baserel = root->simple_rel_array[rtindex];
 	deparse_expr_cxt context;
 
 	/* Set up context struct for recursion */
 	context.root = root;
-	context.foreignrel = baserel;
-	context.scanrel = baserel;
+	context.foreignrel = foreignrel;
+	context.scanrel = foreignrel;
 	context.buf = buf;
 	context.params_list = params_list;
 
 	appendStringInfoString(buf, "DELETE FROM ");
 	deparseRelation(buf, rel);
+	if (foreignrel->reloptkind == RELOPT_JOINREL)
+		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
+
+	if (foreignrel->reloptkind == RELOPT_JOINREL)
+	{
+		List	   *target_conds = NIL;
+
+		/* Pull up the target relation's conditions into the WHERE clause */
+		pull_up_target_conditions(root, foreignrel, rtindex, &target_conds);
+		remote_conds = list_concat(target_conds, remote_conds);
+
+		appendStringInfo(buf, " USING ");
+		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+							  params_list);
+	}
 
 	if (remote_conds)
 	{
@@ -1639,8 +1735,11 @@ deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
 		appendConditions(remote_conds, &context);
 	}
 
-	deparseReturningList(buf, root, rtindex, rel, false,
-						 returningList, retrieved_attrs);
+	if (foreignrel->reloptkind == RELOPT_JOINREL)
+		deparseExplicitReturningList(returningList, retrieved_attrs, &context);
+	else
+		deparseReturningList(buf, root, rtindex, rel, false,
+							 returningList, retrieved_attrs);
 }
 
 /*
@@ -1680,6 +1779,106 @@ deparseReturningList(StringInfo buf, PlannerInfo *root,
 }
 
 /*
+ * Add a RETURNING clause, if needed, to an UPDATE/DELETE on a join.
+ */
+static void
+deparseExplicitReturningList(List *rlist,
+							 List **retrieved_attrs,
+							 deparse_expr_cxt *context)
+{
+	deparseExplicitTargetList(rlist, true, retrieved_attrs, context);
+}
+
+/*
+ * Look for conditions mentioning the target relation in the given join tree,
+ * which will be pulled up into the WHERE clause.  Note that this is safe due
+ * to the same reason stated in comments in deparseFromExprForRel.
+ */
+static void
+pull_up_target_conditions(PlannerInfo *root, RelOptInfo *foreignrel,
+						  Index target_rel, List **target_conds)
+{
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+	RelOptInfo *outerrel = fpinfo->outerrel;
+	RelOptInfo *innerrel = fpinfo->innerrel;
+
+	Assert(foreignrel->reloptkind == RELOPT_JOINREL);
+	Assert(bms_is_member(target_rel, foreignrel->relids));
+
+	/* No work if not an inner join. */
+	if (fpinfo->jointype == JOIN_INNER)
+	{
+		/* The remote_conds should be empty (see foreign_join_ok). */
+		Assert(fpinfo->remote_conds == NIL);
+
+		/*
+		 * If either input is the target relation, get all the join clauses.
+		 * Otherwise extract conditions mentioning the target relation from
+		 * the join clauses.
+		 */
+		if ((outerrel->reloptkind == RELOPT_BASEREL &&
+			 outerrel->relid == target_rel) ||
+			(innerrel->reloptkind == RELOPT_BASEREL &&
+			 innerrel->relid == target_rel))
+		{
+			*target_conds = list_concat(*target_conds,
+										list_copy(fpinfo->joinclauses));
+			fpinfo->joinclauses = NIL;
+		}
+		else
+			extract_target_conditions(&fpinfo->joinclauses,
+									  target_rel, target_conds);
+	}
+
+	/* Recurse into either input relation. */
+	if (outerrel->reloptkind == RELOPT_JOINREL &&
+		bms_is_member(target_rel, outerrel->relids))
+		pull_up_target_conditions(root, outerrel, target_rel, target_conds);
+	else if (innerrel->reloptkind == RELOPT_JOINREL &&
+			 bms_is_member(target_rel, innerrel->relids))
+		pull_up_target_conditions(root, innerrel, target_rel, target_conds);
+}
+
+/*
+ * Extract conditions from *joinclauses, separating those that mention
+ * the given relation from those that don't.
+ */
+static void
+extract_target_conditions(List **joinclauses,	/* in/out parameters */
+						  Index target_rel,
+						  List **target_conds	/* output parameters */)
+{
+	List	   *other_conds = NIL;
+	ListCell   *lc;
+
+	foreach(lc, *joinclauses)
+	{
+		Node	   *clause = (Node *) lfirst(lc);
+		Relids		relids;
+
+		/* Extract clause from RestrictInfo, if needed. */
+		if (IsA(clause, RestrictInfo))
+		{
+			RestrictInfo *ri = (RestrictInfo *) clause;
+
+			clause = (Node *) ri->clause;
+		}
+
+		/* Retrieve all relids mentioned within the clause. */
+		relids = pull_varnos(clause);
+
+		/* Classify the clause as mentioning the given relation or not. */
+		if (bms_is_member(target_rel, relids))
+			*target_conds = lappend(*target_conds, clause);
+		else
+			other_conds = lappend(other_conds, clause);
+	}
+
+	/* Replace *joinclauses. */
+	*joinclauses = other_conds;
+}
+
+/*
  * Construct SELECT statement to acquire size in blocks of given relation.
  *
  * Note: we use local definition of block size, not remote definition.
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 0b9e3e4..1834f0f 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -4088,27 +4088,13 @@ UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
 
 EXPLAIN (verbose, costs off)
 UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
-  FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
-                                                                                                                                                        QUERY PLAN                                                                                                                                                         
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
+                                                                                                   QUERY PLAN                                                                                                    
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Update on public.ft2
-   Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3, c7 = $4 WHERE ctid = $1
-   ->  Foreign Scan
-         Output: ft2.c1, (ft2.c2 + 500), NULL::integer, (ft2.c3 || '_update9'::text), ft2.c4, ft2.c5, ft2.c6, 'ft2       '::character(10), ft2.c8, ft2.ctid, ft1.*
-         Relations: (public.ft2) INNER JOIN (public.ft1)
-         Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c8, r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9)))) FOR UPDATE OF r1
-         ->  Hash Join
-               Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid, ft1.*
-               Hash Cond: (ft2.c2 = ft1.c1)
-               ->  Foreign Scan on public.ft2
-                     Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid
-                     Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c8, ctid FROM "S 1"."T 1" FOR UPDATE
-               ->  Hash
-                     Output: ft1.*, ft1.c1
-                     ->  Foreign Scan on public.ft1
-                           Output: ft1.*, ft1.c1
-                           Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))
-(17 rows)
+   ->  Foreign Update
+         Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 500), c3 = (r1.c3 || '_update9'::text), c7 = 'ft2       '::character(10) FROM "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9))
+(3 rows)
 
 UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
@@ -4231,27 +4217,13 @@ DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
 (103 rows)
 
 EXPLAIN (verbose, costs off)
-DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
-                                                                                                                              QUERY PLAN                                                                                                                               
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
+                                                         QUERY PLAN                                                         
+----------------------------------------------------------------------------------------------------------------------------
  Delete on public.ft2
-   Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1
-   ->  Foreign Scan
-         Output: ft2.ctid, ft1.*
-         Relations: (public.ft2) INNER JOIN (public.ft1)
-         Remote SQL: SELECT r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2)))) FOR UPDATE OF r1
-         ->  Hash Join
-               Output: ft2.ctid, ft1.*
-               Hash Cond: (ft2.c2 = ft1.c1)
-               ->  Foreign Scan on public.ft2
-                     Output: ft2.ctid, ft2.c2
-                     Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" FOR UPDATE
-               ->  Hash
-                     Output: ft1.*, ft1.c1
-                     ->  Foreign Scan on public.ft1
-                           Output: ft1.*, ft1.c1
-                           Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))
-(17 rows)
+   ->  Foreign Delete
+         Remote SQL: DELETE FROM "S 1"."T 1" r1 USING "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2))
+(3 rows)
 
 DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
 SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
@@ -5127,6 +5099,175 @@ DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;
  ft2
 (1 row)
 
+INSERT INTO ft2 (c1,c2,c3)
+  SELECT id, id % 10, to_char(id, 'FM00000') FROM generate_series(10001, 10100) id;
+CREATE TABLE j1_tbl (c1 int NOT NULL, c2 text);
+CREATE TABLE j2_tbl (c1 int NOT NULL, c2 text);
+INSERT INTO j1_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+INSERT INTO j2_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+DELETE FROM j2_tbl WHERE c1 % 2 != 0;
+CREATE FOREIGN TABLE j1_ftbl (c1 int NOT NULL, c2 text)
+  SERVER loopback OPTIONS (table_name 'j1_tbl');
+CREATE FOREIGN TABLE j2_ftbl (c1 int NOT NULL, c2 text)
+  SERVER loopback OPTIONS (table_name 'j2_tbl');
+ANALYZE j1_ftbl;
+ANALYZE j2_ftbl;
+EXPLAIN (verbose, costs off)
+UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+  FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+  RETURNING ft2.*;
+                                                                                                                                                     QUERY PLAN                                                                                                                                                     
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.ft2
+   Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8
+   ->  Foreign Update
+         Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 200), c3 = (r1.c3 || '_update2'::text) FROM (public.j1_tbl r2 INNER JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 2)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8
+(4 rows)
+
+UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+  FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+  RETURNING ft2.*;
+  c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 
+-------+-----+---------------+----+----+----+------------+----
+ 10002 | 202 | 10002_update2 |    |    |    | ft2        | 
+ 10012 | 202 | 10012_update2 |    |    |    | ft2        | 
+ 10022 | 202 | 10022_update2 |    |    |    | ft2        | 
+ 10032 | 202 | 10032_update2 |    |    |    | ft2        | 
+ 10042 | 202 | 10042_update2 |    |    |    | ft2        | 
+ 10052 | 202 | 10052_update2 |    |    |    | ft2        | 
+ 10062 | 202 | 10062_update2 |    |    |    | ft2        | 
+ 10072 | 202 | 10072_update2 |    |    |    | ft2        | 
+ 10082 | 202 | 10082_update2 |    |    |    | ft2        | 
+ 10092 | 202 | 10092_update2 |    |    |    | ft2        | 
+(10 rows)
+
+EXPLAIN (verbose, costs off)
+UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+  FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+  WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+  RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+                                                                                                                                                                                               QUERY PLAN                                                                                                                                                                                               
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Update on public.ft2
+   Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.c1, j2_ftbl.c2
+   ->  Foreign Update
+         Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 800), c3 = (r1.c3 || '_update8'::text) FROM (("S 1"."T 1" r2 INNER JOIN public.j1_tbl r3 ON (((r2."C 1" = (r3.c1 + 10000))))) LEFT JOIN public.j2_tbl r4 ON (((r3.c1 = r4.c1)))) WHERE ((r1."C 1" = r2."C 1")) AND (((r1."C 1" % 10) = 8)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r3.c1, r3.c2, r4.c1, r4.c2
+(4 rows)
+
+UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+  FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+  WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+  RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+  c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 | c1 |  c2   | c1 |  c2   
+-------+-----+---------------+----+----+----+------------+----+----+-------+----+-------
+ 10008 | 808 | 10008_update8 |    |    |    | ft2        |    |  8 | 00008 |  8 | 00008
+ 10018 | 808 | 10018_update8 |    |    |    | ft2        |    | 18 | 00018 | 18 | 00018
+ 10028 | 808 | 10028_update8 |    |    |    | ft2        |    | 28 | 00028 | 28 | 00028
+ 10038 | 808 | 10038_update8 |    |    |    | ft2        |    | 38 | 00038 | 38 | 00038
+ 10048 | 808 | 10048_update8 |    |    |    | ft2        |    | 48 | 00048 | 48 | 00048
+ 10058 | 808 | 10058_update8 |    |    |    | ft2        |    | 58 | 00058 | 58 | 00058
+ 10068 | 808 | 10068_update8 |    |    |    | ft2        |    | 68 | 00068 | 68 | 00068
+ 10078 | 808 | 10078_update8 |    |    |    | ft2        |    | 78 | 00078 | 78 | 00078
+ 10088 | 808 | 10088_update8 |    |    |    | ft2        |    | 88 | 00088 | 88 | 00088
+ 10098 | 808 | 10098_update8 |    |    |    | ft2        |    | 98 | 00098 | 98 | 00098
+(10 rows)
+
+EXPLAIN (verbose, costs off)
+DELETE FROM ft2
+  USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+  RETURNING 100;
+                                                                                         QUERY PLAN                                                                                          
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Delete on public.ft2
+   Output: 100
+   ->  Foreign Delete
+         Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (public.j1_tbl r2 INNER JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 2))
+(4 rows)
+
+DELETE FROM ft2
+  USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+  RETURNING 100;
+ ?column? 
+----------
+      100
+      100
+      100
+      100
+      100
+      100
+      100
+      100
+      100
+      100
+(10 rows)
+
+EXPLAIN (verbose, costs off)
+DELETE FROM ft2
+  USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+  RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+                                                                                                                                     QUERY PLAN                                                                                                                                     
+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Delete on public.ft2
+   Output: ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.ctid, j2_ftbl.c1, j2_ftbl.c2
+   ->  Foreign Delete
+         Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (public.j1_tbl r2 LEFT JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 0)) RETURNING r1.ctid, r1."C 1", r1.c2, r1.c3, r2.ctid, r2.c1, r2.c2, r3.ctid, r3.c1, r3.c2
+(4 rows)
+
+DELETE FROM ft2
+  USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+  RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+   ctid   |  c1   | c2 |  c3   |  ctid   | c1  |  c2   |  ctid   | c1  |  c2   
+----------+-------+----+-------+---------+-----+-------+---------+-----+-------
+ (12,11)  | 10010 |  0 | 10010 | (0,10)  |  10 | 00010 | (0,10)  |  10 | 00010
+ (12,21)  | 10020 |  0 | 10020 | (0,20)  |  20 | 00020 | (0,20)  |  20 | 00020
+ (12,31)  | 10030 |  0 | 10030 | (0,30)  |  30 | 00030 | (0,30)  |  30 | 00030
+ (12,41)  | 10040 |  0 | 10040 | (0,40)  |  40 | 00040 | (0,40)  |  40 | 00040
+ (12,51)  | 10050 |  0 | 10050 | (0,50)  |  50 | 00050 | (0,50)  |  50 | 00050
+ (12,61)  | 10060 |  0 | 10060 | (0,60)  |  60 | 00060 | (0,60)  |  60 | 00060
+ (12,71)  | 10070 |  0 | 10070 | (0,70)  |  70 | 00070 | (0,70)  |  70 | 00070
+ (12,81)  | 10080 |  0 | 10080 | (0,80)  |  80 | 00080 | (0,80)  |  80 | 00080
+ (12,91)  | 10090 |  0 | 10090 | (0,90)  |  90 | 00090 | (0,90)  |  90 | 00090
+ (12,101) | 10100 |  0 | 10100 | (0,100) | 100 | 00100 | (0,100) | 100 | 00100
+(10 rows)
+
+EXPLAIN (verbose, costs off)
+DELETE FROM ft2
+  USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+  WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+  RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+                                                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                                                     
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Delete on public.ft2
+   Output: ft2.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, j1_ftbl.*, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.*, j2_ftbl.c1, j2_ftbl.c2
+   ->  Foreign Delete
+         Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (("S 1"."T 1" r2 INNER JOIN public.j1_tbl r3 ON (((r2."C 1" = (r3.c1 + 10000))))) LEFT JOIN public.j2_tbl r4 ON (((r3.c1 = r4.c1)))) WHERE ((r1."C 1" = r2."C 1")) AND (((r1."C 1" % 10) = 8)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2) END, r3.c1, r3.c2, CASE WHEN (r4.*)::text IS NOT NULL THEN ROW(r4.c1, r4.c2) END, r4.c1, r4.c2
+(4 rows)
+
+DELETE FROM ft2
+  USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+  WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+  RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+                    ft2                     |  c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 |  j1_ftbl   | c1 |  c2   |  j2_ftbl   | c1 |  c2   
+--------------------------------------------+-------+-----+---------------+----+----+----+------------+----+------------+----+-------+------------+----+-------
+ (10008,808,10008_update8,,,,"ft2       ",) | 10008 | 808 | 10008_update8 |    |    |    | ft2        |    | (8,00008)  |  8 | 00008 | (8,00008)  |  8 | 00008
+ (10018,808,10018_update8,,,,"ft2       ",) | 10018 | 808 | 10018_update8 |    |    |    | ft2        |    | (18,00018) | 18 | 00018 | (18,00018) | 18 | 00018
+ (10028,808,10028_update8,,,,"ft2       ",) | 10028 | 808 | 10028_update8 |    |    |    | ft2        |    | (28,00028) | 28 | 00028 | (28,00028) | 28 | 00028
+ (10038,808,10038_update8,,,,"ft2       ",) | 10038 | 808 | 10038_update8 |    |    |    | ft2        |    | (38,00038) | 38 | 00038 | (38,00038) | 38 | 00038
+ (10048,808,10048_update8,,,,"ft2       ",) | 10048 | 808 | 10048_update8 |    |    |    | ft2        |    | (48,00048) | 48 | 00048 | (48,00048) | 48 | 00048
+ (10058,808,10058_update8,,,,"ft2       ",) | 10058 | 808 | 10058_update8 |    |    |    | ft2        |    | (58,00058) | 58 | 00058 | (58,00058) | 58 | 00058
+ (10068,808,10068_update8,,,,"ft2       ",) | 10068 | 808 | 10068_update8 |    |    |    | ft2        |    | (68,00068) | 68 | 00068 | (68,00068) | 68 | 00068
+ (10078,808,10078_update8,,,,"ft2       ",) | 10078 | 808 | 10078_update8 |    |    |    | ft2        |    | (78,00078) | 78 | 00078 | (78,00078) | 78 | 00078
+ (10088,808,10088_update8,,,,"ft2       ",) | 10088 | 808 | 10088_update8 |    |    |    | ft2        |    | (88,00088) | 88 | 00088 | (88,00088) | 88 | 00088
+ (10098,808,10098_update8,,,,"ft2       ",) | 10098 | 808 | 10098_update8 |    |    |    | ft2        |    | (98,00098) | 98 | 00098 | (98,00098) | 98 | 00098
+(10 rows)
+
+DELETE FROM ft2 WHERE ft2.c1 > 10000;
 -- Test that trigger on remote table works as expected
 CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
 BEGIN
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 5d270b9..4fff4e9 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -211,6 +211,12 @@ typedef struct PgFdwDirectModifyState
 	PGresult   *result;			/* result for query */
 	int			num_tuples;		/* # of result tuples */
 	int			next_tuple;		/* index of next one to return */
+	Relation	resultRel;		/* relcache entry for the target table */
+	TupleTableSlot *resultSlot;	/* slot for updated/deleted tuple */
+	AttrNumber *attnoMap;		/* array of attnums of input user columns */
+	AttrNumber	ctidAttno;		/* attnum of input ctid column */
+	AttrNumber	oidAttno;		/* attnum of input oid column */
+	bool		hasSystemCols;	/* are there system columns of resultRel? */
 
 	/* working memory context */
 	MemoryContext temp_cxt;		/* context for per-tuple temporary data */
@@ -377,8 +383,18 @@ static const char **convert_prep_stmt_params(PgFdwModifyState *fmstate,
 						 TupleTableSlot *slot);
 static void store_returning_result(PgFdwModifyState *fmstate,
 					   TupleTableSlot *slot, PGresult *res);
+static List *make_explicit_returning_list(Index rtindex, Relation rel,
+										  List *returningList);
+static List *rewrite_fdw_scan_tlist(List *fdw_scan_tlist,  Index rtindex,
+					   List *returningList);
 static void execute_dml_stmt(ForeignScanState *node);
 static TupleTableSlot *get_returning_data(ForeignScanState *node);
+static void init_returning_filter(PgFdwDirectModifyState *dmstate,
+					  List *fdw_scan_tlist,
+					  Index rtindex,
+					  EState *estate);
+static TupleTableSlot *apply_returning_filter(PgFdwDirectModifyState *dmstate,
+					   TupleTableSlot *slot);
 static void prepare_query_params(PlanState *node,
 					 List *fdw_exprs,
 					 int numParams,
@@ -2106,6 +2122,7 @@ postgresPlanDirectModify(PlannerInfo *root,
 	Relation	rel;
 	StringInfoData sql;
 	ForeignScan *fscan;
+	RelOptInfo *foreignrel;
 	List	   *targetAttrs = NIL;
 	List	   *remote_conds;
 	List	   *params_list = NIL;
@@ -2137,13 +2154,6 @@ postgresPlanDirectModify(PlannerInfo *root,
 		return false;
 
 	/*
-	 * We can't handle an UPDATE or DELETE on a foreign join for now.
-	 */
-	fscan = (ForeignScan *) subplan;
-	if (fscan->scan.scanrelid == 0)
-		return false;
-
-	/*
 	 * It's unsafe to update a foreign table directly, if any expressions to
 	 * assign to the target columns are unsafe to evaluate remotely.
 	 */
@@ -2182,6 +2192,8 @@ postgresPlanDirectModify(PlannerInfo *root,
 	/*
 	 * Ok, rewrite subplan so as to modify the foreign table directly.
 	 */
+	fscan = (ForeignScan *) subplan;
+
 	initStringInfo(&sql);
 
 	/*
@@ -2191,6 +2203,18 @@ postgresPlanDirectModify(PlannerInfo *root,
 	rel = heap_open(rte->relid, NoLock);
 
 	/*
+	 * Get a rel for this foreign table or join.
+	 */
+	if (fscan->scan.scanrelid == 0)
+	{
+		foreignrel = find_join_rel(root, fscan->fs_relids);
+		/* We should have a rel for this foreign join. */
+		Assert(foreignrel);
+	}
+	else
+		foreignrel = find_base_rel(root, resultRelation);
+
+	/*
 	 * Extract the baserestrictinfo clauses that can be evaluated remotely.
 	 */
 	remote_conds = (List *) list_nth(fscan->fdw_private,
@@ -2200,8 +2224,19 @@ postgresPlanDirectModify(PlannerInfo *root,
 	 * Extract the relevant RETURNING list if any.
 	 */
 	if (plan->returningLists)
+	{
 		returningList = (List *) list_nth(plan->returningLists, subplan_index);
 
+		/*
+		 * If UPDATE/DELETE on a join, create a RETURNING list used in the
+		 * remote query.
+		 */
+		if (fscan->scan.scanrelid == 0)
+			returningList = make_explicit_returning_list(resultRelation,
+														 rel,
+														 returningList);
+	}
+
 	/*
 	 * Construct the SQL command string.
 	 */
@@ -2209,6 +2244,7 @@ postgresPlanDirectModify(PlannerInfo *root,
 	{
 		case CMD_UPDATE:
 			deparseDirectUpdateSql(&sql, root, resultRelation, rel,
+								   foreignrel,
 								   ((Plan *) fscan)->targetlist,
 								   targetAttrs,
 								   remote_conds, &params_list,
@@ -2216,6 +2252,7 @@ postgresPlanDirectModify(PlannerInfo *root,
 			break;
 		case CMD_DELETE:
 			deparseDirectDeleteSql(&sql, root, resultRelation, rel,
+								   foreignrel,
 								   remote_conds, &params_list,
 								   returningList, &retrieved_attrs);
 			break;
@@ -2243,6 +2280,21 @@ postgresPlanDirectModify(PlannerInfo *root,
 									retrieved_attrs,
 									makeInteger(plan->canSetTag));
 
+	/*
+	 * Update the foreign-join-related fields.
+	 */
+	if (fscan->scan.scanrelid == 0)
+	{
+		/* No need for the outer subplan. */
+		fscan->scan.plan.lefttree = NULL;
+
+		/* If having RETURNING, rewrite fdw_scan_tlist to include it. */
+		if (returningList)
+			fscan->fdw_scan_tlist =
+				rewrite_fdw_scan_tlist(fscan->fdw_scan_tlist, resultRelation,
+									   returningList);
+	}
+
 	heap_close(rel, NoLock);
 	return true;
 }
@@ -2257,6 +2309,7 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
 	ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
 	EState	   *estate = node->ss.ps.state;
 	PgFdwDirectModifyState *dmstate;
+	Index		rtindex;
 	RangeTblEntry *rte;
 	Oid			userid;
 	ForeignTable *table;
@@ -2279,11 +2332,15 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
 	 * Identify which user to do the remote access as.  This should match what
 	 * ExecCheckRTEPerms() does.
 	 */
-	rte = rt_fetch(fsplan->scan.scanrelid, estate->es_range_table);
+	rtindex = estate->es_result_relation_info->ri_RangeTableIndex;
+	rte = rt_fetch(rtindex, estate->es_range_table);
 	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
 
 	/* Get info about foreign table. */
-	dmstate->rel = node->ss.ss_currentRelation;
+	if (fsplan->scan.scanrelid == 0)
+		dmstate->rel = ExecOpenScanRelation(estate, rtindex, eflags);
+	else
+		dmstate->rel = node->ss.ss_currentRelation;
 	table = GetForeignTable(RelationGetRelid(dmstate->rel));
 	user = GetUserMapping(userid, table->serverid);
 
@@ -2293,6 +2350,16 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
 	 */
 	dmstate->conn = GetConnection(user, false);
 
+	/* Update the foreign-join-related fields. */
+	if (fsplan->scan.scanrelid == 0)
+	{
+		/* Save info about target table. */
+		dmstate->resultRel = dmstate->rel;
+
+		/* rel should be NULL. */
+		dmstate->rel = NULL;
+	}
+
 	/* Initialize state variable */
 	dmstate->num_tuples = -1;	/* -1 means not set yet */
 
@@ -2313,7 +2380,24 @@ postgresBeginDirectModify(ForeignScanState *node, int eflags)
 
 	/* Prepare for input conversion of RETURNING results. */
 	if (dmstate->has_returning)
-		dmstate->attinmeta = TupleDescGetAttInMetadata(RelationGetDescr(dmstate->rel));
+	{
+		TupleDesc	tupdesc;
+
+		if (fsplan->scan.scanrelid == 0)
+			tupdesc = node->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
+		else
+			tupdesc = RelationGetDescr(dmstate->rel);
+
+		dmstate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
+
+		/*
+		 * If UPDATE/DELETE on a join, initialize a filter to extract a result
+		 * tuple from a scan tuple.
+		 */
+		if (fsplan->scan.scanrelid == 0)
+			init_returning_filter(dmstate, fsplan->fdw_scan_tlist,
+								  rtindex, estate);
+	}
 
 	/*
 	 * Prepare for processing of parameters used in remote query, if any.
@@ -2394,6 +2478,10 @@ postgresEndDirectModify(ForeignScanState *node)
 	ReleaseConnection(dmstate->conn);
 	dmstate->conn = NULL;
 
+	/* close the result relation. */
+	if (dmstate->resultRel)
+		ExecCloseScanRelation(dmstate->resultRel);
+
 	/* MemoryContext will be deleted automatically. */
 }
 
@@ -3262,6 +3350,128 @@ store_returning_result(PgFdwModifyState *fmstate,
 }
 
 /*
+ * Create an explicit RETURNING list used in the remote query.
+ */
+static List *
+make_explicit_returning_list(Index rtindex, Relation rel, List *returningList)
+{
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	bool		have_wholerow = false;
+	List	   *rlist = NIL;
+	List	   *vars;
+	ListCell   *lc;
+
+	if (returningList == NIL)
+		return NIL;
+
+	vars = pull_var_clause((Node *) returningList, PVC_INCLUDE_PLACEHOLDERS);
+
+	/*
+	 * If there's a whole-row reference to the target relation, then we'll need
+	 * all the columns of the relation.
+	 */
+	foreach(lc, vars)
+	{
+		Var		   *var = (Var *) lfirst(lc);
+
+		if (IsA(var, Var) &&
+			var->varno == rtindex &&
+			var->varattno == InvalidAttrNumber)
+		{
+			have_wholerow = true;
+			break;
+		}
+	}
+
+	if (have_wholerow)
+	{
+		int			i;
+
+		for (i = 1; i <= tupdesc->natts; i++)
+		{
+			Form_pg_attribute attr = tupdesc->attrs[i - 1];
+			Var		   *var;
+
+			/* Ignore dropped attributes. */
+			if (attr->attisdropped)
+				continue;
+
+			var = makeVar(rtindex,
+						  i,
+						  attr->atttypid,
+						  attr->atttypmod,
+						  attr->attcollation,
+						  0);
+
+			rlist = lappend(rlist,
+							makeTargetEntry((Expr *) var,
+											list_length(rlist) + 1,
+											NULL,
+											false));
+		}
+	}
+
+	/* Now add any remaining columns to rlist. */
+	foreach(lc, vars)
+	{
+		Var		   *var = (Var *) lfirst(lc);
+
+		/*
+		 * No need for whole-row references to the target relation.  We don't
+		 * need system columns other than ctid and oid either, since those are
+		 * set locally.
+		 */
+		if (IsA(var, Var) &&
+			var->varno == rtindex &&
+			var->varattno <= InvalidAttrNumber &&
+			var->varattno != SelfItemPointerAttributeNumber &&
+			var->varattno != ObjectIdAttributeNumber)
+			continue;		/* don't need it */
+
+		if (tlist_member((Node *) var, rlist))
+			continue;		/* already got it */
+
+		rlist = lappend(rlist,
+						makeTargetEntry((Expr *) var,
+										list_length(rlist) + 1,
+										NULL,
+										false));
+	}
+
+	list_free(vars);
+
+	return rlist;
+}
+
+/*
+ * Rewrite the given fdw_scan_tlist so it contains all the expressions
+ * specified in the RETURNING list.
+ */
+static List *
+rewrite_fdw_scan_tlist(List *fdw_scan_tlist,  Index rtindex,
+					   List *returningList)
+{
+	List	   *tlist = list_copy(returningList);
+	ListCell   *lc;
+
+	foreach(lc, fdw_scan_tlist)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+		if (tlist_member((Node *) tle->expr, tlist))
+			continue;		/* already got it */
+
+		tlist = lappend(tlist,
+						makeTargetEntry(tle->expr,
+										list_length(tlist) + 1,
+										NULL,
+										false));
+	}
+
+	return tlist;
+}
+
+/*
  * Execute a direct UPDATE/DELETE statement.
  */
 static void
@@ -3321,6 +3531,7 @@ get_returning_data(ForeignScanState *node)
 	EState	   *estate = node->ss.ps.state;
 	ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
 	TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+	TupleTableSlot *resultSlot;
 
 	Assert(resultRelInfo->ri_projectReturning);
 
@@ -3338,7 +3549,10 @@ get_returning_data(ForeignScanState *node)
 	 * "UPDATE/DELETE .. RETURNING 1" for example.)
 	 */
 	if (!dmstate->has_returning)
+	{
 		ExecStoreAllNullTuple(slot);
+		resultSlot = slot;
+	}
 	else
 	{
 		/*
@@ -3354,7 +3568,7 @@ get_returning_data(ForeignScanState *node)
 												dmstate->rel,
 												dmstate->attinmeta,
 												dmstate->retrieved_attrs,
-												NULL,
+												node,
 												dmstate->temp_cxt);
 			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
 		}
@@ -3365,16 +3579,197 @@ get_returning_data(ForeignScanState *node)
 			PG_RE_THROW();
 		}
 		PG_END_TRY();
+
+		/* Get the updated/deleted tuple. */
+		if (dmstate->rel)
+			resultSlot = slot;
+		else
+			resultSlot = apply_returning_filter(dmstate, slot);
 	}
 	dmstate->next_tuple++;
 
 	/* Make slot available for evaluation of the local query RETURNING list. */
-	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = slot;
+	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = resultSlot;
 
 	return slot;
 }
 
 /*
+ * Initialize a filter to extract an updated/deleted tuple from a scan tuple.
+ */
+static void
+init_returning_filter(PgFdwDirectModifyState *dmstate,
+					  List *fdw_scan_tlist,
+					  Index rtindex,
+					  EState *estate)
+{
+	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+	ListCell   *lc;
+	int			i;
+
+	/* Make a new slot for storing the result tuple. */
+	dmstate->resultSlot = ExecInitExtraTupleSlot(estate);
+
+	ExecSetSlotDescriptor(dmstate->resultSlot, resultTupType);
+
+	/*
+	 * Calculate the mapping between the fdw_scan_tlist's entries and the
+	 * result tuple's attributes.
+	 *
+	 * The "map" is an array of indexes of the result tuple's attributes in
+	 * fdw_scan_tlist, i.e., one entry for every attribute of the result
+	 * tuple.  We store zero for any attributes that don't have the
+	 * corresponding entries in that list, marking that a NULL is needed in
+	 * the result tuple.
+	 *
+	 * Also get the indexes of the entries for ctid and oid if any.
+	 */
+	dmstate->attnoMap = (AttrNumber *) palloc0(resultTupType->natts * sizeof(AttrNumber));
+
+	dmstate->ctidAttno = dmstate->oidAttno = 0;
+
+	i = 1;
+	dmstate->hasSystemCols = false;
+	foreach(lc, fdw_scan_tlist)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+		Var		   *var = (Var *) tle->expr;
+
+		if (list_member_int(dmstate->retrieved_attrs, i))
+		{
+			if (IsA(var, Var) && var->varno == rtindex)
+			{
+				int			attrno = var->varattno;
+
+				if (attrno < 0)
+				{
+				  	/*
+					 * We don't retrieve system columns other than ctid and
+					 * oid.
+					 */
+					if (attrno == SelfItemPointerAttributeNumber)
+						dmstate->ctidAttno = i;
+					else if (attrno == ObjectIdAttributeNumber)
+						dmstate->oidAttno = i;
+					else
+						Assert(false);
+					dmstate->hasSystemCols = true;
+				}
+				else
+				{
+					/*
+					 * We don't retrieve whole-row references to the result
+					 * relation either.
+					 */
+					Assert(attrno > 0);
+					dmstate->attnoMap[attrno - 1] = i;
+				}
+			}
+		}
+		i++;
+	}
+}
+
+/*
+ * Extract and return an updated/deleted tuple from a scan tuple.
+ */
+static TupleTableSlot *
+apply_returning_filter(PgFdwDirectModifyState *dmstate,
+					   TupleTableSlot *slot)
+{
+	TupleTableSlot *resultSlot = dmstate->resultSlot;
+	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+	Datum	   *values;
+	bool	   *isnull;
+	Datum	   *old_values;
+	bool	   *old_isnull;
+	int			i;
+
+	/*
+	 * Extract all the values of the scan tuple.
+	 */
+	slot_getallattrs(slot);
+	old_values = slot->tts_values;
+	old_isnull = slot->tts_isnull;
+
+	/*
+	 * Prepare to build the result tuple.
+	 */
+	ExecClearTuple(resultSlot);
+	values = resultSlot->tts_values;
+	isnull = resultSlot->tts_isnull;
+
+	/*
+	 * Transpose data into proper fields of the result tuple.
+	 */
+	for (i = 0; i < resultTupType->natts; i++)
+	{
+		int			j = dmstate->attnoMap[i];
+
+		if (j == 0)
+		{
+			values[i] = (Datum) 0;
+			isnull[i] = true;
+		}
+		else
+		{
+			values[i] = old_values[j - 1];
+			isnull[i] = old_isnull[j - 1];
+		}
+	}
+
+	/*
+	 * Build the virtual tuple.
+	 */
+	ExecStoreVirtualTuple(resultSlot);
+
+	/*
+	 * If we have any system columns to return, install them.
+	 */
+	if (dmstate->hasSystemCols)
+	{
+		HeapTuple	resultTup = ExecMaterializeSlot(resultSlot);
+
+		/* ctid */
+		if (dmstate->ctidAttno)
+		{
+			ItemPointer ctid = NULL;
+
+			ctid = (ItemPointer) DatumGetPointer(old_values[dmstate->ctidAttno - 1]);
+			resultTup->t_self = *ctid;
+		}
+
+		/* oid */
+		if (dmstate->oidAttno)
+		{
+			Oid			oid = InvalidOid;
+
+			oid = DatumGetObjectId(old_values[dmstate->oidAttno - 1]);
+			HeapTupleSetOid(resultTup, oid);
+		}
+
+		/*
+		 * And remaining columns
+		 *
+		 * Note: since we currently don't allow the result relation to appear
+		 * on the nullable side of an outer join, any system columns wouldn't
+		 * go to NULL.
+		 *
+		 * Note: no need to care about tableoid here because it will be
+		 * initialized in ExecProcessReturning().
+		 */
+		HeapTupleHeaderSetXmin(resultTup->t_data, InvalidTransactionId);
+		HeapTupleHeaderSetXmax(resultTup->t_data, InvalidTransactionId);
+		HeapTupleHeaderSetCmin(resultTup->t_data, InvalidTransactionId);
+	}
+
+	/*
+	 * And return the result tuple.
+	 */
+	return resultSlot;
+}
+
+/*
  * Prepare for processing of parameters used in remote query.
  */
 static void
@@ -4794,11 +5189,8 @@ make_tuple_from_result_row(PGresult *res,
 		tupdesc = RelationGetDescr(rel);
 	else
 	{
-		PgFdwScanState *fdw_sstate;
-
 		Assert(fsstate);
-		fdw_sstate = (PgFdwScanState *) fsstate->fdw_state;
-		tupdesc = fdw_sstate->tupdesc;
+		tupdesc = fsstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
 	}
 
 	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 46cac55..a806b51 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -137,6 +137,7 @@ extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
 				 List **retrieved_attrs);
 extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
 					   Index rtindex, Relation rel,
+					   RelOptInfo *foreignrel,
 					   List *targetlist,
 					   List *targetAttrs,
 					   List *remote_conds,
@@ -149,6 +150,7 @@ extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
 				 List **retrieved_attrs);
 extern void deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
 					   Index rtindex, Relation rel,
+					   RelOptInfo *foreignrel,
 					   List *remote_conds,
 					   List **params_list,
 					   List *returningList,
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 56b01d0..6cd31df 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -1010,14 +1010,14 @@ UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
 UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
 EXPLAIN (verbose, costs off)
 UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
-  FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
+  FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
 UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
 EXPLAIN (verbose, costs off)
   DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
 DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
 EXPLAIN (verbose, costs off)
-DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
+DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
 DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
 SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
 EXPLAIN (verbose, costs off)
@@ -1030,6 +1030,66 @@ EXPLAIN (verbose, costs off)
 DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;                       -- can be pushed down
 DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;
 
+INSERT INTO ft2 (c1,c2,c3)
+  SELECT id, id % 10, to_char(id, 'FM00000') FROM generate_series(10001, 10100) id;
+CREATE TABLE j1_tbl (c1 int NOT NULL, c2 text);
+CREATE TABLE j2_tbl (c1 int NOT NULL, c2 text);
+INSERT INTO j1_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+INSERT INTO j2_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+DELETE FROM j2_tbl WHERE c1 % 2 != 0;
+CREATE FOREIGN TABLE j1_ftbl (c1 int NOT NULL, c2 text)
+  SERVER loopback OPTIONS (table_name 'j1_tbl');
+CREATE FOREIGN TABLE j2_ftbl (c1 int NOT NULL, c2 text)
+  SERVER loopback OPTIONS (table_name 'j2_tbl');
+ANALYZE j1_ftbl;
+ANALYZE j2_ftbl;
+EXPLAIN (verbose, costs off)
+UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+  FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+  RETURNING ft2.*;
+UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+  FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+  RETURNING ft2.*;
+EXPLAIN (verbose, costs off)
+UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+  FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+  WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+  RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+  FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+  WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+  RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+EXPLAIN (verbose, costs off)
+DELETE FROM ft2
+  USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+  RETURNING 100;
+DELETE FROM ft2
+  USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+  RETURNING 100;
+EXPLAIN (verbose, costs off)
+DELETE FROM ft2
+  USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+  RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+DELETE FROM ft2
+  USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+  WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+  RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+EXPLAIN (verbose, costs off)
+DELETE FROM ft2
+  USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+  WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+  RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+DELETE FROM ft2
+  USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+  WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+  RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+DELETE FROM ft2 WHERE ft2.c1 > 10000;
+
 -- Test that trigger on remote table works as expected
 CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
 BEGIN
#19Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Rushabh Lathia (#18)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 2017/02/21 19:31, Rushabh Lathia wrote:

On Mon, Feb 20, 2017 at 1:41 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp <mailto:fujita.etsuro@lab.ntt.co.jp>> wrote:

On 2017/02/13 18:24, Rushabh Lathia wrote:
Here are few comments:

1)

@@ -211,6 +211,12 @@ typedef struct PgFdwDirectModifyState
PGresult   *result;            /* result for query */
int            num_tuples;        /* # of result tuples */
int            next_tuple;        /* index of next one to
return */
+    Relation    resultRel;        /* relcache entry for the
target table */

Why we need resultRel? Can't we directly use dmstate->rel ?

The reason why we need that is because in get_returning_data, we
pass dmstate->rel to make_tuple_from_result_row, which requires that
dmstate->rel be NULL when the scan tuple is described by
fdw_scan_tlist. So in that case we set dmstate->rel to NULL and
have dmstate->resultRel that is the relcache entry for the target
relation in postgresBeginDirectModify.

Thanks for the explanation. We might do something here by using
fdw_scan_tlist or changing the assumption of
make_tuple_from_result_row(), and that way we can avoid two similar
variable pointer in the PgFdwDirectModifyState.

I agree that the two similar variables are annoying, to some extent, but
ISTM that is not that bad because that makes the handling of
dmstate->rel consistent with that of PgFdwScanState's rel. As you know,
PgFdwDirectModifyState is defined in a similar way as PgFdwScanState, in
which the rel is a relcache entry for the foreign table for a simple
foreign table scan and NULL for a foreign join scan (see comments for
the definition of PgFdwScanState).

I am okay with currently also, but it adding a note somewhere about this
would be great. Also let keep this point open for the committer, if
committer feel this is good then lets go ahead with this.

Agreed.

Here are few other cosmetic changes:

1)

+ *
+ * 'target_rel' is either zero or the rangetable index of a target
relation.
+ * In the latter case this construncts FROM clause of UPDATE or USING
clause
+ * of DELETE by simply ignoring the target relation while deparsing the
given

Spell correction: - construncts

2)

+        /*
+         * If either input is the target relation, get all the joinclauses.
+         * Otherwise extract conditions mentioning the target relation from
+         * the joinclauses.
+         */

space between joinclauses needed.

3)

+        /*
+         * If UPDATE/DELETE on a join, create a RETURINING list used in the
+         * remote query.
+         */
+        if (fscan->scan.scanrelid == 0)
+            returningList = make_explicit_returning_list(resultRelation,
+                                                         rel,
+                                                         returningList);

Spell correction: RETURINING

Good catch!

I did above changes in the attached patch. Please have a look once and

I'm fine with that. Thanks for the patch!

then I feel like this patch is ready for committer.

Thanks for reviewing!

Best regards,
Etsuro Fujita

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

#20Rushabh Lathia
rushabh.lathia@gmail.com
In reply to: Etsuro Fujita (#19)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On Wed, Feb 22, 2017 at 12:15 PM, Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp

wrote:

On 2017/02/21 19:31, Rushabh Lathia wrote:

On Mon, Feb 20, 2017 at 1:41 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp <mailto:fujita.etsuro@lab.ntt.co.jp>> wrote:

On 2017/02/13 18:24, Rushabh Lathia wrote:

Here are few comments:

1)

@@ -211,6 +211,12 @@ typedef struct PgFdwDirectModifyState
PGresult   *result;            /* result for query */
int            num_tuples;        /* # of result tuples */
int            next_tuple;        /* index of next one to
return */
+    Relation    resultRel;        /* relcache entry for the
target table */

Why we need resultRel? Can't we directly use dmstate->rel ?

The reason why we need that is because in get_returning_data, we
pass dmstate->rel to make_tuple_from_result_row, which requires that
dmstate->rel be NULL when the scan tuple is described by
fdw_scan_tlist. So in that case we set dmstate->rel to NULL and
have dmstate->resultRel that is the relcache entry for the target
relation in postgresBeginDirectModify.

Thanks for the explanation. We might do something here by using

fdw_scan_tlist or changing the assumption of
make_tuple_from_result_row(), and that way we can avoid two similar
variable pointer in the PgFdwDirectModifyState.

I agree that the two similar variables are annoying, to some extent, but
ISTM that is not that bad because that makes the handling of dmstate->rel
consistent with that of PgFdwScanState's rel. As you know,
PgFdwDirectModifyState is defined in a similar way as PgFdwScanState, in
which the rel is a relcache entry for the foreign table for a simple
foreign table scan and NULL for a foreign join scan (see comments for the
definition of PgFdwScanState).

I am okay with currently also, but it adding a note somewhere about this

would be great. Also let keep this point open for the committer, if
committer feel this is good then lets go ahead with this.

Agreed.

Thanks.

Marked this as Ready for Committer.

Here are few other cosmetic changes:

1)

+ *
+ * 'target_rel' is either zero or the rangetable index of a target
relation.
+ * In the latter case this construncts FROM clause of UPDATE or USING
clause
+ * of DELETE by simply ignoring the target relation while deparsing the
given

Spell correction: - construncts

2)

+        /*
+         * If either input is the target relation, get all the
joinclauses.
+         * Otherwise extract conditions mentioning the target relation
from
+         * the joinclauses.
+         */

space between joinclauses needed.

3)

+        /*
+         * If UPDATE/DELETE on a join, create a RETURINING list used in
the
+         * remote query.
+         */
+        if (fscan->scan.scanrelid == 0)
+            returningList = make_explicit_returning_list(resultRelation,
+                                                         rel,
+                                                         returningList);

Spell correction: RETURINING

Good catch!

I did above changes in the attached patch. Please have a look once and

I'm fine with that. Thanks for the patch!

then I feel like this patch is ready for committer.

Thanks for reviewing!

Best regards,
Etsuro Fujita

--
Rushabh Lathia

#21Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Rushabh Lathia (#20)
1 attachment(s)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 2017/02/22 19:57, Rushabh Lathia wrote:

Marked this as Ready for Committer.

I noticed that this item in the CF app was incorrectly marked as
Committed. This patch isn't committed, so I returned it to the previous
status. I also rebased the patch. Attached is a new version of the patch.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-more-update-pushdown-v4.patchbinary/octet-stream; name=postgres-fdw-more-update-pushdown-v4.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 132,138 **** static void deparseTargetList(StringInfo buf,
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! 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,
--- 132,140 ----
  				  Bitmapset *attrs_used,
  				  bool qualify_col,
  				  List **retrieved_attrs);
! static void deparseExplicitTargetList(List *tlist,
! 						  bool is_returning,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context);
  static void deparseSubqueryTargetList(deparse_expr_cxt *context);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
***************
*** 140,145 **** static void deparseReturningList(StringInfo buf, PlannerInfo *root,
--- 142,154 ----
  					 bool trig_after_row,
  					 List *returningList,
  					 List **retrieved_attrs);
+ static void deparseExplicitReturningList(List *rlist,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context);
+ static void pull_up_target_conditions(PlannerInfo *root, RelOptInfo *foreignrel,
+ 						  Index target_rel, List **target_conds);
+ static void extract_target_conditions(List **joinclauses, Index target_rel,
+ 						  List **target_conds);
  static void deparseColumnRef(StringInfo buf, int varno, int varattno,
  				 PlannerInfo *root, bool qualify_col);
  static void deparseRelation(StringInfo buf, Relation rel);
***************
*** 168,178 **** static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
  static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 					RelOptInfo *joinrel, bool use_alias, List **params_list);
  static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
  							   RelOptInfo *foreignrel, bool make_subquery,
! 							   List **params_list);
  static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
  static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
  static void appendAggOrderBy(List *orderList, List *targetList,
--- 177,188 ----
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
  static void appendConditions(List *exprs, deparse_expr_cxt *context);
  static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 								  RelOptInfo *foreignrel, bool use_alias,
! 								  Index target_rel, List **params_list);
  static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
  							   RelOptInfo *foreignrel, bool make_subquery,
! 							   Index target_rel, List **params_list);
  static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
  static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
  static void appendAggOrderBy(List *orderList, List *targetList,
***************
*** 1027,1033 **** deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
  		 * For a join or upper relation the input tlist gives the list of
  		 * columns required to be fetched from the foreign server.
  		 */
! 		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
  	{
--- 1037,1043 ----
  		 * For a join or upper relation the input tlist gives the list of
  		 * columns required to be fetched from the foreign server.
  		 */
! 		deparseExplicitTargetList(tlist, false, retrieved_attrs, context);
  	}
  	else
  	{
***************
*** 1070,1076 **** deparseFromExpr(List *quals, deparse_expr_cxt *context)
  	appendStringInfoString(buf, " FROM ");
  	deparseFromExprForRel(buf, context->root, scanrel,
  						  (bms_num_members(scanrel->relids) > 1),
! 						  context->params_list);
  
  	/* Construct WHERE clause */
  	if (quals != NIL)
--- 1080,1086 ----
  	appendStringInfoString(buf, " FROM ");
  	deparseFromExprForRel(buf, context->root, scanrel,
  						  (bms_num_members(scanrel->relids) > 1),
! 						  (Index) 0, context->params_list);
  
  	/* Construct WHERE clause */
  	if (quals != NIL)
***************
*** 1343,1351 **** get_jointype_name(JoinType jointype)
   *
   * retrieved_attrs is the list of continuously increasing integers starting
   * from 1. It has same number of entries as tlist.
   */
  static void
! deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
--- 1353,1366 ----
   *
   * retrieved_attrs is the list of continuously increasing integers starting
   * from 1. It has same number of entries as tlist.
+  *
+  * This is used for both SELECT and RETURNING targetlists; the is_returning
+  * parameter is true only for a RETURNING targetlist.
   */
  static void
! deparseExplicitTargetList(List *tlist,
! 						  bool is_returning,
! 						  List **retrieved_attrs,
  						  deparse_expr_cxt *context)
  {
  	ListCell   *lc;
***************
*** 1360,1372 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
  		deparseExpr((Expr *) tle->expr, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  		i++;
  	}
  
! 	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
  
--- 1375,1390 ----
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
+ 		else if (is_returning)
+ 			appendStringInfoString(buf, " RETURNING ");
+ 
  		deparseExpr((Expr *) tle->expr, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  		i++;
  	}
  
! 	if (i == 0 && !is_returning)
  		appendStringInfoString(buf, "NULL");
  }
  
***************
*** 1410,1436 **** deparseSubqueryTargetList(deparse_expr_cxt *context)
   * The function constructs ... JOIN ... ON ... for join relation. For a base
   * relation it just returns schema-qualified tablename, with the appropriate
   * alias if so requested.
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
  
! 		/* Deparse outer relation */
! 		initStringInfo(&join_sql_o);
! 		deparseRangeTblRef(&join_sql_o, root, fpinfo->outerrel,
! 						   fpinfo->make_outerrel_subquery, params_list);
  
! 		/* Deparse inner relation */
! 		initStringInfo(&join_sql_i);
! 		deparseRangeTblRef(&join_sql_i, root, fpinfo->innerrel,
! 						   fpinfo->make_innerrel_subquery, params_list);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
--- 1428,1519 ----
   * The function constructs ... JOIN ... ON ... for join relation. For a base
   * relation it just returns schema-qualified tablename, with the appropriate
   * alias if so requested.
+  *
+  * 'target_rel' is either zero or the rangetable index of a target relation.
+  * In the latter case the function simply ignores the target relation while
+  * deparsing the given join tree, to construct FROM clause of UPDATE or USING
+  * clause of DELETE.  Note that this is safe because the join of the target
+  * relation with any other relation is an inner join and can be interchanged
+  * with higher-level joins.
   */
  static void
  deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 					  bool use_alias, Index target_rel, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
+ 		RelOptInfo *outerrel = fpinfo->outerrel;
+ 		RelOptInfo *innerrel = fpinfo->innerrel;
+ 		bool		outer_contains_targetrel = false;
+ 		bool		inner_contains_targetrel = false;
+ 		bool		outer_is_targetrel = false;
+ 		bool		inner_is_targetrel = false;
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
  
! 		/* Determine which input relation contains the target relation. */
! 		if (target_rel > 0)
! 		{
! 			Assert(bms_is_member(target_rel, foreignrel->relids));
! 
! 			if (bms_is_member(target_rel, outerrel->relids))
! 			{
! 				outer_contains_targetrel = true;
! 
! 				/* Check if outer relation is the target relation. */
! 				if (outerrel->reloptkind == RELOPT_BASEREL &&
! 					outerrel->relid == target_rel)
! 					outer_is_targetrel = true;
! 			}
! 			else
! 			{
! 				inner_contains_targetrel = true;
! 
! 				/* Check if inner relation is the target relation. */
! 				if (innerrel->reloptkind == RELOPT_BASEREL &&
! 					innerrel->relid == target_rel)
! 					inner_is_targetrel = true;
! 			}
! 		}
! 
! 		/* Deparse outer relation if not the target relation. */
! 		if (!outer_is_targetrel)
! 		{
! 			initStringInfo(&join_sql_o);
! 			deparseRangeTblRef(&join_sql_o, root, outerrel,
! 							   fpinfo->make_outerrel_subquery,
! 							   outer_contains_targetrel ? target_rel : 0,
! 							   params_list);
! 
! 			/* If inner relation is the target relation, we are done. */
! 			if (inner_is_targetrel)
! 			{
! 				appendStringInfo(buf, "%s", join_sql_o.data);
! 				return;
! 			}
! 		}
  
! 		/* Deparse inner relation if not the target relation. */
! 		if (!inner_is_targetrel)
! 		{
! 			initStringInfo(&join_sql_i);
! 			deparseRangeTblRef(&join_sql_i, root, innerrel,
! 							   fpinfo->make_innerrel_subquery,
! 							   inner_contains_targetrel ? target_rel : 0,
! 							   params_list);
! 
! 			/* If outer relation is the target relation, we are done. */
! 			if (outer_is_targetrel)
! 			{
! 				appendStringInfo(buf, "%s", join_sql_i.data);
! 				return;
! 			}
! 		}
! 
! 		/* Neither of the relations is the target relation. */
! 		Assert(!outer_is_targetrel && !inner_is_targetrel);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
***************
*** 1490,1496 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
   */
  static void
  deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 				   bool make_subquery, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
--- 1573,1579 ----
   */
  static void
  deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 				   bool make_subquery, Index target_rel, List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
***************
*** 1506,1511 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1589,1597 ----
  		List	   *retrieved_attrs;
  		int			ncols;
  
+ 		/* Shouldn't contain the target relation. */
+ 		Assert(target_rel == 0);
+ 
  		/* Deparse the subquery representing the relation. */
  		appendStringInfoChar(buf, '(');
  		deparseSelectStmtForRel(buf, root, foreignrel, NIL,
***************
*** 1539,1545 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		}
  	}
  	else
! 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
  }
  
  /*
--- 1625,1632 ----
  		}
  	}
  	else
! 		deparseFromExprForRel(buf, root, foreignrel, true, target_rel,
! 							  params_list);
  }
  
  /*
***************
*** 1657,1662 **** deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 1744,1750 ----
  void
  deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 1664,1670 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
- 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  	int			nestlevel;
  	bool		first;
--- 1752,1757 ----
***************
*** 1672,1684 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
! 	context.scanrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
--- 1759,1773 ----
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = foreignrel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
  	appendStringInfoString(buf, " SET ");
  
  	/* Make sure any constants in the exprs are printed portably */
***************
*** 1705,1718 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  
  	reset_transmission_modes(nestlevel);
  
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1794,1823 ----
  
  	reset_transmission_modes(nestlevel);
  
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		List	   *target_conds = NIL;
+ 
+ 		/* Pull up the target relation's conditions into the WHERE clause */
+ 		pull_up_target_conditions(root, foreignrel, rtindex, &target_conds);
+ 		remote_conds = list_concat(target_conds, remote_conds);
+ 
+ 		appendStringInfo(buf, " FROM ");
+ 		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+ 							  params_list);
+ 	}
+ 
  	if (remote_conds)
  	{
  		appendStringInfo(buf, " WHERE ");
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 		deparseExplicitReturningList(returningList, retrieved_attrs, &context);
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1747,1769 **** deparseDeleteSql(StringInfo buf, PlannerInfo *root,
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
- 	RelOptInfo *baserel = root->simple_rel_array[rtindex];
  	deparse_expr_cxt context;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = baserel;
! 	context.scanrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
  
  	if (remote_conds)
  	{
--- 1852,1889 ----
  void
  deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
  					   List **retrieved_attrs)
  {
  	deparse_expr_cxt context;
  
  	/* Set up context struct for recursion */
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = foreignrel;
  	context.buf = buf;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 		appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, rtindex);
+ 
+ 	if (foreignrel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		List	   *target_conds = NIL;
+ 
+ 		/* Pull up the target relation's conditions into the WHERE clause */
+ 		pull_up_target_conditions(root, foreignrel, rtindex, &target_conds);
+ 		remote_conds = list_concat(target_conds, remote_conds);
+ 
+ 		appendStringInfo(buf, " USING ");
+ 		deparseFromExprForRel(buf, root, foreignrel, true, rtindex,
+ 							  params_list);
+ 	}
  
  	if (remote_conds)
  	{
***************
*** 1771,1778 **** deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  		appendConditions(remote_conds, &context);
  	}
  
! 	deparseReturningList(buf, root, rtindex, rel, false,
! 						 returningList, retrieved_attrs);
  }
  
  /*
--- 1891,1901 ----
  		appendConditions(remote_conds, &context);
  	}
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
! 		deparseExplicitReturningList(returningList, retrieved_attrs, &context);
! 	else
! 		deparseReturningList(buf, root, rtindex, rel, false,
! 							 returningList, retrieved_attrs);
  }
  
  /*
***************
*** 1812,1817 **** deparseReturningList(StringInfo buf, PlannerInfo *root,
--- 1935,2040 ----
  }
  
  /*
+  * Add a RETURNING clause, if needed, to an UPDATE/DELETE on a join.
+  */
+ static void
+ deparseExplicitReturningList(List *rlist,
+ 							 List **retrieved_attrs,
+ 							 deparse_expr_cxt *context)
+ {
+ 	deparseExplicitTargetList(rlist, true, retrieved_attrs, context);
+ }
+ 
+ /*
+  * Look for conditions mentioning the target relation in the given join tree,
+  * which will be pulled up into the WHERE clause.  Note that this is safe due
+  * to the same reason stated in comments in deparseFromExprForRel.
+  */
+ static void
+ pull_up_target_conditions(PlannerInfo *root, RelOptInfo *foreignrel,
+ 						  Index target_rel, List **target_conds)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(bms_is_member(target_rel, foreignrel->relids));
+ 
+ 	/* No work if not an inner join. */
+ 	if (fpinfo->jointype == JOIN_INNER)
+ 	{
+ 		/* The remote_conds should be empty (see foreign_join_ok). */
+ 		Assert(fpinfo->remote_conds == NIL);
+ 
+ 		/*
+ 		 * If either input is the target relation, get all the join clauses.
+ 		 * Otherwise extract conditions mentioning the target relation from
+ 		 * the join clauses.
+ 		 */
+ 		if ((outerrel->reloptkind == RELOPT_BASEREL &&
+ 			 outerrel->relid == target_rel) ||
+ 			(innerrel->reloptkind == RELOPT_BASEREL &&
+ 			 innerrel->relid == target_rel))
+ 		{
+ 			*target_conds = list_concat(*target_conds,
+ 										list_copy(fpinfo->joinclauses));
+ 			fpinfo->joinclauses = NIL;
+ 		}
+ 		else
+ 			extract_target_conditions(&fpinfo->joinclauses,
+ 									  target_rel, target_conds);
+ 	}
+ 
+ 	/* Recurse into either input relation. */
+ 	if (outerrel->reloptkind == RELOPT_JOINREL &&
+ 		bms_is_member(target_rel, outerrel->relids))
+ 		pull_up_target_conditions(root, outerrel, target_rel, target_conds);
+ 	else if (innerrel->reloptkind == RELOPT_JOINREL &&
+ 			 bms_is_member(target_rel, innerrel->relids))
+ 		pull_up_target_conditions(root, innerrel, target_rel, target_conds);
+ }
+ 
+ /*
+  * Extract conditions from *joinclauses, separating those that mention
+  * the given relation from those that don't.
+  */
+ static void
+ extract_target_conditions(List **joinclauses,	/* in/out parameters */
+ 						  Index target_rel,
+ 						  List **target_conds	/* output parameters */)
+ {
+ 	List	   *other_conds = NIL;
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, *joinclauses)
+ 	{
+ 		Node	   *clause = (Node *) lfirst(lc);
+ 		Relids		relids;
+ 
+ 		/* Extract clause from RestrictInfo, if needed. */
+ 		if (IsA(clause, RestrictInfo))
+ 		{
+ 			RestrictInfo *ri = (RestrictInfo *) clause;
+ 
+ 			clause = (Node *) ri->clause;
+ 		}
+ 
+ 		/* Retrieve all relids mentioned within the clause. */
+ 		relids = pull_varnos(clause);
+ 
+ 		/* Classify the clause as mentioning the given relation or not. */
+ 		if (bms_is_member(target_rel, relids))
+ 			*target_conds = lappend(*target_conds, clause);
+ 		else
+ 			other_conds = lappend(other_conds, clause);
+ 	}
+ 
+ 	/* Replace *joinclauses. */
+ 	*joinclauses = other_conds;
+ }
+ 
+ /*
   * Construct SELECT statement to acquire size in blocks of given relation.
   *
   * Note: we use local definition of block size, not remote definition.
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 4216,4242 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
!                                                                                                                                                         QUERY PLAN                                                                                                                                                         
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3, c7 = $4 WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.c1, (ft2.c2 + 500), NULL::integer, (ft2.c3 || '_update9'::text), ft2.c4, ft2.c5, ft2.c6, 'ft2       '::character(10), ft2.c8, ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c8, r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9)))) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid
!                      Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c8, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))
! (17 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
--- 4216,4228 ----
  
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
!                                                                                                    QUERY PLAN                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
!    ->  Foreign Update
!          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 500), c3 = (r1.c3 || '_update9'::text), c7 = 'ft2       '::character(10) FROM "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 9))
! (3 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
***************
*** 4359,4385 **** DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
!                                                                                                                               QUERY PLAN                                                                                                                               
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1
!    ->  Foreign Scan
!          Output: ft2.ctid, ft1.*
!          Relations: (public.ft2) INNER JOIN (public.ft1)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r2.*)::text IS NOT NULL THEN ROW(r2."C 1", r2.c2, r2.c3, r2.c4, r2.c5, r2.c6, r2.c7, r2.c8) END FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2)))) FOR UPDATE OF r1
!          ->  Hash Join
!                Output: ft2.ctid, ft1.*
!                Hash Cond: (ft2.c2 = ft1.c1)
!                ->  Foreign Scan on public.ft2
!                      Output: ft2.ctid, ft2.c2
!                      Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" FOR UPDATE
!                ->  Hash
!                      Output: ft1.*, ft1.c1
!                      ->  Foreign Scan on public.ft1
!                            Output: ft1.*, ft1.c1
!                            Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))
! (17 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
--- 4345,4357 ----
  (103 rows)
  
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
!                                                          QUERY PLAN                                                         
! ----------------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
!    ->  Foreign Delete
!          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING "S 1"."T 1" r2 WHERE ((r1.c2 = r2."C 1")) AND (((r2."C 1" % 10) = 2))
! (3 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
***************
*** 5255,5260 **** DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;
--- 5227,5401 ----
   ft2
  (1 row)
  
+ INSERT INTO ft2 (c1,c2,c3)
+   SELECT id, id % 10, to_char(id, 'FM00000') FROM generate_series(10001, 10100) id;
+ CREATE TABLE j1_tbl (c1 int NOT NULL, c2 text);
+ CREATE TABLE j2_tbl (c1 int NOT NULL, c2 text);
+ INSERT INTO j1_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ INSERT INTO j2_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ DELETE FROM j2_tbl WHERE c1 % 2 != 0;
+ CREATE FOREIGN TABLE j1_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j1_tbl');
+ CREATE FOREIGN TABLE j2_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j2_tbl');
+ ANALYZE j1_ftbl;
+ ANALYZE j2_ftbl;
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+                                                                                                                                                      QUERY PLAN                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Update on public.ft2
+    Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8
+    ->  Foreign Update
+          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 200), c3 = (r1.c3 || '_update2'::text) FROM (public.j1_tbl r2 INNER JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 2)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8
+ (4 rows)
+ 
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+   c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 
+ -------+-----+---------------+----+----+----+------------+----
+  10002 | 202 | 10002_update2 |    |    |    | ft2        | 
+  10012 | 202 | 10012_update2 |    |    |    | ft2        | 
+  10022 | 202 | 10022_update2 |    |    |    | ft2        | 
+  10032 | 202 | 10032_update2 |    |    |    | ft2        | 
+  10042 | 202 | 10042_update2 |    |    |    | ft2        | 
+  10052 | 202 | 10052_update2 |    |    |    | ft2        | 
+  10062 | 202 | 10062_update2 |    |    |    | ft2        | 
+  10072 | 202 | 10072_update2 |    |    |    | ft2        | 
+  10082 | 202 | 10082_update2 |    |    |    | ft2        | 
+  10092 | 202 | 10092_update2 |    |    |    | ft2        | 
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                               
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Update on public.ft2
+    Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.c1, j2_ftbl.c2
+    ->  Foreign Update
+          Remote SQL: UPDATE "S 1"."T 1" r1 SET c2 = (r1.c2 + 800), c3 = (r1.c3 || '_update8'::text) FROM (("S 1"."T 1" r2 INNER JOIN public.j1_tbl r3 ON (((r2."C 1" = (r3.c1 + 10000))))) LEFT JOIN public.j2_tbl r4 ON (((r3.c1 = r4.c1)))) WHERE ((r1."C 1" = r2."C 1")) AND (((r1."C 1" % 10) = 8)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, r3.c1, r3.c2, r4.c1, r4.c2
+ (4 rows)
+ 
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+   c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 | c1 |  c2   | c1 |  c2   
+ -------+-----+---------------+----+----+----+------------+----+----+-------+----+-------
+  10008 | 808 | 10008_update8 |    |    |    | ft2        |    |  8 | 00008 |  8 | 00008
+  10018 | 808 | 10018_update8 |    |    |    | ft2        |    | 18 | 00018 | 18 | 00018
+  10028 | 808 | 10028_update8 |    |    |    | ft2        |    | 28 | 00028 | 28 | 00028
+  10038 | 808 | 10038_update8 |    |    |    | ft2        |    | 38 | 00038 | 38 | 00038
+  10048 | 808 | 10048_update8 |    |    |    | ft2        |    | 48 | 00048 | 48 | 00048
+  10058 | 808 | 10058_update8 |    |    |    | ft2        |    | 58 | 00058 | 58 | 00058
+  10068 | 808 | 10068_update8 |    |    |    | ft2        |    | 68 | 00068 | 68 | 00068
+  10078 | 808 | 10078_update8 |    |    |    | ft2        |    | 78 | 00078 | 78 | 00078
+  10088 | 808 | 10088_update8 |    |    |    | ft2        |    | 88 | 00088 | 88 | 00088
+  10098 | 808 | 10098_update8 |    |    |    | ft2        |    | 98 | 00098 | 98 | 00098
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+                                                                                          QUERY PLAN                                                                                          
+ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Delete on public.ft2
+    Output: 100
+    ->  Foreign Delete
+          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (public.j1_tbl r2 INNER JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 2))
+ (4 rows)
+ 
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+  ?column? 
+ ----------
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+       100
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+                                                                                                                                      QUERY PLAN                                                                                                                                     
+ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Delete on public.ft2
+    Output: ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.ctid, j2_ftbl.c1, j2_ftbl.c2
+    ->  Foreign Delete
+          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (public.j1_tbl r2 LEFT JOIN public.j2_tbl r3 ON (((r2.c1 = r3.c1)))) WHERE ((r1."C 1" = (r2.c1 + 10000))) AND (((r1."C 1" % 10) = 0)) RETURNING r1.ctid, r1."C 1", r1.c2, r1.c3, r2.ctid, r2.c1, r2.c2, r3.ctid, r3.c1, r3.c2
+ (4 rows)
+ 
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+    ctid   |  c1   | c2 |  c3   |  ctid   | c1  |  c2   |  ctid   | c1  |  c2   
+ ----------+-------+----+-------+---------+-----+-------+---------+-----+-------
+  (12,11)  | 10010 |  0 | 10010 | (0,10)  |  10 | 00010 | (0,10)  |  10 | 00010
+  (12,21)  | 10020 |  0 | 10020 | (0,20)  |  20 | 00020 | (0,20)  |  20 | 00020
+  (12,31)  | 10030 |  0 | 10030 | (0,30)  |  30 | 00030 | (0,30)  |  30 | 00030
+  (12,41)  | 10040 |  0 | 10040 | (0,40)  |  40 | 00040 | (0,40)  |  40 | 00040
+  (12,51)  | 10050 |  0 | 10050 | (0,50)  |  50 | 00050 | (0,50)  |  50 | 00050
+  (12,61)  | 10060 |  0 | 10060 | (0,60)  |  60 | 00060 | (0,60)  |  60 | 00060
+  (12,71)  | 10070 |  0 | 10070 | (0,70)  |  70 | 00070 | (0,70)  |  70 | 00070
+  (12,81)  | 10080 |  0 | 10080 | (0,80)  |  80 | 00080 | (0,80)  |  80 | 00080
+  (12,91)  | 10090 |  0 | 10090 | (0,90)  |  90 | 00090 | (0,90)  |  90 | 00090
+  (12,101) | 10100 |  0 | 10100 | (0,100) | 100 | 00100 | (0,100) | 100 | 00100
+ (10 rows)
+ 
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+                                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                                     
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Delete on public.ft2
+    Output: ft2.*, ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c7, ft2.c8, j1_ftbl.*, j1_ftbl.c1, j1_ftbl.c2, j2_ftbl.*, j2_ftbl.c1, j2_ftbl.c2
+    ->  Foreign Delete
+          Remote SQL: DELETE FROM "S 1"."T 1" r1 USING (("S 1"."T 1" r2 INNER JOIN public.j1_tbl r3 ON (((r2."C 1" = (r3.c1 + 10000))))) LEFT JOIN public.j2_tbl r4 ON (((r3.c1 = r4.c1)))) WHERE ((r1."C 1" = r2."C 1")) AND (((r1."C 1" % 10) = 8)) RETURNING r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8, CASE WHEN (r3.*)::text IS NOT NULL THEN ROW(r3.c1, r3.c2) END, r3.c1, r3.c2, CASE WHEN (r4.*)::text IS NOT NULL THEN ROW(r4.c1, r4.c2) END, r4.c1, r4.c2
+ (4 rows)
+ 
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+                     ft2                     |  c1   | c2  |      c3       | c4 | c5 | c6 |     c7     | c8 |  j1_ftbl   | c1 |  c2   |  j2_ftbl   | c1 |  c2   
+ --------------------------------------------+-------+-----+---------------+----+----+----+------------+----+------------+----+-------+------------+----+-------
+  (10008,808,10008_update8,,,,"ft2       ",) | 10008 | 808 | 10008_update8 |    |    |    | ft2        |    | (8,00008)  |  8 | 00008 | (8,00008)  |  8 | 00008
+  (10018,808,10018_update8,,,,"ft2       ",) | 10018 | 808 | 10018_update8 |    |    |    | ft2        |    | (18,00018) | 18 | 00018 | (18,00018) | 18 | 00018
+  (10028,808,10028_update8,,,,"ft2       ",) | 10028 | 808 | 10028_update8 |    |    |    | ft2        |    | (28,00028) | 28 | 00028 | (28,00028) | 28 | 00028
+  (10038,808,10038_update8,,,,"ft2       ",) | 10038 | 808 | 10038_update8 |    |    |    | ft2        |    | (38,00038) | 38 | 00038 | (38,00038) | 38 | 00038
+  (10048,808,10048_update8,,,,"ft2       ",) | 10048 | 808 | 10048_update8 |    |    |    | ft2        |    | (48,00048) | 48 | 00048 | (48,00048) | 48 | 00048
+  (10058,808,10058_update8,,,,"ft2       ",) | 10058 | 808 | 10058_update8 |    |    |    | ft2        |    | (58,00058) | 58 | 00058 | (58,00058) | 58 | 00058
+  (10068,808,10068_update8,,,,"ft2       ",) | 10068 | 808 | 10068_update8 |    |    |    | ft2        |    | (68,00068) | 68 | 00068 | (68,00068) | 68 | 00068
+  (10078,808,10078_update8,,,,"ft2       ",) | 10078 | 808 | 10078_update8 |    |    |    | ft2        |    | (78,00078) | 78 | 00078 | (78,00078) | 78 | 00078
+  (10088,808,10088_update8,,,,"ft2       ",) | 10088 | 808 | 10088_update8 |    |    |    | ft2        |    | (88,00088) | 88 | 00088 | (88,00088) | 88 | 00088
+  (10098,808,10098_update8,,,,"ft2       ",) | 10098 | 808 | 10098_update8 |    |    |    | ft2        |    | (98,00098) | 98 | 00098 | (98,00098) | 98 | 00098
+ (10 rows)
+ 
+ DELETE FROM ft2 WHERE ft2.c1 > 10000;
  -- Test that trigger on remote table works as expected
  CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
  BEGIN
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 212,217 **** typedef struct PgFdwDirectModifyState
--- 212,223 ----
  	PGresult   *result;			/* result for query */
  	int			num_tuples;		/* # of result tuples */
  	int			next_tuple;		/* index of next one to return */
+ 	Relation	resultRel;		/* relcache entry for the target table */
+ 	TupleTableSlot *resultSlot;	/* slot for updated/deleted tuple */
+ 	AttrNumber *attnoMap;		/* array of attnums of input user columns */
+ 	AttrNumber	ctidAttno;		/* attnum of input ctid column */
+ 	AttrNumber	oidAttno;		/* attnum of input oid column */
+ 	bool		hasSystemCols;	/* are there system columns of resultRel? */
  
  	/* working memory context */
  	MemoryContext temp_cxt;		/* context for per-tuple temporary data */
***************
*** 378,385 **** static const char **convert_prep_stmt_params(PgFdwModifyState *fmstate,
--- 384,401 ----
  						 TupleTableSlot *slot);
  static void store_returning_result(PgFdwModifyState *fmstate,
  					   TupleTableSlot *slot, PGresult *res);
+ static List *make_explicit_returning_list(Index rtindex, Relation rel,
+ 										  List *returningList);
+ static List *rewrite_fdw_scan_tlist(List *fdw_scan_tlist,  Index rtindex,
+ 					   List *returningList);
  static void execute_dml_stmt(ForeignScanState *node);
  static TupleTableSlot *get_returning_data(ForeignScanState *node);
+ static void init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate);
+ static TupleTableSlot *apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot);
  static void prepare_query_params(PlanState *node,
  					 List *fdw_exprs,
  					 int numParams,
***************
*** 2114,2119 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2130,2136 ----
  	Relation	rel;
  	StringInfoData sql;
  	ForeignScan *fscan;
+ 	RelOptInfo *foreignrel;
  	List	   *targetAttrs = NIL;
  	List	   *remote_conds;
  	List	   *params_list = NIL;
***************
*** 2145,2157 **** postgresPlanDirectModify(PlannerInfo *root,
  		return false;
  
  	/*
- 	 * We can't handle an UPDATE or DELETE on a foreign join for now.
- 	 */
- 	fscan = (ForeignScan *) subplan;
- 	if (fscan->scan.scanrelid == 0)
- 		return false;
- 
- 	/*
  	 * It's unsafe to update a foreign table directly, if any expressions to
  	 * assign to the target columns are unsafe to evaluate remotely.
  	 */
--- 2162,2167 ----
***************
*** 2190,2195 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2200,2207 ----
  	/*
  	 * Ok, rewrite subplan so as to modify the foreign table directly.
  	 */
+ 	fscan = (ForeignScan *) subplan;
+ 
  	initStringInfo(&sql);
  
  	/*
***************
*** 2199,2204 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2211,2228 ----
  	rel = heap_open(rte->relid, NoLock);
  
  	/*
+ 	 * Get a rel for this foreign table or join.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 	{
+ 		foreignrel = find_join_rel(root, fscan->fs_relids);
+ 		/* We should have a rel for this foreign join. */
+ 		Assert(foreignrel);
+ 	}
+ 	else
+ 		foreignrel = find_base_rel(root, resultRelation);
+ 
+ 	/*
  	 * Extract the baserestrictinfo clauses that can be evaluated remotely.
  	 */
  	remote_conds = (List *) list_nth(fscan->fdw_private,
***************
*** 2208,2215 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2232,2250 ----
  	 * Extract the relevant RETURNING list if any.
  	 */
  	if (plan->returningLists)
+ 	{
  		returningList = (List *) list_nth(plan->returningLists, subplan_index);
  
+ 		/*
+ 		 * If UPDATE/DELETE on a join, create a RETURNING list used in the
+ 		 * remote query.
+ 		 */
+ 		if (fscan->scan.scanrelid == 0)
+ 			returningList = make_explicit_returning_list(resultRelation,
+ 														 rel,
+ 														 returningList);
+ 	}
+ 
  	/*
  	 * Construct the SQL command string.
  	 */
***************
*** 2217,2222 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2252,2258 ----
  	{
  		case CMD_UPDATE:
  			deparseDirectUpdateSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
  								   ((Plan *) fscan)->targetlist,
  								   targetAttrs,
  								   remote_conds, &params_list,
***************
*** 2224,2229 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2260,2266 ----
  			break;
  		case CMD_DELETE:
  			deparseDirectDeleteSql(&sql, root, resultRelation, rel,
+ 								   foreignrel,
  								   remote_conds, &params_list,
  								   returningList, &retrieved_attrs);
  			break;
***************
*** 2251,2256 **** postgresPlanDirectModify(PlannerInfo *root,
--- 2288,2308 ----
  									retrieved_attrs,
  									makeInteger(plan->canSetTag));
  
+ 	/*
+ 	 * Update the foreign-join-related fields.
+ 	 */
+ 	if (fscan->scan.scanrelid == 0)
+ 	{
+ 		/* No need for the outer subplan. */
+ 		fscan->scan.plan.lefttree = NULL;
+ 
+ 		/* If having RETURNING, rewrite fdw_scan_tlist to include it. */
+ 		if (returningList)
+ 			fscan->fdw_scan_tlist =
+ 				rewrite_fdw_scan_tlist(fscan->fdw_scan_tlist, resultRelation,
+ 									   returningList);
+ 	}
+ 
  	heap_close(rel, NoLock);
  	return true;
  }
***************
*** 2265,2270 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2317,2323 ----
  	ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
  	EState	   *estate = node->ss.ps.state;
  	PgFdwDirectModifyState *dmstate;
+ 	Index		rtindex;
  	RangeTblEntry *rte;
  	Oid			userid;
  	ForeignTable *table;
***************
*** 2287,2297 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rte = rt_fetch(fsplan->scan.scanrelid, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	dmstate->rel = node->ss.ss_currentRelation;
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
--- 2340,2354 ----
  	 * Identify which user to do the remote access as.  This should match what
  	 * ExecCheckRTEPerms() does.
  	 */
! 	rtindex = estate->es_result_relation_info->ri_RangeTableIndex;
! 	rte = rt_fetch(rtindex, estate->es_range_table);
  	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
  
  	/* Get info about foreign table. */
! 	if (fsplan->scan.scanrelid == 0)
! 		dmstate->rel = ExecOpenScanRelation(estate, rtindex, eflags);
! 	else
! 		dmstate->rel = node->ss.ss_currentRelation;
  	table = GetForeignTable(RelationGetRelid(dmstate->rel));
  	user = GetUserMapping(userid, table->serverid);
  
***************
*** 2301,2306 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
--- 2358,2373 ----
  	 */
  	dmstate->conn = GetConnection(user, false);
  
+ 	/* Update the foreign-join-related fields. */
+ 	if (fsplan->scan.scanrelid == 0)
+ 	{
+ 		/* Save info about target table. */
+ 		dmstate->resultRel = dmstate->rel;
+ 
+ 		/* rel should be NULL. */
+ 		dmstate->rel = NULL;
+ 	}
+ 
  	/* Initialize state variable */
  	dmstate->num_tuples = -1;	/* -1 means not set yet */
  
***************
*** 2321,2327 **** postgresBeginDirectModify(ForeignScanState *node, int eflags)
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(RelationGetDescr(dmstate->rel));
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
--- 2388,2411 ----
  
  	/* Prepare for input conversion of RETURNING results. */
  	if (dmstate->has_returning)
! 	{
! 		TupleDesc	tupdesc;
! 
! 		if (fsplan->scan.scanrelid == 0)
! 			tupdesc = node->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
! 		else
! 			tupdesc = RelationGetDescr(dmstate->rel);
! 
! 		dmstate->attinmeta = TupleDescGetAttInMetadata(tupdesc);
! 
! 		/*
! 		 * If UPDATE/DELETE on a join, initialize a filter to extract a result
! 		 * tuple from a scan tuple.
! 		 */
! 		if (fsplan->scan.scanrelid == 0)
! 			init_returning_filter(dmstate, fsplan->fdw_scan_tlist,
! 								  rtindex, estate);
! 	}
  
  	/*
  	 * Prepare for processing of parameters used in remote query, if any.
***************
*** 2402,2407 **** postgresEndDirectModify(ForeignScanState *node)
--- 2486,2495 ----
  	ReleaseConnection(dmstate->conn);
  	dmstate->conn = NULL;
  
+ 	/* close the result relation. */
+ 	if (dmstate->resultRel)
+ 		ExecCloseScanRelation(dmstate->resultRel);
+ 
  	/* MemoryContext will be deleted automatically. */
  }
  
***************
*** 3270,3275 **** store_returning_result(PgFdwModifyState *fmstate,
--- 3358,3485 ----
  }
  
  /*
+  * Create an explicit RETURNING list used in the remote query.
+  */
+ static List *
+ make_explicit_returning_list(Index rtindex, Relation rel, List *returningList)
+ {
+ 	TupleDesc	tupdesc = RelationGetDescr(rel);
+ 	bool		have_wholerow = false;
+ 	List	   *rlist = NIL;
+ 	List	   *vars;
+ 	ListCell   *lc;
+ 
+ 	if (returningList == NIL)
+ 		return NIL;
+ 
+ 	vars = pull_var_clause((Node *) returningList, PVC_INCLUDE_PLACEHOLDERS);
+ 
+ 	/*
+ 	 * If there's a whole-row reference to the target relation, then we'll need
+ 	 * all the columns of the relation.
+ 	 */
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno == InvalidAttrNumber)
+ 		{
+ 			have_wholerow = true;
+ 			break;
+ 		}
+ 	}
+ 
+ 	if (have_wholerow)
+ 	{
+ 		int			i;
+ 
+ 		for (i = 1; i <= tupdesc->natts; i++)
+ 		{
+ 			Form_pg_attribute attr = tupdesc->attrs[i - 1];
+ 			Var		   *var;
+ 
+ 			/* Ignore dropped attributes. */
+ 			if (attr->attisdropped)
+ 				continue;
+ 
+ 			var = makeVar(rtindex,
+ 						  i,
+ 						  attr->atttypid,
+ 						  attr->atttypmod,
+ 						  attr->attcollation,
+ 						  0);
+ 
+ 			rlist = lappend(rlist,
+ 							makeTargetEntry((Expr *) var,
+ 											list_length(rlist) + 1,
+ 											NULL,
+ 											false));
+ 		}
+ 	}
+ 
+ 	/* Now add any remaining columns to rlist. */
+ 	foreach(lc, vars)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		/*
+ 		 * No need for whole-row references to the target relation.  We don't
+ 		 * need system columns other than ctid and oid either, since those are
+ 		 * set locally.
+ 		 */
+ 		if (IsA(var, Var) &&
+ 			var->varno == rtindex &&
+ 			var->varattno <= InvalidAttrNumber &&
+ 			var->varattno != SelfItemPointerAttributeNumber &&
+ 			var->varattno != ObjectIdAttributeNumber)
+ 			continue;		/* don't need it */
+ 
+ 		if (tlist_member((Node *) var, rlist))
+ 			continue;		/* already got it */
+ 
+ 		rlist = lappend(rlist,
+ 						makeTargetEntry((Expr *) var,
+ 										list_length(rlist) + 1,
+ 										NULL,
+ 										false));
+ 	}
+ 
+ 	list_free(vars);
+ 
+ 	return rlist;
+ }
+ 
+ /*
+  * Rewrite the given fdw_scan_tlist so it contains all the expressions
+  * specified in the given RETURNING list.
+  */
+ static List *
+ rewrite_fdw_scan_tlist(List *fdw_scan_tlist,  Index rtindex,
+ 					   List *returningList)
+ {
+ 	List	   *tlist = list_copy(returningList);
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, fdw_scan_tlist)
+ 	{
+ 		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 
+ 		if (tlist_member((Node *) tle->expr, tlist))
+ 			continue;		/* already got it */
+ 
+ 		tlist = lappend(tlist,
+ 						makeTargetEntry(tle->expr,
+ 										list_length(tlist) + 1,
+ 										NULL,
+ 										false));
+ 	}
+ 
+ 	return tlist;
+ }
+ 
+ /*
   * Execute a direct UPDATE/DELETE statement.
   */
  static void
***************
*** 3329,3334 **** get_returning_data(ForeignScanState *node)
--- 3539,3545 ----
  	EState	   *estate = node->ss.ps.state;
  	ResultRelInfo *resultRelInfo = estate->es_result_relation_info;
  	TupleTableSlot *slot = node->ss.ss_ScanTupleSlot;
+ 	TupleTableSlot *resultSlot;
  
  	Assert(resultRelInfo->ri_projectReturning);
  
***************
*** 3346,3352 **** get_returning_data(ForeignScanState *node)
--- 3557,3566 ----
  	 * "UPDATE/DELETE .. RETURNING 1" for example.)
  	 */
  	if (!dmstate->has_returning)
+ 	{
  		ExecStoreAllNullTuple(slot);
+ 		resultSlot = slot;
+ 	}
  	else
  	{
  		/*
***************
*** 3362,3368 **** get_returning_data(ForeignScanState *node)
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												NULL,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
--- 3576,3582 ----
  												dmstate->rel,
  												dmstate->attinmeta,
  												dmstate->retrieved_attrs,
! 												node,
  												dmstate->temp_cxt);
  			ExecStoreTuple(newtup, slot, InvalidBuffer, false);
  		}
***************
*** 3373,3388 **** get_returning_data(ForeignScanState *node)
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = slot;
  
  	return slot;
  }
  
  /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
--- 3587,3783 ----
  			PG_RE_THROW();
  		}
  		PG_END_TRY();
+ 
+ 		/* Get the updated/deleted tuple. */
+ 		if (dmstate->rel)
+ 			resultSlot = slot;
+ 		else
+ 			resultSlot = apply_returning_filter(dmstate, slot);
  	}
  	dmstate->next_tuple++;
  
  	/* Make slot available for evaluation of the local query RETURNING list. */
! 	resultRelInfo->ri_projectReturning->pi_exprContext->ecxt_scantuple = resultSlot;
  
  	return slot;
  }
  
  /*
+  * Initialize a filter to extract an updated/deleted tuple from a scan tuple.
+  */
+ static void
+ init_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					  List *fdw_scan_tlist,
+ 					  Index rtindex,
+ 					  EState *estate)
+ {
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	ListCell   *lc;
+ 	int			i;
+ 
+ 	/* Make a new slot for storing the result tuple. */
+ 	dmstate->resultSlot = ExecInitExtraTupleSlot(estate);
+ 
+ 	ExecSetSlotDescriptor(dmstate->resultSlot, resultTupType);
+ 
+ 	/*
+ 	 * Calculate the mapping between the fdw_scan_tlist's entries and the
+ 	 * result tuple's attributes.
+ 	 *
+ 	 * The "map" is an array of indexes of the result tuple's attributes in
+ 	 * fdw_scan_tlist, i.e., one entry for every attribute of the result
+ 	 * tuple.  We store zero for any attributes that don't have the
+ 	 * corresponding entries in that list, marking that a NULL is needed in
+ 	 * the result tuple.
+ 	 *
+ 	 * Also get the indexes of the entries for ctid and oid if any.
+ 	 */
+ 	dmstate->attnoMap = (AttrNumber *) palloc0(resultTupType->natts * sizeof(AttrNumber));
+ 
+ 	dmstate->ctidAttno = dmstate->oidAttno = 0;
+ 
+ 	i = 1;
+ 	dmstate->hasSystemCols = false;
+ 	foreach(lc, fdw_scan_tlist)
+ 	{
+ 		TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ 		Var		   *var = (Var *) tle->expr;
+ 
+ 		if (list_member_int(dmstate->retrieved_attrs, i))
+ 		{
+ 			if (IsA(var, Var) && var->varno == rtindex)
+ 			{
+ 				int			attrno = var->varattno;
+ 
+ 				if (attrno < 0)
+ 				{
+ 				  	/*
+ 					 * We don't retrieve system columns other than ctid and
+ 					 * oid.
+ 					 */
+ 					if (attrno == SelfItemPointerAttributeNumber)
+ 						dmstate->ctidAttno = i;
+ 					else if (attrno == ObjectIdAttributeNumber)
+ 						dmstate->oidAttno = i;
+ 					else
+ 						Assert(false);
+ 					dmstate->hasSystemCols = true;
+ 				}
+ 				else
+ 				{
+ 					/*
+ 					 * We don't retrieve whole-row references to the result
+ 					 * relation either.
+ 					 */
+ 					Assert(attrno > 0);
+ 					dmstate->attnoMap[attrno - 1] = i;
+ 				}
+ 			}
+ 		}
+ 		i++;
+ 	}
+ }
+ 
+ /*
+  * Extract and return an updated/deleted tuple from a scan tuple.
+  */
+ static TupleTableSlot *
+ apply_returning_filter(PgFdwDirectModifyState *dmstate,
+ 					   TupleTableSlot *slot)
+ {
+ 	TupleTableSlot *resultSlot = dmstate->resultSlot;
+ 	TupleDesc	resultTupType = RelationGetDescr(dmstate->resultRel);
+ 	Datum	   *values;
+ 	bool	   *isnull;
+ 	Datum	   *old_values;
+ 	bool	   *old_isnull;
+ 	int			i;
+ 
+ 	/*
+ 	 * Extract all the values of the scan tuple.
+ 	 */
+ 	slot_getallattrs(slot);
+ 	old_values = slot->tts_values;
+ 	old_isnull = slot->tts_isnull;
+ 
+ 	/*
+ 	 * Prepare to build the result tuple.
+ 	 */
+ 	ExecClearTuple(resultSlot);
+ 	values = resultSlot->tts_values;
+ 	isnull = resultSlot->tts_isnull;
+ 
+ 	/*
+ 	 * Transpose data into proper fields of the result tuple.
+ 	 */
+ 	for (i = 0; i < resultTupType->natts; i++)
+ 	{
+ 		int			j = dmstate->attnoMap[i];
+ 
+ 		if (j == 0)
+ 		{
+ 			values[i] = (Datum) 0;
+ 			isnull[i] = true;
+ 		}
+ 		else
+ 		{
+ 			values[i] = old_values[j - 1];
+ 			isnull[i] = old_isnull[j - 1];
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * Build the virtual tuple.
+ 	 */
+ 	ExecStoreVirtualTuple(resultSlot);
+ 
+ 	/*
+ 	 * If we have any system columns to return, install them.
+ 	 */
+ 	if (dmstate->hasSystemCols)
+ 	{
+ 		HeapTuple	resultTup = ExecMaterializeSlot(resultSlot);
+ 
+ 		/* ctid */
+ 		if (dmstate->ctidAttno)
+ 		{
+ 			ItemPointer ctid = NULL;
+ 
+ 			ctid = (ItemPointer) DatumGetPointer(old_values[dmstate->ctidAttno - 1]);
+ 			resultTup->t_self = *ctid;
+ 		}
+ 
+ 		/* oid */
+ 		if (dmstate->oidAttno)
+ 		{
+ 			Oid			oid = InvalidOid;
+ 
+ 			oid = DatumGetObjectId(old_values[dmstate->oidAttno - 1]);
+ 			HeapTupleSetOid(resultTup, oid);
+ 		}
+ 
+ 		/*
+ 		 * And remaining columns
+ 		 *
+ 		 * Note: since we currently don't allow the result relation to appear
+ 		 * on the nullable side of an outer join, any system columns wouldn't
+ 		 * go to NULL.
+ 		 *
+ 		 * Note: no need to care about tableoid here because it will be
+ 		 * initialized in ExecProcessReturning().
+ 		 */
+ 		HeapTupleHeaderSetXmin(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetXmax(resultTup->t_data, InvalidTransactionId);
+ 		HeapTupleHeaderSetCmin(resultTup->t_data, InvalidTransactionId);
+ 	}
+ 
+ 	/*
+ 	 * And return the result tuple.
+ 	 */
+ 	return resultSlot;
+ }
+ 
+ /*
   * Prepare for processing of parameters used in remote query.
   */
  static void
***************
*** 4848,4858 **** make_tuple_from_result_row(PGresult *res,
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
- 		PgFdwScanState *fdw_sstate;
- 
  		Assert(fsstate);
! 		fdw_sstate = (PgFdwScanState *) fsstate->fdw_state;
! 		tupdesc = fdw_sstate->tupdesc;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
--- 5243,5250 ----
  		tupdesc = RelationGetDescr(rel);
  	else
  	{
  		Assert(fsstate);
! 		tupdesc = fsstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
  	}
  
  	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 151,156 **** extern void deparseUpdateSql(StringInfo buf, PlannerInfo *root,
--- 151,157 ----
  				 List **retrieved_attrs);
  extern void deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *targetlist,
  					   List *targetAttrs,
  					   List *remote_conds,
***************
*** 163,168 **** extern void deparseDeleteSql(StringInfo buf, PlannerInfo *root,
--- 164,170 ----
  				 List **retrieved_attrs);
  extern void deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
  					   Index rtindex, Relation rel,
+ 					   RelOptInfo *foreignrel,
  					   List *remote_conds,
  					   List **params_list,
  					   List *returningList,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 1033,1046 **** UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can't be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can't be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
--- 1033,1046 ----
  UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING *;
  EXPLAIN (verbose, costs off)
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
!   FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;                               -- can be pushed down
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
  EXPLAIN (verbose, costs off)
    DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;                               -- can be pushed down
  DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
  EXPLAIN (verbose, costs off)
! DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;                -- can be pushed down
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
  EXPLAIN (verbose, costs off)
***************
*** 1053,1058 **** EXPLAIN (verbose, costs off)
--- 1053,1118 ----
  DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;                       -- can be pushed down
  DELETE FROM ft2 WHERE c1 = 9999 RETURNING tableoid::regclass;
  
+ INSERT INTO ft2 (c1,c2,c3)
+   SELECT id, id % 10, to_char(id, 'FM00000') FROM generate_series(10001, 10100) id;
+ CREATE TABLE j1_tbl (c1 int NOT NULL, c2 text);
+ CREATE TABLE j2_tbl (c1 int NOT NULL, c2 text);
+ INSERT INTO j1_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ INSERT INTO j2_tbl SELECT id, to_char(id, 'FM00000') FROM generate_series(1, 100) id;
+ DELETE FROM j2_tbl WHERE c1 % 2 != 0;
+ CREATE FOREIGN TABLE j1_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j1_tbl');
+ CREATE FOREIGN TABLE j2_ftbl (c1 int NOT NULL, c2 text)
+   SERVER loopback OPTIONS (table_name 'j2_tbl');
+ ANALYZE j1_ftbl;
+ ANALYZE j2_ftbl;
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+ UPDATE ft2 SET c2 = ft2.c2 + 200, c3 = ft2.c3 || '_update2'
+   FROM j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING ft2.*;
+ EXPLAIN (verbose, costs off)
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+ UPDATE ft2 SET c2 = ft2.c2 + 800, c3 = ft2.c3 || '_update8'
+   FROM ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2.*, j1_ftbl.*, j2_ftbl.*;
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+ DELETE FROM ft2
+   USING j1_ftbl INNER JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 2
+   RETURNING 100;
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+ DELETE FROM ft2
+   USING j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)
+   WHERE ft2.c1 = j1_ftbl.c1 + 10000 AND ft2.c1 % 10 = 0
+   RETURNING ft2.ctid, ft2.c1, ft2.c2, ft2.c3, j1_ftbl.ctid, j1_ftbl.*, j2_ftbl.ctid, j2_ftbl.*;
+ EXPLAIN (verbose, costs off)
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+ DELETE FROM ft2
+   USING ft1 INNER JOIN (j1_ftbl LEFT JOIN j2_ftbl ON (j1_ftbl.c1 = j2_ftbl.c1)) ON (ft1.c1 = j1_ftbl.c1 + 10000)
+   WHERE ft2.c1 = ft1.c1 AND ft2.c1 % 10 = 8
+   RETURNING ft2, ft2.*, j1_ftbl, j1_ftbl.*, j2_ftbl, j2_ftbl.*;
+ DELETE FROM ft2 WHERE ft2.c1 > 10000;
+ 
  -- Test that trigger on remote table works as expected
  CREATE OR REPLACE FUNCTION "S 1".F_BRTRIG() RETURNS trigger AS $$
  BEGIN
#22Robert Haas
robertmhaas@gmail.com
In reply to: Etsuro Fujita (#21)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On Wed, Mar 22, 2017 at 6:20 AM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/02/22 19:57, Rushabh Lathia wrote:

Marked this as Ready for Committer.

I noticed that this item in the CF app was incorrectly marked as Committed.
This patch isn't committed, so I returned it to the previous status. I also
rebased the patch. Attached is a new version of the patch.

Sorry, I marked the wrong patch as committed. Apologies for that.

This doesn't apply any more because of recent changes.

git diff --check complains:
contrib/postgres_fdw/postgres_fdw.c:3653: space before tab in indent.

+        /* Shouldn't contain the target relation. */
+        Assert(target_rel == 0);

This comment should give a reason.

void
deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
Index rtindex, Relation rel,
+ RelOptInfo *foreignrel,
List *targetlist,
List *targetAttrs,
List *remote_conds,

Could you add a comment explaining the meaning of these various
arguments? It takes rtindex, rel, and foreignrel, which apparently
are all different things, but the meaning is not explained.

 /*
+ * Add a RETURNING clause, if needed, to an UPDATE/DELETE on a join.
+ */
+static void
+deparseExplicitReturningList(List *rlist,
+                             List **retrieved_attrs,
+                             deparse_expr_cxt *context)
+{
+    deparseExplicitTargetList(rlist, true, retrieved_attrs, context);
+}

Do we really want to add a function for one line of code?

+/*
+ * Look for conditions mentioning the target relation in the given join tree,
+ * which will be pulled up into the WHERE clause.  Note that this is safe due
+ * to the same reason stated in comments in deparseFromExprForRel.
+ */

The comments for deparseFromExprForRel do not seem to address the
topic of why this is safe. Also, the answer to the question "safe
from what?" is not clear.

-    deparseReturningList(buf, root, rtindex, rel, false,
-                         returningList, retrieved_attrs);
+    if (foreignrel->reloptkind == RELOPT_JOINREL)
+        deparseExplicitReturningList(returningList, retrieved_attrs, &context);
+    else
+        deparseReturningList(buf, root, rtindex, rel, false,
+                             returningList, retrieved_attrs);

Why do these cases need to be handled differently? Maybe add a brief comment?

+        if ((outerrel->reloptkind == RELOPT_BASEREL &&
+             outerrel->relid == target_rel) ||
+            (innerrel->reloptkind == RELOPT_BASEREL &&
+             innerrel->relid == target_rel))

1. Surely it's redundant to check the RelOptKind if the RTI matches?

2. Generally, the tests in this patch against various RelOptKind
values should be adapted to use the new macros introduced in
7a39b5e4d11229ece930a51fd7cb29e535db4494.

The regression tests remove every remaining case where an update or
delete gets fails to get pushed to the remote side. I think we should
still test that path, because we've still got that code. Maybe use a
non-pushable function in the join clause, or something.

The new test cases could use some brief comments explaining their purpose.

if (plan->returningLists)
+ {
returningList = (List *) list_nth(plan->returningLists, subplan_index);

+        /*
+         * If UPDATE/DELETE on a join, create a RETURNING list used in the
+         * remote query.
+         */
+        if (fscan->scan.scanrelid == 0)
+            returningList = make_explicit_returning_list(resultRelation,
+                                                         rel,
+                                                         returningList);
+    }

Again, the comment doesn't really explain why we're doing this. And
initializing returningList twice in a row seems strange, too.

I am unfortunately too tired to finish properly reviewing this tonight. :-(

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

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

#23David Steele
david@pgmasters.net
In reply to: Etsuro Fujita (#21)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 3/22/17 6:20 AM, Etsuro Fujita wrote:

On 2017/02/22 19:57, Rushabh Lathia wrote:

Marked this as Ready for Committer.

I noticed that this item in the CF app was incorrectly marked as
Committed. This patch isn't committed, so I returned it to the previous
status. I also rebased the patch. Attached is a new version of the patch.

This submission has been moved to CF 2017-07.

--
-David
david@pgmasters.net

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

#24Daniel Gustafsson
daniel@yesql.se
In reply to: David Steele (#23)
Re: Push down more UPDATEs/DELETEs in postgres_fdw

On 08 Apr 2017, at 16:14, David Steele <david@pgmasters.net> wrote:

On 3/22/17 6:20 AM, Etsuro Fujita wrote:

On 2017/02/22 19:57, Rushabh Lathia wrote:

Marked this as Ready for Committer.

I noticed that this item in the CF app was incorrectly marked as
Committed. This patch isn't committed, so I returned it to the previous
status. I also rebased the patch. Attached is a new version of the patch.

This submission has been moved to CF 2017-07.

This patch has been marked Ready for Committer in the current commitfest
without being committed or rejected. Moving to the next commitfest, but since
it has bitrotted I’m moving it to Waiting for author.

cheers ./daniel

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

#25Michael Paquier
michael.paquier@gmail.com
In reply to: Daniel Gustafsson (#24)
Re: [HACKERS] Push down more UPDATEs/DELETEs in postgres_fdw

On Mon, Oct 2, 2017 at 9:08 AM, Daniel Gustafsson <daniel@yesql.se> wrote:

This patch has been marked Ready for Committer in the current commitfest
without being committed or rejected. Moving to the next commitfest, but since
it has bitrotted I’m moving it to Waiting for author.

No updates have showed up since, so returned with feedback. Please
feel free to send a new version if you can hack it.
--
Michael