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
*** 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
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to