Push down more full joins in postgres_fdw

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

Hi,

The postgres_fdw join pushdown in 9.6 is great, but it can't handle full
joins on relations with restrictions. The reason for that is,
postgres_fdw can't support deparsing subqueries when creating a remote
join query. So, by adding the deparsing logic to it, I removed that
limitation. Attached is a patch for that, which is based on the patch I
posted before [1]/messages/by-id/5710D7E2.7010302@lab.ntt.co.jp.

Also, by the deparsing logic, I improved the handling of PHVs so that
PHVs are evaluated remotely if it's safe, as discussed in [2]/messages/by-id/b4549406-909f-7d15-dc34-499835a8f0b3@lab.ntt.co.jp. Here is
an example from the regression test, which pushes down multiple levels
of PHVs to the remote:

EXPLAIN (VERBOSE, COSTS OFF)
SELECT ss.*, ft2.c1 FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM
(SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1)
WHERE ft2.\
c1 BETWEEN 10 AND 15) ss(f1, f2, f3) ON (ft2.c1 = ss.f1) WHERE ft2.c1
BETWEEN 10 AND 15;
\
QUERY PLAN \

-------------------------------------------------------------------------------------------------------------------------------------------------------\
-------------------------------------------------------------------------------------------------------------------------------------------------------\
---------------------------------------------------------------
Foreign Scan
Output: (13), (13), ft2_1.c1, ft2.c1
Relations: (public.ft2) LEFT JOIN ((public.ft2) LEFT JOIN (public.ft1))
Remote SQL: SELECT r1."C 1", ss2.c1, ss2.c2, ss2.c3 FROM ("S 1"."T
1" r1 LEFT JOIN (SELECT r5."C 1", 13, ss1.c1 FROM ("S 1"."T 1" r5 LEFT
JOIN (SELE\
CT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) ss1(c1) ON (((13 = r5."C
1")))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))) ss2(c1, c2, c3)
ON (((r1.\
"C 1" = 13)))) WHERE ((r1."C 1" >= 10)) AND ((r1."C 1" <= 15))
(4 rows)

Also, in the same way as the PHV handling, I improved the handling of
whole-row reference (and system columns other than ctid), as proposed in
[3]: /messages/by-id/a1fa1c4c-bf96-8ea5-cff5-85b927298e73@lab.ntt.co.jp
conditions any more, as shown in the below example:

EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)
ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
\
QUERY PLAN \

-------------------------------------------------------------------------------------------------------------------------------------------------------\
-------------------------------------------------------------------------------------------------------------------------------------------------------\
-----------------------------------------------------------
Limit
Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
-> Foreign Scan
Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss1.c4, ss2.c1
FROM ((SELECT ctid, ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1", c3
FROM "S 1"."T \
1") ss1(c1, c2, c3, c4) INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5,
c6, c7, c8), "C 1" FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE
((ss1.c3 = ss2.\
c2)) ORDER BY ss1.c4 ASC NULLS LAST, ss1.c3 ASC NULLS LAST
(6 rows)

I'll add this to the next CF. Comments are welcome!

Best regards,
Etsuro Fujita

[1]: /messages/by-id/5710D7E2.7010302@lab.ntt.co.jp
[2]: /messages/by-id/b4549406-909f-7d15-dc34-499835a8f0b3@lab.ntt.co.jp
/messages/by-id/b4549406-909f-7d15-dc34-499835a8f0b3@lab.ntt.co.jp
[3]: /messages/by-id/a1fa1c4c-bf96-8ea5-cff5-85b927298e73@lab.ntt.co.jp
/messages/by-id/a1fa1c4c-bf96-8ea5-cff5-85b927298e73@lab.ntt.co.jp

Attachments:

postgres-fdw-more-full-join-pushdown.patchbinary/octet-stream; name=postgres-fdw-more-full-join-pushdown.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 48,53 ****
--- 48,54 ----
  #include "nodes/nodeFuncs.h"
  #include "nodes/plannodes.h"
  #include "optimizer/clauses.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/prep.h"
  #include "optimizer/tlist.h"
  #include "optimizer/var.h"
***************
*** 88,93 **** typedef struct foreign_loc_cxt
--- 89,108 ----
  } foreign_loc_cxt;
  
  /*
+  * Aliased expresions delivered by subqueries deparsed in the FROM clause of
+  * the remote query.
+  */
+ typedef struct AliasedExprs
+ {
+ 	List	   *exprs;			/* output columns of subqueries in FROM */
+ 	int			max_exprs;		/* maximum number of columns stored */
+ 	int		   *tabnos;			/* subselect table alias numbers of columns */
+ 	int		   *colnos;			/* subselect column alias numbers of columns */
+ 	int			next_tabno;		/* table alias number of next subquery */
+ 	Relids		relids;			/* all relids appearing in subqueries */
+ } AliasedExprs;
+ 
+ /*
   * Context for deparseExpr
   */
  typedef struct deparse_expr_cxt
***************
*** 96,104 **** typedef struct deparse_expr_cxt
--- 111,122 ----
  	RelOptInfo *foreignrel;		/* the foreign relation we are planning for */
  	StringInfo	buf;			/* output buffer to append to */
  	List	  **params_list;	/* exprs that will become remote Params */
+ 	AliasedExprs *aliased;		/* aliased exprs delivered by subqueries */
  } deparse_expr_cxt;
  
  #define REL_ALIAS_PREFIX	"r"
+ #define SS_TAB_ALIAS_PREFIX	"ss"
+ #define SS_COL_ALIAS_PREFIX	"c"
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
***************
*** 135,140 **** static void deparseColumnRef(StringInfo buf, int varno, int varattno,
--- 153,159 ----
  static void deparseRelation(StringInfo buf, Relation rel);
  static void deparseExpr(Expr *expr, deparse_expr_cxt *context);
  static void deparseVar(Var *node, deparse_expr_cxt *context);
+ static void deparsePlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context);
  static void deparseConst(Const *node, deparse_expr_cxt *context);
  static void deparseParam(Param *node, deparse_expr_cxt *context);
  static void deparseArrayRef(ArrayRef *node, deparse_expr_cxt *context);
***************
*** 152,164 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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);
  
  
  /*
--- 171,189 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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,
! 					  List **params_list, AliasedExprs *aliased);
! 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);
  
  
  /*
***************
*** 324,329 **** foreign_expr_walker(Node *node,
--- 349,365 ----
  				}
  			}
  			break;
+ 		case T_PlaceHolderVar:
+ 			{
+ 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 
+ 				/*
+ 				 * Check if the contained expr is safe to execute remotely.
+ 				 */
+ 				return foreign_expr_walker((Node *) phv->phexpr,
+ 										   glob_cxt, outer_cxt);
+ 			}
+ 			break;
  		case T_Const:
  			{
  				Const	   *c = (Const *) node;
***************
*** 722,742 **** deparse_type_name(Oid type_oid, int32 typemod)
   * foreign server for the given relation.
   */
  List *
! build_tlist_to_deparse(RelOptInfo *foreignrel)
  {
! 	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
! 	 * We require columns specified in foreignrel->reltarget->exprs and those
! 	 * required for evaluating the local conditions.
  	 */
! 	tlist = add_to_flat_tlist(tlist,
! 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
! 									   PVC_RECURSE_PLACEHOLDERS));
! 	tlist = add_to_flat_tlist(tlist,
! 							  pull_var_clause((Node *) fpinfo->local_conds,
! 											  PVC_RECURSE_PLACEHOLDERS));
  
  	return tlist;
  }
--- 758,837 ----
   * foreign server for the given relation.
   */
  List *
! build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel)
  {
! 	List	   *tlist;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
! 	 * Add expressions in the foreignrel's reltarget as-is if the reltarget
! 	 * expressions are all shippable.  Otherwise add shipplable expressions in
! 	 * the reltarget as-is plus expressions required to evaluate non-shippable
! 	 * expressions in the reltarget.
! 	 */
! 	if (fpinfo->reltarget_is_shippable)
! 		tlist = make_tlist_from_pathtarget(foreignrel->reltarget);
! 	else
! 	{
! 		List	   *exprs = NIL;
! 		ListCell   *lc;
! 
! 		/* Note: we have at least one non-shippable PHV in the reltarget. */
! 		foreach(lc, foreignrel->reltarget->exprs)
! 		{
! 			Node	   *node = (Node *) lfirst(lc);
! 
! 			Assert(IsA(node, Var) || IsA(node, PlaceHolderVar));
! 
! 			if (IsA(node, Var))
! 				exprs = lappend(exprs, node);
! 			else
! 			{
! 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 				PlaceHolderInfo *phinfo = find_placeholder_info(root, phv,
! 																false);
! 
! 				if (bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->outerrel->relids) &&
! 					bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->innerrel->relids))
! 				{
! 					/*
! 					 * The PHV coming from an either input should be shippable.
! 					 */
! 					exprs = lappend(exprs, node);
! 				}
! 				else
! 				{
! 					/*
! 					 * The PHV may be shippable or may be not, but in any case
! 					 * just retrieve expressions required to evaluate the PHV.
! 					 */
! 					List	   *exprs2 = pull_var_clause(node,
! 													PVC_INCLUDE_PLACEHOLDERS);
! 					ListCell   *lc2;
! 
! 					foreach(lc2, exprs2)
! 					{
! 						Node	   *node2 = (Node *) lfirst(lc2);
! 
! 						exprs = lappend(exprs, node2);
! 					}
! 				}
! 			}
! 		}
! 
! 		tlist = NIL;
! 		tlist = add_to_flat_tlist(tlist, exprs);
! 	}
! 
! 	/*
! 	 * Add expressions required for evaluating local conditions, if any.
  	 */
! 	if (fpinfo->local_conds)
! 		tlist = add_to_flat_tlist(tlist,
! 								pull_var_clause((Node *) fpinfo->local_conds,
! 												PVC_INCLUDE_PLACEHOLDERS));
  
  	return tlist;
  }
***************
*** 766,771 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
--- 861,867 ----
  						List *tlist, List *remote_conds, List *pathkeys,
  						List **retrieved_attrs, List **params_list)
  {
+ 	AliasedExprs aliased;
  	deparse_expr_cxt context;
  
  	/* We handle relations for foreign tables and joins between those */
***************
*** 773,795 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  		   rel->reloptkind == RELOPT_BASEREL ||
  		   rel->reloptkind == RELOPT_OTHER_MEMBER_REL);
  
  	/* Fill portions of context common to join and base relation */
  	context.buf = buf;
  	context.root = root;
  	context.foreignrel = rel;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause and FROM clause */
! 	deparseSelectSql(tlist, retrieved_attrs, &context);
! 
! 	/*
! 	 * Construct WHERE clause
! 	 */
! 	if (remote_conds)
! 	{
! 		appendStringInfo(buf, " WHERE ");
! 		appendConditions(remote_conds, &context);
! 	}
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
--- 869,891 ----
  		   rel->reloptkind == RELOPT_BASEREL ||
  		   rel->reloptkind == RELOPT_OTHER_MEMBER_REL);
  
+ 	/* 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;
+ 
  	/* Fill portions of context common to join and base relation */
  	context.buf = buf;
  	context.root = root;
  	context.foreignrel = rel;
  	context.params_list = params_list;
+ 	context.aliased = &aliased;
  
! 	/* Construct SELECT clause and FROM clause and WHERE clause */
! 	deparseSelectSql(tlist, remote_conds, retrieved_attrs, &context);
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
***************
*** 802,808 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ....".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
--- 898,904 ----
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ... WHERE ...".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
***************
*** 811,831 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
  	PlannerInfo *root = context->root;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
  	 * Construct SELECT list
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
--- 907,944 ----
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
! 				 deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
  	PlannerInfo *root = context->root;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	StringInfoData jointree;
+ 
+ 	/*
+ 	 * Deparse a join tree expression in FROM clause first.
+ 	 */
+ 	initStringInfo(&jointree);
+ 	deparseFromExprForRel(&jointree, root, foreignrel,
+ 						  (foreignrel->reloptkind == RELOPT_JOINREL),
+ 						  context->params_list, context->aliased);
  
  	/*
  	 * Construct SELECT list
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	/*
! 	 * Join relation should have a non-NIL tlist, but base relation may have
! 	 * it as well!  For example, if a base relation involved in a foreign
! 	 * join has non-Var expressions such as whole-row Vars in the reltarget,
! 	 * the tlist of the base relation would be non-NIL.
! 	 */
! 	if (tlist != NIL)
  	{
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
***************
*** 850,859 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	/*
  	 * Construct FROM clause
  	 */
! 	appendStringInfoString(buf, " FROM ");
! 	deparseFromExprForRel(buf, root, foreignrel,
! 						  (foreignrel->reloptkind == RELOPT_JOINREL),
! 						  context->params_list);
  }
  
  /*
--- 963,978 ----
  	/*
  	 * Construct FROM clause
  	 */
! 	appendStringInfo(buf, " FROM %s", jointree.data);
! 
! 	/*
! 	 * Construct WHERE clause
! 	 */
! 	if (remote_conds)
! 	{
! 		appendStringInfoString(buf, " WHERE ");
! 		appendConditions(remote_conds, context);
! 	}
  }
  
  /*
***************
*** 881,887 **** deparseTargetList(StringInfo buf,
  	bool		first;
  	int			i;
  
! 	*retrieved_attrs = NIL;
  
  	/* If there's a whole-row reference, we'll need all the columns. */
  	have_wholerow = bms_is_member(0 - FirstLowInvalidHeapAttributeNumber,
--- 1000,1007 ----
  	bool		first;
  	int			i;
  
! 	if (retrieved_attrs)
! 		*retrieved_attrs = NIL;
  
  	/* If there's a whole-row reference, we'll need all the columns. */
  	have_wholerow = bms_is_member(0 - FirstLowInvalidHeapAttributeNumber,
***************
*** 908,914 **** deparseTargetList(StringInfo buf,
  
  			deparseColumnRef(buf, rtindex, i, root, qualify_col);
  
! 			*retrieved_attrs = lappend_int(*retrieved_attrs, i);
  		}
  	}
  
--- 1028,1035 ----
  
  			deparseColumnRef(buf, rtindex, i, root, qualify_col);
  
! 			if (retrieved_attrs)
! 				*retrieved_attrs = lappend_int(*retrieved_attrs, i);
  		}
  	}
  
***************
*** 929,936 **** deparseTargetList(StringInfo buf,
  			ADD_REL_QUALIFIER(buf, rtindex);
  		appendStringInfoString(buf, "ctid");
  
! 		*retrieved_attrs = lappend_int(*retrieved_attrs,
! 									   SelfItemPointerAttributeNumber);
  	}
  
  	/* Don't generate bad syntax if no undropped columns */
--- 1050,1058 ----
  			ADD_REL_QUALIFIER(buf, rtindex);
  		appendStringInfoString(buf, "ctid");
  
! 		if (retrieved_attrs)
! 			*retrieved_attrs = lappend_int(*retrieved_attrs,
! 										   SelfItemPointerAttributeNumber);
  	}
  
  	/* Don't generate bad syntax if no undropped columns */
***************
*** 948,957 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1070,1084 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->foreignrel;
+ 	AliasedExprs *aliased = context->aliased;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
+ 		/* Ignore if it's already deparsed in a lower subquery. */
+ 		if (bms_is_member(relid, aliased->relids))
+ 			continue;
+ 
  		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
***************
*** 1115,1133 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		/* We expect only Var nodes here */
! 		if (!IsA(var, Var))
! 			elog(ERROR, "non-Var not expected in target list");
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseVar(var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
--- 1242,1262 ----
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		/* We expect only Var or PlaceHolderVar nodes here */
! 		if (!IsA(var, Var) && !IsA(var, PlaceHolderVar))
! 			elog(ERROR, "unexpected node type in target list: %d",
! 			     (int) nodeTag(var));
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseExpr((Expr *) var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
+ 	/* Don't generate bad syntax if no columns */
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
***************
*** 1140,1147 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
   * 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;
  
--- 1269,1277 ----
   * alias if so requested.
   */
  static void
! deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
! 					  RelOptInfo *foreignrel, bool add_rel_alias,
! 					  List **params_list, AliasedExprs *aliased)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
***************
*** 1149,1164 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  	{
  		RelOptInfo *rel_o = fpinfo->outerrel;
  		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
--- 1279,1358 ----
  	{
  		RelOptInfo *rel_o = fpinfo->outerrel;
  		RelOptInfo *rel_i = fpinfo->innerrel;
+ 		PgFdwRelationInfo *fpinfo_o = (PgFdwRelationInfo *) rel_o->fdw_private;
+ 		PgFdwRelationInfo *fpinfo_i = (PgFdwRelationInfo *) rel_i->fdw_private;
  		StringInfoData join_sql_o;
  		StringInfoData join_sql_i;
  
+ 		Assert(fpinfo_o->local_conds == NIL);
+ 		Assert(fpinfo_i->local_conds == NIL);
+ 		Assert(fpinfo_o->reltarget_is_shippable);
+ 		Assert(fpinfo_i->reltarget_is_shippable);
+ 
  		/* Deparse outer relation */
  		initStringInfo(&join_sql_o);
! 		/*
! 		 * If the outer-relation's reltarget has any non-Var expressions, or
! 		 * if the foreign join is a full join and the outer relation has any
! 		 * conditions that need to be evaluated remotely, we deparse the outer
! 		 * relation as a subquery.
! 		 */
! 		if (fpinfo_o->reltarget_has_non_vars ||
! 			(fpinfo->jointype == JOIN_FULL && fpinfo_o->remote_conds))
! 		{
! 			List	   *tlist = NIL;
! 			List	   *retrieved_attrs = NIL;
! 			deparse_expr_cxt context;
! 
! 			context.buf = &join_sql_o;
! 			context.root = root;
! 			context.foreignrel = rel_o;
! 			context.params_list = params_list;
! 			context.aliased = aliased;
! 
! 			tlist = build_tlist_to_deparse(root, rel_o);
! 			appendStringInfoChar(&join_sql_o, '(');
! 			deparseSelectSql(tlist,
! 							 fpinfo_o->remote_conds,
! 							 &retrieved_attrs,
! 							 &context);
! 			deparseLockingClause(&context);
! 			appendStringInfoChar(&join_sql_o, ')');
! 			appendSubselectAlias(rel_o->reltarget->exprs, &context);
! 		}
! 		else
! 			deparseFromExprForRel(&join_sql_o, root, rel_o, true,
! 								  params_list, aliased);
  
  		/* Deparse inner relation */
  		initStringInfo(&join_sql_i);
! 		/* Likewise for the inner relation */
! 		if (fpinfo_i->reltarget_has_non_vars ||
! 			(fpinfo->jointype == JOIN_FULL && fpinfo_i->remote_conds))
! 		{
! 			List	   *tlist = NIL;
! 			List	   *retrieved_attrs = NIL;
! 			deparse_expr_cxt context;
! 
! 			context.buf = &join_sql_i;
! 			context.root = root;
! 			context.foreignrel = rel_i;
! 			context.params_list = params_list;
! 			context.aliased = aliased;
! 
! 			tlist = build_tlist_to_deparse(root, rel_i);
! 			appendStringInfoChar(&join_sql_i, '(');
! 			deparseSelectSql(tlist,
! 							 fpinfo_i->remote_conds,
! 							 &retrieved_attrs,
! 							 &context);
! 			deparseLockingClause(&context);
! 			appendStringInfoChar(&join_sql_i, ')');
! 			appendSubselectAlias(rel_i->reltarget->exprs, &context);
! 		}
! 		else
! 			deparseFromExprForRel(&join_sql_i, root, rel_i, true,
! 								  params_list, aliased);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
***************
*** 1177,1182 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1371,1377 ----
  			context.foreignrel = foreignrel;
  			context.root = root;
  			context.params_list = params_list;
+ 			context.aliased = aliased;
  
  			appendStringInfo(buf, "(");
  			appendConditions(fpinfo->joinclauses, &context);
***************
*** 1205,1211 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (use_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
--- 1400,1406 ----
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (add_rel_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
***************
*** 1213,1218 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1408,1529 ----
  }
  
  /*
+  * Add a subselect alias to a subquery in the FROM clause of the remote query.
+  */
+ static void
+ appendSubselectAlias(List *exprs, deparse_expr_cxt *context)
+ {
+ 	StringInfo	buf = context->buf;
+ 	RelOptInfo *foreignrel = context->foreignrel;
+ 	AliasedExprs *aliased = context->aliased;
+ 	int			tabno = aliased->next_tabno;
+ 	int			colno = 1;
+ 	bool		first;
+ 	ListCell   *lc;
+ 
+ 	/* Append the subselect table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the subselect column aliases */
+ 	first = true;
+ 	appendStringInfoChar(buf, '(');
+ 	foreach(lc, exprs)
+ 	{
+ 		Expr	   *expr = (Expr *) lfirst(lc);
+ 
+ 		if (!first)
+ 			appendStringInfoString(buf, ", ");
+ 		first = false;
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, colno);
+ 
+ 		registerAliasedExpr(expr, tabno, colno, aliased);
+ 
+ 		colno++;
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ 
+ 	/* Update next table number for next subquery */
+ 	aliased->next_tabno++;
+ 
+ 	/* Add foreignrel's relids to aliased->relids */
+ 	aliased->relids = bms_add_members(aliased->relids, foreignrel->relids);
+ }
+ 
+ /*
+  * Save a given aliased expression into the AliasedExprs struct.
+  */
+ static void
+ registerAliasedExpr(Expr *expr, int tabno, int colno, AliasedExprs *aliased)
+ {
+ 	int			num_exprs = list_length(aliased->exprs);
+ 	int			max_exprs = aliased->max_exprs;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/*
+ 	 * If we already have the expression, all we need to do is rewrite the
+ 	 * corresponding table/column numbers in the tabnos/colnos arrays into
+ 	 * the given table/column numbers.
+ 	 */
+ 	i = 0;
+ 	foreach(lc, aliased->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) expr))
+ 		{
+ 			aliased->tabnos[i] = tabno;
+ 			aliased->colnos[i] = colno;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/*
+ 	 * This is a new one; add the given expression to the exprs list and
+ 	 * write the given table/column numbers into the tabnos/colnos arrays.
+ 	 */
+ 	aliased->exprs = lappend(aliased->exprs, expr);
+ 	if (num_exprs + 1 >= max_exprs)
+ 	{
+ 		max_exprs *= 2;
+ 		aliased->tabnos = (int *) repalloc(aliased->tabnos,
+ 										   max_exprs * sizeof(int));
+ 		aliased->colnos = (int *) repalloc(aliased->colnos,
+ 										   max_exprs * sizeof(int));
+ 		aliased->max_exprs = max_exprs;
+ 	}
+ 	aliased->tabnos[num_exprs] = tabno;
+ 	aliased->colnos[num_exprs] = colno;
+ }
+ 
+ /*
+  * Returns true if given expr is delivered and aliased by a subquery-in-FROM.
+  */
+ static bool
+ is_aliased_expr(Expr *node, int *tabno, int *colno, deparse_expr_cxt *context)
+ {
+ 	AliasedExprs *aliased = context->aliased;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	if (!aliased)
+ 		return false;
+ 
+ 	i = 0;
+ 	foreach(lc, aliased->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*tabno = aliased->tabnos[i];
+ 			*colno = aliased->colnos[i];
+ 			return true;
+ 		}
+ 		i++;
+ 	}
+ 	return false;
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 1345,1350 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
--- 1656,1662 ----
  	context.foreignrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
+ 	context.aliased = NULL;
  
  	appendStringInfoString(buf, "UPDATE ");
  	deparseRelation(buf, rel);
***************
*** 1429,1434 **** deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
--- 1741,1747 ----
  	context.foreignrel = baserel;
  	context.buf = buf;
  	context.params_list = params_list;
+ 	context.aliased = NULL;
  
  	appendStringInfoString(buf, "DELETE FROM ");
  	deparseRelation(buf, rel);
***************
*** 1585,1610 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.  However, we must be
! 		 * careful; the table could be beneath an outer join, in which case it
! 		 * must go to NULL whenever the rest of the row does.
  		 */
  		Oid			fetchval = 0;
  
  		if (varattno == TableOidAttributeNumber)
  		{
  			rte = planner_rt_fetch(varno, root);
  			fetchval = rte->relid;
  		}
  
! 		if (qualify_col)
! 		{
! 			appendStringInfoString(buf, "CASE WHEN (");
! 			ADD_REL_QUALIFIER(buf, varno);
! 			appendStringInfo(buf, "*)::text IS NOT NULL THEN %u END", fetchval);
! 		}
! 		else
! 			appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
--- 1898,1916 ----
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.
  		 */
  		Oid			fetchval = 0;
  
+ 		Assert(!qualify_col);
+ 
  		if (varattno == TableOidAttributeNumber)
  		{
  			rte = planner_rt_fetch(varno, root);
  			fetchval = rte->relid;
  		}
  
! 		appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
***************
*** 1612,1619 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  		Relation	rel;
  		Bitmapset  *attrs_used;
  
! 		/* Required only to be passed down to deparseTargetList(). */
! 		List	   *retrieved_attrs;
  
  		/* Get RangeTblEntry from array in PlannerInfo. */
  		rte = planner_rt_fetch(varno, root);
--- 1918,1924 ----
  		Relation	rel;
  		Bitmapset  *attrs_used;
  
! 		Assert(!qualify_col);
  
  		/* Get RangeTblEntry from array in PlannerInfo. */
  		rte = planner_rt_fetch(varno, root);
***************
*** 1635,1662 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  		attrs_used = bms_add_member(NULL,
  									0 - FirstLowInvalidHeapAttributeNumber);
  
- 		/*
- 		 * In case the whole-row reference is under an outer join then it has
- 		 * to go NULL whenever the rest of the row goes NULL. Deparsing a join
- 		 * query would always involve multiple relations, thus qualify_col
- 		 * would be true.
- 		 */
- 		if (qualify_col)
- 		{
- 			appendStringInfoString(buf, "CASE WHEN (");
- 			ADD_REL_QUALIFIER(buf, varno);
- 			appendStringInfo(buf, "*)::text IS NOT NULL THEN ");
- 		}
- 
  		appendStringInfoString(buf, "ROW(");
  		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
! 						  &retrieved_attrs);
  		appendStringInfoString(buf, ")");
  
- 		/* Complete the CASE WHEN statement started above. */
- 		if (qualify_col)
- 			appendStringInfo(buf, " END");
- 
  		heap_close(rel, NoLock);
  		bms_free(attrs_used);
  	}
--- 1940,1950 ----
  		attrs_used = bms_add_member(NULL,
  									0 - FirstLowInvalidHeapAttributeNumber);
  
  		appendStringInfoString(buf, "ROW(");
  		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
! 						  NULL);
  		appendStringInfoString(buf, ")");
  
  		heap_close(rel, NoLock);
  		bms_free(attrs_used);
  	}
***************
*** 1785,1798 **** deparseStringLiteral(StringInfo buf, const char *val)
--- 2073,2100 ----
  static void
  deparseExpr(Expr *node, deparse_expr_cxt *context)
  {
+ 	int			tabno;
+ 	int			colno;
+ 
  	if (node == NULL)
  		return;
  
+ 	if (is_aliased_expr(node, &tabno, &colno, context))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	switch (nodeTag(node))
  	{
  		case T_Var:
  			deparseVar((Var *) node, context);
  			break;
+ 		case T_PlaceHolderVar:
+ 			deparsePlaceHolderVar((PlaceHolderVar *) node, context);
+ 			break;
  		case T_Const:
  			deparseConst((Const *) node, context);
  			break;
***************
*** 1882,1887 **** deparseVar(Var *node, deparse_expr_cxt *context)
--- 2184,2198 ----
  }
  
  /*
+  * Deparse given PlaceHolderVar node into context->buf.
+  */
+ static void
+ deparsePlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context)
+ {
+ 	deparseExpr(node->phexpr, context);
+ }
+ 
+ /*
   * Deparse given constant value into context->buf.
   *
   * This function has to be kept in sync with ruleutils.c's get_const_expr.
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 448,455 **** SELECT t1.c1, t2."C 1" FROM ft2 t1 LEFT JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1"
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                        QUERY PLAN                                                                        
! ---------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
--- 448,455 ----
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
***************
*** 458,464 **** EXPLAIN (VERBOSE, COSTS OFF)
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (((r2."C 1" = r3."C 1")))) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
--- 458,464 ----
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (TRUE)) WHERE ((r2."C 1" = r3."C 1")) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
***************
*** 971,984 **** ANALYZE ft5;
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                         QUERY PLAN                                                                                        
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 971,984 ----
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                              QUERY PLAN                                                                                              
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 999,1006 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                             QUERY PLAN                                                                                             
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
--- 999,1006 ----
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                                       QUERY PLAN                                                                                                       
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
***************
*** 1009,1015 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) INNER JOIN "S 1"."T 3" r4 ON (((r1."C 1" = r4.c1))))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
--- 1009,1015 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) INNER JOIN "S 1"."T 3" r4 ON (TRUE)) WHERE ((r1."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
***************
*** 1215,1237 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1215,1227 ----
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                       QUERY PLAN                                                                                                                                       
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT ss1.c1, ss2.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) ss1(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) ss2(c1) ON (((ss1.c1 = ss2.c1)))) ORDER BY ss1.c1 ASC NULLS LAST, ss2.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1249,1262 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                            QUERY PLAN                                                                                                                                            
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
--- 1239,1252 ----
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                        QUERY PLAN                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT ss1.c1, ss1.c2, r4.c1 FROM ((SELECT r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))) ss1(c1, c2) FULL JOIN "S 1"."T 3" r4 ON (((ss1.c2 = r4.c1)))) ORDER BY ss1.c1 ASC NULLS LAST, ss1.c2 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 1445,1458 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) LEFT
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                      QUERY PLAN                                                                                      
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ON (((r2."C 1" = r4.c1))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
--- 1435,1448 ----
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) ON (((r2."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 1505,1512 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1495,1502 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                   
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1514,1520 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1504,1510 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1549,1556 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1539,1546 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                         
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1558,1564 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1548,1554 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1594,1601 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                               
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1584,1591 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                   QUERY PLAN                                                                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1603,1609 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1593,1599 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1638,1645 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1628,1635 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                        
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1647,1653 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1637,1643 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1683,1697 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                              QUERY PLAN                                                              
! -------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
--- 1673,1687 ----
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                                    QUERY PLAN                                                                   
! ------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
***************
*** 1717,1730 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1707,1720 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss1.c4, ss2.c1 FROM ((SELECT ctid, ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1", c3 FROM "S 1"."T 1") ss1(c1, c2, c3, c4) INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c3 = ss2.c2)) ORDER BY ss1.c4 ASC NULLS LAST, ss1.c3 ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 1947,1954 **** SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                       QUERY PLAN                                                                       
! -------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
--- 1937,1944 ----
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                             QUERY PLAN                                                                            
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
***************
*** 1958,1964 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 1948,1954 ----
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1979,1986 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                      QUERY PLAN                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
--- 1969,1976 ----
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                           QUERY PLAN                                                                           
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
***************
*** 1996,2006 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
--- 1986,1996 ----
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
***************
*** 2021,2028 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
--- 2011,2018 ----
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                                   QUERY PLAN                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
***************
*** 2035,2041 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
--- 2025,2031 ----
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
***************
*** 2053,2077 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
     1
  (10 rows)
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                         QUERY PLAN                                                         
! ---------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: (13), ft2.c1
!    Join Filter: (13 = ft2.c1)
!    ->  Foreign Scan on public.ft2
!          Output: ft2.c1
!          Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (("C 1" >= 10)) AND (("C 1" <= 15)) ORDER BY "C 1" ASC NULLS LAST
!    ->  Materialize
!          Output: (13)
!          ->  Foreign Scan on public.ft1
!                Output: 13
!                Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 13))
! (11 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
--- 2043,2058 ----
     1
  (10 rows)
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                                                                QUERY PLAN                                                                                               
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: (13), ft2.c1
!    Relations: (public.ft2) LEFT JOIN (public.ft1)
!    Remote SQL: SELECT r2."C 1", ss1.c1 FROM ("S 1"."T 1" r2 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) ss1(c1) ON (((13 = r2."C 1")))) WHERE ((r2."C 1" >= 10)) AND ((r2."C 1" <= 15))
! (4 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
***************
*** 2084,2107 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
      | 15
  (6 rows)
  
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                     QUERY PLAN                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Join Filter: (ft4.c1 = ft1.c1)
!    ->  Foreign Scan on public.ft4
!          Output: ft4.c1, ft4.c2, ft4.c3
!          Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 10)) AND ((c1 <= 15))
!    ->  Materialize
!          Output: ft1.c1, ft2.c1, (13)
!          ->  Foreign Scan
!                Output: ft1.c1, ft2.c1, 13
!                Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
! (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
--- 2065,2079 ----
      | 15
  (6 rows)
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                                                                                   QUERY PLAN                                                                                                                                                  
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Relations: (public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2))
!    Remote SQL: SELECT r1.c1, ss1.c1, ss1.c2, ss1.c3 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4."C 1", r5."C 1", 13 FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12))) ss1(c1, c2, c3) ON (((r1.c1 = ss1.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
! (4 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
***************
*** 2111,2126 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
   14 |    |    |   
  (3 rows)
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                 QUERY PLAN                                                                                                                                 
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2083,2124 ----
   14 |    |    |   
  (3 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ss.*, ft2.c1 FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) ss(f1, f2, f3) ON (ft2.c1 = ss.f1) WHERE ft2.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                                  QUERY PLAN                                                                                                                                                                                  
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (13), (13), ft2_1.c1, ft2.c1
+    Relations: (public.ft2) LEFT JOIN ((public.ft2) LEFT JOIN (public.ft1))
+    Remote SQL: SELECT r1."C 1", ss2.c1, ss2.c2, ss2.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r5."C 1", 13, ss1.c1 FROM ("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) ss1(c1) ON (((13 = r5."C 1")))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))) ss2(c1, c2, c3) ON (((r1."C 1" = 13)))) WHERE ((r1."C 1" >= 10)) AND ((r1."C 1" <= 15))
+ (4 rows)
+ 
+ SELECT ss.*, ft2.c1 FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) ss(f1, f2, f3) ON (ft2.c1 = ss.f1) WHERE ft2.c1 BETWEEN 10 AND 15;
+  f1 | f2 | f3 | c1 
+ ----+----+----+----
+     |    |    | 10
+     |    |    | 11
+     |    |    | 12
+  13 |    | 10 | 13
+  13 |    | 11 | 13
+  13 |    | 12 | 13
+  13 | 13 | 13 | 13
+  13 |    | 14 | 13
+  13 |    | 15 | 13
+     |    |    | 14
+     |    |    | 15
+ (11 rows)
+ 
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                        QUERY PLAN                                                                                                                                       
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss1.c4, r2.c1, r2.c2 FROM ((SELECT ROW(c1, c2, c3), c1, c2, c3 FROM "S 1"."T 4") ss1(c1, c2, c3, c4) INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((ss1.c2 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY ss1.c2 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 2280,2291 **** DROP ROLE regress_view_owner;
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                           QUERY PLAN                                                          
! ------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = 2)) AND ((r1."C 1" = 1))))
  (4 rows)
  
  EXECUTE st1(1, 1);
--- 2278,2289 ----
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                                QUERY PLAN                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r2."C 1" = 2)) AND ((r1."C 1" = 1))
  (4 rows)
  
  EXECUTE st1(1, 1);
***************
*** 2894,2907 **** 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)
--- 2892,2905 ----
  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)
***************
*** 3037,3050 **** 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
!                                                                                                                               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)
--- 3035,3048 ----
  
  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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 27,32 ****
--- 27,33 ----
  #include "optimizer/cost.h"
  #include "optimizer/pathnode.h"
  #include "optimizer/paths.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/planmain.h"
  #include "optimizer/restrictinfo.h"
  #include "optimizer/var.h"
***************
*** 640,645 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 641,704 ----
  	}
  
  	/*
+ 	 * Detect whether the reltarget expressions are all shippable and whether
+ 	 * there are any non-Var expressions in the reltarget.
+ 	 *
+ 	 * Note: if the relation is an appendrel child, it isn't in the main join
+ 	 * tree, so these are not used.
+ 	 */
+ 	if (baserel->reloptkind == RELOPT_BASEREL)
+ 	{
+ 		bool		reltarget_is_shippable = true;
+ 		bool		reltarget_has_non_vars = false;
+ 		int			i;
+ 
+ 		foreach(lc, baserel->reltarget->exprs)
+ 		{
+ 			Node	   *node = (Node *) lfirst(lc);
+ 
+ 			Assert(IsA(node, Var) || IsA(node, PlaceHolderVar));
+ 
+ 			if (IsA(node, PlaceHolderVar))
+ 			{
+ 				reltarget_has_non_vars = true;
+ 				if (!is_foreign_expr(root, baserel, (Expr *) node))
+ 				{
+ 					reltarget_is_shippable = false;
+ 					break;
+ 				}
+ 			}
+ 		}
+ 
+ 		/*
+ 		 * Treat system columns other than citd and whole-row Vars like PHVs.
+ 		 *
+ 		 * Note: such system columns are deaprsed as 0, except for tableoid,
+ 		 * which is deparsed as a valid value for the local table OID, and
+ 		 * whole-row Vars as ROW() expressions.  Any Vars including these in
+ 		 * the reltarget are considered shippable, though.
+ 		 */
+ 		if (reltarget_is_shippable)
+ 		{
+ 			for (i = FirstLowInvalidHeapAttributeNumber + 1; i <= 0; i++)
+ 			{
+ 				if (i == SelfItemPointerAttributeNumber)
+ 					continue;
+ 
+ 				if (bms_is_member(i - FirstLowInvalidHeapAttributeNumber,
+ 							  fpinfo->attrs_used))
+ 				{
+ 					reltarget_has_non_vars = true;
+ 					break;
+ 				}
+ 			}
+ 		}
+ 
+ 		fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 		fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 	}
+ 
+ 	/*
  	 * Set the name of relation in fpinfo, while we are constructing it here.
  	 * It will be used to build the string describing the join relation in
  	 * EXPLAIN output. We can't know whether VERBOSE option is specified or
***************
*** 1177,1183 **** postgresGetForeignPlan(PlannerInfo *root,
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
--- 1236,1242 ----
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
***************
*** 2514,2520 **** estimate_path_cost_size(PlannerInfo *root,
  
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL)
! 			fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
--- 2573,2579 ----
  
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL)
! 			fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
***************
*** 3955,3960 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4014,4021 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	bool		reltarget_is_shippable = true;
+ 	bool		reltarget_has_non_vars = false;
  
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
***************
*** 3984,3989 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4045,4058 ----
  	if (fpinfo_o->local_conds || fpinfo_i->local_conds)
  		return false;
  
+ 	/*
+ 	 * If the reltargets of joining relations are not shippable, those are
+ 	 *  required to be evaluated locally, so the join can not be pushed down.
+ 	 */
+ 	if (!fpinfo_o->reltarget_is_shippable ||
+ 		!fpinfo_i->reltarget_is_shippable)
+ 		return false;
+ 
  	/* Separate restrict list into join quals and quals on join relation */
  	if (IS_OUTER_JOIN(jointype))
  		extract_actual_join_clauses(extra->restrictlist, &joinclauses, &otherclauses);
***************
*** 4009,4034 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			return false;
  	}
  
- 	/*
- 	 * deparseExplicitTargetList() isn't smart enough to handle anything other
- 	 * than a Var.  In particular, if there's some PlaceHolderVar that would
- 	 * need to be evaluated within this join tree (because there's an upper
- 	 * reference to a quantity that may go to NULL as a result of an outer
- 	 * join), then we can't try to push the join down because we'll fail when
- 	 * we get to deparseExplicitTargetList().  However, a PlaceHolderVar that
- 	 * needs to be evaluated *at the top* of this join tree is OK, because we
- 	 * can do that locally after fetching the results from the remote side.
- 	 */
- 	foreach(lc, root->placeholder_list)
- 	{
- 		PlaceHolderInfo *phinfo = lfirst(lc);
- 		Relids		relids = joinrel->relids;
- 
- 		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
- 			bms_nonempty_difference(relids, phinfo->ph_eval_at))
- 			return false;
- 	}
- 
  	/* Save the join clauses, for later use. */
  	fpinfo->joinclauses = joinclauses;
  
--- 4078,4083 ----
***************
*** 4057,4064 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4106,4112 ----
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4069,4076 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4117,4123 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4078,4106 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4125,4158 ----
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
  		case JOIN_FULL:
! 			/* nothing to do */
  			break;
  
  		default:
***************
*** 4108,4127 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			elog(ERROR, "unsupported join type %d", jointype);
  	}
  
  	/*
! 	 * For an inner join, all restrictions can be treated alike. Treating the
! 	 * pushed down conditions as join conditions allows a top level full outer
! 	 * join to be deparsed without requiring subqueries.
  	 */
! 	if (jointype == JOIN_INNER)
  	{
! 		Assert(!fpinfo->joinclauses);
! 		fpinfo->joinclauses = fpinfo->remote_conds;
! 		fpinfo->remote_conds = NIL;
! 	}
  
! 	/* Mark that this join can be pushed down safely */
! 	fpinfo->pushdown_safe = true;
  
  	/*
  	 * If user is willing to estimate cost for a scan of either of the joining
--- 4160,4198 ----
  			elog(ERROR, "unsupported join type %d", jointype);
  	}
  
+ 	/* Mark that this join can be pushed down safely */
+ 	fpinfo->pushdown_safe = true;
+ 
  	/*
! 	 * Detect whether the reltarget expressions are all shippable and whether
! 	 * there are any non-Var expressions in the reltarget.
  	 */
! 	foreach(lc, joinrel->reltarget->exprs)
  	{
! 		Node	   *node = (Node *) lfirst(lc);
  
! 		Assert(IsA(node, Var) || IsA(node, PlaceHolderVar));
! 
! 		if (IsA(node, PlaceHolderVar))
! 		{
! 			PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 			PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
! 
! 			/* Ignore the PHV if it has bubbled up from an either input. */
! 			if (bms_is_subset(phinfo->ph_eval_at, outerrel->relids) &&
! 				bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
! 				continue;
! 
! 			reltarget_has_non_vars = true;
! 			if (!is_foreign_expr(root, joinrel, (Expr *) node))
! 			{
! 				reltarget_is_shippable = false;
! 				break;
! 			}
! 		}
! 	}
! 	fpinfo->reltarget_is_shippable = reltarget_is_shippable;
! 	fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
  
  	/*
  	 * If user is willing to estimate cost for a scan of either of the joining
***************
*** 4512,4517 **** conversion_error_callback(void *arg)
--- 4583,4589 ----
  	const char *attname = NULL;
  	const char *relname = NULL;
  	bool		is_wholerow = false;
+ 	bool		is_placeholder = false;
  	ConversionLocation *errpos = (ConversionLocation *) arg;
  
  	if (errpos->rel)
***************
*** 4534,4559 **** conversion_error_callback(void *arg)
  		EState	   *estate = fsstate->ss.ps.state;
  		TargetEntry *tle;
  		Var		   *var;
- 		RangeTblEntry *rte;
  
  		Assert(IsA(fsplan, ForeignScan));
  		tle = (TargetEntry *) list_nth(fsplan->fdw_scan_tlist,
  									   errpos->cur_attno - 1);
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
- 		Assert(IsA(var, Var));
- 
- 		rte = rt_fetch(var->varno, estate->es_range_table);
  
! 		if (var->varattno == 0)
! 			is_wholerow = true;
  		else
! 			attname = get_relid_attribute_name(rte->relid, var->varattno);
  
! 		relname = get_rel_name(rte->relid);
  	}
  
! 	if (relname)
  	{
  		if (is_wholerow)
  			errcontext("whole-row reference to foreign table \"%s\"", relname);
--- 4606,4640 ----
  		EState	   *estate = fsstate->ss.ps.state;
  		TargetEntry *tle;
  		Var		   *var;
  
  		Assert(IsA(fsplan, ForeignScan));
  		tle = (TargetEntry *) list_nth(fsplan->fdw_scan_tlist,
  									   errpos->cur_attno - 1);
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		if (!IsA(var, Var))
! 		{
! 			Assert(IsA(var, PlaceHolderVar));
! 			is_placeholder = true;
! 		}
  		else
! 		{
! 			RangeTblEntry *rte  = rt_fetch(var->varno, estate->es_range_table);
! 
! 			if (var->varattno == 0)
! 				is_wholerow = true;
! 			else
! 				attname = get_relid_attribute_name(rte->relid, var->varattno);
  
! 			relname = get_rel_name(rte->relid);
! 		}
  	}
  
! 	if (is_placeholder)
! 		errcontext("placeholder expression at position %d in select list",
! 				   errpos->cur_attno);
! 	else if (relname)
  	{
  		if (is_wholerow)
  			errcontext("whole-row reference to foreign table \"%s\"", relname);
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 51,56 **** typedef struct PgFdwRelationInfo
--- 51,67 ----
  	/* Bitmap of attr numbers we need to fetch from the remote server. */
  	Bitmapset  *attrs_used;
  
+ 	/*
+ 	 * The relation is allowed to get joined with any other foreign table (or
+ 	 * join) at the remote server if the local_conds is NIL and the reltarget
+ 	 * expressions are all shippable.  In that case the relation involved in
+ 	 * a foreign join is deparsed as a subquery if the reltarget has non-Var
+ 	 * expressions or if the foreign join is a full join and the remote_conds
+ 	 * isn't NIL.
+ 	 */
+ 	bool		reltarget_is_shippable; /* are reltarget exprs shippable? */
+ 	bool		reltarget_has_non_vars;	/* are there any non-Var exprs? */
+ 
  	/* Cost and selectivity of local_conds. */
  	QualCost	local_conds_cost;
  	Selectivity local_conds_sel;
***************
*** 155,161 **** extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(RelOptInfo *foreign_rel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
--- 166,172 ----
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 485,501 **** EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
- 
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
--- 485,500 ----
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ss.*, ft2.c1 FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) ss(f1, f2, f3) ON (ft2.c1 = ss.f1) WHERE ft2.c1 BETWEEN 10 AND 15;
+ SELECT ss.*, ft2.c1 FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) ss(f1, f2, f3) ON (ft2.c1 = ss.f1) WHERE ft2.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
#2Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#1)
Re: Push down more full joins in postgres_fdw

Thanks Fujita-san for working on this. I took a quick look at the patch.
Here are some quick comments.

1. deparsePlaceHolderVar looks odd - each of the deparse* function is named
as deparse + <name of the parser node the string would parse into>.
PlaceHolderVar is not a parser node, so no string is going to be parsed as
a PlaceHolderVar. May be you want to name it as
deparseExerFromPlaceholderVar or something like that.

2. You will need to check phlevelsup member while assessing whether a PHV
is safe to push down.

3. I think registerAlias stuff should happen really at the time of creating
paths and should be stored in fpinfo. Without that it will be computed
every time we deparse the query. This means every time we try to EXPLAIN
the query at various levels in the join tree and once for the query to be
sent to the foreign server.

4. The changes related to retrieved_attrs look unrelated to the patch.
Either your patch should use the current method of handling retrieved_attrs
or there should be a separate patch for retrieved_attrs changes. May be you
want to take a look at the discussion in join pushdown thread as to why we
assume retrieved_attrs to be non-NIL always.

5. The blocks related to inner and outer relations in
deparseFromExprForRel() look same. We should probably separate that code
out into a function and call it at two places.

6.
! if (is_placeholder)
! errcontext("placeholder expression at position %d in select list",
! errpos->cur_attno);
A user wouldn't know what a placeholder expression is, there is no such
term in the documentation. We have to device a better way to provide an
error context for an expression in general.

On Fri, Aug 19, 2016 at 2:50 PM, Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp>
wrote:

Hi,

The postgres_fdw join pushdown in 9.6 is great, but it can't handle full
joins on relations with restrictions. The reason for that is, postgres_fdw
can't support deparsing subqueries when creating a remote join query. So,
by adding the deparsing logic to it, I removed that limitation. Attached
is a patch for that, which is based on the patch I posted before [1].

Also, by the deparsing logic, I improved the handling of PHVs so that PHVs
are evaluated remotely if it's safe, as discussed in [2]. Here is an
example from the regression test, which pushes down multiple levels of PHVs
to the remote:

EXPLAIN (VERBOSE, COSTS OFF)
SELECT ss.*, ft2.c1 FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM
(SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1)
WHERE ft2.\
c1 BETWEEN 10 AND 15) ss(f1, f2, f3) ON (ft2.c1 = ss.f1) WHERE ft2.c1
BETWEEN 10 AND 15;
\
QUERY PLAN \

------------------------------------------------------------
------------------------------------------------------------
-------------------------------\
------------------------------------------------------------
------------------------------------------------------------
-------------------------------\
---------------------------------------------------------------
Foreign Scan
Output: (13), (13), ft2_1.c1, ft2.c1
Relations: (public.ft2) LEFT JOIN ((public.ft2) LEFT JOIN (public.ft1))
Remote SQL: SELECT r1."C 1", ss2.c1, ss2.c2, ss2.c3 FROM ("S 1"."T 1"
r1 LEFT JOIN (SELECT r5."C 1", 13, ss1.c1 FROM ("S 1"."T 1" r5 LEFT JOIN
(SELE\
CT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) ss1(c1) ON (((13 = r5."C
1")))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))) ss2(c1, c2, c3) ON
(((r1.\
"C 1" = 13)))) WHERE ((r1."C 1" >= 10)) AND ((r1."C 1" <= 15))
(4 rows)

Also, in the same way as the PHV handling, I improved the handling of
whole-row reference (and system columns other than ctid), as proposed in
[3]. We don't need the ""CASE WHEN ... IS NOT NULL THEN ROW(...) END"
conditions any more, as shown in the below example:

EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)
ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
\
QUERY PLAN \

------------------------------------------------------------
------------------------------------------------------------
-------------------------------\
------------------------------------------------------------
------------------------------------------------------------
-------------------------------\
-----------------------------------------------------------
Limit
Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
-> Foreign Scan
Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss1.c4, ss2.c1 FROM
((SELECT ctid, ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1", c3 FROM "S
1"."T \
1") ss1(c1, c2, c3, c4) INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6,
c7, c8), "C 1" FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c3 =
ss2.\
c2)) ORDER BY ss1.c4 ASC NULLS LAST, ss1.c3 ASC NULLS LAST
(6 rows)

I'll add this to the next CF. Comments are welcome!

Best regards,
Etsuro Fujita

[1] /messages/by-id/5710D7E2.7010302@lab.ntt.co.jp
[2] /messages/by-id/b4549406-909f-7d15-dc3
4-499835a8f0b3%40lab.ntt.co.jp
[3] /messages/by-id/a1fa1c4c-bf96-8ea5-cff
5-85b927298e73%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

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

#3Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#2)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

Hi Ashutosh,

On 2016/08/22 15:49, Ashutosh Bapat wrote:

1. deparsePlaceHolderVar looks odd - each of the deparse* function is
named as deparse + <name of the parser node the string would parse
into>. PlaceHolderVar is not a parser node, so no string is going to be
parsed as a PlaceHolderVar. May be you want to name it as
deparseExerFromPlaceholderVar or something like that.

The name "deparseExerFromPlaceholderVar" seems long to me. How about
"deparsePlaceHolderExpr"?

2. You will need to check phlevelsup member while assessing whether a
PHV is safe to push down.

Good catch! In addition to that, I added another check that the eval_at
set for the PHV should be included in the relids set for the foreign
relation. I think that would make the shippability check more robust.

3. I think registerAlias stuff should happen really at the time of
creating paths and should be stored in fpinfo. Without that it will be
computed every time we deparse the query. This means every time we try
to EXPLAIN the query at various levels in the join tree and once for the
query to be sent to the foreign server.

Hmm. I think the overhead in calculating aliases would be negligible
compared to the overhead in explaining each remote query for costing or
sending the remote query for execution. So, I created aliases in the
same way as remote params created and stored into params_list in
deparse_expr_cxt. I'm not sure it's worth complicating the code.

4. The changes related to retrieved_attrs look unrelated to the patch.
Either your patch should use the current method of handling
retrieved_attrs or there should be a separate patch for retrieved_attrs
changes. May be you want to take a look at the discussion in join
pushdown thread as to why we assume retrieved_attrs to be non-NIL always.

OK, I removed those changes from the patch.

5. The blocks related to inner and outer relations in
deparseFromExprForRel() look same. We should probably separate that code
out into a function and call it at two places.

Done.

6.
! if (is_placeholder)
! errcontext("placeholder expression at position %d in select list",
! errpos->cur_attno);
A user wouldn't know what a placeholder expression is, there is no such
term in the documentation. We have to device a better way to provide an
error context for an expression in general.

Though I proposed that, I don't think that it's that important to let
users know that the expression is from a PHV. How about just saying
"expression", not "placeholder expression", so that we have the message
"expression at position %d in select list" in the context?

Attached is an updated version of the patch.

Other changes:

* Add a bit more regression test
* Revise code/comments
* Cleanups

Thanks for the comments!

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-more-full-join-pushdown-v2.patchbinary/octet-stream; name=postgres-fdw-more-full-join-pushdown-v2.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 48,53 ****
--- 48,54 ----
  #include "nodes/nodeFuncs.h"
  #include "nodes/plannodes.h"
  #include "optimizer/clauses.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/prep.h"
  #include "optimizer/tlist.h"
  #include "optimizer/var.h"
***************
*** 88,93 **** typedef struct foreign_loc_cxt
--- 89,107 ----
  } foreign_loc_cxt;
  
  /*
+  * Aliased expressions delivered by subqueries in FROM of the remote query.
+  */
+ typedef struct AliasedExprs
+ {
+ 	List	   *exprs;			/* output columns of subqueries in FROM */
+ 	int			max_exprs;		/* maximum number of columns stored */
+ 	int		   *tabnos;			/* subselect table alias numbers of columns */
+ 	int		   *colnos;			/* subselect column alias numbers of columns */
+ 	int			next_tabno;		/* table alias number of next subquery */
+ 	Relids		relids;			/* all relids of subqueries in FROM */
+ } AliasedExprs;
+ 
+ /*
   * Context for deparseExpr
   */
  typedef struct deparse_expr_cxt
***************
*** 95,104 **** typedef struct deparse_expr_cxt
--- 109,121 ----
  	PlannerInfo *root;			/* global planner state */
  	RelOptInfo *foreignrel;		/* the foreign relation we are planning for */
  	StringInfo	buf;			/* output buffer to append to */
+ 	AliasedExprs *aliased;		/* aliased exprs of subqueries in FROM */
  	List	  **params_list;	/* exprs that will become remote Params */
  } deparse_expr_cxt;
  
  #define REL_ALIAS_PREFIX	"r"
+ #define SS_TAB_ALIAS_PREFIX	"ss"
+ #define SS_COL_ALIAS_PREFIX	"c"
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
***************
*** 148,164 **** static void deparseRelabelType(RelabelType *node, deparse_expr_cxt *context);
  static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context);
  static void deparseNullTest(NullTest *node, deparse_expr_cxt *context);
  static void deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context);
  static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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);
  
  
  /*
--- 165,192 ----
  static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context);
  static void deparseNullTest(NullTest *node, deparse_expr_cxt *context);
  static void deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context);
+ static void deparsePlaceHolderExpr(PlaceHolderVar *node, deparse_expr_cxt *context);
  static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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);
  
  
  /*
***************
*** 631,636 **** foreign_expr_walker(Node *node,
--- 659,683 ----
  				check_type = false;
  			}
  			break;
+ 		case T_PlaceHolderVar:
+ 			{
+ 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 				PlaceHolderInfo *phinfo = find_placeholder_info(glob_cxt->root,
+ 																phv, false);
+ 
+ 				/*
+ 				 * If the PHV's contained expression is computable on the
+ 				 * remote server, we consider the PHV safe to send.
+ 				 */
+ 				if (phv->phlevelsup == 0 &&
+ 					bms_is_subset(phinfo->ph_eval_at,
+ 								  glob_cxt->foreignrel->relids))
+ 					return foreign_expr_walker((Node *) phv->phexpr,
+ 											   glob_cxt, outer_cxt);
+ 				else
+ 					return false;
+ 			}
+ 			break;
  		default:
  
  			/*
***************
*** 723,743 **** deparse_type_name(Oid type_oid, int32 typemod)
   * foreign server for the given relation.
   */
  List *
! build_tlist_to_deparse(RelOptInfo *foreignrel)
  {
! 	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
! 	 * We require columns specified in foreignrel->reltarget->exprs and those
! 	 * required for evaluating the local conditions.
  	 */
! 	tlist = add_to_flat_tlist(tlist,
! 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
! 									   PVC_RECURSE_PLACEHOLDERS));
! 	tlist = add_to_flat_tlist(tlist,
! 							  pull_var_clause((Node *) fpinfo->local_conds,
! 											  PVC_RECURSE_PLACEHOLDERS));
  
  	return tlist;
  }
--- 770,849 ----
   * foreign server for the given relation.
   */
  List *
! build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel)
  {
! 	List	   *tlist;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
! 	 * Add expressions in foreignrel's reltarget as-is if the expressions are
! 	 * all shippable.  Otherwise add shipplable expressions in the reltarget
! 	 * as-is plus expressions required to evaluate non-shippable expressions
! 	 * in the reltarget.
  	 */
! 	if (fpinfo->reltarget_is_shippable)
! 		tlist = make_tlist_from_pathtarget(foreignrel->reltarget);
! 	else
! 	{
! 		List	   *exprs = NIL;
! 		ListCell   *lc;
! 
! 		/* Note: we have at least one non-shippable PHV in the reltarget. */
! 		foreach(lc, foreignrel->reltarget->exprs)
! 		{
! 			Node	   *node = (Node *) lfirst(lc);
! 
! 			Assert(IsA(node, Var) || IsA(node, PlaceHolderVar));
! 
! 			if (IsA(node, Var))
! 				exprs = lappend(exprs, node);
! 			else
! 			{
! 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 				PlaceHolderInfo *phinfo = find_placeholder_info(root, phv,
! 																false);
! 
! 				if (bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->outerrel->relids) &&
! 					bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->innerrel->relids))
! 				{
! 					/*
! 					 * The PHV coming from an either input should be shippable.
! 					 */
! 					exprs = lappend(exprs, node);
! 				}
! 				else
! 				{
! 					/*
! 					 * The PHV might be shippable, but just retrieve
! 					 * expressions required to evaluate the PHV.
! 					 */
! 					List	   *exprs2 = pull_var_clause(node,
! 													PVC_INCLUDE_PLACEHOLDERS);
! 					ListCell   *lc2;
! 
! 					foreach(lc2, exprs2)
! 					{
! 						Node	   *node2 = (Node *) lfirst(lc2);
! 
! 						exprs = lappend(exprs, node2);
! 					}
! 				}
! 			}
! 		}
! 
! 		tlist = NIL;
! 		tlist = add_to_flat_tlist(tlist, exprs);
! 	}
! 
! 	/*
! 	 * Add expressions required to evaluate local conditions, if any.
! 	 */
! 	if (fpinfo->local_conds)
! 		tlist = add_to_flat_tlist(tlist,
! 								pull_var_clause((Node *) fpinfo->local_conds,
! 												PVC_INCLUDE_PLACEHOLDERS));
  
  	return tlist;
  }
***************
*** 767,772 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
--- 873,879 ----
  						List *tlist, List *remote_conds, List *pathkeys,
  						List **retrieved_attrs, List **params_list)
  {
+ 	AliasedExprs aliased;
  	deparse_expr_cxt context;
  
  	/* We handle relations for foreign tables and joins between those */
***************
*** 774,796 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  		   rel->reloptkind == RELOPT_BASEREL ||
  		   rel->reloptkind == RELOPT_OTHER_MEMBER_REL);
  
  	/* Fill portions of context common to join and base relation */
  	context.buf = buf;
  	context.root = root;
  	context.foreignrel = rel;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause and FROM clause */
! 	deparseSelectSql(tlist, retrieved_attrs, &context);
! 
! 	/*
! 	 * Construct WHERE clause
! 	 */
! 	if (remote_conds)
! 	{
! 		appendStringInfo(buf, " WHERE ");
! 		appendConditions(remote_conds, &context);
! 	}
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
--- 881,903 ----
  		   rel->reloptkind == RELOPT_BASEREL ||
  		   rel->reloptkind == RELOPT_OTHER_MEMBER_REL);
  
+ 	/* 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;
+ 
  	/* Fill portions of context common to join and base relation */
  	context.buf = buf;
  	context.root = root;
  	context.foreignrel = rel;
+ 	context.aliased = &aliased;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause, FROM clause, and WHERE clause */
! 	deparseSelectSql(tlist, remote_conds, retrieved_attrs, &context);
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
***************
*** 803,809 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ....".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
--- 910,916 ----
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ... WHERE ...".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
***************
*** 812,832 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
  	PlannerInfo *root = context->root;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
  	 * Construct SELECT list
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
--- 919,955 ----
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
! 				 deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
  	PlannerInfo *root = context->root;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	StringInfoData jointree;
+ 
+ 	/*
+ 	 * Deparse a join tree expression in FROM clause first.
+ 	 */
+ 	initStringInfo(&jointree);
+ 	deparseFromExprForRel(&jointree, root, foreignrel,
+ 						  (foreignrel->reloptkind == RELOPT_JOINREL),
+ 						  context->aliased, context->params_list);
  
  	/*
  	 * Construct SELECT list
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	/*
! 	 * Note: tlist for a base relation may be non-NIL.  For example, if the
! 	 * base relation that is involved in a foreign join has a whole-row Var
! 	 * in the reltarget, then tlist for the base relation would be non-NIL.
! 	 */
! 	if (tlist != NIL)
  	{
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
***************
*** 843,848 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
--- 966,973 ----
  		 */
  		Relation	rel = heap_open(rte->relid, NoLock);
  
+ 		Assert(foreignrel->reloptkind != RELOPT_JOINREL);
+ 
  		deparseTargetList(buf, root, foreignrel->relid, rel, false,
  						  fpinfo->attrs_used, false, retrieved_attrs);
  		heap_close(rel, NoLock);
***************
*** 851,860 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	/*
  	 * Construct FROM clause
  	 */
! 	appendStringInfoString(buf, " FROM ");
! 	deparseFromExprForRel(buf, root, foreignrel,
! 						  (foreignrel->reloptkind == RELOPT_JOINREL),
! 						  context->params_list);
  }
  
  /*
--- 976,991 ----
  	/*
  	 * Construct FROM clause
  	 */
! 	appendStringInfo(buf, " FROM %s", jointree.data);
! 
! 	/*
! 	 * Construct WHERE clause
! 	 */
! 	if (remote_conds)
! 	{
! 		appendStringInfoString(buf, " WHERE ");
! 		appendConditions(remote_conds, context);
! 	}
  }
  
  /*
***************
*** 965,974 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1096,1110 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->foreignrel;
+ 	AliasedExprs *aliased = context->aliased;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
+ 		/* Ignore if it's already deparsed in a lower subquery. */
+ 		if (bms_is_member(relid, aliased->relids))
+ 			continue;
+ 
  		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
***************
*** 1132,1150 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		/* We expect only Var nodes here */
! 		if (!IsA(var, Var))
! 			elog(ERROR, "non-Var not expected in target list");
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseVar(var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
--- 1268,1288 ----
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		/* We expect only Var or PlaceHolderVar nodes here */
! 		if (!IsA(var, Var) && !IsA(var, PlaceHolderVar))
! 			elog(ERROR, "unexpected node type in target list: %d",
! 			     (int) nodeTag(var));
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseExpr((Expr *) var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
+ 	/* Don't generate bad syntax if no columns */
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
***************
*** 1152,1191 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  /*
   * Construct FROM clause for given relation
   *
!  * 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)
  	{
! 		RelOptInfo *rel_o = fpinfo->outerrel;
! 		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
! 		 *
! 		 * ((outer relation) <join type> (inner relation) ON (joinclauses))
! 		 */
! 		appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
! 					   get_jointype_name(fpinfo->jointype), join_sql_i.data);
  
! 		/* Append join clause; (TRUE) if no join clause */
  		if (fpinfo->joinclauses)
  		{
  			deparse_expr_cxt context;
--- 1290,1325 ----
  /*
   * Construct FROM clause for given relation
   *
!  * For a join relation the clause of the following form is appended to buf:
!  * ((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 ");
  		if (fpinfo->joinclauses)
  		{
  			deparse_expr_cxt context;
***************
*** 1193,1209 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  			context.buf = buf;
  			context.foreignrel = foreignrel;
  			context.root = root;
  			context.params_list = params_list;
  
! 			appendStringInfo(buf, "(");
  			appendConditions(fpinfo->joinclauses, &context);
! 			appendStringInfo(buf, ")");
  		}
  		else
  			appendStringInfoString(buf, "(TRUE)");
  
  		/* End the FROM clause entry. */
! 		appendStringInfo(buf, ")");
  	}
  	else
  	{
--- 1327,1347 ----
  			context.buf = buf;
  			context.foreignrel = foreignrel;
  			context.root = root;
+ 			context.aliased = aliased;
  			context.params_list = params_list;
  
! 			appendStringInfoChar(buf, '(');
  			appendConditions(fpinfo->joinclauses, &context);
! 			appendStringInfoChar(buf, ')');
  		}
  		else
+ 		{
+ 			/* No join conditions; add "(TRUE)" */
  			appendStringInfoString(buf, "(TRUE)");
+ 		}
  
  		/* End the FROM clause entry. */
! 		appendStringInfoChar(buf, ')');
  	}
  	else
  	{
***************
*** 1222,1228 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (use_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
--- 1360,1366 ----
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (add_rel_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
***************
*** 1230,1235 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1368,1544 ----
  }
  
  /*
+  * Append operand relation of foreign join to buf.
+  *
+  * If the given relation is an operand of the foreign join performing a full
+  * outer join and has conditions that need to be evaluated below the full
+  * outer join, deparse the relation as a subquery.  Also, if the relation's
+  * reltarget has non-Var expressions, do the same, regardless of the join type
+  * of the foreign join.
+  */
+ 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;
+ 
+ 	Assert(fpinfo->local_conds == NIL);
+ 	Assert(fpinfo->reltarget_is_shippable);
+ 
+ 	if ((jointype == JOIN_FULL && fpinfo->remote_conds) ||
+ 		fpinfo->reltarget_has_non_vars)
+ 	{
+ 		List	   *tlist;
+ 		List	   *retrieved_attrs;
+ 		deparse_expr_cxt context;
+ 
+ 		context.buf = buf;
+ 		context.root = root;
+ 		context.foreignrel = foreignrel;
+ 		context.aliased = aliased;
+ 		context.params_list = params_list;
+ 
+ 		tlist = build_tlist_to_deparse(root, foreignrel);
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectSql(tlist,
+ 						 fpinfo->remote_conds,
+ 						 &retrieved_attrs,
+ 						 &context);
+ 		deparseLockingClause(&context);
+ 		appendStringInfoChar(buf, ')');
+ 		appendSubselectAlias(foreignrel->reltarget->exprs, &context);
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel,
+ 							  true, aliased, params_list);
+ }
+ 
+ /*
+  * Add a subselect alias to a subquery in FROM of the remote query.
+  */
+ static void
+ appendSubselectAlias(List *exprs, deparse_expr_cxt *context)
+ {
+ 	StringInfo	buf = context->buf;
+ 	RelOptInfo *foreignrel = context->foreignrel;
+ 	AliasedExprs *aliased = context->aliased;
+ 	int			tabno = aliased->next_tabno;
+ 	int			colno = 1;
+ 	bool		first;
+ 	ListCell   *lc;
+ 
+ 	/* Append the subselect table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the subselect column aliases */
+ 	first = true;
+ 	appendStringInfoChar(buf, '(');
+ 	foreach(lc, exprs)
+ 	{
+ 		Expr	   *expr = (Expr *) lfirst(lc);
+ 
+ 		if (!first)
+ 			appendStringInfoString(buf, ", ");
+ 		first = false;
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, colno);
+ 
+ 		registerAliasedExpr(expr, tabno, colno, aliased);
+ 
+ 		colno++;
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ 
+ 	/* Update next table number for next subquery */
+ 	aliased->next_tabno++;
+ 
+ 	/* Add foreignrel's relids to aliased->relids */
+ 	aliased->relids = bms_add_members(aliased->relids, foreignrel->relids);
+ }
+ 
+ /*
+  * Save given aliased expression into the AliasedExprs struct.
+  *
+  * Note: when an upper foreign join references the given expression, we use
+  * the alias for the expression saved here when deparsing the expression in
+  * the foreign join in deparseExpr.
+  */
+ static void
+ registerAliasedExpr(Expr *expr, int tabno, int colno, AliasedExprs *aliased)
+ {
+ 	int			num_exprs = list_length(aliased->exprs);
+ 	int			max_exprs = aliased->max_exprs;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/*
+ 	 * If we already have the expression, all we need to do is rewrite the
+ 	 * corresponding table/column numbers in the tabnos/colnos arrays into
+ 	 * the given table/column numbers.
+ 	 */
+ 	i = 0;
+ 	foreach(lc, aliased->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) expr))
+ 		{
+ 			aliased->tabnos[i] = tabno;
+ 			aliased->colnos[i] = colno;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/*
+ 	 * This is a new one; add the given expression to the exprs list and
+ 	 * write the given table/column numbers into the tabnos/colnos arrays.
+ 	 */
+ 	aliased->exprs = lappend(aliased->exprs, expr);
+ 	if (num_exprs + 1 >= max_exprs)
+ 	{
+ 		max_exprs *= 2;
+ 		aliased->tabnos = (int *) repalloc(aliased->tabnos,
+ 										   max_exprs * sizeof(int));
+ 		aliased->colnos = (int *) repalloc(aliased->colnos,
+ 										   max_exprs * sizeof(int));
+ 		aliased->max_exprs = max_exprs;
+ 	}
+ 	aliased->tabnos[num_exprs] = tabno;
+ 	aliased->colnos[num_exprs] = colno;
+ }
+ 
+ /*
+  * Returns true if given expression is aliased by a subquery in FROM.
+  *
+  * If so, the table and column alias numbers of the expression are returned to
+  * *tabno and *colno, respectively.
+  */
+ static bool
+ is_aliased_expr(Expr *node, int *tabno, int *colno, deparse_expr_cxt *context)
+ {
+ 	AliasedExprs *aliased = context->aliased;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	if (!aliased)
+ 		return false;
+ 
+ 	i = 0;
+ 	foreach(lc, aliased->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*tabno = aliased->tabnos[i];
+ 			*colno = aliased->colnos[i];
+ 			return true;
+ 		}
+ 		i++;
+ 	}
+ 	return false;
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 1361,1366 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
--- 1670,1676 ----
  	context.root = root;
  	context.foreignrel = baserel;
  	context.buf = buf;
+ 	context.aliased = NULL;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
***************
*** 1445,1450 **** deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
--- 1755,1761 ----
  	context.root = root;
  	context.foreignrel = baserel;
  	context.buf = buf;
+ 	context.aliased = NULL;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
***************
*** 1608,1616 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.  However, we must be
! 		 * careful; the table could be beneath an outer join, in which case it
! 		 * must go to NULL whenever the rest of the row does.
  		 */
  		Oid			fetchval = 0;
  
--- 1919,1925 ----
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.
  		 */
  		Oid			fetchval = 0;
  
***************
*** 1620,1633 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  			fetchval = rte->relid;
  		}
  
! 		if (qualify_col)
! 		{
! 			appendStringInfoString(buf, "CASE WHEN (");
! 			ADD_REL_QUALIFIER(buf, varno);
! 			appendStringInfo(buf, "*)::text IS NOT NULL THEN %u END", fetchval);
! 		}
! 		else
! 			appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
--- 1929,1935 ----
  			fetchval = rte->relid;
  		}
  
! 		appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
***************
*** 1658,1685 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  		attrs_used = bms_add_member(NULL,
  									0 - FirstLowInvalidHeapAttributeNumber);
  
- 		/*
- 		 * In case the whole-row reference is under an outer join then it has
- 		 * to go NULL whenever the rest of the row goes NULL. Deparsing a join
- 		 * query would always involve multiple relations, thus qualify_col
- 		 * would be true.
- 		 */
- 		if (qualify_col)
- 		{
- 			appendStringInfoString(buf, "CASE WHEN (");
- 			ADD_REL_QUALIFIER(buf, varno);
- 			appendStringInfo(buf, "*)::text IS NOT NULL THEN ");
- 		}
- 
  		appendStringInfoString(buf, "ROW(");
  		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
  						  &retrieved_attrs);
  		appendStringInfoString(buf, ")");
  
- 		/* Complete the CASE WHEN statement started above. */
- 		if (qualify_col)
- 			appendStringInfo(buf, " END");
- 
  		heap_close(rel, NoLock);
  		bms_free(attrs_used);
  	}
--- 1960,1970 ----
***************
*** 1808,1816 **** deparseStringLiteral(StringInfo buf, const char *val)
--- 2093,2113 ----
  static void
  deparseExpr(Expr *node, deparse_expr_cxt *context)
  {
+ 	int			tabno;
+ 	int			colno;
+ 
  	if (node == NULL)
  		return;
  
+ 	/* Use the alias if it's an aliased expression */
+ 	if (is_aliased_expr(node, &tabno, &colno, context))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	switch (nodeTag(node))
  	{
  		case T_Var:
***************
*** 1849,1854 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 2146,2154 ----
  		case T_ArrayExpr:
  			deparseArrayExpr((ArrayExpr *) node, context);
  			break;
+ 		case T_PlaceHolderVar:
+ 			deparsePlaceHolderExpr((PlaceHolderVar *) node, context);
+ 			break;
  		default:
  			elog(ERROR, "unsupported expression type for deparse: %d",
  				 (int) nodeTag(node));
***************
*** 2420,2425 **** deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context)
--- 2720,2734 ----
  }
  
  /*
+  * Deparse a PlaceHolderVar's contained expression.
+  */
+ static void
+ deparsePlaceHolderExpr(PlaceHolderVar *node, deparse_expr_cxt *context)
+ {
+ 	deparseExpr(node->phexpr, context);
+ }
+ 
+ /*
   * Print the representation of a parameter to be sent to the remote side.
   *
   * Note: we always label the Param's type explicitly rather than relying on
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 456,463 **** SELECT t1.c1, t2."C 1" FROM ft2 t1 LEFT JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1"
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                        QUERY PLAN                                                                        
! ---------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
--- 456,463 ----
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
***************
*** 466,472 **** EXPLAIN (VERBOSE, COSTS OFF)
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (((r2."C 1" = r3."C 1")))) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
--- 466,472 ----
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (TRUE)) WHERE ((r2."C 1" = r3."C 1")) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
***************
*** 979,992 **** ANALYZE ft5;
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                         QUERY PLAN                                                                                        
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 979,992 ----
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                              QUERY PLAN                                                                                              
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1007,1014 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                             QUERY PLAN                                                                                             
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
--- 1007,1014 ----
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                                       QUERY PLAN                                                                                                       
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
***************
*** 1017,1023 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) INNER JOIN "S 1"."T 3" r4 ON (((r1."C 1" = r4.c1))))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
--- 1017,1023 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) INNER JOIN "S 1"."T 3" r4 ON (TRUE)) WHERE ((r1."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
***************
*** 1223,1245 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1223,1235 ----
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                       QUERY PLAN                                                                                                                                       
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT ss1.c1, ss2.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) ss1(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) ss2(c1) ON (((ss1.c1 = ss2.c1)))) ORDER BY ss1.c1 ASC NULLS LAST, ss2.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1257,1270 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                            QUERY PLAN                                                                                                                                            
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
--- 1247,1260 ----
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                        QUERY PLAN                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT ss1.c1, ss1.c2, r4.c1 FROM ((SELECT r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))) ss1(c1, c2) FULL JOIN "S 1"."T 3" r4 ON (((ss1.c2 = r4.c1)))) ORDER BY ss1.c1 ASC NULLS LAST, ss1.c2 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 1453,1466 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) LEFT
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                      QUERY PLAN                                                                                      
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ON (((r2."C 1" = r4.c1))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
--- 1443,1456 ----
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) ON (((r2."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 1513,1520 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1503,1510 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                   
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1522,1528 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1512,1518 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1557,1564 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1547,1554 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                         
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1566,1572 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1556,1562 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1602,1609 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                               
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1592,1599 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                   QUERY PLAN                                                                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1611,1617 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1601,1607 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1646,1653 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1636,1643 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                        
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1655,1661 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1645,1651 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1691,1705 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                              QUERY PLAN                                                              
! -------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
--- 1681,1695 ----
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                                    QUERY PLAN                                                                   
! ------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
***************
*** 1725,1738 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1715,1728 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss1.c4, ss2.c1 FROM ((SELECT ctid, ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1", c3 FROM "S 1"."T 1") ss1(c1, c2, c3, c4) INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c3 = ss2.c2)) ORDER BY ss1.c4 ASC NULLS LAST, ss1.c3 ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 1955,1962 **** SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                       QUERY PLAN                                                                       
! -------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
--- 1945,1952 ----
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                             QUERY PLAN                                                                            
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
***************
*** 1966,1972 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 1956,1962 ----
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1987,1994 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                      QUERY PLAN                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
--- 1977,1984 ----
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                           QUERY PLAN                                                                           
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
***************
*** 2004,2014 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
--- 1994,2004 ----
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
***************
*** 2029,2036 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
--- 2019,2026 ----
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                                   QUERY PLAN                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
***************
*** 2043,2049 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
--- 2033,2039 ----
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
***************
*** 2061,2085 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
     1
  (10 rows)
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                         QUERY PLAN                                                         
! ---------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: (13), ft2.c1
!    Join Filter: (13 = ft2.c1)
!    ->  Foreign Scan on public.ft2
!          Output: ft2.c1
!          Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (("C 1" >= 10)) AND (("C 1" <= 15)) ORDER BY "C 1" ASC NULLS LAST
!    ->  Materialize
!          Output: (13)
!          ->  Foreign Scan on public.ft1
!                Output: 13
!                Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 13))
! (11 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
--- 2051,2066 ----
     1
  (10 rows)
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                                                                QUERY PLAN                                                                                               
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: (13), ft2.c1
!    Relations: (public.ft2) LEFT JOIN (public.ft1)
!    Remote SQL: SELECT r2."C 1", ss1.c1 FROM ("S 1"."T 1" r2 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) ss1(c1) ON (((13 = r2."C 1")))) WHERE ((r2."C 1" >= 10)) AND ((r2."C 1" <= 15))
! (4 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
***************
*** 2092,2115 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
      | 15
  (6 rows)
  
! -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                     QUERY PLAN                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Join Filter: (ft4.c1 = ft1.c1)
!    ->  Foreign Scan on public.ft4
!          Output: ft4.c1, ft4.c2, ft4.c3
!          Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 10)) AND ((c1 <= 15))
!    ->  Materialize
!          Output: ft1.c1, ft2.c1, (13)
!          ->  Foreign Scan
!                Output: ft1.c1, ft2.c1, 13
!                Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
! (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
--- 2073,2113 ----
      | 15
  (6 rows)
  
! EXPLAIN (VERBOSE, COSTS OFF)
! SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                                                                                                                                                  QUERY PLAN                                                                                                                                                                                  
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
!    Output: ft2.c1, (13), (13), ft2_1.c1
!    Relations: (public.ft2) LEFT JOIN ((public.ft2) LEFT JOIN (public.ft1))
!    Remote SQL: SELECT r1."C 1", ss2.c1, ss2.c2, ss2.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r5."C 1", 13, ss1.c1 FROM ("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) ss1(c1) ON (((13 = r5."C 1")))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))) ss2(c1, c2, c3) ON (((r1."C 1" = 13)))) WHERE ((r1."C 1" >= 10)) AND ((r1."C 1" <= 15))
! (4 rows)
! 
! SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
!  c1 | a2 | b2 | c2 
! ----+----+----+----
!  10 |    |    |   
!  11 |    |    |   
!  12 |    |    |   
!  13 | 13 |    | 10
!  13 | 13 |    | 11
!  13 | 13 |    | 12
!  13 | 13 | 13 | 13
!  13 | 13 |    | 14
!  13 | 13 |    | 15
!  14 |    |    |   
!  15 |    |    |   
! (11 rows)
! 
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                                                                                   QUERY PLAN                                                                                                                                                  
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Relations: (public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2))
!    Remote SQL: SELECT r1.c1, ss1.c1, ss1.c2, ss1.c3 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4."C 1", r5."C 1", 13 FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12))) ss1(c1, c2, c3) ON (((r1.c1 = ss1.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
! (4 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
***************
*** 2119,2134 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
   14 |    |    |   
  (3 rows)
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                 QUERY PLAN                                                                                                                                 
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2117,2150 ----
   14 |    |    |   
  (3 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.b IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                      
+ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ((ft1.c1 IS NOT NULL)), ft4_1.c1, (13), ft1.c1, ft2.c1
+    Relations: (public.ft4) LEFT JOIN ((public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2)))
+    Remote SQL: SELECT r1.c1, ss2.c1, ss2.c2, ss2.c3, ss2.c4, ss2.c5 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4.c1, ss1.c1, ss1.c2, (ss1.c1 IS NOT NULL), ss1.c3 FROM ("S 1"."T 3" r4 LEFT JOIN (SELECT r7."C 1", r8."C 1", 13 FROM ("S 1"."T 1" r7 INNER JOIN "S 1"."T 1" r8 ON (TRUE)) WHERE ((r8."C 1" = 12)) AND ((r7."C 1" = 12))) ss1(c1, c2, c3) ON (((r4.c1 = ss1.c1)))) WHERE ((r4.c1 >= 10)) AND ((r4.c1 <= 15))) ss2(c1, c2, c3, c4, c5) ON (((r1.c1 = ss2.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
+ (4 rows)
+ 
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.b IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 | d2 | e2 
+ ----+----+----+----+----+----
+  10 | f  | 10 |    |    |   
+  12 | t  | 12 | 13 | 12 | 12
+  14 | f  | 14 |    |    |   
+ (3 rows)
+ 
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                        QUERY PLAN                                                                                                                                       
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss1.c4, r2.c1, r2.c2 FROM ((SELECT ROW(c1, c2, c3), c1, c2, c3 FROM "S 1"."T 4") ss1(c1, c2, c3, c4) INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((ss1.c2 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY ss1.c2 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 2288,2299 **** DROP ROLE regress_view_owner;
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                           QUERY PLAN                                                          
! ------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = 2)) AND ((r1."C 1" = 1))))
  (4 rows)
  
  EXECUTE st1(1, 1);
--- 2304,2315 ----
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                                QUERY PLAN                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r2."C 1" = 2)) AND ((r1."C 1" = 1))
  (4 rows)
  
  EXECUTE st1(1, 1);
***************
*** 2917,2930 **** 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)
--- 2933,2946 ----
  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)
***************
*** 3060,3073 **** 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
!                                                                                                                               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)
--- 3076,3089 ----
  
  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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 27,32 ****
--- 27,33 ----
  #include "optimizer/cost.h"
  #include "optimizer/pathnode.h"
  #include "optimizer/paths.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/planmain.h"
  #include "optimizer/restrictinfo.h"
  #include "optimizer/var.h"
***************
*** 640,645 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 641,704 ----
  	}
  
  	/*
+ 	 * Detect whether the reltarget expressions are all shippable and whether
+ 	 * there are any non-Var expressions in the reltarget.
+ 	 *
+ 	 * Note: if the relation is an appendrel child, it isn't in the main join
+ 	 * tree, so the info is not used.
+ 	 */
+ 	if (baserel->reloptkind == RELOPT_BASEREL)
+ 	{
+ 		bool		reltarget_is_shippable = true;
+ 		bool		reltarget_has_non_vars = false;
+ 		int			i;
+ 
+ 		foreach(lc, baserel->reltarget->exprs)
+ 		{
+ 			Node	   *node = (Node *) lfirst(lc);
+ 
+ 			Assert(IsA(node, Var) || IsA(node, PlaceHolderVar));
+ 
+ 			if (IsA(node, PlaceHolderVar))
+ 			{
+ 				reltarget_has_non_vars = true;
+ 				if (!is_foreign_expr(root, baserel, (Expr *) node))
+ 				{
+ 					reltarget_is_shippable = false;
+ 					break;
+ 				}
+ 			}
+ 		}
+ 
+ 		/*
+ 		 * Treat system columns other than citd and whole-row Vars like PHVs.
+ 		 *
+ 		 * Note: such system columns are deaprsed as 0, except for tableoid,
+ 		 * which is deparsed as a valid value for the local table OID, and
+ 		 * whole-row Vars as ROW() expressions.  Any Vars including these in
+ 		 * the reltarget are considered shippable, though.
+ 		 */
+ 		if (reltarget_is_shippable)
+ 		{
+ 			for (i = FirstLowInvalidHeapAttributeNumber + 1; i <= 0; i++)
+ 			{
+ 				if (i == SelfItemPointerAttributeNumber)
+ 					continue;
+ 
+ 				if (bms_is_member(i - FirstLowInvalidHeapAttributeNumber,
+ 							  fpinfo->attrs_used))
+ 				{
+ 					reltarget_has_non_vars = true;
+ 					break;
+ 				}
+ 			}
+ 		}
+ 
+ 		fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 		fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 	}
+ 
+ 	/*
  	 * Set the name of relation in fpinfo, while we are constructing it here.
  	 * It will be used to build the string describing the join relation in
  	 * EXPLAIN output. We can't know whether VERBOSE option is specified or
***************
*** 1177,1183 **** postgresGetForeignPlan(PlannerInfo *root,
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
--- 1236,1242 ----
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
***************
*** 2506,2512 **** estimate_path_cost_size(PlannerInfo *root,
  
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL)
! 			fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
--- 2565,2571 ----
  
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL)
! 			fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
***************
*** 3947,3952 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4006,4013 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	bool		reltarget_is_shippable = true;
+ 	bool		reltarget_has_non_vars = false;
  
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
***************
*** 3976,3981 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4037,4050 ----
  	if (fpinfo_o->local_conds || fpinfo_i->local_conds)
  		return false;
  
+ 	/*
+ 	 * If the reltargets of joining relations are not shippable, those are
+ 	 *  required to be evaluated locally, so the join can not be pushed down.
+ 	 */
+ 	if (!fpinfo_o->reltarget_is_shippable ||
+ 		!fpinfo_i->reltarget_is_shippable)
+ 		return false;
+ 
  	/* Separate restrict list into join quals and quals on join relation */
  	if (IS_OUTER_JOIN(jointype))
  		extract_actual_join_clauses(extra->restrictlist, &joinclauses, &otherclauses);
***************
*** 4001,4026 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			return false;
  	}
  
- 	/*
- 	 * deparseExplicitTargetList() isn't smart enough to handle anything other
- 	 * than a Var.  In particular, if there's some PlaceHolderVar that would
- 	 * need to be evaluated within this join tree (because there's an upper
- 	 * reference to a quantity that may go to NULL as a result of an outer
- 	 * join), then we can't try to push the join down because we'll fail when
- 	 * we get to deparseExplicitTargetList().  However, a PlaceHolderVar that
- 	 * needs to be evaluated *at the top* of this join tree is OK, because we
- 	 * can do that locally after fetching the results from the remote side.
- 	 */
- 	foreach(lc, root->placeholder_list)
- 	{
- 		PlaceHolderInfo *phinfo = lfirst(lc);
- 		Relids		relids = joinrel->relids;
- 
- 		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
- 			bms_nonempty_difference(relids, phinfo->ph_eval_at))
- 			return false;
- 	}
- 
  	/* Save the join clauses, for later use. */
  	fpinfo->joinclauses = joinclauses;
  
--- 4070,4075 ----
***************
*** 4049,4056 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4098,4104 ----
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4061,4068 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4109,4115 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4070,4098 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4117,4150 ----
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
  		case JOIN_FULL:
! 			/* nothing to do */
  			break;
  
  		default:
***************
*** 4100,4119 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			elog(ERROR, "unsupported join type %d", jointype);
  	}
  
  	/*
! 	 * For an inner join, all restrictions can be treated alike. Treating the
! 	 * pushed down conditions as join conditions allows a top level full outer
! 	 * join to be deparsed without requiring subqueries.
  	 */
! 	if (jointype == JOIN_INNER)
  	{
! 		Assert(!fpinfo->joinclauses);
! 		fpinfo->joinclauses = fpinfo->remote_conds;
! 		fpinfo->remote_conds = NIL;
! 	}
  
! 	/* Mark that this join can be pushed down safely */
! 	fpinfo->pushdown_safe = true;
  
  	/*
  	 * If user is willing to estimate cost for a scan of either of the joining
--- 4152,4190 ----
  			elog(ERROR, "unsupported join type %d", jointype);
  	}
  
+ 	/* Mark that this join can be pushed down safely */
+ 	fpinfo->pushdown_safe = true;
+ 
  	/*
! 	 * Detect whether the reltarget expressions are all shippable and whether
! 	 * there are any non-Var expressions in the reltarget.
  	 */
! 	foreach(lc, joinrel->reltarget->exprs)
  	{
! 		Node	   *node = (Node *) lfirst(lc);
  
! 		Assert(IsA(node, Var) || IsA(node, PlaceHolderVar));
! 
! 		if (IsA(node, PlaceHolderVar))
! 		{
! 			PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 			PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
! 
! 			/* Ignore the PHV if it has bubbled up from an either input. */
! 			if (bms_is_subset(phinfo->ph_eval_at, outerrel->relids) &&
! 				bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
! 				continue;
! 
! 			reltarget_has_non_vars = true;
! 			if (!is_foreign_expr(root, joinrel, (Expr *) node))
! 			{
! 				reltarget_is_shippable = false;
! 				break;
! 			}
! 		}
! 	}
! 	fpinfo->reltarget_is_shippable = reltarget_is_shippable;
! 	fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
  
  	/*
  	 * If user is willing to estimate cost for a scan of either of the joining
***************
*** 4526,4531 **** conversion_error_callback(void *arg)
--- 4597,4603 ----
  	const char *attname = NULL;
  	const char *relname = NULL;
  	bool		is_wholerow = false;
+ 	bool		is_placeholder = false;
  	ConversionLocation *errpos = (ConversionLocation *) arg;
  
  	if (errpos->rel)
***************
*** 4550,4580 **** conversion_error_callback(void *arg)
  		EState	   *estate = fsstate->ss.ps.state;
  		TargetEntry *tle;
  		Var		   *var;
- 		RangeTblEntry *rte;
  
  		Assert(IsA(fsplan, ForeignScan));
  		tle = (TargetEntry *) list_nth(fsplan->fdw_scan_tlist,
  									   errpos->cur_attno - 1);
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
- 		Assert(IsA(var, Var));
- 
- 		rte = rt_fetch(var->varno, estate->es_range_table);
  
! 		if (var->varattno == 0)
! 			is_wholerow = true;
  		else
! 			attname = get_relid_attribute_name(rte->relid, var->varattno);
  
! 		relname = get_rel_name(rte->relid);
  	}
  
! 	if (relname)
  	{
  		if (is_wholerow)
! 			errcontext("whole-row reference to foreign table \"%s\"", relname);
  		else if (attname)
! 			errcontext("column \"%s\" of foreign table \"%s\"", attname, relname);
  	}
  }
  
--- 4622,4663 ----
  		EState	   *estate = fsstate->ss.ps.state;
  		TargetEntry *tle;
  		Var		   *var;
  
  		Assert(IsA(fsplan, ForeignScan));
  		tle = (TargetEntry *) list_nth(fsplan->fdw_scan_tlist,
  									   errpos->cur_attno - 1);
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		if (!IsA(var, Var))
! 		{
! 			Assert(IsA(var, PlaceHolderVar));
! 			is_placeholder = true;
! 		}
  		else
! 		{
! 			RangeTblEntry *rte  = rt_fetch(var->varno, estate->es_range_table);
! 
! 			if (var->varattno == 0)
! 				is_wholerow = true;
! 			else
! 				attname = get_relid_attribute_name(rte->relid, var->varattno);
  
! 			relname = get_rel_name(rte->relid);
! 		}
  	}
  
! 	if (is_placeholder)
! 		errcontext("expression at position %d in select list",
! 				   errpos->cur_attno);
! 	else if (relname)
  	{
  		if (is_wholerow)
! 			errcontext("whole-row reference to foreign table \"%s\"",
! 					   relname);
  		else if (attname)
! 			errcontext("column \"%s\" of foreign table \"%s\"",
! 					   attname, relname);
  	}
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 92,97 **** typedef struct PgFdwRelationInfo
--- 92,106 ----
  	RelOptInfo *innerrel;
  	JoinType	jointype;
  	List	   *joinclauses;
+ 
+ 	/*
+ 	 * Flags on Path output tlist for the relation.  These are used to detect
+ 	 * whether the relation can be joined with any other foreign table (or
+ 	 * join) on the remote server, and detect whether the relation has to be
+ 	 * deparsed as a subquery in that case.
+ 	 */
+ 	bool		reltarget_is_shippable; /* are reltarget exprs shippable? */
+ 	bool		reltarget_has_non_vars;	/* are there any non-Var exprs? */
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
***************
*** 155,161 **** extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(RelOptInfo *foreign_rel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
--- 164,170 ----
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 493,509 **** EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
! 
! -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
--- 493,511 ----
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
! EXPLAIN (VERBOSE, COSTS OFF)
! SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
! SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.b IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.b IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
#4Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#3)
Re: Push down more full joins in postgres_fdw

On Fri, Sep 2, 2016 at 3:55 PM, Etsuro Fujita <fujita.etsuro@lab.ntt.co.jp>
wrote:

Hi Ashutosh,

On 2016/08/22 15:49, Ashutosh Bapat wrote:

1. deparsePlaceHolderVar looks odd - each of the deparse* function is
named as deparse + <name of the parser node the string would parse
into>. PlaceHolderVar is not a parser node, so no string is going to be
parsed as a PlaceHolderVar. May be you want to name it as
deparseExerFromPlaceholderVar or something like that.

The name "deparseExerFromPlaceholderVar" seems long to me. How about
"deparsePlaceHolderExpr"?

There isn't any node with name PlaceHolderExpr.

2. You will need to check phlevelsup member while assessing whether a

PHV is safe to push down.

Good catch! In addition to that, I added another check that the eval_at
set for the PHV should be included in the relids set for the foreign
relation. I think that would make the shippability check more robust.

Thanks.

3. I think registerAlias stuff should happen really at the time of

creating paths and should be stored in fpinfo. Without that it will be
computed every time we deparse the query. This means every time we try
to EXPLAIN the query at various levels in the join tree and once for the
query to be sent to the foreign server.

Hmm. I think the overhead in calculating aliases would be negligible
compared to the overhead in explaining each remote query for costing or
sending the remote query for execution. So, I created aliases in the same
way as remote params created and stored into params_list in
deparse_expr_cxt. I'm not sure it's worth complicating the code.

We defer remote parameter creation till deparse time since the the
parameter numbers are dependent upon the sequence in which we deparse the
query. Creating them at the time of path creation and storing them in
fpinfo is not possible because a. those present in the joining relations
will conflict with each other and need some kind of serialization at the
time of deparsing b. those defer for differently parameterized paths or
paths with different pathkeys. We don't defer it because it's very light on
performance.

That's not true with the alias information. As long as we detect which
relations need subqueries, their RTIs are enough to create unique aliases
e.g. if a relation involves RTIs 1, 2, 3 corresponding subquery can have
alias r123 without conflicting with any other alias.

However minimum overhead it might have, we will deparse the query every
time we create a path involving that kind of relation i.e. for different
pathkeys, different parameterization and different joins in which that
relation participates in. This number can be significant. We want to avoid
this overhead if we can.

5. The blocks related to inner and outer relations in

deparseFromExprForRel() look same. We should probably separate that code
out into a function and call it at two places.

Done.

Thanks. I see you have created function deparseOperandRelation() for the
same. I guess, this should be renamed as deparseRangeTblRef() since it will
create RangeTblRef node when parsed back.

6.

! if (is_placeholder)
! errcontext("placeholder expression at position %d in select
list",
! errpos->cur_attno);
A user wouldn't know what a placeholder expression is, there is no such
term in the documentation. We have to device a better way to provide an
error context for an expression in general.

Though I proposed that, I don't think that it's that important to let
users know that the expression is from a PHV. How about just saying
"expression", not "placeholder expression", so that we have the message
"expression at position %d in select list" in the context?

Hmm, that's better than placeholder expression, but not as explanatory as

it should be since we won't be printing the "select list" in the error
context and user won't have a clue as to what error context is about. But I
don't have any good suggestions right now. May be we should print the whole
expression? But that can be very long, I don't know.

This patch tries to do two things at a time 1. support join pushdown for
FULL join when the joining relations have remote conditions 2. better
support for fetching placeholder vars, whole row references and some system
columns. To make reviews easy, I think we should split the patch into two
1. supporting subqueries to be deparsed with support for one of the above
(I will suggest FULL join support) 2. support for the other.

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

#5Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#4)
Re: Push down more full joins in postgres_fdw

On 2016/09/06 22:07, Ashutosh Bapat wrote:

On Fri, Sep 2, 2016 at 3:55 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp <mailto:fujita.etsuro@lab.ntt.co.jp>> wrote:

On 2016/08/22 15:49, Ashutosh Bapat wrote:
1. deparsePlaceHolderVar looks odd - each of the deparse*
function is
named as deparse + <name of the parser node the string would parse
into>. PlaceHolderVar is not a parser node, so no string is
going to be
parsed as a PlaceHolderVar. May be you want to name it as
deparseExerFromPlaceholderVar or something like that.

The name "deparseExerFromPlaceholderVar" seems long to me. How
about "deparsePlaceHolderExpr"?

There isn't any node with name PlaceHolderExpr.

I'll rename it to "deparseExerInPlaceholderVar", then.

3. I think registerAlias stuff should happen really at the time of
creating paths and should be stored in fpinfo. Without that it
will be
computed every time we deparse the query. This means every time
we try
to EXPLAIN the query at various levels in the join tree and once
for the
query to be sent to the foreign server.

Hmm. I think the overhead in calculating aliases would be
negligible compared to the overhead in explaining each remote query
for costing or sending the remote query for execution. So, I
created aliases in the same way as remote params created and stored
into params_list in deparse_expr_cxt. I'm not sure it's worth
complicating the code.

We defer remote parameter creation till deparse time since the the
parameter numbers are dependent upon the sequence in which we deparse
the query. Creating them at the time of path creation and storing them
in fpinfo is not possible because a. those present in the joining
relations will conflict with each other and need some kind of
serialization at the time of deparsing b. those defer for differently
parameterized paths or paths with different pathkeys. We don't defer it
because it's very light on performance.

That's not true with the alias information. As long as we detect which
relations need subqueries, their RTIs are enough to create unique
aliases e.g. if a relation involves RTIs 1, 2, 3 corresponding subquery
can have alias r123 without conflicting with any other alias.

Hmm. But another concern about the approach of generating an subselect
alias for a path, if needed, at the path creation time would be that the
path might be rejected by add_path, which would result in totally
wasting cycles for generating the subselect alias for the path.

However minimum overhead it might have, we will deparse the query every
time we create a path involving that kind of relation i.e. for different
pathkeys, different parameterization and different joins in which that
relation participates in. This number can be significant. We want to
avoid this overhead if we can.

Exactly. As I said above, I think the overhead would be negligible
compared to the overhead in explaining each remote query for costing or
the overhead in sending the final remote query for execution, though.

5. The blocks related to inner and outer relations in
deparseFromExprForRel() look same. We should probably separate
that code
out into a function and call it at two places.

Done.

I see you have created function deparseOperandRelation() for the
same. I guess, this should be renamed as deparseRangeTblRef() since it
will create RangeTblRef node when parsed back.

OK, if there no opinions of others, I'll rename it to the name proposed
by you, "deparseRangeTblRef".

6.
! if (is_placeholder)
! errcontext("placeholder expression at position %d in
select list",
! errpos->cur_attno);
A user wouldn't know what a placeholder expression is, there is
no such
term in the documentation. We have to device a better way to
provide an
error context for an expression in general.

Though I proposed that, I don't think that it's that important to
let users know that the expression is from a PHV. How about just
saying "expression", not "placeholder expression", so that we have
the message "expression at position %d in select list" in the context?

Hmm, that's better than placeholder expression, but not as explanatory
as it should be since we won't be printing the "select list" in the
error context and user won't have a clue as to what error context is
about.

I don't think so. Consider an example of the conversion error message,
which is from the regression test:

SELECT ft1.c1, ft2.c2, ft1 FROM ft1, ft2 WHERE ft1.c1 = ft2.c1 AND
ft1.c1 = 1;
ERROR: invalid input syntax for integer: "foo"
CONTEXT: whole-row reference to foreign table "ft1"

As shown in the example, the error message is displayed under a remote
query for execution. So, ISTM it's reasonable to print something like
"expression at position %d in select list" in the context if an
expression in a PHV.

But I don't have any good suggestions right now. May be we should
print the whole expression? But that can be very long, I don't know.

ISTM that it's a bit too expensive to print the whole expression in the
error context.

This patch tries to do two things at a time 1. support join pushdown for
FULL join when the joining relations have remote conditions 2. better
support for fetching placeholder vars, whole row references and some
system columns. To make reviews easy, I think we should split the patch
into two 1. supporting subqueries to be deparsed with support for one of
the above (I will suggest FULL join support) 2. support for the other.

OK, will try.

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

#6Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#5)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

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

On 2016/09/06 22:07, Ashutosh Bapat wrote:

This patch tries to do two things at a time 1. support join pushdown for
FULL join when the joining relations have remote conditions 2. better
support for fetching placeholder vars, whole row references and some
system columns. To make reviews easy, I think we should split the patch
into two 1. supporting subqueries to be deparsed with support for one of
the above (I will suggest FULL join support) 2. support for the other.

OK, will try.

I extracted #1 from the patch. Attached is a patch for that. If that
patch is reasonable, I'll create a patch for #2 on top of it.

Comments welcome!

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-more-full-join-pushdown-v3.patchbinary/octet-stream; name=postgres-fdw-more-full-join-pushdown-v3.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 88,93 **** typedef struct foreign_loc_cxt
--- 88,106 ----
  } foreign_loc_cxt;
  
  /*
+  * Aliased expressions delivered by subqueries in FROM of the remote query.
+  */
+ typedef struct AliasedExprs
+ {
+ 	List	   *exprs;			/* output columns of subqueries in FROM */
+ 	int			max_exprs;		/* maximum number of columns stored */
+ 	int		   *tabnos;			/* subselect table alias numbers of columns */
+ 	int		   *colnos;			/* subselect column alias numbers of columns */
+ 	int			next_tabno;		/* table alias number of next subquery */
+ 	Relids		relids;			/* all relids of subqueries in FROM */
+ } AliasedExprs;
+ 
+ /*
   * Context for deparseExpr
   */
  typedef struct deparse_expr_cxt
***************
*** 95,104 **** typedef struct deparse_expr_cxt
--- 108,120 ----
  	PlannerInfo *root;			/* global planner state */
  	RelOptInfo *foreignrel;		/* the foreign relation we are planning for */
  	StringInfo	buf;			/* output buffer to append to */
+ 	AliasedExprs *aliased;		/* aliased exprs of subqueries in FROM */
  	List	  **params_list;	/* exprs that will become remote Params */
  } deparse_expr_cxt;
  
  #define REL_ALIAS_PREFIX	"r"
+ #define SS_TAB_ALIAS_PREFIX	"ss"
+ #define SS_COL_ALIAS_PREFIX	"c"
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
***************
*** 152,164 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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);
  
  
  /*
--- 168,190 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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 deparseRangeTblRef(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);
  
  
  /*
***************
*** 767,772 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
--- 793,799 ----
  						List *tlist, List *remote_conds, List *pathkeys,
  						List **retrieved_attrs, List **params_list)
  {
+ 	AliasedExprs aliased;
  	deparse_expr_cxt context;
  
  	/* We handle relations for foreign tables and joins between those */
***************
*** 774,796 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  		   rel->reloptkind == RELOPT_BASEREL ||
  		   rel->reloptkind == RELOPT_OTHER_MEMBER_REL);
  
  	/* Fill portions of context common to join and base relation */
  	context.buf = buf;
  	context.root = root;
  	context.foreignrel = rel;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause and FROM clause */
! 	deparseSelectSql(tlist, retrieved_attrs, &context);
! 
! 	/*
! 	 * Construct WHERE clause
! 	 */
! 	if (remote_conds)
! 	{
! 		appendStringInfo(buf, " WHERE ");
! 		appendConditions(remote_conds, &context);
! 	}
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
--- 801,823 ----
  		   rel->reloptkind == RELOPT_BASEREL ||
  		   rel->reloptkind == RELOPT_OTHER_MEMBER_REL);
  
+ 	/* 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;
+ 
  	/* Fill portions of context common to join and base relation */
  	context.buf = buf;
  	context.root = root;
  	context.foreignrel = rel;
+ 	context.aliased = &aliased;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause, FROM clause, and WHERE clause */
! 	deparseSelectSql(tlist, remote_conds, retrieved_attrs, &context);
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
***************
*** 803,809 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ....".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
--- 830,836 ----
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ... WHERE ...".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
***************
*** 812,832 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
  	PlannerInfo *root = context->root;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
  	 * Construct SELECT list
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
--- 839,877 ----
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
! 				 deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
  	PlannerInfo *root = context->root;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	StringInfoData jointree;
+ 
+ 	/*
+ 	 * Deparse a join tree expression in FROM clause first.
+ 	 */
+ 	initStringInfo(&jointree);
+ 	deparseFromExprForRel(&jointree, root, foreignrel,
+ 						  (foreignrel->reloptkind == RELOPT_JOINREL),
+ 						  context->aliased, context->params_list);
  
  	/*
  	 * Construct SELECT list
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	/*
! 	 * Note: tlist for a base relation may be non-NIL.  For example, if the
! 	 * base relation is an operand of a foreign join performing a full outer
! 	 * join and has conditions that need to be evaluated below the full outer
! 	 * join, then we deparse the base relation as a subquery, so in that case
! 	 * tlist for the base relation would be non-NIL.
! 	 */
! 	if (tlist != NIL)
  	{
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
***************
*** 843,848 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
--- 888,895 ----
  		 */
  		Relation	rel = heap_open(rte->relid, NoLock);
  
+ 		Assert(foreignrel->reloptkind != RELOPT_JOINREL);
+ 
  		deparseTargetList(buf, root, foreignrel->relid, rel, false,
  						  fpinfo->attrs_used, false, retrieved_attrs);
  		heap_close(rel, NoLock);
***************
*** 851,860 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	/*
  	 * Construct FROM clause
  	 */
! 	appendStringInfoString(buf, " FROM ");
! 	deparseFromExprForRel(buf, root, foreignrel,
! 						  (foreignrel->reloptkind == RELOPT_JOINREL),
! 						  context->params_list);
  }
  
  /*
--- 898,913 ----
  	/*
  	 * Construct FROM clause
  	 */
! 	appendStringInfo(buf, " FROM %s", jointree.data);
! 
! 	/*
! 	 * Construct WHERE clause
! 	 */
! 	if (remote_conds)
! 	{
! 		appendStringInfoString(buf, " WHERE ");
! 		appendConditions(remote_conds, context);
! 	}
  }
  
  /*
***************
*** 965,974 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1018,1032 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->foreignrel;
+ 	AliasedExprs *aliased = context->aliased;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
+ 		/* Ignore if it's already deparsed in a lower subquery. */
+ 		if (bms_is_member(relid, aliased->relids))
+ 			continue;
+ 
  		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
***************
*** 1138,1150 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseVar(var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
--- 1196,1209 ----
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseExpr((Expr *) var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
+ 	/* Don't generate bad syntax if no columns */
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
***************
*** 1152,1191 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  /*
   * Construct FROM clause for given relation
   *
!  * 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)
  	{
! 		RelOptInfo *rel_o = fpinfo->outerrel;
! 		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
! 		 *
! 		 * ((outer relation) <join type> (inner relation) ON (joinclauses))
! 		 */
! 		appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
! 					   get_jointype_name(fpinfo->jointype), join_sql_i.data);
  
! 		/* Append join clause; (TRUE) if no join clause */
  		if (fpinfo->joinclauses)
  		{
  			deparse_expr_cxt context;
--- 1211,1246 ----
  /*
   * Construct FROM clause for given relation
   *
!  * For a join relation the clause of the following form is appended to buf:
!  * ((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 */
! 		deparseRangeTblRef(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 */
! 		deparseRangeTblRef(buf, root, fpinfo->innerrel, fpinfo->jointype,
! 						   true, aliased, params_list);
  
! 		/* Append join conditions */
! 		appendStringInfoString(buf, " ON ");
  		if (fpinfo->joinclauses)
  		{
  			deparse_expr_cxt context;
***************
*** 1193,1209 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  			context.buf = buf;
  			context.foreignrel = foreignrel;
  			context.root = root;
  			context.params_list = params_list;
  
! 			appendStringInfo(buf, "(");
  			appendConditions(fpinfo->joinclauses, &context);
! 			appendStringInfo(buf, ")");
  		}
  		else
  			appendStringInfoString(buf, "(TRUE)");
  
! 		/* End the FROM clause entry. */
! 		appendStringInfo(buf, ")");
  	}
  	else
  	{
--- 1248,1268 ----
  			context.buf = buf;
  			context.foreignrel = foreignrel;
  			context.root = root;
+ 			context.aliased = aliased;
  			context.params_list = params_list;
  
! 			appendStringInfoChar(buf, '(');
  			appendConditions(fpinfo->joinclauses, &context);
! 			appendStringInfoChar(buf, ')');
  		}
  		else
+ 		{
+ 			/* No join conditions; add "(TRUE)" */
  			appendStringInfoString(buf, "(TRUE)");
+ 		}
  
! 		/* End the FROM clause entry */
! 		appendStringInfoChar(buf, ')');
  	}
  	else
  	{
***************
*** 1222,1228 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (use_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
--- 1281,1287 ----
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (add_rel_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
***************
*** 1230,1235 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1289,1460 ----
  }
  
  /*
+  * Append operand relation of foreign join to buf.
+  *
+  * If the given relation is an operand of a foreign join performing a full
+  * outer join and has conditions that need to be evaluated below the full
+  * outer join, deparse the relation as a subquery.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
+ 				   RelOptInfo *foreignrel, JoinType jointype,
+ 				   bool add_rel_alias, AliasedExprs *aliased,
+ 				   List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	if (jointype == JOIN_FULL && fpinfo->remote_conds)
+ 	{
+ 		List	   *tlist;
+ 		List	   *retrieved_attrs;
+ 		deparse_expr_cxt context;
+ 
+ 		context.buf = buf;
+ 		context.root = root;
+ 		context.foreignrel = foreignrel;
+ 		context.aliased = aliased;
+ 		context.params_list = params_list;
+ 
+ 		tlist = build_tlist_to_deparse(foreignrel);
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectSql(tlist,
+ 						 fpinfo->remote_conds,
+ 						 &retrieved_attrs,
+ 						 &context);
+ 		deparseLockingClause(&context);
+ 		appendStringInfoChar(buf, ')');
+ 		appendSubselectAlias(foreignrel->reltarget->exprs, &context);
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel,
+ 							  true, aliased, params_list);
+ }
+ 
+ /*
+  * Add a subselect alias to a subquery in FROM of the remote query.
+  */
+ static void
+ appendSubselectAlias(List *exprs, deparse_expr_cxt *context)
+ {
+ 	StringInfo	buf = context->buf;
+ 	RelOptInfo *foreignrel = context->foreignrel;
+ 	AliasedExprs *aliased = context->aliased;
+ 	int			tabno = aliased->next_tabno;
+ 	int			colno = 1;
+ 	bool		first;
+ 	ListCell   *lc;
+ 
+ 	/* Append the subselect table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the subselect column aliases */
+ 	first = true;
+ 	appendStringInfoChar(buf, '(');
+ 	foreach(lc, exprs)
+ 	{
+ 		Expr	   *expr = (Expr *) lfirst(lc);
+ 
+ 		if (!first)
+ 			appendStringInfoString(buf, ", ");
+ 		first = false;
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, colno);
+ 
+ 		registerAliasedExpr(expr, tabno, colno, aliased);
+ 
+ 		colno++;
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ 
+ 	/* Update next table number for next subquery */
+ 	aliased->next_tabno++;
+ 
+ 	/* Add foreignrel's relids to aliased->relids */
+ 	aliased->relids = bms_add_members(aliased->relids, foreignrel->relids);
+ }
+ 
+ /*
+  * Save given aliased expression into the AliasedExprs struct.
+  *
+  * Note: we use the alias saved here for the expression when deparsing the
+  * expression referenced in a higher-level foreign join in deparseExpr.
+  */
+ static void
+ registerAliasedExpr(Expr *expr, int tabno, int colno, AliasedExprs *aliased)
+ {
+ 	int			num_exprs = list_length(aliased->exprs);
+ 	int			max_exprs = aliased->max_exprs;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/*
+ 	 * If we already have the expression, all we need to do is rewrite the
+ 	 * corresponding table/column numbers in the tabnos/colnos arrays into
+ 	 * the given table/column numbers.
+ 	 */
+ 	i = 0;
+ 	foreach(lc, aliased->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) expr))
+ 		{
+ 			aliased->tabnos[i] = tabno;
+ 			aliased->colnos[i] = colno;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/*
+ 	 * This is a new one; add the given expression to the exprs list and
+ 	 * write the given table/column numbers into the tabnos/colnos arrays.
+ 	 */
+ 	aliased->exprs = lappend(aliased->exprs, expr);
+ 	if (num_exprs + 1 >= max_exprs)
+ 	{
+ 		max_exprs *= 2;
+ 		aliased->tabnos = (int *) repalloc(aliased->tabnos,
+ 										   max_exprs * sizeof(int));
+ 		aliased->colnos = (int *) repalloc(aliased->colnos,
+ 										   max_exprs * sizeof(int));
+ 		aliased->max_exprs = max_exprs;
+ 	}
+ 	aliased->tabnos[num_exprs] = tabno;
+ 	aliased->colnos[num_exprs] = colno;
+ }
+ 
+ /*
+  * Returns true if given expression is aliased by a subquery in FROM.
+  *
+  * If so, the table and column alias numbers of the expression are returned to
+  * *tabno and *colno, respectively.
+  */
+ static bool
+ is_aliased_expr(Expr *node, int *tabno, int *colno, deparse_expr_cxt *context)
+ {
+ 	AliasedExprs *aliased = context->aliased;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	if (!aliased)
+ 		return false;
+ 
+ 	i = 0;
+ 	foreach(lc, aliased->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*tabno = aliased->tabnos[i];
+ 			*colno = aliased->colnos[i];
+ 			return true;
+ 		}
+ 		i++;
+ 	}
+ 	return false;
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 1361,1366 **** deparseDirectUpdateSql(StringInfo buf, PlannerInfo *root,
--- 1586,1592 ----
  	context.root = root;
  	context.foreignrel = baserel;
  	context.buf = buf;
+ 	context.aliased = NULL;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "UPDATE ");
***************
*** 1445,1450 **** deparseDirectDeleteSql(StringInfo buf, PlannerInfo *root,
--- 1671,1677 ----
  	context.root = root;
  	context.foreignrel = baserel;
  	context.buf = buf;
+ 	context.aliased = NULL;
  	context.params_list = params_list;
  
  	appendStringInfoString(buf, "DELETE FROM ");
***************
*** 1808,1816 **** deparseStringLiteral(StringInfo buf, const char *val)
--- 2035,2055 ----
  static void
  deparseExpr(Expr *node, deparse_expr_cxt *context)
  {
+ 	int			tabno;
+ 	int			colno;
+ 
  	if (node == NULL)
  		return;
  
+ 	/* Use the alias if it's an aliased expression */
+ 	if (is_aliased_expr(node, &tabno, &colno, context))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	switch (nodeTag(node))
  	{
  		case T_Var:
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 456,463 **** SELECT t1.c1, t2."C 1" FROM ft2 t1 LEFT JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1"
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                        QUERY PLAN                                                                        
! ---------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
--- 456,463 ----
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
***************
*** 466,472 **** EXPLAIN (VERBOSE, COSTS OFF)
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (((r2."C 1" = r3."C 1")))) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
--- 466,472 ----
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (TRUE)) WHERE ((r2."C 1" = r3."C 1")) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
***************
*** 979,992 **** ANALYZE ft5;
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                         QUERY PLAN                                                                                        
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 979,992 ----
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                              QUERY PLAN                                                                                              
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1007,1014 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                             QUERY PLAN                                                                                             
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
--- 1007,1014 ----
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                                       QUERY PLAN                                                                                                       
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
***************
*** 1017,1023 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) INNER JOIN "S 1"."T 3" r4 ON (((r1."C 1" = r4.c1))))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
--- 1017,1023 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) INNER JOIN "S 1"."T 3" r4 ON (TRUE)) WHERE ((r1."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
***************
*** 1223,1245 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1223,1235 ----
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                       QUERY PLAN                                                                                                                                       
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT ss1.c1, ss2.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) ss1(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) ss2(c1) ON (((ss1.c1 = ss2.c1)))) ORDER BY ss1.c1 ASC NULLS LAST, ss2.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1257,1270 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                            QUERY PLAN                                                                                                                                            
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
--- 1247,1260 ----
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                        QUERY PLAN                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT ss1.c1, ss1.c2, r4.c1 FROM ((SELECT r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))) ss1(c1, c2) FULL JOIN "S 1"."T 3" r4 ON (((ss1.c2 = r4.c1)))) ORDER BY ss1.c1 ASC NULLS LAST, ss1.c2 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 1453,1466 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) LEFT
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                      QUERY PLAN                                                                                      
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ON (((r2."C 1" = r4.c1))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
--- 1443,1456 ----
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) ON (((r2."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 1513,1520 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1503,1510 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1522,1528 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1512,1518 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1557,1564 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1547,1554 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1566,1572 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1556,1562 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1602,1609 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                               
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1592,1599 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                     
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1611,1617 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1601,1607 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1646,1653 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1636,1643 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                             QUERY PLAN                                                                                                                                                                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1655,1661 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1645,1651 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1691,1705 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                              QUERY PLAN                                                              
! -------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
--- 1681,1695 ----
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                                    QUERY PLAN                                                                   
! ------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
***************
*** 1725,1738 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1715,1728 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                          QUERY PLAN                                                                                                                                                                                                         
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 1955,1962 **** SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                       QUERY PLAN                                                                       
! -------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
--- 1945,1952 ----
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                             QUERY PLAN                                                                            
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
***************
*** 1966,1972 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 1956,1962 ----
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1987,1994 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                      QUERY PLAN                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
--- 1977,1984 ----
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                           QUERY PLAN                                                                           
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
***************
*** 2004,2014 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
--- 1994,2004 ----
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
***************
*** 2029,2036 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
--- 2019,2026 ----
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                                   QUERY PLAN                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
***************
*** 2043,2049 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
--- 2033,2039 ----
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
***************
*** 2095,2102 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
  -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                     QUERY PLAN                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
     Join Filter: (ft4.c1 = ft1.c1)
--- 2085,2092 ----
  -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
     Join Filter: (ft4.c1 = ft1.c1)
***************
*** 2108,2114 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
           ->  Foreign Scan
                 Output: ft1.c1, ft2.c1, 13
                 Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
  (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
--- 2098,2104 ----
           ->  Foreign Scan
                 Output: ft1.c1, ft2.c1, 13
                 Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12)) ORDER BY r4."C 1" ASC NULLS LAST
  (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
***************
*** 2123,2134 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                 QUERY PLAN                                                                                                                                 
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2113,2124 ----
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                       QUERY PLAN                                                                                                                                      
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 2288,2299 **** DROP ROLE regress_view_owner;
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                           QUERY PLAN                                                          
! ------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = 2)) AND ((r1."C 1" = 1))))
  (4 rows)
  
  EXECUTE st1(1, 1);
--- 2278,2289 ----
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                                QUERY PLAN                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r2."C 1" = 2)) AND ((r1."C 1" = 1))
  (4 rows)
  
  EXECUTE st1(1, 1);
***************
*** 2917,2930 **** 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)
--- 2907,2920 ----
  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 (TRUE)) WHERE ((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)
***************
*** 3060,3073 **** 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
!                                                                                                                               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)
--- 3050,3063 ----
  
  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 (TRUE)) WHERE ((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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 403,408 **** static bool foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel,
--- 403,409 ----
  static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
  								 RelOptInfo *rel);
  static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+ static bool reltarget_has_non_vars(RelOptInfo *foreignrel);
  static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path);
  
***************
*** 4049,4056 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4050,4056 ----
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4061,4068 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4061,4067 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4091,4097 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
  				return false;
  			break;
  
--- 4090,4098 ----
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_o->remote_conds && reltarget_has_non_vars(outerrel))
! 				return false;
! 			if (fpinfo_i->remote_conds && reltarget_has_non_vars(innerrel))
  				return false;
  			break;
  
***************
*** 4100,4117 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			elog(ERROR, "unsupported join type %d", jointype);
  	}
  
- 	/*
- 	 * For an inner join, all restrictions can be treated alike. Treating the
- 	 * pushed down conditions as join conditions allows a top level full outer
- 	 * join to be deparsed without requiring subqueries.
- 	 */
- 	if (jointype == JOIN_INNER)
- 	{
- 		Assert(!fpinfo->joinclauses);
- 		fpinfo->joinclauses = fpinfo->remote_conds;
- 		fpinfo->remote_conds = NIL;
- 	}
- 
  	/* Mark that this join can be pushed down safely */
  	fpinfo->pushdown_safe = true;
  
--- 4101,4106 ----
***************
*** 4176,4181 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4165,4196 ----
  	return true;
  }
  
+ /*
+  * Detect whether there are whole-row Vars or system columns other than ctid
+  * and oid in the given relation's reltarget.
+  *
+  * Note: currently, deparseExplicitTargetList can't properly handle anything
+  * other than a Var that is a ctid, oid, or user column, when deparsing the
+  * SELECT list of a subquery for a full join on relations with restrictions.
+  */
+ static bool
+ reltarget_has_non_vars(RelOptInfo *foreignrel)
+ {
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		Assert(IsA(var, Var));
+ 		if (var->varattno <= 0 &&
+ 			var->varattno != SelfItemPointerAttributeNumber &&
+ 			var->varattno != ObjectIdAttributeNumber)
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
  static void
  add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path)
#7Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#6)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/09/09 21:35, Etsuro Fujita wrote:

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

On 2016/09/06 22:07, Ashutosh Bapat wrote:

This patch tries to do two things at a time 1. support join pushdown for
FULL join when the joining relations have remote conditions 2. better
support for fetching placeholder vars, whole row references and some
system columns. To make reviews easy, I think we should split the patch
into two 1. supporting subqueries to be deparsed with support for one of
the above (I will suggest FULL join support) 2. support for the other.

OK, will try.

I extracted #1 from the patch. Attached is a patch for that. If that
patch is reasonable, I'll create a patch for #2 on top of it.

Attached is a patch for #2. In that patch I fixed some bugs and added a
bit more comments. For testing, please apply the patch for #1 first.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-phv-pushdown-v1.patchbinary/octet-stream; name=postgres-fdw-phv-pushdown-v1.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 48,53 ****
--- 48,54 ----
  #include "nodes/nodeFuncs.h"
  #include "nodes/plannodes.h"
  #include "optimizer/clauses.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/prep.h"
  #include "optimizer/tlist.h"
  #include "optimizer/var.h"
***************
*** 164,169 **** static void deparseRelabelType(RelabelType *node, deparse_expr_cxt *context);
--- 165,171 ----
  static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context);
  static void deparseNullTest(NullTest *node, deparse_expr_cxt *context);
  static void deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context);
+ static void deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context);
  static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
***************
*** 657,662 **** foreign_expr_walker(Node *node,
--- 659,683 ----
  				check_type = false;
  			}
  			break;
+ 		case T_PlaceHolderVar:
+ 			{
+ 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 				PlaceHolderInfo *phinfo = find_placeholder_info(glob_cxt->root,
+ 																phv, false);
+ 
+ 				/*
+ 				 * If the PHV's contained expression is computable on the
+ 				 * remote server, we consider the PHV safe to send.
+ 				 */
+ 				if (phv->phlevelsup == 0 &&
+ 					bms_is_subset(phinfo->ph_eval_at,
+ 								  glob_cxt->foreignrel->relids))
+ 					return foreign_expr_walker((Node *) phv->phexpr,
+ 											   glob_cxt, outer_cxt);
+ 				else
+ 					return false;
+ 			}
+ 			break;
  		default:
  
  			/*
***************
*** 749,769 **** deparse_type_name(Oid type_oid, int32 typemod)
   * foreign server for the given relation.
   */
  List *
! build_tlist_to_deparse(RelOptInfo *foreignrel)
  {
! 	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
! 	 * We require columns specified in foreignrel->reltarget->exprs and those
! 	 * required for evaluating the local conditions.
  	 */
! 	tlist = add_to_flat_tlist(tlist,
! 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
! 									   PVC_RECURSE_PLACEHOLDERS));
! 	tlist = add_to_flat_tlist(tlist,
! 							  pull_var_clause((Node *) fpinfo->local_conds,
! 											  PVC_RECURSE_PLACEHOLDERS));
  
  	return tlist;
  }
--- 770,849 ----
   * foreign server for the given relation.
   */
  List *
! build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel)
  {
! 	List	   *tlist;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
! 	 * Add expressions in foreignrel's reltarget as-is if the expressions are
! 	 * all shippable.  Otherwise add shipplable expressions in the reltarget
! 	 * as-is plus expressions required to evaluate non-shippable expressions
! 	 * in the reltarget.
  	 */
! 	if (fpinfo->reltarget_is_shippable)
! 		tlist = make_tlist_from_pathtarget(foreignrel->reltarget);
! 	else
! 	{
! 		List	   *exprs = NIL;
! 		ListCell   *lc;
! 
! 		/* Note: we have at least one non-shippable PHV in the reltarget. */
! 		foreach(lc, foreignrel->reltarget->exprs)
! 		{
! 			Node	   *node = (Node *) lfirst(lc);
! 
! 			Assert(IsA(node, Var) || IsA(node, PlaceHolderVar));
! 
! 			if (IsA(node, Var))
! 				exprs = lappend(exprs, node);
! 			else
! 			{
! 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 				PlaceHolderInfo *phinfo = find_placeholder_info(root, phv,
! 																false);
! 
! 				if (bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->outerrel->relids) ||
! 					bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->innerrel->relids))
! 				{
! 					/*
! 					 * The PHV coming from an either input should be shippable.
! 					 */
! 					exprs = lappend(exprs, node);
! 				}
! 				else
! 				{
! 					/*
! 					 * The PHV might be shippable, but in any case we retrieve
! 					 * expressions required to evaluate the PHV.
! 					 */
! 					List	   *exprs2 = pull_var_clause(node,
! 													PVC_INCLUDE_PLACEHOLDERS);
! 					ListCell   *lc2;
! 
! 					foreach(lc2, exprs2)
! 					{
! 						Node	   *node2 = (Node *) lfirst(lc2);
! 
! 						exprs = lappend(exprs, node2);
! 					}
! 				}
! 			}
! 		}
! 
! 		tlist = NIL;
! 		tlist = add_to_flat_tlist(tlist, exprs);
! 	}
! 
! 	/*
! 	 * Add expressions required to evaluate local conditions, if any.
! 	 */
! 	if (fpinfo->local_conds)
! 		tlist = add_to_flat_tlist(tlist,
! 								pull_var_clause((Node *) fpinfo->local_conds,
! 												PVC_INCLUDE_PLACEHOLDERS));
  
  	return tlist;
  }
***************
*** 1190,1198 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		/* We expect only Var nodes here */
! 		if (!IsA(var, Var))
! 			elog(ERROR, "non-Var not expected in target list");
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
--- 1270,1279 ----
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		/* Should be a Var or PlaceHolderVar node */
! 		if (!IsA(var, Var) && !IsA(var, PlaceHolderVar))
! 			elog(ERROR, "unexpected node type in target list: %d",
! 			     (int) nodeTag(var));
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
***************
*** 1293,1299 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
   *
   * If the given relation is an operand of a foreign join performing a full
   * outer join and has conditions that need to be evaluated below the full
!  * outer join, deparse the relation as a subquery.
   */
  static void
  deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
--- 1374,1382 ----
   *
   * If the given relation is an operand of a foreign join performing a full
   * outer join and has conditions that need to be evaluated below the full
!  * outer join, deparse the relation as a subquery.  Also, if the relation's
!  * reltarget has non-Var expressions, do the same, regardless of the join type
!  * of the foreign join.
   */
  static void
  deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
***************
*** 1304,1311 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	Assert(fpinfo->local_conds == NIL);
  
! 	if (jointype == JOIN_FULL && fpinfo->remote_conds)
  	{
  		List	   *tlist;
  		List	   *retrieved_attrs;
--- 1387,1396 ----
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	Assert(fpinfo->local_conds == NIL);
+ 	Assert(fpinfo->reltarget_is_shippable);
  
! 	if ((jointype == JOIN_FULL && fpinfo->remote_conds) ||
! 		fpinfo->reltarget_has_non_vars)
  	{
  		List	   *tlist;
  		List	   *retrieved_attrs;
***************
*** 1317,1323 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
  		context.aliased = aliased;
  		context.params_list = params_list;
  
! 		tlist = build_tlist_to_deparse(foreignrel);
  		appendStringInfoChar(buf, '(');
  		deparseSelectSql(tlist,
  						 fpinfo->remote_conds,
--- 1402,1408 ----
  		context.aliased = aliased;
  		context.params_list = params_list;
  
! 		tlist = build_tlist_to_deparse(root, foreignrel);
  		appendStringInfoChar(buf, '(');
  		deparseSelectSql(tlist,
  						 fpinfo->remote_conds,
***************
*** 1847,1860 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  			fetchval = rte->relid;
  		}
  
! 		if (qualify_col)
! 		{
! 			appendStringInfoString(buf, "CASE WHEN (");
! 			ADD_REL_QUALIFIER(buf, varno);
! 			appendStringInfo(buf, "*)::text IS NOT NULL THEN %u END", fetchval);
! 		}
! 		else
! 			appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
--- 1932,1938 ----
  			fetchval = rte->relid;
  		}
  
! 		appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
***************
*** 1885,1912 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  		attrs_used = bms_add_member(NULL,
  									0 - FirstLowInvalidHeapAttributeNumber);
  
- 		/*
- 		 * In case the whole-row reference is under an outer join then it has
- 		 * to go NULL whenever the rest of the row goes NULL. Deparsing a join
- 		 * query would always involve multiple relations, thus qualify_col
- 		 * would be true.
- 		 */
- 		if (qualify_col)
- 		{
- 			appendStringInfoString(buf, "CASE WHEN (");
- 			ADD_REL_QUALIFIER(buf, varno);
- 			appendStringInfo(buf, "*)::text IS NOT NULL THEN ");
- 		}
- 
  		appendStringInfoString(buf, "ROW(");
  		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
  						  &retrieved_attrs);
  		appendStringInfoString(buf, ")");
  
- 		/* Complete the CASE WHEN statement started above. */
- 		if (qualify_col)
- 			appendStringInfo(buf, " END");
- 
  		heap_close(rel, NoLock);
  		bms_free(attrs_used);
  	}
--- 1963,1973 ----
***************
*** 2088,2093 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 2149,2157 ----
  		case T_ArrayExpr:
  			deparseArrayExpr((ArrayExpr *) node, context);
  			break;
+ 		case T_PlaceHolderVar:
+ 			deparseExprInPlaceHolderVar((PlaceHolderVar *) node, context);
+ 			break;
  		default:
  			elog(ERROR, "unsupported expression type for deparse: %d",
  				 (int) nodeTag(node));
***************
*** 2659,2664 **** deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context)
--- 2723,2737 ----
  }
  
  /*
+  * Deparse a PlaceHolderVar's contained expression.
+  */
+ static void
+ deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context)
+ {
+ 	deparseExpr(node->phexpr, context);
+ }
+ 
+ /*
   * Print the representation of a parameter to be sent to the remote side.
   *
   * Note: we always label the Param's type explicitly rather than relying on
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1503,1510 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1503,1510 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                   
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1512,1518 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1512,1518 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1547,1554 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1547,1554 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                         
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1556,1562 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1556,1562 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1592,1599 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                     
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1592,1599 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                   QUERY PLAN                                                                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1601,1607 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1601,1607 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1636,1643 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                             QUERY PLAN                                                                                                                                                                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1636,1643 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                        
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1645,1651 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1645,1651 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss2.c1, ss2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) ss1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c1 = ss2.c1)) ORDER BY ss1.c2 ASC NULLS LAST, ss1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1715,1728 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                          QUERY PLAN                                                                                                                                                                                                         
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1715,1728 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss1.c4, ss2.c1 FROM ((SELECT ctid, ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1", c3 FROM "S 1"."T 1") ss1(c1, c2, c3, c4) INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1") ss2(c1, c2) ON (TRUE)) WHERE ((ss1.c3 = ss2.c2)) ORDER BY ss1.c4 ASC NULLS LAST, ss1.c3 ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 2051,2075 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
     1
  (10 rows)
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                         QUERY PLAN                                                         
! ---------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: (13), ft2.c1
!    Join Filter: (13 = ft2.c1)
!    ->  Foreign Scan on public.ft2
!          Output: ft2.c1
!          Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (("C 1" >= 10)) AND (("C 1" <= 15)) ORDER BY "C 1" ASC NULLS LAST
!    ->  Materialize
!          Output: (13)
!          ->  Foreign Scan on public.ft1
!                Output: 13
!                Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 13))
! (11 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
--- 2051,2066 ----
     1
  (10 rows)
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                                                                QUERY PLAN                                                                                               
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: (13), ft2.c1
!    Relations: (public.ft2) LEFT JOIN (public.ft1)
!    Remote SQL: SELECT r2."C 1", ss1.c1 FROM ("S 1"."T 1" r2 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) ss1(c1) ON (((13 = r2."C 1")))) WHERE ((r2."C 1" >= 10)) AND ((r2."C 1" <= 15))
! (4 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
***************
*** 2082,2105 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
      | 15
  (6 rows)
  
! -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Join Filter: (ft4.c1 = ft1.c1)
!    ->  Foreign Scan on public.ft4
!          Output: ft4.c1, ft4.c2, ft4.c3
!          Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 10)) AND ((c1 <= 15))
!    ->  Materialize
!          Output: ft1.c1, ft2.c1, (13)
!          ->  Foreign Scan
!                Output: ft1.c1, ft2.c1, 13
!                Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12)) ORDER BY r4."C 1" ASC NULLS LAST
! (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
--- 2073,2113 ----
      | 15
  (6 rows)
  
! EXPLAIN (VERBOSE, COSTS OFF)
! SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                                                                                                                                                  QUERY PLAN                                                                                                                                                                                  
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
!    Output: ft2.c1, (13), (13), ft2_1.c1
!    Relations: (public.ft2) LEFT JOIN ((public.ft2) LEFT JOIN (public.ft1))
!    Remote SQL: SELECT r1."C 1", ss2.c1, ss2.c2, ss2.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r5."C 1", 13, ss1.c1 FROM ("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) ss1(c1) ON (((13 = r5."C 1")))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))) ss2(c1, c2, c3) ON (((r1."C 1" = 13)))) WHERE ((r1."C 1" >= 10)) AND ((r1."C 1" <= 15))
! (4 rows)
! 
! SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
!  c1 | a2 | b2 | c2 
! ----+----+----+----
!  10 |    |    |   
!  11 |    |    |   
!  12 |    |    |   
!  13 | 13 |    | 10
!  13 | 13 |    | 11
!  13 | 13 |    | 12
!  13 | 13 | 13 | 13
!  13 | 13 |    | 14
!  13 | 13 |    | 15
!  14 |    |    |   
!  15 |    |    |   
! (11 rows)
! 
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                                                                                   QUERY PLAN                                                                                                                                                  
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Relations: (public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2))
!    Remote SQL: SELECT r1.c1, ss1.c1, ss1.c2, ss1.c3 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4."C 1", r5."C 1", 13 FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12))) ss1(c1, c2, c3) ON (((r1.c1 = ss1.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
! (4 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
***************
*** 2109,2124 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
   14 |    |    |   
  (3 rows)
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                       QUERY PLAN                                                                                                                                      
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2117,2150 ----
   14 |    |    |   
  (3 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.b IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                      
+ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ((ft1.c1 IS NOT NULL)), ft4_1.c1, (13), ft1.c1, ft2.c1
+    Relations: (public.ft4) LEFT JOIN ((public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2)))
+    Remote SQL: SELECT r1.c1, ss2.c1, ss2.c2, ss2.c3, ss2.c4, ss2.c5 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4.c1, ss1.c1, ss1.c2, (ss1.c1 IS NOT NULL), ss1.c3 FROM ("S 1"."T 3" r4 LEFT JOIN (SELECT r7."C 1", r8."C 1", 13 FROM ("S 1"."T 1" r7 INNER JOIN "S 1"."T 1" r8 ON (TRUE)) WHERE ((r8."C 1" = 12)) AND ((r7."C 1" = 12))) ss1(c1, c2, c3) ON (((r4.c1 = ss1.c1)))) WHERE ((r4.c1 >= 10)) AND ((r4.c1 <= 15))) ss2(c1, c2, c3, c4, c5) ON (((r1.c1 = ss2.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
+ (4 rows)
+ 
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.b IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 | d2 | e2 
+ ----+----+----+----+----+----
+  10 | f  | 10 |    |    |   
+  12 | t  | 12 | 13 | 12 | 12
+  14 | f  | 14 |    |    |   
+ (3 rows)
+ 
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                        QUERY PLAN                                                                                                                                       
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT ss1.c1, ss1.c2, ss1.c3, ss1.c4, r2.c1, r2.c2 FROM ((SELECT ROW(c1, c2, c3), c1, c2, c3 FROM "S 1"."T 4") ss1(c1, c2, c3, c4) INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((ss1.c2 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY ss1.c2 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 2907,2920 **** 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 (TRUE)) WHERE ((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)
--- 2933,2946 ----
  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)
***************
*** 3050,3063 **** 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
!                                                                                                                                     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 (TRUE)) WHERE ((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)
--- 3076,3089 ----
  
  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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 27,32 ****
--- 27,33 ----
  #include "optimizer/cost.h"
  #include "optimizer/pathnode.h"
  #include "optimizer/paths.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/planmain.h"
  #include "optimizer/restrictinfo.h"
  #include "optimizer/var.h"
***************
*** 403,409 **** static bool foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel,
  static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
  								 RelOptInfo *rel);
  static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
- static bool reltarget_has_non_vars(RelOptInfo *foreignrel);
  static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path);
  
--- 404,409 ----
***************
*** 641,646 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 641,707 ----
  	}
  
  	/*
+ 	 * Detect whether the reltarget expressions are all shippable and whether
+ 	 * there are any non-Var expressions in the reltarget.
+ 	 *
+ 	 * Note: if the relation is an appendrel child, it isn't in the main join
+ 	 * tree, so the info is not used.
+ 	 */
+ 	if (baserel->reloptkind == RELOPT_BASEREL)
+ 	{
+ 		bool		reltarget_is_shippable = true;
+ 		bool		reltarget_has_non_vars = false;
+ 		int			i;
+ 
+ 		foreach(lc, baserel->reltarget->exprs)
+ 		{
+ 			Node	   *node = (Node *) lfirst(lc);
+ 
+ 			Assert(IsA(node, Var) || IsA(node, PlaceHolderVar));
+ 
+ 			if (IsA(node, PlaceHolderVar))
+ 			{
+ 				reltarget_has_non_vars = true;
+ 				if (!is_foreign_expr(root, baserel, (Expr *) node))
+ 				{
+ 					reltarget_is_shippable = false;
+ 					break;
+ 				}
+ 			}
+ 		}
+ 
+ 		/*
+ 		 * Treat whole-row references and system columns other than citd and
+ 		 * oid like PHVs.
+ 		 *
+ 		 * Note: we deparse whole-row references as ROW() and system columns
+ 		 * as 0, except for tableoid, which is deparsed as a valid value for
+ 		 * the local table OID, in deparseExplicitTargetList.
+ 		 *
+ 		 * Note: any Vars including those are considered shippable.
+ 		 */
+ 		if (reltarget_is_shippable)
+ 		{
+ 			for (i = FirstLowInvalidHeapAttributeNumber + 1; i <= 0; i++)
+ 			{
+ 				if (i == SelfItemPointerAttributeNumber ||
+ 					i == ObjectIdAttributeNumber)
+ 					continue;
+ 
+ 				if (bms_is_member(i - FirstLowInvalidHeapAttributeNumber,
+ 								  fpinfo->attrs_used))
+ 				{
+ 					reltarget_has_non_vars = true;
+ 					break;
+ 				}
+ 			}
+ 		}
+ 
+ 		fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 		fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 	}
+ 
+ 	/*
  	 * Set the name of relation in fpinfo, while we are constructing it here.
  	 * It will be used to build the string describing the join relation in
  	 * EXPLAIN output. We can't know whether VERBOSE option is specified or
***************
*** 1178,1184 **** postgresGetForeignPlan(PlannerInfo *root,
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
--- 1239,1245 ----
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
***************
*** 2507,2513 **** estimate_path_cost_size(PlannerInfo *root,
  
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL)
! 			fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
--- 2568,2574 ----
  
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL)
! 			fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
***************
*** 3948,3953 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4009,4016 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	bool		reltarget_is_shippable = true;
+ 	bool		reltarget_has_non_vars = false;
  
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
***************
*** 3977,3982 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4040,4053 ----
  	if (fpinfo_o->local_conds || fpinfo_i->local_conds)
  		return false;
  
+ 	/*
+ 	 * If the reltargets of joining relations are not shippable, those are
+ 	 * required to be evaluated locally, so the join can not be pushed down.
+ 	 */
+ 	if (!fpinfo_o->reltarget_is_shippable ||
+ 		!fpinfo_i->reltarget_is_shippable)
+ 		return false;
+ 
  	/* Separate restrict list into join quals and quals on join relation */
  	if (IS_OUTER_JOIN(jointype))
  		extract_actual_join_clauses(extra->restrictlist, &joinclauses, &otherclauses);
***************
*** 4002,4027 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			return false;
  	}
  
- 	/*
- 	 * deparseExplicitTargetList() isn't smart enough to handle anything other
- 	 * than a Var.  In particular, if there's some PlaceHolderVar that would
- 	 * need to be evaluated within this join tree (because there's an upper
- 	 * reference to a quantity that may go to NULL as a result of an outer
- 	 * join), then we can't try to push the join down because we'll fail when
- 	 * we get to deparseExplicitTargetList().  However, a PlaceHolderVar that
- 	 * needs to be evaluated *at the top* of this join tree is OK, because we
- 	 * can do that locally after fetching the results from the remote side.
- 	 */
- 	foreach(lc, root->placeholder_list)
- 	{
- 		PlaceHolderInfo *phinfo = lfirst(lc);
- 		Relids		relids = joinrel->relids;
- 
- 		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
- 			bms_nonempty_difference(relids, phinfo->ph_eval_at))
- 			return false;
- 	}
- 
  	/* Save the join clauses, for later use. */
  	fpinfo->joinclauses = joinclauses;
  
--- 4073,4078 ----
***************
*** 4069,4099 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_o->remote_conds && reltarget_has_non_vars(outerrel))
! 				return false;
! 			if (fpinfo_i->remote_conds && reltarget_has_non_vars(innerrel))
! 				return false;
  			break;
  
  		default:
--- 4120,4153 ----
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
  		case JOIN_FULL:
! 			/* can't do anything here */
  			break;
  
  		default:
***************
*** 4105,4110 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4159,4199 ----
  	fpinfo->pushdown_safe = true;
  
  	/*
+ 	 * Detect whether the reltarget expressions are all shippable and whether
+ 	 * there are any non-Var expressions in the reltarget.
+ 	 *
+ 	 * Note: we don't need to consider whole-row references or system columns
+ 	 * other than ctid and oid here, since those are to be deparsed in leaf
+ 	 * subqueries.
+ 	 */
+ 	foreach(lc, joinrel->reltarget->exprs)
+ 	{
+ 		Node	   *node = (Node *) lfirst(lc);
+ 
+ 		Assert(IsA(node, Var) || IsA(node, PlaceHolderVar));
+ 
+ 		if (IsA(node, PlaceHolderVar))
+ 		{
+ 			PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 			PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
+ 
+ 			/* Ignore the PHV if it has bubbled up from an either input. */
+ 			if (bms_is_subset(phinfo->ph_eval_at, outerrel->relids) ||
+ 				bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
+ 				continue;
+ 
+ 			reltarget_has_non_vars = true;
+ 			if (!is_foreign_expr(root, joinrel, (Expr *) node))
+ 			{
+ 				reltarget_is_shippable = false;
+ 				break;
+ 			}
+ 		}
+ 	}
+ 	fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 	fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 
+ 	/*
  	 * If user is willing to estimate cost for a scan of either of the joining
  	 * relations using EXPLAIN, he intends to estimate scans on that relation
  	 * more accurately. Then, it makes sense to estimate the cost the join
***************
*** 4165,4196 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	return true;
  }
  
- /*
-  * Detect whether there are whole-row Vars or system columns other than ctid
-  * and oid in the given relation's reltarget.
-  *
-  * Note: currently, deparseExplicitTargetList can't properly handle anything
-  * other than a Var that is a ctid, oid, or user column, when deparsing the
-  * SELECT list of a subquery for a full join on relations with restrictions.
-  */
- static bool
- reltarget_has_non_vars(RelOptInfo *foreignrel)
- {
- 	ListCell   *lc;
- 
- 	foreach(lc, foreignrel->reltarget->exprs)
- 	{
- 		Var		   *var = (Var *) lfirst(lc);
- 
- 		Assert(IsA(var, Var));
- 		if (var->varattno <= 0 &&
- 			var->varattno != SelfItemPointerAttributeNumber &&
- 			var->varattno != ObjectIdAttributeNumber)
- 			return true;
- 	}
- 	return false;
- }
- 
  static void
  add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path)
--- 4254,4259 ----
***************
*** 4541,4546 **** conversion_error_callback(void *arg)
--- 4604,4610 ----
  	const char *attname = NULL;
  	const char *relname = NULL;
  	bool		is_wholerow = false;
+ 	bool		is_placeholder = false;
  	ConversionLocation *errpos = (ConversionLocation *) arg;
  
  	if (errpos->rel)
***************
*** 4565,4595 **** conversion_error_callback(void *arg)
  		EState	   *estate = fsstate->ss.ps.state;
  		TargetEntry *tle;
  		Var		   *var;
- 		RangeTblEntry *rte;
  
  		Assert(IsA(fsplan, ForeignScan));
  		tle = (TargetEntry *) list_nth(fsplan->fdw_scan_tlist,
  									   errpos->cur_attno - 1);
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
- 		Assert(IsA(var, Var));
- 
- 		rte = rt_fetch(var->varno, estate->es_range_table);
  
! 		if (var->varattno == 0)
! 			is_wholerow = true;
  		else
! 			attname = get_relid_attribute_name(rte->relid, var->varattno);
  
! 		relname = get_rel_name(rte->relid);
  	}
  
! 	if (relname)
  	{
  		if (is_wholerow)
! 			errcontext("whole-row reference to foreign table \"%s\"", relname);
  		else if (attname)
! 			errcontext("column \"%s\" of foreign table \"%s\"", attname, relname);
  	}
  }
  
--- 4629,4670 ----
  		EState	   *estate = fsstate->ss.ps.state;
  		TargetEntry *tle;
  		Var		   *var;
  
  		Assert(IsA(fsplan, ForeignScan));
  		tle = (TargetEntry *) list_nth(fsplan->fdw_scan_tlist,
  									   errpos->cur_attno - 1);
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		if (!IsA(var, Var))
! 		{
! 			Assert(IsA(var, PlaceHolderVar));
! 			is_placeholder = true;
! 		}
  		else
! 		{
! 			RangeTblEntry *rte  = rt_fetch(var->varno, estate->es_range_table);
! 
! 			if (var->varattno == 0)
! 				is_wholerow = true;
! 			else
! 				attname = get_relid_attribute_name(rte->relid, var->varattno);
  
! 			relname = get_rel_name(rte->relid);
! 		}
  	}
  
! 	if (is_placeholder)
! 		errcontext("expression at position %d in select list",
! 				   errpos->cur_attno);
! 	else if (relname)
  	{
  		if (is_wholerow)
! 			errcontext("whole-row reference to foreign table \"%s\"",
! 					   relname);
  		else if (attname)
! 			errcontext("column \"%s\" of foreign table \"%s\"",
! 					   attname, relname);
  	}
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 92,97 **** typedef struct PgFdwRelationInfo
--- 92,106 ----
  	RelOptInfo *innerrel;
  	JoinType	jointype;
  	List	   *joinclauses;
+ 
+ 	/*
+ 	 * Flags on Path output tlist for the relation.  These are used to detect
+ 	 * whether the relation can be joined with any other foreign table (or
+ 	 * join) on the remote server, and detect whether the relation has to be
+ 	 * deparsed as a subquery in that case.
+ 	 */
+ 	bool		reltarget_is_shippable; /* are reltarget exprs shippable? */
+ 	bool		reltarget_has_non_vars;	/* are there any non-Var exprs? */
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
***************
*** 155,161 **** extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(RelOptInfo *foreign_rel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
--- 164,170 ----
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 493,509 **** EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
! 
! -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
--- 493,511 ----
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
! EXPLAIN (VERBOSE, COSTS OFF)
! SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
! SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.b IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.b IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
#8Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#5)
Re: Push down more full joins in postgres_fdw

3. I think registerAlias stuff should happen really at the time of

creating paths and should be stored in fpinfo. Without that it
will be
computed every time we deparse the query. This means every time
we try
to EXPLAIN the query at various levels in the join tree and once
for the
query to be sent to the foreign server.

Hmm. I think the overhead in calculating aliases would be

negligible compared to the overhead in explaining each remote query
for costing or sending the remote query for execution. So, I
created aliases in the same way as remote params created and stored
into params_list in deparse_expr_cxt. I'm not sure it's worth
complicating the code.

We defer remote parameter creation till deparse time since the the

parameter numbers are dependent upon the sequence in which we deparse
the query. Creating them at the time of path creation and storing them
in fpinfo is not possible because a. those present in the joining
relations will conflict with each other and need some kind of
serialization at the time of deparsing b. those defer for differently
parameterized paths or paths with different pathkeys. We don't defer it
because it's very light on performance.

That's not true with the alias information. As long as we detect which
relations need subqueries, their RTIs are enough to create unique
aliases e.g. if a relation involves RTIs 1, 2, 3 corresponding subquery
can have alias r123 without conflicting with any other alias.

Hmm. But another concern about the approach of generating an subselect
alias for a path, if needed, at the path creation time would be that the
path might be rejected by add_path, which would result in totally wasting
cycles for generating the subselect alias for the path.

A path may get rejected but the relation is not rejected. The alias applies
to a relation and its targetlist, which will remain same for all paths
created for that relation, esp when it's a base relation or join. So, AFAIU
that work never gets wasted. Also, for costing paths with
use_remote_estimate, we deparse the query, which builds the alias
information again and again for very path. That's much worse than building
it once for a given relation.

However minimum overhead it might have, we will deparse the query every

time we create a path involving that kind of relation i.e. for different
pathkeys, different parameterization and different joins in which that
relation participates in. This number can be significant. We want to
avoid this overhead if we can.

Exactly. As I said above, I think the overhead would be negligible
compared to the overhead in explaining each remote query for costing or the
overhead in sending the final remote query for execution, though.

It won't remain minimal as the number of paths created increases,
increasing the number of times a query is deparsed. We deparse query every
time time we cost a path for a relation with use_remote_estimates true. As
we try to push down more and more stuff, we will create more paths and
deparse the query more time.

Also, that makes the interface better. Right now, in your patch, you have
changed the order of deparsing in the existing code, so that the aliases
are registered while deparsing FROM clause and before any Var nodes are
deparsed. If we create aliases at the time of path creation, only once in
GetForeignJoinPaths or GetForeignPaths as appropriate, that would require
less code churn and would save some CPU cycles as well.

6.

! if (is_placeholder)
! errcontext("placeholder expression at position %d in
select list",
! errpos->cur_attno);
A user wouldn't know what a placeholder expression is, there is
no such
term in the documentation. We have to device a better way to
provide an
error context for an expression in general.

Though I proposed that, I don't think that it's that important to

let users know that the expression is from a PHV. How about just
saying "expression", not "placeholder expression", so that we have
the message "expression at position %d in select list" in the context?

Hmm, that's better than placeholder expression, but not as explanatory

as it should be since we won't be printing the "select list" in the
error context and user won't have a clue as to what error context is
about.

I don't think so. Consider an example of the conversion error message,
which is from the regression test:

SELECT ft1.c1, ft2.c2, ft1 FROM ft1, ft2 WHERE ft1.c1 = ft2.c1 AND
ft1.c1 = 1;
ERROR: invalid input syntax for integer: "foo"
CONTEXT: whole-row reference to foreign table "ft1"

As shown in the example, the error message is displayed under a remote
query for execution. So, ISTM it's reasonable to print something like
"expression at position %d in select list" in the context if an expression
in a PHV.

I missed it. Sorry. Looks ok.

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

#9Robert Haas
robertmhaas@gmail.com
In reply to: Ashutosh Bapat (#4)
Re: Push down more full joins in postgres_fdw

On Tue, Sep 6, 2016 at 9:07 AM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

That's not true with the alias information. As long as we detect which
relations need subqueries, their RTIs are enough to create unique aliases
e.g. if a relation involves RTIs 1, 2, 3 corresponding subquery can have
alias r123 without conflicting with any other alias.

What if RTI 123 is also used in the query?

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

#10Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Robert Haas (#9)
Re: Push down more full joins in postgres_fdw

On Tue, Sep 13, 2016 at 10:28 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Sep 6, 2016 at 9:07 AM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

That's not true with the alias information. As long as we detect which
relations need subqueries, their RTIs are enough to create unique aliases
e.g. if a relation involves RTIs 1, 2, 3 corresponding subquery can have
alias r123 without conflicting with any other alias.

What if RTI 123 is also used in the query?

Good catch. I actually meant some combination of 1, 2 and 3, which is
unique for a join between r1, r2 and r3. How about r1_2_3 or
r1_r2_r3?

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

#11Robert Haas
robertmhaas@gmail.com
In reply to: Ashutosh Bapat (#10)
Re: Push down more full joins in postgres_fdw

On Tue, Sep 13, 2016 at 11:38 PM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

On Tue, Sep 13, 2016 at 10:28 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Sep 6, 2016 at 9:07 AM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

That's not true with the alias information. As long as we detect which
relations need subqueries, their RTIs are enough to create unique aliases
e.g. if a relation involves RTIs 1, 2, 3 corresponding subquery can have
alias r123 without conflicting with any other alias.

What if RTI 123 is also used in the query?

Good catch. I actually meant some combination of 1, 2 and 3, which is
unique for a join between r1, r2 and r3. How about r1_2_3 or
r1_r2_r3?

Sure, something like that can work, but if you have a big enough join
the identifier might get too long. I'm not sure why it wouldn't work
to just use the lowest RTI involved in the join, though; the others
won't appear anywhere else at that query level.

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

#12Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Robert Haas (#11)
Re: Push down more full joins in postgres_fdw

On Wed, Sep 14, 2016 at 8:52 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Sep 13, 2016 at 11:38 PM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

On Tue, Sep 13, 2016 at 10:28 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Sep 6, 2016 at 9:07 AM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

That's not true with the alias information. As long as we detect which
relations need subqueries, their RTIs are enough to create unique aliases
e.g. if a relation involves RTIs 1, 2, 3 corresponding subquery can have
alias r123 without conflicting with any other alias.

What if RTI 123 is also used in the query?

Good catch. I actually meant some combination of 1, 2 and 3, which is
unique for a join between r1, r2 and r3. How about r1_2_3 or
r1_r2_r3?

Sure, something like that can work, but if you have a big enough join
the identifier might get too long. I'm not sure why it wouldn't work
to just use the lowest RTI involved in the join, though; the others
won't appear anywhere else at that query level.

Yes, that will work too and is much more preferred than long r1_2_3.
The idea is to come with a unique alias name from RTIs involved in
that relation. Thinking loudly, r1_2_3 is more descriptive to debug
issues. It tells that the subquery it refers to covers RTIs 1, 2 and
3. That information may be quite helpful to understand the remote SQL.
r1 on the other hand can refer to relation with RTI 1 or a join
relation where least RTI is 1. That can be solved by using s<RTI> for
subquery and r<RTI> for a bare relation. But it still doesn't tell us
which all relations are involved.

But since the subquery is part of remote SQL and we have to take a
look at it to find out meaning of individual columns, that benefit may
be smaller compared to the convenience of smaller alias. So +1 for
using the smallest RTI to indicate a subquery.

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

#13Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#8)
Re: Push down more full joins in postgres_fdw

On 2016/09/13 14:17, Ashutosh Bapat wrote:

But another concern about the approach of generating an
subselect alias for a path, if needed, at the path creation time
would be that the path might be rejected by add_path, which would
result in totally wasting cycles for generating the subselect alias
for the path.

A path may get rejected but the relation is not rejected. The alias
applies to a relation and its targetlist, which will remain same for all
paths created for that relation, esp when it's a base relation or join.
So, AFAIU that work never gets wasted.

I think there is no guarantee that add_path won't reject foreign join
paths. The possibility would be very low, though.

However minimum overhead it might have, we will deparse the
query every
time we create a path involving that kind of relation i.e. for
different
pathkeys, different parameterization and different joins in
which that
relation participates in. This number can be significant. We want to
avoid this overhead if we can.

Exactly. As I said above, I think the overhead would be negligible
compared to the overhead in explaining each remote query for costing
or the overhead in sending the final remote query for execution, though.

It won't remain minimal as the number of paths created increases,
increasing the number of times a query is deparsed. We deparse query
every time time we cost a path for a relation with use_remote_estimates
true. As we try to push down more and more stuff, we will create more
paths and deparse the query more time.

Also, that makes the interface better. Right now, in your patch, you
have changed the order of deparsing in the existing code, so that the
aliases are registered while deparsing FROM clause and before any Var
nodes are deparsed. If we create aliases at the time of path creation,
only once in GetForeignJoinPaths or GetForeignPaths as appropriate, that
would require less code churn and would save some CPU cycles as well.

Agreed. Will fix.

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: Ashutosh Bapat (#12)
Re: Push down more full joins in postgres_fdw

On 2016/09/15 15:29, Ashutosh Bapat wrote:

On Wed, Sep 14, 2016 at 8:52 PM, Robert Haas <robertmhaas@gmail.com> wrote:

I'm not sure why it wouldn't work
to just use the lowest RTI involved in the join, though; the others
won't appear anywhere else at that query level.

So +1 for
using the smallest RTI to indicate a subquery.

+1 for the general idea.

ISTM that the use of the same RTI for subqueries in multi-levels in a
remote SQL makes the SQL a bit difficult to read. How about using the
position of the join rel in join_rel_list, (more precisely, the position
plus list_length(root->parse->rtable)), instead?

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

#15Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#14)
Re: Push down more full joins in postgres_fdw

On Mon, Sep 26, 2016 at 1:05 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/09/15 15:29, Ashutosh Bapat wrote:

On Wed, Sep 14, 2016 at 8:52 PM, Robert Haas <robertmhaas@gmail.com>
wrote:

I'm not sure why it wouldn't work
to just use the lowest RTI involved in the join, though; the others
won't appear anywhere else at that query level.

So +1 for
using the smallest RTI to indicate a subquery.

+1 for the general idea.

ISTM that the use of the same RTI for subqueries in multi-levels in a remote
SQL makes the SQL a bit difficult to read. How about using the position of
the join rel in join_rel_list, (more precisely, the position plus
list_length(root->parse->rtable)), instead?

We switch to hash table to maintain the join RelOptInfos when the
number of joins grows larger, where the position won't make much
sense. We might differentiate between a base relation alias and
subquery alias by using different prefixes like "r" and "s" resp.

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

#16Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#15)
Re: Push down more full joins in postgres_fdw

On 2016/09/26 18:06, Ashutosh Bapat wrote:

On Mon, Sep 26, 2016 at 1:05 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

ISTM that the use of the same RTI for subqueries in multi-levels in a remote
SQL makes the SQL a bit difficult to read. How about using the position of
the join rel in join_rel_list, (more precisely, the position plus
list_length(root->parse->rtable)), instead?

We switch to hash table to maintain the join RelOptInfos when the
number of joins grows larger, where the position won't make much
sense.

That's right, but we still store the joinrel into join_rel_list after
creating that hash table. That hash table is just for fast lookup. See
build_join_rel.

We might differentiate between a base relation alias and
subquery alias by using different prefixes like "r" and "s" resp.

Hmm. My concern about that would the recursive use of "s" with the same
RTI as subquery aliases for different level subqueries in a single
remote SQL. For example, if those subqueries include a base rel with
RTI=1, then "s1" would be used again and again within that SQL. That
would be logically correct, but seems confusing to me.

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

#17Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#16)
Re: Push down more full joins in postgres_fdw

On Mon, Sep 26, 2016 at 4:06 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/09/26 18:06, Ashutosh Bapat wrote:

On Mon, Sep 26, 2016 at 1:05 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

ISTM that the use of the same RTI for subqueries in multi-levels in a
remote
SQL makes the SQL a bit difficult to read. How about using the position
of
the join rel in join_rel_list, (more precisely, the position plus
list_length(root->parse->rtable)), instead?

We switch to hash table to maintain the join RelOptInfos when the
number of joins grows larger, where the position won't make much
sense.

That's right, but we still store the joinrel into join_rel_list after
creating that hash table. That hash table is just for fast lookup. See
build_join_rel.

Sorry, I wasn't clear in my reply. As the list grows, it will take
longer to locate the RelOptInfo of the given join. Doing that for
creating an alias seems an overkill.

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

#18Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#17)
Re: Push down more full joins in postgres_fdw

On 2016/09/26 20:20, Ashutosh Bapat wrote:

On Mon, Sep 26, 2016 at 4:06 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/09/26 18:06, Ashutosh Bapat wrote:

On Mon, Sep 26, 2016 at 1:05 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

ISTM that the use of the same RTI for subqueries in multi-levels in a
remote
SQL makes the SQL a bit difficult to read. How about using the position
of
the join rel in join_rel_list, (more precisely, the position plus
list_length(root->parse->rtable)), instead?

We switch to hash table to maintain the join RelOptInfos when the
number of joins grows larger, where the position won't make much
sense.

That's right, but we still store the joinrel into join_rel_list after
creating that hash table.

As the list grows, it will take
longer to locate the RelOptInfo of the given join. Doing that for
creating an alias seems an overkill.

The join rel is appended to the end of the list, so I was thinking to
get the position info by list_length during postgresGetForeignJoinPaths.

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

#19Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#18)
Re: Push down more full joins in postgres_fdw

On Tue, Sep 27, 2016 at 8:48 AM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/09/26 20:20, Ashutosh Bapat wrote:

On Mon, Sep 26, 2016 at 4:06 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/09/26 18:06, Ashutosh Bapat wrote:

On Mon, Sep 26, 2016 at 1:05 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

ISTM that the use of the same RTI for subqueries in multi-levels in a
remote
SQL makes the SQL a bit difficult to read. How about using the
position
of
the join rel in join_rel_list, (more precisely, the position plus
list_length(root->parse->rtable)), instead?

We switch to hash table to maintain the join RelOptInfos when the
number of joins grows larger, where the position won't make much
sense.

That's right, but we still store the joinrel into join_rel_list after
creating that hash table.

As the list grows, it will take
longer to locate the RelOptInfo of the given join. Doing that for
creating an alias seems an overkill.

The join rel is appended to the end of the list, so I was thinking to get
the position info by list_length during postgresGetForeignJoinPaths.

That's true only when the paths are being added to a newly created
joinrel. But that's not true always. We may add paths with different
joining order to an existing joinrel, in which case list_length would
not give its position. Am I missing something?

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

#20Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#19)
Re: Push down more full joins in postgres_fdw

On 2016/09/27 13:33, Ashutosh Bapat wrote:

I wrote:

ISTM that the use of the same RTI for subqueries in multi-levels in a
remote
SQL makes the SQL a bit difficult to read. How about using the
position
of
the join rel in join_rel_list, (more precisely, the position plus
list_length(root->parse->rtable)), instead?

I wrote:

The join rel is appended to the end of the list, so I was thinking to get
the position info by list_length during postgresGetForeignJoinPaths.

That's true only when the paths are being added to a newly created
joinrel. But that's not true always. We may add paths with different
joining order to an existing joinrel, in which case list_length would
not give its position. Am I missing something?

I think you are right, but postgresGetForeignJoinPaths only allows us to
add a foreign join path to a newly created joinrel. The reason is
because that routine skips all its work after the first call for that
joinrel, by checking to see if joinrel->fdw_private is not NULL. So, I
think it's reasonable to get the position by list_length in that routine.

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

#21Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#13)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/09/26 16:30, Etsuro Fujita wrote:

On 2016/09/13 14:17, Ashutosh Bapat wrote:

It won't remain minimal as the number of paths created increases,
increasing the number of times a query is deparsed. We deparse query
every time time we cost a path for a relation with use_remote_estimates
true. As we try to push down more and more stuff, we will create more
paths and deparse the query more time.

Also, that makes the interface better. Right now, in your patch, you
have changed the order of deparsing in the existing code, so that the
aliases are registered while deparsing FROM clause and before any Var
nodes are deparsed. If we create aliases at the time of path creation,
only once in GetForeignJoinPaths or GetForeignPaths as appropriate, that
would require less code churn and would save some CPU cycles as well.

Agreed. Will fix.

Done. Attached is an updated version of the patch.

I didn't create aliases at anytime. Instead, I added a logic to get
info about the alias to a given expression from reltarget->exprs for
relations in a given join tree. See isSubqueryExpr and
getSubselectAliasInfo.

As proposed by you, the patch differentiates between a base relation
alias and a subquery alias by using different prefixes "r" and "s",
respectively. Also, subquery aliases are indexed by RTI for baserels
and the position in join_rel_list + the length of rtable for joinrels,
as proposed upthread.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-more-full-join-pushdown-v4.patchapplication/x-patch; name=postgres-fdw-more-full-join-pushdown-v4.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 102,107 **** typedef struct deparse_expr_cxt
--- 102,109 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 152,164 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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);
  
  
  /*
--- 154,175 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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,
! 					  List **params_list);
! static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 				   bool is_subquery, List **params_list);
! static void appendSubselectAlias(deparse_expr_cxt *context);
! static void getSubselectAliasInfo(Expr *node, int *tabno, int *colno,
! 					  RelOptInfo *foreignrel);
! static bool isSubqueryExpr(Expr *node, int *tabno, int *colno, RelOptInfo *foreignrel);
  
  
  /*
***************
*** 780,796 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  	context.foreignrel = rel;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause and FROM clause */
! 	deparseSelectSql(tlist, retrieved_attrs, &context);
! 
! 	/*
! 	 * Construct WHERE clause
! 	 */
! 	if (remote_conds)
! 	{
! 		appendStringInfo(buf, " WHERE ");
! 		appendConditions(remote_conds, &context);
! 	}
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
--- 791,798 ----
  	context.foreignrel = rel;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause, FROM clause, and WHERE clause */
! 	deparseSelectSql(tlist, remote_conds, retrieved_attrs, &context);
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
***************
*** 803,809 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ....".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
--- 805,811 ----
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ... WHERE ...".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
***************
*** 812,818 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
--- 814,823 ----
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
! 				 deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
***************
*** 824,832 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
--- 829,842 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	/*
! 	 * Note: tlist for a base relation may be non-NIL.  For example, if the
! 	 * base relation is an operand of a foreign join performing a full outer
! 	 * join and has non-NIL remote_conds, then we deparse the base relation
! 	 * as a subquery, so tlist for the base relation would be non-NIL.
! 	 */
! 	if (tlist != NIL)
  	{
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
***************
*** 843,848 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
--- 853,860 ----
  		 */
  		Relation	rel = heap_open(rte->relid, NoLock);
  
+ 		Assert(foreignrel->reloptkind != RELOPT_JOINREL);
+ 
  		deparseTargetList(buf, root, foreignrel->relid, rel, false,
  						  fpinfo->attrs_used, false, retrieved_attrs);
  		heap_close(rel, NoLock);
***************
*** 855,860 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
--- 867,881 ----
  	deparseFromExprForRel(buf, root, foreignrel,
  						  (foreignrel->reloptkind == RELOPT_JOINREL),
  						  context->params_list);
+ 
+ 	/*
+ 	 * Construct WHERE clause
+ 	 */
+ 	if (remote_conds)
+ 	{
+ 		appendStringInfoString(buf, " WHERE ");
+ 		appendConditions(remote_conds, context);
+ 	}
  }
  
  /*
***************
*** 965,975 **** deparseLockingClause(deparse_expr_cxt *context)
--- 986,1005 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->foreignrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it's contained in a lower subquery because in
+ 		 * that case we've already considered locking for the relation when
+ 		 * deparsing the subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subqueried_relids))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1138,1150 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseVar(var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
--- 1168,1181 ----
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseExpr((Expr *) var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
+ 	/* Don't generate bad syntax if no columns */
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
***************
*** 1152,1191 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  /*
   * Construct FROM clause for given relation
   *
!  * 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)
  	{
! 		RelOptInfo *rel_o = fpinfo->outerrel;
! 		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
! 		 *
! 		 * ((outer relation) <join type> (inner relation) ON (joinclauses))
! 		 */
! 		appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
! 					   get_jointype_name(fpinfo->jointype), join_sql_i.data);
  
! 		/* Append join clause; (TRUE) if no join clause */
  		if (fpinfo->joinclauses)
  		{
  			deparse_expr_cxt context;
--- 1183,1223 ----
  /*
   * Construct FROM clause for given relation
   *
!  * For a join relation the clause of the following form is appended to buf:
!  * ((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,
! 					  List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
! 		/* Begin the FROM clause entry */
! 		appendStringInfoChar(buf, '(');
  
  		/* Deparse outer relation */
! 		deparseRangeTblRef(buf, root,
! 						   fpinfo->outerrel,
! 						   fpinfo->outer_is_subquery,
! 						   params_list);
  
! 		/* Append join type */
! 		appendStringInfo(buf, " %s JOIN ",
! 						 get_jointype_name(fpinfo->jointype));
  
! 		/* Deparse inner relation */
! 		deparseRangeTblRef(buf, root,
! 						   fpinfo->innerrel,
! 						   fpinfo->inner_is_subquery,
! 						   params_list);
  
! 		/* Append join conditions */
! 		appendStringInfoString(buf, " ON ");
  		if (fpinfo->joinclauses)
  		{
  			deparse_expr_cxt context;
***************
*** 1195,1209 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  			context.root = root;
  			context.params_list = params_list;
  
! 			appendStringInfo(buf, "(");
  			appendConditions(fpinfo->joinclauses, &context);
! 			appendStringInfo(buf, ")");
  		}
  		else
  			appendStringInfoString(buf, "(TRUE)");
  
! 		/* End the FROM clause entry. */
! 		appendStringInfo(buf, ")");
  	}
  	else
  	{
--- 1227,1244 ----
  			context.root = root;
  			context.params_list = params_list;
  
! 			appendStringInfoChar(buf, '(');
  			appendConditions(fpinfo->joinclauses, &context);
! 			appendStringInfoChar(buf, ')');
  		}
  		else
+ 		{
+ 			/* No join conditions; add "(TRUE)" */
  			appendStringInfoString(buf, "(TRUE)");
+ 		}
  
! 		/* End the FROM clause entry */
! 		appendStringInfoChar(buf, ')');
  	}
  	else
  	{
***************
*** 1222,1228 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (use_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
--- 1257,1263 ----
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (add_rel_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
***************
*** 1230,1235 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1265,1412 ----
  }
  
  /*
+  * Append operand relation of foreign join to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool is_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	if (is_subquery)
+ 	{
+ 		List	   *tlist;
+ 		List	   *retrieved_attrs;
+ 		deparse_expr_cxt context;
+ 
+ 		context.buf = buf;
+ 		context.root = root;
+ 		context.foreignrel = foreignrel;
+ 		context.params_list = params_list;
+ 
+ 		tlist = build_tlist_to_deparse(foreignrel);
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectSql(tlist,
+ 						 fpinfo->remote_conds,
+ 						 &retrieved_attrs,
+ 						 &context);
+ 		deparseLockingClause(&context);
+ 		appendStringInfoChar(buf, ')');
+ 		appendSubselectAlias(&context);
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Add a subselect alias to a subquery-in-FROM.
+  */
+ static void
+ appendSubselectAlias(deparse_expr_cxt *context)
+ {
+ 	StringInfo	buf = context->buf;
+ 	RelOptInfo *foreignrel = context->foreignrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			num_columns = list_length(foreignrel->reltarget->exprs);
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, fpinfo->relation_index);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= num_columns; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get info about the subselect alias to given expression.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively.
+  */
+ static void
+ getSubselectAliasInfo(Expr *node, int *tabno, int *colno,
+ 					  RelOptInfo *foreignrel)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 	/* shouldn't get here */
+ 	elog(ERROR, "found expression not expected in subquery output");
+ }
+ 
+ /*
+  * Returns true if given expression is an output column of a subquery-in-FROM.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively, in that case.
+  */
+ static bool
+ isSubqueryExpr(Expr *node, int *tabno, int *colno, RelOptInfo *foreignrel)
+ {
+ 	Var		   *var = (Var *) node;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!fpinfo->subqueried_relids)
+ 		return false;
+ 
+ 	Assert(IsA(var, Var));
+ 
+ 	if (bms_is_member(var->varno, outerrel->relids))
+ 	{
+ 		if (fpinfo->outer_is_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, tabno, colno, outerrel);
+ 			return true;
+ 		}
+ 		if (isSubqueryExpr(node, tabno, colno, outerrel))
+ 			return true;
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(var->varno, innerrel->relids));
+ 
+ 		if (fpinfo->inner_is_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, tabno, colno, innerrel);
+ 			return true;
+ 		}
+ 		if (isSubqueryExpr(node, tabno, colno, innerrel))
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 1811,1816 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 1988,2011 ----
  	if (node == NULL)
  		return;
  
+ 	/*
+ 	 * If the given expression is an output column of a subquery-in-FROM,
+ 	 * deparse the alias to the expression instead.
+ 	 */
+ 	if (IsA(node, Var))
+ 	{
+ 		int			tabno;
+ 		int			colno;
+ 
+ 		if (isSubqueryExpr(node, &tabno, &colno, context->foreignrel))
+ 		{
+ 			appendStringInfo(context->buf, "%s%d.%s%d",
+ 							 SS_TAB_ALIAS_PREFIX, tabno,
+ 							 SS_COL_ALIAS_PREFIX, colno);
+ 			return;
+ 		}
+ 	}
+ 
  	switch (nodeTag(node))
  	{
  		case T_Var:
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 456,463 **** SELECT t1.c1, t2."C 1" FROM ft2 t1 LEFT JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1"
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                        QUERY PLAN                                                                        
! ---------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
--- 456,463 ----
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
***************
*** 466,472 **** EXPLAIN (VERBOSE, COSTS OFF)
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (((r2."C 1" = r3."C 1")))) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
--- 466,472 ----
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (TRUE)) WHERE ((r2."C 1" = r3."C 1")) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
***************
*** 979,992 **** ANALYZE ft5;
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                         QUERY PLAN                                                                                        
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 979,992 ----
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                              QUERY PLAN                                                                                              
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1007,1014 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                             QUERY PLAN                                                                                             
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
--- 1007,1014 ----
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                                       QUERY PLAN                                                                                                       
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
***************
*** 1017,1023 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) INNER JOIN "S 1"."T 3" r4 ON (((r1."C 1" = r4.c1))))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
--- 1017,1023 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) INNER JOIN "S 1"."T 3" r4 ON (TRUE)) WHERE ((r1."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
***************
*** 1223,1245 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1223,1235 ----
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1257,1270 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                            QUERY PLAN                                                                                                                                            
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
--- 1247,1260 ----
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                     QUERY PLAN                                                                                                                                                                    
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT s6.c1, s6.c2, r4.c1 FROM ((SELECT r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))) s6(c1, c2) FULL JOIN "S 1"."T 3" r4 ON (((s6.c2 = r4.c1)))) ORDER BY s6.c1 ASC NULLS LAST, s6.c2 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 1282,1287 **** SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 a
--- 1272,1315 ----
      |    | 16
  (10 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+                                                                                                                         QUERY PLAN                                                                                                                         
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: t1.*, t1.c1, t2.c1, t3.c1
+    ->  Sort
+          Output: t1.*, t1.c1, t2.c1, t3.c1
+          Sort Key: t1.c1, t2.c1, t3.c1
+          ->  Hash Full Join
+                Output: t1.*, t1.c1, t2.c1, t3.c1
+                Hash Cond: (t2.c1 = t3.c1)
+                ->  Foreign Scan
+                      Output: t1.*, t1.c1, t2.c1
+                      Relations: (public.ft4 t1) INNER JOIN (public.ft5 t2)
+                      Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))
+                ->  Hash
+                      Output: t3.c1
+                      ->  Foreign Scan on public.ft4 t3
+                            Output: t3.c1
+                            Remote SQL: SELECT c1 FROM "S 1"."T 3"
+ (17 rows)
+ 
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+        t1       | c1 | c1 | c1 
+ ----------------+----+----+----
+  (52,53,AAA052) | 52 | 51 |   
+  (58,59,AAA058) | 58 | 57 |   
+                 |    |    |  2
+                 |    |    |  4
+                 |    |    |  6
+                 |    |    |  8
+                 |    |    | 10
+                 |    |    | 12
+                 |    |    | 14
+                 |    |    | 16
+ (10 rows)
+ 
  -- full outer join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 1453,1466 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) LEFT
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                      QUERY PLAN                                                                                      
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ON (((r2."C 1" = r4.c1))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
--- 1481,1494 ----
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) ON (((r2."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 1513,1520 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1541,1548 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1522,1528 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1550,1556 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1557,1564 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1585,1592 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1566,1572 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1594,1600 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1602,1609 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                               
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1630,1637 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                     
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1611,1617 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1639,1645 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1646,1653 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1674,1681 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                             QUERY PLAN                                                                                                                                                                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1655,1661 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1683,1689 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1691,1705 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                              QUERY PLAN                                                              
! -------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
--- 1719,1733 ----
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                                    QUERY PLAN                                                                   
! ------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
***************
*** 1725,1738 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1753,1766 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                          QUERY PLAN                                                                                                                                                                                                         
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 1955,1962 **** SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                       QUERY PLAN                                                                       
! -------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
--- 1983,1990 ----
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                             QUERY PLAN                                                                            
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
***************
*** 1966,1972 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 1994,2000 ----
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1987,1994 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                      QUERY PLAN                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
--- 2015,2022 ----
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                           QUERY PLAN                                                                           
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
***************
*** 2004,2014 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
--- 2032,2042 ----
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
***************
*** 2029,2036 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
--- 2057,2064 ----
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                                   QUERY PLAN                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
***************
*** 2043,2049 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
--- 2071,2077 ----
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
***************
*** 2095,2102 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
  -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                     QUERY PLAN                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
     Join Filter: (ft4.c1 = ft1.c1)
--- 2123,2130 ----
  -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
     Join Filter: (ft4.c1 = ft1.c1)
***************
*** 2108,2114 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
           ->  Foreign Scan
                 Output: ft1.c1, ft2.c1, 13
                 Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
  (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
--- 2136,2142 ----
           ->  Foreign Scan
                 Output: ft1.c1, ft2.c1, 13
                 Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12)) ORDER BY r4."C 1" ASC NULLS LAST
  (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
***************
*** 2123,2134 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                 QUERY PLAN                                                                                                                                 
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2151,2162 ----
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                       QUERY PLAN                                                                                                                                      
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 2288,2299 **** DROP ROLE regress_view_owner;
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                           QUERY PLAN                                                          
! ------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = 2)) AND ((r1."C 1" = 1))))
  (4 rows)
  
  EXECUTE st1(1, 1);
--- 2316,2327 ----
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                                QUERY PLAN                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r2."C 1" = 2)) AND ((r1."C 1" = 1))
  (4 rows)
  
  EXECUTE st1(1, 1);
***************
*** 2917,2930 **** 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)
--- 2945,2958 ----
  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 (TRUE)) WHERE ((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)
***************
*** 3060,3073 **** 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
!                                                                                                                               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)
--- 3088,3101 ----
  
  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 (TRUE)) WHERE ((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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 403,408 **** static bool foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel,
--- 403,409 ----
  static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
  								 RelOptInfo *rel);
  static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+ static bool reltarget_has_non_vars(RelOptInfo *foreignrel);
  static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path);
  
***************
*** 655,660 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 656,669 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Set the subquery information */
+ 	fpinfo->outer_is_subquery = false;
+ 	fpinfo->inner_is_subquery = false;
+ 	fpinfo->subqueried_relids = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 3947,3952 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 3956,3962 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	Relids		relids;
  
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
***************
*** 4011,4020 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 * needs to be evaluated *at the top* of this join tree is OK, because we
  	 * can do that locally after fetching the results from the remote side.
  	 */
  	foreach(lc, root->placeholder_list)
  	{
  		PlaceHolderInfo *phinfo = lfirst(lc);
- 		Relids		relids = joinrel->relids;
  
  		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
  			bms_nonempty_difference(relids, phinfo->ph_eval_at))
--- 4021,4030 ----
  	 * needs to be evaluated *at the top* of this join tree is OK, because we
  	 * can do that locally after fetching the results from the remote side.
  	 */
+ 	relids = joinrel->relids;
  	foreach(lc, root->placeholder_list)
  	{
  		PlaceHolderInfo *phinfo = lfirst(lc);
  
  		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
  			bms_nonempty_difference(relids, phinfo->ph_eval_at))
***************
*** 4049,4056 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4059,4065 ----
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4061,4068 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4070,4076 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4091,4097 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
  				return false;
  			break;
  
--- 4099,4113 ----
  			break;
  
  		case JOIN_FULL:
! 			/*
! 			 * We can't do anything here, and if there are any non-Vars in the
! 			 * outerrel/innerrel's reltarget, give up pushing down this join
! 			 * because we currently don't support deparsing such a relation as
! 			 * a subquery.
! 			 */
! 			if (reltarget_has_non_vars(outerrel))
! 				return false;
! 			if (reltarget_has_non_vars(innerrel))
  				return false;
  			break;
  
***************
*** 4100,4117 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			elog(ERROR, "unsupported join type %d", jointype);
  	}
  
- 	/*
- 	 * For an inner join, all restrictions can be treated alike. Treating the
- 	 * pushed down conditions as join conditions allows a top level full outer
- 	 * join to be deparsed without requiring subqueries.
- 	 */
- 	if (jointype == JOIN_INNER)
- 	{
- 		Assert(!fpinfo->joinclauses);
- 		fpinfo->joinclauses = fpinfo->remote_conds;
- 		fpinfo->remote_conds = NIL;
- 	}
- 
  	/* Mark that this join can be pushed down safely */
  	fpinfo->pushdown_safe = true;
  
--- 4116,4121 ----
***************
*** 4173,4181 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4177,4242 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the subquery information
+ 	 *
+ 	 * If the relation performs a full outer join, then the input rel with
+ 	 * non-NIL remote_conds needs to be deparsed as a subquery.
+ 	 */
+ 	relids = NULL;
+ 	if (jointype == JOIN_FULL && fpinfo_o->remote_conds)
+ 	{
+ 		fpinfo->outer_is_subquery = true;
+ 		relids = bms_add_members(relids, outerrel->relids);
+ 	}
+ 	else
+ 	{
+ 		fpinfo->outer_is_subquery = false;
+ 		relids = bms_add_members(relids, fpinfo_o->subqueried_relids);
+ 	}
+ 	if (jointype == JOIN_FULL && fpinfo_i->remote_conds)
+ 	{
+ 		fpinfo->inner_is_subquery = true;
+ 		relids = bms_add_members(relids, innerrel->relids);
+ 	}
+ 	else
+ 	{
+ 		fpinfo->inner_is_subquery = false;
+ 		relids = bms_add_members(relids, fpinfo_i->subqueried_relids);
+ 	}
+ 	fpinfo->subqueried_relids = relids;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
+ /*
+  * Detect whether there are whole-row Vars or system columns other than ctid
+  * and oid in the given relation's reltarget.
+  *
+  * Note: currently deparseExplicitTargetList can't properly handle such Vars.
+  */
+ static bool
+ reltarget_has_non_vars(RelOptInfo *foreignrel)
+ {
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		Assert(IsA(var, Var));
+ 		if (var->varattno <= 0 &&
+ 			var->varattno != SelfItemPointerAttributeNumber &&
+ 			var->varattno != ObjectIdAttributeNumber)
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
  static void
  add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path)
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 87,97 **** typedef struct PgFdwRelationInfo
--- 87,108 ----
  	 */
  	StringInfo	relation_name;
  
+ 	/*
+ 	 * Index of the relation.  It is used for creating a subselect alias when
+ 	 * deparsing the relation as a subquery.
+ 	 */
+ 	Index		relation_index;
+ 
  	/* Join information */
  	RelOptInfo *outerrel;
  	RelOptInfo *innerrel;
  	JoinType	jointype;
  	List	   *joinclauses;
+ 
+ 	/* Subquery information */
+ 	bool		outer_is_subquery;
+ 	bool		inner_is_subquery;
+ 	Relids		subqueried_relids;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 398,403 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 398,406 ----
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  -- full outer join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
#22Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#21)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/09/28 18:35, Etsuro Fujita wrote:

Attached is an updated version of the patch.

I found a minor bug in that patch; the relation_index added to
PgFdwRelationInfo was defined as Index, but I used the modifier %d to
print that. So, I changed the data type to int. Also, I added a bit
more comments. Please find attached an updated version of the patch.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-more-full-join-pushdown-v5.patchbinary/octet-stream; name=postgres-fdw-more-full-join-pushdown-v5.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 102,107 **** typedef struct deparse_expr_cxt
--- 102,109 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 152,164 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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);
  
  
  /*
--- 154,175 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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,
! 					  List **params_list);
! static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 				   bool is_subquery, List **params_list);
! static void appendSubselectAlias(deparse_expr_cxt *context);
! static void getSubselectAliasInfo(Expr *node, int *tabno, int *colno,
! 					  RelOptInfo *foreignrel);
! static bool isSubqueryExpr(Expr *node, int *tabno, int *colno, RelOptInfo *foreignrel);
  
  
  /*
***************
*** 780,796 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  	context.foreignrel = rel;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause and FROM clause */
! 	deparseSelectSql(tlist, retrieved_attrs, &context);
! 
! 	/*
! 	 * Construct WHERE clause
! 	 */
! 	if (remote_conds)
! 	{
! 		appendStringInfo(buf, " WHERE ");
! 		appendConditions(remote_conds, &context);
! 	}
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
--- 791,798 ----
  	context.foreignrel = rel;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause, FROM clause, and WHERE clause */
! 	deparseSelectSql(tlist, remote_conds, retrieved_attrs, &context);
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
***************
*** 803,809 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ....".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
--- 805,811 ----
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ... WHERE ...".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
***************
*** 812,818 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
--- 814,823 ----
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
! 				 deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
***************
*** 824,832 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
--- 829,842 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	/*
! 	 * Note: tlist for a base relation may be non-NIL.  For example, if the
! 	 * base relation is an operand of a foreign join performing a full outer
! 	 * join and has non-NIL remote_conds, then we deparse the base relation
! 	 * as a subquery, so tlist for the base relation would be non-NIL.
! 	 */
! 	if (tlist != NIL)
  	{
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
***************
*** 843,848 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
--- 853,860 ----
  		 */
  		Relation	rel = heap_open(rte->relid, NoLock);
  
+ 		Assert(foreignrel->reloptkind != RELOPT_JOINREL);
+ 
  		deparseTargetList(buf, root, foreignrel->relid, rel, false,
  						  fpinfo->attrs_used, false, retrieved_attrs);
  		heap_close(rel, NoLock);
***************
*** 855,860 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
--- 867,881 ----
  	deparseFromExprForRel(buf, root, foreignrel,
  						  (foreignrel->reloptkind == RELOPT_JOINREL),
  						  context->params_list);
+ 
+ 	/*
+ 	 * Construct WHERE clause
+ 	 */
+ 	if (remote_conds)
+ 	{
+ 		appendStringInfoString(buf, " WHERE ");
+ 		appendConditions(remote_conds, context);
+ 	}
  }
  
  /*
***************
*** 965,975 **** deparseLockingClause(deparse_expr_cxt *context)
--- 986,1005 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->foreignrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it has already appeared in a lower subquery
+ 		 * because in that case we considered locking for the relation if
+ 		 * need at the time of deparsing the subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subqueried_relids))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1138,1150 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseVar(var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
--- 1168,1181 ----
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseExpr((Expr *) var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
+ 	/* Don't generate bad syntax if no columns */
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
***************
*** 1152,1191 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  /*
   * Construct FROM clause for given relation
   *
!  * 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)
  	{
! 		RelOptInfo *rel_o = fpinfo->outerrel;
! 		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
! 		 *
! 		 * ((outer relation) <join type> (inner relation) ON (joinclauses))
! 		 */
! 		appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
! 					   get_jointype_name(fpinfo->jointype), join_sql_i.data);
  
! 		/* Append join clause; (TRUE) if no join clause */
  		if (fpinfo->joinclauses)
  		{
  			deparse_expr_cxt context;
--- 1183,1223 ----
  /*
   * Construct FROM clause for given relation
   *
!  * For a join relation the clause of the following form is appended to buf:
!  * ((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,
! 					  List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
! 		/* Begin the FROM clause entry */
! 		appendStringInfoChar(buf, '(');
  
  		/* Deparse outer relation */
! 		deparseRangeTblRef(buf, root,
! 						   fpinfo->outerrel,
! 						   fpinfo->outer_is_subquery,
! 						   params_list);
  
! 		/* Append join type */
! 		appendStringInfo(buf, " %s JOIN ",
! 						 get_jointype_name(fpinfo->jointype));
  
! 		/* Deparse inner relation */
! 		deparseRangeTblRef(buf, root,
! 						   fpinfo->innerrel,
! 						   fpinfo->inner_is_subquery,
! 						   params_list);
  
! 		/* Append join conditions */
! 		appendStringInfoString(buf, " ON ");
  		if (fpinfo->joinclauses)
  		{
  			deparse_expr_cxt context;
***************
*** 1195,1209 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  			context.root = root;
  			context.params_list = params_list;
  
! 			appendStringInfo(buf, "(");
  			appendConditions(fpinfo->joinclauses, &context);
! 			appendStringInfo(buf, ")");
  		}
  		else
  			appendStringInfoString(buf, "(TRUE)");
  
! 		/* End the FROM clause entry. */
! 		appendStringInfo(buf, ")");
  	}
  	else
  	{
--- 1227,1244 ----
  			context.root = root;
  			context.params_list = params_list;
  
! 			appendStringInfoChar(buf, '(');
  			appendConditions(fpinfo->joinclauses, &context);
! 			appendStringInfoChar(buf, ')');
  		}
  		else
+ 		{
+ 			/* No join conditions; add "(TRUE)" */
  			appendStringInfoString(buf, "(TRUE)");
+ 		}
  
! 		/* End the FROM clause entry */
! 		appendStringInfoChar(buf, ')');
  	}
  	else
  	{
***************
*** 1222,1228 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (use_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
--- 1257,1263 ----
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (add_rel_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
***************
*** 1230,1235 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1265,1412 ----
  }
  
  /*
+  * Append operand relation of foreign join to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool is_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	if (is_subquery)
+ 	{
+ 		List	   *tlist;
+ 		List	   *retrieved_attrs;
+ 		deparse_expr_cxt context;
+ 
+ 		context.buf = buf;
+ 		context.root = root;
+ 		context.foreignrel = foreignrel;
+ 		context.params_list = params_list;
+ 
+ 		tlist = build_tlist_to_deparse(foreignrel);
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectSql(tlist,
+ 						 fpinfo->remote_conds,
+ 						 &retrieved_attrs,
+ 						 &context);
+ 		deparseLockingClause(&context);
+ 		appendStringInfoChar(buf, ')');
+ 		appendSubselectAlias(&context);
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Add a subselect alias to a subquery-in-FROM.
+  */
+ static void
+ appendSubselectAlias(deparse_expr_cxt *context)
+ {
+ 	StringInfo	buf = context->buf;
+ 	RelOptInfo *foreignrel = context->foreignrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			num_columns = list_length(foreignrel->reltarget->exprs);
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, fpinfo->relation_index);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= num_columns; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get info about the subselect alias to given expression.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively.
+  */
+ static void
+ getSubselectAliasInfo(Expr *node, int *tabno, int *colno,
+ 					  RelOptInfo *foreignrel)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 	/* shouldn't get here */
+ 	elog(ERROR, "found expression not expected in subquery output");
+ }
+ 
+ /*
+  * Returns true if given expression is an output column of a subquery-in-FROM.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively, in that case.
+  */
+ static bool
+ isSubqueryExpr(Expr *node, int *tabno, int *colno, RelOptInfo *foreignrel)
+ {
+ 	Var		   *var = (Var *) node;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!fpinfo->subqueried_relids)
+ 		return false;
+ 
+ 	Assert(IsA(var, Var));
+ 
+ 	if (bms_is_member(var->varno, outerrel->relids))
+ 	{
+ 		if (fpinfo->outer_is_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, tabno, colno, outerrel);
+ 			return true;
+ 		}
+ 		if (isSubqueryExpr(node, tabno, colno, outerrel))
+ 			return true;
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(var->varno, innerrel->relids));
+ 
+ 		if (fpinfo->inner_is_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, tabno, colno, innerrel);
+ 			return true;
+ 		}
+ 		if (isSubqueryExpr(node, tabno, colno, innerrel))
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 1811,1816 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 1988,2011 ----
  	if (node == NULL)
  		return;
  
+ 	/*
+ 	 * If the given expression is an output column of a subquery-in-FROM,
+ 	 * deparse the alias to the expression instead.
+ 	 */
+ 	if (IsA(node, Var))
+ 	{
+ 		int			tabno;
+ 		int			colno;
+ 
+ 		if (isSubqueryExpr(node, &tabno, &colno, context->foreignrel))
+ 		{
+ 			appendStringInfo(context->buf, "%s%d.%s%d",
+ 							 SS_TAB_ALIAS_PREFIX, tabno,
+ 							 SS_COL_ALIAS_PREFIX, colno);
+ 			return;
+ 		}
+ 	}
+ 
  	switch (nodeTag(node))
  	{
  		case T_Var:
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 456,463 **** SELECT t1.c1, t2."C 1" FROM ft2 t1 LEFT JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1"
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                        QUERY PLAN                                                                        
! ---------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
--- 456,463 ----
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
***************
*** 466,472 **** EXPLAIN (VERBOSE, COSTS OFF)
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (((r2."C 1" = r3."C 1")))) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
--- 466,472 ----
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (TRUE)) WHERE ((r2."C 1" = r3."C 1")) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
***************
*** 979,992 **** ANALYZE ft5;
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                         QUERY PLAN                                                                                        
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 979,992 ----
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                              QUERY PLAN                                                                                              
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1007,1014 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                             QUERY PLAN                                                                                             
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
--- 1007,1014 ----
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                                       QUERY PLAN                                                                                                       
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
***************
*** 1017,1023 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) INNER JOIN "S 1"."T 3" r4 ON (((r1."C 1" = r4.c1))))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
--- 1017,1023 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) INNER JOIN "S 1"."T 3" r4 ON (TRUE)) WHERE ((r1."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
***************
*** 1223,1245 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1223,1235 ----
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1257,1270 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                            QUERY PLAN                                                                                                                                            
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
--- 1247,1260 ----
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                     QUERY PLAN                                                                                                                                                                    
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT s6.c1, s6.c2, r4.c1 FROM ((SELECT r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))) s6(c1, c2) FULL JOIN "S 1"."T 3" r4 ON (((s6.c2 = r4.c1)))) ORDER BY s6.c1 ASC NULLS LAST, s6.c2 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 1282,1287 **** SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 a
--- 1272,1315 ----
      |    | 16
  (10 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+                                                                                                                         QUERY PLAN                                                                                                                         
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: t1.*, t1.c1, t2.c1, t3.c1
+    ->  Sort
+          Output: t1.*, t1.c1, t2.c1, t3.c1
+          Sort Key: t1.c1, t2.c1, t3.c1
+          ->  Hash Full Join
+                Output: t1.*, t1.c1, t2.c1, t3.c1
+                Hash Cond: (t2.c1 = t3.c1)
+                ->  Foreign Scan
+                      Output: t1.*, t1.c1, t2.c1
+                      Relations: (public.ft4 t1) INNER JOIN (public.ft5 t2)
+                      Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))
+                ->  Hash
+                      Output: t3.c1
+                      ->  Foreign Scan on public.ft4 t3
+                            Output: t3.c1
+                            Remote SQL: SELECT c1 FROM "S 1"."T 3"
+ (17 rows)
+ 
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+        t1       | c1 | c1 | c1 
+ ----------------+----+----+----
+  (52,53,AAA052) | 52 | 51 |   
+  (58,59,AAA058) | 58 | 57 |   
+                 |    |    |  2
+                 |    |    |  4
+                 |    |    |  6
+                 |    |    |  8
+                 |    |    | 10
+                 |    |    | 12
+                 |    |    | 14
+                 |    |    | 16
+ (10 rows)
+ 
  -- full outer join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 1453,1466 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) LEFT
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                      QUERY PLAN                                                                                      
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ON (((r2."C 1" = r4.c1))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
--- 1481,1494 ----
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) ON (((r2."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 1513,1520 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1541,1548 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1522,1528 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1550,1556 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1557,1564 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1585,1592 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1566,1572 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1594,1600 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1602,1609 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                               
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1630,1637 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                     
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1611,1617 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1639,1645 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1646,1653 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1674,1681 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                             QUERY PLAN                                                                                                                                                                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1655,1661 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1683,1689 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1691,1705 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                              QUERY PLAN                                                              
! -------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
--- 1719,1733 ----
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                                    QUERY PLAN                                                                   
! ------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
***************
*** 1725,1738 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1753,1766 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                          QUERY PLAN                                                                                                                                                                                                         
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 1955,1962 **** SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                       QUERY PLAN                                                                       
! -------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
--- 1983,1990 ----
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                             QUERY PLAN                                                                            
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
***************
*** 1966,1972 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 1994,2000 ----
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1987,1994 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                      QUERY PLAN                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
--- 2015,2022 ----
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                           QUERY PLAN                                                                           
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
***************
*** 2004,2014 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
--- 2032,2042 ----
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
***************
*** 2029,2036 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
--- 2057,2064 ----
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                                   QUERY PLAN                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
***************
*** 2043,2049 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
--- 2071,2077 ----
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
***************
*** 2095,2102 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
  -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                     QUERY PLAN                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
     Join Filter: (ft4.c1 = ft1.c1)
--- 2123,2130 ----
  -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
     Join Filter: (ft4.c1 = ft1.c1)
***************
*** 2108,2114 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
           ->  Foreign Scan
                 Output: ft1.c1, ft2.c1, 13
                 Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
  (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
--- 2136,2142 ----
           ->  Foreign Scan
                 Output: ft1.c1, ft2.c1, 13
                 Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12)) ORDER BY r4."C 1" ASC NULLS LAST
  (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
***************
*** 2123,2134 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                 QUERY PLAN                                                                                                                                 
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2151,2162 ----
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                       QUERY PLAN                                                                                                                                      
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 2288,2299 **** DROP ROLE regress_view_owner;
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                           QUERY PLAN                                                          
! ------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = 2)) AND ((r1."C 1" = 1))))
  (4 rows)
  
  EXECUTE st1(1, 1);
--- 2316,2327 ----
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                                QUERY PLAN                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r2."C 1" = 2)) AND ((r1."C 1" = 1))
  (4 rows)
  
  EXECUTE st1(1, 1);
***************
*** 2917,2930 **** 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)
--- 2945,2958 ----
  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 (TRUE)) WHERE ((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)
***************
*** 3060,3073 **** 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
!                                                                                                                               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)
--- 3088,3101 ----
  
  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 (TRUE)) WHERE ((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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 403,408 **** static bool foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel,
--- 403,409 ----
  static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
  								 RelOptInfo *rel);
  static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+ static bool reltarget_has_non_vars(RelOptInfo *foreignrel);
  static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path);
  
***************
*** 655,660 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 656,669 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Set the subquery information */
+ 	fpinfo->outer_is_subquery = false;
+ 	fpinfo->inner_is_subquery = false;
+ 	fpinfo->subqueried_relids = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 3947,3952 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 3956,3962 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	Relids		relids;
  
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
***************
*** 4011,4020 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 * needs to be evaluated *at the top* of this join tree is OK, because we
  	 * can do that locally after fetching the results from the remote side.
  	 */
  	foreach(lc, root->placeholder_list)
  	{
  		PlaceHolderInfo *phinfo = lfirst(lc);
- 		Relids		relids = joinrel->relids;
  
  		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
  			bms_nonempty_difference(relids, phinfo->ph_eval_at))
--- 4021,4030 ----
  	 * needs to be evaluated *at the top* of this join tree is OK, because we
  	 * can do that locally after fetching the results from the remote side.
  	 */
+ 	relids = joinrel->relids;
  	foreach(lc, root->placeholder_list)
  	{
  		PlaceHolderInfo *phinfo = lfirst(lc);
  
  		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
  			bms_nonempty_difference(relids, phinfo->ph_eval_at))
***************
*** 4049,4056 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4059,4065 ----
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4061,4068 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4070,4076 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4091,4097 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
  				return false;
  			break;
  
--- 4099,4113 ----
  			break;
  
  		case JOIN_FULL:
! 			/*
! 			 * We can't do anything here, and if there are any non-Vars in the
! 			 * outerrel/innerrel's reltarget, give up pushing down this join
! 			 * because we currently don't support deparsing such a relation as
! 			 * a subquery.
! 			 */
! 			if (reltarget_has_non_vars(outerrel))
! 				return false;
! 			if (reltarget_has_non_vars(innerrel))
  				return false;
  			break;
  
***************
*** 4100,4117 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			elog(ERROR, "unsupported join type %d", jointype);
  	}
  
- 	/*
- 	 * For an inner join, all restrictions can be treated alike. Treating the
- 	 * pushed down conditions as join conditions allows a top level full outer
- 	 * join to be deparsed without requiring subqueries.
- 	 */
- 	if (jointype == JOIN_INNER)
- 	{
- 		Assert(!fpinfo->joinclauses);
- 		fpinfo->joinclauses = fpinfo->remote_conds;
- 		fpinfo->remote_conds = NIL;
- 	}
- 
  	/* Mark that this join can be pushed down safely */
  	fpinfo->pushdown_safe = true;
  
--- 4116,4121 ----
***************
*** 4173,4181 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4177,4249 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the subquery information
+ 	 *
+ 	 * If the relation performs a full outer join, then the input rel with
+ 	 * non-NIL remote_conds needs to be deparsed as a subquery.
+ 	 */
+ 	relids = NULL;
+ 	if (jointype == JOIN_FULL && fpinfo_o->remote_conds)
+ 	{
+ 		fpinfo->outer_is_subquery = true;
+ 		relids = bms_add_members(relids, outerrel->relids);
+ 	}
+ 	else
+ 	{
+ 		fpinfo->outer_is_subquery = false;
+ 		relids = bms_add_members(relids, fpinfo_o->subqueried_relids);
+ 	}
+ 	if (jointype == JOIN_FULL && fpinfo_i->remote_conds)
+ 	{
+ 		fpinfo->inner_is_subquery = true;
+ 		relids = bms_add_members(relids, innerrel->relids);
+ 	}
+ 	else
+ 	{
+ 		fpinfo->inner_is_subquery = false;
+ 		relids = bms_add_members(relids, fpinfo_i->subqueried_relids);
+ 	}
+ 	fpinfo->subqueried_relids = relids;
+ 
+ 	/*
+ 	 * Set the relation index
+ 	 *
+ 	 * This is defined as the position of this joinrel in the join_rel_list
+ 	 * list plus the length of the rtable list.  Note that this joinrel is
+ 	 * appended to the end of the list at the time we are called, so we can
+ 	 * get the position by list_length.
+ 	 */
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
+ /*
+  * Detect whether there are whole-row Vars or system columns other than ctid
+  * and oid in the given relation's reltarget.
+  *
+  * Note: currently deparseExplicitTargetList can't properly handle such Vars.
+  */
+ static bool
+ reltarget_has_non_vars(RelOptInfo *foreignrel)
+ {
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		Assert(IsA(var, Var));
+ 		if (var->varattno <= 0 &&
+ 			var->varattno != SelfItemPointerAttributeNumber &&
+ 			var->varattno != ObjectIdAttributeNumber)
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
  static void
  add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path)
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 87,97 **** typedef struct PgFdwRelationInfo
--- 87,108 ----
  	 */
  	StringInfo	relation_name;
  
+ 	/*
+ 	 * Index of the relation.  It is used for creating a subselect alias when
+ 	 * deparsing the relation as a subquery.
+ 	 */
+ 	int			relation_index;
+ 
  	/* Join information */
  	RelOptInfo *outerrel;
  	RelOptInfo *innerrel;
  	JoinType	jointype;
  	List	   *joinclauses;
+ 
+ 	/* Subquery information */
+ 	bool		outer_is_subquery;
+ 	bool		inner_is_subquery;
+ 	Relids		subqueried_relids;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 398,403 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 398,406 ----
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  -- full outer join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
#23Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#22)
2 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/09/29 21:12, Etsuro Fujita wrote:

Please find attached an updated version of the patch.

Here is a new version (V6) of the patch, which is basically the same as
the previous patch, but I slightly modified that patch. Another patch I
am attaching is an updated patch for pushing down PHVs to the remote,
which is created on top of the V6 patch.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-more-full-join-pushdown-v6.patchbinary/octet-stream; name=postgres-fdw-more-full-join-pushdown-v6.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 102,107 **** typedef struct deparse_expr_cxt
--- 102,109 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 152,164 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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);
  
  
  /*
--- 154,175 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  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,
! 					  List **params_list);
! static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
! 				   bool make_subquery, List **params_list);
! static void appendSubselectAlias(deparse_expr_cxt *context);
! static void getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
! 					  int *tabno, int *colno);
! static bool isSubqueryExpr(Expr *node, RelOptInfo *foreignrel, int *tabno, int *colno);
  
  
  /*
***************
*** 780,796 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  	context.foreignrel = rel;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause and FROM clause */
! 	deparseSelectSql(tlist, retrieved_attrs, &context);
! 
! 	/*
! 	 * Construct WHERE clause
! 	 */
! 	if (remote_conds)
! 	{
! 		appendStringInfo(buf, " WHERE ");
! 		appendConditions(remote_conds, &context);
! 	}
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
--- 791,798 ----
  	context.foreignrel = rel;
  	context.params_list = params_list;
  
! 	/* Construct SELECT clause, FROM clause, and WHERE clause */
! 	deparseSelectSql(tlist, remote_conds, retrieved_attrs, &context);
  
  	/* Add ORDER BY clause if we found any useful pathkeys */
  	if (pathkeys)
***************
*** 803,809 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ....".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
--- 805,811 ----
  /*
   * Construct a simple SELECT statement that retrieves desired columns
   * of the specified foreign table, and append it to "buf".  The output
!  * contains just "SELECT ... FROM ... WHERE ...".
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
***************
*** 812,818 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
--- 814,823 ----
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist,
! 				 List *remote_conds,
! 				 List **retrieved_attrs,
! 				 deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
***************
*** 824,832 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
--- 829,842 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	/*
! 	 * Note: tlist for a base relation might be non-NIL.  For example, if the
! 	 * base relation is an operand of a foreign join performing a full outer
! 	 * join and has non-NIL remote_conds, the base relation will be deparsed
! 	 * as a subquery, so the tlist for the base relation could be non-NIL.
! 	 */
! 	if (tlist != NIL)
  	{
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
***************
*** 843,848 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
--- 853,860 ----
  		 */
  		Relation	rel = heap_open(rte->relid, NoLock);
  
+ 		Assert(foreignrel->reloptkind != RELOPT_JOINREL);
+ 
  		deparseTargetList(buf, root, foreignrel->relid, rel, false,
  						  fpinfo->attrs_used, false, retrieved_attrs);
  		heap_close(rel, NoLock);
***************
*** 855,860 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
--- 867,881 ----
  	deparseFromExprForRel(buf, root, foreignrel,
  						  (foreignrel->reloptkind == RELOPT_JOINREL),
  						  context->params_list);
+ 
+ 	/*
+ 	 * Construct WHERE clause
+ 	 */
+ 	if (remote_conds)
+ 	{
+ 		appendStringInfoString(buf, " WHERE ");
+ 		appendConditions(remote_conds, context);
+ 	}
  }
  
  /*
***************
*** 965,975 **** deparseLockingClause(deparse_expr_cxt *context)
--- 986,1005 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->foreignrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery, because in that
+ 		 * case we would have already considered locking for it while
+ 		 * deparsing the lower subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1138,1150 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseVar(var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
--- 1168,1181 ----
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
! 		deparseExpr((Expr *) var, context);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
  
  		i++;
  	}
  
+ 	/* Don't generate bad syntax if no columns */
  	if (i == 0)
  		appendStringInfoString(buf, "NULL");
  }
***************
*** 1152,1191 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  /*
   * Construct FROM clause for given relation
   *
!  * 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)
  	{
! 		RelOptInfo *rel_o = fpinfo->outerrel;
! 		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
! 		 *
! 		 * ((outer relation) <join type> (inner relation) ON (joinclauses))
! 		 */
! 		appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
! 					   get_jointype_name(fpinfo->jointype), join_sql_i.data);
  
! 		/* Append join clause; (TRUE) if no join clause */
  		if (fpinfo->joinclauses)
  		{
  			deparse_expr_cxt context;
--- 1183,1223 ----
  /*
   * Construct FROM clause for given relation
   *
!  * For a join relation the clause of the following form is appended to buf:
!  * ((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,
! 					  List **params_list)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
! 		/* Begin the FROM clause entry */
! 		appendStringInfoChar(buf, '(');
  
  		/* Deparse outer relation */
! 		deparseRangeTblRef(buf, root,
! 						   fpinfo->outerrel,
! 						   fpinfo->make_outerrel_subquery,
! 						   params_list);
  
! 		/* Append join type */
! 		appendStringInfo(buf, " %s JOIN ",
! 						 get_jointype_name(fpinfo->jointype));
  
! 		/* Deparse inner relation */
! 		deparseRangeTblRef(buf, root,
! 						   fpinfo->innerrel,
! 						   fpinfo->make_innerrel_subquery,
! 						   params_list);
  
! 		/* Append join conditions */
! 		appendStringInfoString(buf, " ON ");
  		if (fpinfo->joinclauses)
  		{
  			deparse_expr_cxt context;
***************
*** 1195,1209 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  			context.root = root;
  			context.params_list = params_list;
  
! 			appendStringInfo(buf, "(");
  			appendConditions(fpinfo->joinclauses, &context);
! 			appendStringInfo(buf, ")");
  		}
  		else
  			appendStringInfoString(buf, "(TRUE)");
  
! 		/* End the FROM clause entry. */
! 		appendStringInfo(buf, ")");
  	}
  	else
  	{
--- 1227,1244 ----
  			context.root = root;
  			context.params_list = params_list;
  
! 			appendStringInfoChar(buf, '(');
  			appendConditions(fpinfo->joinclauses, &context);
! 			appendStringInfoChar(buf, ')');
  		}
  		else
+ 		{
+ 			/* No join conditions; add "(TRUE)" */
  			appendStringInfoString(buf, "(TRUE)");
+ 		}
  
! 		/* End the FROM clause entry */
! 		appendStringInfoChar(buf, ')');
  	}
  	else
  	{
***************
*** 1222,1228 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (use_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
--- 1257,1263 ----
  		 * pulled up subqueries in the query being built for a pushed down
  		 * join.
  		 */
! 		if (add_rel_alias)
  			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
  
  		heap_close(rel, NoLock);
***************
*** 1230,1235 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1265,1421 ----
  }
  
  /*
+  * Append operand relation of foreign join to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool make_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	if (make_subquery)
+ 	{
+ 		List	   *tlist;
+ 		List	   *retrieved_attrs;
+ 		deparse_expr_cxt context;
+ 
+ 		context.buf = buf;
+ 		context.root = root;
+ 		context.foreignrel = foreignrel;
+ 		context.params_list = params_list;
+ 
+ 		tlist = build_tlist_to_deparse(foreignrel);
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectSql(tlist,
+ 						 fpinfo->remote_conds,
+ 						 &retrieved_attrs,
+ 						 &context);
+ 		deparseLockingClause(&context);
+ 		appendStringInfoChar(buf, ')');
+ 		appendSubselectAlias(&context);
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Add a subselect alias to a subquery-in-FROM.
+  */
+ static void
+ appendSubselectAlias(deparse_expr_cxt *context)
+ {
+ 	StringInfo	buf = context->buf;
+ 	RelOptInfo *foreignrel = context->foreignrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			num_columns = list_length(foreignrel->reltarget->exprs);
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, fpinfo->relation_index);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= num_columns; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get info about the subselect alias to given expression.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively.
+  */
+ static void
+ getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
+ 					  int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 	/* shouldn't get here */
+ 	elog(ERROR, "unexpected expression in subquery output");
+ }
+ 
+ /*
+  * Returns true if given expression is an output column of a subquery-in-FROM.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively, in that case.
+  */
+ static bool
+ isSubqueryExpr(Expr *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+ {
+ 	Var		   *var = (Var *) node;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!fpinfo->subquery_rels)
+ 		return false;
+ 
+ 	Assert(IsA(var, Var));
+ 
+ 	if (bms_is_member(var->varno, outerrel->relids))
+ 	{
+ 		/*
+ 		 * If outer relation is deparsed as a subqeury, the given expression
+ 		 * will be an output column of the subquery; get the subselect alias
+ 		 * info for the given expression.
+ 		 */
+ 		if (fpinfo->make_outerrel_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, outerrel, tabno, colno);
+ 			return true;
+ 		}
+ 		/* Otherwise, recurse into outer relation */
+ 		if (isSubqueryExpr(node, outerrel, tabno, colno))
+ 			return true;
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(var->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * Likewise for inner relation
+ 		 */
+ 		if (fpinfo->make_innerrel_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, innerrel, tabno, colno);
+ 			return true;
+ 		}
+ 		if (isSubqueryExpr(node, innerrel, tabno, colno))
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 1811,1816 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 1997,2020 ----
  	if (node == NULL)
  		return;
  
+ 	/*
+ 	 * If the given expression is an output column of a subquery-in-FROM,
+ 	 * deparse the alias to the expression instead.
+ 	 */
+ 	if (IsA(node, Var))
+ 	{
+ 		int			tabno;
+ 		int			colno;
+ 
+ 		if (isSubqueryExpr(node, context->foreignrel, &tabno, &colno))
+ 		{
+ 			appendStringInfo(context->buf, "%s%d.%s%d",
+ 							 SS_TAB_ALIAS_PREFIX, tabno,
+ 							 SS_COL_ALIAS_PREFIX, colno);
+ 			return;
+ 		}
+ 	}
+ 
  	switch (nodeTag(node))
  	{
  		case T_Var:
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 456,463 **** SELECT t1.c1, t2."C 1" FROM ft2 t1 LEFT JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1"
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                        QUERY PLAN                                                                        
! ---------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
--- 456,463 ----
  -- foreign join so that the local table can be joined using merge join strategy.
  EXPLAIN (VERBOSE, COSTS OFF)
  	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Merge Right Join
***************
*** 466,472 **** EXPLAIN (VERBOSE, COSTS OFF)
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (((r2."C 1" = r3."C 1")))) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
--- 466,472 ----
           ->  Foreign Scan
                 Output: t3.c1
                 Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (TRUE)) WHERE ((r2."C 1" = r3."C 1")) ORDER BY r2."C 1" ASC NULLS LAST
           ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                 Output: t1."C 1"
  (11 rows)
***************
*** 979,992 **** ANALYZE ft5;
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                         QUERY PLAN                                                                                        
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 979,992 ----
  -- join two tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                              QUERY PLAN                                                                                              
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1007,1014 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                             QUERY PLAN                                                                                             
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
--- 1007,1014 ----
  -- join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
!                                                                                                       QUERY PLAN                                                                                                       
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3, t1.c3
     ->  Sort
***************
*** 1017,1023 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) INNER JOIN "S 1"."T 3" r4 ON (((r1."C 1" = r4.c1))))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
--- 1017,1023 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c2, t3.c3, t1.c3
                 Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) INNER JOIN "S 1"."T 3" r4 ON (TRUE)) WHERE ((r1."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))
  (9 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
***************
*** 1223,1245 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1223,1235 ----
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1257,1270 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                            QUERY PLAN                                                                                                                                            
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
--- 1247,1260 ----
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                     QUERY PLAN                                                                                                                                                                    
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT s6.c1, s6.c2, r4.c1 FROM ((SELECT r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))) s6(c1, c2) FULL JOIN "S 1"."T 3" r4 ON (((s6.c2 = r4.c1)))) ORDER BY s6.c1 ASC NULLS LAST, s6.c2 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 1282,1287 **** SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 a
--- 1272,1315 ----
      |    | 16
  (10 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+                                                                                                                         QUERY PLAN                                                                                                                         
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: t1.*, t1.c1, t2.c1, t3.c1
+    ->  Sort
+          Output: t1.*, t1.c1, t2.c1, t3.c1
+          Sort Key: t1.c1, t2.c1, t3.c1
+          ->  Hash Full Join
+                Output: t1.*, t1.c1, t2.c1, t3.c1
+                Hash Cond: (t2.c1 = t3.c1)
+                ->  Foreign Scan
+                      Output: t1.*, t1.c1, t2.c1
+                      Relations: (public.ft4 t1) INNER JOIN (public.ft5 t2)
+                      Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))
+                ->  Hash
+                      Output: t3.c1
+                      ->  Foreign Scan on public.ft4 t3
+                            Output: t3.c1
+                            Remote SQL: SELECT c1 FROM "S 1"."T 3"
+ (17 rows)
+ 
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+        t1       | c1 | c1 | c1 
+ ----------------+----+----+----
+  (52,53,AAA052) | 52 | 51 |   
+  (58,59,AAA058) | 58 | 57 |   
+                 |    |    |  2
+                 |    |    |  4
+                 |    |    |  6
+                 |    |    |  8
+                 |    |    | 10
+                 |    |    | 12
+                 |    |    | 14
+                 |    |    | 16
+ (10 rows)
+ 
  -- full outer join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 1453,1466 **** SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) LEFT
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                      QUERY PLAN                                                                                      
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ON (((r2."C 1" = r4.c1))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
--- 1481,1494 ----
  -- left outer join + right outer join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c2, t3.c3
     ->  Foreign Scan
           Output: t1.c1, t2.c2, t3.c3
           Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
!          Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) ON (((r2."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))))
  (6 rows)
  
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 1513,1520 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1541,1548 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1522,1528 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1550,1556 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1557,1564 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1585,1592 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1566,1572 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1594,1600 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1602,1609 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                               
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1630,1637 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                     
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1611,1617 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1639,1645 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1646,1653 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1674,1681 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                             QUERY PLAN                                                                                                                                                                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1655,1661 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1683,1689 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1691,1705 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                              QUERY PLAN                                                              
! -------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
--- 1719,1733 ----
  -- join in CTE
  EXPLAIN (VERBOSE, COSTS OFF)
  WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
!                                                                    QUERY PLAN                                                                   
! ------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t.c1_1, t.c2_1, t.c1_3
     CTE t
       ->  Foreign Scan
             Output: t1.c1, t1.c3, t2.c1
             Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!            Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
     ->  Sort
           Output: t.c1_1, t.c2_1, t.c1_3
           Sort Key: t.c1_3, t.c1_1
***************
*** 1725,1738 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1753,1766 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                          QUERY PLAN                                                                                                                                                                                                         
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 1955,1962 **** SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                       QUERY PLAN                                                                       
! -------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
--- 1983,1990 ----
  -- into one of the joining sides.
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                             QUERY PLAN                                                                            
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3
     ->  Sort
***************
*** 1966,1972 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
--- 1994,2000 ----
                 Output: t1.c1, t2.c1, t1.c3
                 Filter: (t1.c8 = t2.c8)
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (10 rows)
  
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
***************
*** 1987,1994 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                      QUERY PLAN                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
--- 2015,2022 ----
  -- Aggregate after UNION, for testing setrefs
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
!                                                                           QUERY PLAN                                                                           
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, (avg((t1.c1 + t2.c1)))
     ->  Sort
***************
*** 2004,2014 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
--- 2032,2042 ----
                             ->  Foreign Scan
                                   Output: t1.c1, t2.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
                             ->  Foreign Scan
                                   Output: t1_1.c1, t2_1.c1
                                   Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                                  Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
  (20 rows)
  
  SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
***************
*** 2029,2036 **** SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                              QUERY PLAN                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
--- 2057,2064 ----
  -- join with lateral reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
!                                                                                   QUERY PLAN                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1."C 1"
     ->  Nested Loop
***************
*** 2043,2049 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
--- 2071,2077 ----
                 ->  Foreign Scan
                       Output: t2.c1, t3.c1
                       Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
!                      Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))
  (13 rows)
  
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
***************
*** 2095,2102 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
  -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                     QUERY PLAN                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
     Join Filter: (ft4.c1 = ft1.c1)
--- 2123,2130 ----
  -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
     Join Filter: (ft4.c1 = ft1.c1)
***************
*** 2108,2114 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
           ->  Foreign Scan
                 Output: ft1.c1, ft2.c1, 13
                 Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
  (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
--- 2136,2142 ----
           ->  Foreign Scan
                 Output: ft1.c1, ft2.c1, 13
                 Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12)) ORDER BY r4."C 1" ASC NULLS LAST
  (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
***************
*** 2123,2134 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                 QUERY PLAN                                                                                                                                 
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2151,2162 ----
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                       QUERY PLAN                                                                                                                                      
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 2288,2299 **** DROP ROLE regress_view_owner;
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                           QUERY PLAN                                                          
! ------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = 2)) AND ((r1."C 1" = 1))))
  (4 rows)
  
  EXECUTE st1(1, 1);
--- 2316,2327 ----
  -- simple join
  PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
  EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
!                                                                QUERY PLAN                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: t1.c3, t2.c3
     Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!    Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r2."C 1" = 2)) AND ((r1."C 1" = 1))
  (4 rows)
  
  EXECUTE st1(1, 1);
***************
*** 2917,2930 **** 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)
--- 2945,2958 ----
  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 (TRUE)) WHERE ((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)
***************
*** 3060,3073 **** 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
!                                                                                                                               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)
--- 3088,3101 ----
  
  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 (TRUE)) WHERE ((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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 403,408 **** static bool foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel,
--- 403,409 ----
  static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
  								 RelOptInfo *rel);
  static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+ static bool reltarget_has_non_vars(RelOptInfo *foreignrel);
  static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path);
  
***************
*** 655,660 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 656,669 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Set the subquery information */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	fpinfo->subquery_rels = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 3947,3952 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 3956,3962 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	Relids		relids;
  
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
***************
*** 4011,4020 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 * needs to be evaluated *at the top* of this join tree is OK, because we
  	 * can do that locally after fetching the results from the remote side.
  	 */
  	foreach(lc, root->placeholder_list)
  	{
  		PlaceHolderInfo *phinfo = lfirst(lc);
- 		Relids		relids = joinrel->relids;
  
  		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
  			bms_nonempty_difference(relids, phinfo->ph_eval_at))
--- 4021,4030 ----
  	 * needs to be evaluated *at the top* of this join tree is OK, because we
  	 * can do that locally after fetching the results from the remote side.
  	 */
+ 	relids = joinrel->relids;
  	foreach(lc, root->placeholder_list)
  	{
  		PlaceHolderInfo *phinfo = lfirst(lc);
  
  		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
  			bms_nonempty_difference(relids, phinfo->ph_eval_at))
***************
*** 4049,4056 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4059,4065 ----
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4061,4068 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4070,4076 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4091,4097 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
  				return false;
  			break;
  
--- 4099,4112 ----
  			break;
  
  		case JOIN_FULL:
! 			/*
! 			 * We can't do anything here, and if there are any non-Vars in the
! 			 * outerrel/innerrel's reltarget, give up pushing down this join,
! 			 * because the deparsing logic can't support such a case currently.
! 			 */
! 			if (reltarget_has_non_vars(outerrel))
! 				return false;
! 			if (reltarget_has_non_vars(innerrel))
  				return false;
  			break;
  
***************
*** 4100,4117 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			elog(ERROR, "unsupported join type %d", jointype);
  	}
  
- 	/*
- 	 * For an inner join, all restrictions can be treated alike. Treating the
- 	 * pushed down conditions as join conditions allows a top level full outer
- 	 * join to be deparsed without requiring subqueries.
- 	 */
- 	if (jointype == JOIN_INNER)
- 	{
- 		Assert(!fpinfo->joinclauses);
- 		fpinfo->joinclauses = fpinfo->remote_conds;
- 		fpinfo->remote_conds = NIL;
- 	}
- 
  	/* Mark that this join can be pushed down safely */
  	fpinfo->pushdown_safe = true;
  
--- 4115,4120 ----
***************
*** 4173,4181 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4176,4245 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the subquery information.  If the relation performs a full outer
+ 	 * join and if the input relations have non-NIL remote_conds, the input
+ 	 * relations need to be deparsed as a subquery.
+ 	 */
+ 	relids = NULL;
+ 	if (jointype == JOIN_FULL && fpinfo_o->remote_conds)
+ 	{
+ 		fpinfo->make_outerrel_subquery = true;
+ 		relids = bms_add_members(relids, outerrel->relids);
+ 	}
+ 	else
+ 	{
+ 		fpinfo->make_outerrel_subquery = false;
+ 		relids = bms_add_members(relids, fpinfo_o->subquery_rels);
+ 	}
+ 	if (jointype == JOIN_FULL && fpinfo_i->remote_conds)
+ 	{
+ 		fpinfo->make_innerrel_subquery = true;
+ 		relids = bms_add_members(relids, innerrel->relids);
+ 	}
+ 	else
+ 	{
+ 		fpinfo->make_innerrel_subquery = false;
+ 		relids = bms_add_members(relids, fpinfo_i->subquery_rels);
+ 	}
+ 	fpinfo->subquery_rels = relids;
+ 
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
+ /*
+  * Detect whether there are whole-row Vars or system columns other than ctid
+  * and oid in the given relation's reltarget.
+  *
+  * Note: currently deparseExplicitTargetList can't properly handle such Vars.
+  */
+ static bool
+ reltarget_has_non_vars(RelOptInfo *foreignrel)
+ {
+ 	ListCell   *lc;
+ 
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 
+ 		Assert(IsA(var, Var));
+ 		if (var->varattno <= 0 &&
+ 			var->varattno != SelfItemPointerAttributeNumber &&
+ 			var->varattno != ObjectIdAttributeNumber)
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
  static void
  add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path)
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 92,97 **** typedef struct PgFdwRelationInfo
--- 92,110 ----
  	RelOptInfo *innerrel;
  	JoinType	jointype;
  	List	   *joinclauses;
+ 
+ 	/* Subquery information */
+ 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+ 										 * subquery? */
+ 	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+ 										 * subquery? */
+ 	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+ 
+ 	/*
+ 	 * Index of the relation.  It is used for creating a subselect alias when
+ 	 * deparsing the relation as a subquery.
+ 	 */
+ 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 398,403 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 398,406 ----
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+ SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  -- full outer join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
postgres-fdw-phv-pushdown-v2.patchbinary/octet-stream; name=postgres-fdw-phv-pushdown-v2.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 48,53 ****
--- 48,54 ----
  #include "nodes/nodeFuncs.h"
  #include "nodes/plannodes.h"
  #include "optimizer/clauses.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/prep.h"
  #include "optimizer/tlist.h"
  #include "optimizer/var.h"
***************
*** 150,155 **** static void deparseRelabelType(RelabelType *node, deparse_expr_cxt *context);
--- 151,157 ----
  static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context);
  static void deparseNullTest(NullTest *node, deparse_expr_cxt *context);
  static void deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context);
+ static void deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context);
  static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
***************
*** 169,175 **** static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *fo
  static void appendSubselectAlias(deparse_expr_cxt *context);
  static void getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
  					  int *tabno, int *colno);
! static bool isSubqueryExpr(Expr *node, RelOptInfo *foreignrel, int *tabno, int *colno);
  
  
  /*
--- 171,178 ----
  static void appendSubselectAlias(deparse_expr_cxt *context);
  static void getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
  					  int *tabno, int *colno);
! static bool isSubqueryExpr(Expr *node, PlannerInfo *root, RelOptInfo *foreignrel,
! 			   int *tabno, int *colno);
  
  
  /*
***************
*** 642,647 **** foreign_expr_walker(Node *node,
--- 645,669 ----
  				check_type = false;
  			}
  			break;
+ 		case T_PlaceHolderVar:
+ 			{
+ 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 				PlaceHolderInfo *phinfo = find_placeholder_info(glob_cxt->root,
+ 																phv, false);
+ 
+ 				/*
+ 				 * If the PHV's contained expression is computable on the
+ 				 * remote server, we consider the PHV safe to send.
+ 				 */
+ 				if (phv->phlevelsup == 0 &&
+ 					bms_is_subset(phinfo->ph_eval_at,
+ 								  glob_cxt->foreignrel->relids))
+ 					return foreign_expr_walker((Node *) phv->phexpr,
+ 											   glob_cxt, outer_cxt);
+ 				else
+ 					return false;
+ 			}
+ 			break;
  		default:
  
  			/*
***************
*** 734,754 **** deparse_type_name(Oid type_oid, int32 typemod)
   * foreign server for the given relation.
   */
  List *
! build_tlist_to_deparse(RelOptInfo *foreignrel)
  {
  	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
! 	 * We require columns specified in foreignrel->reltarget->exprs and those
! 	 * required for evaluating the local conditions.
  	 */
! 	tlist = add_to_flat_tlist(tlist,
! 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
! 									   PVC_RECURSE_PLACEHOLDERS));
! 	tlist = add_to_flat_tlist(tlist,
! 							  pull_var_clause((Node *) fpinfo->local_conds,
! 											  PVC_RECURSE_PLACEHOLDERS));
  
  	return tlist;
  }
--- 756,834 ----
   * foreign server for the given relation.
   */
  List *
! build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel)
  {
  	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	/*
! 	 * Fetch all expressions in foreignrel's reltarget if the
! 	 * reltarget_is_shippable flag is set TRUE.  Otherwise, fetch shipplable
! 	 * expressions in the reltarget plus expressions required for evaluating
! 	 * non-shippable expressions in the reltarget.
! 	 */
! 	if (fpinfo->reltarget_is_shippable)
! 		tlist = make_tlist_from_pathtarget(foreignrel->reltarget);
! 	else
! 	{
! 		List	   *exprs = NIL;
! 		ListCell   *lc;
! 
! 		/* Note: we have at least one non-shippable PHV in the reltarget. */
! 		foreach(lc, foreignrel->reltarget->exprs)
! 		{
! 			Node	   *node = (Node *) lfirst(lc);
! 
! 			if (IsA(node, Var))
! 				exprs = lappend(exprs, node);
! 			else if (IsA(node, PlaceHolderVar))
! 			{
! 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 				PlaceHolderInfo *phinfo = find_placeholder_info(root, phv,
! 																false);
! 
! 				if (bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->outerrel->relids) ||
! 					bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->innerrel->relids))
! 				{
! 					/*
! 					 * The PHV coming from an either input should be shippable.
! 					 */
! 					exprs = lappend(exprs, node);
! 				}
! 				else
! 				{
! 					/*
! 					 * The PHV might be shippable, but in any case just fetch
! 					 * expressions required for evaluating the PHV.
! 					 */
! 					List	   *exprs2 = pull_var_clause(node,
! 													PVC_INCLUDE_PLACEHOLDERS);
! 					ListCell   *lc2;
! 
! 					foreach(lc2, exprs2)
! 					{
! 						Node	   *node2 = (Node *) lfirst(lc2);
! 
! 						exprs = lappend(exprs, node2);
! 					}
! 				}
! 			}
! 			else
! 				elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
! 		}
! 
! 		tlist = add_to_flat_tlist(tlist, exprs);
! 	}
! 
! 	/*
! 	 * Fetch expressions required for evaluating local conditions, if any.
  	 */
! 	if (fpinfo->local_conds)
! 		tlist = add_to_flat_tlist(tlist,
! 								pull_var_clause((Node *) fpinfo->local_conds,
! 												PVC_INCLUDE_PLACEHOLDERS));
  
  	return tlist;
  }
***************
*** 1162,1170 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		/* We expect only Var nodes here */
! 		if (!IsA(var, Var))
! 			elog(ERROR, "non-Var not expected in target list");
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
--- 1242,1251 ----
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		/* Must be a Var or a PlaceHolderVar here */
! 		if (!IsA(var, Var) && !IsA(var, PlaceHolderVar))
! 			elog(ERROR, "unexpected node type in target list: %d",
! 			     (int) nodeTag(var));
  
  		if (i > 0)
  			appendStringInfoString(buf, ", ");
***************
*** 1274,1279 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1355,1361 ----
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  
  	Assert(fpinfo->local_conds == NIL);
+ 	Assert(fpinfo->reltarget_is_shippable);
  
  	if (make_subquery)
  	{
***************
*** 1286,1292 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		context.foreignrel = foreignrel;
  		context.params_list = params_list;
  
! 		tlist = build_tlist_to_deparse(foreignrel);
  		appendStringInfoChar(buf, '(');
  		deparseSelectSql(tlist,
  						 fpinfo->remote_conds,
--- 1368,1374 ----
  		context.foreignrel = foreignrel;
  		context.params_list = params_list;
  
! 		tlist = build_tlist_to_deparse(root, foreignrel);
  		appendStringInfoChar(buf, '(');
  		deparseSelectSql(tlist,
  						 fpinfo->remote_conds,
***************
*** 1366,1377 **** getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
   * respectively, in that case.
   */
  static bool
! isSubqueryExpr(Expr *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  {
- 	Var		   *var = (Var *) node;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
  	RelOptInfo *innerrel = fpinfo->innerrel;
  
  	if (foreignrel->reloptkind != RELOPT_JOINREL)
  		return false;
--- 1448,1460 ----
   * respectively, in that case.
   */
  static bool
! isSubqueryExpr(Expr *node, PlannerInfo *root, RelOptInfo *foreignrel,
! 			   int *tabno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
  	RelOptInfo *innerrel = fpinfo->innerrel;
+ 	bool		is_outer_var;
  
  	if (foreignrel->reloptkind != RELOPT_JOINREL)
  		return false;
***************
*** 1379,1387 **** isSubqueryExpr(Expr *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  	if (!fpinfo->subquery_rels)
  		return false;
  
! 	Assert(IsA(var, Var));
  
! 	if (bms_is_member(var->varno, outerrel->relids))
  	{
  		/*
  		 * If outer relation is deparsed as a subqeury, the given expression
--- 1462,1493 ----
  	if (!fpinfo->subquery_rels)
  		return false;
  
! 	if (IsA(node, Var))
! 	{
! 		Var		   *var = (Var *) node;
! 
! 		is_outer_var = bms_is_member(var->varno, outerrel->relids);
! 	}
! 	else if (IsA(node, PlaceHolderVar))
! 	{
! 		PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 		PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
! 		bool		is_inner_var;
! 
! 		is_outer_var = bms_is_subset(phinfo->ph_eval_at, outerrel->relids);
! 		is_inner_var = bms_is_subset(phinfo->ph_eval_at, innerrel->relids);
! 
! 		/*
! 		 * If the PHV can't be computed in either input, it will be computed
! 		 * in the current join level; it's not a subquery output column.
! 		 */
! 		if (!is_outer_var && !is_inner_var)
! 			return false;
! 	}
! 	else
! 		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
  
! 	if (is_outer_var)
  	{
  		/*
  		 * If outer relation is deparsed as a subqeury, the given expression
***************
*** 1394,1406 **** isSubqueryExpr(Expr *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  			return true;
  		}
  		/* Otherwise, recurse into outer relation */
! 		if (isSubqueryExpr(node, outerrel, tabno, colno))
  			return true;
  	}
  	else
  	{
- 		Assert(bms_is_member(var->varno, innerrel->relids));
- 
  		/*
  		 * Likewise for inner relation
  		 */
--- 1500,1510 ----
  			return true;
  		}
  		/* Otherwise, recurse into outer relation */
! 		if (isSubqueryExpr(node, root, outerrel, tabno, colno))
  			return true;
  	}
  	else
  	{
  		/*
  		 * Likewise for inner relation
  		 */
***************
*** 1409,1415 **** isSubqueryExpr(Expr *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  			getSubselectAliasInfo(node, innerrel, tabno, colno);
  			return true;
  		}
! 		if (isSubqueryExpr(node, innerrel, tabno, colno))
  			return true;
  	}
  	return false;
--- 1513,1519 ----
  			getSubselectAliasInfo(node, innerrel, tabno, colno);
  			return true;
  		}
! 		if (isSubqueryExpr(node, root, innerrel, tabno, colno))
  			return true;
  	}
  	return false;
***************
*** 1794,1802 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.  However, we must be
! 		 * careful; the table could be beneath an outer join, in which case it
! 		 * must go to NULL whenever the rest of the row does.
  		 */
  		Oid			fetchval = 0;
  
--- 1898,1904 ----
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.
  		 */
  		Oid			fetchval = 0;
  
***************
*** 1806,1819 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  			fetchval = rte->relid;
  		}
  
! 		if (qualify_col)
! 		{
! 			appendStringInfoString(buf, "CASE WHEN (");
! 			ADD_REL_QUALIFIER(buf, varno);
! 			appendStringInfo(buf, "*)::text IS NOT NULL THEN %u END", fetchval);
! 		}
! 		else
! 			appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
--- 1908,1914 ----
  			fetchval = rte->relid;
  		}
  
! 		appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
***************
*** 1844,1871 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  		attrs_used = bms_add_member(NULL,
  									0 - FirstLowInvalidHeapAttributeNumber);
  
- 		/*
- 		 * In case the whole-row reference is under an outer join then it has
- 		 * to go NULL whenever the rest of the row goes NULL. Deparsing a join
- 		 * query would always involve multiple relations, thus qualify_col
- 		 * would be true.
- 		 */
- 		if (qualify_col)
- 		{
- 			appendStringInfoString(buf, "CASE WHEN (");
- 			ADD_REL_QUALIFIER(buf, varno);
- 			appendStringInfo(buf, "*)::text IS NOT NULL THEN ");
- 		}
- 
  		appendStringInfoString(buf, "ROW(");
  		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
  						  &retrieved_attrs);
  		appendStringInfoString(buf, ")");
  
- 		/* Complete the CASE WHEN statement started above. */
- 		if (qualify_col)
- 			appendStringInfo(buf, " END");
- 
  		heap_close(rel, NoLock);
  		bms_free(attrs_used);
  	}
--- 1939,1949 ----
***************
*** 2001,2012 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
  	 * If the given expression is an output column of a subquery-in-FROM,
  	 * deparse the alias to the expression instead.
  	 */
! 	if (IsA(node, Var))
  	{
  		int			tabno;
  		int			colno;
  
! 		if (isSubqueryExpr(node, context->foreignrel, &tabno, &colno))
  		{
  			appendStringInfo(context->buf, "%s%d.%s%d",
  							 SS_TAB_ALIAS_PREFIX, tabno,
--- 2079,2093 ----
  	 * If the given expression is an output column of a subquery-in-FROM,
  	 * deparse the alias to the expression instead.
  	 */
! 	if (IsA(node, Var) || IsA(node, PlaceHolderVar))
  	{
  		int			tabno;
  		int			colno;
  
! 		if (isSubqueryExpr(node,
! 						   context->root,
! 						   context->foreignrel,
! 						   &tabno, &colno))
  		{
  			appendStringInfo(context->buf, "%s%d.%s%d",
  							 SS_TAB_ALIAS_PREFIX, tabno,
***************
*** 2053,2058 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 2134,2142 ----
  		case T_ArrayExpr:
  			deparseArrayExpr((ArrayExpr *) node, context);
  			break;
+ 		case T_PlaceHolderVar:
+ 			deparseExprInPlaceHolderVar((PlaceHolderVar *) node, context);
+ 			break;
  		default:
  			elog(ERROR, "unsupported expression type for deparse: %d",
  				 (int) nodeTag(node));
***************
*** 2624,2629 **** deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context)
--- 2708,2722 ----
  }
  
  /*
+  * Deparse a PlaceHolderVar's contained expression.
+  */
+ static void
+ deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context)
+ {
+ 	deparseExpr(node->phexpr, context);
+ }
+ 
+ /*
   * Print the representation of a parameter to be sent to the remote side.
   *
   * Note: we always label the Param's type explicitly rather than relying on
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1274,1299 **** SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 a
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                         QUERY PLAN                                                                                                                         
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.*, t1.c1, t2.c1, t3.c1
!    ->  Sort
           Output: t1.*, t1.c1, t2.c1, t3.c1
!          Sort Key: t1.c1, t2.c1, t3.c1
!          ->  Hash Full Join
!                Output: t1.*, t1.c1, t2.c1, t3.c1
!                Hash Cond: (t2.c1 = t3.c1)
!                ->  Foreign Scan
!                      Output: t1.*, t1.c1, t2.c1
!                      Relations: (public.ft4 t1) INNER JOIN (public.ft5 t2)
!                      Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))
!                ->  Hash
!                      Output: t3.c1
!                      ->  Foreign Scan on public.ft4 t3
!                            Output: t3.c1
!                            Remote SQL: SELECT c1 FROM "S 1"."T 3"
! (17 rows)
  
  SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
         t1       | c1 | c1 | c1 
--- 1274,1288 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                                                 QUERY PLAN                                                                                                                                                                                                
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.*, t1.c1, t2.c1, t3.c1
!    ->  Foreign Scan
           Output: t1.*, t1.c1, t2.c1, t3.c1
!          Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT s6.c1, s6.c2, s6.c3, r4.c1 FROM ((SELECT s1.c1, s1.c2, r2.c1 FROM ((SELECT ROW(c1, c2, c3), c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s1(c1, c2) INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((s1.c2 = (r2.c1 + 1)))) s6(c1, c2, c3) FULL JOIN "S 1"."T 3" r4 ON (((s6.c3 = r4.c1)))) ORDER BY s6.c2 ASC NULLS LAST, s6.c3 ASC NULLS LAST, r4.c1 ASC NULLS LAST
! (6 rows)
  
  SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
         t1       | c1 | c1 | c1 
***************
*** 1541,1548 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                     
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1530,1537 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                              QUERY PLAN                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1550,1556 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1539,1545 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") s2(c1, c2) ON (TRUE)) WHERE ((s1.c1 = s2.c1)) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1585,1592 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1574,1581 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                   
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1594,1600 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1583,1589 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s2(c1, c2) ON (TRUE)) WHERE ((s1.c1 = s2.c1)) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1630,1637 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                     
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1619,1626 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                              QUERY PLAN                                                                                                                                                                             
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1639,1645 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1628,1634 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") s2(c1, c2) ON (TRUE)) WHERE ((s1.c1 = s2.c1)) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1674,1681 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                             QUERY PLAN                                                                                                                                                                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1663,1670 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                   QUERY PLAN                                                                                                                                                                                  
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1683,1689 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1672,1678 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s2(c1, c2) ON (TRUE)) WHERE ((s1.c1 = s2.c1)) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1753,1766 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                          QUERY PLAN                                                                                                                                                                                                         
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1742,1755 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                           QUERY PLAN                                                                                                                                                                          
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s1.c4, s2.c1 FROM ((SELECT ctid, ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1", c3 FROM "S 1"."T 1") s1(c1, c2, c3, c4) INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1") s2(c1, c2) ON (TRUE)) WHERE ((s1.c3 = s2.c2)) ORDER BY s1.c4 ASC NULLS LAST, s1.c3 ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 2089,2113 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
     1
  (10 rows)
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                         QUERY PLAN                                                         
! ---------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: (13), ft2.c1
!    Join Filter: (13 = ft2.c1)
!    ->  Foreign Scan on public.ft2
!          Output: ft2.c1
!          Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (("C 1" >= 10)) AND (("C 1" <= 15)) ORDER BY "C 1" ASC NULLS LAST
!    ->  Materialize
!          Output: (13)
!          ->  Foreign Scan on public.ft1
!                Output: 13
!                Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 13))
! (11 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
--- 2078,2093 ----
     1
  (10 rows)
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                                                               QUERY PLAN                                                                                              
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: (13), ft2.c1
!    Relations: (public.ft2) LEFT JOIN (public.ft1)
!    Remote SQL: SELECT r2."C 1", s4.c1 FROM ("S 1"."T 1" r2 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s4(c1) ON (((13 = r2."C 1")))) WHERE ((r2."C 1" >= 10)) AND ((r2."C 1" <= 15))
! (4 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
***************
*** 2120,2143 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
      | 15
  (6 rows)
  
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                           QUERY PLAN                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Join Filter: (ft4.c1 = ft1.c1)
!    ->  Foreign Scan on public.ft4
!          Output: ft4.c1, ft4.c2, ft4.c3
!          Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 10)) AND ((c1 <= 15))
!    ->  Materialize
!          Output: ft1.c1, ft2.c1, (13)
!          ->  Foreign Scan
!                Output: ft1.c1, ft2.c1, 13
!                Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12)) ORDER BY r4."C 1" ASC NULLS LAST
! (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
--- 2100,2114 ----
      | 15
  (6 rows)
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                                                                                QUERY PLAN                                                                                                                                                
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Relations: (public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2))
!    Remote SQL: SELECT r1.c1, s7.c1, s7.c2, s7.c3 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4."C 1", r5."C 1", 13 FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12))) s7(c1, c2, c3) ON (((r1.c1 = s7.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
! (4 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
***************
*** 2147,2162 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
   14 |    |    |   
  (3 rows)
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                       QUERY PLAN                                                                                                                                      
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2118,2177 ----
   14 |    |    |   
  (3 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                               QUERY PLAN                                                                                                                                                                               
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft2.c1, (13), (13), ft2_1.c1
+    Relations: (public.ft2) LEFT JOIN ((public.ft2) LEFT JOIN (public.ft1))
+    Remote SQL: SELECT r1."C 1", s8.c1, s8.c2, s8.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r5."C 1", 13, s7.c1 FROM ("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s7(c1) ON (((13 = r5."C 1")))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))) s8(c1, c2, c3) ON (((r1."C 1" = 13)))) WHERE ((r1."C 1" >= 10)) AND ((r1."C 1" <= 15))
+ (4 rows)
+ 
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 
+ ----+----+----+----
+  10 |    |    |   
+  11 |    |    |   
+  12 |    |    |   
+  13 | 13 |    | 10
+  13 | 13 |    | 11
+  13 | 13 |    | 12
+  13 | 13 | 13 | 13
+  13 | 13 |    | 14
+  13 | 13 |    | 15
+  14 |    |    |   
+  15 |    |    |   
+ (11 rows)
+ 
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                      
+ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ((13 IS NOT NULL)), ft4_1.c1, (13), ft1.c1, ft2.c1
+    Relations: (public.ft4) LEFT JOIN ((public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2)))
+    Remote SQL: SELECT r1.c1, s11.c1, s11.c2, s11.c3, s11.c4, s11.c5 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4.c1, s10.c1, s10.c2, (s10.c3 IS NOT NULL), s10.c3 FROM ("S 1"."T 3" r4 LEFT JOIN (SELECT r7."C 1", r8."C 1", 13 FROM ("S 1"."T 1" r7 INNER JOIN "S 1"."T 1" r8 ON (TRUE)) WHERE ((r8."C 1" = 12)) AND ((r7."C 1" = 12))) s10(c1, c2, c3) ON (((r4.c1 = s10.c1)))) WHERE ((r4.c1 >= 10)) AND ((r4.c1 <= 15))) s11(c1, c2, c3, c4, c5) ON (((r1.c1 = s11.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
+ (4 rows)
+ 
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 | d2 | e2 
+ ----+----+----+----+----+----
+  10 | f  | 10 |    |    |   
+  12 | t  | 12 | 13 | 12 | 12
+  14 | f  | 14 |    |    |   
+ (3 rows)
+ 
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                    QUERY PLAN                                                                                                                                    
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s1.c4, r2.c1, r2.c2 FROM ((SELECT ROW(c1, c2, c3), c1, c2, c3 FROM "S 1"."T 4") s1(c1, c2, c3, c4) INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((s1.c2 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY s1.c2 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 2945,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, 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 (TRUE)) WHERE ((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)
--- 2960,2973 ----
  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 (TRUE)) WHERE ((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)
***************
*** 3088,3101 **** 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
!                                                                                                                                     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 (TRUE)) WHERE ((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)
--- 3103,3116 ----
  
  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 (TRUE)) WHERE ((r1.c2 = s2.c2)) FOR UPDATE OF r1
           ->  Hash Join
                 Output: ft2.ctid, ft1.*
                 Hash Cond: (ft2.c2 = ft1.c1)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 27,32 ****
--- 27,33 ----
  #include "optimizer/cost.h"
  #include "optimizer/pathnode.h"
  #include "optimizer/paths.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/planmain.h"
  #include "optimizer/restrictinfo.h"
  #include "optimizer/var.h"
***************
*** 403,409 **** static bool foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel,
  static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
  								 RelOptInfo *rel);
  static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
- static bool reltarget_has_non_vars(RelOptInfo *foreignrel);
  static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path);
  
--- 404,409 ----
***************
*** 657,662 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 657,704 ----
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
  
+ 	/*
+ 	 * Decide whether expressions in the reltarget are shippable and whether
+ 	 * there are any non-Vars in those expressions.
+ 	 *
+ 	 * Note: if the relation is an appendrel child, it isn't in the main join
+ 	 * tree, so the info is not used.
+ 	 */
+ 	if (baserel->reloptkind == RELOPT_BASEREL)
+ 	{
+ 		bool		reltarget_is_shippable = true;
+ 		bool		reltarget_has_non_vars = false;
+ 
+ 		foreach(lc, baserel->reltarget->exprs)
+ 		{
+ 			Node	   *node = (Node *) lfirst(lc);
+ 
+ 			/* Must be a Var or a PlaceHolderVar here */
+ 			Assert(IsA(node, Var) ||  IsA(node, PlaceHolderVar));
+ 
+ 			if (IsA(node, PlaceHolderVar))
+ 			{
+ 				reltarget_has_non_vars = true;
+ 				if (!is_foreign_expr(root, baserel, (Expr *) node))
+ 				{
+ 					reltarget_is_shippable = false;
+ 					break;
+ 				}
+ 			}
+ 			else
+ 			{
+ 				Var		   *var = (Var *) node;
+ 
+ 				if (var->varattno <= 0 &&
+ 					var->varattno != SelfItemPointerAttributeNumber &&
+ 					var->varattno != ObjectIdAttributeNumber)
+ 					reltarget_has_non_vars = true;
+ 			}
+ 		}
+ 		fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 		fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 	}
+ 
  	/* Set the subquery information */
  	fpinfo->make_outerrel_subquery = false;
  	fpinfo->make_innerrel_subquery = false;
***************
*** 1186,1192 **** postgresGetForeignPlan(PlannerInfo *root,
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
--- 1228,1234 ----
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
***************
*** 2515,2521 **** estimate_path_cost_size(PlannerInfo *root,
  
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL)
! 			fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
--- 2557,2563 ----
  
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL)
! 			fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
***************
*** 3956,3961 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 3998,4005 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	bool		reltarget_is_shippable = true;
+ 	bool		reltarget_has_non_vars = false;
  	Relids		relids;
  
  	/*
***************
*** 3986,3991 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4030,4043 ----
  	if (fpinfo_o->local_conds || fpinfo_i->local_conds)
  		return false;
  
+ 	/*
+ 	 * If the reltargets of joining relations are not shippable, those are
+ 	 * required to be evaluated locally, so the join can not be pushed down.
+ 	 */
+ 	if (!fpinfo_o->reltarget_is_shippable ||
+ 		!fpinfo_i->reltarget_is_shippable)
+ 		return false;
+ 
  	/* Separate restrict list into join quals and quals on join relation */
  	if (IS_OUTER_JOIN(jointype))
  		extract_actual_join_clauses(extra->restrictlist, &joinclauses, &otherclauses);
***************
*** 4011,4036 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			return false;
  	}
  
- 	/*
- 	 * deparseExplicitTargetList() isn't smart enough to handle anything other
- 	 * than a Var.  In particular, if there's some PlaceHolderVar that would
- 	 * need to be evaluated within this join tree (because there's an upper
- 	 * reference to a quantity that may go to NULL as a result of an outer
- 	 * join), then we can't try to push the join down because we'll fail when
- 	 * we get to deparseExplicitTargetList().  However, a PlaceHolderVar that
- 	 * needs to be evaluated *at the top* of this join tree is OK, because we
- 	 * can do that locally after fetching the results from the remote side.
- 	 */
- 	relids = joinrel->relids;
- 	foreach(lc, root->placeholder_list)
- 	{
- 		PlaceHolderInfo *phinfo = lfirst(lc);
- 
- 		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
- 			bms_nonempty_difference(relids, phinfo->ph_eval_at))
- 			return false;
- 	}
- 
  	/* Save the join clauses, for later use. */
  	fpinfo->joinclauses = joinclauses;
  
--- 4063,4068 ----
***************
*** 4078,4113 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
  		case JOIN_FULL:
! 			/*
! 			 * We can't do anything here, and if there are any non-Vars in the
! 			 * outerrel/innerrel's reltarget, give up pushing down this join,
! 			 * because the deparsing logic can't support such a case currently.
! 			 */
! 			if (reltarget_has_non_vars(outerrel))
! 				return false;
! 			if (reltarget_has_non_vars(innerrel))
! 				return false;
  			break;
  
  		default:
--- 4110,4143 ----
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			if (!fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			if (!fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
  		case JOIN_FULL:
! 			/* can't do anything here */
  			break;
  
  		default:
***************
*** 4177,4188 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  					 fpinfo_i->relation_name->data);
  
  	/*
  	 * Set the subquery information.  If the relation performs a full outer
  	 * join and if the input relations have non-NIL remote_conds, the input
! 	 * relations need to be deparsed as a subquery.
  	 */
  	relids = NULL;
! 	if (jointype == JOIN_FULL && fpinfo_o->remote_conds)
  	{
  		fpinfo->make_outerrel_subquery = true;
  		relids = bms_add_members(relids, outerrel->relids);
--- 4207,4259 ----
  					 fpinfo_i->relation_name->data);
  
  	/*
+ 	 * Decide whether expressions in the reltarget are shippable and whether
+ 	 * there are any non-Vars in those expressions.
+ 	 *
+ 	 * Note: we don't need to be careful about whole-row references or system
+ 	 * columns other than ctid and oid here; those would be handled as simple
+ 	 * output columns of lower subqueries, if any.
+ 	 */
+ 	foreach(lc, joinrel->reltarget->exprs)
+ 	{
+ 		Node	   *node = (Node *) lfirst(lc);
+ 
+ 		/* Must be a Var or a PlaceHolderVar here */
+ 		Assert(IsA(node, Var) ||  IsA(node, PlaceHolderVar));
+ 
+ 		if (IsA(node, PlaceHolderVar))
+ 		{
+ 			PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 			PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
+ 
+ 			/* Ignore the PHV if it has bubbled up from an either input. */
+ 			if (bms_is_subset(phinfo->ph_eval_at, outerrel->relids) ||
+ 				bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
+ 				continue;
+ 
+ 			reltarget_has_non_vars = true;
+ 			if (!is_foreign_expr(root, joinrel, (Expr *) node))
+ 			{
+ 				reltarget_is_shippable = false;
+ 				break;
+ 			}
+ 		}
+ 		else
+ 			Assert(IsA(node, Var));
+ 	}
+ 	fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 	fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 
+ 	/*
  	 * Set the subquery information.  If the relation performs a full outer
  	 * join and if the input relations have non-NIL remote_conds, the input
! 	 * relations need to be deparsed as subqueries.  Also, if the input
! 	 * relations have reltarget_has_non_vars=TRUE, we need to deparse the
! 	 * input relations as subqueries, regardless of the jointype.
  	 */
  	relids = NULL;
! 	if ((jointype == JOIN_FULL && fpinfo_o->remote_conds) ||
! 		fpinfo_o->reltarget_has_non_vars)
  	{
  		fpinfo->make_outerrel_subquery = true;
  		relids = bms_add_members(relids, outerrel->relids);
***************
*** 4192,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  		fpinfo->make_outerrel_subquery = false;
  		relids = bms_add_members(relids, fpinfo_o->subquery_rels);
  	}
! 	if (jointype == JOIN_FULL && fpinfo_i->remote_conds)
  	{
  		fpinfo->make_innerrel_subquery = true;
  		relids = bms_add_members(relids, innerrel->relids);
--- 4263,4270 ----
  		fpinfo->make_outerrel_subquery = false;
  		relids = bms_add_members(relids, fpinfo_o->subquery_rels);
  	}
! 	if ((jointype == JOIN_FULL && fpinfo_i->remote_conds) ||
! 		fpinfo_i->reltarget_has_non_vars)
  	{
  		fpinfo->make_innerrel_subquery = true;
  		relids = bms_add_members(relids, innerrel->relids);
***************
*** 4216,4245 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	return true;
  }
  
- /*
-  * Detect whether there are whole-row Vars or system columns other than ctid
-  * and oid in the given relation's reltarget.
-  *
-  * Note: currently deparseExplicitTargetList can't properly handle such Vars.
-  */
- static bool
- reltarget_has_non_vars(RelOptInfo *foreignrel)
- {
- 	ListCell   *lc;
- 
- 	foreach(lc, foreignrel->reltarget->exprs)
- 	{
- 		Var		   *var = (Var *) lfirst(lc);
- 
- 		Assert(IsA(var, Var));
- 		if (var->varattno <= 0 &&
- 			var->varattno != SelfItemPointerAttributeNumber &&
- 			var->varattno != ObjectIdAttributeNumber)
- 			return true;
- 	}
- 	return false;
- }
- 
  static void
  add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path)
--- 4288,4293 ----
***************
*** 4590,4595 **** conversion_error_callback(void *arg)
--- 4638,4644 ----
  	const char *attname = NULL;
  	const char *relname = NULL;
  	bool		is_wholerow = false;
+ 	bool		is_placeholder = false;
  	ConversionLocation *errpos = (ConversionLocation *) arg;
  
  	if (errpos->rel)
***************
*** 4614,4644 **** conversion_error_callback(void *arg)
  		EState	   *estate = fsstate->ss.ps.state;
  		TargetEntry *tle;
  		Var		   *var;
- 		RangeTblEntry *rte;
  
  		Assert(IsA(fsplan, ForeignScan));
  		tle = (TargetEntry *) list_nth(fsplan->fdw_scan_tlist,
  									   errpos->cur_attno - 1);
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
- 		Assert(IsA(var, Var));
  
! 		rte = rt_fetch(var->varno, estate->es_range_table);
  
! 		if (var->varattno == 0)
! 			is_wholerow = true;
  		else
! 			attname = get_relid_attribute_name(rte->relid, var->varattno);
  
! 		relname = get_rel_name(rte->relid);
  	}
  
! 	if (relname)
  	{
  		if (is_wholerow)
! 			errcontext("whole-row reference to foreign table \"%s\"", relname);
  		else if (attname)
! 			errcontext("column \"%s\" of foreign table \"%s\"", attname, relname);
  	}
  }
  
--- 4663,4704 ----
  		EState	   *estate = fsstate->ss.ps.state;
  		TargetEntry *tle;
  		Var		   *var;
  
  		Assert(IsA(fsplan, ForeignScan));
  		tle = (TargetEntry *) list_nth(fsplan->fdw_scan_tlist,
  									   errpos->cur_attno - 1);
  		Assert(IsA(tle, TargetEntry));
  		var = (Var *) tle->expr;
  
! 		/* Must be a Var or a PlaceHolderVar here */
! 		Assert(IsA(var, Var) ||  IsA(var, PlaceHolderVar));
  
! 		if (IsA(var, PlaceHolderVar))
! 			is_placeholder = true;
  		else
! 		{
! 			RangeTblEntry *rte  = rt_fetch(var->varno, estate->es_range_table);
! 
! 			if (var->varattno == 0)
! 				is_wholerow = true;
! 			else
! 				attname = get_relid_attribute_name(rte->relid, var->varattno);
  
! 			relname = get_rel_name(rte->relid);
! 		}
  	}
  
! 	if (is_placeholder)
! 		errcontext("expression at position %d in select list",
! 				   errpos->cur_attno);
! 	else if (relname)
  	{
  		if (is_wholerow)
! 			errcontext("whole-row reference to foreign table \"%s\"",
! 					   relname);
  		else if (attname)
! 			errcontext("column \"%s\" of foreign table \"%s\"",
! 					   attname, relname);
  	}
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 33,38 **** typedef struct PgFdwRelationInfo
--- 33,64 ----
  	bool		pushdown_safe;
  
  	/*
+ 	 * Flags on Path output tlist for the relation.  These are used to decide
+ 	 * whether the relation can be joined with any other foreign table (or
+ 	 * join) and whether the relation is deparsed as a subquery when creating
+ 	 * a remote query in that case.
+ 	 *
+ 	 * If each expression in the relation's reltarget is shippable (i.e.,
+ 	 * computable on the remote side), reltarget_is_shippabe will be set TRUE,
+ 	 * and possible foreign joins will be considered in foreign_join_ok().
+ 	 * Note that any Vars belonging to the relation are shippable, so if any
+ 	 * PHVs in the reltarget are shippable, the flag is set TRUE.
+ 	 *
+ 	 * If the reltarget has PHVs, reltarget_has_non_vars will be set TRUE.  In
+ 	 * that case, if performing a foreign join, the relation will be deparsed
+ 	 * as a subquery emitting the reltarget expressions in a remote query for
+ 	 * the foreign join.  For base relations the flag will also be set TRUE,
+ 	 * if the reltarget contains any whole-row references or system columns
+ 	 * other than citd and oid.  In that case, if performing a foreign join,
+ 	 * the relation will be deparsed as a subquery emitting ROW() expressions
+ 	 * for whole-row references, and 0 for system columns other than ctid and
+ 	 * oid, except for tableoid, in which case a value for the local table OID
+ 	 * will be emitted by the subquery.
+ 	 */
+ 	bool		reltarget_is_shippable; /* are reltarget exprs shippable? */
+ 	bool		reltarget_has_non_vars;	/* are there any non-Var exprs? */
+ 
+ 	/*
  	 * Restriction clauses, divided into safe and unsafe to pushdown subsets.
  	 *
  	 * For a base foreign relation this is a list of clauses along-with
***************
*** 168,174 **** extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(RelOptInfo *foreign_rel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
--- 194,200 ----
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 496,512 **** EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
- 
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
--- 496,514 ----
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
#24Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#23)
Re: Push down more full joins in postgres_fdw

Review for postgres-fdw-more-full-join-pushdown-v6 patch.

The patch applies cleanly and regression is clean (make check in
regress directory and that in postgres_fdw).

Here are some comments.
1. Because of the following code change, for a joinrel we might end up using
attrs_used, which will be garbage for a join rel. The assumption that tlist can
not be NIL for a join relation is wrong. Even for a join relation, tlist can be
NULL.  Consider query 'explain verbose select count(*) from ft1, ft2' Since
count(*) doesn't need any columns, the tlist is NIL. With the patch the server
crashes for this query.
-    if (foreignrel->reloptkind == RELOPT_JOINREL)
+    /*
+     * Note: tlist for a base relation might be non-NIL.  For example, if the
+     * base relation is an operand of a foreign join performing a full outer
+     * join and has non-NIL remote_conds, the base relation will be deparsed
+     * as a subquery, so the tlist for the base relation could be non-NIL.
+     */
+    if (tlist != NIL)
     {
-        /* For a join relation use the input tlist */

We can not decide whether to use deparseExplicitTargetList or
deparse it from attrs_used based on the tlist. SELECT lists, whether it's
topmost SELECT or a subquery (even on a base relation), for deparsing a
JOIN query need to use deparseExplicitTargetList.

2. The code in deparseRangeTblRef() dealing with subqueries, is very similar to
deparseSelectStmtForRel(). The only thing deparseRangeTblRef() does not require
is the appending ORDER BY, which is determined by existence of pathkeys
argument. Probably we should reuse deparseSelectStmtForRel(), instead of
duplicating the code. This will also make the current changes to
deparseSelectSql unnecessary.

3. Why do we need following change? The elog() just few lines above says that
we expect only Var nodes. Why then we use deparseExpr() instead of
deparseVar()?
         if (i > 0)
             appendStringInfoString(buf, ", ");
-        deparseVar(var, context);
+        deparseExpr((Expr *) var, context);

*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);

And I get the answer for that question, somewhere down in the patch
+    /*
+     * If the given expression is an output column of a subquery-in-FROM,
+     * deparse the alias to the expression instead.
+     */
+    if (IsA(node, Var))
+    {
+        int            tabno;
+        int            colno;
+
+        if (isSubqueryExpr(node, context->foreignrel, &tabno, &colno))
+        {
+            appendStringInfo(context->buf, "%s%d.%s%d",
+                             SS_TAB_ALIAS_PREFIX, tabno,
+                             SS_COL_ALIAS_PREFIX, colno);
+            return;
+        }
+    }
+

Functionally, this code belongs to deparseVar() and not deparseExpr(). Like all
other functions which handle Expr nodes, deparseExpr() is expected to be a
dispatcher to the node specific function.

4. This comment is good to have there, but is unrelated to this patch. May be a
separate patch?
+ /* Don't generate bad syntax if no columns */

5. Changed comment doesn't say anything different from the original comment. It
may have been good to have it this way to start with, but changing it now
doesn't bring anything new. You seem to have just merged prologue of the
function with a comment in that function. I think, this change is unnecessary.
- * 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.
+ * For a join relation the clause of the following form is appended to buf:
+ * ((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.
+    /* Don't generate bad syntax if no columns */
6. Change may be good but unrelated to this patch. May be material for a
separate patch. There are few such changes in this function. While these
changes may be good by themselves, they distract reviewer from the goal of the
patch.
-            appendStringInfo(buf, "(");
+            appendStringInfoChar(buf, '(');
7. I don't understand why you need to change this function so much. Take for
example the following change.
-        RelOptInfo *rel_o = fpinfo->outerrel;
-        RelOptInfo *rel_i = fpinfo->innerrel;
-        StringInfoData join_sql_o;
-        StringInfoData join_sql_i;
+        /* Begin the FROM clause entry */
+        appendStringInfoChar(buf, '(');
         /* Deparse outer relation */
-        initStringInfo(&join_sql_o);
-        deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
+        deparseRangeTblRef(buf, root,
+                           fpinfo->outerrel,
+                           fpinfo->make_outerrel_subquery,
+                           params_list);
         /* Deparse outer relation */
-        initStringInfo(&join_sql_o);
-        deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
+        deparseRangeTblRef(buf, root,
+                           fpinfo->outerrel,
+                           fpinfo->make_outerrel_subquery,
+                           params_list);
It removes few variables, instead directly accesses the members from fpinfo.
Also, deparses the individual relations in the provided buffer directly, rather
than in separate buffers in the original code. Instead of this, all you had to
do was replace a call
-        deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
with
+        deparseRangeTblRef(&join_sql_o, root, rel_o,
fpinfo->make_outerrel_subquery, params_list)
Similarly for the inner relation. Again, the changes you have done might have
been good, if done in the original code, but doing those in this patch just
creates distractions and increases the size of the patch.
Similarly changes
-        /* Append join clause; (TRUE) if no join clause */
+        /* Append join conditions */
+        {
+            /* No join conditions; add "(TRUE)" */
             appendStringInfoString(buf, "(TRUE)");
+        }
+        appendStringInfoString(buf, " ON ");

- /* End the FROM clause entry. */
- appendStringInfo(buf, ")");

+ /* End the FROM clause entry */
+ appendStringInfoChar(buf, ')');

8. Why have you changed the name of variable here?
-        if (use_alias)
+        if (add_rel_alias)

9. In deparseRangeTblRef(), the targetlist for the subquery is obtained by
calling build_tlist_to_deparse(), but appendSubselectAlias() constructs the
column aliases based on foreignrel->reltarget->exprs. Those two are usually
same, but there is no code which guarantees that. Instead, pass tlist to
appendSubselectAlias() or simply pass the number of entries in that list
(list_length(tlist), which is all it needs. OR use foreignrel->reltarget->exprs
directly instead of calling build_tlist_to_deparse(). Similar problem exists
with getSubselectAliasInfo().

10. Deparsing a query in the form of a subquery makes it hard-to-read. The
original code aimed at avoiding a subquery. Also, this change has created many
expected output changes, which again seem unnecessary. In fact, having the
pushable join clauses of an inner join in ON clause, which is closer to JOIN
clause, is better than having them farther in the WHERE clause.
- /*
- * For an inner join, all restrictions can be treated alike. Treating the
- * pushed down conditions as join conditions allows a top level full outer
- * join to be deparsed without requiring subqueries.
- */
- if (jointype == JOIN_INNER)
- {
- Assert(!fpinfo->joinclauses);
- fpinfo->joinclauses = fpinfo->remote_conds;
- fpinfo->remote_conds = NIL;
- }
-

11. I have reworded following comment and restructured the code that follows it
in the attached patch.
+    /*
+     * Set the subquery information.  If the relation performs a full outer
+     * join and if the input relations have non-NIL remote_conds, the input
+     * relations need to be deparsed as a subquery.
+     */
The code esp. the if .. else .. block followed by another one, made it a bit
unreadable. Hence I restructured it so that it's readable and doesn't require
variable "relids" from earlier code, which was being reused. Let me know if
this change looks good.
12. The code below deletes the condition, which is understandable.
-            if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
But why does it add a new unrelated condition here? What the comment claims,
looks like an existing bug, unrelated to the patch. Can you please give an
example? If that it's an existing bug, it should be fixed as a separate patch.
I don't understand the relation of this code with what the patch is doing.
+            /*
+             * We can't do anything here, and if there are any non-Vars in the
+             * outerrel/innerrel's reltarget, give up pushing down this join,
+             * because the deparsing logic can't support such a case currently.
+             */
+            if (reltarget_has_non_vars(outerrel))
+                return false;
+            if (reltarget_has_non_vars(innerrel))
                 return false;
13. The comment below is missing the main purpose i.e. creating a a unique
alias, in case the relation gets converted into a subquery. Lowest or highest
relid will create a unique alias at given level of join and that would be more
future proof. If we decide to consider paths for every join order, following
comment will no more be true.
+    /*
+     * Set the relation index.  This is defined as the position of this
+     * joinrel in the join_rel_list list plus the length of the rtable list.
+     * Note that since this joinrel is at the end of the list when we are
+     * called, we can get the position by list_length.
+     */
+    fpinfo->relation_index =
+        list_length(root->parse->rtable) + list_length(root->join_rel_list);
14. The function name talks about non-vars but the Assert few lines below
asserts that every node there is a Var. Need better naming.
+reltarget_has_non_vars(RelOptInfo *foreignrel)
+{
+    ListCell   *lc;
+
+    foreach(lc, foreignrel->reltarget->exprs)
+    {
+        Var           *var = (Var *) lfirst(lc);
+
+        Assert(IsA(var, Var));
And also an explanation for the claim
+ * Note: currently deparseExplicitTargetList can't properly handle such Vars.

15. While deparsing every Var, we are descending the RelOptInfo hierarchy.
Instead of that, we should probably gather all the alias information once and
store it in context as an array indexed by relid. While deparsing a Var we can
get the targetlist and alias for a given relation by using var->varno as index.
And then search for given Var node in the targetlist to deparse the column name
by its position in the targetlist. For the relations that are not converted
into subqueries, this array will not hold any information and the Var will be
deparsed as it's being done today.

Testing
-------
1. The code is changing deparser but doesn't have testcases
testing impact of the code. For example, we should have testcases with remote
query deparsed as nested subqueries or nested subqueries within subqueries and
so on May be testcases where a relation deeper in the RelOptInfo hierarchy has
conditions but it's immediate upper relation does not have those.

2. The only testcase added by this patch, copies an existing case and adds a
whole row reference to one of the relations being joined. Instead we could add
that whole-row reference to the existing testcase itself.

On Thu, Oct 13, 2016 at 4:05 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/09/29 21:12, Etsuro Fujita wrote:

Please find attached an updated version of the patch.

Here is a new version (V6) of the patch, which is basically the same as the
previous patch, but I slightly modified that patch. Another patch I am
attaching is an updated patch for pushing down PHVs to the remote, which is
created on top of the V6 patch.

Best regards,
Etsuro Fujita

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

#25Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Ashutosh Bapat (#24)
1 attachment(s)
Re: Push down more full joins in postgres_fdw
11. I have reworded following comment and restructured the code that follows it
in the attached patch.
+    /*
+     * Set the subquery information.  If the relation performs a full outer
+     * join and if the input relations have non-NIL remote_conds, the input
+     * relations need to be deparsed as a subquery.
+     */
The code esp. the if .. else .. block followed by another one, made it a bit
unreadable. Hence I restructured it so that it's readable and doesn't require
variable "relids" from earlier code, which was being reused. Let me know if
this change looks good.

Sorry, forgot to attach the patch. Here it is.

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

Attachments:

subquery_deparse_fulljoin_restructure.patchtext/x-diff; charset=US-ASCII; name=subquery_deparse_fulljoin_restructure.patchDownload
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 691658f..aa1f111 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -95,20 +95,22 @@ typedef struct deparse_expr_cxt
 	PlannerInfo *root;			/* global planner state */
 	RelOptInfo *foreignrel;		/* the foreign relation we are planning for */
 	StringInfo	buf;			/* output buffer to append to */
 	List	  **params_list;	/* exprs that will become remote Params */
 } deparse_expr_cxt;
 
 #define REL_ALIAS_PREFIX	"r"
 /* Handy macro to add relation name qualification */
 #define ADD_REL_QUALIFIER(buf, varno)	\
 		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+#define SS_TAB_ALIAS_PREFIX	"s"
+#define SS_COL_ALIAS_PREFIX	"c"
 
 /*
  * Functions to determine whether an expression can be evaluated safely on
  * remote server.
  */
 static bool foreign_expr_walker(Node *node,
 					foreign_glob_cxt *glob_cxt,
 					foreign_loc_cxt *outer_cxt);
 static char *deparse_type_name(Oid type_oid, int32 typemod);
 
@@ -145,27 +147,36 @@ static void deparseDistinctExpr(DistinctExpr *node, deparse_expr_cxt *context);
 static void deparseScalarArrayOpExpr(ScalarArrayOpExpr *node,
 						 deparse_expr_cxt *context);
 static void deparseRelabelType(RelabelType *node, deparse_expr_cxt *context);
 static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context);
 static void deparseNullTest(NullTest *node, deparse_expr_cxt *context);
 static void deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context);
 static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
 				 deparse_expr_cxt *context);
 static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
 					   deparse_expr_cxt *context);
-static void deparseSelectSql(List *tlist, List **retrieved_attrs,
+static void deparseSelectSql(List *tlist,
+				 List *remote_conds,
+				 List **retrieved_attrs,
 				 deparse_expr_cxt *context);
 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 add_rel_alias,
+					  List **params_list);
+static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+				   bool make_subquery, List **params_list);
+static void appendSubselectAlias(deparse_expr_cxt *context);
+static void getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
+					  int *tabno, int *colno);
+static bool isSubqueryExpr(Expr *node, RelOptInfo *foreignrel, int *tabno, int *colno);
 
 
 /*
  * Examine each qual clause in input_conds, and classify them into two groups,
  * which are returned as two lists:
  *	- remote_conds contains expressions that can be evaluated remotely
  *	- local_conds contains expressions that can't be evaluated remotely
  */
 void
 classifyConditions(PlannerInfo *root,
@@ -773,95 +784,105 @@ deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
 	Assert(rel->reloptkind == RELOPT_JOINREL ||
 		   rel->reloptkind == RELOPT_BASEREL ||
 		   rel->reloptkind == RELOPT_OTHER_MEMBER_REL);
 
 	/* Fill portions of context common to join and base relation */
 	context.buf = buf;
 	context.root = root;
 	context.foreignrel = rel;
 	context.params_list = params_list;
 
-	/* Construct SELECT clause and FROM clause */
-	deparseSelectSql(tlist, retrieved_attrs, &context);
-
-	/*
-	 * Construct WHERE clause
-	 */
-	if (remote_conds)
-	{
-		appendStringInfo(buf, " WHERE ");
-		appendConditions(remote_conds, &context);
-	}
+	/* Construct SELECT clause, FROM clause, and WHERE clause */
+	deparseSelectSql(tlist, remote_conds, retrieved_attrs, &context);
 
 	/* Add ORDER BY clause if we found any useful pathkeys */
 	if (pathkeys)
 		appendOrderByClause(pathkeys, &context);
 
 	/* Add any necessary FOR UPDATE/SHARE. */
 	deparseLockingClause(&context);
 }
 
 /*
  * Construct a simple SELECT statement that retrieves desired columns
  * of the specified foreign table, and append it to "buf".  The output
- * contains just "SELECT ... FROM ....".
+ * contains just "SELECT ... FROM ... WHERE ...".
  *
  * We also create an integer List of the columns being retrieved, which is
  * returned to *retrieved_attrs.
  *
  * tlist is the list of desired columns. Read prologue of
  * deparseSelectStmtForRel() for details.
  */
 static void
-deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
+deparseSelectSql(List *tlist,
+				 List *remote_conds,
+				 List **retrieved_attrs,
+				 deparse_expr_cxt *context)
 {
 	StringInfo	buf = context->buf;
 	RelOptInfo *foreignrel = context->foreignrel;
 	PlannerInfo *root = context->root;
 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
 
 	/*
 	 * Construct SELECT list
 	 */
 	appendStringInfoString(buf, "SELECT ");
 
-	if (foreignrel->reloptkind == RELOPT_JOINREL)
+	/*
+	 * Note: tlist for a base relation might be non-NIL.  For example, if the
+	 * base relation is an operand of a foreign join performing a full outer
+	 * join and has non-NIL remote_conds, the base relation will be deparsed
+	 * as a subquery, so the tlist for the base relation could be non-NIL.
+	 */
+	if (tlist != NIL)
 	{
-		/* For a join relation use the input tlist */
 		deparseExplicitTargetList(tlist, retrieved_attrs, context);
 	}
 	else
 	{
 		/*
 		 * For a base relation fpinfo->attrs_used gives the list of columns
 		 * required to be fetched from the foreign server.
 		 */
 		RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
 
 		/*
 		 * Core code already has some lock on each rel being planned, so we
 		 * can use NoLock here.
 		 */
 		Relation	rel = heap_open(rte->relid, NoLock);
 
+		Assert(foreignrel->reloptkind != RELOPT_JOINREL);
+
 		deparseTargetList(buf, root, foreignrel->relid, rel, false,
 						  fpinfo->attrs_used, false, retrieved_attrs);
 		heap_close(rel, NoLock);
 	}
 
 	/*
 	 * Construct FROM clause
 	 */
 	appendStringInfoString(buf, " FROM ");
 	deparseFromExprForRel(buf, root, foreignrel,
 						  (foreignrel->reloptkind == RELOPT_JOINREL),
 						  context->params_list);
+
+	/*
+	 * Construct WHERE clause
+	 */
+	if (remote_conds)
+	{
+		appendStringInfoString(buf, " WHERE ");
+		appendConditions(remote_conds, context);
+	}
 }
 
 /*
  * Emit a target list that retrieves the columns specified in attrs_used.
  * This is used for both SELECT and RETURNING targetlists; the is_returning
  * parameter is true only for a RETURNING targetlist.
  *
  * The tlist text is appended to buf, and we also create an integer List
  * of the columns being retrieved, which is returned to *retrieved_attrs.
  *
@@ -958,25 +979,34 @@ deparseTargetList(StringInfo buf,
 /*
  * Deparse the appropriate locking clause (FOR UPDATE or FOR SHARE) for a
  * given relation (context->foreignrel).
  */
 static void
 deparseLockingClause(deparse_expr_cxt *context)
 {
 	StringInfo	buf = context->buf;
 	PlannerInfo *root = context->root;
 	RelOptInfo *rel = context->foreignrel;
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
 	int			relid = -1;
 
 	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
 	{
 		/*
+		 * Ignore relation if it appears in a lower subquery, because in that
+		 * case we would have already considered locking for it while
+		 * deparsing the lower subquery.
+		 */
+		if (bms_is_member(relid, fpinfo->subquery_rels))
+			continue;
+
+		/*
 		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
 		 * initial row fetch, rather than later on as is done for local
 		 * tables. The extra roundtrips involved in trying to duplicate the
 		 * local semantics exactly don't seem worthwhile (see also comments
 		 * for RowMarkType).
 		 *
 		 * Note: because we actually run the query as a cursor, this assumes
 		 * that DECLARE CURSOR ... FOR UPDATE is supported, which it isn't
 		 * before 8.3.
 		 */
@@ -1131,112 +1161,268 @@ deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
 		/* Extract expression if TargetEntry node */
 		Assert(IsA(tle, TargetEntry));
 		var = (Var *) tle->expr;
 
 		/* We expect only Var nodes here */
 		if (!IsA(var, Var))
 			elog(ERROR, "non-Var not expected in target list");
 
 		if (i > 0)
 			appendStringInfoString(buf, ", ");
-		deparseVar(var, context);
+		deparseExpr((Expr *) var, context);
 
 		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
 
 		i++;
 	}
 
+	/* Don't generate bad syntax if no columns */
 	if (i == 0)
 		appendStringInfoString(buf, "NULL");
 }
 
 /*
  * Construct FROM clause for given relation
  *
- * 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.
+ * For a join relation the clause of the following form is appended to buf:
+ * ((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 use_alias, List **params_list)
+deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
+					  RelOptInfo *foreignrel, bool add_rel_alias,
+					  List **params_list)
 {
 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
 
 	if (foreignrel->reloptkind == RELOPT_JOINREL)
 	{
-		RelOptInfo *rel_o = fpinfo->outerrel;
-		RelOptInfo *rel_i = fpinfo->innerrel;
-		StringInfoData join_sql_o;
-		StringInfoData join_sql_i;
+		/* Begin the FROM clause entry */
+		appendStringInfoChar(buf, '(');
 
 		/* Deparse outer relation */
-		initStringInfo(&join_sql_o);
-		deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
+		deparseRangeTblRef(buf, root,
+						   fpinfo->outerrel,
+						   fpinfo->make_outerrel_subquery,
+						   params_list);
 
-		/* Deparse inner relation */
-		initStringInfo(&join_sql_i);
-		deparseFromExprForRel(&join_sql_i, root, rel_i, true, params_list);
+		/* Append join type */
+		appendStringInfo(buf, " %s JOIN ",
+						 get_jointype_name(fpinfo->jointype));
 
-		/*
-		 * For a join relation FROM clause entry is deparsed as
-		 *
-		 * ((outer relation) <join type> (inner relation) ON (joinclauses))
-		 */
-		appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
-					   get_jointype_name(fpinfo->jointype), join_sql_i.data);
+		/* Deparse inner relation */
+		deparseRangeTblRef(buf, root,
+						   fpinfo->innerrel,
+						   fpinfo->make_innerrel_subquery,
+						   params_list);
 
-		/* Append join clause; (TRUE) if no join clause */
+		/* Append join conditions */
+		appendStringInfoString(buf, " ON ");
 		if (fpinfo->joinclauses)
 		{
 			deparse_expr_cxt context;
 
 			context.buf = buf;
 			context.foreignrel = foreignrel;
 			context.root = root;
 			context.params_list = params_list;
 
-			appendStringInfo(buf, "(");
+			appendStringInfoChar(buf, '(');
 			appendConditions(fpinfo->joinclauses, &context);
-			appendStringInfo(buf, ")");
+			appendStringInfoChar(buf, ')');
 		}
 		else
+		{
+			/* No join conditions; add "(TRUE)" */
 			appendStringInfoString(buf, "(TRUE)");
+		}
 
-		/* End the FROM clause entry. */
-		appendStringInfo(buf, ")");
+		/* End the FROM clause entry */
+		appendStringInfoChar(buf, ')');
 	}
 	else
 	{
 		RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
 
 		/*
 		 * Core code already has some lock on each rel being planned, so we
 		 * can use NoLock here.
 		 */
 		Relation	rel = heap_open(rte->relid, NoLock);
 
 		deparseRelation(buf, rel);
 
 		/*
 		 * Add a unique alias to avoid any conflict in relation names due to
 		 * pulled up subqueries in the query being built for a pushed down
 		 * join.
 		 */
-		if (use_alias)
+		if (add_rel_alias)
 			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
 
 		heap_close(rel, NoLock);
 	}
 }
 
 /*
+ * Append operand relation of foreign join to buf.
+ */
+static void
+deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+				   bool make_subquery, List **params_list)
+{
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+
+	Assert(fpinfo->local_conds == NIL);
+
+	if (make_subquery)
+	{
+		List	   *tlist;
+		List	   *retrieved_attrs;
+		deparse_expr_cxt context;
+
+		context.buf = buf;
+		context.root = root;
+		context.foreignrel = foreignrel;
+		context.params_list = params_list;
+
+		tlist = build_tlist_to_deparse(foreignrel);
+		appendStringInfoChar(buf, '(');
+		deparseSelectSql(tlist,
+						 fpinfo->remote_conds,
+						 &retrieved_attrs,
+						 &context);
+		deparseLockingClause(&context);
+		appendStringInfoChar(buf, ')');
+		appendSubselectAlias(&context);
+	}
+	else
+		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+}
+
+/*
+ * Add a subselect alias to a subquery-in-FROM.
+ */
+static void
+appendSubselectAlias(deparse_expr_cxt *context)
+{
+	StringInfo	buf = context->buf;
+	RelOptInfo *foreignrel = context->foreignrel;
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+	int			num_columns = list_length(foreignrel->reltarget->exprs);
+	int			i;
+
+	/* Append the table alias */
+	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, fpinfo->relation_index);
+
+	/* Append the column aliases */
+	appendStringInfoChar(buf, '(');
+	for (i = 1; i <= num_columns; i++)
+	{
+		if (i > 1)
+			appendStringInfoString(buf, ", ");
+
+		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+	}
+	appendStringInfoChar(buf, ')');
+}
+
+/*
+ * Get info about the subselect alias to given expression.
+ *
+ * The subselect table and column numbers are returned to *tabno and *colno,
+ * respectively.
+ */
+static void
+getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
+					  int *tabno, int *colno)
+{
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+	int			i;
+	ListCell   *lc;
+
+	/* Get the table number */
+	*tabno = fpinfo->relation_index;
+
+	/* Get the column number */
+	i = 1;
+	foreach(lc, foreignrel->reltarget->exprs)
+	{
+		if (equal(lfirst(lc), (Node *) node))
+		{
+			*colno = i;
+			return;
+		}
+		i++;
+	}
+	/* shouldn't get here */
+	elog(ERROR, "unexpected expression in subquery output");
+}
+
+/*
+ * Returns true if given expression is an output column of a subquery-in-FROM.
+ *
+ * The subselect table and column numbers are returned to *tabno and *colno,
+ * respectively, in that case.
+ */
+static bool
+isSubqueryExpr(Expr *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+{
+	Var		   *var = (Var *) node;
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+	RelOptInfo *outerrel = fpinfo->outerrel;
+	RelOptInfo *innerrel = fpinfo->innerrel;
+
+	if (foreignrel->reloptkind != RELOPT_JOINREL)
+		return false;
+
+	if (!fpinfo->subquery_rels)
+		return false;
+
+	Assert(IsA(var, Var));
+
+	if (bms_is_member(var->varno, outerrel->relids))
+	{
+		/*
+		 * If outer relation is deparsed as a subqeury, the given expression
+		 * will be an output column of the subquery; get the subselect alias
+		 * info for the given expression.
+		 */
+		if (fpinfo->make_outerrel_subquery)
+		{
+			getSubselectAliasInfo(node, outerrel, tabno, colno);
+			return true;
+		}
+		/* Otherwise, recurse into outer relation */
+		if (isSubqueryExpr(node, outerrel, tabno, colno))
+			return true;
+	}
+	else
+	{
+		Assert(bms_is_member(var->varno, innerrel->relids));
+
+		/*
+		 * Likewise for inner relation
+		 */
+		if (fpinfo->make_innerrel_subquery)
+		{
+			getSubselectAliasInfo(node, innerrel, tabno, colno);
+			return true;
+		}
+		if (isSubqueryExpr(node, innerrel, tabno, colno))
+			return true;
+	}
+	return false;
+}
+
+/*
  * deparse remote INSERT statement
  *
  * The statement text is appended to buf, and we also create an integer List
  * of the columns being retrieved by RETURNING (if any), which is returned
  * to *retrieved_attrs.
  */
 void
 deparseInsertSql(StringInfo buf, PlannerInfo *root,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing,
@@ -1804,20 +1990,38 @@ deparseStringLiteral(StringInfo buf, const char *val)
  * Note: unlike ruleutils.c, we just use a simple hard-wired parenthesization
  * scheme: anything more complex than a Var, Const, function call or cast
  * should be self-parenthesized.
  */
 static void
 deparseExpr(Expr *node, deparse_expr_cxt *context)
 {
 	if (node == NULL)
 		return;
 
+	/*
+	 * If the given expression is an output column of a subquery-in-FROM,
+	 * deparse the alias to the expression instead.
+	 */
+	if (IsA(node, Var))
+	{
+		int			tabno;
+		int			colno;
+
+		if (isSubqueryExpr(node, context->foreignrel, &tabno, &colno))
+		{
+			appendStringInfo(context->buf, "%s%d.%s%d",
+							 SS_TAB_ALIAS_PREFIX, tabno,
+							 SS_COL_ALIAS_PREFIX, colno);
+			return;
+		}
+	}
+
 	switch (nodeTag(node))
 	{
 		case T_Var:
 			deparseVar((Var *) node, context);
 			break;
 		case T_Const:
 			deparseConst((Const *) node, context);
 			break;
 		case T_Param:
 			deparseParam((Param *) node, context);
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index d97e694..65465c3 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -449,31 +449,31 @@ SELECT t1.c1, t2."C 1" FROM ft2 t1 LEFT JOIN "S 1"."T 1" t2 ON (t1.c1 = t2."C 1"
  107 | 107
  108 | 108
  109 | 109
  110 | 110
 (10 rows)
 
 -- A join between local table and foreign join. ORDER BY clause is added to the
 -- foreign join so that the local table can be joined using merge join strategy.
 EXPLAIN (VERBOSE, COSTS OFF)
 	SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
-                                                                       QUERY PLAN                                                                        
----------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                             QUERY PLAN                                                                             
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1."C 1"
    ->  Merge Right Join
          Output: t1."C 1"
          Merge Cond: (t3.c1 = t1."C 1")
          ->  Foreign Scan
                Output: t3.c1
                Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
-               Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (((r2."C 1" = r3."C 1")))) ORDER BY r2."C 1" ASC NULLS LAST
+               Remote SQL: SELECT r3."C 1" FROM ("S 1"."T 1" r2 INNER JOIN "S 1"."T 1" r3 ON (TRUE)) WHERE ((r2."C 1" = r3."C 1")) ORDER BY r2."C 1" ASC NULLS LAST
          ->  Index Only Scan using t1_pkey on "S 1"."T 1" t1
                Output: t1."C 1"
 (11 rows)
 
 SELECT t1."C 1" FROM "S 1"."T 1" t1 left join ft1 t2 join ft2 t3 on (t2.c1 = t3.c1) on (t3.c1 = t1."C 1") OFFSET 100 LIMIT 10;
  C 1 
 -----
  101
  102
  103
@@ -972,59 +972,59 @@ SELECT count(c3) FROM ft1 t1 WHERE t1.c1 === t1.c2;
 -- ===================================================================
 -- JOIN queries
 -- ===================================================================
 -- Analyze ft4 and ft5 so that we have better statistics. These tables do not
 -- have use_remote_estimate set.
 ANALYZE ft4;
 ANALYZE ft5;
 -- join two tables
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-                                                                                        QUERY PLAN                                                                                        
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                             QUERY PLAN                                                                                              
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c1, t1.c3
    ->  Foreign Scan
          Output: t1.c1, t2.c1, t1.c3
          Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-         Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
+         Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
 (6 rows)
 
 SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
  c1  | c1  
 -----+-----
  101 | 101
  102 | 102
  103 | 103
  104 | 104
  105 | 105
  106 | 106
  107 | 107
  108 | 108
  109 | 109
  110 | 110
 (10 rows)
 
 -- join three tables
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
-                                                                                            QUERY PLAN                                                                                             
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                      QUERY PLAN                                                                                                       
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c2, t3.c3, t1.c3
    ->  Sort
          Output: t1.c1, t2.c2, t3.c3, t1.c3
          Sort Key: t1.c3, t1.c1
          ->  Foreign Scan
                Output: t1.c1, t2.c2, t3.c3, t1.c3
                Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3)
-               Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) INNER JOIN "S 1"."T 3" r4 ON (((r1."C 1" = r4.c1))))
+               Remote SQL: SELECT r1."C 1", r1.c3, r2.c2, r4.c3 FROM (("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) INNER JOIN "S 1"."T 3" r4 ON (TRUE)) WHERE ((r1."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))
 (9 rows)
 
 SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10;
  c1 | c2 |   c3   
 ----+----+--------
  22 |  2 | AAA022
  24 |  4 | AAA024
  26 |  6 | AAA026
  28 |  8 | AAA028
  30 |  0 | AAA030
@@ -1216,79 +1216,107 @@ SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
      |  3
      |  9
      | 15
      | 21
      | 27
 (10 rows)
 
 -- full outer join with restrictions on the joining relations
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
-                                           QUERY PLAN                                           
-------------------------------------------------------------------------------------------------
- Sort
+                                                                                                                                  QUERY PLAN                                                                                                                                   
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
    Output: ft4.c1, ft5.c1
-   Sort Key: ft4.c1, ft5.c1
-   ->  Hash Full Join
-         Output: ft4.c1, ft5.c1
-         Hash Cond: (ft4.c1 = ft5.c1)
-         ->  Foreign Scan on public.ft4
-               Output: ft4.c1, ft4.c2, ft4.c3
-               Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
-         ->  Hash
-               Output: ft5.c1
-               ->  Foreign Scan on public.ft5
-                     Output: ft5.c1
-                     Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
-(14 rows)
+   Relations: (public.ft4) FULL JOIN (public.ft5)
+   Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
+(4 rows)
 
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  c1 | c1 
 ----+----
  50 |   
  52 |   
  54 | 54
  56 |   
  58 |   
  60 | 60
     | 51
     | 57
 (8 rows)
 
 -- full outer join + inner join
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
-                                                                                                                                           QUERY PLAN                                                                                                                                            
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                                                    QUERY PLAN                                                                                                                                                                    
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c1, t3.c1
    ->  Foreign Scan
          Output: t1.c1, t2.c1, t3.c1
          Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
-         Remote SQL: SELECT r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
+         Remote SQL: SELECT s6.c1, s6.c2, r4.c1 FROM ((SELECT r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))) s6(c1, c2) FULL JOIN "S 1"."T 3" r4 ON (((s6.c2 = r4.c1)))) ORDER BY s6.c1 ASC NULLS LAST, s6.c2 ASC NULLS LAST, r4.c1 ASC NULLS LAST
 (6 rows)
 
 SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  c1 | c1 | c1 
 ----+----+----
  52 | 51 |   
  58 | 57 |   
     |    |  2
     |    |  4
     |    |  6
     |    |  8
     |    | 10
     |    | 12
     |    | 14
     |    | 16
 (10 rows)
 
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+                                                                                                                        QUERY PLAN                                                                                                                         
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Limit
+   Output: t1.*, t1.c1, t2.c1, t3.c1
+   ->  Sort
+         Output: t1.*, t1.c1, t2.c1, t3.c1
+         Sort Key: t1.c1, t2.c1, t3.c1
+         ->  Hash Full Join
+               Output: t1.*, t1.c1, t2.c1, t3.c1
+               Hash Cond: (t2.c1 = t3.c1)
+               ->  Foreign Scan
+                     Output: t1.*, t1.c1, t2.c1
+                     Relations: (public.ft4 t1) INNER JOIN (public.ft5 t2)
+                     Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r2.c1 FROM ("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (TRUE)) WHERE ((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60))
+               ->  Hash
+                     Output: t3.c1
+                     ->  Foreign Scan on public.ft4 t3
+                           Output: t3.c1
+                           Remote SQL: SELECT c1 FROM "S 1"."T 3"
+(17 rows)
+
+SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+       t1       | c1 | c1 | c1 
+----------------+----+----+----
+ (52,53,AAA052) | 52 | 51 |   
+ (58,59,AAA058) | 58 | 57 |   
+                |    |    |  2
+                |    |    |  4
+                |    |    |  6
+                |    |    |  8
+                |    |    | 10
+                |    |    | 12
+                |    |    | 14
+                |    |    | 16
+(10 rows)
+
 -- full outer join three tables
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
                                                                                      QUERY PLAN                                                                                     
 ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c2, t3.c3
    ->  Foreign Scan
          Output: t1.c1, t2.c2, t3.c3
          Relations: ((public.ft2 t1) FULL JOIN (public.ft2 t2)) FULL JOIN (public.ft4 t3)
@@ -1446,28 +1474,28 @@ SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) LEFT
  16 |  6 | AAA016
  17 |  7 | 
  18 |  8 | AAA018
  19 |  9 | 
  20 |  0 | AAA020
 (10 rows)
 
 -- left outer join + right outer join
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
-                                                                                     QUERY PLAN                                                                                      
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                          QUERY PLAN                                                                                          
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c2, t3.c3
    ->  Foreign Scan
          Output: t1.c1, t2.c2, t3.c3
          Relations: (public.ft4 t3) LEFT JOIN ((public.ft2 t1) INNER JOIN (public.ft2 t2))
-         Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")))) ON (((r2."C 1" = r4.c1))))
+         Remote SQL: SELECT r4.c3, r1."C 1", r2.c2 FROM ("S 1"."T 3" r4 LEFT JOIN ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) ON (((r2."C 1" = r4.c1)) AND ((r1."C 1" = r2."C 1"))))
 (6 rows)
 
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
  c1 | c2 |   c3   
 ----+----+--------
  22 |  2 | AAA022
  24 |  4 | AAA024
  26 |  6 | AAA026
  28 |  8 | AAA028
  30 |  0 | AAA030
@@ -1506,30 +1534,30 @@ SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
     |  3
     |  9
     | 15
     | 21
 (10 rows)
 
 -- join two tables with FOR UPDATE clause
 -- tests whole-row reference for row marks
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
-                                                                                                                                                                                                               QUERY PLAN                                                                                                                                                                                                                
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                     
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
    ->  LockRows
          Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
          ->  Foreign Scan
                Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-               Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
+               Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                ->  Merge Join
                      Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                      Merge Cond: (t1.c1 = t2.c1)
                      ->  Sort
                            Output: t1.c1, t1.c3, t1.*
                            Sort Key: t1.c1
                            ->  Foreign Scan on public.ft1 t1
                                  Output: t1.c1, t1.c3, t1.*
                                  Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" FOR UPDATE
                      ->  Sort
@@ -1550,30 +1578,30 @@ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  105 | 105
  106 | 106
  107 | 107
  108 | 108
  109 | 109
  110 | 110
 (10 rows)
 
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
-                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                        
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                                                                                                             QUERY PLAN                                                                                                                                                                                                                              
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
    ->  LockRows
          Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
          ->  Foreign Scan
                Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-               Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
+               Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                ->  Merge Join
                      Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                      Merge Cond: (t1.c1 = t2.c1)
                      ->  Sort
                            Output: t1.c1, t1.c3, t1.*
                            Sort Key: t1.c1
                            ->  Foreign Scan on public.ft1 t1
                                  Output: t1.c1, t1.c3, t1.*
                                  Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" FOR UPDATE
                      ->  Sort
@@ -1595,30 +1623,30 @@ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  106 | 106
  107 | 107
  108 | 108
  109 | 109
  110 | 110
 (10 rows)
 
 -- join two tables with FOR SHARE clause
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
-                                                                                                                                                                                                               QUERY PLAN                                                                                                                                                                                                               
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                                     
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
    ->  LockRows
          Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
          ->  Foreign Scan
                Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-               Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
+               Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                ->  Merge Join
                      Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                      Merge Cond: (t1.c1 = t2.c1)
                      ->  Sort
                            Output: t1.c1, t1.c3, t1.*
                            Sort Key: t1.c1
                            ->  Foreign Scan on public.ft1 t1
                                  Output: t1.c1, t1.c3, t1.*
                                  Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" FOR SHARE
                      ->  Sort
@@ -1639,30 +1667,30 @@ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  105 | 105
  106 | 106
  107 | 107
  108 | 108
  109 | 109
  110 | 110
 (10 rows)
 
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
-                                                                                                                                                                                                                       QUERY PLAN                                                                                                                                                                                                                       
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                                                                                                            QUERY PLAN                                                                                                                                                                                                                             
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
    ->  LockRows
          Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
          ->  Foreign Scan
                Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-               Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
+               Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                ->  Merge Join
                      Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                      Merge Cond: (t1.c1 = t2.c1)
                      ->  Sort
                            Output: t1.c1, t1.c3, t1.*
                            Sort Key: t1.c1
                            ->  Foreign Scan on public.ft1 t1
                                  Output: t1.c1, t1.c3, t1.*
                                  Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" FOR SHARE
                      ->  Sort
@@ -1684,29 +1712,29 @@ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  106 | 106
  107 | 107
  108 | 108
  109 | 109
  110 | 110
 (10 rows)
 
 -- join in CTE
 EXPLAIN (VERBOSE, COSTS OFF)
 WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
-                                                             QUERY PLAN                                                              
--------------------------------------------------------------------------------------------------------------------------------------
+                                                                   QUERY PLAN                                                                   
+------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t.c1_1, t.c2_1, t.c1_3
    CTE t
      ->  Foreign Scan
            Output: t1.c1, t1.c3, t2.c1
            Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-           Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
+           Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
    ->  Sort
          Output: t.c1_1, t.c2_1, t.c1_3
          Sort Key: t.c1_3, t.c1_1
          ->  CTE Scan on t
                Output: t.c1_1, t.c2_1, t.c1_3
 (12 rows)
 
 WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10;
  c1_1 | c2_1 
 ------+------
@@ -1718,28 +1746,28 @@ WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
   106 |  106
   107 |  107
   108 |  108
   109 |  109
   110 |  110
 (10 rows)
 
 -- ctid with whole-row reference
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-                                                                                                                                                                                                   QUERY PLAN                                                                                                                                                                                                    
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                         
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
    ->  Foreign Scan
          Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
          Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-         Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
+         Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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 (TRUE)) WHERE ((r1."C 1" = r2."C 1")) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
 (6 rows)
 
 -- SEMI JOIN, not pushed down
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
                                          QUERY PLAN                                          
 ---------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1
    ->  Merge Semi Join
@@ -1948,109 +1976,109 @@ SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8
  109 | 109
  110 | 110
 (10 rows)
 
 -- join where unsafe to pushdown condition in WHERE clause has a column not
 -- in the SELECT clause. In this test unsafe clause needs to have column
 -- references from both joining sides so that the clause is not pushed down
 -- into one of the joining sides.
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-                                                                      QUERY PLAN                                                                       
--------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                            QUERY PLAN                                                                            
+------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c1, t1.c3
    ->  Sort
          Output: t1.c1, t2.c1, t1.c3
          Sort Key: t1.c3, t1.c1
          ->  Foreign Scan
                Output: t1.c1, t2.c1, t1.c3
                Filter: (t1.c8 = t2.c8)
                Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-               Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
+               Remote SQL: SELECT r1."C 1", r1.c3, r2."C 1", r1.c8, r2.c8 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
 (10 rows)
 
 SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = t2.c8 ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
  c1  | c1  
 -----+-----
  101 | 101
  102 | 102
  103 | 103
  104 | 104
  105 | 105
  106 | 106
  107 | 107
  108 | 108
  109 | 109
  110 | 110
 (10 rows)
 
 -- Aggregate after UNION, for testing setrefs
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
-                                                                     QUERY PLAN                                                                     
-----------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                          QUERY PLAN                                                                           
+---------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, (avg((t1.c1 + t2.c1)))
    ->  Sort
          Output: t1.c1, (avg((t1.c1 + t2.c1)))
          Sort Key: t1.c1
          ->  HashAggregate
                Output: t1.c1, avg((t1.c1 + t2.c1))
                Group Key: t1.c1
                ->  HashAggregate
                      Output: t1.c1, t2.c1
                      Group Key: t1.c1, t2.c1
                      ->  Append
                            ->  Foreign Scan
                                  Output: t1.c1, t2.c1
                                  Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-                                 Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
+                                 Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
                            ->  Foreign Scan
                                  Output: t1_1.c1, t2_1.c1
                                  Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-                                 Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1"))))
+                                 Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1"))
 (20 rows)
 
 SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10;
  t1c1 |         avg          
 ------+----------------------
   101 | 202.0000000000000000
   102 | 204.0000000000000000
   103 | 206.0000000000000000
   104 | 208.0000000000000000
   105 | 210.0000000000000000
   106 | 212.0000000000000000
   107 | 214.0000000000000000
   108 | 216.0000000000000000
   109 | 218.0000000000000000
   110 | 220.0000000000000000
 (10 rows)
 
 -- join with lateral reference
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
-                                                                             QUERY PLAN                                                                             
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                  QUERY PLAN                                                                                   
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1."C 1"
    ->  Nested Loop
          Output: t1."C 1"
          ->  Index Scan using t1_pkey on "S 1"."T 1" t1
                Output: t1."C 1", t1.c2, t1.c3, t1.c4, t1.c5, t1.c6, t1.c7, t1.c8
          ->  HashAggregate
                Output: t2.c1, t3.c1
                Group Key: t2.c1, t3.c1
                ->  Foreign Scan
                      Output: t2.c1, t3.c1
                      Relations: (public.ft1 t2) INNER JOIN (public.ft2 t3)
-                     Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))))
+                     Remote SQL: SELECT r1."C 1", r2."C 1" FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r1."C 1" = r2."C 1")) AND ((r1.c2 = $1::integer))
 (13 rows)
 
 SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  C 1 
 -----
    1
    1
    1
    1
    1
@@ -2088,54 +2116,54 @@ SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
     | 11
     | 12
  13 | 13
     | 14
     | 15
 (6 rows)
 
 -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
-                                                                                    QUERY PLAN                                                                                     
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                          QUERY PLAN                                                                                          
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Left Join
    Output: ft4.c1, (13), ft1.c1, ft2.c1
    Join Filter: (ft4.c1 = ft1.c1)
    ->  Foreign Scan on public.ft4
          Output: ft4.c1, ft4.c2, ft4.c3
          Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 10)) AND ((c1 <= 15))
    ->  Materialize
          Output: ft1.c1, ft2.c1, (13)
          ->  Foreign Scan
                Output: ft1.c1, ft2.c1, 13
                Relations: (public.ft1) INNER JOIN (public.ft2)
-               Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
+               Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (TRUE)) WHERE ((r5."C 1" = 12)) AND ((r4."C 1" = 12)) ORDER BY r4."C 1" ASC NULLS LAST
 (12 rows)
 
 SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  c1 | a  | b  | c  
 ----+----+----+----
  10 |    |    |   
  12 | 13 | 12 | 12
  14 |    |    |   
 (3 rows)
 
 -- join with nullable side with some columns with null values
 UPDATE ft5 SET c3 = null where c1 % 9 = 0;
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
-                                                                                                                                QUERY PLAN                                                                                                                                 
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                      QUERY PLAN                                                                                                                                      
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Foreign Scan
    Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
    Relations: (public.ft5) INNER JOIN (public.ft4)
-   Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
+   Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (TRUE)) WHERE ((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)) ORDER BY r1.c1 ASC NULLS LAST
 (4 rows)
 
 SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
       ft5       | c1 | c2 |   c3   | c1 | c2 
 ----------------+----+----+--------+----+----
  (12,13,AAA012) | 12 | 13 | AAA012 | 12 | 13
  (18,19,)       | 18 | 19 |        | 18 | 19
  (24,25,AAA024) | 24 | 25 | AAA024 | 24 | 25
  (30,31,AAA030) | 30 | 31 | AAA030 | 30 | 31
 (4 rows)
@@ -2281,26 +2309,26 @@ SELECT t1.c1, t2.c2 FROM v4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c
 ALTER VIEW v4 OWNER TO regress_view_owner;
 -- cleanup
 DROP OWNED BY regress_view_owner;
 DROP ROLE regress_view_owner;
 -- ===================================================================
 -- parameterized queries
 -- ===================================================================
 -- simple join
 PREPARE st1(int, int) AS SELECT t1.c3, t2.c3 FROM ft1 t1, ft2 t2 WHERE t1.c1 = $1 AND t2.c1 = $2;
 EXPLAIN (VERBOSE, COSTS OFF) EXECUTE st1(1, 2);
-                                                          QUERY PLAN                                                          
-------------------------------------------------------------------------------------------------------------------------------
+                                                               QUERY PLAN                                                                
+-----------------------------------------------------------------------------------------------------------------------------------------
  Foreign Scan
    Output: t1.c3, t2.c3
    Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
-   Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = 2)) AND ((r1."C 1" = 1))))
+   Remote SQL: SELECT r1.c3, r2.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (TRUE)) WHERE ((r2."C 1" = 2)) AND ((r1."C 1" = 1))
 (4 rows)
 
 EXECUTE st1(1, 1);
   c3   |  c3   
 -------+-------
  00001 | 00001
 (1 row)
 
 EXECUTE st1(101, 101);
   c3   |  c3   
@@ -2910,28 +2938,28 @@ UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING
   977 | 407 | 00977_update7      | Thu Mar 19 00:00:00 1970 PST | Thu Mar 19 00:00:00 1970 | 7  | 7          | foo
   987 | 407 | 00987_update7      | Sun Mar 29 00:00:00 1970 PST | Sun Mar 29 00:00:00 1970 | 7  | 7          | foo
   997 | 407 | 00997_update7      | Wed Apr 08 00:00:00 1970 PST | Wed Apr 08 00:00:00 1970 | 7  | 7          | foo
  1007 | 507 | 0000700007_update7 |                              |                          |    | ft2        | 
  1017 | 507 | 0001700017_update7 |                              |                          |    | ft2        | 
 (102 rows)
 
 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                                                                                                                                                         
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                                              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
+         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 (TRUE)) WHERE ((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
@@ -3053,28 +3081,28 @@ DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4;
   975 | Tue Mar 17 00:00:00 1970 PST
   985 | Fri Mar 27 00:00:00 1970 PST
   995 | Mon Apr 06 00:00:00 1970 PST
  1005 | 
  1015 | 
  1105 | 
 (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                                                                                                                               
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+                                                                                                                                    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
+         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 (TRUE)) WHERE ((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
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index daf0438..fb4b6af 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -396,20 +396,21 @@ static HeapTuple make_tuple_from_result_row(PGresult *res,
 						   List *retrieved_attrs,
 						   ForeignScanState *fsstate,
 						   MemoryContext temp_context);
 static void conversion_error_callback(void *arg);
 static bool foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel,
 				JoinType jointype, RelOptInfo *outerrel, RelOptInfo *innerrel,
 				JoinPathExtraData *extra);
 static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
 								 RelOptInfo *rel);
 static List *get_useful_ecs_for_relation(PlannerInfo *root, RelOptInfo *rel);
+static bool reltarget_has_non_vars(RelOptInfo *foreignrel);
 static void add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
 								Path *epq_path);
 
 
 /*
  * Foreign-data wrapper handler function: return a struct with pointers
  * to my callback routines.
  */
 Datum
 postgres_fdw_handler(PG_FUNCTION_ARGS)
@@ -648,20 +649,28 @@ postgresGetForeignRelSize(PlannerInfo *root,
 	fpinfo->relation_name = makeStringInfo();
 	namespace = get_namespace_name(get_rel_namespace(foreigntableid));
 	relname = get_rel_name(foreigntableid);
 	refname = rte->eref->aliasname;
 	appendStringInfo(fpinfo->relation_name, "%s.%s",
 					 quote_identifier(namespace),
 					 quote_identifier(relname));
 	if (*refname && strcmp(refname, relname) != 0)
 		appendStringInfo(fpinfo->relation_name, " %s",
 						 quote_identifier(rte->eref->aliasname));
+
+	/* Set the subquery information */
+	fpinfo->make_outerrel_subquery = false;
+	fpinfo->make_innerrel_subquery = false;
+	fpinfo->subquery_rels = NULL;
+
+	/* Set the relation index */
+	fpinfo->relation_index = baserel->relid;
 }
 
 /*
  * get_useful_ecs_for_relation
  *		Determine which EquivalenceClasses might be involved in useful
  *		orderings of this relation.
  *
  * This function is in some respects a mirror image of the core function
  * pathkeys_useful_for_merging: for a regular table, we know what indexes
  * we have and want to test whether any of them are useful.  For a foreign
@@ -3940,20 +3949,21 @@ static bool
 foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 				RelOptInfo *outerrel, RelOptInfo *innerrel,
 				JoinPathExtraData *extra)
 {
 	PgFdwRelationInfo *fpinfo;
 	PgFdwRelationInfo *fpinfo_o;
 	PgFdwRelationInfo *fpinfo_i;
 	ListCell   *lc;
 	List	   *joinclauses;
 	List	   *otherclauses;
+	Relids		relids;
 
 	/*
 	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
 	 * Constructing queries representing SEMI and ANTI joins is hard, hence
 	 * not considered right now.
 	 */
 	if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
 		jointype != JOIN_RIGHT && jointype != JOIN_FULL)
 		return false;
 
@@ -4004,24 +4014,24 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 	/*
 	 * deparseExplicitTargetList() isn't smart enough to handle anything other
 	 * than a Var.  In particular, if there's some PlaceHolderVar that would
 	 * need to be evaluated within this join tree (because there's an upper
 	 * reference to a quantity that may go to NULL as a result of an outer
 	 * join), then we can't try to push the join down because we'll fail when
 	 * we get to deparseExplicitTargetList().  However, a PlaceHolderVar that
 	 * needs to be evaluated *at the top* of this join tree is OK, because we
 	 * can do that locally after fetching the results from the remote side.
 	 */
+	relids = joinrel->relids;
 	foreach(lc, root->placeholder_list)
 	{
 		PlaceHolderInfo *phinfo = lfirst(lc);
-		Relids		relids = joinrel->relids;
 
 		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
 			bms_nonempty_difference(relids, phinfo->ph_eval_at))
 			return false;
 	}
 
 	/* Save the join clauses, for later use. */
 	fpinfo->joinclauses = joinclauses;
 
 	/*
@@ -4040,36 +4050,48 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 			fpinfo->local_conds = lappend(fpinfo->local_conds, expr);
 		else
 			fpinfo->remote_conds = lappend(fpinfo->remote_conds, expr);
 	}
 
 	fpinfo->outerrel = outerrel;
 	fpinfo->innerrel = innerrel;
 	fpinfo->jointype = jointype;
 
 	/*
+	 * By default both the joining relations are not required to be deparsed as
+	 * subqueries. But there might be some relations covered by the joining
+	 * relations that are required to be deparsed as subqueries. Those need to
+	 * be bubbled up to the topmost join that can be pushed down.
+	 */
+	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+
+	fpinfo->make_outerrel_subquery = false;
+	fpinfo->make_innerrel_subquery = false;
+	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+									  fpinfo_o->subquery_rels);
+
+	/*
 	 * Pull the other remote conditions from the joining relations into join
 	 * clauses or other remote clauses (remote_conds) of this relation
-	 * wherever possible. This avoids building subqueries at every join step,
-	 * which is not currently supported by the deparser logic.
+	 * wherever possible. This avoids building subqueries at every join step.
 	 *
 	 * For an inner join, clauses from both the relations are added to the
 	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
 	 * the outer side are added to remote_conds since those can be evaluated
 	 * after the join is evaluated. The clauses from inner side are added to
 	 * the joinclauses, since they need to evaluated while constructing the
 	 * join.
 	 *
 	 * For a FULL OUTER JOIN, the other clauses from either relation can not
 	 * be added to the joinclauses or remote_conds, since each relation acts
-	 * as an outer relation for the other. Consider such full outer join as
-	 * unshippable because of the reasons mentioned above in this comment.
+	 * as an outer relation for the other.
 	 *
 	 * The joining sides can not have local conditions, thus no need to test
 	 * shippability of the clauses being pulled up.
 	 */
 	switch (jointype)
 	{
 		case JOIN_INNER:
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
 										  list_copy(fpinfo_i->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
@@ -4084,41 +4106,57 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 			break;
 
 		case JOIN_RIGHT:
 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
 										  list_copy(fpinfo_o->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
 										  list_copy(fpinfo_i->remote_conds));
 			break;
 
 		case JOIN_FULL:
-			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
+			/*
+			 * We can't do anything here, and if there are any non-Vars in the
+			 * outerrel/innerrel's reltarget, give up pushing down this join,
+			 * because the deparsing logic can't support such a case currently.
+			 */
+			if (reltarget_has_non_vars(outerrel))
+				return false;
+			if (reltarget_has_non_vars(innerrel))
 				return false;
+
+			/*
+			 * If any of the joining relations have conditions and this is a
+			 * FULL OUTER join, we need to deparse that relation as a subquery
+			 * so that conditions can be evaluated before join. Remember it in
+			 * the fpinfo so that deparser can take appropriate action. We also
+			 * save the relids of the relations that are covered by subqueries
+			 * for deparser.
+			 */
+			if (fpinfo_o->remote_conds)
+			{
+				fpinfo->make_outerrel_subquery = true;
+				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
+														outerrel->relids);
+			}
+			if (fpinfo_i->remote_conds)
+			{
+				fpinfo->make_innerrel_subquery = true;
+				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
+														innerrel->relids);
+			}
 			break;
 
 		default:
 			/* Should not happen, we have just check this above */
 			elog(ERROR, "unsupported join type %d", jointype);
 	}
 
-	/*
-	 * For an inner join, all restrictions can be treated alike. Treating the
-	 * pushed down conditions as join conditions allows a top level full outer
-	 * join to be deparsed without requiring subqueries.
-	 */
-	if (jointype == JOIN_INNER)
-	{
-		Assert(!fpinfo->joinclauses);
-		fpinfo->joinclauses = fpinfo->remote_conds;
-		fpinfo->remote_conds = NIL;
-	}
-
 	/* Mark that this join can be pushed down safely */
 	fpinfo->pushdown_safe = true;
 
 	/*
 	 * If user is willing to estimate cost for a scan of either of the joining
 	 * relations using EXPLAIN, he intends to estimate scans on that relation
 	 * more accurately. Then, it makes sense to estimate the cost the join
 	 * with that relation more accurately using EXPLAIN.
 	 */
 	fpinfo->use_remote_estimate = fpinfo_o->use_remote_estimate ||
@@ -4166,23 +4204,56 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 	/*
 	 * Set the string describing this join relation to be used in EXPLAIN
 	 * output of corresponding ForeignScan.
 	 */
 	fpinfo->relation_name = makeStringInfo();
 	appendStringInfo(fpinfo->relation_name, "(%s) %s JOIN (%s)",
 					 fpinfo_o->relation_name->data,
 					 get_jointype_name(fpinfo->jointype),
 					 fpinfo_i->relation_name->data);
 
+	/*
+	 * Set the relation index.  This is defined as the position of this
+	 * joinrel in the join_rel_list list plus the length of the rtable list.
+	 * Note that since this joinrel is at the end of the list when we are
+	 * called, we can get the position by list_length.
+	 */
+	fpinfo->relation_index =
+		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+
 	return true;
 }
 
+/*
+ * Detect whether there are whole-row Vars or system columns other than ctid
+ * and oid in the given relation's reltarget.
+ *
+ * Note: currently deparseExplicitTargetList can't properly handle such Vars.
+ */
+static bool
+reltarget_has_non_vars(RelOptInfo *foreignrel)
+{
+	ListCell   *lc;
+
+	foreach(lc, foreignrel->reltarget->exprs)
+	{
+		Var		   *var = (Var *) lfirst(lc);
+
+		Assert(IsA(var, Var));
+		if (var->varattno <= 0 &&
+			var->varattno != SelfItemPointerAttributeNumber &&
+			var->varattno != ObjectIdAttributeNumber)
+			return true;
+	}
+	return false;
+}
+
 static void
 add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
 								Path *epq_path)
 {
 	List	   *useful_pathkeys_list = NIL;		/* List of all pathkeys */
 	ListCell   *lc;
 
 	useful_pathkeys_list = get_useful_pathkeys_for_relation(root, rel);
 
 	/* Create one path for each set of pathkeys we found above. */
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index 67126bc..bcf7ed8 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -85,20 +85,33 @@ typedef struct PgFdwRelationInfo
 	 * relations but is set for all relations. For join relation, the name
 	 * indicates which foreign tables are being joined and the join type used.
 	 */
 	StringInfo	relation_name;
 
 	/* Join information */
 	RelOptInfo *outerrel;
 	RelOptInfo *innerrel;
 	JoinType	jointype;
 	List	   *joinclauses;
+
+	/* Subquery information */
+	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+										 * subquery? */
+	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+										 * subquery? */
+	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+
+	/*
+	 * Index of the relation.  It is used for creating a subselect alias when
+	 * deparsing the relation as a subquery.
+	 */
+	int			relation_index;
 } PgFdwRelationInfo;
 
 /* in postgres_fdw.c */
 extern int	set_transmission_modes(void);
 extern void reset_transmission_modes(int nestlevel);
 
 /* in connection.c */
 extern PGconn *GetConnection(UserMapping *user, bool will_prep_stmt);
 extern void ReleaseConnection(PGconn *conn);
 extern unsigned int GetCursorNumber(PGconn *conn);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 4f68e89..53181fe 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -389,22 +389,25 @@ SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGH
 -- full outer join
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
 SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
 -- full outer join with restrictions on the joining relations
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
 -- full outer join + inner join
 EXPLAIN (VERBOSE, COSTS OFF)
-SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
-SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
+SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
 -- full outer join three tables
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 -- full outer join + right outer join
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 -- right outer join + full outer join
 EXPLAIN (VERBOSE, COSTS OFF)
#26Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#24)
Re: Push down more full joins in postgres_fdw

On 2016/10/22 17:19, Ashutosh Bapat wrote:

Review for postgres-fdw-more-full-join-pushdown-v6 patch.

The patch applies cleanly and regression is clean (make check in
regress directory and that in postgres_fdw).

Thanks for the review!

Here are some comments.
1. Because of the following code change, for a joinrel we might end up using
attrs_used, which will be garbage for a join rel. The assumption that tlist can
not be NIL for a join relation is wrong. Even for a join relation, tlist can be
NULL.  Consider query 'explain verbose select count(*) from ft1, ft2' Since
count(*) doesn't need any columns, the tlist is NIL. With the patch the server
crashes for this query.
-    if (foreignrel->reloptkind == RELOPT_JOINREL)
+    /*
+     * Note: tlist for a base relation might be non-NIL.  For example, if the
+     * base relation is an operand of a foreign join performing a full outer
+     * join and has non-NIL remote_conds, the base relation will be deparsed
+     * as a subquery, so the tlist for the base relation could be non-NIL.
+     */
+    if (tlist != NIL)
{
-        /* For a join relation use the input tlist */

We can not decide whether to use deparseExplicitTargetList or
deparse it from attrs_used based on the tlist. SELECT lists, whether it's
topmost SELECT or a subquery (even on a base relation), for deparsing a
JOIN query need to use deparseExplicitTargetList.

Will fix.

2. The code in deparseRangeTblRef() dealing with subqueries, is very similar to
deparseSelectStmtForRel(). The only thing deparseRangeTblRef() does not require
is the appending ORDER BY, which is determined by existence of pathkeys
argument. Probably we should reuse deparseSelectStmtForRel(), instead of
duplicating the code. This will also make the current changes to
deparseSelectSql unnecessary.

Will do.

3. Why do we need following change? The elog() just few lines above says that
we expect only Var nodes. Why then we use deparseExpr() instead of
deparseVar()?
if (i > 0)
appendStringInfoString(buf, ", ");
-        deparseVar(var, context);
+        deparseExpr((Expr *) var, context);

*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);

And I get the answer for that question, somewhere down in the patch
+    /*
+     * If the given expression is an output column of a subquery-in-FROM,
+     * deparse the alias to the expression instead.
+     */
+    if (IsA(node, Var))
+    {
+        int            tabno;
+        int            colno;
+
+        if (isSubqueryExpr(node, context->foreignrel, &tabno, &colno))
+        {
+            appendStringInfo(context->buf, "%s%d.%s%d",
+                             SS_TAB_ALIAS_PREFIX, tabno,
+                             SS_COL_ALIAS_PREFIX, colno);
+            return;
+        }
+    }
+

Functionally, this code belongs to deparseVar() and not deparseExpr(). Like all
other functions which handle Expr nodes, deparseExpr() is expected to be a
dispatcher to the node specific function.

OK, will change that way.

4. This comment is good to have there, but is unrelated to this patch. May be a
separate patch?
+ /* Don't generate bad syntax if no columns */

5. Changed comment doesn't say anything different from the original comment. It
may have been good to have it this way to start with, but changing it now
doesn't bring anything new. You seem to have just merged prologue of the
function with a comment in that function. I think, this change is unnecessary.
- * 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.
+ * For a join relation the clause of the following form is appended to buf:
+ * ((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.
+    /* Don't generate bad syntax if no columns */
6. Change may be good but unrelated to this patch. May be material for a
separate patch. There are few such changes in this function. While these
changes may be good by themselves, they distract reviewer from the goal of the
patch.
-            appendStringInfo(buf, "(");
+            appendStringInfoChar(buf, '(');

OK on #4, #5, and #6, Will remove all the changes. I will leave those
for separate patches.

7. I don't understand why you need to change this function so much. Take for
example the following change.
-        RelOptInfo *rel_o = fpinfo->outerrel;
-        RelOptInfo *rel_i = fpinfo->innerrel;
-        StringInfoData join_sql_o;
-        StringInfoData join_sql_i;
+        /* Begin the FROM clause entry */
+        appendStringInfoChar(buf, '(');
/* Deparse outer relation */
-        initStringInfo(&join_sql_o);
-        deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
+        deparseRangeTblRef(buf, root,
+                           fpinfo->outerrel,
+                           fpinfo->make_outerrel_subquery,
+                           params_list);
/* Deparse outer relation */
-        initStringInfo(&join_sql_o);
-        deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
+        deparseRangeTblRef(buf, root,
+                           fpinfo->outerrel,
+                           fpinfo->make_outerrel_subquery,
+                           params_list);
It removes few variables, instead directly accesses the members from fpinfo.
Also, deparses the individual relations in the provided buffer directly, rather
than in separate buffers in the original code. Instead of this, all you had to
do was replace a call
-        deparseFromExprForRel(&join_sql_o, root, rel_o, true, params_list);
with
+        deparseRangeTblRef(&join_sql_o, root, rel_o,
fpinfo->make_outerrel_subquery, params_list)
Similarly for the inner relation. Again, the changes you have done might have
been good, if done in the original code, but doing those in this patch just
creates distractions and increases the size of the patch.

I did that for efficiency because deparsing the relation directly into
the given buffer would save cycles, but I agree that that is another
issue. Will remove that change, and leave that for another patch.

8. Why have you changed the name of variable here?
-        if (use_alias)
+        if (add_rel_alias)

Sorry, I forgot to remove this change from the patch. Will fix.

9. In deparseRangeTblRef(), the targetlist for the subquery is obtained by
calling build_tlist_to_deparse(), but appendSubselectAlias() constructs the
column aliases based on foreignrel->reltarget->exprs. Those two are usually
same, but there is no code which guarantees that. Instead, pass tlist to
appendSubselectAlias() or simply pass the number of entries in that list
(list_length(tlist), which is all it needs. OR use foreignrel->reltarget->exprs
directly instead of calling build_tlist_to_deparse(). Similar problem exists
with getSubselectAliasInfo().

Actually, the patch guarantees that since in that case (1)
foreignrel->reltarget->exprs doesn't contain any PHVs (note that passing
the following check in foreign_join_ok implies that
foreignrel->reltarget->exprs of the input rels for a given foreign join
doesn't contain any PHVs and (2) we have fpinfo->local_conds == NIL, so
build_tlist_to_deparse() would produce a tlist equivalent to the
foreignrel->reltarget->exprs. But as you proposed, to make the code
easier to understand, I will change to use foreignrel->reltarget->exprs
directly.

/*
* deparseExplicitTargetList() isn't smart enough to handle
anything other
* than a Var. In particular, if there's some PlaceHolderVar that
would
* need to be evaluated within this join tree (because there's an upper
* reference to a quantity that may go to NULL as a result of an outer
* join), then we can't try to push the join down because we'll
fail when
* we get to deparseExplicitTargetList(). However, a
PlaceHolderVar that
* needs to be evaluated *at the top* of this join tree is OK,
because we
* can do that locally after fetching the results from the remote side.
*/
relids = joinrel->relids;
foreach(lc, root->placeholder_list)
{
PlaceHolderInfo *phinfo = lfirst(lc);

if (bms_is_subset(phinfo->ph_eval_at, relids) &&
bms_nonempty_difference(relids, phinfo->ph_eval_at))
return false;
}

10. Deparsing a query in the form of a subquery makes it hard-to-read. The
original code aimed at avoiding a subquery. Also, this change has created many
expected output changes, which again seem unnecessary. In fact, having the
pushable join clauses of an inner join in ON clause, which is closer to JOIN
clause, is better than having them farther in the WHERE clause.
- /*
- * For an inner join, all restrictions can be treated alike. Treating the
- * pushed down conditions as join conditions allows a top level full outer
- * join to be deparsed without requiring subqueries.
- */
- if (jointype == JOIN_INNER)
- {
- Assert(!fpinfo->joinclauses);
- fpinfo->joinclauses = fpinfo->remote_conds;
- fpinfo->remote_conds = NIL;
- }

I added this change for another patch that I proposed for extending the
DML pushdown in 9.6 so that it can perform an update/delete on a join
remotely. To create a remote query for that, I think we need to pull up
inner join conditions mentioning the target relation in the JOIN/ON
clauses into the WHERE clause. But I have to admit that's unrelated to
this patch, so I will leave that as-is.

11. I have reworded following comment and restructured the code that follows it
in the attached patch.
+    /*
+     * Set the subquery information.  If the relation performs a full outer
+     * join and if the input relations have non-NIL remote_conds, the input
+     * relations need to be deparsed as a subquery.
+     */
The code esp. the if .. else .. block followed by another one, made it a bit
unreadable. Hence I restructured it so that it's readable and doesn't require
variable "relids" from earlier code, which was being reused. Let me know if
this change looks good.

That's a good idea. Thanks!

12. The code below deletes the condition, which is understandable.
-            if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
But why does it add a new unrelated condition here? What the comment claims,
looks like an existing bug, unrelated to the patch. Can you please give an
example? If that it's an existing bug, it should be fixed as a separate patch.
I don't understand the relation of this code with what the patch is doing.
+            /*
+             * We can't do anything here, and if there are any non-Vars in the
+             * outerrel/innerrel's reltarget, give up pushing down this join,
+             * because the deparsing logic can't support such a case currently.
+             */
+            if (reltarget_has_non_vars(outerrel))
+                return false;
+            if (reltarget_has_non_vars(innerrel))
return false;

I thought the current deparsing logic wouldn't work well if the target
list of a given subquery contained eg, system columns other than ctid
and oid, but I noticed that I was wrong; it can, so we don't need this
restriction. Will remove. (I don't think there is any bug.)

13. The comment below is missing the main purpose i.e. creating a a unique
alias, in case the relation gets converted into a subquery. Lowest or highest
relid will create a unique alias at given level of join and that would be more
future proof. If we decide to consider paths for every join order, following
comment will no more be true.
+    /*
+     * Set the relation index.  This is defined as the position of this
+     * joinrel in the join_rel_list list plus the length of the rtable list.
+     * Note that since this joinrel is at the end of the list when we are
+     * called, we can get the position by list_length.
+     */
+    fpinfo->relation_index =
+        list_length(root->parse->rtable) + list_length(root->join_rel_list);

I don't agree on that point. As I said before, the approach using the
lowest/highest relid would make a remote query having nested subqueries
difficult to read. And even if we decided to consider paths for every
join order, we could define the relation_index safely, by modifying
postgresGetForeignJoinPaths so that it (1) sets the relation_index the
proposed way at the first time it is called and (2) avoids setting the
relation_index after the first call, by checking to see
fpinfo->relation_index > 0.

14. The function name talks about non-vars but the Assert few lines below
asserts that every node there is a Var. Need better naming.
+reltarget_has_non_vars(RelOptInfo *foreignrel)
+{
+    ListCell   *lc;
+
+    foreach(lc, foreignrel->reltarget->exprs)
+    {
+        Var           *var = (Var *) lfirst(lc);
+
+        Assert(IsA(var, Var));
And also an explanation for the claim
+ * Note: currently deparseExplicitTargetList can't properly handle such Vars.

Will remove this function for the reason described in #12.

15. While deparsing every Var, we are descending the RelOptInfo hierarchy.

Right. I think that not only avoids creating redundant data to find the
alias of a given Var node but also would be able to find it in optimal cost.

Instead of that, we should probably gather all the alias information once and
store it in context as an array indexed by relid. While deparsing a Var we can
get the targetlist and alias for a given relation by using var->varno as index.
And then search for given Var node in the targetlist to deparse the column name
by its position in the targetlist. For the relations that are not converted
into subqueries, this array will not hold any information and the Var will be
deparsed as it's being done today.

Sorry, I don't understand this part. How does that work when creating a
remote query having nested subqueries? Is that able to search for a
given Var node efficiently?

Testing
-------
1. The code is changing deparser but doesn't have testcases
testing impact of the code. For example, we should have testcases with remote
query deparsed as nested subqueries or nested subqueries within subqueries and
so on May be testcases where a relation deeper in the RelOptInfo hierarchy has
conditions but it's immediate upper relation does not have those.

Will do.

2. The only testcase added by this patch, copies an existing case and adds a
whole row reference to one of the relations being joined. Instead we could add
that whole-row reference to the existing testcase itself.

Will do.

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

#27Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#26)
Re: Push down more full joins in postgres_fdw
13. The comment below is missing the main purpose i.e. creating a a unique
alias, in case the relation gets converted into a subquery. Lowest or
highest
relid will create a unique alias at given level of join and that would be
more
future proof. If we decide to consider paths for every join order,
following
comment will no more be true.
+    /*
+     * Set the relation index.  This is defined as the position of this
+     * joinrel in the join_rel_list list plus the length of the rtable
list.
+     * Note that since this joinrel is at the end of the list when we are
+     * called, we can get the position by list_length.
+     */
+    fpinfo->relation_index =
+        list_length(root->parse->rtable) +
list_length(root->join_rel_list);

I don't agree on that point. As I said before, the approach using the
lowest/highest relid would make a remote query having nested subqueries
difficult to read. And even if we decided to consider paths for every join
order, we could define the relation_index safely, by modifying
postgresGetForeignJoinPaths so that it (1) sets the relation_index the
proposed way at the first time it is called and (2) avoids setting the
relation_index after the first call, by checking to see
fpinfo->relation_index > 0.

OK. Although, the alias which contains a number, which apparently
doesn't show any relation with the relation (no pun intended :)) being
deparsed, won't make much sense in the deparsed query. But I am fine
leaving this to the committer's judgement. May be you want to add an
assert here making sure that relation_index is set only once.
Something like Assert(fpinfo->relation_index == 0) just before setting
it.

14. The function name talks about non-vars but the Assert few lines below
asserts that every node there is a Var. Need better naming.
+reltarget_has_non_vars(RelOptInfo *foreignrel)
+{
+    ListCell   *lc;
+
+    foreach(lc, foreignrel->reltarget->exprs)
+    {
+        Var           *var = (Var *) lfirst(lc);
+
+        Assert(IsA(var, Var));
And also an explanation for the claim
+ * Note: currently deparseExplicitTargetList can't properly handle such
Vars.

Will remove this function for the reason described in #12.

15. While deparsing every Var, we are descending the RelOptInfo hierarchy.

Right. I think that not only avoids creating redundant data to find the
alias of a given Var node but also would be able to find it in optimal cost.

Instead of that, we should probably gather all the alias information once
and
store it in context as an array indexed by relid. While deparsing a Var we
can
get the targetlist and alias for a given relation by using var->varno as
index.
And then search for given Var node in the targetlist to deparse the column
name
by its position in the targetlist. For the relations that are not
converted
into subqueries, this array will not hold any information and the Var will
be
deparsed as it's being done today.

Sorry, I don't understand this part. How does that work when creating a
remote query having nested subqueries? Is that able to search for a given
Var node efficiently?

For every relation that is deparsed as a subquery, we will create a
separate context. Each such context will have an array described
above. This array will contain the targetlist and aliases for all the
relations, covered by that relation, that are required to be deparsed
as subqueries. These arrays bubble up to topmost relation that is not
required to be deparsed as subquery. When a relation is required to be
deparsed as a subquery, its immediate upper relation sets the alias
and targetlist chosen for that relation at all the indexes
corresponding to all the base relations covered by that relation. For
example, let's assume that a relation (1, 2, 3) is required to be
deparsed as subquery for an immediate upper relation (1, 2, 3, 4, 5)
(thus the other joining relation being (4,5)). While deparsing for
relation (1,2,3,4,5), the context will contain a 5 element array, with
positions 1, 2, 3 filled by same targetlist and alias whereas
positions 4 and 5 will not be filled as those relations are not
deparsed as subqueries. Let's assume in relation (1, 2, 3), (1, 3) in
turn requires subquery but (2) does not. Thus the context created
while deparsing (1, 2, 3) will have a 3 element array with positions 1
and 3 containing the same targetlist and alias, where as position 2
will be empty. When deparsing a Var node with varno = N and varattno =
m, if the nth position in the array in the context is empty, that Var
node will be deparsed as rN.<column name>. But if that position is has
alias sZ, then we search for that Var node in the targetlist and if
it's found at kth position in the targetlist, we will deparse it as
sZ.ck. The search in the targetlist can be performed using
tlist_member, and then fetching the position by TargetEntry::resno.
This does not require any recursion and thus saves stack space and
some CPU cycles required for recursion. I guess, the arrays need to be
computed only once for any relation when the query for that relation
is deparsed the first time.

Thanks for considering other suggestions.

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

#28Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#27)
Re: Push down more full joins in postgres_fdw

On 2016/10/25 18:58, Ashutosh Bapat wrote:

You wrote:

13. The comment below is missing the main purpose i.e. creating a a unique
alias, in case the relation gets converted into a subquery. Lowest or
highest
relid will create a unique alias at given level of join and that would be
more
future proof. If we decide to consider paths for every join order,
following
comment will no more be true.
+    /*
+     * Set the relation index.  This is defined as the position of this
+     * joinrel in the join_rel_list list plus the length of the rtable
list.
+     * Note that since this joinrel is at the end of the list when we are
+     * called, we can get the position by list_length.
+     */
+    fpinfo->relation_index =
+        list_length(root->parse->rtable) +
list_length(root->join_rel_list);

I wrote:

I don't agree on that point. As I said before, the approach using the
lowest/highest relid would make a remote query having nested subqueries
difficult to read. And even if we decided to consider paths for every join
order, we could define the relation_index safely, by modifying
postgresGetForeignJoinPaths so that it (1) sets the relation_index the
proposed way at the first time it is called and (2) avoids setting the
relation_index after the first call, by checking to see
fpinfo->relation_index > 0.

OK. Although, the alias which contains a number, which apparently
doesn't show any relation with the relation (no pun intended :)) being
deparsed, won't make much sense in the deparsed query. But I am fine
leaving this to the committer's judgement.

Fine with me.

May be you want to add an
assert here making sure that relation_index is set only once.
Something like Assert(fpinfo->relation_index == 0) just before setting
it.

Will do.

15. While deparsing every Var, we are descending the RelOptInfo hierarchy.

Right. I think that not only avoids creating redundant data to find the
alias of a given Var node but also would be able to find it in optimal cost.

Instead of that, we should probably gather all the alias information once
and
store it in context as an array indexed by relid. While deparsing a Var we
can
get the targetlist and alias for a given relation by using var->varno as
index.
And then search for given Var node in the targetlist to deparse the column
name
by its position in the targetlist. For the relations that are not
converted
into subqueries, this array will not hold any information and the Var will
be
deparsed as it's being done today.

Sorry, I don't understand this part. How does that work when creating a
remote query having nested subqueries? Is that able to search for a given
Var node efficiently?

For every relation that is deparsed as a subquery, we will create a
separate context. Each such context will have an array described
above. This array will contain the targetlist and aliases for all the
relations, covered by that relation, that are required to be deparsed
as subqueries. These arrays bubble up to topmost relation that is not
required to be deparsed as subquery. When a relation is required to be
deparsed as a subquery, its immediate upper relation sets the alias
and targetlist chosen for that relation at all the indexes
corresponding to all the base relations covered by that relation.

Thanks for the explanation!

For
example, let's assume that a relation (1, 2, 3) is required to be
deparsed as subquery for an immediate upper relation (1, 2, 3, 4, 5)
(thus the other joining relation being (4,5)). While deparsing for
relation (1,2,3,4,5), the context will contain a 5 element array, with
positions 1, 2, 3 filled by same targetlist and alias whereas
positions 4 and 5 will not be filled as those relations are not
deparsed as subqueries.

Sorry, I don't understand this. In that case, the immediate upper
relation (1, 2, 3, 4, 5) would need to fill the targetlist and aliases
for *the join relation (1, 2, 3) somewhere*, not the targetlist and
aliases for each of the component relations 1, 2, and 3, because the
join relation is deparsed as a subquery. Maybe I'm missing something,
though.

Let's assume in relation (1, 2, 3), (1, 3) in
turn requires subquery but (2) does not. Thus the context created
while deparsing (1, 2, 3) will have a 3 element array with positions 1
and 3 containing the same targetlist and alias, where as position 2
will be empty.

When deparsing a Var node with varno = N and varattno =
m, if the nth position in the array in the context is empty, that Var
node will be deparsed as rN.<column name>.

What happens when deparsing eg, a Var with varno = 2 at the topmost
relation (1, 2, 3, 4, 5)? The second position of the array is empty,
but the join relation (1, 2, 3) is deparsed as a subquery, so the Var
should be deparsed as an alias of an output column of the subquery at
the topmost relation, I think.

But if that position is has
alias sZ, then we search for that Var node in the targetlist and if
it's found at kth position in the targetlist, we will deparse it as
sZ.ck. The search in the targetlist can be performed using
tlist_member, and then fetching the position by TargetEntry::resno.

This does not require any recursion and thus saves stack space and
some CPU cycles required for recursion.

Is that true?

some CPU cycles required for recursion. I guess, the arrays need to be
computed only once for any relation when the query for that relation
is deparsed the first time.

Does this algorithm extend to the case where we consider paths for every
join order?

Thanks for considering other suggestions.

Your suggestions are really helpful to improve the patch. Thanks!

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

#29Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#28)
Re: Push down more full joins in postgres_fdw

For every relation that is deparsed as a subquery, we will create a
separate context. Each such context will have an array described
above. This array will contain the targetlist and aliases for all the
relations, covered by that relation, that are required to be deparsed
as subqueries. These arrays bubble up to topmost relation that is not
required to be deparsed as subquery. When a relation is required to be
deparsed as a subquery, its immediate upper relation sets the alias
and targetlist chosen for that relation at all the indexes
corresponding to all the base relations covered by that relation.

Thanks for the explanation!

For
example, let's assume that a relation (1, 2, 3) is required to be
deparsed as subquery for an immediate upper relation (1, 2, 3, 4, 5)
(thus the other joining relation being (4,5)). While deparsing for
relation (1,2,3,4,5), the context will contain a 5 element array, with
positions 1, 2, 3 filled by same targetlist and alias whereas
positions 4 and 5 will not be filled as those relations are not
deparsed as subqueries.

Sorry, I don't understand this. In that case, the immediate upper relation
(1, 2, 3, 4, 5) would need to fill the targetlist and aliases for *the join
relation (1, 2, 3) somewhere*, not the targetlist and aliases for each of
the component relations 1, 2, and 3, because the join relation is deparsed
as a subquery. Maybe I'm missing something, though.

The description above does not specify "targetlist and alias" for each
of (1, 2, 3). The array in the context will have positions 1, 2, 3
filled with *same* alias and targetlist which is derived from relation
(1, 2, 3).

Let's assume in relation (1, 2, 3), (1, 3) in
turn requires subquery but (2) does not. Thus the context created
while deparsing (1, 2, 3) will have a 3 element array with positions 1
and 3 containing the same targetlist and alias, where as position 2
will be empty.

When deparsing a Var node with varno = N and varattno =
m, if the nth position in the array in the context is empty, that Var
node will be deparsed as rN.<column name>.

What happens when deparsing eg, a Var with varno = 2 at the topmost relation
(1, 2, 3, 4, 5)? The second position of the array is empty, but the join
relation (1, 2, 3) is deparsed as a subquery, so the Var should be deparsed
as an alias of an output column of the subquery at the topmost relation, I
think.

position 2 will not be empty, it will be filled by the alias and
targetlist derived from relation (1, 2, 3).

But if that position is has
alias sZ, then we search for that Var node in the targetlist and if
it's found at kth position in the targetlist, we will deparse it as
sZ.ck. The search in the targetlist can be performed using
tlist_member, and then fetching the position by TargetEntry::resno.

This does not require any recursion and thus saves stack space and
some CPU cycles required for recursion.

Is that true?

Yes, unless you explain why is that false.

some CPU cycles required for recursion. I guess, the arrays need to be
computed only once for any relation when the query for that relation
is deparsed the first time.

Does this algorithm extend to the case where we consider paths for every
join order?

Yes, if we store the information about which of relations need
subquery and which don't for every join order.

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

#30Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#29)
Re: Push down more full joins in postgres_fdw

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

You wrote:

For
example, let's assume that a relation (1, 2, 3) is required to be
deparsed as subquery for an immediate upper relation (1, 2, 3, 4, 5)
(thus the other joining relation being (4,5)). While deparsing for
relation (1,2,3,4,5), the context will contain a 5 element array, with
positions 1, 2, 3 filled by same targetlist and alias whereas
positions 4 and 5 will not be filled as those relations are not
deparsed as subqueries.

I wrote:

Sorry, I don't understand this. In that case, the immediate upper relation
(1, 2, 3, 4, 5) would need to fill the targetlist and aliases for *the join
relation (1, 2, 3) somewhere*, not the targetlist and aliases for each of
the component relations 1, 2, and 3, because the join relation is deparsed
as a subquery. Maybe I'm missing something, though.

The description above does not specify "targetlist and alias" for each
of (1, 2, 3). The array in the context will have positions 1, 2, 3
filled with *same* alias and targetlist which is derived from relation
(1, 2, 3).

OK

Let's assume in relation (1, 2, 3), (1, 3) in
turn requires subquery but (2) does not. Thus the context created
while deparsing (1, 2, 3) will have a 3 element array with positions 1
and 3 containing the same targetlist and alias, where as position 2
will be empty.

When deparsing a Var node with varno = N and varattno =
m, if the nth position in the array in the context is empty, that Var
node will be deparsed as rN.<column name>.

What happens when deparsing eg, a Var with varno = 2 at the topmost relation
(1, 2, 3, 4, 5)? The second position of the array is empty, but the join
relation (1, 2, 3) is deparsed as a subquery, so the Var should be deparsed
as an alias of an output column of the subquery at the topmost relation, I
think.

position 2 will not be empty, it will be filled by the alias and
targetlist derived from relation (1, 2, 3).

OK

But if that position is has
alias sZ, then we search for that Var node in the targetlist and if
it's found at kth position in the targetlist, we will deparse it as
sZ.ck. The search in the targetlist can be performed using
tlist_member, and then fetching the position by TargetEntry::resno.

This does not require any recursion and thus saves stack space and
some CPU cycles required for recursion.

Is that true?

Yes, unless you explain why is that false.

OK, that would be true, I think.

I guess, the arrays need to be
computed only once for any relation when the query for that relation
is deparsed the first time.

Does this algorithm extend to the case where we consider paths for every
join order?

Yes, if we store the information about which of relations need
subquery and which don't for every join order.

Hmm. Sorry, I'm not so excited about this proposal. I think (1) that
is solving a problem that hasn't been proven to be a problem, (2) that
would complicate the deparser logic, and (3) the cost of creating this
array for each relation by the bottom-up method while deparsing a remote
query would be not small (especially when the query is large), so that
might need more cycles for deparsing the query than what I proposed when
use_remote_estimate=false. So, I'd like to go with what I proposed, at
least as the first cut.

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

#31Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#30)
Re: Push down more full joins in postgres_fdw

I guess, the arrays need to be
computed only once for any relation when the query for that relation
is deparsed the first time.

Does this algorithm extend to the case where we consider paths for every
join order?

Yes, if we store the information about which of relations need
subquery and which don't for every join order.

Hmm. Sorry, I'm not so excited about this proposal. I think (1) that is
solving a problem that hasn't been proven to be a problem, (2) that would
complicate the deparser logic, and (3) the cost of creating this array for
each relation by the bottom-up method while deparsing a remote query would
be not small (especially when the query is large), so that might need more
cycles for deparsing the query than what I proposed when
use_remote_estimate=false. So, I'd like to go with what I proposed, at
least as the first cut.

The arrays will need to computed only when there is at least one
relation required to be deparsed as subquery. Every relation above the
relation which is converted into subquery requires the array. Each
array will be N * size of pointer long where N is the largest relid
covered by that relation. N will vary across the RelOptInfo hierarchy.
All that array holds is targetlist pointer which are required to
computed anyway. So, the amount of memory is bounded by N * size of
pointer * (2N - 1), which is way lesser than what we use in other
places.

Your patch calls isSubqueryExpr() recursively for every Var in the
targetlist, which can be many for foreign tables with many columns.
For every such Var it may need to reach upto the relation which is
converted into subquery, which can as bad as reaching every base
relation. So, it looks like the number of recursive calls to
isSubqueryExpr() is bounded by V * N (i.e. worst case depth of the
RelOptInfo tree), which can be quite costly. If use_remote_estimates
is true, we do this for every intermediate relation and for every path
created. That isn't very performance efficient.

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

#32Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#31)
Re: Push down more full joins in postgres_fdw

On 2016/10/26 18:25, Ashutosh Bapat wrote:

Your patch calls isSubqueryExpr() recursively for every Var in the
targetlist, which can be many for foreign tables with many columns.
For every such Var it may need to reach upto the relation which is
converted into subquery, which can as bad as reaching every base
relation. So, it looks like the number of recursive calls to
isSubqueryExpr() is bounded by V * N (i.e. worst case depth of the
RelOptInfo tree), which can be quite costly. If use_remote_estimates
is true, we do this for every intermediate relation and for every path
created. That isn't very performance efficient.

In practice, the search cost would be negligible compared to the cost of
explaining/executing the query.

My concern about your proposal is: it might not be worth complicating
the code to solve a problem that is actually not a problem in practice.

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

#33Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#32)
Re: Push down more full joins in postgres_fdw

On Wed, Oct 26, 2016 at 3:35 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/10/26 18:25, Ashutosh Bapat wrote:

Your patch calls isSubqueryExpr() recursively for every Var in the
targetlist, which can be many for foreign tables with many columns.
For every such Var it may need to reach upto the relation which is
converted into subquery, which can as bad as reaching every base
relation. So, it looks like the number of recursive calls to
isSubqueryExpr() is bounded by V * N (i.e. worst case depth of the
RelOptInfo tree), which can be quite costly. If use_remote_estimates
is true, we do this for every intermediate relation and for every path
created. That isn't very performance efficient.

In practice, the search cost would be negligible compared to the cost of
explaining/executing the query.

My concern about your proposal is: it might not be worth complicating the
code to solve a problem that is actually not a problem in practice.

To me the current code looks complicated esp. because of the recursion
involved and usage of out parameters to isSubqueryExpr(). My
suggestion goes inline with the current method of deparsing a Var.

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

#34Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#33)
Re: Push down more full joins in postgres_fdw

On 2016/10/26 19:53, Ashutosh Bapat wrote:

On Wed, Oct 26, 2016 at 3:35 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

In practice, the search cost would be negligible compared to the cost of
explaining/executing the query.

My concern about your proposal is: it might not be worth complicating the
code to solve a problem that is actually not a problem in practice.

To me the current code looks complicated esp. because of the recursion
involved and usage of out parameters to isSubqueryExpr().

I don't think so. isSubqueryExpr is prety small, written in less than
50 lines, and the code looks rather simple to me.

My
suggestion goes inline with the current method of deparsing a Var.

Yeah, I think your approach makes it easy to search for the alias to a
given Var from the array you proposed. I think the complexity of your
approach would be in extra work for building and maintaining the array
while deparsing the query. I think that would probably need more
invasive and much larger changes to the existing code than what I proposed.

I think this issue is optional, so I'd like to propose to leave this for
the committer's judge.

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

#35Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#34)
Re: Push down more full joins in postgres_fdw

On Thu, Oct 27, 2016 at 12:46 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/10/26 19:53, Ashutosh Bapat wrote:

On Wed, Oct 26, 2016 at 3:35 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

In practice, the search cost would be negligible compared to the cost of
explaining/executing the query.

My concern about your proposal is: it might not be worth complicating the
code to solve a problem that is actually not a problem in practice.

To me the current code looks complicated esp. because of the recursion
involved and usage of out parameters to isSubqueryExpr().

I don't think so. isSubqueryExpr is prety small, written in less than 50
lines, and the code looks rather simple to me.

My
suggestion goes inline with the current method of deparsing a Var.

Yeah, I think your approach makes it easy to search for the alias to a given
Var from the array you proposed. I think the complexity of your approach
would be in extra work for building and maintaining the array while
deparsing the query. I think that would probably need more invasive and
much larger changes to the existing code than what I proposed.

I think this issue is optional, so I'd like to propose to leave this for the
committer's judge.

Fine with me.

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

#36Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#35)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/10/27 18:06, Ashutosh Bapat wrote:

On Thu, Oct 27, 2016 at 12:46 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/10/26 19:53, Ashutosh Bapat wrote:

On Wed, Oct 26, 2016 at 3:35 PM, Etsuro Fujita

My concern about your proposal is: it might not be worth complicating the
code to solve a problem that is actually not a problem in practice.

To me the current code looks complicated esp. because of the recursion
involved and usage of out parameters to isSubqueryExpr().

I don't think so. isSubqueryExpr is prety small, written in less than 50
lines, and the code looks rather simple to me.

My
suggestion goes inline with the current method of deparsing a Var.

Yeah, I think your approach makes it easy to search for the alias to a given
Var from the array you proposed. I think the complexity of your approach
would be in extra work for building and maintaining the array while
deparsing the query. I think that would probably need more invasive and
much larger changes to the existing code than what I proposed.

I think this issue is optional, so I'd like to propose to leave this for the
committer's judge.

Fine with me.

OK

Here is the updated version, which includes the restructuring you
proposed. Other than the above issue and the alias issue we discussed,
I addressed all your comments except one on testing; I tried to add test
cases where the remote query is deparsed as nested subqueries, but I
couldn't because IIUC, reduce_outer_joins reduced full joins to inner
joins or left joins. So, I added two test cases: (1) the joining
relations are both base relations (actually, we already have that) and
(2) one of the joining relations is a base relation and the other is a
join relation. I rebased the patch to HEAD, so I added a test case with
aggregate pushdown also.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v1.patchapplication/x-patch; name=postgres-fdw-subquery-support-v1.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 159,172 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
- static void deparseSelectSql(List *tlist, List **retrieved_attrs,
- 				 deparse_expr_cxt *context);
  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);
  static void appendAggOrderBy(List *orderList, List *targetList,
--- 161,177 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
  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 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(Var *node, RelOptInfo *foreignrel,
! 					  int *tabno, int *colno);
! static bool isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno);
  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,
***************
*** 899,1000 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
   * List of columns selected is returned in retrieved_attrs.
   */
  extern void
! deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
! 						List *tlist, List *remote_conds, List *pathkeys,
  						List **retrieved_attrs, List **params_list)
  {
  	deparse_expr_cxt context;
! 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	List	   *quals;
  
  	/*
  	 * We handle relations for foreign tables, joins between those and upper
  	 * relations.
  	 */
! 	Assert(rel->reloptkind == RELOPT_JOINREL ||
! 		   rel->reloptkind == RELOPT_BASEREL ||
! 		   rel->reloptkind == RELOPT_OTHER_MEMBER_REL ||
! 		   rel->reloptkind == RELOPT_UPPER_REL);
  
  	/* Fill portions of context common to upper, join and base relation */
  	context.buf = buf;
  	context.root = root;
! 	context.foreignrel = rel;
! 	context.scanrel = (rel->reloptkind == RELOPT_UPPER_REL) ?
! 		fpinfo->outerrel : rel;
  	context.params_list = params_list;
  
- 	/* Construct SELECT clause */
- 	deparseSelectSql(tlist, retrieved_attrs, &context);
- 
- 	/*
- 	 * For upper relations, the WHERE clause is built from the remote
- 	 * conditions of the underlying scan relation; otherwise, we can use the
- 	 * supplied list of remote conditions directly.
- 	 */
- 	if (rel->reloptkind == RELOPT_UPPER_REL)
- 	{
- 		PgFdwRelationInfo *ofpinfo;
- 
- 		ofpinfo = (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
- 		quals = ofpinfo->remote_conds;
- 	}
- 	else
- 		quals = remote_conds;
- 
- 	/* Construct FROM and WHERE clauses */
- 	deparseFromExpr(quals, &context);
- 
- 	if (rel->reloptkind == RELOPT_UPPER_REL)
- 	{
- 		/* Append GROUP BY clause */
- 		appendGroupByClause(tlist, &context);
- 
- 		/* Append HAVING clause */
- 		if (remote_conds)
- 		{
- 			appendStringInfo(buf, " HAVING ");
- 			appendConditions(remote_conds, &context);
- 		}
- 	}
- 
- 	/* Add ORDER BY clause if we found any useful pathkeys */
- 	if (pathkeys)
- 		appendOrderByClause(pathkeys, &context);
- 
- 	/* Add any necessary FOR UPDATE/SHARE. */
- 	deparseLockingClause(&context);
- }
- 
- /*
-  * Construct a simple SELECT statement that retrieves desired columns
-  * of the specified foreign table, and append it to "buf".  The output
-  * contains just "SELECT ... ".
-  *
-  * We also create an integer List of the columns being retrieved, which is
-  * returned to *retrieved_attrs.
-  *
-  * tlist is the list of desired columns. Read prologue of
-  * deparseSelectStmtForRel() for details.
-  */
- static void
- deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
- {
- 	StringInfo	buf = context->buf;
- 	RelOptInfo *foreignrel = context->foreignrel;
- 	PlannerInfo *root = context->root;
- 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
- 
  	/*
  	 * Construct SELECT list
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
  	{
! 		/* For a join relation use the input tlist */
! 		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
  	{
--- 904,959 ----
   * List of columns selected is returned in retrieved_attrs.
   */
  extern void
! deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
! 						RelOptInfo *foreignrel, List *tlist,
! 						List *remote_conds, List *pathkeys,
  						List **retrieved_attrs, List **params_list)
  {
  	deparse_expr_cxt context;
! 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	List	   *quals;
+ 	RelOptInfo *scanrel = (foreignrel->reloptkind == RELOPT_UPPER_REL) ?
+ 		fpinfo->outerrel : foreignrel;
  
  	/*
  	 * We handle relations for foreign tables, joins between those and upper
  	 * relations.
  	 */
! 	Assert(foreignrel->reloptkind == RELOPT_JOINREL ||
! 		   foreignrel->reloptkind == RELOPT_BASEREL ||
! 		   foreignrel->reloptkind == RELOPT_OTHER_MEMBER_REL ||
! 		   foreignrel->reloptkind == RELOPT_UPPER_REL);
! 
! 	/* For upper relations, scanrel must be either a joinrel or a baserel */
! 	Assert(foreignrel->reloptkind != RELOPT_UPPER_REL ||
! 		   scanrel->reloptkind == RELOPT_JOINREL ||
! 		   scanrel->reloptkind == RELOPT_BASEREL);
  
  	/* Fill portions of context common to upper, join and base relation */
  	context.buf = buf;
  	context.root = root;
! 	context.foreignrel = foreignrel;
! 	context.scanrel = scanrel;
  	context.params_list = params_list;
  
  	/*
  	 * Construct SELECT list
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
+ 	/*
+ 	 * Note: non-NIL tlist might be supplied for a baserel; if the baserel is
+ 	 * full-outer-joined to any other foreign table (or join), and has any
+ 	 * remote conditions, then non-NIL tlist will be supplied for the baserel,
+ 	 * to deparse it as a subquery (see deparseRangeTblRef).  In that case,
+ 	 * use deparseExplicitTargetList.
+ 	 */
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL ||
! 		tlist != NIL)
  	{
! 		/* Use the input tlist */
! 		deparseExplicitTargetList(tlist, retrieved_attrs, &context);
  	}
  	else
  	{
***************
*** 1014,1050 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  						  fpinfo->attrs_used, false, retrieved_attrs);
  		heap_close(rel, NoLock);
  	}
- }
  
! /*
!  * Construct a FROM clause and, if needed, a WHERE clause, and append those to
!  * "buf".
!  *
!  * quals is the list of clauses to be included in the WHERE clause.
!  */
! static void
! deparseFromExpr(List *quals, deparse_expr_cxt *context)
! {
! 	StringInfo	buf = context->buf;
! 	RelOptInfo *scanrel = context->scanrel;
  
! 	/* For upper relations, scanrel must be either a joinrel or a baserel */
! 	Assert(context->foreignrel->reloptkind != RELOPT_UPPER_REL ||
! 		   scanrel->reloptkind == RELOPT_JOINREL ||
! 		   scanrel->reloptkind == RELOPT_BASEREL);
  
! 	/* Construct FROM clause */
! 	appendStringInfoString(buf, " FROM ");
! 	deparseFromExprForRel(buf, context->root, scanrel,
! 						  (bms_num_members(scanrel->relids) > 1),
! 						  context->params_list);
  
! 	/* Construct WHERE clause */
  	if (quals != NIL)
  	{
  		appendStringInfo(buf, " WHERE ");
! 		appendConditions(quals, context);
  	}
  }
  
  /*
--- 973,1030 ----
  						  fpinfo->attrs_used, false, retrieved_attrs);
  		heap_close(rel, NoLock);
  	}
  
! 	/*
! 	 * Construct FROM clause
! 	 */
! 	appendStringInfoString(buf, " FROM ");
! 	deparseFromExprForRel(buf, root, scanrel,
! 						  (scanrel->reloptkind == RELOPT_JOINREL),
! 						  params_list);
  
! 	/*
! 	 * For upper relations, the WHERE clause is built from the remote
! 	 * conditions of the underlying scan relation; otherwise, we can use the
! 	 * supplied list of remote conditions directly.
! 	 */
! 	if (foreignrel->reloptkind == RELOPT_UPPER_REL)
! 	{
! 		PgFdwRelationInfo *ofpinfo;
  
! 		ofpinfo = (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
! 		quals = ofpinfo->remote_conds;
! 	}
! 	else
! 		quals = remote_conds;
  
! 	/*
! 	 * Construct WHERE clause
! 	 */
  	if (quals != NIL)
  	{
  		appendStringInfo(buf, " WHERE ");
! 		appendConditions(quals, &context);
! 	}
! 
! 	if (foreignrel->reloptkind == RELOPT_UPPER_REL)
! 	{
! 		/* Append GROUP BY clause */
! 		appendGroupByClause(tlist, &context);
! 
! 		/* Append HAVING clause */
! 		if (remote_conds)
! 		{
! 			appendStringInfo(buf, " HAVING ");
! 			appendConditions(remote_conds, &context);
! 		}
  	}
+ 
+ 	/* Add ORDER BY clause if we found any useful pathkeys */
+ 	if (pathkeys)
+ 		appendOrderByClause(pathkeys, &context);
+ 
+ 	/* Add any necessary FOR UPDATE/SHARE. */
+ 	deparseLockingClause(&context);
  }
  
  /*
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1135,1154 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery, because in that
+ 		 * case we would have already considered locking for the relation
+ 		 * while deparsing the lower subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1336,1357 ----
  
  	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
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1407,1557 ----
  }
  
  /*
+  * Append operand relation of foreign join to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool make_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	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);
+ }
+ 
+ /*
+  * Add a subselect alias to a subquery-in-FROM.
+  *
+  * The table alias and column aliases are appended to buf.  The index for the
+  * table alias is tabno.  The number of the column aliases added is ncols and
+  * the indexes for the column aliases are indexed 1..ncols.
+  */
+ static void
+ appendSubselectAlias(StringInfo buf, int tabno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= ncols; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get info about the subselect alias to given expression.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively.
+  */
+ static void
+ getSubselectAliasInfo(Var *node, RelOptInfo *foreignrel,
+ 					  int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/* Shouldn't get here */
+ 	elog(ERROR, "unexpected expression in subquery output");
+ }
+ 
+ /*
+  * Returns true if given expression is an output column of a subquery-in-FROM.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively, in that case.
+  */
+ static bool
+ isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!fpinfo->subquery_rels)
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		/*
+ 		 * If outer relation is deparsed as a subqeury, the given expression
+ 		 * will be an output column of the subquery; get the subselect alias
+ 		 * info for the given expression.
+ 		 */
+ 		if (fpinfo->make_outerrel_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, outerrel, tabno, colno);
+ 			return true;
+ 		}
+ 		/* Otherwise, recurse into outer relation */
+ 		if (isSubqueryExpr(node, outerrel, tabno, colno))
+ 			return true;
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * Likewise for inner relation
+ 		 */
+ 		if (fpinfo->make_innerrel_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, innerrel, tabno, colno);
+ 			return true;
+ 		}
+ 		if (isSubqueryExpr(node, innerrel, tabno, colno))
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2195,2220 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
+ 		context->scanrel : context->foreignrel;
+ 	int			tabno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var is an output column of a subquery-in-FROM, deparse
+ 	 * the alias to the given Var instead.
+ 	 */
+ 	if (isSubqueryExpr(node, rel, &tabno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1281 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
      | 57
  (8 rows)
  
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
! SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                            QUERY PLAN                                                                                                                                            
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
!    Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
!          Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
! SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!  c1 | c1 | c1 
! ----+----+----
!  52 | 51 |   
!  58 | 57 |   
!     |    |  2
!     |    |  4
!     |    |  6
!     |    |  8
!     |    | 10
!     |    | 12
!     |    | 14
!     |    | 16
  (10 rows)
  
  -- full outer join three tables
--- 1241,1295 ----
      | 57
  (8 rows)
  
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
! SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                               QUERY PLAN                                                                                                                                                                               
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
!    Output: t1.*, t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
!          Output: t1.*, t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
! SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!        t1       | c1 | c1 | c1 
! ----------------+----+----+----
!  (52,53,AAA052) | 52 | 51 |   
!  (58,59,AAA058) | 58 | 57 |   
!                 |    |    |  2
!                 |    |    |  4
!                 |    |    |  6
!                 |    |    |  8
!                 |    |    | 10
!                 |    |    | 12
!                 |    |    | 14
!                 |    |    | 16
  (10 rows)
  
  -- full outer join three tables
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3076,3099 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,681 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Set the subquery information */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	fpinfo->subquery_rels = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 4147,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4155,4176 ----
  	fpinfo->jointype = jointype;
  
  	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use of deparser.
+ 	 */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+ 	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+ 									  fpinfo_o->subquery_rels);
+ 
+ 	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4181,4187 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4210,4236 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that deparser can take appropriate action.  We
! 			 * also save the relids of that relation that is covered by the
! 			 * subquery for later use of deparser.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo->make_innerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														innerrel->relids);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4311,4326 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 95,100 **** typedef struct PgFdwRelationInfo
--- 95,113 ----
  
  	/* Grouping information */
  	List	   *grouped_tlist;
+ 
+ 	/* Subquery information */
+ 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+ 										 * subquery? */
+ 	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+ 										 * subquery? */
+ 	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+ 
+ 	/*
+ 	 * Index of the relation.  It is used for creating a subselect alias when
+ 	 * deparsing the relation as a subquery.
+ 	 */
+ 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,403 **** EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
! SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
! SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  -- full outer join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
--- 391,409 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
! SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
! SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  -- full outer join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 799,810 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
#37Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#36)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/10/27 20:41, Etsuro Fujita wrote:

Here is the updated version, which includes the restructuring you
proposed. Other than the above issue and the alias issue we discussed,
I addressed all your comments except one on testing; I tried to add test
cases where the remote query is deparsed as nested subqueries, but I
couldn't because IIUC, reduce_outer_joins reduced full joins to inner
joins or left joins. So, I added two test cases: (1) the joining
relations are both base relations (actually, we already have that) and
(2) one of the joining relations is a base relation and the other is a
join relation. I rebased the patch to HEAD, so I added a test case with
aggregate pushdown also.

I've updated another patch for evaluating PHVs remotely. Attached is an
updated version of the patch, which is created on top of the patch for
supporting deparsing subqueries. You can find test cases where a remote
join query is deparsed as nested subqueries.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-phv-pushdown-v3.patchapplication/x-patch; name=postgres-fdw-phv-pushdown-v3.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 49,54 ****
--- 49,55 ----
  #include "nodes/nodeFuncs.h"
  #include "nodes/plannodes.h"
  #include "optimizer/clauses.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/prep.h"
  #include "optimizer/tlist.h"
  #include "optimizer/var.h"
***************
*** 157,162 **** static void deparseRelabelType(RelabelType *node, deparse_expr_cxt *context);
--- 158,164 ----
  static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context);
  static void deparseNullTest(NullTest *node, deparse_expr_cxt *context);
  static void deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context);
+ static void deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context);
  static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
***************
*** 169,177 **** static void deparseFromExprForRel(StringInfo buf, PlannerInfo *root,
  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(Var *node, RelOptInfo *foreignrel,
  					  int *tabno, int *colno);
! static bool isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno);
  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,
--- 171,180 ----
  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);
! static bool isSubqueryExpr(Expr *node, PlannerInfo *root, RelOptInfo *foreignrel,
! 			   int *tabno, int *colno);
  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,
***************
*** 762,767 **** foreign_expr_walker(Node *node,
--- 765,789 ----
  					state = FDW_COLLATE_UNSAFE;
  			}
  			break;
+ 		case T_PlaceHolderVar:
+ 			{
+ 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 				PlaceHolderInfo *phinfo = find_placeholder_info(glob_cxt->root,
+ 																phv, false);
+ 
+ 				/*
+ 				 * If the PHV's contained expression is computable on the
+ 				 * remote server, we consider the PHV safe to send.
+ 				 */
+ 				if (phv->phlevelsup == 0 &&
+ 					bms_is_subset(phinfo->ph_eval_at,
+ 								  glob_cxt->foreignrel->relids))
+ 					return foreign_expr_walker((Node *) phv->phexpr,
+ 											   glob_cxt, outer_cxt);
+ 				else
+ 					return false;
+ 			}
+ 			break;
  		default:
  
  			/*
***************
*** 856,862 **** deparse_type_name(Oid type_oid, int32 typemod)
   * foreign server.
   */
  List *
! build_tlist_to_deparse(RelOptInfo *foreignrel)
  {
  	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
--- 878,884 ----
   * foreign server.
   */
  List *
! build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel)
  {
  	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
***************
*** 869,883 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
  		return fpinfo->grouped_tlist;
  
  	/*
! 	 * We require columns specified in foreignrel->reltarget->exprs and those
! 	 * required for evaluating the local conditions.
  	 */
! 	tlist = add_to_flat_tlist(tlist,
! 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
! 									   PVC_RECURSE_PLACEHOLDERS));
! 	tlist = add_to_flat_tlist(tlist,
! 							  pull_var_clause((Node *) fpinfo->local_conds,
! 											  PVC_RECURSE_PLACEHOLDERS));
  
  	return tlist;
  }
--- 891,963 ----
  		return fpinfo->grouped_tlist;
  
  	/*
! 	 * Fetch all expressions in foreignrel's reltarget if the
! 	 * reltarget_is_shippable flag is set TRUE.  Otherwise, fetch shipplable
! 	 * expressions in the reltarget plus expressions required for evaluating
! 	 * non-shippable expressions in the reltarget.
! 	 */
! 	if (fpinfo->reltarget_is_shippable)
! 		tlist = make_tlist_from_pathtarget(foreignrel->reltarget);
! 	else
! 	{
! 		List	   *exprs = NIL;
! 		ListCell   *lc;
! 
! 		/* Note: we have at least one non-shippable PHV in the reltarget. */
! 		foreach(lc, foreignrel->reltarget->exprs)
! 		{
! 			Node	   *node = (Node *) lfirst(lc);
! 
! 			if (IsA(node, Var))
! 				exprs = lappend(exprs, node);
! 			else if (IsA(node, PlaceHolderVar))
! 			{
! 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 				PlaceHolderInfo *phinfo = find_placeholder_info(root, phv,
! 																false);
! 
! 				if (bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->outerrel->relids) ||
! 					bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->innerrel->relids))
! 				{
! 					/*
! 					 * The PHV coming from an either input should be shippable.
! 					 */
! 					exprs = lappend(exprs, node);
! 				}
! 				else
! 				{
! 					/*
! 					 * The PHV might be shippable, but in any case just fetch
! 					 * expressions required for evaluating the PHV.
! 					 */
! 					List	   *exprs2 = pull_var_clause(node,
! 													PVC_INCLUDE_PLACEHOLDERS);
! 					ListCell   *lc2;
! 
! 					foreach(lc2, exprs2)
! 					{
! 						Node	   *node2 = (Node *) lfirst(lc2);
! 
! 						exprs = lappend(exprs, node2);
! 					}
! 				}
! 			}
! 			else
! 				elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
! 		}
! 
! 		tlist = add_to_flat_tlist(tlist, exprs);
! 	}
! 
! 	/*
! 	 * Fetch expressions required for evaluating local conditions, if any.
  	 */
! 	if (fpinfo->local_conds)
! 		tlist = add_to_flat_tlist(tlist,
! 								pull_var_clause((Node *) fpinfo->local_conds,
! 												PVC_INCLUDE_PLACEHOLDERS));
  
  	return tlist;
  }
***************
*** 1417,1423 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
  		   foreignrel->reloptkind == RELOPT_JOINREL);
! 	Assert(fpinfo->local_conds == NIL);
  
  	if (make_subquery)
  	{
--- 1497,1503 ----
  
  	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
  		   foreignrel->reloptkind == RELOPT_JOINREL);
! 	Assert(fpinfo->reltarget_is_shippable && fpinfo->local_conds == NIL);
  
  	if (make_subquery)
  	{
***************
*** 1472,1478 **** appendSubselectAlias(StringInfo buf, int tabno, int ncols)
   * respectively.
   */
  static void
! getSubselectAliasInfo(Var *node, RelOptInfo *foreignrel,
  					  int *tabno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
--- 1552,1558 ----
   * respectively.
   */
  static void
! getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
  					  int *tabno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
***************
*** 1505,1515 **** getSubselectAliasInfo(Var *node, RelOptInfo *foreignrel,
   * respectively, in that case.
   */
  static bool
! isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
  	RelOptInfo *innerrel = fpinfo->innerrel;
  
  	if (foreignrel->reloptkind != RELOPT_JOINREL)
  		return false;
--- 1585,1597 ----
   * respectively, in that case.
   */
  static bool
! isSubqueryExpr(Expr *node, PlannerInfo *root, RelOptInfo *foreignrel,
! 			   int *tabno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
  	RelOptInfo *innerrel = fpinfo->innerrel;
+ 	bool		is_outer_var;
  
  	if (foreignrel->reloptkind != RELOPT_JOINREL)
  		return false;
***************
*** 1517,1523 **** isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  	if (!fpinfo->subquery_rels)
  		return false;
  
! 	if (bms_is_member(node->varno, outerrel->relids))
  	{
  		/*
  		 * If outer relation is deparsed as a subqeury, the given expression
--- 1599,1630 ----
  	if (!fpinfo->subquery_rels)
  		return false;
  
! 	if (IsA(node, Var))
! 	{
! 		Var		   *var = (Var *) node;
! 
! 		is_outer_var = bms_is_member(var->varno, outerrel->relids);
! 	}
! 	else if (IsA(node, PlaceHolderVar))
! 	{
! 		PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 		PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
! 		bool		is_inner_var;
! 
! 		is_outer_var = bms_is_subset(phinfo->ph_eval_at, outerrel->relids);
! 		is_inner_var = bms_is_subset(phinfo->ph_eval_at, innerrel->relids);
! 
! 		/*
! 		 * If the PHV can't be computed in either input, it will be computed
! 		 * in the current join level; it's not a subquery output column.
! 		 */
! 		if (!is_outer_var && !is_inner_var)
! 			return false;
! 	}
! 	else
! 		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
! 
! 	if (is_outer_var)
  	{
  		/*
  		 * If outer relation is deparsed as a subqeury, the given expression
***************
*** 1530,1542 **** isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  			return true;
  		}
  		/* Otherwise, recurse into outer relation */
! 		if (isSubqueryExpr(node, outerrel, tabno, colno))
  			return true;
  	}
  	else
  	{
- 		Assert(bms_is_member(node->varno, innerrel->relids));
- 
  		/*
  		 * Likewise for inner relation
  		 */
--- 1637,1647 ----
  			return true;
  		}
  		/* Otherwise, recurse into outer relation */
! 		if (isSubqueryExpr(node, root, outerrel, tabno, colno))
  			return true;
  	}
  	else
  	{
  		/*
  		 * Likewise for inner relation
  		 */
***************
*** 1545,1551 **** isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  			getSubselectAliasInfo(node, innerrel, tabno, colno);
  			return true;
  		}
! 		if (isSubqueryExpr(node, innerrel, tabno, colno))
  			return true;
  	}
  	return false;
--- 1650,1656 ----
  			getSubselectAliasInfo(node, innerrel, tabno, colno);
  			return true;
  		}
! 		if (isSubqueryExpr(node, root, innerrel, tabno, colno))
  			return true;
  	}
  	return false;
***************
*** 1932,1940 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.  However, we must be
! 		 * careful; the table could be beneath an outer join, in which case it
! 		 * must go to NULL whenever the rest of the row does.
  		 */
  		Oid			fetchval = 0;
  
--- 2037,2043 ----
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.
  		 */
  		Oid			fetchval = 0;
  
***************
*** 1944,1957 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  			fetchval = rte->relid;
  		}
  
! 		if (qualify_col)
! 		{
! 			appendStringInfoString(buf, "CASE WHEN (");
! 			ADD_REL_QUALIFIER(buf, varno);
! 			appendStringInfo(buf, "*)::text IS NOT NULL THEN %u END", fetchval);
! 		}
! 		else
! 			appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
--- 2047,2053 ----
  			fetchval = rte->relid;
  		}
  
! 		appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
***************
*** 1982,2009 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  		attrs_used = bms_add_member(NULL,
  									0 - FirstLowInvalidHeapAttributeNumber);
  
- 		/*
- 		 * In case the whole-row reference is under an outer join then it has
- 		 * to go NULL whenever the rest of the row goes NULL. Deparsing a join
- 		 * query would always involve multiple relations, thus qualify_col
- 		 * would be true.
- 		 */
- 		if (qualify_col)
- 		{
- 			appendStringInfoString(buf, "CASE WHEN (");
- 			ADD_REL_QUALIFIER(buf, varno);
- 			appendStringInfo(buf, "*)::text IS NOT NULL THEN ");
- 		}
- 
  		appendStringInfoString(buf, "ROW(");
  		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
  						  &retrieved_attrs);
  		appendStringInfoString(buf, ")");
  
- 		/* Complete the CASE WHEN statement started above. */
- 		if (qualify_col)
- 			appendStringInfo(buf, " END");
- 
  		heap_close(rel, NoLock);
  		bms_free(attrs_used);
  	}
--- 2078,2088 ----
***************
*** 2176,2181 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 2255,2263 ----
  		case T_Aggref:
  			deparseAggref((Aggref *) node, context);
  			break;
+ 		case T_PlaceHolderVar:
+ 			deparseExprInPlaceHolderVar((PlaceHolderVar *) node, context);
+ 			break;
  		default:
  			elog(ERROR, "unsupported expression type for deparse: %d",
  				 (int) nodeTag(node));
***************
*** 2195,2200 **** static void
--- 2277,2283 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	PlannerInfo *root = context->root;
  	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
  		context->scanrel : context->foreignrel;
  	int			tabno;
***************
*** 2207,2213 **** deparseVar(Var *node, deparse_expr_cxt *context)
  	 * If the given Var is an output column of a subquery-in-FROM, deparse
  	 * the alias to the given Var instead.
  	 */
! 	if (isSubqueryExpr(node, rel, &tabno, &colno))
  	{
  		appendStringInfo(context->buf, "%s%d.%s%d",
  						 SS_TAB_ALIAS_PREFIX, tabno,
--- 2290,2296 ----
  	 * If the given Var is an output column of a subquery-in-FROM, deparse
  	 * the alias to the given Var instead.
  	 */
! 	if (isSubqueryExpr((Expr *) node, root, rel, &tabno, &colno))
  	{
  		appendStringInfo(context->buf, "%s%d.%s%d",
  						 SS_TAB_ALIAS_PREFIX, tabno,
***************
*** 2901,2906 **** appendAggOrderBy(List *orderList, List *targetList, deparse_expr_cxt *context)
--- 2984,3016 ----
  }
  
  /*
+  * Deparse a PlaceHolderVar's contained expression.
+  */
+ static void
+ deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context)
+ {
+ 	PlannerInfo *root = context->root;
+ 	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
+ 		context->scanrel : context->foreignrel;
+ 	int			tabno;
+ 	int			colno;
+ 
+ 	/*
+ 	 * If the given PHV is an output column of a subquery-in-FROM, deparse
+ 	 * the alias to the given PHV instead.
+ 	 */
+ 	if (isSubqueryExpr((Expr *) node, root, rel, &tabno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
+ 	deparseExpr(node->phexpr, context);
+ }
+ 
+ /*
   * Print the representation of a parameter to be sent to the remote side.
   *
   * Note: we always label the Param's type explicitly rather than relying on
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1267,1280 **** SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                               QUERY PLAN                                                                                                                                                                               
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.*, t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.*, t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
--- 1267,1280 ----
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                   QUERY PLAN                                                                                                                                                                  
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.*, t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
           Output: t1.*, t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT s1.c1, s1.c2, r2.c1, r4.c1 FROM (((SELECT ROW(c1, c2, c3), c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s1(c1, c2) INNER JOIN "S 1"."T 4" r2 ON (((s1.c2 = (r2.c1 + 1))))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY s1.c2 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
  SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 1523,1530 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1523,1530 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                         QUERY PLAN                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1532,1538 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1532,1538 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1567,1574 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1567,1574 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                              QUERY PLAN                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1576,1582 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1576,1582 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1612,1619 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                               
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1612,1619 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                        QUERY PLAN                                                                                                                                                                        
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1621,1627 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1621,1627 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1656,1663 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1656,1663 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                             QUERY PLAN                                                                                                                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1665,1671 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1665,1671 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1735,1748 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1735,1748 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                     QUERY PLAN                                                                                                                                                                     
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s1.c4, s2.c1 FROM ((SELECT ctid, ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1", c3 FROM "S 1"."T 1") s1(c1, c2, c3, c4) INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c3 = s2.c2)))) ORDER BY s1.c4 ASC NULLS LAST, s1.c3 ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 2071,2095 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
     1
  (10 rows)
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                         QUERY PLAN                                                         
! ---------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: (13), ft2.c1
!    Join Filter: (13 = ft2.c1)
!    ->  Foreign Scan on public.ft2
!          Output: ft2.c1
!          Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (("C 1" >= 10)) AND (("C 1" <= 15)) ORDER BY "C 1" ASC NULLS LAST
!    ->  Materialize
!          Output: (13)
!          ->  Foreign Scan on public.ft1
!                Output: 13
!                Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 13))
! (11 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
--- 2071,2086 ----
     1
  (10 rows)
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                                                               QUERY PLAN                                                                                              
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: (13), ft2.c1
!    Relations: (public.ft2) LEFT JOIN (public.ft1)
!    Remote SQL: SELECT r2."C 1", s4.c1 FROM ("S 1"."T 1" r2 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s4(c1) ON (((13 = r2."C 1")))) WHERE ((r2."C 1" >= 10)) AND ((r2."C 1" <= 15))
! (4 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
***************
*** 2102,2125 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
      | 15
  (6 rows)
  
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                     QUERY PLAN                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Join Filter: (ft4.c1 = ft1.c1)
!    ->  Foreign Scan on public.ft4
!          Output: ft4.c1, ft4.c2, ft4.c3
!          Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 10)) AND ((c1 <= 15))
!    ->  Materialize
!          Output: ft1.c1, ft2.c1, (13)
!          ->  Foreign Scan
!                Output: ft1.c1, ft2.c1, 13
!                Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
! (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
--- 2093,2107 ----
      | 15
  (6 rows)
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                                                                           QUERY PLAN                                                                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Relations: (public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2))
!    Remote SQL: SELECT r1.c1, s7.c1, s7.c2, s7.c3 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4."C 1", r5."C 1", 13 FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12))))) s7(c1, c2, c3) ON (((r1.c1 = s7.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
! (4 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
***************
*** 2129,2144 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
   14 |    |    |   
  (3 rows)
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                 QUERY PLAN                                                                                                                                 
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2111,2170 ----
   14 |    |    |   
  (3 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                               QUERY PLAN                                                                                                                                                                               
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft2.c1, (13), (13), ft2_1.c1
+    Relations: (public.ft2) LEFT JOIN ((public.ft2) LEFT JOIN (public.ft1))
+    Remote SQL: SELECT r1."C 1", s8.c1, s8.c2, s8.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r5."C 1", 13, s7.c1 FROM ("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s7(c1) ON (((13 = r5."C 1")))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))) s8(c1, c2, c3) ON (((r1."C 1" = 13)))) WHERE ((r1."C 1" >= 10)) AND ((r1."C 1" <= 15))
+ (4 rows)
+ 
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 
+ ----+----+----+----
+  10 |    |    |   
+  11 |    |    |   
+  12 |    |    |   
+  13 | 13 |    | 10
+  13 | 13 |    | 11
+  13 | 13 |    | 12
+  13 | 13 | 13 | 13
+  13 | 13 |    | 14
+  13 | 13 |    | 15
+  14 |    |    |   
+  15 |    |    |   
+ (11 rows)
+ 
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                                                                                                 QUERY PLAN                                                                                                                                                                                                                                                
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ((13 IS NOT NULL)), ft4_1.c1, (13), ft1.c1, ft2.c1
+    Relations: (public.ft4) LEFT JOIN ((public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2)))
+    Remote SQL: SELECT r1.c1, s11.c1, s11.c2, s11.c3, s11.c4, s11.c5 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4.c1, s10.c1, s10.c2, (s10.c3 IS NOT NULL), s10.c3 FROM ("S 1"."T 3" r4 LEFT JOIN (SELECT r7."C 1", r8."C 1", 13 FROM ("S 1"."T 1" r7 INNER JOIN "S 1"."T 1" r8 ON (((r8."C 1" = 12)) AND ((r7."C 1" = 12))))) s10(c1, c2, c3) ON (((r4.c1 = s10.c1)))) WHERE ((r4.c1 >= 10)) AND ((r4.c1 <= 15))) s11(c1, c2, c3, c4, c5) ON (((r1.c1 = s11.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
+ (4 rows)
+ 
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 | d2 | e2 
+ ----+----+----+----+----+----
+  10 | f  | 10 |    |    |   
+  12 | t  | 12 | 13 | 12 | 12
+  14 | f  | 14 |    |    |   
+ (3 rows)
+ 
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                               QUERY PLAN                                                                                                                              
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s1.c4, r2.c1, r2.c2 FROM ((SELECT ROW(c1, c2, c3), c1, c2, c3 FROM "S 1"."T 4") s1(c1, c2, c3, c4) INNER JOIN "S 1"."T 3" r2 ON (((s1.c2 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY s1.c2 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 4040,4053 **** 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)
--- 4066,4079 ----
  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)
***************
*** 4183,4196 **** 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
!                                                                                                                               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)
--- 4209,4222 ----
  
  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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 28,33 ****
--- 28,34 ----
  #include "optimizer/clauses.h"
  #include "optimizer/pathnode.h"
  #include "optimizer/paths.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/planmain.h"
  #include "optimizer/restrictinfo.h"
  #include "optimizer/var.h"
***************
*** 582,587 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 583,630 ----
  	}
  
  	/*
+ 	 * Decide whether expressions in the reltarget are shippable and whether
+ 	 * there are any non-Vars in those expressions.
+ 	 *
+ 	 * Note: if the relation is an appendrel child, it isn't in the main join
+ 	 * tree, so the info is not used.
+ 	 */
+ 	if (baserel->reloptkind == RELOPT_BASEREL)
+ 	{
+ 		bool		reltarget_is_shippable = true;
+ 		bool		reltarget_has_non_vars = false;
+ 
+ 		foreach(lc, baserel->reltarget->exprs)
+ 		{
+ 			Node	   *node = (Node *) lfirst(lc);
+ 
+ 			if (IsA(node, Var))
+ 			{
+ 				Var		   *var = (Var *) node;
+ 
+ 				if (var->varattno <= 0 &&
+ 					var->varattno != SelfItemPointerAttributeNumber &&
+ 					var->varattno != ObjectIdAttributeNumber)
+ 					reltarget_has_non_vars = true;
+ 			}
+ 			else if (IsA(node, PlaceHolderVar))
+ 			{
+ 				reltarget_has_non_vars = true;
+ 
+ 				if (!is_foreign_expr(root, baserel, (Expr *) node))
+ 				{
+ 					reltarget_is_shippable = false;
+ 					break;
+ 				}
+ 			}
+ 			else
+ 				elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
+ 		}
+ 		fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 		fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 	}
+ 
+ 	/*
  	 * Compute the selectivity and cost of the local_conds, so we don't have
  	 * to do it over again for each path.  The best we can do for these
  	 * conditions is to estimate selectivity on the basis of local statistics.
***************
*** 1199,1205 **** postgresGetForeignPlan(PlannerInfo *root,
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
--- 1242,1248 ----
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
***************
*** 2539,2545 **** estimate_path_cost_size(PlannerInfo *root,
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL ||
  			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 			fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
--- 2582,2588 ----
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL ||
  			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 			fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
***************
*** 4055,4060 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4098,4105 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	bool		reltarget_is_shippable = true;
+ 	bool		reltarget_has_non_vars = false;
  
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
***************
*** 4084,4089 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4129,4142 ----
  	if (fpinfo_o->local_conds || fpinfo_i->local_conds)
  		return false;
  
+ 	/*
+ 	 * If the reltargets of joining relations are not shippable, those are
+ 	 * required to be evaluated locally, so the join can not be pushed down.
+ 	 */
+ 	if (!fpinfo_o->reltarget_is_shippable ||
+ 		!fpinfo_i->reltarget_is_shippable)
+ 		return false;
+ 
  	/* Separate restrict list into join quals and quals on join relation */
  	if (IS_OUTER_JOIN(jointype))
  		extract_actual_join_clauses(extra->restrictlist, &joinclauses, &otherclauses);
***************
*** 4109,4134 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			return false;
  	}
  
- 	/*
- 	 * deparseExplicitTargetList() isn't smart enough to handle anything other
- 	 * than a Var.  In particular, if there's some PlaceHolderVar that would
- 	 * need to be evaluated within this join tree (because there's an upper
- 	 * reference to a quantity that may go to NULL as a result of an outer
- 	 * join), then we can't try to push the join down because we'll fail when
- 	 * we get to deparseExplicitTargetList().  However, a PlaceHolderVar that
- 	 * needs to be evaluated *at the top* of this join tree is OK, because we
- 	 * can do that locally after fetching the results from the remote side.
- 	 */
- 	foreach(lc, root->placeholder_list)
- 	{
- 		PlaceHolderInfo *phinfo = lfirst(lc);
- 		Relids		relids = joinrel->relids;
- 
- 		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
- 			bms_nonempty_difference(relids, phinfo->ph_eval_at))
- 			return false;
- 	}
- 
  	/* Save the join clauses, for later use. */
  	fpinfo->joinclauses = joinclauses;
  
--- 4162,4167 ----
***************
*** 4185,4211 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
  	 */
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
--- 4218,4267 ----
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
+ 	 *
+ 	 * If the joining relation has reltarget_has_non_vars=TRUE, it's deparsed
+ 	 * as a subquery in which the remote_conds of the relation is transformed
+ 	 * into the subquery's WHERE clause; so in that case, don't pull up the
+ 	 * remote_conds into the joinclauses or remote_conds of this relation.
  	 */
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->make_innerrel_subquery = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->make_outerrel_subquery = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->make_innerrel_subquery = true;
! 			else
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->make_outerrel_subquery = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->make_outerrel_subquery = true;
! 			else
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->make_innerrel_subquery = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
***************
*** 4215,4236 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			 * In this case, if any of the joining relations has conditions,
  			 * we need to deparse that relation as a subquery so that
  			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that deparser can take appropriate action.  We
! 			 * also save the relids of that relation that is covered by the
! 			 * subquery for later use of deparser.
  			 */
! 			if (fpinfo_o->remote_conds)
! 			{
  				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
  				fpinfo->make_innerrel_subquery = true;
- 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
- 														innerrel->relids);
- 			}
  			break;
  
  		default:
--- 4271,4282 ----
  			 * In this case, if any of the joining relations has conditions,
  			 * we need to deparse that relation as a subquery so that
  			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that deparser can take appropriate action.
  			 */
! 			if (fpinfo_o->reltarget_has_non_vars || fpinfo_o->remote_conds)
  				fpinfo->make_outerrel_subquery = true;
! 			if (fpinfo_i->reltarget_has_non_vars || fpinfo_i->remote_conds)
  				fpinfo->make_innerrel_subquery = true;
  			break;
  
  		default:
***************
*** 4239,4244 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4285,4301 ----
  	}
  
  	/*
+ 	 * If the joining relation is deparsed as a subquery, save the relids for
+ 	 * later use of deparser.
+ 	 */
+ 	if (fpinfo->make_outerrel_subquery)
+ 		fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
+ 												outerrel->relids);
+ 	if (fpinfo->make_innerrel_subquery)
+ 		fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
+ 												innerrel->relids);
+ 
+ 	/*
  	 * For an inner join, all restrictions can be treated alike. Treating the
  	 * pushed down conditions as join conditions allows a top level full outer
  	 * join to be deparsed without requiring subqueries.
***************
*** 4254,4259 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4311,4354 ----
  	fpinfo->pushdown_safe = true;
  
  	/*
+ 	 * Decide whether expressions in the reltarget are shippable and whether
+ 	 * there are any non-Vars in those expressions.
+ 	 *
+ 	 * Note: we don't need to be careful about whole-row references or system
+ 	 * columns other than ctid and oid here; those would be handled as output
+ 	 * columns of lower subqueries, if any.
+ 	 */
+ 	foreach(lc, joinrel->reltarget->exprs)
+ 	{
+ 		Node	   *node = (Node *) lfirst(lc);
+ 
+ 		if (IsA(node, Var))
+ 			continue;
+ 		if (IsA(node, PlaceHolderVar))
+ 		{
+ 			PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 			PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
+ 
+ 			/* Ignore the PHV if it has bubbled up from an either input. */
+ 			if (bms_is_subset(phinfo->ph_eval_at, outerrel->relids) ||
+ 				bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
+ 				continue;
+ 
+ 			reltarget_has_non_vars = true;
+ 
+ 			if (!is_foreign_expr(root, joinrel, (Expr *) node))
+ 			{
+ 				reltarget_is_shippable = false;
+ 				break;
+ 			}
+ 		}
+ 		else
+ 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
+ 	}
+ 	fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 	fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 
+ 	/*
  	 * If user is willing to estimate cost for a scan of either of the joining
  	 * relations using EXPLAIN, he intends to estimate scans on that relation
  	 * more accurately. Then, it makes sense to estimate the cost the join
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 33,38 **** typedef struct PgFdwRelationInfo
--- 33,61 ----
  	bool		pushdown_safe;
  
  	/*
+ 	 * Flags on the reltarget of the relation.
+ 	 *
+ 	 * If each expression in the relation's reltarget was shippable (i.e.,
+ 	 * computable on the remote side), we set reltarget_is_shippalbe to TRUE.
+ 	 * The flag is used to decide whether the relation can be joined with any
+ 	 * other foreign table or join (see foreign_join_ok).  Note that Vars in
+ 	 * the reltarget are shippable, so if PHVs in the reltarget (if any) were
+ 	 * shippable, the flag is set TRUE.
+ 	 *
+ 	 * If the reltarget contained any PHVs, we need to deparse the relation as
+ 	 * a subquery when creating a remote query; so in that case, we set
+ 	 * reltarget_has_non_vars to TRUE, to let deparse.c know about that.  If
+ 	 * the reltarget of a base relation contained a whole-row reference and/or
+ 	 * system columns other than citd and oid, the flag is also set TRUE; in
+ 	 * that case, the subquery will emit a ROW() expression for a whole-row
+ 	 * reference and 0 for system columns other than ctid and oid, except for
+ 	 * tableoid, in which case, a valid value for the local table OID will be
+ 	 * emitted by the subquery.
+ 	 */
+ 	bool		reltarget_is_shippable; /* are reltarget exprs shippable? */
+ 	bool		reltarget_has_non_vars;	/* are there any non-Var exprs? */
+ 
+ 	/*
  	 * Restriction clauses, divided into safe and unsafe to pushdown subsets.
  	 *
  	 * For a base foreign relation this is a list of clauses along-with
***************
*** 171,177 **** extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
--- 194,200 ----
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 499,515 **** EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
- 
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
--- 499,517 ----
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
#38Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#36)
Re: Push down more full joins in postgres_fdw

Here is the updated version, which includes the restructuring you proposed.
Other than the above issue and the alias issue we discussed, I addressed all
your comments except one on testing; I tried to add test cases where the
remote query is deparsed as nested subqueries, but I couldn't because IIUC,
reduce_outer_joins reduced full joins to inner joins or left joins.

No always. It will do so in some cases as explained in the prologue of
reduce_outer_joins()

* More generally, an outer join can be reduced in strength if there is a
* strict qual above it in the qual tree that constrains a Var from the
* nullable side of the join to be non-null. (For FULL joins this applies
* to each side separately.)

So, I
added two test cases: (1) the joining relations are both base relations
(actually, we already have that) and (2) one of the joining relations is a
base relation and the other is a join relation. I rebased the patch to
HEAD, so I added a test case with aggregate pushdown also.

This patch again has a lot of unrelated changes, esp. in
deparseSelectStmtForRel(). What was earlier part of deparseSelectSql()
and deparseFromExpr() is now flattened in deparseSelectStmtForRel(),
which seems unnecessary. Or there are changes like

-deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
-                       List *tlist, List *remote_conds, List *pathkeys,
+deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
+                       RelOptInfo *foreignrel, List *tlist,
+                       List *remote_conds, List *pathkeys,

which arise because rel has been renamed as foreignrel. The patch will
work even without such renaming.

I think such refactoring, should be done in a separate patch.

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

#39Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#38)
Re: Push down more full joins in postgres_fdw

On 2016/11/03 18:52, Ashutosh Bapat wrote:

Here is the updated version, which includes the restructuring you proposed.
Other than the above issue and the alias issue we discussed, I addressed all
your comments except one on testing; I tried to add test cases where the
remote query is deparsed as nested subqueries, but I couldn't because IIUC,
reduce_outer_joins reduced full joins to inner joins or left joins.

No always. It will do so in some cases as explained in the prologue of
reduce_outer_joins()

* More generally, an outer join can be reduced in strength if there is a
* strict qual above it in the qual tree that constrains a Var from the
* nullable side of the join to be non-null. (For FULL joins this applies
* to each side separately.)

Hmm. Would it be necessary for this patch to include test cases for
nested subqueries? As mentioned in a previous email, those test cases
can be added by the split patch that allow PHVs to be evaluated on the
remote side.

This patch again has a lot of unrelated changes, esp. in
deparseSelectStmtForRel(). What was earlier part of deparseSelectSql()
and deparseFromExpr() is now flattened in deparseSelectStmtForRel(),
which seems unnecessary.

IIUC, I think this change was done to address your comment (see the
comment #2 in [1]/messages/by-id/CAFjFpRfwoSsJr9418b2jA7g0nwagjZSWhPQPUmK9M6z5XSOAqQ@mail.gmail.com). Am I missing something?

Or there are changes like

-deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
-                       List *tlist, List *remote_conds, List *pathkeys,
+deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
+                       RelOptInfo *foreignrel, List *tlist,
+                       List *remote_conds, List *pathkeys,

which arise because rel has been renamed as foreignrel. The patch will
work even without such renaming.

I did that because we have a Relation named rel (the same name!) within
that function, to execute deparseTargetList in a base-relation case.
Another reason for that is because (1) that would match with other
function definitions in deparse.c and (2) that would be consistent with
the existing function definition for that function in postgres_fdw.h.

Best regards,
Etsuro Fujita

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

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

#40Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#39)
Re: Push down more full joins in postgres_fdw

On Fri, Nov 4, 2016 at 9:19 AM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/11/03 18:52, Ashutosh Bapat wrote:

Here is the updated version, which includes the restructuring you
proposed.
Other than the above issue and the alias issue we discussed, I addressed
all
your comments except one on testing; I tried to add test cases where the
remote query is deparsed as nested subqueries, but I couldn't because
IIUC,
reduce_outer_joins reduced full joins to inner joins or left joins.

No always. It will do so in some cases as explained in the prologue of
reduce_outer_joins()

* More generally, an outer join can be reduced in strength if there is a
* strict qual above it in the qual tree that constrains a Var from the
* nullable side of the join to be non-null. (For FULL joins this applies
* to each side separately.)

Hmm. Would it be necessary for this patch to include test cases for nested
subqueries? As mentioned in a previous email, those test cases can be added
by the split patch that allow PHVs to be evaluated on the remote side.

A patch should have testcases testing the functionality added. This
patch adds functionality to deparse nested subqueries, so there should
be testcase to test it. If we can not come up with a testcase, then
it's very much possible that the code related to that functionality is
not needed. PHV is a separate patch and we do not know whether it will
be committed or when it will be committed after this patch is
committed. It's better to have self-sufficient patch.

This patch again has a lot of unrelated changes, esp. in
deparseSelectStmtForRel(). What was earlier part of deparseSelectSql()
and deparseFromExpr() is now flattened in deparseSelectStmtForRel(),
which seems unnecessary.

IIUC, I think this change was done to address your comment (see the comment
#2 in [1]). Am I missing something?

There is some misunderstanding here. That comment basically says,
deparseRangeTblRef() duplicates code in deparseSelectStmtForRel(), so
we should either remove deparseRangeTblRef() altogether or should keep
it to minimal avoiding duplication of code. What might have confused
you is the last sentence in that comment "This will also make the
current changes to deparseSelectSql unnecessary." By current changes I
meant changes to deparseSelectSql() in your patch, not the one that's
in the repository. I don't think we should flatten
deparseSelectStmtForRel() in this patch.

Or there are changes like

-deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo
*rel,
-                       List *tlist, List *remote_conds, List *pathkeys,
+deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
+                       RelOptInfo *foreignrel, List *tlist,
+                       List *remote_conds, List *pathkeys,

which arise because rel has been renamed as foreignrel. The patch will
work even without such renaming.

I did that because we have a Relation named rel (the same name!) within that
function, to execute deparseTargetList in a base-relation case.

That's because you have flattened deparseSelectStmtForRel(). If we
don't flatten it out, the variable name conflict doesn't arise.

Another
reason for that is because (1) that would match with other function
definitions in deparse.c and (2) that would be consistent with the existing
function definition for that function in postgres_fdw.h.

That would be a separate patch, I guess.

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

#41Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#40)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/11/04 13:08, Ashutosh Bapat wrote:

On Fri, Nov 4, 2016 at 9:19 AM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/11/03 18:52, Ashutosh Bapat wrote:

I wrote:

Here is the updated version,

Other than the above issue and the alias issue we discussed, I addressed
all
your comments except one on testing; I tried to add test cases where the
remote query is deparsed as nested subqueries, but I couldn't because
IIUC,
reduce_outer_joins reduced full joins to inner joins or left joins.

No always. It will do so in some cases as explained in the prologue of
reduce_outer_joins()

* More generally, an outer join can be reduced in strength if there is a
* strict qual above it in the qual tree that constrains a Var from the
* nullable side of the join to be non-null. (For FULL joins this applies
* to each side separately.)

Right.

Would it be necessary for this patch to include test cases for nested
subqueries?

A patch should have testcases testing the functionality added. This
patch adds functionality to deparse nested subqueries, so there should
be testcase to test it.

OK, I added such a test case.

This patch again has a lot of unrelated changes, esp. in
deparseSelectStmtForRel(). What was earlier part of deparseSelectSql()
and deparseFromExpr() is now flattened in deparseSelectStmtForRel(),
which seems unnecessary.

IIUC, I think this change was done to address your comment (see the comment
#2 in [1]). Am I missing something?

There is some misunderstanding here. That comment basically says,
deparseRangeTblRef() duplicates code in deparseSelectStmtForRel(), so
we should either remove deparseRangeTblRef() altogether or should keep
it to minimal avoiding duplication of code. What might have confused
you is the last sentence in that comment "This will also make the
current changes to deparseSelectSql unnecessary." By current changes I
meant changes to deparseSelectSql() in your patch, not the one that's
in the repository. I don't think we should flatten
deparseSelectStmtForRel() in this patch.

I noticed that I misunderstood your words. Sorry about that. I agree
with you, so I removed that change from the patch.

Attached is an updated version of the patch.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v2.patchapplication/x-patch; name=postgres-fdw-subquery-support-v2.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,180 ----
  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(Var *node, RelOptInfo *foreignrel,
+ 					  int *tabno, int *colno);
+ static bool isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno);
  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,
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1163,1182 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery, because in that
+ 		 * case we would have already considered locking for the relation
+ 		 * while deparsing the lower subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1364,1385 ----
  
  	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
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1435,1585 ----
  }
  
  /*
+  * Append operand relation of foreign join to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool make_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	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);
+ }
+ 
+ /*
+  * Add a subselect alias to a subquery-in-FROM.
+  *
+  * The table alias and column aliases are appended to buf.  tabno is the index
+  * for the table alias, and ncols is the number of the column aliases to add.
+  * (The indexes for the column aliases are 1..ncols.)
+  */
+ static void
+ appendSubselectAlias(StringInfo buf, int tabno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= ncols; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get info about the subselect alias to given expression.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively.
+  */
+ static void
+ getSubselectAliasInfo(Var *node, RelOptInfo *foreignrel,
+ 					  int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/* Shouldn't get here */
+ 	elog(ERROR, "unexpected expression in subquery output");
+ }
+ 
+ /*
+  * Returns true if given expression is an output column of a subquery-in-FROM.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively, in that case.
+  */
+ static bool
+ isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!fpinfo->subquery_rels)
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		/*
+ 		 * If outer relation is deparsed as a subqeury, the given expression
+ 		 * will be an output column of the subquery; get the subselect alias
+ 		 * info for the given expression.
+ 		 */
+ 		if (fpinfo->make_outerrel_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, outerrel, tabno, colno);
+ 			return true;
+ 		}
+ 		/* Otherwise, recurse into outer relation */
+ 		if (isSubqueryExpr(node, outerrel, tabno, colno))
+ 			return true;
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * Likewise for inner relation
+ 		 */
+ 		if (fpinfo->make_innerrel_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, innerrel, tabno, colno);
+ 			return true;
+ 		}
+ 		if (isSubqueryExpr(node, innerrel, tabno, colno))
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2223,2248 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
+ 		context->scanrel : context->foreignrel;
+ 	int			tabno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var is an output column of a subquery-in-FROM, deparse
+ 	 * the alias to the given Var instead.
+ 	 */
+ 	if (isSubqueryExpr(node, rel, &tabno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1281 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
      | 57
  (8 rows)
  
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
! SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                            QUERY PLAN                                                                                                                                            
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
!    Output: t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
!          Output: t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
! SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!  c1 | c1 | c1 
! ----+----+----
!  52 | 51 |   
!  58 | 57 |   
!     |    |  2
!     |    |  4
!     |    |  6
!     |    |  8
!     |    | 10
!     |    | 12
!     |    | 14
!     |    | 16
  (10 rows)
  
  -- full outer join three tables
--- 1241,1319 ----
      | 57
  (8 rows)
  
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
! SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!                                                                                                                                                                               QUERY PLAN                                                                                                                                                                               
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
!    Output: t1.*, t1.c1, t2.c1, t3.c1
     ->  Foreign Scan
!          Output: t1.*, t1.c1, t2.c1, t3.c1
           Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
!          Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r2.c1, r4.c1 FROM (("S 1"."T 3" r1 INNER JOIN "S 1"."T 4" r2 ON (((r1.c1 = (r2.c1 + 1))) AND ((r1.c1 >= 50)) AND ((r1.c1 <= 60)))) FULL JOIN "S 1"."T 3" r4 ON (((r2.c1 = r4.c1)))) ORDER BY r1.c1 ASC NULLS LAST, r2.c1 ASC NULLS LAST, r4.c1 ASC NULLS LAST
  (6 rows)
  
! SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
!        t1       | c1 | c1 | c1 
! ----------------+----+----+----
!  (52,53,AAA052) | 52 | 51 |   
!  (58,59,AAA058) | 58 | 57 |   
!                 |    |    |  2
!                 |    |    |  4
!                 |    |    |  6
!                 |    |    |  8
!                 |    |    | 10
!                 |    |    | 12
!                 |    |    | 14
!                 |    |    | 16
  (10 rows)
  
  -- full outer join three tables
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3100,3123 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,681 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Set the subquery information */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	fpinfo->subquery_rels = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 4147,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4155,4176 ----
  	fpinfo->jointype = jointype;
  
  	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use of deparser.
+ 	 */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+ 	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+ 									  fpinfo_o->subquery_rels);
+ 
+ 	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4181,4187 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4210,4236 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that deparser can take appropriate action.  We
! 			 * also save the relids of that relation that is covered by the
! 			 * subquery for later use of deparser.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo->make_innerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														innerrel->relids);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4311,4326 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 95,100 **** typedef struct PgFdwRelationInfo
--- 95,113 ----
  
  	/* Grouping information */
  	List	   *grouped_tlist;
+ 
+ 	/* Subquery information */
+ 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+ 										 * subquery? */
+ 	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+ 										 * subquery? */
+ 	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+ 
+ 	/*
+ 	 * Index of the relation.  It is used for creating a subselect alias when
+ 	 * deparsing the relation as a subquery.
+ 	 */
+ 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,403 **** EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
! SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
! SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  -- full outer join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
--- 391,413 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
! SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
! SELECT t1, t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
  -- full outer join three tables
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 803,814 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
#42Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#41)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/11/04 19:55, Etsuro Fujita wrote:

Attached is an updated version of the patch.

I noticed that I have included an unrelated regression test in the
patch. Attached is a patch with the test removed.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v3.patchapplication/x-patch; name=postgres-fdw-subquery-support-v3.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,180 ----
  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(Var *node, RelOptInfo *foreignrel,
+ 					  int *tabno, int *colno);
+ static bool isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno);
  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,
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1163,1182 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery, because in that
+ 		 * case we would have already considered locking for the relation
+ 		 * while deparsing the lower subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1364,1385 ----
  
  	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
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1435,1585 ----
  }
  
  /*
+  * Append operand relation of foreign join to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool make_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	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);
+ }
+ 
+ /*
+  * Add a subselect alias to a subquery-in-FROM.
+  *
+  * The table alias and column aliases are appended to buf.  tabno is the index
+  * for the table alias, and ncols is the number of the column aliases to add.
+  * (The indexes for the column aliases are 1..ncols.)
+  */
+ static void
+ appendSubselectAlias(StringInfo buf, int tabno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= ncols; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get info about the subselect alias to given expression.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively.
+  */
+ static void
+ getSubselectAliasInfo(Var *node, RelOptInfo *foreignrel,
+ 					  int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/* Shouldn't get here */
+ 	elog(ERROR, "unexpected expression in subquery output");
+ }
+ 
+ /*
+  * Returns true if given expression is an output column of a subquery-in-FROM.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively, in that case.
+  */
+ static bool
+ isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!fpinfo->subquery_rels)
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		/*
+ 		 * If outer relation is deparsed as a subqeury, the given expression
+ 		 * will be an output column of the subquery; get the subselect alias
+ 		 * info for the given expression.
+ 		 */
+ 		if (fpinfo->make_outerrel_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, outerrel, tabno, colno);
+ 			return true;
+ 		}
+ 		/* Otherwise, recurse into outer relation */
+ 		if (isSubqueryExpr(node, outerrel, tabno, colno))
+ 			return true;
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * Likewise for inner relation
+ 		 */
+ 		if (fpinfo->make_innerrel_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, innerrel, tabno, colno);
+ 			return true;
+ 		}
+ 		if (isSubqueryExpr(node, innerrel, tabno, colno))
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2223,2248 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
+ 		context->scanrel : context->foreignrel;
+ 	int			tabno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var is an output column of a subquery-in-FROM, deparse
+ 	 * the alias to the given Var instead.
+ 	 */
+ 	if (isSubqueryExpr(node, rel, &tabno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1293 ----
      | 57
  (8 rows)
  
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3100,3123 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,681 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Set the subquery information */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	fpinfo->subquery_rels = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 4147,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4155,4176 ----
  	fpinfo->jointype = jointype;
  
  	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use of deparser.
+ 	 */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+ 	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+ 									  fpinfo_o->subquery_rels);
+ 
+ 	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4181,4187 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4210,4236 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that deparser can take appropriate action.  We
! 			 * also save the relids of that relation that is covered by the
! 			 * subquery for later use of deparser.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo->make_innerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														innerrel->relids);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4311,4326 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 95,100 **** typedef struct PgFdwRelationInfo
--- 95,113 ----
  
  	/* Grouping information */
  	List	   *grouped_tlist;
+ 
+ 	/* Subquery information */
+ 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+ 										 * subquery? */
+ 	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+ 										 * subquery? */
+ 	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+ 
+ 	/*
+ 	 * Index of the relation.  It is used for creating a subselect alias when
+ 	 * deparsing the relation as a subquery.
+ 	 */
+ 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,409 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 803,814 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
#43Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#42)
2 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/11/07 11:24, Etsuro Fujita wrote:

On 2016/11/04 19:55, Etsuro Fujita wrote:

Attached is an updated version of the patch.

I noticed that I have included an unrelated regression test in the
patch. Attached is a patch with the test removed.

I noticed that I inadvertently removed some changes from that patch, so
I fixed that. Please find attached an updated version of the patch.
I'm also attaching an updated version of another patch for evaluating
PHVs remotely, which has been created on top of that patch. Changes to
the latter: I revised comments and added a bit more regression tests.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v4.patchapplication/x-patch; name=postgres-fdw-subquery-support-v4.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,180 ----
  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(Var *node, RelOptInfo *foreignrel,
+ 					  int *tabno, int *colno);
+ static bool isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno);
  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,
***************
*** 990,999 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
  	{
! 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
--- 998,1013 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
+ 	/*
+ 	 * Note: non-NIL tlist might be supplied for a baserel; if the baserel is
+ 	 * deparsed as a subquery, non-NIL tlist will be supplied for the baserel.
+ 	 * (See deparseRangeTblRef.)  In that case use deparseExplicitTargetList.
+ 	 */
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL ||
! 		tlist != NIL)
  	{
! 		/* Use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1169,1188 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery, because in that
+ 		 * case we would have already considered locking for the relation
+ 		 * while deparsing the lower subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1370,1391 ----
  
  	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
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1441,1591 ----
  }
  
  /*
+  * Append operand relation of foreign join to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool make_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	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);
+ }
+ 
+ /*
+  * Add a subselect alias to a subquery-in-FROM.
+  *
+  * The table alias and column aliases are appended to buf.  tabno is the index
+  * for the table alias, and ncols is the number of the column aliases to add.
+  * (The indexes for the column aliases are 1..ncols.)
+  */
+ static void
+ appendSubselectAlias(StringInfo buf, int tabno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= ncols; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get info about the subselect alias to given expression.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively.
+  */
+ static void
+ getSubselectAliasInfo(Var *node, RelOptInfo *foreignrel,
+ 					  int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/* Shouldn't get here */
+ 	elog(ERROR, "unexpected expression in subquery output");
+ }
+ 
+ /*
+  * Returns true if given expression is an output column of a subquery-in-FROM.
+  *
+  * The subselect table and column numbers are returned to *tabno and *colno,
+  * respectively, in that case.
+  */
+ static bool
+ isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!fpinfo->subquery_rels)
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		/*
+ 		 * If outer relation is deparsed as a subqeury, the given expression
+ 		 * will be an output column of the subquery; get the subselect alias
+ 		 * info for the given expression.
+ 		 */
+ 		if (fpinfo->make_outerrel_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, outerrel, tabno, colno);
+ 			return true;
+ 		}
+ 		/* Otherwise, recurse into outer relation */
+ 		if (isSubqueryExpr(node, outerrel, tabno, colno))
+ 			return true;
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * Likewise for inner relation
+ 		 */
+ 		if (fpinfo->make_innerrel_subquery)
+ 		{
+ 			getSubselectAliasInfo(node, innerrel, tabno, colno);
+ 			return true;
+ 		}
+ 		if (isSubqueryExpr(node, innerrel, tabno, colno))
+ 			return true;
+ 	}
+ 	return false;
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2229,2254 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
+ 		context->scanrel : context->foreignrel;
+ 	int			tabno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var is an output column of a subquery-in-FROM, deparse
+ 	 * the alias to the given Var instead.
+ 	 */
+ 	if (isSubqueryExpr(node, rel, &tabno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1293 ----
      | 57
  (8 rows)
  
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3100,3123 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,681 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Set the subquery information */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	fpinfo->subquery_rels = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 4147,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4155,4176 ----
  	fpinfo->jointype = jointype;
  
  	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use of deparser.
+ 	 */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+ 	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+ 									  fpinfo_o->subquery_rels);
+ 
+ 	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4181,4187 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4210,4236 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that deparser can take appropriate action.  We
! 			 * also save the relids of that relation that is covered by the
! 			 * subquery for later use of deparser.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo->make_innerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														innerrel->relids);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4311,4326 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 95,100 **** typedef struct PgFdwRelationInfo
--- 95,113 ----
  
  	/* Grouping information */
  	List	   *grouped_tlist;
+ 
+ 	/* Subquery information */
+ 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+ 										 * subquery? */
+ 	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+ 										 * subquery? */
+ 	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+ 
+ 	/*
+ 	 * Index of the relation.  It is used for creating a subselect alias when
+ 	 * deparsing the relation as a subquery.
+ 	 */
+ 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,409 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 803,814 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
postgres-fdw-phv-pushdown-v4.patchapplication/x-patch; name=postgres-fdw-phv-pushdown-v4.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 49,54 ****
--- 49,55 ----
  #include "nodes/nodeFuncs.h"
  #include "nodes/plannodes.h"
  #include "optimizer/clauses.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/prep.h"
  #include "optimizer/tlist.h"
  #include "optimizer/var.h"
***************
*** 157,162 **** static void deparseRelabelType(RelabelType *node, deparse_expr_cxt *context);
--- 158,164 ----
  static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context);
  static void deparseNullTest(NullTest *node, deparse_expr_cxt *context);
  static void deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context);
+ static void deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context);
  static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
***************
*** 172,180 **** 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(Var *node, RelOptInfo *foreignrel,
  					  int *tabno, int *colno);
! static bool isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno);
  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,
--- 174,183 ----
  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);
! static bool isSubqueryExpr(Expr *node, PlannerInfo *root, RelOptInfo *foreignrel,
! 			   int *tabno, int *colno);
  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,
***************
*** 765,770 **** foreign_expr_walker(Node *node,
--- 768,792 ----
  					state = FDW_COLLATE_UNSAFE;
  			}
  			break;
+ 		case T_PlaceHolderVar:
+ 			{
+ 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 				PlaceHolderInfo *phinfo = find_placeholder_info(glob_cxt->root,
+ 																phv, false);
+ 
+ 				/*
+ 				 * If the PHV's contained expression is computable on the
+ 				 * remote server, we consider the PHV safe to send.
+ 				 */
+ 				if (phv->phlevelsup == 0 &&
+ 					bms_is_subset(phinfo->ph_eval_at,
+ 								  glob_cxt->foreignrel->relids))
+ 					return foreign_expr_walker((Node *) phv->phexpr,
+ 											   glob_cxt, outer_cxt);
+ 				else
+ 					return false;
+ 			}
+ 			break;
  		default:
  
  			/*
***************
*** 859,865 **** deparse_type_name(Oid type_oid, int32 typemod)
   * foreign server.
   */
  List *
! build_tlist_to_deparse(RelOptInfo *foreignrel)
  {
  	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
--- 881,887 ----
   * foreign server.
   */
  List *
! build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel)
  {
  	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
***************
*** 872,886 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
  		return fpinfo->grouped_tlist;
  
  	/*
! 	 * We require columns specified in foreignrel->reltarget->exprs and those
! 	 * required for evaluating the local conditions.
  	 */
! 	tlist = add_to_flat_tlist(tlist,
! 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
! 									   PVC_RECURSE_PLACEHOLDERS));
! 	tlist = add_to_flat_tlist(tlist,
! 							  pull_var_clause((Node *) fpinfo->local_conds,
! 											  PVC_RECURSE_PLACEHOLDERS));
  
  	return tlist;
  }
--- 894,966 ----
  		return fpinfo->grouped_tlist;
  
  	/*
! 	 * Fetch all expressions in foreignrel's reltarget if the
! 	 * reltarget_is_shippable flag is set TRUE.  Otherwise, fetch shipplable
! 	 * expressions in the reltarget plus expressions required for evaluating
! 	 * non-shippable expressions in the reltarget.
! 	 */
! 	if (fpinfo->reltarget_is_shippable)
! 		tlist = make_tlist_from_pathtarget(foreignrel->reltarget);
! 	else
! 	{
! 		List	   *exprs = NIL;
! 		ListCell   *lc;
! 
! 		/* Note: we have at least one non-shippable PHV in the reltarget. */
! 		foreach(lc, foreignrel->reltarget->exprs)
! 		{
! 			Node	   *node = (Node *) lfirst(lc);
! 
! 			if (IsA(node, Var))
! 				exprs = lappend(exprs, node);
! 			else if (IsA(node, PlaceHolderVar))
! 			{
! 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 				PlaceHolderInfo *phinfo = find_placeholder_info(root, phv,
! 																false);
! 
! 				if (bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->outerrel->relids) ||
! 					bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->innerrel->relids))
! 				{
! 					/*
! 					 * The PHV coming from an either input should be shippable.
! 					 */
! 					exprs = lappend(exprs, node);
! 				}
! 				else
! 				{
! 					/*
! 					 * The PHV might be shippable, but in any case just fetch
! 					 * expressions required for evaluating the PHV.
! 					 */
! 					List	   *exprs2 = pull_var_clause(node,
! 													PVC_INCLUDE_PLACEHOLDERS);
! 					ListCell   *lc2;
! 
! 					foreach(lc2, exprs2)
! 					{
! 						Node	   *node2 = (Node *) lfirst(lc2);
! 
! 						exprs = lappend(exprs, node2);
! 					}
! 				}
! 			}
! 			else
! 				elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
! 		}
! 
! 		tlist = add_to_flat_tlist(tlist, exprs);
! 	}
! 
! 	/*
! 	 * Fetch expressions required for evaluating local conditions, if any.
  	 */
! 	if (fpinfo->local_conds)
! 		tlist = add_to_flat_tlist(tlist,
! 								pull_var_clause((Node *) fpinfo->local_conds,
! 												PVC_INCLUDE_PLACEHOLDERS));
  
  	return tlist;
  }
***************
*** 1451,1457 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
  		   foreignrel->reloptkind == RELOPT_JOINREL);
! 	Assert(fpinfo->local_conds == NIL);
  
  	if (make_subquery)
  	{
--- 1531,1537 ----
  
  	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
  		   foreignrel->reloptkind == RELOPT_JOINREL);
! 	Assert(fpinfo->reltarget_is_shippable && fpinfo->local_conds == NIL);
  
  	if (make_subquery)
  	{
***************
*** 1506,1512 **** appendSubselectAlias(StringInfo buf, int tabno, int ncols)
   * respectively.
   */
  static void
! getSubselectAliasInfo(Var *node, RelOptInfo *foreignrel,
  					  int *tabno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
--- 1586,1592 ----
   * respectively.
   */
  static void
! getSubselectAliasInfo(Expr *node, RelOptInfo *foreignrel,
  					  int *tabno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
***************
*** 1539,1549 **** getSubselectAliasInfo(Var *node, RelOptInfo *foreignrel,
   * respectively, in that case.
   */
  static bool
! isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
  	RelOptInfo *innerrel = fpinfo->innerrel;
  
  	if (foreignrel->reloptkind != RELOPT_JOINREL)
  		return false;
--- 1619,1631 ----
   * respectively, in that case.
   */
  static bool
! isSubqueryExpr(Expr *node, PlannerInfo *root, RelOptInfo *foreignrel,
! 			   int *tabno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
  	RelOptInfo *innerrel = fpinfo->innerrel;
+ 	bool		is_outer_var;
  
  	if (foreignrel->reloptkind != RELOPT_JOINREL)
  		return false;
***************
*** 1551,1557 **** isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  	if (!fpinfo->subquery_rels)
  		return false;
  
! 	if (bms_is_member(node->varno, outerrel->relids))
  	{
  		/*
  		 * If outer relation is deparsed as a subqeury, the given expression
--- 1633,1664 ----
  	if (!fpinfo->subquery_rels)
  		return false;
  
! 	if (IsA(node, Var))
! 	{
! 		Var		   *var = (Var *) node;
! 
! 		is_outer_var = bms_is_member(var->varno, outerrel->relids);
! 	}
! 	else if (IsA(node, PlaceHolderVar))
! 	{
! 		PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 		PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
! 		bool		is_inner_var;
! 
! 		is_outer_var = bms_is_subset(phinfo->ph_eval_at, outerrel->relids);
! 		is_inner_var = bms_is_subset(phinfo->ph_eval_at, innerrel->relids);
! 
! 		/*
! 		 * If the PHV can't be computed in either input, it should be computed
! 		 * in the current join level, so it's not a subquery output column.
! 		 */
! 		if (!is_outer_var && !is_inner_var)
! 			return false;
! 	}
! 	else
! 		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
! 
! 	if (is_outer_var)
  	{
  		/*
  		 * If outer relation is deparsed as a subqeury, the given expression
***************
*** 1564,1576 **** isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  			return true;
  		}
  		/* Otherwise, recurse into outer relation */
! 		if (isSubqueryExpr(node, outerrel, tabno, colno))
  			return true;
  	}
  	else
  	{
- 		Assert(bms_is_member(node->varno, innerrel->relids));
- 
  		/*
  		 * Likewise for inner relation
  		 */
--- 1671,1681 ----
  			return true;
  		}
  		/* Otherwise, recurse into outer relation */
! 		if (isSubqueryExpr(node, root, outerrel, tabno, colno))
  			return true;
  	}
  	else
  	{
  		/*
  		 * Likewise for inner relation
  		 */
***************
*** 1579,1585 **** isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
  			getSubselectAliasInfo(node, innerrel, tabno, colno);
  			return true;
  		}
! 		if (isSubqueryExpr(node, innerrel, tabno, colno))
  			return true;
  	}
  	return false;
--- 1684,1690 ----
  			getSubselectAliasInfo(node, innerrel, tabno, colno);
  			return true;
  		}
! 		if (isSubqueryExpr(node, root, innerrel, tabno, colno))
  			return true;
  	}
  	return false;
***************
*** 1966,1974 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.  However, we must be
! 		 * careful; the table could be beneath an outer join, in which case it
! 		 * must go to NULL whenever the rest of the row does.
  		 */
  		Oid			fetchval = 0;
  
--- 2071,2077 ----
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.
  		 */
  		Oid			fetchval = 0;
  
***************
*** 1978,1991 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  			fetchval = rte->relid;
  		}
  
! 		if (qualify_col)
! 		{
! 			appendStringInfoString(buf, "CASE WHEN (");
! 			ADD_REL_QUALIFIER(buf, varno);
! 			appendStringInfo(buf, "*)::text IS NOT NULL THEN %u END", fetchval);
! 		}
! 		else
! 			appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
--- 2081,2087 ----
  			fetchval = rte->relid;
  		}
  
! 		appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
***************
*** 2016,2043 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  		attrs_used = bms_add_member(NULL,
  									0 - FirstLowInvalidHeapAttributeNumber);
  
- 		/*
- 		 * In case the whole-row reference is under an outer join then it has
- 		 * to go NULL whenever the rest of the row goes NULL. Deparsing a join
- 		 * query would always involve multiple relations, thus qualify_col
- 		 * would be true.
- 		 */
- 		if (qualify_col)
- 		{
- 			appendStringInfoString(buf, "CASE WHEN (");
- 			ADD_REL_QUALIFIER(buf, varno);
- 			appendStringInfo(buf, "*)::text IS NOT NULL THEN ");
- 		}
- 
  		appendStringInfoString(buf, "ROW(");
  		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
  						  &retrieved_attrs);
  		appendStringInfoString(buf, ")");
  
- 		/* Complete the CASE WHEN statement started above. */
- 		if (qualify_col)
- 			appendStringInfo(buf, " END");
- 
  		heap_close(rel, NoLock);
  		bms_free(attrs_used);
  	}
--- 2112,2122 ----
***************
*** 2210,2215 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 2289,2297 ----
  		case T_Aggref:
  			deparseAggref((Aggref *) node, context);
  			break;
+ 		case T_PlaceHolderVar:
+ 			deparseExprInPlaceHolderVar((PlaceHolderVar *) node, context);
+ 			break;
  		default:
  			elog(ERROR, "unsupported expression type for deparse: %d",
  				 (int) nodeTag(node));
***************
*** 2229,2234 **** static void
--- 2311,2317 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	PlannerInfo *root = context->root;
  	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
  		context->scanrel : context->foreignrel;
  	int			tabno;
***************
*** 2241,2247 **** deparseVar(Var *node, deparse_expr_cxt *context)
  	 * If the given Var is an output column of a subquery-in-FROM, deparse
  	 * the alias to the given Var instead.
  	 */
! 	if (isSubqueryExpr(node, rel, &tabno, &colno))
  	{
  		appendStringInfo(context->buf, "%s%d.%s%d",
  						 SS_TAB_ALIAS_PREFIX, tabno,
--- 2324,2330 ----
  	 * If the given Var is an output column of a subquery-in-FROM, deparse
  	 * the alias to the given Var instead.
  	 */
! 	if (isSubqueryExpr((Expr *) node, root, rel, &tabno, &colno))
  	{
  		appendStringInfo(context->buf, "%s%d.%s%d",
  						 SS_TAB_ALIAS_PREFIX, tabno,
***************
*** 2935,2940 **** appendAggOrderBy(List *orderList, List *targetList, deparse_expr_cxt *context)
--- 3018,3050 ----
  }
  
  /*
+  * Deparse a PlaceHolderVar's contained expression.
+  */
+ static void
+ deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context)
+ {
+ 	PlannerInfo *root = context->root;
+ 	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
+ 		context->scanrel : context->foreignrel;
+ 	int			tabno;
+ 	int			colno;
+ 
+ 	/*
+ 	 * If the given PHV is an output column of a subquery-in-FROM, deparse
+ 	 * the alias to the given PHV instead.
+ 	 */
+ 	if (isSubqueryExpr((Expr *) node, root, rel, &tabno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
+ 	deparseExpr(node->phexpr, context);
+ }
+ 
+ /*
   * Print the representation of a parameter to be sent to the remote side.
   *
   * Note: we always label the Param's type explicitly rather than relying on
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1547,1554 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1547,1554 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                         QUERY PLAN                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1556,1562 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1556,1562 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1591,1598 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1591,1598 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                              QUERY PLAN                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1600,1606 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1600,1606 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1636,1643 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                               
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1636,1643 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                        QUERY PLAN                                                                                                                                                                        
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1645,1651 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1645,1651 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1680,1687 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1680,1687 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                             QUERY PLAN                                                                                                                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1689,1695 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1689,1695 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1759,1772 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1759,1772 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                     QUERY PLAN                                                                                                                                                                     
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s1.c4, s2.c1 FROM ((SELECT ctid, ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1", c3 FROM "S 1"."T 1") s1(c1, c2, c3, c4) INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c3 = s2.c2)))) ORDER BY s1.c4 ASC NULLS LAST, s1.c3 ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 2095,2119 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
     1
  (10 rows)
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                         QUERY PLAN                                                         
! ---------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: (13), ft2.c1
!    Join Filter: (13 = ft2.c1)
!    ->  Foreign Scan on public.ft2
!          Output: ft2.c1
!          Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (("C 1" >= 10)) AND (("C 1" <= 15)) ORDER BY "C 1" ASC NULLS LAST
!    ->  Materialize
!          Output: (13)
!          ->  Foreign Scan on public.ft1
!                Output: 13
!                Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 13))
! (11 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
--- 2095,2110 ----
     1
  (10 rows)
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                                                               QUERY PLAN                                                                                              
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: (13), ft2.c1
!    Relations: (public.ft2) LEFT JOIN (public.ft1)
!    Remote SQL: SELECT r2."C 1", s4.c1 FROM ("S 1"."T 1" r2 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s4(c1) ON (((13 = r2."C 1")))) WHERE ((r2."C 1" >= 10)) AND ((r2."C 1" <= 15))
! (4 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
***************
*** 2126,2149 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
      | 15
  (6 rows)
  
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                     QUERY PLAN                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Join Filter: (ft4.c1 = ft1.c1)
!    ->  Foreign Scan on public.ft4
!          Output: ft4.c1, ft4.c2, ft4.c3
!          Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 10)) AND ((c1 <= 15))
!    ->  Materialize
!          Output: ft1.c1, ft2.c1, (13)
!          ->  Foreign Scan
!                Output: ft1.c1, ft2.c1, 13
!                Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
! (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
--- 2117,2131 ----
      | 15
  (6 rows)
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                                                                           QUERY PLAN                                                                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Relations: (public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2))
!    Remote SQL: SELECT r1.c1, s7.c1, s7.c2, s7.c3 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4."C 1", r5."C 1", 13 FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12))))) s7(c1, c2, c3) ON (((r1.c1 = s7.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
! (4 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
***************
*** 2153,2168 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
   14 |    |    |   
  (3 rows)
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                 QUERY PLAN                                                                                                                                 
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2135,2215 ----
   14 |    |    |   
  (3 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                               QUERY PLAN                                                                                                                                                                               
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft2.c1, (13), (13), ft2_1.c1
+    Relations: (public.ft2) LEFT JOIN ((public.ft2) LEFT JOIN (public.ft1))
+    Remote SQL: SELECT r1."C 1", s8.c1, s8.c2, s8.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r5."C 1", 13, s7.c1 FROM ("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s7(c1) ON (((13 = r5."C 1")))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))) s8(c1, c2, c3) ON (((r1."C 1" = 13)))) WHERE ((r1."C 1" >= 10)) AND ((r1."C 1" <= 15))
+ (4 rows)
+ 
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 
+ ----+----+----+----
+  10 |    |    |   
+  11 |    |    |   
+  12 |    |    |   
+  13 | 13 |    | 10
+  13 | 13 |    | 11
+  13 | 13 |    | 12
+  13 | 13 | 13 | 13
+  13 | 13 |    | 14
+  13 | 13 |    | 15
+  14 |    |    |   
+  15 |    |    |   
+ (11 rows)
+ 
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+                                                                                                                             QUERY PLAN                                                                                                                            
+ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft2.c1, 13, (13), ft2_1.c1
+    Relations: ((public.ft2) LEFT JOIN (public.ft1)) LEFT JOIN (public.ft2)
+    Remote SQL: SELECT r5."C 1", r1."C 1", s7.c1 FROM (("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s7(c1) ON (((13 = r5."C 1")))) LEFT JOIN "S 1"."T 1" r1 ON (((r1."C 1" = 13)))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))
+ (4 rows)
+ 
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+  c1 | a2 | b2 | c2 
+ ----+----+----+----
+  13 | 13 |    | 10
+  13 | 13 |    | 11
+  13 | 13 |    | 12
+  13 | 13 | 13 | 13
+  13 | 13 |    | 14
+  13 | 13 |    | 15
+ (6 rows)
+ 
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                                                                                                 QUERY PLAN                                                                                                                                                                                                                                                
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ((13 IS NOT NULL)), ft4_1.c1, (13), ft1.c1, ft2.c1
+    Relations: (public.ft4) LEFT JOIN ((public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2)))
+    Remote SQL: SELECT r1.c1, s11.c1, s11.c2, s11.c3, s11.c4, s11.c5 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4.c1, s10.c1, s10.c2, (s10.c3 IS NOT NULL), s10.c3 FROM ("S 1"."T 3" r4 LEFT JOIN (SELECT r7."C 1", r8."C 1", 13 FROM ("S 1"."T 1" r7 INNER JOIN "S 1"."T 1" r8 ON (((r8."C 1" = 12)) AND ((r7."C 1" = 12))))) s10(c1, c2, c3) ON (((r4.c1 = s10.c1)))) WHERE ((r4.c1 >= 10)) AND ((r4.c1 <= 15))) s11(c1, c2, c3, c4, c5) ON (((r1.c1 = s11.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
+ (4 rows)
+ 
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 | d2 | e2 
+ ----+----+----+----+----+----
+  10 | f  | 10 |    |    |   
+  12 | t  | 12 | 13 | 12 | 12
+  14 | f  | 14 |    |    |   
+ (3 rows)
+ 
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                               QUERY PLAN                                                                                                                              
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s1.c4, r2.c1, r2.c2 FROM ((SELECT ROW(c1, c2, c3), c1, c2, c3 FROM "S 1"."T 4") s1(c1, c2, c3, c4) INNER JOIN "S 1"."T 3" r2 ON (((s1.c2 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY s1.c2 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 4064,4077 **** 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)
--- 4111,4124 ----
  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)
***************
*** 4207,4220 **** 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
!                                                                                                                               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)
--- 4254,4267 ----
  
  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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 28,33 ****
--- 28,34 ----
  #include "optimizer/clauses.h"
  #include "optimizer/pathnode.h"
  #include "optimizer/paths.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/planmain.h"
  #include "optimizer/restrictinfo.h"
  #include "optimizer/var.h"
***************
*** 582,587 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 583,633 ----
  	}
  
  	/*
+ 	 * Detect whether expressions in the relation's reltarget are shippable
+ 	 * and whether there are any non-Vars in those expressions.
+ 	 *
+ 	 * Note: whole-row Vars and system columns other than ctid and oid are
+ 	 * treated like non-Vars.  (See comments in postgres_fdw.h.)
+ 	 *
+ 	 * Note: if the relation is an appendrel child, it isn't in the main join
+ 	 * tree, so those information is not used.
+ 	 */
+ 	if (baserel->reloptkind == RELOPT_BASEREL)
+ 	{
+ 		bool		reltarget_is_shippable = true;
+ 		bool		reltarget_has_non_vars = false;
+ 
+ 		foreach(lc, baserel->reltarget->exprs)
+ 		{
+ 			Node	   *node = (Node *) lfirst(lc);
+ 
+ 			if (IsA(node, Var))
+ 			{
+ 				Var		   *var = (Var *) node;
+ 
+ 				if (var->varattno <= 0 &&
+ 					var->varattno != SelfItemPointerAttributeNumber &&
+ 					var->varattno != ObjectIdAttributeNumber)
+ 					reltarget_has_non_vars = true;
+ 			}
+ 			else if (IsA(node, PlaceHolderVar))
+ 			{
+ 				reltarget_has_non_vars = true;
+ 
+ 				if (!is_foreign_expr(root, baserel, (Expr *) node))
+ 				{
+ 					reltarget_is_shippable = false;
+ 					break;
+ 				}
+ 			}
+ 			else
+ 				elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
+ 		}
+ 		fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 		fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 	}
+ 
+ 	/*
  	 * Compute the selectivity and cost of the local_conds, so we don't have
  	 * to do it over again for each path.  The best we can do for these
  	 * conditions is to estimate selectivity on the basis of local statistics.
***************
*** 1199,1205 **** postgresGetForeignPlan(PlannerInfo *root,
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
--- 1245,1251 ----
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
***************
*** 2539,2545 **** estimate_path_cost_size(PlannerInfo *root,
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL ||
  			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 			fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
--- 2585,2591 ----
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL ||
  			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 			fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
***************
*** 4055,4060 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4101,4108 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	bool		reltarget_is_shippable = true;
+ 	bool		reltarget_has_non_vars = false;
  
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
***************
*** 4084,4089 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4132,4145 ----
  	if (fpinfo_o->local_conds || fpinfo_i->local_conds)
  		return false;
  
+ 	/*
+ 	 * If the reltargets of joining relations are not shippable, those are
+ 	 * required to be evaluated locally, so the join can not be pushed down.
+ 	 */
+ 	if (!fpinfo_o->reltarget_is_shippable ||
+ 		!fpinfo_i->reltarget_is_shippable)
+ 		return false;
+ 
  	/* Separate restrict list into join quals and quals on join relation */
  	if (IS_OUTER_JOIN(jointype))
  		extract_actual_join_clauses(extra->restrictlist, &joinclauses, &otherclauses);
***************
*** 4109,4134 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			return false;
  	}
  
- 	/*
- 	 * deparseExplicitTargetList() isn't smart enough to handle anything other
- 	 * than a Var.  In particular, if there's some PlaceHolderVar that would
- 	 * need to be evaluated within this join tree (because there's an upper
- 	 * reference to a quantity that may go to NULL as a result of an outer
- 	 * join), then we can't try to push the join down because we'll fail when
- 	 * we get to deparseExplicitTargetList().  However, a PlaceHolderVar that
- 	 * needs to be evaluated *at the top* of this join tree is OK, because we
- 	 * can do that locally after fetching the results from the remote side.
- 	 */
- 	foreach(lc, root->placeholder_list)
- 	{
- 		PlaceHolderInfo *phinfo = lfirst(lc);
- 		Relids		relids = joinrel->relids;
- 
- 		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
- 			bms_nonempty_difference(relids, phinfo->ph_eval_at))
- 			return false;
- 	}
- 
  	/* Save the join clauses, for later use. */
  	fpinfo->joinclauses = joinclauses;
  
--- 4165,4170 ----
***************
*** 4185,4211 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
  	 */
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
--- 4221,4271 ----
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
+ 	 *
+ 	 * If the joining relation has reltarget_has_non_vars=TRUE, that relation
+ 	 * is deparsed as a subquery and the remote_conds of that relation is
+ 	 * transformed into the WHERE clause of the subquery.  So in that case,
+ 	 * don't pull up the remote_conds into the joinclauses or remote_conds of
+ 	 * this relation.
  	 */
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->make_innerrel_subquery = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->make_outerrel_subquery = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->make_innerrel_subquery = true;
! 			else
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->make_outerrel_subquery = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo->make_outerrel_subquery = true;
! 			else
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo->make_innerrel_subquery = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
***************
*** 4215,4236 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			 * In this case, if any of the joining relations has conditions,
  			 * we need to deparse that relation as a subquery so that
  			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that deparser can take appropriate action.  We
! 			 * also save the relids of that relation that is covered by the
! 			 * subquery for later use of deparser.
  			 */
! 			if (fpinfo_o->remote_conds)
! 			{
  				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
  				fpinfo->make_innerrel_subquery = true;
- 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
- 														innerrel->relids);
- 			}
  			break;
  
  		default:
--- 4275,4286 ----
  			 * In this case, if any of the joining relations has conditions,
  			 * we need to deparse that relation as a subquery so that
  			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that deparser can take appropriate action.
  			 */
! 			if (fpinfo_o->reltarget_has_non_vars || fpinfo_o->remote_conds)
  				fpinfo->make_outerrel_subquery = true;
! 			if (fpinfo_i->reltarget_has_non_vars || fpinfo_i->remote_conds)
  				fpinfo->make_innerrel_subquery = true;
  			break;
  
  		default:
***************
*** 4239,4244 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4289,4305 ----
  	}
  
  	/*
+ 	 * If the joining relation is deparsed as a subquery, save the relids of
+ 	 * 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);
+ 	if (fpinfo->make_innerrel_subquery)
+ 		fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
+ 												innerrel->relids);
+ 
+ 	/*
  	 * For an inner join, all restrictions can be treated alike. Treating the
  	 * pushed down conditions as join conditions allows a top level full outer
  	 * join to be deparsed without requiring subqueries.
***************
*** 4254,4259 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4315,4358 ----
  	fpinfo->pushdown_safe = true;
  
  	/*
+ 	 * Detect whether expressions in the relation's reltarget are shippable
+ 	 * and whether there are any non-Vars in those expressions.
+ 	 *
+ 	 * Note: whole-row Vars and system columns other than ctid and oid of a
+ 	 * base relation (if any) are handled as output columns of a lower
+ 	 * subquery by deparser.  So no need to be careful about those columns.
+ 	 */
+ 	foreach(lc, joinrel->reltarget->exprs)
+ 	{
+ 		Node	   *node = (Node *) lfirst(lc);
+ 
+ 		if (IsA(node, Var))
+ 			continue;
+ 		if (IsA(node, PlaceHolderVar))
+ 		{
+ 			PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 			PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
+ 
+ 			/* Ignore the PHV if it has bubbled up from an either input. */
+ 			if (bms_is_subset(phinfo->ph_eval_at, outerrel->relids) ||
+ 				bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
+ 				continue;
+ 
+ 			reltarget_has_non_vars = true;
+ 
+ 			if (!is_foreign_expr(root, joinrel, (Expr *) node))
+ 			{
+ 				reltarget_is_shippable = false;
+ 				break;
+ 			}
+ 		}
+ 		else
+ 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
+ 	}
+ 	fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 	fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 
+ 	/*
  	 * If user is willing to estimate cost for a scan of either of the joining
  	 * relations using EXPLAIN, he intends to estimate scans on that relation
  	 * more accurately. Then, it makes sense to estimate the cost the join
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 33,38 **** typedef struct PgFdwRelationInfo
--- 33,61 ----
  	bool		pushdown_safe;
  
  	/*
+ 	 * Flags on the reltarget of the relation.
+ 	 *
+ 	 * If each expression in the relation's reltarget is shippable (i.e.,
+ 	 * computable on the remote side), we set reltarget_is_shippalbe to TRUE.
+ 	 * The flag is used to detect whether the relation can be joined with any
+ 	 * other foreign table (or join) on the remote server.  Note that Vars in
+ 	 * the reltarget are shippable, so if any PHVs in the reltarget are
+ 	 * shippable, then the flag is set TRUE.  Also, if the reltarget contains
+ 	 * any PHVs, we set reltarget_has_non_vars to TRUE.  In that case, if the
+ 	 * relation is joined with any other foreign table on the remote server,
+ 	 * the relation is deparsed as a subquery emitting the PHVs when creating
+ 	 * a remote query.  Note that whole-row Vars and system columns other than
+ 	 * ctid and oid in the reltarget of a base relation are treated like PHVs,
+ 	 * because we fetch the whole-row Vars as ROW() expressions and the system
+ 	 * columns as 0, except for tableoid, in which case we fetch it as a valid
+ 	 * value for the local table OID, and because the base table could be
+ 	 * beneath an outer join, in which case those columns must go to NULL
+ 	 * whenever the rest of the row does.
+ 	 */
+ 	bool		reltarget_is_shippable; /* are reltarget exprs shippable? */
+ 	bool		reltarget_has_non_vars;	/* are there any non-Var exprs? */
+ 
+ 	/*
  	 * Restriction clauses, divided into safe and unsafe to pushdown subsets.
  	 *
  	 * For a base foreign relation this is a list of clauses along-with
***************
*** 171,177 **** extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
--- 194,200 ----
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 503,519 **** EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
- 
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
--- 503,524 ----
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
#44Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#43)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On Mon, Nov 7, 2016 at 5:50 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/11/07 11:24, Etsuro Fujita wrote:

On 2016/11/04 19:55, Etsuro Fujita wrote:

Attached is an updated version of the patch.

I noticed that I have included an unrelated regression test in the
patch. Attached is a patch with the test removed.

I noticed that I inadvertently removed some changes from that patch, so I
fixed that. Please find attached an updated version of the patch. I'm also
attaching an updated version of another patch for evaluating PHVs remotely,
which has been created on top of that patch. Changes to the latter: I
revised comments and added a bit more regression tests.

The patch looks in good shape now. Here are some comments. I have also
made several changes to comments correcting grammar, typos, style and
at few places logic. Let me know if the patch looks good.

I guess, below code
+ if (!fpinfo->subquery_rels)
+ return false;
can be changed to
if (!bms_is_member(node->varno, fpinfo->subquery_rels))
return false;
Also the return values from the recursive calls to isSubqueryExpr() can be
returned as is. I have included this change in the patch.

deparse.c seems to be using capitalized names for function which
actually deparse something and an non-capitalized form for helper
functions. From that perspective attached patch renames isSubqueryExpr
as is_subquery_var() and getSubselectAliasInfo() as
get_alias_id_for_var(). Actually both these functions accept a Var
node but somehow their names refer to expr.

This patch is using make_tlist_from_pathtarget() to create tlist to be
deparsed but search in RelOptInfo::reltarget::exprs for a given Var.
As long as the relations deparsed do not carry expressions, this might
work, but it will certainly break once we start deparsing relations
with expressions since the upper most query's tlist contains only
Vars. Instead, we should probably, create tlist and save it in fpinfo
and use it later for searching (tlist_member()?). Possibly use using
build_tlist_to_deparse(), to create the tlist similar so that
targetlist list creation logic is same for all the relations being
deparsed. I haven't included this change in the patch.

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

Attachments:

postgres-fdw-subquery-support-v5.patchtext/x-patch; charset=US-ASCII; name=postgres-fdw-subquery-support-v5.patchDownload
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 66b059a..9946f8b 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -102,20 +102,22 @@ typedef struct deparse_expr_cxt
 								 * foreignrel, when that represents a join or
 								 * a base relation. */
 	StringInfo	buf;			/* output buffer to append to */
 	List	  **params_list;	/* exprs that will become remote Params */
 } deparse_expr_cxt;
 
 #define REL_ALIAS_PREFIX	"r"
 /* Handy macro to add relation name qualification */
 #define ADD_REL_QUALIFIER(buf, varno)	\
 		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+#define SS_TAB_ALIAS_PREFIX	"s"
+#define SS_COL_ALIAS_PREFIX	"c"
 
 /*
  * Functions to determine whether an expression can be evaluated safely on
  * remote server.
  */
 static bool foreign_expr_walker(Node *node,
 					foreign_glob_cxt *glob_cxt,
 					foreign_loc_cxt *outer_cxt);
 static char *deparse_type_name(Oid type_oid, int32 typemod);
 
@@ -160,20 +162,26 @@ static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
 static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
 					   deparse_expr_cxt *context);
 static void deparseSelectSql(List *tlist, List **retrieved_attrs,
 				 deparse_expr_cxt *context);
 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(Var *node, RelOptInfo *foreignrel,
+					  int *tabno, int *colno);
+static bool isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno);
 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,
 				 deparse_expr_cxt *context);
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist,
 					   deparse_expr_cxt *context);
 
 
 /*
@@ -983,26 +991,32 @@ deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
 	StringInfo	buf = context->buf;
 	RelOptInfo *foreignrel = context->foreignrel;
 	PlannerInfo *root = context->root;
 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
 
 	/*
 	 * Construct SELECT list
 	 */
 	appendStringInfoString(buf, "SELECT ");
 
+	/*
+	 * For a non-base relation, use the input tlist. If a base relation is
+	 * being deparsed as a subquery, use input tlist, if the caller has passed
+	 * one. The column aliases of the subquery are crafted based on the input
+	 * tlist. If tlist is NIL, there will not be any expression referring to
+	 * any of the columns of the base relation being deparsed. Hence it doesn't
+	 * matter whether the base relation is being deparsed as subquery or not.
+	 */
 	if (foreignrel->reloptkind == RELOPT_JOINREL ||
-		foreignrel->reloptkind == RELOPT_UPPER_REL)
-	{
-		/* For a join relation use the input tlist */
+		foreignrel->reloptkind == RELOPT_UPPER_REL ||
+		tlist != NIL)
 		deparseExplicitTargetList(tlist, retrieved_attrs, context);
-	}
 	else
 	{
 		/*
 		 * For a base relation fpinfo->attrs_used gives the list of columns
 		 * required to be fetched from the foreign server.
 		 */
 		RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
 
 		/*
 		 * Core code already has some lock on each rel being planned, so we
@@ -1148,25 +1162,34 @@ deparseTargetList(StringInfo buf,
 /*
  * Deparse the appropriate locking clause (FOR UPDATE or FOR SHARE) for a
  * given relation (context->scanrel).
  */
 static void
 deparseLockingClause(deparse_expr_cxt *context)
 {
 	StringInfo	buf = context->buf;
 	PlannerInfo *root = context->root;
 	RelOptInfo *rel = context->scanrel;
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
 	int			relid = -1;
 
 	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
 	{
 		/*
+		 * Ignore relation if it appears in a lower subquery, because in that
+		 * case we would have already considered locking for the relation
+		 * while deparsing the lower subquery.
+		 */
+		if (bms_is_member(relid, fpinfo->subquery_rels))
+			continue;
+
+		/*
 		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
 		 * initial row fetch, rather than later on as is done for local
 		 * tables. The extra roundtrips involved in trying to duplicate the
 		 * local semantics exactly don't seem worthwhile (see also comments
 		 * for RowMarkType).
 		 *
 		 * Note: because we actually run the query as a cursor, this assumes
 		 * that DECLARE CURSOR ... FOR UPDATE is supported, which it isn't
 		 * before 8.3.
 		 */
@@ -1340,32 +1363,32 @@ deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  * 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)
 	{
-		RelOptInfo *rel_o = fpinfo->outerrel;
-		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);
+		deparseRangeTblRef(&join_sql_o, root, fpinfo->outerrel,
+						   fpinfo->make_outerrel_subquery, params_list);
 
 		/* Deparse inner relation */
 		initStringInfo(&join_sql_i);
-		deparseFromExprForRel(&join_sql_i, root, rel_i, true, params_list);
+		deparseRangeTblRef(&join_sql_i, root, fpinfo->innerrel,
+						   fpinfo->make_innerrel_subquery, params_list);
 
 		/*
 		 * For a join relation FROM clause entry is deparsed as
 		 *
 		 * ((outer relation) <join type> (inner relation) ON (joinclauses))
 		 */
 		appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
 					   get_jointype_name(fpinfo->jointype), join_sql_i.data);
 
 		/* Append join clause; (TRUE) if no join clause */
@@ -1407,20 +1430,174 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
 		 * join.
 		 */
 		if (use_alias)
 			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
 
 		heap_close(rel, NoLock);
 	}
 }
 
 /*
+ * Append FROM clause entry for the given relation to buf.
+ *
+ * If make_subquery is true, deparse the relation as a subquery. Otherwise,
+ * deparse it as relation name with alias.
+ */
+static void
+deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+				   bool make_subquery, List **params_list)
+{
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+
+	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+		   foreignrel->reloptkind == RELOPT_JOINREL);
+	Assert(fpinfo->local_conds == NIL);
+
+	if (make_subquery)
+	{
+		List	   *tlist;
+		List	   *retrieved_attrs;
+
+		tlist = make_tlist_from_pathtarget(foreignrel->reltarget);
+		Assert(tlist != NIL);
+
+		/* Append subquery representing the given relation. */
+		appendStringInfoChar(buf, '(');
+		deparseSelectStmtForRel(buf, root, foreignrel, tlist,
+								fpinfo->remote_conds, NIL,
+								&retrieved_attrs, params_list);
+		appendStringInfoChar(buf, ')');
+
+		/*
+		 * Append the relation and column aliases, so that it becomes easy to
+		 * refer to this relation in rest of the query.
+		 */
+		appendSubselectAlias(buf, fpinfo->relation_index,
+							 list_length(foreignrel->reltarget->exprs));
+	}
+	else
+		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+}
+
+/*
+ * Append the relation and column aliases to the subquery.
+ *
+ * 'tabno' is an integer which uniquely identifies a given relation.
+ * 'ncols' is the number of the column aliases to add.
+ */
+static void
+appendSubselectAlias(StringInfo buf, int tabno, int ncols)
+{
+	int			i;
+
+	/* Append the table alias */
+	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+
+	/* Append the column aliases */
+	appendStringInfoChar(buf, '(');
+	for (i = 1; i <= ncols; i++)
+	{
+		if (i > 1)
+			appendStringInfoString(buf, ", ");
+
+		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+	}
+	appendStringInfoChar(buf, ')');
+}
+
+/*
+ * Get the relation and column alias for given Var node, which belongs to
+ * input foreignrel. They are returned in *tabno and *colno respectively.
+ */
+static void
+getSubselectAliasInfo(Var *node, RelOptInfo *foreignrel,
+					  int *tabno, int *colno)
+{
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+	int			i;
+	ListCell   *lc;
+
+	/* Get the table number */
+	*tabno = fpinfo->relation_index;
+
+	/* Get the column number */
+	i = 1;
+	foreach(lc, foreignrel->reltarget->exprs)
+	{
+		if (equal(lfirst(lc), (Node *) node))
+		{
+			*colno = i;
+			return;
+		}
+		i++;
+	}
+
+	/* Shouldn't get here */
+	elog(ERROR, "unexpected expression in subquery output");
+}
+
+/*
+ * Returns true if given Var node belongs to a relation being deparsed as a
+ * subquery. Returns false otherwise. When returning true, it sets tabno and
+ * colno to unique indexes identifying the relation and column resp. to which
+ * the given Var will be deparsed.
+ */
+static bool
+isSubqueryExpr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+{
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+	RelOptInfo *outerrel = fpinfo->outerrel;
+	RelOptInfo *innerrel = fpinfo->innerrel;
+
+	if (foreignrel->reloptkind != RELOPT_JOINREL)
+		return false;
+
+	if (!bms_is_member(node->varno, fpinfo->subquery_rels))
+		return false;
+
+	if (bms_is_member(node->varno, outerrel->relids))
+	{
+		/*
+		 * If outer relation is deparsed as a subquery, get the identifiers for
+		 * the relation and column corresponding to the given Var node.
+		 */
+		if (fpinfo->make_outerrel_subquery)
+		{
+			getSubselectAliasInfo(node, outerrel, tabno, colno);
+			return true;
+		}
+
+		/* Otherwise, recurse into the outer relation. */
+		return isSubqueryExpr(node, outerrel, tabno, colno);
+	}
+	else
+	{
+		Assert(bms_is_member(node->varno, innerrel->relids));
+
+		/*
+		 * If inner relation is deparsed as a subquery, get the identifiers for
+		 * the relation and column corresponding to the given Var node.
+		 */
+		if (fpinfo->make_innerrel_subquery)
+		{
+			getSubselectAliasInfo(node, innerrel, tabno, colno);
+			return true;
+		}
+
+		return isSubqueryExpr(node, innerrel, tabno, colno);
+	}
+
+	/* Keep compiler happy. */
+	return false;
+}
+
+/*
  * deparse remote INSERT statement
  *
  * The statement text is appended to buf, and we also create an integer List
  * of the columns being retrieved by RETURNING (if any), which is returned
  * to *retrieved_attrs.
  */
 void
 deparseInsertSql(StringInfo buf, PlannerInfo *root,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing,
@@ -2050,24 +2227,40 @@ deparseExpr(Expr *node, deparse_expr_cxt *context)
  *
  * If the Var belongs to the foreign relation, just print its remote name.
  * Otherwise, it's effectively a Param (and will in fact be a Param at
  * run time).  Handle it the same way we handle plain Params --- see
  * deparseParam for comments.
  */
 static void
 deparseVar(Var *node, deparse_expr_cxt *context)
 {
 	Relids		relids = context->scanrel->relids;
+	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
+		context->scanrel : context->foreignrel;
+	int			tabno;
+	int			colno;
 
 	/* Qualify columns when multiple relations are involved. */
 	bool		qualify_col = (bms_num_members(relids) > 1);
 
+	/*
+	 * If the given Var belongs to a relation deparsed as subquery, use column
+	 * alias provided by the subquery instead of the actual column name.
+	 */
+	if (isSubqueryExpr(node, rel, &tabno, &colno))
+	{
+		appendStringInfo(context->buf, "%s%d.%s%d",
+						 SS_TAB_ALIAS_PREFIX, tabno,
+						 SS_COL_ALIAS_PREFIX, colno);
+		return;
+	}
+
 	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
 		deparseColumnRef(context->buf, node->varno, node->varattno,
 						 context->root, qualify_col);
 	else
 	{
 		/* Treat like a Param */
 		if (context->params_list)
 		{
 			int			pindex = 0;
 			ListCell   *lc;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 785f520..75297fb 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -1210,53 +1210,91 @@ SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
   98 |   
  100 |   
      |  3
      |  9
      | 15
      | 21
      | 27
 (10 rows)
 
 -- full outer join with restrictions on the joining relations
+-- a. the joining relations are both base relations
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
-                                           QUERY PLAN                                           
-------------------------------------------------------------------------------------------------
- Sort
+                                                                                                                                  QUERY PLAN                                                                                                                                   
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
    Output: ft4.c1, ft5.c1
-   Sort Key: ft4.c1, ft5.c1
-   ->  Hash Full Join
-         Output: ft4.c1, ft5.c1
-         Hash Cond: (ft4.c1 = ft5.c1)
-         ->  Foreign Scan on public.ft4
-               Output: ft4.c1, ft4.c2, ft4.c3
-               Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
-         ->  Hash
-               Output: ft5.c1
-               ->  Foreign Scan on public.ft5
-                     Output: ft5.c1
-                     Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
-(14 rows)
+   Relations: (public.ft4) FULL JOIN (public.ft5)
+   Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
+(4 rows)
 
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  c1 | c1 
 ----+----
  50 |   
  52 |   
  54 | 54
  56 |   
  58 |   
  60 | 60
     | 51
     | 57
 (8 rows)
 
+-- b. one of the joining relations is a base relation and the other is a join
+-- relation
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                      
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+   Output: ft4.c1, t2.c1, t3.c1
+   Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+   Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+(4 rows)
+
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ c1 | a  | b  
+----+----+----
+ 50 | 50 |   
+ 52 | 52 |   
+ 54 | 54 | 54
+ 56 | 56 |   
+ 58 | 58 |   
+ 60 | 60 | 60
+(6 rows)
+
+-- c. test deparsing the remote query as nested subqueries
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                                                     
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+   Output: ft4.c1, ft4_1.c1, ft5.c1
+   Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+   Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+(4 rows)
+
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ c1 | a  | b  
+----+----+----
+ 50 | 50 |   
+ 52 | 52 |   
+ 54 | 54 | 54
+ 56 | 56 |   
+ 58 | 58 |   
+ 60 | 60 | 60
+    |    | 51
+    |    | 57
+(8 rows)
+
 -- full outer join + inner join
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
                                                                                                                                            QUERY PLAN                                                                                                                                            
 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c1, t3.c1
    ->  Foreign Scan
          Output: t1.c1, t2.c1, t3.c1
          Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
@@ -3055,20 +3093,38 @@ select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
 (7 rows)
 
 select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
          avg         | sum 
 ---------------------+-----
  51.0000000000000000 |    
                      |   3
                      |   9
 (3 rows)
 
+-- Aggregate over FULL join needing to deparse the joining relations as
+-- subqueries.
+explain (verbose, costs off)
+select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                  QUERY PLAN                                                                                                                   
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+   Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+   Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+   Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+(4 rows)
+
+select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ count | sum |         avg         
+-------+-----+---------------------
+     8 | 330 | 55.5000000000000000
+(1 row)
+
 -- ORDER BY expression is part of the target list but not pushed down to
 -- foreign server.
 explain (verbose, costs off)
 select sum(c2) * (random() <= 1)::int as sum from ft1 order by 1;
                                    QUERY PLAN                                   
 --------------------------------------------------------------------------------
  Sort
    Output: (((sum(c2)) * ((random() <= '1'::double precision))::integer))
    Sort Key: (((sum(ft1.c2)) * ((random() <= '1'::double precision))::integer))
    ->  Foreign Scan
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index fbe6929..e80a3ff 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -661,20 +661,28 @@ postgresGetForeignRelSize(PlannerInfo *root,
 	fpinfo->relation_name = makeStringInfo();
 	namespace = get_namespace_name(get_rel_namespace(foreigntableid));
 	relname = get_rel_name(foreigntableid);
 	refname = rte->eref->aliasname;
 	appendStringInfo(fpinfo->relation_name, "%s.%s",
 					 quote_identifier(namespace),
 					 quote_identifier(relname));
 	if (*refname && strcmp(refname, relname) != 0)
 		appendStringInfo(fpinfo->relation_name, " %s",
 						 quote_identifier(rte->eref->aliasname));
+
+	/* No outer and inner relation for a base relation. */
+	fpinfo->make_outerrel_subquery = false;
+	fpinfo->make_innerrel_subquery = false;
+	fpinfo->subquery_rels = NULL;
+
+	/* Set the relation index */
+	fpinfo->relation_index = baserel->relid;
 }
 
 /*
  * get_useful_ecs_for_relation
  *		Determine which EquivalenceClasses might be involved in useful
  *		orderings of this relation.
  *
  * This function is in some respects a mirror image of the core function
  * pathkeys_useful_for_merging: for a regular table, we know what indexes
  * we have and want to test whether any of them are useful.  For a foreign
@@ -4140,36 +4148,47 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 			fpinfo->local_conds = lappend(fpinfo->local_conds, expr);
 		else
 			fpinfo->remote_conds = lappend(fpinfo->remote_conds, expr);
 	}
 
 	fpinfo->outerrel = outerrel;
 	fpinfo->innerrel = innerrel;
 	fpinfo->jointype = jointype;
 
 	/*
+	 * By default both the joining relations are not required to be deparsed as
+	 * subqueries.  But there might be some relations covered by the joining
+	 * relations that are required to be deparsed as subqueries, so save the
+	 * relids of those relations for later use by the deparser.
+	 */
+	fpinfo->make_outerrel_subquery = false;
+	fpinfo->make_innerrel_subquery = false;
+	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+									  fpinfo_o->subquery_rels);
+
+	/*
 	 * Pull the other remote conditions from the joining relations into join
 	 * clauses or other remote clauses (remote_conds) of this relation
-	 * wherever possible. This avoids building subqueries at every join step,
-	 * which is not currently supported by the deparser logic.
+	 * wherever possible. This avoids building subqueries at every join step.
 	 *
 	 * For an inner join, clauses from both the relations are added to the
 	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
 	 * the outer side are added to remote_conds since those can be evaluated
 	 * after the join is evaluated. The clauses from inner side are added to
 	 * the joinclauses, since they need to be evaluated while constructing the
 	 * join.
 	 *
 	 * For a FULL OUTER JOIN, the other clauses from either relation can not
 	 * be added to the joinclauses or remote_conds, since each relation acts
-	 * as an outer relation for the other. Consider such full outer join as
-	 * unshippable because of the reasons mentioned above in this comment.
+	 * as an outer relation for the other.
 	 *
 	 * The joining sides can not have local conditions, thus no need to test
 	 * shippability of the clauses being pulled up.
 	 */
 	switch (jointype)
 	{
 		case JOIN_INNER:
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
 										  list_copy(fpinfo_i->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
@@ -4184,22 +4203,41 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 			break;
 
 		case JOIN_RIGHT:
 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
 										  list_copy(fpinfo_o->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
 										  list_copy(fpinfo_i->remote_conds));
 			break;
 
 		case JOIN_FULL:
-			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
-				return false;
+
+			/*
+			 * In this case, if any of the joining relations has conditions,
+			 * we need to deparse that relation as a subquery so that
+			 * conditions can be evaluated before the join.  Remember it in
+			 * the fpinfo so that deparser can take appropriate action.  We
+			 * also save the relids of the base relations covered by joining
+			 * relation.
+			 */
+			if (fpinfo_o->remote_conds)
+			{
+				fpinfo->make_outerrel_subquery = true;
+				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
+														outerrel->relids);
+			}
+			if (fpinfo_i->remote_conds)
+			{
+				fpinfo->make_innerrel_subquery = true;
+				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
+														innerrel->relids);
+			}
 			break;
 
 		default:
 			/* Should not happen, we have just check this above */
 			elog(ERROR, "unsupported join type %d", jointype);
 	}
 
 	/*
 	 * For an inner join, all restrictions can be treated alike. Treating the
 	 * pushed down conditions as join conditions allows a top level full outer
@@ -4266,20 +4304,30 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 	/*
 	 * Set the string describing this join relation to be used in EXPLAIN
 	 * output of corresponding ForeignScan.
 	 */
 	fpinfo->relation_name = makeStringInfo();
 	appendStringInfo(fpinfo->relation_name, "(%s) %s JOIN (%s)",
 					 fpinfo_o->relation_name->data,
 					 get_jointype_name(fpinfo->jointype),
 					 fpinfo_i->relation_name->data);
 
+	/*
+	 * Set the relation index.  This is defined as the position of this
+	 * joinrel in the join_rel_list list plus the length of the rtable list.
+	 * Note that since this joinrel is at the end of the list when we are
+	 * called, we can get the position by list_length.
+	 */
+	Assert(fpinfo->relation_index == 0);
+	fpinfo->relation_index =
+		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+
 	return true;
 }
 
 static void
 add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
 								Path *epq_path)
 {
 	List	   *useful_pathkeys_list = NIL;		/* List of all pathkeys */
 	ListCell   *lc;
 
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index f8c255e..59633b2 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -88,20 +88,33 @@ typedef struct PgFdwRelationInfo
 	StringInfo	relation_name;
 
 	/* Join information */
 	RelOptInfo *outerrel;
 	RelOptInfo *innerrel;
 	JoinType	jointype;
 	List	   *joinclauses;
 
 	/* Grouping information */
 	List	   *grouped_tlist;
+
+	/* Subquery information */
+	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+										 * subquery? */
+	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+										 * subquery? */
+	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+
+	/*
+	 * Unique integer identifying this relation. It is used for creating a
+	 * subselect alias when deparsing the relation as a subquery.
+	 */
+	int			relation_index;
 } PgFdwRelationInfo;
 
 /* in postgres_fdw.c */
 extern int	set_transmission_modes(void);
 extern void reset_transmission_modes(int nestlevel);
 
 /* in connection.c */
 extern PGconn *GetConnection(UserMapping *user, bool will_prep_stmt);
 extern void ReleaseConnection(PGconn *conn);
 extern unsigned int GetCursorNumber(PGconn *conn);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index f48743c..1dd2cb6 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -384,23 +384,33 @@ SELECT t1.c1, t2.c1 FROM ft5 t1 RIGHT JOIN ft4 t2 ON (t1.c1 = t2.c1) ORDER BY t2
 SELECT t1.c1, t2.c1 FROM ft5 t1 RIGHT JOIN ft4 t2 ON (t1.c1 = t2.c1) ORDER BY t2.c1, t1.c1 OFFSET 10 LIMIT 10;
 -- right outer join three tables
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 -- full outer join
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
 SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
 -- full outer join with restrictions on the joining relations
+-- a. the joining relations are both base relations
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+-- b. one of the joining relations is a base relation and the other is a join
+-- relation
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+-- c. test deparsing the remote query as nested subqueries
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
 -- full outer join + inner join
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
 SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
 -- full outer join three tables
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 -- full outer join + right outer join
 EXPLAIN (VERBOSE, COSTS OFF)
@@ -786,20 +796,26 @@ select count(t1.c3) from ft1 t1, ft1 t2 where t1.c1 = postgres_fdw_abs(t1.c2);
 -- Subquery in FROM clause having aggregate
 explain (verbose, costs off)
 select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2;
 select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2;
 
 -- FULL join with IS NULL check in HAVING
 explain (verbose, costs off)
 select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
 select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
 
+-- Aggregate over FULL join needing to deparse the joining relations as
+-- subqueries.
+explain (verbose, costs off)
+select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+
 -- ORDER BY expression is part of the target list but not pushed down to
 -- foreign server.
 explain (verbose, costs off)
 select sum(c2) * (random() <= 1)::int as sum from ft1 order by 1;
 select sum(c2) * (random() <= 1)::int as sum from ft1 order by 1;
 
 -- LATERAL join, with parameterization
 set enable_hashagg to false;
 explain (verbose, costs off)
 select c2, sum from "S 1"."T 1" t1, lateral (select sum(t2.c1 + t1."C 1") sum from ft2 t2 group by t2.c1) qry where t1.c2 * 2 = qry.sum and t1.c2 < 10 order by 1;
#45Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#44)
Re: Push down more full joins in postgres_fdw

On 2016/11/11 19:22, Ashutosh Bapat wrote:

The patch looks in good shape now.

Thanks for the review!

The patch looks in good shape now. Here are some comments. I have also
made several changes to comments correcting grammar, typos, style and
at few places logic. Let me know if the patch looks good.

OK, will look into that.

I guess, below code
+ if (!fpinfo->subquery_rels)
+ return false;
can be changed to
if (!bms_is_member(node->varno, fpinfo->subquery_rels))
return false;
Also the return values from the recursive calls to isSubqueryExpr() can be
returned as is. I have included this change in the patch.

Will look into that too.

deparse.c seems to be using capitalized names for function which
actually deparse something and an non-capitalized form for helper
functions.

That's not true. There is a function named classifyConditions(). The
function naming in deparse.c is a bit arbitrary.

From that perspective attached patch renames isSubqueryExpr
as is_subquery_var() and getSubselectAliasInfo() as
get_alias_id_for_var(). Actually both these functions accept a Var
node but somehow their names refer to expr.

The reason why I named that function isSubqueryExpr is that I think that
function would be soon extended so as to handle PHVs. See another patch
for evaluating PHVs remotely.

This patch is using make_tlist_from_pathtarget() to create tlist to be
deparsed but search in RelOptInfo::reltarget::exprs for a given Var.
As long as the relations deparsed do not carry expressions, this might
work, but it will certainly break once we start deparsing relations
with expressions since the upper most query's tlist contains only
Vars. Instead, we should probably, create tlist and save it in fpinfo
and use it later for searching (tlist_member()?). Possibly use using
build_tlist_to_deparse(), to create the tlist similar so that
targetlist list creation logic is same for all the relations being
deparsed. I haven't included this change in the patch.

Seems like a good idea. Will revise.

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

#46Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#45)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/11/11 20:50, Etsuro Fujita wrote:

On 2016/11/11 19:22, Ashutosh Bapat wrote:

The patch looks in good shape now. Here are some comments. I have also
made several changes to comments correcting grammar, typos, style and
at few places logic. Let me know if the patch looks good.

OK, will look into that.

Done. +1 for the changes you made, except for few things; (1) You added
the following comments to deparseSelectSql:

+       /*
+        * For a non-base relation, use the input tlist. If a base 
relation is
+        * being deparsed as a subquery, use input tlist, if the caller 
has passed
+        * one. The column aliases of the subquery are crafted based on 
the input
+        * tlist. If tlist is NIL, there will not be any expression 
referring to
+        * any of the columns of the base relation being deparsed. Hence 
it doesn't
+        * matter whether the base relation is being deparsed as 
subquery or not.
+        */

The last sentence seems confusing to me. My understanding is: if a base
relation has tlist=NIL, then the base relation *must* be deparsed as
ordinary, not as a subquery. Maybe I'm missing something, though. (I
left that as-is, but I think we need to reword that to be more clear, at
least.)

(2) You added the following comments to deparseRangeTblRef:

+ * If make_subquery is true, deparse the relation as a subquery.

Otherwise,

+ * deparse it as relation name with alias.

The second sentence seems confusing to me, too, because it looks like
the relation being deparsed is assumed to be a base relation, but the
relation can be a join relation, which might join base relations, lower
join relations, and/or lower subqueries. So, I modified the sentence a bit.

(3) I don't think we need this in isSubqueryExpr, so I removed it from
the patch:

+ /* Keep compiler happy. */
+ return false;

Also, I revised comments you added a little bit.

I guess, below code
+ if (!fpinfo->subquery_rels)
+ return false;
can be changed to
if (!bms_is_member(node->varno, fpinfo->subquery_rels))
return false;
Also the return values from the recursive calls to isSubqueryExpr()
can be
returned as is. I have included this change in the patch.

Will look into that too.

Done. That's a good idea!

deparse.c seems to be using capitalized names for function which
actually deparse something and an non-capitalized form for helper
functions.

From that perspective attached patch renames isSubqueryExpr
as is_subquery_var() and getSubselectAliasInfo() as
get_alias_id_for_var(). Actually both these functions accept a Var
node but somehow their names refer to expr.

OK, I changed isSubqueryExpr to is_subquery_expr; I kept to refer to
expr because I think we would soon extend that function so that it can
handle PHVs, as I said upthread. For getSubselectAliasInfo, I changed
the name to get_subselect_alias_id, because (1) the word "alias" seems
general and (2) the "for_var" part would make the name a bit long.

This patch is using make_tlist_from_pathtarget() to create tlist to be
deparsed but search in RelOptInfo::reltarget::exprs for a given Var.
As long as the relations deparsed do not carry expressions, this might
work, but it will certainly break once we start deparsing relations
with expressions since the upper most query's tlist contains only
Vars. Instead, we should probably, create tlist and save it in fpinfo
and use it later for searching (tlist_member()?). Possibly use using
build_tlist_to_deparse(), to create the tlist similar so that
targetlist list creation logic is same for all the relations being
deparsed. I haven't included this change in the patch.

Seems like a good idea. Will revise.

Done. I modified the patch as proposed; create the tlist by
build_tlist_to_deparse in foreign_join_ok, if needed, and search the
tlist by tlist_member. I also added a new member "tlist" to
PgFdwRelationInfo to save the tlist created in foreign_join_ok.

Another idea on the "tlist" member would be to save a tlist created for
EXPLAIN into that member in estimate_patch_cost_size, so that we don't
need to generate the tlist again in postgresGetForeignPlan, when
use_remote_estimate=true. But I'd like to leave that for another patch.

Please find attached an updated version of the patch.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v6.patchapplication/x-patch; name=postgres-fdw-subquery-support-v6.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,180 ----
  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 get_subselect_alias_id(Var *node, RelOptInfo *foreignrel,
+ 					   int *tabno, int *colno);
+ static bool is_subquery_expr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno);
  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,
***************
*** 990,1001 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
! 	{
! 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
- 	}
  	else
  	{
  		/*
--- 998,1015 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
+ 	/*
+ 	 * For a non-base relation, use the input tlist. If a base relation is
+ 	 * being deparsed as a subquery, use input tlist, if the caller has passed
+ 	 * one. The column aliases of the subquery are crafted based on the input
+ 	 * tlist. If tlist is NIL, there will not be any expression referring to
+ 	 * any of the columns of the base relation being deparsed. Hence it doesn't
+ 	 * matter whether the base relation is being deparsed as subquery or not.
+ 	 */
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL ||
! 		tlist != NIL)
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	else
  	{
  		/*
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1169,1188 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery, because in that
+ 		 * case we would have already considered locking for the relation
+ 		 * while deparsing the lower subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1370,1387 ----
  
  	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
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1437,1580 ----
  }
  
  /*
+  * Append FROM clause entry for the given relation to buf.
+  *
+  * If make_subquery is true, deparse the relation as a subquery.  Otherwise,
+  * deparse it as ordinary.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool make_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	if (make_subquery)
+ 	{
+ 		List	   *retrieved_attrs;
+ 
+ 		/* Append the subquery representing the given relation. */
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectStmtForRel(buf, root, foreignrel, fpinfo->tlist,
+ 								fpinfo->remote_conds, NIL,
+ 								&retrieved_attrs, params_list);
+ 		appendStringInfoChar(buf, ')');
+ 
+ 		/*
+ 		 * Append the relation and column aliases, so that it becomes easy to
+ 		 * refer to this relation in the rest of the query.
+ 		 */
+ 		appendSubselectAlias(buf, fpinfo->relation_index,
+ 							 list_length(fpinfo->tlist));
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Append the relation and column aliases to the subquery.
+  *
+  * 'tabno' is an integer which uniquely identifies the subquery.
+  * 'ncols' is the number of the column aliases to add.
+  */
+ static void
+ appendSubselectAlias(StringInfo buf, int tabno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= ncols; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get the relation and column alias for a given Var node, which belongs to
+  * input foreignrel. They are returned in *tabno and *colno respectively.
+  */
+ static void
+ get_subselect_alias_id(Var *node, RelOptInfo *foreignrel,
+ 					   int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	TargetEntry *tle;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	tle = tlist_member((Node *) node, fpinfo->tlist);
+ 	if (!tle)
+ 		elog(ERROR, "unexpected expression in subquery output");
+ 	*colno = tle->resno;
+ }
+ 
+ /*
+  * Returns true if a given Var node belongs to a relation being deparsed as a
+  * subquery. Returns false otherwise. When returning true, it sets tabno and
+  * colno to unique indexes identifying the relation and column resp. to which
+  * the given Var will be deparsed.
+  */
+ static bool
+ is_subquery_expr(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!bms_is_member(node->varno, fpinfo->subquery_rels))
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		/*
+ 		 * If outer relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column corresponding to the given Var node.
+ 		 */
+ 		if (fpinfo->make_outerrel_subquery)
+ 		{
+ 			get_subselect_alias_id(node, outerrel, tabno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the outer relation. */
+ 		return is_subquery_expr(node, outerrel, tabno, colno);
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * If inner relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column corresponding to the given Var node.
+ 		 */
+ 		if (fpinfo->make_innerrel_subquery)
+ 		{
+ 			get_subselect_alias_id(node, innerrel, tabno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the inner relation. */
+ 		return is_subquery_expr(node, innerrel, tabno, colno);
+ 	}
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2218,2244 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
+ 		context->scanrel : context->foreignrel;
+ 	int			tabno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var belongs to a relation deparsed as a subquery, use the
+ 	 * relation and column alias provided by the subquery, instead of the
+ 	 * actual column name.
+ 	 */
+ 	if (is_subquery_expr(node, rel, &tabno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1293 ----
      | 57
  (8 rows)
  
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3100,3123 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,681 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* No outer and inner relation for a base relation. */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	fpinfo->subquery_rels = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 4147,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4155,4176 ----
  	fpinfo->jointype = jointype;
  
  	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use by the deparser.
+ 	 */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+ 	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+ 									  fpinfo_o->subquery_rels);
+ 
+ 	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4181,4187 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4210,4247 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that the deparser can take appropriate action.
! 			 * We also save the relids of the base relations covered by the
! 			 * joining relation.  We also create the tlist for the subquery.
! 			 *
! 			 * Note: the tlist would have one-to-one correspondence with the
! 			 * joining relation's reltarget->exprs because (1) the above test
! 			 * on PHVs guarantees that the reltarget->exprs doesn't contain
! 			 * any PHVs and (2) the joining relation's local_conds is NIL.
! 			 * This allows us to search the targetlist entry matching a given
! 			 * Var node from the tlist in get_subselect_alias_id.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														outerrel->relids);
! 				fpinfo_o->tlist = build_tlist_to_deparse(outerrel);
! 				Assert(fpinfo_o->tlist != NIL);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo->make_innerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														innerrel->relids);
! 				fpinfo_i->tlist = build_tlist_to_deparse(innerrel);
! 				Assert(fpinfo_i->tlist != NIL);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4322,4337 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 51,56 **** typedef struct PgFdwRelationInfo
--- 51,59 ----
  	/* Bitmap of attr numbers we need to fetch from the remote server. */
  	Bitmapset  *attrs_used;
  
+ 	/* Optional tlist describing the contents of the scan tuple */
+ 	List	   *tlist;
+ 
  	/* Cost and selectivity of local_conds. */
  	QualCost	local_conds_cost;
  	Selectivity local_conds_sel;
***************
*** 95,100 **** typedef struct PgFdwRelationInfo
--- 98,116 ----
  
  	/* Grouping information */
  	List	   *grouped_tlist;
+ 
+ 	/* Subquery information */
+ 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+ 										 * subquery? */
+ 	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+ 										 * subquery? */
+ 	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+ 
+ 	/*
+ 	 * Unique integer identifying this relation. It is used for creating a
+ 	 * subselect alias when deparsing the relation as a subquery.
+ 	 */
+ 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,409 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 803,814 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
#47Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#46)
Re: Push down more full joins in postgres_fdw

Thanks.

except for few things; (1) You added the
following comments to deparseSelectSql:

+       /*
+        * For a non-base relation, use the input tlist. If a base relation
is
+        * being deparsed as a subquery, use input tlist, if the caller has
passed
+        * one. The column aliases of the subquery are crafted based on the
input
+        * tlist. If tlist is NIL, there will not be any expression
referring to
+        * any of the columns of the base relation being deparsed. Hence it
doesn't
+        * matter whether the base relation is being deparsed as subquery or
not.
+        */

The last sentence seems confusing to me. My understanding is: if a base
relation has tlist=NIL, then the base relation *must* be deparsed as
ordinary, not as a subquery. Maybe I'm missing something, though. (I left
that as-is, but I think we need to reword that to be more clear, at least.)

Hmm, I agree. I think the comment should just say, "Use tlist to
create the SELECT clause if one has been provided. For a base relation
with tlist = NIL, check attrs_needed information.". Does that sound
good?

(2) You added the following comments to deparseRangeTblRef:

+  * If make_subquery is true, deparse the relation as a subquery.
Otherwise,
+  * deparse it as relation name with alias.

The second sentence seems confusing to me, too, because it looks like the
relation being deparsed is assumed to be a base relation, but the relation
can be a join relation, which might join base relations, lower join
relations, and/or lower subqueries. So, I modified the sentence a bit.

OK.

(3) I don't think we need this in isSubqueryExpr, so I removed it from the
patch:

+ /* Keep compiler happy. */
+ return false;

Doesn't that cause compiler warning, saying "non-void function
returning nothing" or something like that. Actually, the "if
(bms_is_member(node->varno, outerrel->relids))" ends with a "return"
always. Hence we don't need to encapsulate the code in "else" block in
else { }. It could be taken outside.

OK, I changed isSubqueryExpr to is_subquery_expr; I kept to refer to expr
because I think we would soon extend that function so that it can handle
PHVs, as I said upthread. For getSubselectAliasInfo, I changed the name to
get_subselect_alias_id, because (1) the word "alias" seems general and (2)
the "for_var" part would make the name a bit long.

is_subquery_expr(Var *node -- that looks odd. Either it should
is_subquery_var(Var * ... OR it should be is_subquery_expr(Expr * ...
. I would prefer the first one, since that's what current patch is
doing. When we introduce PHVs, we may change it, if required.

Done. I modified the patch as proposed; create the tlist by
build_tlist_to_deparse in foreign_join_ok, if needed, and search the tlist
by tlist_member. I also added a new member "tlist" to PgFdwRelationInfo to
save the tlist created in foreign_join_ok.

Instead of adding a new member, you might want to reuse grouped_tlist
by renaming it.

Another idea on the "tlist" member would be to save a tlist created for
EXPLAIN into that member in estimate_patch_cost_size, so that we don't need
to generate the tlist again in postgresGetForeignPlan, when
use_remote_estimate=true. But I'd like to leave that for another patch.

I think that will happen automatically, while deparsing, whether for
EXPLAIN or for actual execution.

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

#48Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#47)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/11/16 18:14, Ashutosh Bapat wrote:

(1) You added the
following comments to deparseSelectSql:

+       /*
+        * For a non-base relation, use the input tlist. If a base relation
is
+        * being deparsed as a subquery, use input tlist, if the caller has
passed
+        * one. The column aliases of the subquery are crafted based on the
input
+        * tlist. If tlist is NIL, there will not be any expression
referring to
+        * any of the columns of the base relation being deparsed. Hence it
doesn't
+        * matter whether the base relation is being deparsed as subquery or
not.
+        */

The last sentence seems confusing to me. My understanding is: if a base
relation has tlist=NIL, then the base relation *must* be deparsed as
ordinary, not as a subquery. Maybe I'm missing something, though. (I left
that as-is, but I think we need to reword that to be more clear, at least.)

Hmm, I agree. I think the comment should just say, "Use tlist to
create the SELECT clause if one has been provided. For a base relation
with tlist = NIL, check attrs_needed information.". Does that sound
good?

I don't think that is 100% correct, because (1) tlist can be NIL for a
join relation, you pointed out upthread, but we need to use
deparseExplicitTargetList, so the first sentence is not completely
correct, and (2) we need to check attrs_needed information not only for
a baserel but for an otherrel, so the second sentence is not completely
correct, either. How about this, instead?:

/*
* For a join relation or an upper relation, use
deparseExplicitTargetList.
* Likewise, for a base relation that is being deparsed as a
subquery, in
* which case the caller would have passed tlist that is non-NIL,
use that
* function. Otherwise, use deparseTargetList.
*/

(3) I don't think we need this in isSubqueryExpr, so I removed it from the
patch:

+ /* Keep compiler happy. */
+ return false;

Doesn't that cause compiler warning, saying "non-void function
returning nothing" or something like that. Actually, the "if
(bms_is_member(node->varno, outerrel->relids))" ends with a "return"
always. Hence we don't need to encapsulate the code in "else" block in
else { }. It could be taken outside.

Yeah, I think so too, but I like the "if () { } else { }" coding. That
coding can be found in other places in core, eg,
operator_same_subexprs_lookup.

OK, I changed isSubqueryExpr to is_subquery_expr; I kept to refer to expr
because I think we would soon extend that function so that it can handle
PHVs, as I said upthread. For getSubselectAliasInfo, I changed the name to
get_subselect_alias_id, because (1) the word "alias" seems general and (2)
the "for_var" part would make the name a bit long.

is_subquery_expr(Var *node -- that looks odd. Either it should
is_subquery_var(Var * ... OR it should be is_subquery_expr(Expr * ...
. I would prefer the first one, since that's what current patch is
doing. When we introduce PHVs, we may change it, if required.

OK, I used is_subquery_var().

Done. I modified the patch as proposed; create the tlist by
build_tlist_to_deparse in foreign_join_ok, if needed, and search the tlist
by tlist_member. I also added a new member "tlist" to PgFdwRelationInfo to
save the tlist created in foreign_join_ok.

Instead of adding a new member, you might want to reuse grouped_tlist
by renaming it.

Done.

Another idea on the "tlist" member would be to save a tlist created for
EXPLAIN into that member in estimate_patch_cost_size, so that we don't need
to generate the tlist again in postgresGetForeignPlan, when
use_remote_estimate=true. But I'd like to leave that for another patch.

I think that will happen automatically, while deparsing, whether for
EXPLAIN or for actual execution.

Really? Anyway, I'd like to leave that as-is.

Please find attached a new version of the patch.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v7.patchapplication/x-patch; name=postgres-fdw-subquery-support-v7.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,180 ----
  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 get_subselect_alias_id(Var *node, RelOptInfo *foreignrel,
+ 					   int *tabno, int *colno);
+ static bool is_subquery_var(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno);
  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,
***************
*** 861,867 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
  	 * checking shippability, so just return that.
  	 */
  	if (foreignrel->reloptkind == RELOPT_UPPER_REL)
! 		return fpinfo->grouped_tlist;
  
  	/*
  	 * We require columns specified in foreignrel->reltarget->exprs and those
--- 869,875 ----
  	 * checking shippability, so just return that.
  	 */
  	if (foreignrel->reloptkind == RELOPT_UPPER_REL)
! 		return fpinfo->tlist;
  
  	/*
  	 * We require columns specified in foreignrel->reltarget->exprs and those
***************
*** 990,1001 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
! 	{
! 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
- 	}
  	else
  	{
  		/*
--- 998,1013 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
+ 	/*
+ 	 * For a join relation or an upper relation, use deparseExplicitTargetList.
+ 	 * Likewise, for a base relation that is being deparsed as a subquery, in
+ 	 * which case the caller would have passed tlist that is non-NIL, use that
+ 	 * function.  Otherwise, use deparseTargetList.
+ 	 */
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL ||
! 		tlist != NIL)
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	else
  	{
  		/*
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1167,1186 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery, because in that
+ 		 * case we would have already considered locking for the relation
+ 		 * while deparsing the lower subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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,1385 ----
  
  	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
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1435,1578 ----
  }
  
  /*
+  * Append FROM clause entry for the given relation to buf.
+  *
+  * If make_subquery is true, deparse the relation as a subquery.  Otherwise,
+  * deparse it as ordinary.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool make_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	if (make_subquery)
+ 	{
+ 		List	   *retrieved_attrs;
+ 
+ 		/* Append the subquery representing the given relation. */
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectStmtForRel(buf, root, foreignrel, fpinfo->tlist,
+ 								fpinfo->remote_conds, NIL,
+ 								&retrieved_attrs, params_list);
+ 		appendStringInfoChar(buf, ')');
+ 
+ 		/*
+ 		 * Append the relation and column aliases, so that it becomes easy to
+ 		 * refer to this relation in the rest of the query.
+ 		 */
+ 		appendSubselectAlias(buf, fpinfo->relation_index,
+ 							 list_length(fpinfo->tlist));
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Append the relation and column aliases to the subquery.
+  *
+  * 'tabno' is an integer which uniquely identifies the subquery.
+  * 'ncols' is the number of the column aliases to add.
+  */
+ static void
+ appendSubselectAlias(StringInfo buf, int tabno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= ncols; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get the relation and column alias for a given Var node, which belongs to
+  * input foreignrel. They are returned in *tabno and *colno respectively.
+  */
+ static void
+ get_subselect_alias_id(Var *node, RelOptInfo *foreignrel,
+ 					   int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	TargetEntry *tle;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	tle = tlist_member((Node *) node, fpinfo->tlist);
+ 	if (!tle)
+ 		elog(ERROR, "unexpected expression in subquery output");
+ 	*colno = tle->resno;
+ }
+ 
+ /*
+  * Returns true if a given Var node belongs to a relation being deparsed as a
+  * subquery. Returns false otherwise. When returning true, it sets tabno and
+  * colno to unique indexes identifying the relation and column resp. to which
+  * the given Var will be deparsed.
+  */
+ static bool
+ is_subquery_var(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!bms_is_member(node->varno, fpinfo->subquery_rels))
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		/*
+ 		 * If outer relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column corresponding to the given Var node.
+ 		 */
+ 		if (fpinfo->make_outerrel_subquery)
+ 		{
+ 			get_subselect_alias_id(node, outerrel, tabno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the outer relation. */
+ 		return is_subquery_var(node, outerrel, tabno, colno);
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * If inner relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column corresponding to the given Var node.
+ 		 */
+ 		if (fpinfo->make_innerrel_subquery)
+ 		{
+ 			get_subselect_alias_id(node, innerrel, tabno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the inner relation. */
+ 		return is_subquery_var(node, innerrel, tabno, colno);
+ 	}
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2216,2242 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	RelOptInfo *rel = (context->foreignrel->reloptkind == RELOPT_UPPER_REL) ?
+ 		context->scanrel : context->foreignrel;
+ 	int			tabno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var belongs to a relation deparsed as a subquery, use the
+ 	 * relation and column alias provided by the subquery, instead of the
+ 	 * actual column name.
+ 	 */
+ 	if (is_subquery_var(node, rel, &tabno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1293 ----
      | 57
  (8 rows)
  
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3100,3123 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,681 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* No outer and inner relation for a base relation. */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	fpinfo->subquery_rels = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 2708,2714 **** estimate_path_cost_size(PlannerInfo *root,
  			MemSet(&aggcosts, 0, sizeof(AggClauseCosts));
  			if (root->parse->hasAggs)
  			{
! 				get_agg_clause_costs(root, (Node *) fpinfo->grouped_tlist,
  									 AGGSPLIT_SIMPLE, &aggcosts);
  				get_agg_clause_costs(root, (Node *) root->parse->havingQual,
  									 AGGSPLIT_SIMPLE, &aggcosts);
--- 2716,2722 ----
  			MemSet(&aggcosts, 0, sizeof(AggClauseCosts));
  			if (root->parse->hasAggs)
  			{
! 				get_agg_clause_costs(root, (Node *) fpinfo->tlist,
  									 AGGSPLIT_SIMPLE, &aggcosts);
  				get_agg_clause_costs(root, (Node *) root->parse->havingQual,
  									 AGGSPLIT_SIMPLE, &aggcosts);
***************
*** 2718,2724 **** estimate_path_cost_size(PlannerInfo *root,
  			numGroupCols = list_length(root->parse->groupClause);
  			numGroups = estimate_num_groups(root,
  							get_sortgrouplist_exprs(root->parse->groupClause,
! 													fpinfo->grouped_tlist),
  											input_rows, NULL);
  
  			/*
--- 2726,2732 ----
  			numGroupCols = list_length(root->parse->groupClause);
  			numGroups = estimate_num_groups(root,
  							get_sortgrouplist_exprs(root->parse->groupClause,
! 													fpinfo->tlist),
  											input_rows, NULL);
  
  			/*
***************
*** 4147,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4155,4176 ----
  	fpinfo->jointype = jointype;
  
  	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use by the deparser.
+ 	 */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+ 	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+ 									  fpinfo_o->subquery_rels);
+ 
+ 	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4181,4187 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4210,4247 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that the deparser can take appropriate action.
! 			 * We also save the relids of the base relations covered by the
! 			 * joining relation.  We also create the tlist for the subquery.
! 			 *
! 			 * Note: the tlist would have one-to-one correspondence with the
! 			 * joining relation's reltarget->exprs because (1) the above test
! 			 * on PHVs guarantees that the reltarget->exprs doesn't contain
! 			 * any PHVs and (2) the joining relation's local_conds is NIL.
! 			 * This allows us to search the targetlist entry matching a given
! 			 * Var node from the tlist in get_subselect_alias_id.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														outerrel->relids);
! 				fpinfo_o->tlist = build_tlist_to_deparse(outerrel);
! 				Assert(fpinfo_o->tlist != NIL);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo->make_innerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														innerrel->relids);
! 				fpinfo_i->tlist = build_tlist_to_deparse(innerrel);
! 				Assert(fpinfo_i->tlist != NIL);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4322,4337 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
***************
*** 4613,4619 **** foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
  	apply_pathtarget_labeling_to_tlist(tlist, grouping_target);
  
  	/* Store generated targetlist */
! 	fpinfo->grouped_tlist = tlist;
  
  	/* Safe to pushdown */
  	fpinfo->pushdown_safe = true;
--- 4672,4678 ----
  	apply_pathtarget_labeling_to_tlist(tlist, grouping_target);
  
  	/* Store generated targetlist */
! 	fpinfo->tlist = tlist;
  
  	/* Safe to pushdown */
  	fpinfo->pushdown_safe = true;
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 51,56 **** typedef struct PgFdwRelationInfo
--- 51,59 ----
  	/* Bitmap of attr numbers we need to fetch from the remote server. */
  	Bitmapset  *attrs_used;
  
+ 	/* Optional tlist describing the contents of the scan tuple */
+ 	List	   *tlist;
+ 
  	/* Cost and selectivity of local_conds. */
  	QualCost	local_conds_cost;
  	Selectivity local_conds_sel;
***************
*** 93,100 **** typedef struct PgFdwRelationInfo
  	JoinType	jointype;
  	List	   *joinclauses;
  
! 	/* Grouping information */
! 	List	   *grouped_tlist;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
--- 96,113 ----
  	JoinType	jointype;
  	List	   *joinclauses;
  
! 	/* Subquery information */
! 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
! 										 * subquery? */
! 	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
! 										 * subquery? */
! 	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
! 
! 	/*
! 	 * Unique integer identifying this relation. It is used for creating a
! 	 * subselect alias when deparsing the relation as a subquery.
! 	 */
! 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,409 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 803,814 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
#49Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#48)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

/*
* For a join relation or an upper relation, use
deparseExplicitTargetList.
* Likewise, for a base relation that is being deparsed as a subquery,
in
* which case the caller would have passed tlist that is non-NIL, use
that
* function. Otherwise, use deparseTargetList.
*/

This looks correct. I have modified it to make it simple in the given
patch. But, I think we shouldn't refer to a function e.g.
deparseExplicitTargetlist() in the comment. Instead we should describe
the intent e.g. "deparse SELECT clause from the given targetlist" or
"deparse SELECT clause from attr_needed".

(3) I don't think we need this in isSubqueryExpr, so I removed it from
the
patch:

+ /* Keep compiler happy. */
+ return false;

Doesn't that cause compiler warning, saying "non-void function
returning nothing" or something like that. Actually, the "if
(bms_is_member(node->varno, outerrel->relids))" ends with a "return"
always. Hence we don't need to encapsulate the code in "else" block in
else { }. It could be taken outside.

Yeah, I think so too, but I like the "if () { } else { }" coding. That
coding can be found in other places in core, eg,
operator_same_subexprs_lookup.

OK.

Done. I modified the patch as proposed; create the tlist by
build_tlist_to_deparse in foreign_join_ok, if needed, and search the
tlist
by tlist_member. I also added a new member "tlist" to PgFdwRelationInfo
to
save the tlist created in foreign_join_ok.

Instead of adding a new member, you might want to reuse grouped_tlist
by renaming it.

Done.

Right now, we are calculating tlist whether or not the ForeignPath
emerges as the cheapest path. Instead we should calculate tlist, the
first time we need it and then add it to the fpinfo.

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

Attachments:

postgres-fdw-subquery-support-v8.patchtext/x-diff; charset=US-ASCII; name=postgres-fdw-subquery-support-v8.patchDownload
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index 66b059a..c230009 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -102,20 +102,22 @@ typedef struct deparse_expr_cxt
 								 * foreignrel, when that represents a join or
 								 * a base relation. */
 	StringInfo	buf;			/* output buffer to append to */
 	List	  **params_list;	/* exprs that will become remote Params */
 } deparse_expr_cxt;
 
 #define REL_ALIAS_PREFIX	"r"
 /* Handy macro to add relation name qualification */
 #define ADD_REL_QUALIFIER(buf, varno)	\
 		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+#define SS_TAB_ALIAS_PREFIX	"s"
+#define SS_COL_ALIAS_PREFIX	"c"
 
 /*
  * Functions to determine whether an expression can be evaluated safely on
  * remote server.
  */
 static bool foreign_expr_walker(Node *node,
 					foreign_glob_cxt *glob_cxt,
 					foreign_loc_cxt *outer_cxt);
 static char *deparse_type_name(Oid type_oid, int32 typemod);
 
@@ -160,20 +162,28 @@ static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
 static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
 					   deparse_expr_cxt *context);
 static void deparseSelectSql(List *tlist, List **retrieved_attrs,
 				 deparse_expr_cxt *context);
 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 get_subselect_alias_id(Var *node, RelOptInfo *foreignrel,
+					   int *tabno, int *colno);
+static bool is_subquery_var(Var *node, RelOptInfo *foreignrel, int *tabno,
+						int *colno);
 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,
 				 deparse_expr_cxt *context);
 static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
 static Node *deparseSortGroupClause(Index ref, List *tlist,
 					   deparse_expr_cxt *context);
 
 
 /*
@@ -854,21 +864,21 @@ List *
 build_tlist_to_deparse(RelOptInfo *foreignrel)
 {
 	List	   *tlist = NIL;
 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
 
 	/*
 	 * For an upper relation, we have already built the target list while
 	 * checking shippability, so just return that.
 	 */
 	if (foreignrel->reloptkind == RELOPT_UPPER_REL)
-		return fpinfo->grouped_tlist;
+		return fpinfo->tlist;
 
 	/*
 	 * We require columns specified in foreignrel->reltarget->exprs and those
 	 * required for evaluating the local conditions.
 	 */
 	tlist = add_to_flat_tlist(tlist,
 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
 									   PVC_RECURSE_PLACEHOLDERS));
 	tlist = add_to_flat_tlist(tlist,
 							  pull_var_clause((Node *) fpinfo->local_conds,
@@ -983,26 +993,30 @@ deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
 	StringInfo	buf = context->buf;
 	RelOptInfo *foreignrel = context->foreignrel;
 	PlannerInfo *root = context->root;
 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
 
 	/*
 	 * Construct SELECT list
 	 */
 	appendStringInfoString(buf, "SELECT ");
 
+	/*
+	 * For a join relation or an upper relation, use deparseExplicitTargetList.
+	 * Likewise, for a base relation that is being deparsed as a subquery, in
+	 * which case the caller would have passed non-NIL tlist, use that
+	 * function. Otherwise, use deparseTargetList.
+	 */
 	if (foreignrel->reloptkind == RELOPT_JOINREL ||
-		foreignrel->reloptkind == RELOPT_UPPER_REL)
-	{
-		/* For a join relation use the input tlist */
+		foreignrel->reloptkind == RELOPT_UPPER_REL ||
+		tlist != NIL)
 		deparseExplicitTargetList(tlist, retrieved_attrs, context);
-	}
 	else
 	{
 		/*
 		 * For a base relation fpinfo->attrs_used gives the list of columns
 		 * required to be fetched from the foreign server.
 		 */
 		RangeTblEntry *rte = planner_rt_fetch(foreignrel->relid, root);
 
 		/*
 		 * Core code already has some lock on each rel being planned, so we
@@ -1148,25 +1162,33 @@ deparseTargetList(StringInfo buf,
 /*
  * Deparse the appropriate locking clause (FOR UPDATE or FOR SHARE) for a
  * given relation (context->scanrel).
  */
 static void
 deparseLockingClause(deparse_expr_cxt *context)
 {
 	StringInfo	buf = context->buf;
 	PlannerInfo *root = context->root;
 	RelOptInfo *rel = context->scanrel;
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
 	int			relid = -1;
 
 	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
 	{
 		/*
+		 * Ignore relation if it appears in a lower subquery. Locking clause
+		 * for such a relation is included in the subquery.
+		 */
+		if (bms_is_member(relid, fpinfo->subquery_rels))
+			continue;
+
+		/*
 		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
 		 * initial row fetch, rather than later on as is done for local
 		 * tables. The extra roundtrips involved in trying to duplicate the
 		 * local semantics exactly don't seem worthwhile (see also comments
 		 * for RowMarkType).
 		 *
 		 * Note: because we actually run the query as a cursor, this assumes
 		 * that DECLARE CURSOR ... FOR UPDATE is supported, which it isn't
 		 * before 8.3.
 		 */
@@ -1340,32 +1362,32 @@ deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
  * 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)
 	{
-		RelOptInfo *rel_o = fpinfo->outerrel;
-		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);
+		deparseRangeTblRef(&join_sql_o, root, fpinfo->outerrel,
+						   fpinfo->make_outerrel_subquery, params_list);
 
 		/* Deparse inner relation */
 		initStringInfo(&join_sql_i);
-		deparseFromExprForRel(&join_sql_i, root, rel_i, true, params_list);
+		deparseRangeTblRef(&join_sql_i, root, fpinfo->innerrel,
+						   fpinfo->make_innerrel_subquery, params_list);
 
 		/*
 		 * For a join relation FROM clause entry is deparsed as
 		 *
 		 * ((outer relation) <join type> (inner relation) ON (joinclauses))
 		 */
 		appendStringInfo(buf, "(%s %s JOIN %s ON ", join_sql_o.data,
 					   get_jointype_name(fpinfo->jointype), join_sql_i.data);
 
 		/* Append join clause; (TRUE) if no join clause */
@@ -1407,20 +1429,157 @@ deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
 		 * join.
 		 */
 		if (use_alias)
 			appendStringInfo(buf, " %s%d", REL_ALIAS_PREFIX, foreignrel->relid);
 
 		heap_close(rel, NoLock);
 	}
 }
 
 /*
+ * Append FROM clause entry for the given relation to buf.
+ *
+ * If make_subquery is true, deparse the relation as a subquery.
+ */
+static void
+deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+				   bool make_subquery, List **params_list)
+{
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+
+	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+		   foreignrel->reloptkind == RELOPT_JOINREL);
+	Assert(fpinfo->local_conds == NIL);
+
+	if (make_subquery)
+	{
+		List	   *retrieved_attrs;
+
+		/* Append the subquery representing the given relation. */
+		appendStringInfoChar(buf, '(');
+		deparseSelectStmtForRel(buf, root, foreignrel, fpinfo->tlist,
+								fpinfo->remote_conds, NIL,
+								&retrieved_attrs, params_list);
+		appendStringInfoChar(buf, ')');
+
+		/*
+		 * Append the relation and column aliases, so that it becomes easy to
+		 * refer to this relation in the rest of the query.
+		 */
+		appendSubselectAlias(buf, fpinfo->relation_index,
+							 list_length(fpinfo->tlist));
+	}
+	else
+		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+}
+
+/*
+ * Append the relation and column aliases to the subquery.
+ *
+ * 'tabno' is an integer which uniquely identifies the subquery.
+ * 'ncols' is the number of the column aliases to add.
+ */
+static void
+appendSubselectAlias(StringInfo buf, int tabno, int ncols)
+{
+	int			i;
+
+	/* Append the table alias */
+	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+
+	/* Append the column aliases */
+	appendStringInfoChar(buf, '(');
+	for (i = 1; i <= ncols; i++)
+	{
+		if (i > 1)
+			appendStringInfoString(buf, ", ");
+
+		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+	}
+	appendStringInfoChar(buf, ')');
+}
+
+/*
+ * Get the relation and column alias for a given Var node, which belongs to
+ * input foreignrel. They are returned in *tabno and *colno respectively.
+ */
+static void
+get_subselect_alias_id(Var *node, RelOptInfo *foreignrel,
+					   int *tabno, int *colno)
+{
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+	TargetEntry *tle;
+
+	/* Get the table number */
+	*tabno = fpinfo->relation_index;
+
+	/* Get the column number */
+	tle = tlist_member((Node *) node, fpinfo->tlist);
+	if (!tle)
+		elog(ERROR, "unexpected expression in subquery output");
+	*colno = tle->resno;
+}
+
+/*
+ * Returns true if a given Var node belongs to a relation being deparsed as a
+ * subquery. Returns false otherwise. When returning true, it sets tabno and
+ * colno to unique indexes identifying the relation and column resp. referred
+ * by the given Var node.
+ */
+static bool
+is_subquery_var(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+{
+	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+	RelOptInfo *outerrel = fpinfo->outerrel;
+	RelOptInfo *innerrel = fpinfo->innerrel;
+
+	if (foreignrel->reloptkind != RELOPT_JOINREL)
+		return false;
+
+	if (!bms_is_member(node->varno, fpinfo->subquery_rels))
+		return false;
+
+	if (bms_is_member(node->varno, outerrel->relids))
+	{
+		/*
+		 * If outer relation is deparsed as a subquery, get the identifiers for
+		 * the relation and column corresponding to the given Var node.
+		 */
+		if (fpinfo->make_outerrel_subquery)
+		{
+			get_subselect_alias_id(node, outerrel, tabno, colno);
+			return true;
+		}
+
+		/* Otherwise, recurse into the outer relation. */
+		return is_subquery_var(node, outerrel, tabno, colno);
+	}
+	else
+	{
+		Assert(bms_is_member(node->varno, innerrel->relids));
+
+		/*
+		 * If inner relation is deparsed as a subquery, get the identifiers for
+		 * the relation and column corresponding to the given Var node.
+		 */
+		if (fpinfo->make_innerrel_subquery)
+		{
+			get_subselect_alias_id(node, innerrel, tabno, colno);
+			return true;
+		}
+
+		/* Otherwise, recurse into the inner relation. */
+		return is_subquery_var(node, innerrel, tabno, colno);
+	}
+}
+
+/*
  * deparse remote INSERT statement
  *
  * The statement text is appended to buf, and we also create an integer List
  * of the columns being retrieved by RETURNING (if any), which is returned
  * to *retrieved_attrs.
  */
 void
 deparseInsertSql(StringInfo buf, PlannerInfo *root,
 				 Index rtindex, Relation rel,
 				 List *targetAttrs, bool doNothing,
@@ -2050,24 +2209,39 @@ deparseExpr(Expr *node, deparse_expr_cxt *context)
  *
  * If the Var belongs to the foreign relation, just print its remote name.
  * Otherwise, it's effectively a Param (and will in fact be a Param at
  * run time).  Handle it the same way we handle plain Params --- see
  * deparseParam for comments.
  */
 static void
 deparseVar(Var *node, deparse_expr_cxt *context)
 {
 	Relids		relids = context->scanrel->relids;
+	int			tabno;
+	int			colno;
 
 	/* Qualify columns when multiple relations are involved. */
 	bool		qualify_col = (bms_num_members(relids) > 1);
 
+	/*
+	 * If the given Var belongs to a relation deparsed as a subquery, use the
+	 * relation and column alias provided by the subquery, instead of the
+	 * actual column name.
+	 */
+	if (is_subquery_var(node, context->scanrel, &tabno, &colno))
+	{
+		appendStringInfo(context->buf, "%s%d.%s%d",
+						 SS_TAB_ALIAS_PREFIX, tabno,
+						 SS_COL_ALIAS_PREFIX, colno);
+		return;
+	}
+
 	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
 		deparseColumnRef(context->buf, node->varno, node->varattno,
 						 context->root, qualify_col);
 	else
 	{
 		/* Treat like a Param */
 		if (context->params_list)
 		{
 			int			pindex = 0;
 			ListCell   *lc;
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 785f520..75297fb 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -1210,53 +1210,91 @@ SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
   98 |   
  100 |   
      |  3
      |  9
      | 15
      | 21
      | 27
 (10 rows)
 
 -- full outer join with restrictions on the joining relations
+-- a. the joining relations are both base relations
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
-                                           QUERY PLAN                                           
-------------------------------------------------------------------------------------------------
- Sort
+                                                                                                                                  QUERY PLAN                                                                                                                                   
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
    Output: ft4.c1, ft5.c1
-   Sort Key: ft4.c1, ft5.c1
-   ->  Hash Full Join
-         Output: ft4.c1, ft5.c1
-         Hash Cond: (ft4.c1 = ft5.c1)
-         ->  Foreign Scan on public.ft4
-               Output: ft4.c1, ft4.c2, ft4.c3
-               Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
-         ->  Hash
-               Output: ft5.c1
-               ->  Foreign Scan on public.ft5
-                     Output: ft5.c1
-                     Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
-(14 rows)
+   Relations: (public.ft4) FULL JOIN (public.ft5)
+   Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
+(4 rows)
 
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  c1 | c1 
 ----+----
  50 |   
  52 |   
  54 | 54
  56 |   
  58 |   
  60 | 60
     | 51
     | 57
 (8 rows)
 
+-- b. one of the joining relations is a base relation and the other is a join
+-- relation
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                      
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+   Output: ft4.c1, t2.c1, t3.c1
+   Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+   Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+(4 rows)
+
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ c1 | a  | b  
+----+----+----
+ 50 | 50 |   
+ 52 | 52 |   
+ 54 | 54 | 54
+ 56 | 56 |   
+ 58 | 58 |   
+ 60 | 60 | 60
+(6 rows)
+
+-- c. test deparsing the remote query as nested subqueries
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                     QUERY PLAN                                                                                                                                                                                                                                                     
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+   Output: ft4.c1, ft4_1.c1, ft5.c1
+   Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+   Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+(4 rows)
+
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ c1 | a  | b  
+----+----+----
+ 50 | 50 |   
+ 52 | 52 |   
+ 54 | 54 | 54
+ 56 | 56 |   
+ 58 | 58 |   
+ 60 | 60 | 60
+    |    | 51
+    |    | 57
+(8 rows)
+
 -- full outer join + inner join
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
                                                                                                                                            QUERY PLAN                                                                                                                                            
 -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Limit
    Output: t1.c1, t2.c1, t3.c1
    ->  Foreign Scan
          Output: t1.c1, t2.c1, t3.c1
          Relations: ((public.ft4 t1) INNER JOIN (public.ft5 t2)) FULL JOIN (public.ft4 t3)
@@ -3055,20 +3093,38 @@ select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
 (7 rows)
 
 select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
          avg         | sum 
 ---------------------+-----
  51.0000000000000000 |    
                      |   3
                      |   9
 (3 rows)
 
+-- Aggregate over FULL join needing to deparse the joining relations as
+-- subqueries.
+explain (verbose, costs off)
+select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                  QUERY PLAN                                                                                                                   
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Foreign Scan
+   Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+   Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+   Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+(4 rows)
+
+select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ count | sum |         avg         
+-------+-----+---------------------
+     8 | 330 | 55.5000000000000000
+(1 row)
+
 -- ORDER BY expression is part of the target list but not pushed down to
 -- foreign server.
 explain (verbose, costs off)
 select sum(c2) * (random() <= 1)::int as sum from ft1 order by 1;
                                    QUERY PLAN                                   
 --------------------------------------------------------------------------------
  Sort
    Output: (((sum(c2)) * ((random() <= '1'::double precision))::integer))
    Sort Key: (((sum(ft1.c2)) * ((random() <= '1'::double precision))::integer))
    ->  Foreign Scan
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index fbe6929..3ad60b7 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -661,20 +661,28 @@ postgresGetForeignRelSize(PlannerInfo *root,
 	fpinfo->relation_name = makeStringInfo();
 	namespace = get_namespace_name(get_rel_namespace(foreigntableid));
 	relname = get_rel_name(foreigntableid);
 	refname = rte->eref->aliasname;
 	appendStringInfo(fpinfo->relation_name, "%s.%s",
 					 quote_identifier(namespace),
 					 quote_identifier(relname));
 	if (*refname && strcmp(refname, relname) != 0)
 		appendStringInfo(fpinfo->relation_name, " %s",
 						 quote_identifier(rte->eref->aliasname));
+
+	/* No outer and inner relation for a base relation. */
+	fpinfo->make_outerrel_subquery = false;
+	fpinfo->make_innerrel_subquery = false;
+	fpinfo->subquery_rels = NULL;
+
+	/* Set the relation index */
+	fpinfo->relation_index = baserel->relid;
 }
 
 /*
  * get_useful_ecs_for_relation
  *		Determine which EquivalenceClasses might be involved in useful
  *		orderings of this relation.
  *
  * This function is in some respects a mirror image of the core function
  * pathkeys_useful_for_merging: for a regular table, we know what indexes
  * we have and want to test whether any of them are useful.  For a foreign
@@ -2701,31 +2709,31 @@ estimate_path_cost_size(PlannerInfo *root,
 			ofpinfo = (PgFdwRelationInfo *) fpinfo->outerrel->fdw_private;
 
 			/* Get rows and width from input rel */
 			input_rows = ofpinfo->rows;
 			width = ofpinfo->width;
 
 			/* Collect statistics about aggregates for estimating costs. */
 			MemSet(&aggcosts, 0, sizeof(AggClauseCosts));
 			if (root->parse->hasAggs)
 			{
-				get_agg_clause_costs(root, (Node *) fpinfo->grouped_tlist,
+				get_agg_clause_costs(root, (Node *) fpinfo->tlist,
 									 AGGSPLIT_SIMPLE, &aggcosts);
 				get_agg_clause_costs(root, (Node *) root->parse->havingQual,
 									 AGGSPLIT_SIMPLE, &aggcosts);
 			}
 
 			/* Get number of grouping columns and possible number of groups */
 			numGroupCols = list_length(root->parse->groupClause);
 			numGroups = estimate_num_groups(root,
 							get_sortgrouplist_exprs(root->parse->groupClause,
-													fpinfo->grouped_tlist),
+													fpinfo->tlist),
 											input_rows, NULL);
 
 			/*
 			 * Number of rows expected from foreign server will be same as
 			 * that of number of groups.
 			 */
 			rows = retrieved_rows = numGroups;
 
 			/*-----
 			 * Startup cost includes:
@@ -4140,36 +4148,47 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 			fpinfo->local_conds = lappend(fpinfo->local_conds, expr);
 		else
 			fpinfo->remote_conds = lappend(fpinfo->remote_conds, expr);
 	}
 
 	fpinfo->outerrel = outerrel;
 	fpinfo->innerrel = innerrel;
 	fpinfo->jointype = jointype;
 
 	/*
+	 * By default both the joining relations are not required to be deparsed as
+	 * subqueries.  But there might be some relations covered by the joining
+	 * relations that are required to be deparsed as subqueries, so save the
+	 * relids of those relations for later use by the deparser.
+	 */
+	fpinfo->make_outerrel_subquery = false;
+	fpinfo->make_innerrel_subquery = false;
+	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+									  fpinfo_o->subquery_rels);
+
+	/*
 	 * Pull the other remote conditions from the joining relations into join
 	 * clauses or other remote clauses (remote_conds) of this relation
-	 * wherever possible. This avoids building subqueries at every join step,
-	 * which is not currently supported by the deparser logic.
+	 * wherever possible. This avoids building subqueries at every join step.
 	 *
 	 * For an inner join, clauses from both the relations are added to the
 	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
 	 * the outer side are added to remote_conds since those can be evaluated
 	 * after the join is evaluated. The clauses from inner side are added to
 	 * the joinclauses, since they need to be evaluated while constructing the
 	 * join.
 	 *
 	 * For a FULL OUTER JOIN, the other clauses from either relation can not
 	 * be added to the joinclauses or remote_conds, since each relation acts
-	 * as an outer relation for the other. Consider such full outer join as
-	 * unshippable because of the reasons mentioned above in this comment.
+	 * as an outer relation for the other.
 	 *
 	 * The joining sides can not have local conditions, thus no need to test
 	 * shippability of the clauses being pulled up.
 	 */
 	switch (jointype)
 	{
 		case JOIN_INNER:
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
 										  list_copy(fpinfo_i->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
@@ -4184,22 +4203,52 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 			break;
 
 		case JOIN_RIGHT:
 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
 										  list_copy(fpinfo_o->remote_conds));
 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
 										  list_copy(fpinfo_i->remote_conds));
 			break;
 
 		case JOIN_FULL:
-			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
-				return false;
+
+			/*
+			 * In this case, if any of the joining relations has conditions,
+			 * we need to deparse that relation as a subquery so that
+			 * conditions can be evaluated before the join.  Remember it in
+			 * the fpinfo so that the deparser can take appropriate action.
+			 * We also save the relids of the base relations covered by the
+			 * joining relation.  We also create the tlist for the subquery.
+			 *
+			 * Note: the tlist would have one-to-one correspondence with the
+			 * joining relation's reltarget->exprs because (1) the above test
+			 * on PHVs guarantees that the reltarget->exprs doesn't contain
+			 * any PHVs and (2) the joining relation's local_conds is NIL.
+			 * This allows us to search the targetlist entry matching a given
+			 * Var node from the tlist in get_subselect_alias_id.
+			 */
+			if (fpinfo_o->remote_conds)
+			{
+				fpinfo->make_outerrel_subquery = true;
+				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
+														outerrel->relids);
+				fpinfo_o->tlist = build_tlist_to_deparse(outerrel);
+				Assert(fpinfo_o->tlist != NIL);
+			}
+			if (fpinfo_i->remote_conds)
+			{
+				fpinfo->make_innerrel_subquery = true;
+				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
+														innerrel->relids);
+				fpinfo_i->tlist = build_tlist_to_deparse(innerrel);
+				Assert(fpinfo_i->tlist != NIL);
+			}
 			break;
 
 		default:
 			/* Should not happen, we have just check this above */
 			elog(ERROR, "unsupported join type %d", jointype);
 	}
 
 	/*
 	 * For an inner join, all restrictions can be treated alike. Treating the
 	 * pushed down conditions as join conditions allows a top level full outer
@@ -4266,20 +4315,30 @@ foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
 	/*
 	 * Set the string describing this join relation to be used in EXPLAIN
 	 * output of corresponding ForeignScan.
 	 */
 	fpinfo->relation_name = makeStringInfo();
 	appendStringInfo(fpinfo->relation_name, "(%s) %s JOIN (%s)",
 					 fpinfo_o->relation_name->data,
 					 get_jointype_name(fpinfo->jointype),
 					 fpinfo_i->relation_name->data);
 
+	/*
+	 * Set the relation index.  This is defined as the position of this
+	 * joinrel in the join_rel_list list plus the length of the rtable list.
+	 * Note that since this joinrel is at the end of the list when we are
+	 * called, we can get the position by list_length.
+	 */
+	Assert(fpinfo->relation_index == 0);
+	fpinfo->relation_index =
+		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+
 	return true;
 }
 
 static void
 add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
 								Path *epq_path)
 {
 	List	   *useful_pathkeys_list = NIL;		/* List of all pathkeys */
 	ListCell   *lc;
 
@@ -4606,21 +4665,21 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
 
 				tlist = add_to_flat_tlist(tlist, aggvars);
 			}
 		}
 	}
 
 	/* Transfer any sortgroupref data to the replacement tlist */
 	apply_pathtarget_labeling_to_tlist(tlist, grouping_target);
 
 	/* Store generated targetlist */
-	fpinfo->grouped_tlist = tlist;
+	fpinfo->tlist = tlist;
 
 	/* Safe to pushdown */
 	fpinfo->pushdown_safe = true;
 
 	/*
 	 * If user is willing to estimate cost for a scan using EXPLAIN, he
 	 * intends to estimate scans on that relation more accurately. Then, it
 	 * makes sense to estimate the cost of the grouping on that relation more
 	 * accurately using EXPLAIN.
 	 */
diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h
index f8c255e..8e115ab 100644
--- a/contrib/postgres_fdw/postgres_fdw.h
+++ b/contrib/postgres_fdw/postgres_fdw.h
@@ -44,20 +44,23 @@ typedef struct PgFdwRelationInfo
 	 * For a join relation, however, they are part of otherclause list
 	 * obtained from extract_actual_join_clauses, which strips RestrictInfo
 	 * construct. So, for a join relation they are list of bare clauses.
 	 */
 	List	   *remote_conds;
 	List	   *local_conds;
 
 	/* Bitmap of attr numbers we need to fetch from the remote server. */
 	Bitmapset  *attrs_used;
 
+	/* Optional tlist describing the output from this relation. */
+	List	   *tlist;
+
 	/* Cost and selectivity of local_conds. */
 	QualCost	local_conds_cost;
 	Selectivity local_conds_sel;
 
 	/* Selectivity of join conditions */
 	Selectivity joinclause_sel;
 
 	/* Estimated size and cost for a scan or join. */
 	double		rows;
 	int			width;
@@ -86,22 +89,32 @@ typedef struct PgFdwRelationInfo
 	 * indicates which foreign tables are being joined and the join type used.
 	 */
 	StringInfo	relation_name;
 
 	/* Join information */
 	RelOptInfo *outerrel;
 	RelOptInfo *innerrel;
 	JoinType	jointype;
 	List	   *joinclauses;
 
-	/* Grouping information */
-	List	   *grouped_tlist;
+	/* Subquery information */
+	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+										 * subquery? */
+	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+										 * subquery? */
+	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+
+	/*
+	 * Unique integer identifying this relation. It is used for creating a
+	 * subselect alias when deparsing the relation as a subquery.
+	 */
+	int			relation_index;
 } PgFdwRelationInfo;
 
 /* in postgres_fdw.c */
 extern int	set_transmission_modes(void);
 extern void reset_transmission_modes(int nestlevel);
 
 /* in connection.c */
 extern PGconn *GetConnection(UserMapping *user, bool will_prep_stmt);
 extern void ReleaseConnection(PGconn *conn);
 extern unsigned int GetCursorNumber(PGconn *conn);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index f48743c..1dd2cb6 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -384,23 +384,33 @@ SELECT t1.c1, t2.c1 FROM ft5 t1 RIGHT JOIN ft4 t2 ON (t1.c1 = t2.c1) ORDER BY t2
 SELECT t1.c1, t2.c1 FROM ft5 t1 RIGHT JOIN ft4 t2 ON (t1.c1 = t2.c1) ORDER BY t2.c1, t1.c1 OFFSET 10 LIMIT 10;
 -- right outer join three tables
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 RIGHT JOIN ft2 t2 ON (t1.c1 = t2.c1) RIGHT JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 -- full outer join
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
 SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
 -- full outer join with restrictions on the joining relations
+-- a. the joining relations are both base relations
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
 SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+-- b. one of the joining relations is a base relation and the other is a join
+-- relation
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+-- c. test deparsing the remote query as nested subqueries
+EXPLAIN (VERBOSE, COSTS OFF)
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
 -- full outer join + inner join
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
 SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
 -- full outer join three tables
 EXPLAIN (VERBOSE, COSTS OFF)
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 SELECT t1.c1, t2.c2, t3.c3 FROM ft2 t1 FULL JOIN ft2 t2 ON (t1.c1 = t2.c1) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) OFFSET 10 LIMIT 10;
 -- full outer join + right outer join
 EXPLAIN (VERBOSE, COSTS OFF)
@@ -786,20 +796,26 @@ select count(t1.c3) from ft1 t1, ft1 t2 where t1.c1 = postgres_fdw_abs(t1.c2);
 -- Subquery in FROM clause having aggregate
 explain (verbose, costs off)
 select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2;
 select count(*), x.b from ft1, (select c2 a, sum(c1) b from ft1 group by c2) x where ft1.c2 = x.a group by x.b order by 1, 2;
 
 -- FULL join with IS NULL check in HAVING
 explain (verbose, costs off)
 select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
 select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
 
+-- Aggregate over FULL join needing to deparse the joining relations as
+-- subqueries.
+explain (verbose, costs off)
+select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+
 -- ORDER BY expression is part of the target list but not pushed down to
 -- foreign server.
 explain (verbose, costs off)
 select sum(c2) * (random() <= 1)::int as sum from ft1 order by 1;
 select sum(c2) * (random() <= 1)::int as sum from ft1 order by 1;
 
 -- LATERAL join, with parameterization
 set enable_hashagg to false;
 explain (verbose, costs off)
 select c2, sum from "S 1"."T 1" t1, lateral (select sum(t2.c1 + t1."C 1") sum from ft2 t2 group by t2.c1) qry where t1.c2 * 2 = qry.sum and t1.c2 < 10 order by 1;
#50Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Ashutosh Bapat (#49)
Re: Push down more full joins in postgres_fdw

Also another point

I guess, this note doesn't add much value in the given context.
Probably we should remove it.
+            * Note: the tlist would have one-to-one correspondence with the
+            * joining relation's reltarget->exprs because (1) the above test
+            * on PHVs guarantees that the reltarget->exprs doesn't contain
+            * any PHVs and (2) the joining relation's local_conds is NIL.
+            * This allows us to search the targetlist entry matching a given
+            * Var node from the tlist in get_subselect_alias_id.

On Fri, Nov 18, 2016 at 5:10 PM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

/*
* For a join relation or an upper relation, use
deparseExplicitTargetList.
* Likewise, for a base relation that is being deparsed as a subquery,
in
* which case the caller would have passed tlist that is non-NIL, use
that
* function. Otherwise, use deparseTargetList.
*/

This looks correct. I have modified it to make it simple in the given
patch. But, I think we shouldn't refer to a function e.g.
deparseExplicitTargetlist() in the comment. Instead we should describe
the intent e.g. "deparse SELECT clause from the given targetlist" or
"deparse SELECT clause from attr_needed".

(3) I don't think we need this in isSubqueryExpr, so I removed it from
the
patch:

+ /* Keep compiler happy. */
+ return false;

Doesn't that cause compiler warning, saying "non-void function
returning nothing" or something like that. Actually, the "if
(bms_is_member(node->varno, outerrel->relids))" ends with a "return"
always. Hence we don't need to encapsulate the code in "else" block in
else { }. It could be taken outside.

Yeah, I think so too, but I like the "if () { } else { }" coding. That
coding can be found in other places in core, eg,
operator_same_subexprs_lookup.

OK.

Done. I modified the patch as proposed; create the tlist by
build_tlist_to_deparse in foreign_join_ok, if needed, and search the
tlist
by tlist_member. I also added a new member "tlist" to PgFdwRelationInfo
to
save the tlist created in foreign_join_ok.

Instead of adding a new member, you might want to reuse grouped_tlist
by renaming it.

Done.

Right now, we are calculating tlist whether or not the ForeignPath
emerges as the cheapest path. Instead we should calculate tlist, the
first time we need it and then add it to the fpinfo.

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

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

#51Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#50)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/11/19 0:16, Ashutosh Bapat wrote:

Also another point

I guess, this note doesn't add much value in the given context.
Probably we should remove it.
+            * Note: the tlist would have one-to-one correspondence with the
+            * joining relation's reltarget->exprs because (1) the above test
+            * on PHVs guarantees that the reltarget->exprs doesn't contain
+            * any PHVs and (2) the joining relation's local_conds is NIL.
+            * This allows us to search the targetlist entry matching a given
+            * Var node from the tlist in get_subselect_alias_id.

OK, I removed.

On Fri, Nov 18, 2016 at 5:10 PM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

I wrote:

/*
* For a join relation or an upper relation, use
deparseExplicitTargetList.
* Likewise, for a base relation that is being deparsed as a subquery,
in
* which case the caller would have passed tlist that is non-NIL, use
that
* function. Otherwise, use deparseTargetList.
*/

This looks correct. I have modified it to make it simple in the given
patch. But, I think we shouldn't refer to a function e.g.
deparseExplicitTargetlist() in the comment. Instead we should describe
the intent e.g. "deparse SELECT clause from the given targetlist" or
"deparse SELECT clause from attr_needed".

My taste would be to refer to those functions, because ISTM that makes
the explanation more simple and direct. So, I'd like to leave that for
the committer's judge.

I wrote:

Done. I modified the patch as proposed; create the tlist by
build_tlist_to_deparse in foreign_join_ok, if needed, and search the
tlist
by tlist_member. I also added a new member "tlist" to PgFdwRelationInfo
to
save the tlist created in foreign_join_ok.

You wrote:

Instead of adding a new member, you might want to reuse grouped_tlist
by renaming it.

Done.

Right now, we are calculating tlist whether or not the ForeignPath
emerges as the cheapest path.

Yeah, I modified the patch so, as I thought that would be consistent
with the aggregate pushdown patch.

Instead we should calculate tlist, the
first time we need it and then add it to the fpinfo.

Having said that, I agree on that point. I'd like to propose (1) adding
a new member to fpinfo, to store a list of output Vars from the
subquery, and (2) creating a tlist from it in deparseRangeTblRef, then,
which would allow us to calculate the tlist only when we need it. The
member added to fpinfo would be also useful to address the comments on
the DML/UPDATE pushdown patch. See the attached patch in [1]/messages/by-id/38245b84-fabf-0899-1b24-8f94cdc5900c@lab.ntt.co.jp. I named
the member a bit differently in that patch, though.

You modified the comments I added to deparseLockingClause into this:

         /*
+        * Ignore relation if it appears in a lower subquery. Locking clause
+        * for such a relation is included in the subquery.
+        */

I don't think the second sentence is 100% correct because a locking
clause isn't always required for such a relation, so I modified the
sentence a bit.

Best regards,
Etsuro Fujita

[1]: /messages/by-id/38245b84-fabf-0899-1b24-8f94cdc5900c@lab.ntt.co.jp
/messages/by-id/38245b84-fabf-0899-1b24-8f94cdc5900c@lab.ntt.co.jp

Attachments:

postgres-fdw-subquery-support-v9.patchapplication/x-patch; name=postgres-fdw-subquery-support-v9.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,182 ----
  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 get_subselect_alias_id(Var *node, RelOptInfo *foreignrel,
+ 					   int *tabno, int *colno);
+ static bool is_subquery_var(Var *node, RelOptInfo *foreignrel, int *tabno,
+ 						int *colno);
  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,
***************
*** 990,1001 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
! 	{
! 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
- 	}
  	else
  	{
  		/*
--- 1000,1015 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
+ 	/*
+ 	 * For a join relation or an upper relation, use deparseExplicitTargetList.
+ 	 * Likewise, for a base relation that is being deparsed as a subquery, in
+ 	 * which case the caller would have passed non-NIL tlist, use that
+ 	 * function. Otherwise, use deparseTargetList.
+ 	 */
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL ||
! 		tlist != NIL)
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	else
  	{
  		/*
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1169,1187 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery. Locking clause
+ 		 * for such a relation, if needed, is included in the subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1369,1386 ----
  
  	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
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1436,1593 ----
  }
  
  /*
+  * Append FROM clause entry for the given relation to buf.
+  *
+  * If make_subquery is true, deparse the relation as a subquery.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool make_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	if (make_subquery)
+ 	{
+ 		List	   *tlist = NIL;
+ 		List	   *retrieved_attrs;
+ 
+ 		/* Build a tlist from the subquery output Vars. */
+ 		tlist = add_to_flat_tlist(tlist, fpinfo->subquery_vars);
+ 		/* Shouldn't be NIL */
+ 		Assert(tlist != NIL);
+ 
+ 		/* Append the subquery representing the given relation. */
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectStmtForRel(buf, root, foreignrel, tlist,
+ 								fpinfo->remote_conds, NIL,
+ 								&retrieved_attrs, params_list);
+ 		appendStringInfoChar(buf, ')');
+ 
+ 		/*
+ 		 * Append the subselect alias, so that it becomes easy to refer to
+ 		 * this relation in the rest of the query.
+ 		 */
+ 		appendSubselectAlias(buf, fpinfo->relation_index, list_length(tlist));
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Append the relation and column aliases to the subquery.
+  *
+  * 'tabno' is an integer which uniquely identifies the subquery.
+  * 'ncols' is the number of the column aliases to add.
+  */
+ static void
+ appendSubselectAlias(StringInfo buf, int tabno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= ncols; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get the relation and column alias for a given Var node, which belongs to
+  * input foreignrel. They are returned in *tabno and *colno respectively.
+  */
+ static void
+ get_subselect_alias_id(Var *node, RelOptInfo *foreignrel,
+ 					   int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	i = 1;
+ 	foreach(lc, fpinfo->subquery_vars)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/* Shouldn't get here */
+ 	elog(ERROR, "unexpected expression in subquery output");
+ }
+ 
+ /*
+  * Returns true if a given Var node belongs to a relation being deparsed as a
+  * subquery. Returns false otherwise. When returning true, it sets tabno and
+  * colno to unique indexes identifying the relation and column resp. referred
+  * by the given Var node.
+  */
+ static bool
+ is_subquery_var(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!bms_is_member(node->varno, fpinfo->subquery_rels))
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		/*
+ 		 * If outer relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column corresponding to the given Var node.
+ 		 */
+ 		if (fpinfo->make_outerrel_subquery)
+ 		{
+ 			get_subselect_alias_id(node, outerrel, tabno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the outer relation. */
+ 		return is_subquery_var(node, outerrel, tabno, colno);
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * If inner relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column corresponding to the given Var node.
+ 		 */
+ 		if (fpinfo->make_innerrel_subquery)
+ 		{
+ 			get_subselect_alias_id(node, innerrel, tabno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the inner relation. */
+ 		return is_subquery_var(node, innerrel, tabno, colno);
+ 	}
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2231,2255 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	int			tabno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var belongs to a relation deparsed as a subquery, use the
+ 	 * relation and column alias provided by the subquery, instead of the
+ 	 * actual column name.
+ 	 */
+ 	if (is_subquery_var(node, context->scanrel, &tabno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1293 ----
      | 57
  (8 rows)
  
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3100,3123 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,681 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* No outer and inner relation for a base relation. */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	fpinfo->subquery_rels = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 4147,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4155,4176 ----
  	fpinfo->jointype = jointype;
  
  	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use by the deparser.
+ 	 */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+ 	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+ 									  fpinfo_o->subquery_rels);
+ 
+ 	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4181,4187 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4210,4238 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that the
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that the deparser can take appropriate action.
! 			 * We also save the relids of the base relations covered by the
! 			 * joining relation.  We also set output Vars from the subquery.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														outerrel->relids);
! 				fpinfo_o->subquery_vars = outerrel->reltarget->exprs;
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo->make_innerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														innerrel->relids);
! 				fpinfo_i->subquery_vars = innerrel->reltarget->exprs;
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4313,4328 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 95,100 **** typedef struct PgFdwRelationInfo
--- 95,119 ----
  
  	/* Grouping information */
  	List	   *grouped_tlist;
+ 
+ 	/* Subquery information */
+ 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+ 										 * subquery? */
+ 	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+ 										 * subquery? */
+ 	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+ 
+ 	/*
+ 	 * Unique integer identifying this relation. It is used for creating a
+ 	 * subselect alias when deparsing the relation as a subquery.
+ 	 */
+ 	int			relation_index;
+ 
+ 	/*
+ 	 * Output Vars from the subquery. It is used for determining the identifier
+ 	 * for the column corresponding to a given Var of the subquery.
+ 	 */
+ 	List	   *subquery_vars;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,409 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 803,814 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
#52Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#51)
Re: Push down more full joins in postgres_fdw

Yeah, I modified the patch so, as I thought that would be consistent with
the aggregate pushdown patch.

aggregate pushdown needs the tlist to judge the shippability of
targetlist. For joins that's not required, so we should defer, if we
can.

Instead we should calculate tlist, the
first time we need it and then add it to the fpinfo.

Having said that, I agree on that point. I'd like to propose (1) adding a
new member to fpinfo, to store a list of output Vars from the subquery, and
(2) creating a tlist from it in deparseRangeTblRef, then, which would allow
us to calculate the tlist only when we need it. The member added to fpinfo
would be also useful to address the comments on the DML/UPDATE pushdown
patch. See the attached patch in [1]. I named the member a bit differently
in that patch, though.

Again the list of Vars will be wasted if we don't choose that path for
final planning. So, I don't see the point of adding list of Vars
there. It looks like we are replacing one list with the other when
none of those are useful, if the path doesn't get chosen for the final
plan. If you think that the member is useful for DML/UDPATE pushdown,
you may want to add it in the other patch.

You modified the comments I added to deparseLockingClause into this:

/*
+        * Ignore relation if it appears in a lower subquery. Locking clause
+        * for such a relation is included in the subquery.
+        */

I don't think the second sentence is 100% correct because a locking clause
isn't always required for such a relation, so I modified the sentence a bit.

I guess, "if required" part was implicit in construct "such a
relation". Your version seems to make it explicit. I am fine with it.

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

#53Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#52)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/11/21 22:02, Ashutosh Bapat wrote:

You wrote:

Instead we should calculate tlist, the
first time we need it and then add it to the fpinfo.

I wrote:

Having said that, I agree on that point. I'd like to propose (1) adding a
new member to fpinfo, to store a list of output Vars from the subquery, and
(2) creating a tlist from it in deparseRangeTblRef, then, which would allow
us to calculate the tlist only when we need it. The member added to fpinfo
would be also useful to address the comments on the DML/UPDATE pushdown
patch. See the attached patch in [1]. I named the member a bit differently
in that patch, though.

Again the list of Vars will be wasted if we don't choose that path for
final planning. So, I don't see the point of adding list of Vars
there.

If you think that the member is useful for DML/UDPATE pushdown,
you may want to add it in the other patch.

OK, I'd like to propose referencing to foreignrel->reltarget->exprs
directly in deparseRangeTblRef and get_subselect_alias_id, then, which
is the same as what I proposed in the first version of the patch, but
I'd also like to change deparseRangeTblRef to use add_to_flat_tlist, not
make_tlist_from_pathtarget, to create a tlist of the subquery, as you
proposed.

You modified the comments I added to deparseLockingClause into this:

/*
+        * Ignore relation if it appears in a lower subquery. Locking clause
+        * for such a relation is included in the subquery.
+        */

I don't think the second sentence is 100% correct because a locking clause
isn't always required for such a relation, so I modified the sentence a bit.

I guess, "if required" part was implicit in construct "such a
relation". Your version seems to make it explicit. I am fine with it.

OK, let's leave that for the committer's judge.

Please find attached an updated version of the patch.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v10.patchapplication/x-patch; name=postgres-fdw-subquery-support-v10.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_TAB_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,182 ----
  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 get_subselect_alias_id(Var *node, RelOptInfo *foreignrel,
+ 					   int *tabno, int *colno);
+ static bool is_subquery_var(Var *node, RelOptInfo *foreignrel, int *tabno,
+ 						int *colno);
  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,
***************
*** 990,1001 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
! 	{
! 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
- 	}
  	else
  	{
  		/*
--- 1000,1015 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
+ 	/*
+ 	 * For a join relation or an upper relation, use deparseExplicitTargetList.
+ 	 * Likewise, for a base relation that is being deparsed as a subquery, in
+ 	 * which case the caller would have passed non-NIL tlist, use that
+ 	 * function. Otherwise, use deparseTargetList.
+ 	 */
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL ||
! 		tlist != NIL)
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	else
  	{
  		/*
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1169,1187 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery. Locking clause
+ 		 * for such a relation, if needed, is included in the subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1369,1386 ----
  
  	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
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1436,1595 ----
  }
  
  /*
+  * Append FROM clause entry for the given relation to buf.
+  *
+  * If make_subquery is true, deparse the relation as a subquery.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool make_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	if (make_subquery)
+ 	{
+ 		List	   *tlist = NIL;
+ 		List	   *retrieved_attrs;
+ 
+ 		/* Build a tlist from the subquery. */
+ 		tlist = add_to_flat_tlist(tlist, foreignrel->reltarget->exprs);
+ 		/* Shouldn't be NIL */
+ 		Assert(tlist != NIL);
+ 		/* Should be same length */
+ 		Assert(list_length(tlist) == list_length(foreignrel->reltarget->exprs));
+ 
+ 		/* Append the subquery representing the given relation. */
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectStmtForRel(buf, root, foreignrel, tlist,
+ 								fpinfo->remote_conds, NIL,
+ 								&retrieved_attrs, params_list);
+ 		appendStringInfoChar(buf, ')');
+ 
+ 		/*
+ 		 * Append the subselect alias, so that it becomes easy to refer to
+ 		 * this relation in the rest of the query.
+ 		 */
+ 		appendSubselectAlias(buf, fpinfo->relation_index, list_length(tlist));
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Append the relation and column aliases to the subquery.
+  *
+  * 'tabno' is an integer which uniquely identifies the subquery.
+  * 'ncols' is the number of the column aliases to add.
+  */
+ static void
+ appendSubselectAlias(StringInfo buf, int tabno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the table alias */
+ 	appendStringInfo(buf, " %s%d", SS_TAB_ALIAS_PREFIX, tabno);
+ 
+ 	/* Append the column aliases */
+ 	appendStringInfoChar(buf, '(');
+ 	for (i = 1; i <= ncols; i++)
+ 	{
+ 		if (i > 1)
+ 			appendStringInfoString(buf, ", ");
+ 
+ 		appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 	}
+ 	appendStringInfoChar(buf, ')');
+ }
+ 
+ /*
+  * Get the relation and column alias for a given Var node, which belongs to
+  * input foreignrel. They are returned in *tabno and *colno respectively.
+  */
+ static void
+ get_subselect_alias_id(Var *node, RelOptInfo *foreignrel,
+ 					   int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the table number */
+ 	*tabno = fpinfo->relation_index;
+ 
+ 	/* Get the column number */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/* Shouldn't get here */
+ 	elog(ERROR, "unexpected expression in subquery output");
+ }
+ 
+ /*
+  * Returns true if a given Var node belongs to a relation being deparsed as a
+  * subquery. Returns false otherwise. When returning true, it sets tabno and
+  * colno to unique indexes identifying the relation and column resp. referred
+  * by the given Var node.
+  */
+ static bool
+ is_subquery_var(Var *node, RelOptInfo *foreignrel, int *tabno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!bms_is_member(node->varno, fpinfo->subquery_rels))
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		/*
+ 		 * If outer relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column corresponding to the given Var node.
+ 		 */
+ 		if (fpinfo->make_outerrel_subquery)
+ 		{
+ 			get_subselect_alias_id(node, outerrel, tabno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the outer relation. */
+ 		return is_subquery_var(node, outerrel, tabno, colno);
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * If inner relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column corresponding to the given Var node.
+ 		 */
+ 		if (fpinfo->make_innerrel_subquery)
+ 		{
+ 			get_subselect_alias_id(node, innerrel, tabno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the inner relation. */
+ 		return is_subquery_var(node, innerrel, tabno, colno);
+ 	}
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2233,2257 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	int			tabno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var belongs to a relation deparsed as a subquery, use the
+ 	 * relation and column alias provided by the subquery, instead of the
+ 	 * actual column name.
+ 	 */
+ 	if (is_subquery_var(node, context->scanrel, &tabno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_TAB_ALIAS_PREFIX, tabno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1293 ----
      | 57
  (8 rows)
  
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3100,3123 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,681 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* No outer and inner relation for a base relation. */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	fpinfo->subquery_rels = NULL;
+ 
+ 	/* Set the relation index */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 4147,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4155,4176 ----
  	fpinfo->jointype = jointype;
  
  	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use by the deparser.
+ 	 */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	Assert(bms_is_subset(fpinfo_i->subquery_rels, innerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_o->subquery_rels, outerrel->relids));
+ 	fpinfo->subquery_rels = bms_union(fpinfo_i->subquery_rels,
+ 									  fpinfo_o->subquery_rels);
+ 
+ 	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4181,4187 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4210,4236 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that the
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that the deparser can take appropriate action.
! 			 * We also save the relids of the base relations covered by the
! 			 * joining relation.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo->make_innerrel_subquery = true;
! 				fpinfo->subquery_rels = bms_add_members(fpinfo->subquery_rels,
! 														innerrel->relids);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4311,4326 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 95,100 **** typedef struct PgFdwRelationInfo
--- 95,113 ----
  
  	/* Grouping information */
  	List	   *grouped_tlist;
+ 
+ 	/* Subquery information */
+ 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+ 										 * subquery? */
+ 	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+ 										 * subquery? */
+ 	Relids		subquery_rels;	/* all relids appearing in lower subqueries */
+ 
+ 	/*
+ 	 * Unique integer identifying this relation. It is used for creating a
+ 	 * subselect alias when deparsing the relation as a subquery.
+ 	 */
+ 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,409 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 803,814 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
#54Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#53)
Re: Push down more full joins in postgres_fdw
The comments should explain why is the assertion true.
+        /* Shouldn't be NIL */
+        Assert(tlist != NIL);
+        /* Should be same length */
+        Assert(list_length(tlist) ==
list_length(foreignrel->reltarget->exprs));

OK, I'd like to propose referencing to foreignrel->reltarget->exprs directly
in deparseRangeTblRef and get_subselect_alias_id, then, which is the same as
what I proposed in the first version of the patch, but I'd also like to
change deparseRangeTblRef to use add_to_flat_tlist, not
make_tlist_from_pathtarget, to create a tlist of the subquery, as you
proposed.

There is a already a function to build targetlist for a given relation
build_tlist_to_deparse(), which does the same thing as this code for a join or
base relation and when there are no local conditions. Why don't we use that
function instead of duplicating that logic? If tomorrow, the logic to build the
targetlist changes, we will need to duplicate those changes. We should avoid
that.
+        /* Build a tlist from the subquery. */
+        tlist = add_to_flat_tlist(tlist, foreignrel->reltarget->exprs);
The comment below says "get the aliases", but what the function really returns
is the identifiers used for creating aliases. Please correct the comment.
+/*
+ * Get the relation and column alias for a given Var node, which belongs to
+ * input foreignrel. They are returned in *tabno and *colno respectively.
+ */
We discussed that we have to deparse and search from the same targetlist. And
that the targetlist should be saved in fpinfo, the first time it gets created.
But the patch seems to be searching in foreignrel->reltarget->exprs and
deparsing from the tlist returned by add_to_flat_tlist(tlist,
foreignrel->reltarget->exprs).
+    foreach(lc, foreignrel->reltarget->exprs)
+    {
+        if (equal(lfirst(lc), (Node *) node))
+        {
+            *colno = i;
+            return;
+        }
+        i++;
+    }
I guess, the reason why you are doing it this way, is SELECT clause for the
outermost query gets deparsed before FROM clause. For later we call
deparseRangeTblRef(), which builds the tlist. So, while deparsing SELECT
clause, we do not have tlist to build from. In that case, I guess, we have to
build the tlist in get_subselect_alias_id() if it's not available and stick it
in fpinfo. Subsequent calls to get_subselect_alias_id() should find tlist
there. Then in deparseRangeTblRef() assert that there's a tlist in fpinfo
and use it to build the SELECT clause of subquery. That way, we don't build
tlist unless it's needed and also use the same tlist for all searches. Please
use tlist_member() to search into the tlist.

The name get_subselect_alias_id() seems to suggest that the function returns
identifier for subselect alias, which isn't correct. It returns the alias
identifiers for deparsing a Var node. But I guess, we have left this to the
committer's judgement.

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

#55Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#54)
Re: Push down more full joins in postgres_fdw

On 2016/11/22 18:28, Ashutosh Bapat wrote:

The comments should explain why is the assertion true.
+        /* Shouldn't be NIL */
+        Assert(tlist != NIL);
+        /* Should be same length */
+        Assert(list_length(tlist) ==
list_length(foreignrel->reltarget->exprs));

Will revise.

OK, I'd like to propose referencing to foreignrel->reltarget->exprs directly
in deparseRangeTblRef and get_subselect_alias_id, then, which is the same as
what I proposed in the first version of the patch, but I'd also like to
change deparseRangeTblRef to use add_to_flat_tlist, not
make_tlist_from_pathtarget, to create a tlist of the subquery, as you
proposed.

There is a already a function to build targetlist for a given relation
build_tlist_to_deparse(), which does the same thing as this code for a join or
base relation and when there are no local conditions. Why don't we use that
function instead of duplicating that logic? If tomorrow, the logic to build the
targetlist changes, we will need to duplicate those changes. We should avoid
that.
+        /* Build a tlist from the subquery. */
+        tlist = add_to_flat_tlist(tlist, foreignrel->reltarget->exprs);

Will modify the patch to use build_tlist_to_deparse. Actually, the
early versions of the patch used that function, but it looks like I
changed not to use that function, as I misunderstood about your comments
on this part at some point. Sorry for that.

The comment below says "get the aliases", but what the function really returns
is the identifiers used for creating aliases. Please correct the comment.
+/*
+ * Get the relation and column alias for a given Var node, which belongs to
+ * input foreignrel. They are returned in *tabno and *colno respectively.
+ */

Actually, this was rewritten into the above by *you*. The original
comment I added was:

+ /*
+  * Get info about the subselect alias to given expression.
+  *
+  * The subselect table and column numbers are returned to *tabno and 
*colno,
+  * respectively.
+  */

I'd like to change the comment into something like the original one.

We discussed that we have to deparse and search from the same targetlist. And
that the targetlist should be saved in fpinfo, the first time it gets created.
But the patch seems to be searching in foreignrel->reltarget->exprs and
deparsing from the tlist returned by add_to_flat_tlist(tlist,
foreignrel->reltarget->exprs).
+    foreach(lc, foreignrel->reltarget->exprs)
+    {
+        if (equal(lfirst(lc), (Node *) node))
+        {
+            *colno = i;
+            return;
+        }
+        i++;
+    }
I guess, the reason why you are doing it this way, is SELECT clause for the
outermost query gets deparsed before FROM clause. For later we call
deparseRangeTblRef(), which builds the tlist. So, while deparsing SELECT
clause, we do not have tlist to build from.

That's right.

In that case, I guess, we have to
build the tlist in get_subselect_alias_id() if it's not available and stick it
in fpinfo. Subsequent calls to get_subselect_alias_id() should find tlist
there. Then in deparseRangeTblRef() assert that there's a tlist in fpinfo
and use it to build the SELECT clause of subquery. That way, we don't build
tlist unless it's needed and also use the same tlist for all searches. Please
use tlist_member() to search into the tlist.

I don't think that's a good idea because that would make the code
unnecessarily complicated and inefficient. I think the direct search
into the foreignrel->reltarget->exprs shown above would be better
because that's more simple and efficient than what you proposed. Note
that since (1) the foreignrel->reltarget->exprs doesn't contain any PHVs
and (2) the local_conds is empty, the tlist created for the subquery by
build_tlist_to_deparse is guaranteed to have one-to-one correspondence
with the foreignrel->reltarget->exprs, so the direct search works well.

The name get_subselect_alias_id() seems to suggest that the function returns
identifier for subselect alias, which isn't correct. It returns the alias
identifiers for deparsing a Var node. But I guess, we have left this to the
committer's judgement.

Fine with me.

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

#56Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#55)
Re: Push down more full joins in postgres_fdw
There is a already a function to build targetlist for a given relation
build_tlist_to_deparse(), which does the same thing as this code for a
join or
base relation and when there are no local conditions. Why don't we use
that
function instead of duplicating that logic? If tomorrow, the logic to
build the
targetlist changes, we will need to duplicate those changes. We should
avoid
that.
+        /* Build a tlist from the subquery. */
+        tlist = add_to_flat_tlist(tlist, foreignrel->reltarget->exprs);

Will modify the patch to use build_tlist_to_deparse. Actually, the early
versions of the patch used that function, but it looks like I changed not to
use that function, as I misunderstood about your comments on this part at
some point. Sorry for that.

The comment below says "get the aliases", but what the function really
returns
is the identifiers used for creating aliases. Please correct the comment.
+/*
+ * Get the relation and column alias for a given Var node, which belongs
to
+ * input foreignrel. They are returned in *tabno and *colno respectively.
+ */

Actually, this was rewritten into the above by *you*. The original comment
I added was:

+ /*
+  * Get info about the subselect alias to given expression.
+  *
+  * The subselect table and column numbers are returned to *tabno and
*colno,
+  * respectively.
+  */

I'd like to change the comment into something like the original one.

Sorry. I think the current version is better than previous one. The
term "subselect alias" is confusing in the previous version. In the
current version, "Get the relation and column alias for a given Var
node," we need to add word "identifiers" like "Get the relation and
column identifiers for a given Var node".

We discussed that we have to deparse and search from the same targetlist.
And
that the targetlist should be saved in fpinfo, the first time it gets
created.
But the patch seems to be searching in foreignrel->reltarget->exprs and
deparsing from the tlist returned by add_to_flat_tlist(tlist,
foreignrel->reltarget->exprs).
+    foreach(lc, foreignrel->reltarget->exprs)
+    {
+        if (equal(lfirst(lc), (Node *) node))
+        {
+            *colno = i;
+            return;
+        }
+        i++;
+    }
I guess, the reason why you are doing it this way, is SELECT clause for
the
outermost query gets deparsed before FROM clause. For later we call
deparseRangeTblRef(), which builds the tlist. So, while deparsing SELECT
clause, we do not have tlist to build from.

That's right.

In that case, I guess, we have to
build the tlist in get_subselect_alias_id() if it's not available and
stick it
in fpinfo. Subsequent calls to get_subselect_alias_id() should find tlist
there. Then in deparseRangeTblRef() assert that there's a tlist in fpinfo
and use it to build the SELECT clause of subquery. That way, we don't
build
tlist unless it's needed and also use the same tlist for all searches.
Please
use tlist_member() to search into the tlist.

I don't think that's a good idea because that would make the code
unnecessarily complicated and inefficient. I think the direct search into
the foreignrel->reltarget->exprs shown above would be better because that's
more simple and efficient than what you proposed. Note that since (1) the
foreignrel->reltarget->exprs doesn't contain any PHVs and (2) the
local_conds is empty, the tlist created for the subquery by
build_tlist_to_deparse is guaranteed to have one-to-one correspondence with
the foreignrel->reltarget->exprs, so the direct search works well.

If we deparse from and search into different objects, that's not very
maintainable code. Changes to any one of them require changes to the
other, thus creating avenues for bugs. Even if slighly expensive, we
should search into and deparse from the same targetlist. I think I
have explained this before.

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

#57Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#56)
Re: Push down more full joins in postgres_fdw

On 2016/11/23 0:30, Ashutosh Bapat wrote:

You wrote:

The comment below says "get the aliases", but what the function really
returns
is the identifiers used for creating aliases. Please correct the comment.
+/*
+ * Get the relation and column alias for a given Var node, which belongs
to
+ * input foreignrel. They are returned in *tabno and *colno respectively.
+ */

I wrote:

Actually, this was rewritten into the above by *you*. The original comment
I added was:

+ /*
+  * Get info about the subselect alias to given expression.
+  *
+  * The subselect table and column numbers are returned to *tabno and
*colno,
+  * respectively.
+  */

I'd like to change the comment into something like the original one.

Sorry. I think the current version is better than previous one. The
term "subselect alias" is confusing in the previous version. In the
current version, "Get the relation and column alias for a given Var
node," we need to add word "identifiers" like "Get the relation and
column identifiers for a given Var node".

OK, but one thing I'm concerned about is the term "relation alias" seems
a bit confusing because we already used the term for the alias of the
form "rN". To avoid that, how about saying "table alias", not "relation
alias"? (in which case, the comment would be something like "Get the
table and column identifiers for a given Var node".)

We discussed that we have to deparse and search from the same targetlist.
And
that the targetlist should be saved in fpinfo, the first time it gets
created.
But the patch seems to be searching in foreignrel->reltarget->exprs and
deparsing from the tlist returned by add_to_flat_tlist(tlist,
foreignrel->reltarget->exprs).
+    foreach(lc, foreignrel->reltarget->exprs)
+    {
+        if (equal(lfirst(lc), (Node *) node))
+        {
+            *colno = i;
+            return;
+        }
+        i++;
+    }
I guess, the reason why you are doing it this way, is SELECT clause for
the
outermost query gets deparsed before FROM clause. For later we call
deparseRangeTblRef(), which builds the tlist. So, while deparsing SELECT
clause, we do not have tlist to build from.

That's right.

In that case, I guess, we have to
build the tlist in get_subselect_alias_id() if it's not available and
stick it
in fpinfo. Subsequent calls to get_subselect_alias_id() should find tlist
there. Then in deparseRangeTblRef() assert that there's a tlist in fpinfo
and use it to build the SELECT clause of subquery. That way, we don't
build
tlist unless it's needed and also use the same tlist for all searches.
Please
use tlist_member() to search into the tlist.

I don't think that's a good idea because that would make the code
unnecessarily complicated and inefficient. I think the direct search into
the foreignrel->reltarget->exprs shown above would be better because that's
more simple and efficient than what you proposed. Note that since (1) the
foreignrel->reltarget->exprs doesn't contain any PHVs and (2) the
local_conds is empty, the tlist created for the subquery by
build_tlist_to_deparse is guaranteed to have one-to-one correspondence with
the foreignrel->reltarget->exprs, so the direct search works well.

If we deparse from and search into different objects, that's not very
maintainable code. Changes to any one of them require changes to the
other, thus creating avenues for bugs. Even if slighly expensive, we
should search into and deparse from the same targetlist.

I don't think so; in the current version, we essentially deparse from
and search into the same object, ie, foreignrel->reltarget->exprs, since
the tlist created by build_tlist_to_deparse is build from the
foreignrel->reltarget->exprs. Also, since it is just created for
deparsing the relation as a subquery in deparseRangeTblRef and isn't
stored into fpinfo or anywhere alse, we would need no concern about
creating such avenues. IMO I think adding comments would be enough for
this. Anyway, I think this is an optional issue, so I'd like to leave
this for the committer's judge.

I think I
have explained this before.

My apologies for having misunderstood your words.

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

#58Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#57)
Re: Push down more full joins in postgres_fdw

Sorry. I think the current version is better than previous one. The
term "subselect alias" is confusing in the previous version. In the
current version, "Get the relation and column alias for a given Var
node," we need to add word "identifiers" like "Get the relation and
column identifiers for a given Var node".

OK, but one thing I'm concerned about is the term "relation alias" seems a
bit confusing because we already used the term for the alias of the form
"rN". To avoid that, how about saying "table alias", not "relation alias"?
(in which case, the comment would be something like "Get the table and
column identifiers for a given Var node".)

table will be misleading as subquery can represent a join and
corresponding alias would represent the join. Relation is better term.

If we deparse from and search into different objects, that's not very
maintainable code. Changes to any one of them require changes to the
other, thus creating avenues for bugs. Even if slighly expensive, we
should search into and deparse from the same targetlist.

I don't think so; in the current version, we essentially deparse from and
search into the same object, ie, foreignrel->reltarget->exprs, since the
tlist created by build_tlist_to_deparse is build from the
foreignrel->reltarget->exprs. Also, since it is just created for deparsing
the relation as a subquery in deparseRangeTblRef and isn't stored into
fpinfo or anywhere alse, we would need no concern about creating such
avenues. IMO I think adding comments would be enough for this.

build_tlist_to_depase() calls pull_var_nodes() before creating the
tlist, whereas the code that searches does not do that. Code-wise
those are not the same things.

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

#59Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#58)
Re: Push down more full joins in postgres_fdw

On 2016/11/24 16:46, Ashutosh Bapat wrote:

Sorry. I think the current version is better than previous one. The
term "subselect alias" is confusing in the previous version. In the
current version, "Get the relation and column alias for a given Var
node," we need to add word "identifiers" like "Get the relation and
column identifiers for a given Var node".

I wrote:

OK, but one thing I'm concerned about is the term "relation alias" seems a
bit confusing because we already used the term for the alias of the form
"rN". To avoid that, how about saying "table alias", not "relation alias"?
(in which case, the comment would be something like "Get the table and
column identifiers for a given Var node".)

table will be misleading as subquery can represent a join and
corresponding alias would represent the join. Relation is better term.

But the documentation uses the word "table" for a join relation. See
7.2.1.2. Table and Column Aliases:
https://www.postgresql.org/docs/9.6/static/queries-table-expressions.html#QUERIES-TABLE-ALIASES

I don't think so; in the current version, we essentially deparse from and
search into the same object, ie, foreignrel->reltarget->exprs, since the
tlist created by build_tlist_to_deparse is build from the
foreignrel->reltarget->exprs. Also, since it is just created for deparsing
the relation as a subquery in deparseRangeTblRef and isn't stored into
fpinfo or anywhere alse, we would need no concern about creating such
avenues. IMO I think adding comments would be enough for this.

build_tlist_to_depase() calls pull_var_nodes() before creating the
tlist, whereas the code that searches does not do that. Code-wise
those are not the same things.

You missed the point; the foreignrel->reltarget->exprs doesn't contain
any PHVs, so the tlist created by build_tlist_to_depase will be
guaranteed to be one-to-one with the foreignrel->reltarget->exprs.

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

#60Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#59)
Re: Push down more full joins in postgres_fdw

On Thu, Nov 24, 2016 at 1:27 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/11/24 16:46, Ashutosh Bapat wrote:

Sorry. I think the current version is better than previous one. The
term "subselect alias" is confusing in the previous version. In the
current version, "Get the relation and column alias for a given Var
node," we need to add word "identifiers" like "Get the relation and
column identifiers for a given Var node".

I wrote:

OK, but one thing I'm concerned about is the term "relation alias" seems
a
bit confusing because we already used the term for the alias of the form
"rN". To avoid that, how about saying "table alias", not "relation
alias"?
(in which case, the comment would be something like "Get the table and
column identifiers for a given Var node".)

table will be misleading as subquery can represent a join and
corresponding alias would represent the join. Relation is better term.

But the documentation uses the word "table" for a join relation. See
7.2.1.2. Table and Column Aliases:
https://www.postgresql.org/docs/9.6/static/queries-table-expressions.html#QUERIES-TABLE-ALIASES

The definition there says "A temporary name can be given to tables and
complex table references to be used for references to the derived
table in the rest of the query. This is called a table alias.", please
note the usage "complex table references". Within the code, we do not
refer to a relation as table. So, the comment in this code should not
refer to a relation as table either.

I don't think so; in the current version, we essentially deparse from and
search into the same object, ie, foreignrel->reltarget->exprs, since the
tlist created by build_tlist_to_deparse is build from the
foreignrel->reltarget->exprs. Also, since it is just created for
deparsing
the relation as a subquery in deparseRangeTblRef and isn't stored into
fpinfo or anywhere alse, we would need no concern about creating such
avenues. IMO I think adding comments would be enough for this.

build_tlist_to_depase() calls pull_var_nodes() before creating the
tlist, whereas the code that searches does not do that. Code-wise
those are not the same things.

You missed the point; the foreignrel->reltarget->exprs doesn't contain any
PHVs, so the tlist created by build_tlist_to_depase will be guaranteed to be
one-to-one with the foreignrel->reltarget->exprs.

It's guaranteed now, but can not be forever. This means anybody who
supports PHV tomorrow, will have to "remember" to update this code as
well. If s/he misses it, a bug will be introduced. That's the avenue
for bug, I am talking about.

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

#61Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#60)
Re: Push down more full joins in postgres_fdw

On 2016/11/24 17:39, Ashutosh Bapat wrote:

On Thu, Nov 24, 2016 at 1:27 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/11/24 16:46, Ashutosh Bapat wrote:

table will be misleading as subquery can represent a join and
corresponding alias would represent the join. Relation is better term.

But the documentation uses the word "table" for a join relation. See
7.2.1.2. Table and Column Aliases:
https://www.postgresql.org/docs/9.6/static/queries-table-expressions.html#QUERIES-TABLE-ALIASES

The definition there says "A temporary name can be given to tables and
complex table references to be used for references to the derived
table in the rest of the query. This is called a table alias.", please
note the usage "complex table references". Within the code, we do not
refer to a relation as table. So, the comment in this code should not
refer to a relation as table either.

OK, will keep using the term "relation".

I wrote:

I don't think so; in the current version, we essentially deparse from and
search into the same object, ie, foreignrel->reltarget->exprs, since the
tlist created by build_tlist_to_deparse is build from the
foreignrel->reltarget->exprs. Also, since it is just created for
deparsing
the relation as a subquery in deparseRangeTblRef and isn't stored into
fpinfo or anywhere alse, we would need no concern about creating such
avenues. IMO I think adding comments would be enough for this.

build_tlist_to_depase() calls pull_var_nodes() before creating the
tlist, whereas the code that searches does not do that. Code-wise
those are not the same things.

You missed the point; the foreignrel->reltarget->exprs doesn't contain any
PHVs, so the tlist created by build_tlist_to_depase will be guaranteed to be
one-to-one with the foreignrel->reltarget->exprs.

It's guaranteed now, but can not be forever. This means anybody who
supports PHV tomorrow, will have to "remember" to update this code as
well. If s/he misses it, a bug will be introduced. That's the avenue
for bug, I am talking about.

It *can* be guaranteed. See another patch for supporting evaluating
PHVs remotely.

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

#62Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#61)
Re: Push down more full joins in postgres_fdw

build_tlist_to_depase() calls pull_var_nodes() before creating the
tlist, whereas the code that searches does not do that. Code-wise
those are not the same things.

You missed the point; the foreignrel->reltarget->exprs doesn't contain
any
PHVs, so the tlist created by build_tlist_to_depase will be guaranteed to
be
one-to-one with the foreignrel->reltarget->exprs.

It's guaranteed now, but can not be forever. This means anybody who
supports PHV tomorrow, will have to "remember" to update this code as
well. If s/he misses it, a bug will be introduced. That's the avenue
for bug, I am talking about.

It *can* be guaranteed. See another patch for supporting evaluating PHVs
remotely.

We can't base assumptions in this patch on something in the other
patch, esp. when that's not even reviewed once. PHV is just one case,
subqueries involving aggregates is other and there are others. But
that's not really the point. The point is
add_to_flat_tlist(pull_var_clause(rel->reltarget->exprs) can not be
proved to be same as rel->reltarget->exprs in general. So, we should
base our code on an assumption that can not be proved.

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

#63Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#62)
Re: Push down more full joins in postgres_fdw

On 2016/11/24 18:20, Ashutosh Bapat wrote:

I wrote:

You missed the point; the foreignrel->reltarget->exprs doesn't contain
any
PHVs, so the tlist created by build_tlist_to_depase will be guaranteed to
be
one-to-one with the foreignrel->reltarget->exprs.

You wrote:

It's guaranteed now, but can not be forever. This means anybody who
supports PHV tomorrow, will have to "remember" to update this code as
well. If s/he misses it, a bug will be introduced. That's the avenue
for bug, I am talking about.

I wrote:

It *can* be guaranteed. See another patch for supporting evaluating PHVs
remotely.

We can't base assumptions in this patch on something in the other
patch, esp. when that's not even reviewed once. PHV is just one case,
subqueries involving aggregates is other and there are others.

Let's discuss this together with the patch for PHVs. That was one of
the reasons why I had merged the two patches into one. I'd like to
leave enhancements such as subqueries involving aggregates for future
work, though.

But
that's not really the point. The point is
add_to_flat_tlist(pull_var_clause(rel->reltarget->exprs) can not be
proved to be same as rel->reltarget->exprs in general.

Really? As mentioned in comments for RelOptInfo in relation.h shown
below, the rel->reltarget->exprs for a base/join relation only contains
Vars and PHVs, so the two things would be proved to be one-to-one.
(Note: we don't need to care about the appendrel-child-relation case
because appendrel child relations wouldn't appear in the main join tree.)

* reltarget - Default Path output tlist for this rel; normally
contains
* Var and PlaceHolderVar nodes for the values we need to
* output from this relation.
* List is in no particular order, but all rels of an
* appendrel set must use corresponding orders.
* NOTE: in an appendrel child relation, may contain
* arbitrary expressions pulled up from a subquery!

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

#64Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#54)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/11/22 18:28, Ashutosh Bapat wrote:

The comments should explain why is the assertion true.
+        /* Shouldn't be NIL */
+        Assert(tlist != NIL);

I noticed that I was wrong; in the Assertion the tlist can be empty. An
example for such a case is:

SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);

In this example any columns of the relations ft4 and ft5 wouldn't be
needed for the join or the final output, so both the tlists for the
relations created in deparseRangeTblRef would be empty. Attached is an
updated version fixing this bug. Changes are:

* I removed make_outerrel_subquery/make_innerrel_subquery from fpinfo
and added a new member "is_subquery_rel" to fpinfo, as a flag on whether
to deparse the relation as a subquery.
* I modified deparseRangeTblRef, deparseSelectSql, and is_subquery_var
to see the is_subquery_rel, not
make_outerrel_subquery/make_innerrel_subquery.
* I modified appendSubselectAlias to handle the case where ncols = 0
properly.
* I renamed subquery_rels in fpinfo to lower_subquery_rels, to make it
easy to distinguish this from is_subquery_rel clearly.
* I added regression tests for that.

The rest of the patch is the same as the previous version, but:

+        /* Should be same length */
+        Assert(list_length(tlist) ==
list_length(foreignrel->reltarget->exprs));

I added comments explaining the reason.

The name get_subselect_alias_id() seems to suggest that the function returns
identifier for subselect alias, which isn't correct. It returns the alias
identifiers for deparsing a Var node. But I guess, we have left this to the
committer's judgement.

I agree that the name isn't good, so I changed it to
get_relation_column_alias_ids(). Let me know if it may be better.

I also revised code and comments a bit, just for consistency.

I will update another patch for PHVs on top of the attached one.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v11.patchtext/x-patch; name=postgres-fdw-subquery-support-v11.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_REL_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,181 ----
  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, List **params_list);
+ static void appendSubselectAlias(StringInfo buf, int relno, int ncols);
+ static void get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
+ 							  int *relno, int *colno);
+ static bool is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno,
+ 						int *colno);
  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,
***************
*** 990,1001 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
! 	{
! 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
- 	}
  	else
  	{
  		/*
--- 999,1013 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
+ 	/*
+ 	 * For a join relation or an upper relation, use deparseExplicitTargetList.
+ 	 * Likewise, for a relation that is being deparsed as a subquery, use that
+ 	 * function. Otherwise, use deparseTargetList.
+ 	 */
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL ||
! 		fpinfo->is_subquery_rel)
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	else
  	{
  		/*
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1167,1185 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery. Locking clause
+ 		 * for such a relation, if needed, is included in the subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->lower_subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1367,1382 ----
  
  	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, params_list);
  
  		/* Deparse inner relation */
  		initStringInfo(&join_sql_i);
! 		deparseRangeTblRef(&join_sql_i, root, fpinfo->innerrel, params_list);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1432,1601 ----
  }
  
  /*
+  * Append FROM clause entry for the given relation to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	/* If is_subquery_rel is true, deparse the relation as a subquery. */
+ 	if (fpinfo->is_subquery_rel)
+ 	{
+ 		List	   *tlist;
+ 		List	   *retrieved_attrs;
+ 
+ 		/* Build a tlist from the subquery. */
+ 		tlist = build_tlist_to_deparse(foreignrel);
+ 
+ 		/*
+ 		 * Since (1) the expressions in foreignrel's reltarget doesn't contain
+ 		 * any PHVs and (2) foreignrel's local_conds is empty, the tlist
+ 		 * created by build_tlist_to_deparse must be one-to-one with the
+ 		 * expressions.
+ 		 */
+ 		Assert(list_length(tlist) == list_length(foreignrel->reltarget->exprs));
+ 
+ 		/* Append the subquery representing the given relation. */
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectStmtForRel(buf, root, foreignrel, tlist,
+ 								fpinfo->remote_conds, NIL,
+ 								&retrieved_attrs, params_list);
+ 		appendStringInfoChar(buf, ')');
+ 
+ 		/*
+ 		 * Append the subselect alias for references to this relation in the
+ 		 * rest of the query.
+ 		 */
+ 		appendSubselectAlias(buf, fpinfo->relation_index, list_length(tlist));
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Append the relation and column aliases to a subquery.
+  *
+  * 'relno' is the relation alias ID.
+  * 'ncols' is the number of the column aliases to add.
+  */
+ static void
+ appendSubselectAlias(StringInfo buf, int relno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the relation alias */
+ 	appendStringInfo(buf, " %s%d", SS_REL_ALIAS_PREFIX, relno);
+ 
+ 	/* Append the column aliases, if needed */
+ 	if (ncols > 0)
+ 	{
+ 		appendStringInfoChar(buf, '(');
+ 		for (i = 1; i <= ncols; i++)
+ 		{
+ 			if (i > 1)
+ 				appendStringInfoString(buf, ", ");
+ 
+ 			appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 		}
+ 		appendStringInfoChar(buf, ')');
+ 	}
+ }
+ 
+ /*
+  * Get the relation and column alias IDs of a given Var node, which belongs
+  * to input foreignrel. They are returned in *relno and *colno respectively.
+  */
+ static void
+ get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
+ 							  int *relno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the relation alias ID */
+ 	*relno = fpinfo->relation_index;
+ 
+ 	/* Get the column alias ID */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/* Shouldn't get here */
+ 	elog(ERROR, "unexpected expression in subquery output");
+ }
+ 
+ /*
+  * Returns true if a given Var node belongs to a relation being deparsed as a
+  * subquery. Returns false otherwise. When returning true, it sets *relno and
+  * *colno to the relation and column alias IDs of the given Var node,
+  * respectively.
+  */
+ static bool
+ is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!bms_is_member(node->varno, fpinfo->lower_subquery_rels))
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
+ 
+ 		/*
+ 		 * If outer relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column alias to the given Var node.
+ 		 */
+ 		if (fpinfo2->is_subquery_rel)
+ 		{
+ 			get_relation_column_alias_ids(node, outerrel, relno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the outer relation. */
+ 		return is_subquery_var(node, outerrel, relno, colno);
+ 	}
+ 	else
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
+ 
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * If inner relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column alias to the given Var node.
+ 		 */
+ 		if (fpinfo2->is_subquery_rel)
+ 		{
+ 			get_relation_column_alias_ids(node, innerrel, relno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the inner relation. */
+ 		return is_subquery_var(node, innerrel, relno, colno);
+ 	}
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2239,2263 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	int			relno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var belongs to a relation deparsed as a subquery, use the
+ 	 * relation and column alias provided by the subquery, instead of the
+ 	 * actual column name.
+ 	 */
+ 	if (is_subquery_var(node, context->scanrel, &relno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_REL_ALIAS_PREFIX, relno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1332 ----
      | 57
  (8 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);
+                                                                                             QUERY PLAN                                                                                            
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: 1
+    Relations: (public.ft4) FULL JOIN (public.ft5)
+    Remote SQL: SELECT NULL FROM ((SELECT NULL FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4 FULL JOIN (SELECT NULL FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5 ON (TRUE))
+ (4 rows)
+ 
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);
+  ?column? 
+ ----------
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+ (24 rows)
+ 
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3139,3162 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,680 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Initialize info about whether to deparse this rel as a subquery. */
+ 	fpinfo->is_subquery_rel = false;
+ 	/* Initialize info about lower subqueries. */
+ 	fpinfo->lower_subquery_rels = NULL;
+ 	/* Set the relation index. */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 4146,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->innerrel = innerrel;
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4153,4176 ----
  	fpinfo->innerrel = innerrel;
  	fpinfo->jointype = jointype;
  
+ 	/* Initialize info about whether to deparse this rel as a subquery. */
+ 	fpinfo->is_subquery_rel = false;
+ 
+ 	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use by the deparser.
+ 	 */
+ 	Assert(bms_is_subset(fpinfo_i->lower_subquery_rels, innerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_o->lower_subquery_rels, outerrel->relids));
+ 	fpinfo->lower_subquery_rels = bms_union(fpinfo_i->lower_subquery_rels,
+ 									  fpinfo_o->lower_subquery_rels);
+ 
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4181,4187 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4210,4238 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that the
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that the deparser can take appropriate action.
! 			 * We also save the relids of the base relations covered by the
! 			 * joining relation.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo_o->is_subquery_rel = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo_i->is_subquery_rel = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									innerrel->relids);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4313,4328 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 95,100 **** typedef struct PgFdwRelationInfo
--- 95,110 ----
  
  	/* Grouping information */
  	List	   *grouped_tlist;
+ 
+ 	/* Subquery information */
+ 	bool		is_subquery_rel;	/* do we deparse this as a subquery? */
+ 	Relids		lower_subquery_rels;	/* all relids appearing in lower
+ 										 * subqueries */
+ 	/*
+ 	 * Index of the relation. It is used for creating a subselect alias when
+ 	 * the is_subquery_rel is true.
+ 	 */
+ 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,412 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 806,817 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
#65Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#64)
2 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/11/24 21:45, Etsuro Fujita wrote:

On 2016/11/22 18:28, Ashutosh Bapat wrote:

The comments should explain why is the assertion true.
+        /* Shouldn't be NIL */
+        Assert(tlist != NIL);

I noticed that I was wrong; in the Assertion the tlist can be empty. An
example for such a case is:

SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);

In this example any columns of the relations ft4 and ft5 wouldn't be
needed for the join or the final output, so both the tlists for the
relations created in deparseRangeTblRef would be empty. Attached is an
updated version fixing this bug. Changes are:

* I removed make_outerrel_subquery/make_innerrel_subquery from fpinfo
and added a new member "is_subquery_rel" to fpinfo, as a flag on whether
to deparse the relation as a subquery.
* I modified deparseRangeTblRef, deparseSelectSql, and is_subquery_var
to see the is_subquery_rel, not
make_outerrel_subquery/make_innerrel_subquery.
* I modified appendSubselectAlias to handle the case where ncols = 0
properly.
* I renamed subquery_rels in fpinfo to lower_subquery_rels, to make it
easy to distinguish this from is_subquery_rel clearly.
* I added regression tests for that.

I noticed that the above changes are not adequate; the updated patch
breaks EXPLAIN outputs for EvalPlanQual subplans of foreign joins in
which foreign tables are full joined and in the remote join query the
foreign tables are deparsed as subqueries. Consider:

postgres=# explain verbose select * from test, ((select * from ft1 where
b between 1 and 3) t1 full join (select * from ft2 where b between 1 and
3) t2
on (t1.a = t2.a)) ss for update of test;

QUERY PLA
N
-------------------------------------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------------------------------
LockRows (cost=100.00..287.33 rows=6780 width=94)
Output: test.a, test.b, ft1.a, ft1.b, ft2.a, ft2.b, test.ctid,
ft1.*, ft2.*
-> Nested Loop (cost=100.00..219.53 rows=6780 width=94)
Output: test.a, test.b, ft1.a, ft1.b, ft2.a, ft2.b, test.ctid,
ft1.*, ft2.*
-> Seq Scan on public.test (cost=0.00..32.60 rows=2260 width=14)
Output: test.a, test.b, test.ctid
-> Materialize (cost=100.00..102.19 rows=3 width=80)
Output: ft1.a, ft1.b, ft1.*, ft2.a, ft2.b, ft2.*
-> Foreign Scan (cost=100.00..102.17 rows=3 width=80)
Output: ft1.a, ft1.b, ft1.*, ft2.a, ft2.b, ft2.*
Relations: (public.ft1) FULL JOIN (public.ft2)
Remote SQL: SELECT s5.c1, s5.c2, s5.c3, s6.c1,
s6.c2, s6.c3 FROM ((SELECT a, b, ROW(a, b) FROM public.t1 WHERE ((b >=
1)) AND ((b
<= 3))) s5(c1, c2, c3) FULL JOIN (SELECT a, b, ROW(a, b) FROM public.t2
WHERE ((b >= 1)) AND ((b <= 3))) s6(c1, c2, c3) ON (((s5.c1 = s6.c1))))
-> Hash Full Join (cost=201.14..202.29 rows=3
width=80)
Output: ft1.a, ft1.b, ft1.*, ft2.a, ft2.b, ft2.*
Hash Cond: (ft1.a = ft2.a)
-> Foreign Scan on public.ft1
(cost=100.00..101.11 rows=3 width=40)
Output: ft1.a, ft1.b, ft1.*
Remote SQL: SELECT NULL FROM public.t1
WHERE ((b >= 1)) AND ((b <= 3))
-> Hash (cost=101.11..101.11 rows=3 width=40)
Output: ft2.a, ft2.b, ft2.*
-> Foreign Scan on public.ft2
(cost=100.00..101.11 rows=3 width=40)
Output: ft2.a, ft2.b, ft2.*
Remote SQL: SELECT NULL FROM
public.t2 WHERE ((b >= 1)) AND ((b <= 3))
(23 rows)

The SELECT clauses of the remote queries for the Foreign Scans in the
EPQ subplan are deparsed incorrectly into "SELECT NULL". The reason for
that is that since the foreign tables' fpinfo->is_subquery_rel has been
set true at the time those clauses are deparsed (due to that the foreign
tables have been deparsed as subqueries in the main remote join query),
deparseSelectSql calls deparseExplicitTargetList with an empty tlist, to
construct the clauses, so it deparses the tlist into "NULL". This is
harmless because we don't use the remote queries for the Foreign Scans
during an EPQ recheck, but that would be confusing for users, so I
modified the patch a bit further to make the EXPLAIN output as-is.
Attached is a new version of the patch.

I also revised code and comments a bit, just for consistency.

Also, I renamed appendSubselectAlias to appendSubqueryAlias, because we
use the term "subquery" for other places.

I will update another patch for PHVs on top of the attached one.

Done. I also revised some code and comments. I'm also attaching the
updated version of the patch for PHVs.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v12.patchtext/x-patch; name=postgres-fdw-subquery-support-v12.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SS_REL_ALIAS_PREFIX	"s"
+ #define SS_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 159,165 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
--- 161,167 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,181 ----
  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, List **params_list);
+ static void appendSubqueryAlias(StringInfo buf, int relno, int ncols);
+ static void get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
+ 							  int *relno, int *colno);
+ static bool is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno,
+ 						int *colno);
  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,
***************
*** 881,888 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
   * Deparse SELECT statement for given relation into buf.
   *
   * tlist contains the list of desired columns to be fetched from foreign server.
!  * For a base relation fpinfo->attrs_used is used to construct SELECT clause,
!  * hence the tlist is ignored for a base relation.
   *
   * remote_conds is the list of conditions to be deparsed into the WHERE clause
   * (or, in the case of upper relations, into the HAVING clause).
--- 890,899 ----
   * Deparse SELECT statement for given relation into buf.
   *
   * tlist contains the list of desired columns to be fetched from foreign server.
!  * We use the tlist to construct the SELECT clause, if the relation is a join
!  * or upper relation or if is_subquery is true, in which case the relation is
!  * deparsed as a subquery.  Otherwise, we use the relation's fpinfo->attrs_used,
!  * in which case the tlist is ignored for the relation.
   *
   * remote_conds is the list of conditions to be deparsed into the WHERE clause
   * (or, in the case of upper relations, into the HAVING clause).
***************
*** 901,907 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
  extern void
  deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  						List *tlist, List *remote_conds, List *pathkeys,
! 						List **retrieved_attrs, List **params_list)
  {
  	deparse_expr_cxt context;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
--- 912,919 ----
  extern void
  deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  						List *tlist, List *remote_conds, List *pathkeys,
! 						bool is_subquery, List **retrieved_attrs,
! 						List **params_list)
  {
  	deparse_expr_cxt context;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
***************
*** 925,931 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  	context.params_list = params_list;
  
  	/* Construct SELECT clause */
! 	deparseSelectSql(tlist, retrieved_attrs, &context);
  
  	/*
  	 * For upper relations, the WHERE clause is built from the remote
--- 937,943 ----
  	context.params_list = params_list;
  
  	/* Construct SELECT clause */
! 	deparseSelectSql(tlist, is_subquery, retrieved_attrs, &context);
  
  	/*
  	 * For upper relations, the WHERE clause is built from the remote
***************
*** 974,984 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
   *
!  * tlist is the list of desired columns. Read prologue of
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
--- 986,998 ----
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
   *
!  * tlist is the list of desired columns.  is_subquery is a flag to indicate
!  * whether to deparse the relation as a subquery.  Read prologue of
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
! 				 deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
***************
*** 990,1001 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
! 	{
! 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
- 	}
  	else
  	{
  		/*
--- 1004,1018 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
+ 	/*
+ 	 * For a join relation or an upper relation, use deparseExplicitTargetList.
+ 	 * Likewise, for a relation that is being deparsed as a subquery, use that
+ 	 * function. Otherwise, use deparseTargetList.
+ 	 */
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL ||
! 		is_subquery)
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	else
  	{
  		/*
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1172,1190 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery. Locking clause
+ 		 * for such a relation, if needed, is included in the subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->lower_subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1372,1387 ----
  
  	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, params_list);
  
  		/* Deparse inner relation */
  		initStringInfo(&join_sql_i);
! 		deparseRangeTblRef(&join_sql_i, root, fpinfo->innerrel, params_list);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1437,1606 ----
  }
  
  /*
+  * Append FROM clause entry for the given relation to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	/* If is_subquery_rel is true, deparse the relation as a subquery. */
+ 	if (fpinfo->is_subquery_rel)
+ 	{
+ 		List	   *tlist;
+ 		List	   *retrieved_attrs;
+ 
+ 		/* Build a tlist from the subquery. */
+ 		tlist = build_tlist_to_deparse(foreignrel);
+ 
+ 		/*
+ 		 * Since (1) the expressions in foreignrel's reltarget doesn't contain
+ 		 * any PHVs and (2) foreignrel's local_conds is empty, the tlist
+ 		 * created by build_tlist_to_deparse must be one-to-one with the
+ 		 * expressions.
+ 		 */
+ 		Assert(list_length(tlist) == list_length(foreignrel->reltarget->exprs));
+ 
+ 		/* Append the subquery representing the given relation. */
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectStmtForRel(buf, root, foreignrel, tlist,
+ 								fpinfo->remote_conds, NIL, true,
+ 								&retrieved_attrs, params_list);
+ 		appendStringInfoChar(buf, ')');
+ 
+ 		/*
+ 		 * Append the subquery alias for references to this relation in the
+ 		 * rest of the query.
+ 		 */
+ 		appendSubqueryAlias(buf, fpinfo->relation_index, list_length(tlist));
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Append the relation and column aliases to a subquery.
+  *
+  * 'relno' is the relation alias ID.
+  * 'ncols' is the number of the column aliases to add.
+  */
+ static void
+ appendSubqueryAlias(StringInfo buf, int relno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the relation alias */
+ 	appendStringInfo(buf, " %s%d", SS_REL_ALIAS_PREFIX, relno);
+ 
+ 	/* Append the column aliases, if needed */
+ 	if (ncols > 0)
+ 	{
+ 		appendStringInfoChar(buf, '(');
+ 		for (i = 1; i <= ncols; i++)
+ 		{
+ 			if (i > 1)
+ 				appendStringInfoString(buf, ", ");
+ 
+ 			appendStringInfo(buf, "%s%d", SS_COL_ALIAS_PREFIX, i);
+ 		}
+ 		appendStringInfoChar(buf, ')');
+ 	}
+ }
+ 
+ /*
+  * Get the relation and column alias IDs of a given Var node, which belongs
+  * to input foreignrel. They are returned in *relno and *colno respectively.
+  */
+ static void
+ get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
+ 							  int *relno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the relation alias ID */
+ 	*relno = fpinfo->relation_index;
+ 
+ 	/* Get the column alias ID */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/* Shouldn't get here */
+ 	elog(ERROR, "unexpected expression in subquery output");
+ }
+ 
+ /*
+  * Returns true if a given Var node belongs to a relation being deparsed as a
+  * subquery. Returns false otherwise. When returning true, it sets *relno and
+  * *colno to the relation and column alias IDs of the given Var node,
+  * respectively.
+  */
+ static bool
+ is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	if (!bms_is_member(node->varno, fpinfo->lower_subquery_rels))
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
+ 
+ 		/*
+ 		 * If outer relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column alias to the given Var node.
+ 		 */
+ 		if (fpinfo2->is_subquery_rel)
+ 		{
+ 			get_relation_column_alias_ids(node, outerrel, relno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the outer relation. */
+ 		return is_subquery_var(node, outerrel, relno, colno);
+ 	}
+ 	else
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
+ 
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * If inner relation is deparsed as a subquery, get the identifiers for
+ 		 * the relation and column alias to the given Var node.
+ 		 */
+ 		if (fpinfo2->is_subquery_rel)
+ 		{
+ 			get_relation_column_alias_ids(node, innerrel, relno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the inner relation. */
+ 		return is_subquery_var(node, innerrel, relno, colno);
+ 	}
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2244,2268 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	int			relno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var belongs to a relation deparsed as a subquery, use the
+ 	 * relation and column alias provided by the subquery, instead of the
+ 	 * actual column name.
+ 	 */
+ 	if (is_subquery_var(node, context->scanrel, &relno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_REL_ALIAS_PREFIX, relno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1380 ----
      | 57
  (8 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);
+                                                                                             QUERY PLAN                                                                                            
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: 1
+    Relations: (public.ft4) FULL JOIN (public.ft5)
+    Remote SQL: SELECT NULL FROM ((SELECT NULL FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4 FULL JOIN (SELECT NULL FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5 ON (TRUE))
+ (4 rows)
+ 
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);
+  ?column? 
+ ----------
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+ (24 rows)
+ 
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
+ -- d. test deparsing the remote queries for simple foreign table scans in
+ -- an EPQ subplan of a foreign join in which the foreign tables are full
+ -- joined and in the remote join query the foreign tables are deparsed as
+ -- subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                             
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  LockRows
+    Output: "T 3".c1, ft4.c1, ft5.c1, "T 3".ctid, ft4.*, ft5.*
+    ->  Nested Loop
+          Output: "T 3".c1, ft4.c1, ft5.c1, "T 3".ctid, ft4.*, ft5.*
+          ->  Foreign Scan
+                Output: ft4.c1, ft4.*, ft5.c1, ft5.*
+                Relations: (public.ft4) FULL JOIN (public.ft5)
+                Remote SQL: SELECT s8.c1, s8.c2, s9.c1, s9.c2 FROM ((SELECT c1, ROW(c1, c2, c3) FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1, c2) FULL JOIN (SELECT c1, ROW(c1, c2, c3) FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1, c2) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL))) ORDER BY s8.c1 ASC NULLS LAST, s9.c1 ASC NULLS LAST
+                ->  Hash Full Join
+                      Output: ft4.c1, ft4.*, ft5.c1, ft5.*
+                      Hash Cond: (ft4.c1 = ft5.c1)
+                      Filter: ((ft4.c1 IS NULL) OR (ft4.c1 IS NOT NULL))
+                      ->  Foreign Scan on public.ft4
+                            Output: ft4.c1, ft4.*
+                            Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
+                      ->  Hash
+                            Output: ft5.c1, ft5.*
+                            ->  Foreign Scan on public.ft5
+                                  Output: ft5.c1, ft5.*
+                                  Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
+          ->  Materialize
+                Output: "T 3".c1, "T 3".ctid
+                ->  Seq Scan on "S 1"."T 3"
+                      Output: "T 3".c1, "T 3".ctid
+                      Filter: ("T 3".c1 = 50)
+ (25 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  50 | 52 |   
+  50 | 54 | 54
+  50 | 56 |   
+  50 | 58 |   
+  50 | 60 | 60
+  50 |    | 51
+  50 |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3187,3210 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,680 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Initialize info about whether to deparse this rel as a subquery. */
+ 	fpinfo->is_subquery_rel = false;
+ 	/* Initialize info about lower subqueries. */
+ 	fpinfo->lower_subquery_rels = NULL;
+ 	/* Set the relation index. */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 1239,1245 **** postgresGetForeignPlan(PlannerInfo *root,
  	initStringInfo(&sql);
  	deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
  							remote_conds, best_path->path.pathkeys,
! 							&retrieved_attrs, &params_list);
  
  	/*
  	 * Build the fdw_private list that will be available to the executor.
--- 1246,1252 ----
  	initStringInfo(&sql);
  	deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
  							remote_conds, best_path->path.pathkeys,
! 							false, &retrieved_attrs, &params_list);
  
  	/*
  	 * Build the fdw_private list that will be available to the executor.
***************
*** 2551,2558 **** estimate_path_cost_size(PlannerInfo *root,
  		initStringInfo(&sql);
  		appendStringInfoString(&sql, "EXPLAIN ");
  		deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
! 								remote_conds, pathkeys, &retrieved_attrs,
! 								NULL);
  
  		/* Get the remote estimate */
  		conn = GetConnection(fpinfo->user, false);
--- 2558,2565 ----
  		initStringInfo(&sql);
  		appendStringInfoString(&sql, "EXPLAIN ");
  		deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
! 								remote_conds, pathkeys, false,
! 								&retrieved_attrs, NULL);
  
  		/* Get the remote estimate */
  		conn = GetConnection(fpinfo->user, false);
***************
*** 4146,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->innerrel = innerrel;
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4153,4176 ----
  	fpinfo->innerrel = innerrel;
  	fpinfo->jointype = jointype;
  
+ 	/* Initialize info about whether to deparse this rel as a subquery. */
+ 	fpinfo->is_subquery_rel = false;
+ 
+ 	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use by the deparser.
+ 	 */
+ 	Assert(bms_is_subset(fpinfo_i->lower_subquery_rels, innerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_o->lower_subquery_rels, outerrel->relids));
+ 	fpinfo->lower_subquery_rels = bms_union(fpinfo_i->lower_subquery_rels,
+ 									  fpinfo_o->lower_subquery_rels);
+ 
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4181,4187 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4210,4238 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that the
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo so that the deparser can take appropriate action.
! 			 * We also save the relids of the base relations covered by the
! 			 * joining relation.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo_o->is_subquery_rel = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo_i->is_subquery_rel = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									innerrel->relids);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4313,4328 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the list when we are
+ 	 * called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 95,100 **** typedef struct PgFdwRelationInfo
--- 95,110 ----
  
  	/* Grouping information */
  	List	   *grouped_tlist;
+ 
+ 	/* Subquery information */
+ 	bool		is_subquery_rel;	/* do we deparse this as a subquery? */
+ 	Relids		lower_subquery_rels;	/* all relids appearing in lower
+ 										 * subqueries */
+ 	/*
+ 	 * Index of the relation. It is used for creating a subquery alias when
+ 	 * the is_subquery_rel is true.
+ 	 */
+ 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
***************
*** 159,168 **** extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
  extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
! extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
! 						RelOptInfo *foreignrel, List *tlist,
! 						List *remote_conds, List *pathkeys,
! 						List **retrieved_attrs, List **params_list);
  
  /* in shippable.c */
  extern bool is_builtin(Oid objectId);
--- 169,178 ----
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
  extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
! extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
! 						List *tlist, List *remote_conds, List *pathkeys,
! 						bool is_subquery, List **retrieved_attrs,
! 						List **params_list);
  
  /* in shippable.c */
  extern bool is_builtin(Oid objectId);
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,419 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- d. test deparsing the remote queries for simple foreign table scans in
+ -- an EPQ subplan of a foreign join in which the foreign tables are full
+ -- joined and in the remote join query the foreign tables are deparsed as
+ -- subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 813,824 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
postgres-fdw-phv-pushdown-v12.patchtext/x-patch; name=postgres-fdw-phv-pushdown-v12.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 49,54 ****
--- 49,55 ----
  #include "nodes/nodeFuncs.h"
  #include "nodes/plannodes.h"
  #include "optimizer/clauses.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/prep.h"
  #include "optimizer/tlist.h"
  #include "optimizer/var.h"
***************
*** 157,162 **** static void deparseRelabelType(RelabelType *node, deparse_expr_cxt *context);
--- 158,164 ----
  static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context);
  static void deparseNullTest(NullTest *node, deparse_expr_cxt *context);
  static void deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context);
+ static void deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context);
  static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
***************
*** 172,181 **** static void deparseFromExpr(List *quals, deparse_expr_cxt *context);
  static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
  				   RelOptInfo *foreignrel, List **params_list);
  static void appendSubqueryAlias(StringInfo buf, int relno, int ncols);
! static void get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
  							  int *relno, int *colno);
! static bool is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno,
! 						int *colno);
  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,
--- 174,184 ----
  static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
  				   RelOptInfo *foreignrel, List **params_list);
  static void appendSubqueryAlias(StringInfo buf, int relno, int ncols);
! static void get_relation_column_alias_ids(Expr *node, RelOptInfo *foreignrel,
  							  int *relno, int *colno);
! static bool is_subquery_expr(Expr *node, PlannerInfo *root, RelOptInfo *foreignrel,
! 				 int *relno, int *colno);
! 
  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,
***************
*** 766,771 **** foreign_expr_walker(Node *node,
--- 769,793 ----
  					state = FDW_COLLATE_UNSAFE;
  			}
  			break;
+ 		case T_PlaceHolderVar:
+ 			{
+ 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 				PlaceHolderInfo *phinfo = find_placeholder_info(glob_cxt->root,
+ 																phv, false);
+ 
+ 				/*
+ 				 * If the PHV's contained expression is computable on the
+ 				 * remote server, we consider the PHV safe to send.
+ 				 */
+ 				if (phv->phlevelsup == 0 &&
+ 					bms_is_subset(phinfo->ph_eval_at,
+ 								  glob_cxt->foreignrel->relids))
+ 					return foreign_expr_walker((Node *) phv->phexpr,
+ 											   glob_cxt, outer_cxt);
+ 				else
+ 					return false;
+ 			}
+ 			break;
  		default:
  
  			/*
***************
*** 860,866 **** deparse_type_name(Oid type_oid, int32 typemod)
   * foreign server.
   */
  List *
! build_tlist_to_deparse(RelOptInfo *foreignrel)
  {
  	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
--- 882,888 ----
   * foreign server.
   */
  List *
! build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel)
  {
  	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
***************
*** 873,887 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
  		return fpinfo->grouped_tlist;
  
  	/*
! 	 * We require columns specified in foreignrel->reltarget->exprs and those
! 	 * required for evaluating the local conditions.
  	 */
! 	tlist = add_to_flat_tlist(tlist,
! 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
! 									   PVC_RECURSE_PLACEHOLDERS));
! 	tlist = add_to_flat_tlist(tlist,
! 							  pull_var_clause((Node *) fpinfo->local_conds,
! 											  PVC_RECURSE_PLACEHOLDERS));
  
  	return tlist;
  }
--- 895,967 ----
  		return fpinfo->grouped_tlist;
  
  	/*
! 	 * Fetch all expressions in foreignrel's reltarget if the
! 	 * reltarget_is_shippable flag is set TRUE.  Otherwise, fetch shipplable
! 	 * expressions in the reltarget plus expressions required for evaluating
! 	 * non-shippable expressions in the reltarget.
  	 */
! 	if (fpinfo->reltarget_is_shippable)
! 		tlist = add_to_flat_tlist(tlist, foreignrel->reltarget->exprs);
! 	else
! 	{
! 		List	   *exprs = NIL;
! 		ListCell   *lc;
! 
! 		/* Note: we have at least one non-shippable PHV in the reltarget. */
! 		foreach(lc, foreignrel->reltarget->exprs)
! 		{
! 			Node	   *node = (Node *) lfirst(lc);
! 
! 			if (IsA(node, Var))
! 				exprs = lappend(exprs, node);
! 			else if (IsA(node, PlaceHolderVar))
! 			{
! 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 				PlaceHolderInfo *phinfo = find_placeholder_info(root, phv,
! 																false);
! 
! 				if (bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->outerrel->relids) ||
! 					bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->innerrel->relids))
! 				{
! 					/*
! 					 * The PHV coming from an either input should be shippable.
! 					 */
! 					exprs = lappend(exprs, node);
! 				}
! 				else
! 				{
! 					/*
! 					 * The PHV might be shippable, but in any case just fetch
! 					 * expressions required for evaluating the PHV.
! 					 */
! 					List	   *exprs2 = pull_var_clause(node,
! 													PVC_INCLUDE_PLACEHOLDERS);
! 					ListCell   *lc2;
! 
! 					foreach(lc2, exprs2)
! 					{
! 						Node	   *node2 = (Node *) lfirst(lc2);
! 
! 						exprs = lappend(exprs, node2);
! 					}
! 				}
! 			}
! 			else
! 				elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
! 		}
! 
! 		tlist = add_to_flat_tlist(tlist, exprs);
! 	}
! 
! 	/*
! 	 * Fetch expressions required for evaluating local conditions, if any.
! 	 */
! 	if (fpinfo->local_conds)
! 		tlist = add_to_flat_tlist(tlist,
! 								pull_var_clause((Node *) fpinfo->local_conds,
! 												PVC_INCLUDE_PLACEHOLDERS));
  
  	return tlist;
  }
***************
*** 1447,1452 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1527,1533 ----
  
  	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
  		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->reltarget_is_shippable);
  	Assert(fpinfo->local_conds == NIL);
  
  	/* If is_subquery_rel is true, deparse the relation as a subquery. */
***************
*** 1456,1468 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  		List	   *retrieved_attrs;
  
  		/* Build a tlist from the subquery. */
! 		tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
! 		 * Since (1) the expressions in foreignrel's reltarget doesn't contain
! 		 * any PHVs and (2) foreignrel's local_conds is empty, the tlist
! 		 * created by build_tlist_to_deparse must be one-to-one with the
! 		 * expressions.
  		 */
  		Assert(list_length(tlist) == list_length(foreignrel->reltarget->exprs));
  
--- 1537,1549 ----
  		List	   *retrieved_attrs;
  
  		/* Build a tlist from the subquery. */
! 		tlist = build_tlist_to_deparse(root, foreignrel);
  
  		/*
! 		 * Since (1) the relation's reltarget_is_shippable is true and (2) the
! 		 * relation's local_conds is empty, the tlist created by
! 		 * build_tlist_to_deparse must be one-to-one with the relation's
! 		 * reltarget.
  		 */
  		Assert(list_length(tlist) == list_length(foreignrel->reltarget->exprs));
  
***************
*** 1513,1529 **** appendSubqueryAlias(StringInfo buf, int relno, int ncols)
  }
  
  /*
!  * Get the relation and column alias IDs of a given Var node, which belongs
   * to input foreignrel. They are returned in *relno and *colno respectively.
   */
  static void
! get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
  							  int *relno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	int			i;
  	ListCell   *lc;
  
  	/* Get the relation alias ID */
  	*relno = fpinfo->relation_index;
  
--- 1594,1612 ----
  }
  
  /*
!  * Get the relation and column alias IDs of a given Var or PHV, which belongs
   * to input foreignrel. They are returned in *relno and *colno respectively.
   */
  static void
! get_relation_column_alias_ids(Expr *node, RelOptInfo *foreignrel,
  							  int *relno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	int			i;
  	ListCell   *lc;
  
+ 	Assert(IsA(node, Var) || IsA(node, PlaceHolderVar));
+ 
  	/* Get the relation alias ID */
  	*relno = fpinfo->relation_index;
  
***************
*** 1544,1568 **** get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
  }
  
  /*
!  * Returns true if a given Var node belongs to a relation being deparsed as a
!  * subquery. Returns false otherwise. When returning true, it sets *relno and
!  * *colno to the relation and column alias IDs of the given Var node,
   * respectively.
   */
  static bool
! is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
  	RelOptInfo *innerrel = fpinfo->innerrel;
  
  	if (foreignrel->reloptkind != RELOPT_JOINREL)
  		return false;
  
! 	if (!bms_is_member(node->varno, fpinfo->lower_subquery_rels))
! 		return false;
  
! 	if (bms_is_member(node->varno, outerrel->relids))
  	{
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
  
--- 1627,1689 ----
  }
  
  /*
!  * Returns true if a given Var or PHV belongs to a relation being deparsed as
!  * a subquery. Returns false otherwise. When returning true, it sets *relno
!  * and *colno to the relation and column alias IDs of the given Var or PHV,
   * respectively.
   */
  static bool
! is_subquery_expr(Expr *node, PlannerInfo *root, RelOptInfo *foreignrel,
! 				 int *relno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
  	RelOptInfo *innerrel = fpinfo->innerrel;
+ 	bool		is_outer_var;
  
  	if (foreignrel->reloptkind != RELOPT_JOINREL)
  		return false;
  
! 	if (IsA(node, Var))
! 	{
! 		Var		   *var = (Var *) node;
  
! 		/*
! 		 * The Var can't be a subquery output column if it doesn't belong to
! 		 * any relation being deparsed as a subquery.
! 		 */
! 		if (!bms_is_member(var->varno, fpinfo->lower_subquery_rels))
! 			return false;
! 
! 		is_outer_var = bms_is_member(var->varno, outerrel->relids);
! 	}
! 	else if (IsA(node, PlaceHolderVar))
! 	{
! 		PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 		PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
! 		bool		is_inner_var;
! 
! 		/*
! 		 * The PHV can't be a subquery output column if it doesn't belong to
! 		 * any relation being deparsed as a subquery.
! 		 */
! 		if (!bms_is_subset(phinfo->ph_eval_at, fpinfo->lower_subquery_rels))
! 			return false;
! 
! 		is_outer_var = bms_is_subset(phinfo->ph_eval_at, outerrel->relids);
! 		is_inner_var = bms_is_subset(phinfo->ph_eval_at, innerrel->relids);
! 
! 		/*
! 		 * The PHV computed here but not in either input isn't a subquery
! 		 * output column.
! 		 */
! 		if (!is_outer_var && !is_inner_var)
! 			return false;
! 	}
! 	else
! 		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
! 
! 	if (is_outer_var)
  	{
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
  
***************
*** 1577,1590 **** is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
  		}
  
  		/* Otherwise, recurse into the outer relation. */
! 		return is_subquery_var(node, outerrel, relno, colno);
  	}
  	else
  	{
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
  
- 		Assert(bms_is_member(node->varno, innerrel->relids));
- 
  		/*
  		 * If inner relation is deparsed as a subquery, get the identifiers for
  		 * the relation and column alias to the given Var node.
--- 1698,1709 ----
  		}
  
  		/* Otherwise, recurse into the outer relation. */
! 		return is_subquery_expr(node, root, outerrel, relno, colno);
  	}
  	else
  	{
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
  
  		/*
  		 * If inner relation is deparsed as a subquery, get the identifiers for
  		 * the relation and column alias to the given Var node.
***************
*** 1596,1602 **** is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
  		}
  
  		/* Otherwise, recurse into the inner relation. */
! 		return is_subquery_var(node, innerrel, relno, colno);
  	}
  }
  
--- 1715,1721 ----
  		}
  
  		/* Otherwise, recurse into the inner relation. */
! 		return is_subquery_expr(node, root, innerrel, relno, colno);
  	}
  }
  
***************
*** 1981,1989 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.  However, we must be
! 		 * careful; the table could be beneath an outer join, in which case it
! 		 * must go to NULL whenever the rest of the row does.
  		 */
  		Oid			fetchval = 0;
  
--- 2100,2106 ----
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.
  		 */
  		Oid			fetchval = 0;
  
***************
*** 1993,2006 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  			fetchval = rte->relid;
  		}
  
! 		if (qualify_col)
! 		{
! 			appendStringInfoString(buf, "CASE WHEN (");
! 			ADD_REL_QUALIFIER(buf, varno);
! 			appendStringInfo(buf, "*)::text IS NOT NULL THEN %u END", fetchval);
! 		}
! 		else
! 			appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
--- 2110,2116 ----
  			fetchval = rte->relid;
  		}
  
! 		appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
***************
*** 2031,2058 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  		attrs_used = bms_add_member(NULL,
  									0 - FirstLowInvalidHeapAttributeNumber);
  
- 		/*
- 		 * In case the whole-row reference is under an outer join then it has
- 		 * to go NULL whenever the rest of the row goes NULL. Deparsing a join
- 		 * query would always involve multiple relations, thus qualify_col
- 		 * would be true.
- 		 */
- 		if (qualify_col)
- 		{
- 			appendStringInfoString(buf, "CASE WHEN (");
- 			ADD_REL_QUALIFIER(buf, varno);
- 			appendStringInfo(buf, "*)::text IS NOT NULL THEN ");
- 		}
- 
  		appendStringInfoString(buf, "ROW(");
  		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
  						  &retrieved_attrs);
  		appendStringInfoString(buf, ")");
  
- 		/* Complete the CASE WHEN statement started above. */
- 		if (qualify_col)
- 			appendStringInfo(buf, " END");
- 
  		heap_close(rel, NoLock);
  		bms_free(attrs_used);
  	}
--- 2141,2151 ----
***************
*** 2225,2230 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 2318,2326 ----
  		case T_Aggref:
  			deparseAggref((Aggref *) node, context);
  			break;
+ 		case T_PlaceHolderVar:
+ 			deparseExprInPlaceHolderVar((PlaceHolderVar *) node, context);
+ 			break;
  		default:
  			elog(ERROR, "unsupported expression type for deparse: %d",
  				 (int) nodeTag(node));
***************
*** 2255,2261 **** deparseVar(Var *node, deparse_expr_cxt *context)
  	 * relation and column alias provided by the subquery, instead of the
  	 * actual column name.
  	 */
! 	if (is_subquery_var(node, context->scanrel, &relno, &colno))
  	{
  		appendStringInfo(context->buf, "%s%d.%s%d",
  						 SS_REL_ALIAS_PREFIX, relno,
--- 2351,2358 ----
  	 * relation and column alias provided by the subquery, instead of the
  	 * actual column name.
  	 */
! 	if (is_subquery_expr((Expr *) node, context->root, context->scanrel,
! 						 &relno, &colno))
  	{
  		appendStringInfo(context->buf, "%s%d.%s%d",
  						 SS_REL_ALIAS_PREFIX, relno,
***************
*** 2949,2954 **** appendAggOrderBy(List *orderList, List *targetList, deparse_expr_cxt *context)
--- 3046,3076 ----
  }
  
  /*
+  * Deparse a PlaceHolderVar's contained expression.
+  */
+ static void
+ deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context)
+ {
+ 	int			relno;
+ 	int			colno;
+ 
+ 	/*
+ 	 * If the given PHV belongs to a relation deparsed as a subquery, use the
+ 	 * relation and column alias provided by the subquery instead.
+ 	 */
+ 	if (is_subquery_expr((Expr *) node, context->root, context->scanrel,
+ 						 &relno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SS_REL_ALIAS_PREFIX, relno,
+ 						 SS_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
+ 	deparseExpr(node->phexpr, context);
+ }
+ 
+ /*
   * Print the representation of a parameter to be sent to the remote side.
   *
   * Note: we always label the Param's type explicitly rather than relying on
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1634,1641 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1634,1641 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                         QUERY PLAN                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1643,1649 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1643,1649 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1678,1685 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1678,1685 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                              QUERY PLAN                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1687,1693 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1687,1693 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1723,1730 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                               
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1723,1730 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                        QUERY PLAN                                                                                                                                                                        
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1732,1738 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1732,1738 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1767,1774 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1767,1774 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                             QUERY PLAN                                                                                                                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1776,1782 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1776,1782 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1846,1859 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1846,1859 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                     QUERY PLAN                                                                                                                                                                     
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s1.c4, s2.c1 FROM ((SELECT ctid, ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1", c3 FROM "S 1"."T 1") s1(c1, c2, c3, c4) INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c3 = s2.c2)))) ORDER BY s1.c4 ASC NULLS LAST, s1.c3 ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 2182,2206 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
     1
  (10 rows)
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                         QUERY PLAN                                                         
! ---------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: (13), ft2.c1
!    Join Filter: (13 = ft2.c1)
!    ->  Foreign Scan on public.ft2
!          Output: ft2.c1
!          Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (("C 1" >= 10)) AND (("C 1" <= 15)) ORDER BY "C 1" ASC NULLS LAST
!    ->  Materialize
!          Output: (13)
!          ->  Foreign Scan on public.ft1
!                Output: 13
!                Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 13))
! (11 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
--- 2182,2197 ----
     1
  (10 rows)
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                                                               QUERY PLAN                                                                                              
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: (13), ft2.c1
!    Relations: (public.ft2) LEFT JOIN (public.ft1)
!    Remote SQL: SELECT r2."C 1", s4.c1 FROM ("S 1"."T 1" r2 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s4(c1) ON (((13 = r2."C 1")))) WHERE ((r2."C 1" >= 10)) AND ((r2."C 1" <= 15))
! (4 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
***************
*** 2213,2236 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
      | 15
  (6 rows)
  
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                     QUERY PLAN                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Join Filter: (ft4.c1 = ft1.c1)
!    ->  Foreign Scan on public.ft4
!          Output: ft4.c1, ft4.c2, ft4.c3
!          Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 10)) AND ((c1 <= 15))
!    ->  Materialize
!          Output: ft1.c1, ft2.c1, (13)
!          ->  Foreign Scan
!                Output: ft1.c1, ft2.c1, 13
!                Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
! (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
--- 2204,2218 ----
      | 15
  (6 rows)
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                                                                           QUERY PLAN                                                                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Relations: (public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2))
!    Remote SQL: SELECT r1.c1, s7.c1, s7.c2, s7.c3 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4."C 1", r5."C 1", 13 FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12))))) s7(c1, c2, c3) ON (((r1.c1 = s7.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
! (4 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
***************
*** 2240,2255 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
   14 |    |    |   
  (3 rows)
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                 QUERY PLAN                                                                                                                                 
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2222,2302 ----
   14 |    |    |   
  (3 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                               QUERY PLAN                                                                                                                                                                               
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft2.c1, (13), (13), ft2_1.c1
+    Relations: (public.ft2) LEFT JOIN ((public.ft2) LEFT JOIN (public.ft1))
+    Remote SQL: SELECT r1."C 1", s8.c1, s8.c2, s8.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r5."C 1", 13, s7.c1 FROM ("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s7(c1) ON (((13 = r5."C 1")))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))) s8(c1, c2, c3) ON (((r1."C 1" = 13)))) WHERE ((r1."C 1" >= 10)) AND ((r1."C 1" <= 15))
+ (4 rows)
+ 
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 
+ ----+----+----+----
+  10 |    |    |   
+  11 |    |    |   
+  12 |    |    |   
+  13 | 13 |    | 10
+  13 | 13 |    | 11
+  13 | 13 |    | 12
+  13 | 13 | 13 | 13
+  13 | 13 |    | 14
+  13 | 13 |    | 15
+  14 |    |    |   
+  15 |    |    |   
+ (11 rows)
+ 
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+                                                                                                                             QUERY PLAN                                                                                                                            
+ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft2.c1, 13, (13), ft2_1.c1
+    Relations: ((public.ft2) LEFT JOIN (public.ft1)) LEFT JOIN (public.ft2)
+    Remote SQL: SELECT r5."C 1", r1."C 1", s7.c1 FROM (("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s7(c1) ON (((13 = r5."C 1")))) LEFT JOIN "S 1"."T 1" r1 ON (((r1."C 1" = 13)))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))
+ (4 rows)
+ 
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+  c1 | a2 | b2 | c2 
+ ----+----+----+----
+  13 | 13 |    | 10
+  13 | 13 |    | 11
+  13 | 13 |    | 12
+  13 | 13 | 13 | 13
+  13 | 13 |    | 14
+  13 | 13 |    | 15
+ (6 rows)
+ 
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                                                                                                 QUERY PLAN                                                                                                                                                                                                                                                
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ((13 IS NOT NULL)), ft4_1.c1, (13), ft1.c1, ft2.c1
+    Relations: (public.ft4) LEFT JOIN ((public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2)))
+    Remote SQL: SELECT r1.c1, s11.c1, s11.c2, s11.c3, s11.c4, s11.c5 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4.c1, s10.c1, s10.c2, (s10.c3 IS NOT NULL), s10.c3 FROM ("S 1"."T 3" r4 LEFT JOIN (SELECT r7."C 1", r8."C 1", 13 FROM ("S 1"."T 1" r7 INNER JOIN "S 1"."T 1" r8 ON (((r8."C 1" = 12)) AND ((r7."C 1" = 12))))) s10(c1, c2, c3) ON (((r4.c1 = s10.c1)))) WHERE ((r4.c1 >= 10)) AND ((r4.c1 <= 15))) s11(c1, c2, c3, c4, c5) ON (((r1.c1 = s11.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
+ (4 rows)
+ 
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 | d2 | e2 
+ ----+----+----+----+----+----
+  10 | f  | 10 |    |    |   
+  12 | t  | 12 | 13 | 12 | 12
+  14 | f  | 14 |    |    |   
+ (3 rows)
+ 
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                               QUERY PLAN                                                                                                                              
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s1.c4, r2.c1, r2.c2 FROM ((SELECT ROW(c1, c2, c3), c1, c2, c3 FROM "S 1"."T 4") s1(c1, c2, c3, c4) INNER JOIN "S 1"."T 3" r2 ON (((s1.c2 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY s1.c2 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 4151,4164 **** 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)
--- 4198,4211 ----
  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)
***************
*** 4294,4307 **** 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
!                                                                                                                               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)
--- 4341,4354 ----
  
  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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 28,33 ****
--- 28,34 ----
  #include "optimizer/clauses.h"
  #include "optimizer/pathnode.h"
  #include "optimizer/paths.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/planmain.h"
  #include "optimizer/restrictinfo.h"
  #include "optimizer/var.h"
***************
*** 675,680 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 676,741 ----
  	fpinfo->lower_subquery_rels = NULL;
  	/* Set the relation index. */
  	fpinfo->relation_index = baserel->relid;
+ 
+ 	/*
+ 	 * Detect whether expressions in the relation's reltarget are shippable
+ 	 * and whether there are any non-Vars in those expressions.
+ 	 *
+ 	 * Note: whole-row Vars and system columns other than ctid and oid are
+ 	 * treated like PHVs.  (See comments in postgres_fdw.h.)
+ 	 *
+ 	 * Note: if the relation is an appendrel child, it isn't in the main join
+ 	 * tree, so those information is not used.
+ 	 */
+ 	if (baserel->reloptkind == RELOPT_BASEREL)
+ 	{
+ 		bool		reltarget_is_shippable = true;
+ 		bool		reltarget_has_non_vars = false;
+ 
+ 		/*
+ 		 * Check to see if any PHVs are requested from the baserel.
+ 		 */
+ 		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))
+ 			{
+ 				reltarget_has_non_vars = true;
+ 
+ 				if (!is_foreign_expr(root, baserel, (Expr *) phinfo->ph_var))
+ 				{
+ 					reltarget_is_shippable = false;
+ 					break;
+ 				}
+ 			}
+ 		}
+ 
+ 		/*
+ 		 * If not, check to see if the baserel is required to emit any
+ 		 * whole-row Vars or system columns other than ctid and oid.
+ 		 */
+ 		if (!reltarget_has_non_vars)
+ 		{
+ 			int			i;
+ 
+ 			Assert(reltarget_is_shippable);
+ 
+ 			for (i = baserel->min_attr; i <= 0; i++)
+ 			{
+ 				if (i == SelfItemPointerAttributeNumber ||
+ 					i == ObjectIdAttributeNumber)
+ 					continue;
+ 
+ 				if (!bms_is_empty(baserel->attr_needed[i - baserel->min_attr]))
+ 					reltarget_has_non_vars = true;
+ 			}
+ 		}
+ 
+ 		fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 		fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 	}
  }
  
  /*
***************
*** 1198,1204 **** postgresGetForeignPlan(PlannerInfo *root,
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
--- 1259,1265 ----
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
***************
*** 2538,2544 **** estimate_path_cost_size(PlannerInfo *root,
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL ||
  			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 			fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
--- 2599,2605 ----
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL ||
  			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 			fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
***************
*** 4054,4059 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4115,4122 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	bool		reltarget_is_shippable = true;
+ 	bool		reltarget_has_non_vars = false;
  
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
***************
*** 4083,4088 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4146,4159 ----
  	if (fpinfo_o->local_conds || fpinfo_i->local_conds)
  		return false;
  
+ 	/*
+ 	 * If the reltargets of joining relations are not shippable, those are
+ 	 * required to be evaluated locally, so the join can not be pushed down.
+ 	 */
+ 	if (!fpinfo_o->reltarget_is_shippable ||
+ 		!fpinfo_i->reltarget_is_shippable)
+ 		return false;
+ 
  	/* Separate restrict list into join quals and quals on join relation */
  	if (IS_OUTER_JOIN(jointype))
  		extract_actual_join_clauses(extra->restrictlist, &joinclauses, &otherclauses);
***************
*** 4108,4133 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			return false;
  	}
  
- 	/*
- 	 * deparseExplicitTargetList() isn't smart enough to handle anything other
- 	 * than a Var.  In particular, if there's some PlaceHolderVar that would
- 	 * need to be evaluated within this join tree (because there's an upper
- 	 * reference to a quantity that may go to NULL as a result of an outer
- 	 * join), then we can't try to push the join down because we'll fail when
- 	 * we get to deparseExplicitTargetList().  However, a PlaceHolderVar that
- 	 * needs to be evaluated *at the top* of this join tree is OK, because we
- 	 * can do that locally after fetching the results from the remote side.
- 	 */
- 	foreach(lc, root->placeholder_list)
- 	{
- 		PlaceHolderInfo *phinfo = lfirst(lc);
- 		Relids		relids = joinrel->relids;
- 
- 		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
- 			bms_nonempty_difference(relids, phinfo->ph_eval_at))
- 			return false;
- 	}
- 
  	/* Save the join clauses, for later use. */
  	fpinfo->joinclauses = joinclauses;
  
--- 4179,4184 ----
***************
*** 4185,4211 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
  	 */
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
--- 4236,4286 ----
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
+ 	 *
+ 	 * If the joining relation has reltarget_has_non_vars=TRUE, then we
+ 	 * deparse that relation as a subquery in which the remote_conds of that
+ 	 * relation is transformed into the WHERE clause.  So in that case, don't
+ 	 * pull up the remote_conds into the joinclauses or remote_conds of this
+ 	 * relation.
  	 */
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo_i->is_subquery_rel = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo_o->is_subquery_rel = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo_i->is_subquery_rel = true;
! 			else
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo_o->is_subquery_rel = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo_o->is_subquery_rel = true;
! 			else
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo_i->is_subquery_rel = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
***************
*** 4216,4238 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			 * we need to deparse that relation as a subquery so that the
  			 * conditions can be evaluated before the join.  Remember it in
  			 * the fpinfo so that the deparser can take appropriate action.
- 			 * We also save the relids of the base relations covered by the
- 			 * joining relation.
  			 */
! 			if (fpinfo_o->remote_conds)
! 			{
  				fpinfo_o->is_subquery_rel = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
  				fpinfo_i->is_subquery_rel = true;
- 				fpinfo->lower_subquery_rels =
- 					bms_add_members(fpinfo->lower_subquery_rels,
- 									innerrel->relids);
- 			}
  			break;
  
  		default:
--- 4291,4301 ----
  			 * we need to deparse that relation as a subquery so that the
  			 * conditions can be evaluated before the join.  Remember it in
  			 * the fpinfo so that the deparser can take appropriate action.
  			 */
! 			if (fpinfo_o->reltarget_has_non_vars || fpinfo_o->remote_conds)
  				fpinfo_o->is_subquery_rel = true;
! 			if (fpinfo_i->reltarget_has_non_vars || fpinfo_i->remote_conds)
  				fpinfo_i->is_subquery_rel = true;
  			break;
  
  		default:
***************
*** 4241,4246 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4304,4322 ----
  	}
  
  	/*
+ 	 * If the joining relation is deparsed as a subquery, save the relids of
+ 	 * base relations covered by the subquery for later use by the deparser.
+ 	 */
+ 	if (fpinfo_o->is_subquery_rel)
+ 		fpinfo->lower_subquery_rels =
+ 			bms_add_members(fpinfo->lower_subquery_rels,
+ 							outerrel->relids);
+ 	if (fpinfo_i->is_subquery_rel)
+ 		fpinfo->lower_subquery_rels =
+ 			bms_add_members(fpinfo->lower_subquery_rels,
+ 							innerrel->relids);
+ 
+ 	/*
  	 * For an inner join, all restrictions can be treated alike. Treating the
  	 * pushed down conditions as join conditions allows a top level full outer
  	 * join to be deparsed without requiring subqueries.
***************
*** 4256,4261 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4332,4372 ----
  	fpinfo->pushdown_safe = true;
  
  	/*
+ 	 * Detect whether expressions in the relation's reltarget are shippable
+ 	 * and whether there are any non-Vars in those expressions.
+ 	 *
+ 	 * Note: whole-row Vars and system columns other than ctid and oid of a
+ 	 * base relation are handled as output columns of a lower subquery.  So
+ 	 * no need to be careful about those columns.
+ 	 */
+ 	foreach(lc, joinrel->reltarget->exprs)
+ 	{
+ 		Node	   *node = (Node *) lfirst(lc);
+ 
+ 		if (IsA(node, PlaceHolderVar))
+ 		{
+ 			PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 			PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
+ 
+ 			/* Ignore the PHV if it has bubbled up from an either input. */
+ 			if (bms_is_subset(phinfo->ph_eval_at, outerrel->relids) ||
+ 				bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
+ 				continue;
+ 
+ 			reltarget_has_non_vars = true;
+ 
+ 			if (!is_foreign_expr(root, joinrel, (Expr *) node))
+ 			{
+ 				reltarget_is_shippable = false;
+ 				break;
+ 			}
+ 		}
+ 	}
+ 
+ 	fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 	fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 
+ 	/*
  	 * If user is willing to estimate cost for a scan of either of the joining
  	 * relations using EXPLAIN, he intends to estimate scans on that relation
  	 * more accurately. Then, it makes sense to estimate the cost the join
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 33,38 **** typedef struct PgFdwRelationInfo
--- 33,61 ----
  	bool		pushdown_safe;
  
  	/*
+ 	 * Flags on the reltarget of the relation.
+ 	 *
+ 	 * If each expression in the relation's reltarget is shippable (i.e.,
+ 	 * computable on the remote side), we set reltarget_is_shippalbe to TRUE.
+ 	 * The flag is used to determine whether the relation can be joined with
+ 	 * any other foreign table (or join) on the remote side.  Note that Vars
+ 	 * in the reltarget are shippable, so if any PHV in the reltarget is
+ 	 * shippable, then the flag is set TRUE.  Also, if there are any PHVs in
+ 	 * in the reltarget, we set reltarget_has_non_vars to TRUE.  In that case,
+ 	 * we deparse the relation as a subquery emitting the PHVs when creating
+ 	 * a remote query for doing a join of the relation with any other foreign
+ 	 * table remotely.  Note that whole-row Vars and system columns other than
+ 	 * ctid and oid in the reltarget of a base relation are treated like PHVs,
+ 	 * because (1) we deparse the whole-row Vars as ROW() expressions and the
+ 	 * system columns as 0, except for tableoid, in which case it is deparsed
+ 	 * as a valid value for the local table OID, and (2) the base table could
+ 	 * be beneath an outer join, in which case those columns must go to NULL
+ 	 * whenever the rest of the row does.
+ 	 */
+ 	bool		reltarget_is_shippable; /* are reltarget exprs shippable? */
+ 	bool		reltarget_has_non_vars;	/* are there any non-Var exprs? */
+ 
+ 	/*
  	 * Restriction clauses, divided into safe and unsafe to pushdown subsets.
  	 *
  	 * For a base foreign relation this is a list of clauses along-with
***************
*** 168,174 **** extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  						List *tlist, List *remote_conds, List *pathkeys,
  						bool is_subquery, List **retrieved_attrs,
--- 191,197 ----
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  						List *tlist, List *remote_conds, List *pathkeys,
  						bool is_subquery, List **retrieved_attrs,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 513,529 **** EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
- 
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
--- 513,534 ----
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
#66Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#65)
Re: Push down more full joins in postgres_fdw

On Mon, Nov 28, 2016 at 3:52 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/11/24 21:45, Etsuro Fujita wrote:

On 2016/11/22 18:28, Ashutosh Bapat wrote:

The comments should explain why is the assertion true.
+        /* Shouldn't be NIL */
+        Assert(tlist != NIL);

I noticed that I was wrong; in the Assertion the tlist can be empty. An
example for such a case is:

SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);

In this example any columns of the relations ft4 and ft5 wouldn't be
needed for the join or the final output, so both the tlists for the
relations created in deparseRangeTblRef would be empty. Attached is an
updated version fixing this bug. Changes are:

* I removed make_outerrel_subquery/make_innerrel_subquery from fpinfo
and added a new member "is_subquery_rel" to fpinfo, as a flag on whether
to deparse the relation as a subquery.

Replacing make_outerrel_subquery/make_innerrel_subquery with is_subquery_rel
seems to be a bad idea. Whether a relation needs to be deparsed as a subquery
or not depends upon the relation which joins it with another relation. Take for
example a case when a join ABC can pull up the conditions on AB, but ABD can
not. In this case we will mark that AB in ABD needs to be deparsed as a
subquery but will not mark so in ABC. So, if we choose to join ABCD as (ABC)D
we don't need AB to be deparsed as a subquery. If we choose to join ABCD as
(ABD)C, we will need AB to deparsed as a subquery. Regression doesn't have a
testcase like that hence the code seems to be working.

Some more comments
1. The output of the following query
+SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);
produces 21 rows all containing a "1". That's not quite good use of expected
output space, given that all that the test tests is the empty
targetlists are deparsed
correctly. You might want to either just test EXPLAIN output or make "between 50
and 60" condition more stringent to reduce the number of rows output.

2. Got lost here. I guess, all you need to say is "test deparsing FOR UPDATE
clause with subqueries.". Anyway, the sentence is very hard to read and needs
simplification.
+-- d. test deparsing the remote queries for simple foreign table scans in
+-- an EPQ subplan of a foreign join in which the foreign tables are full
+-- joined and in the remote join query the foreign tables are deparsed as
+-- subqueries
3. Why is foreignrel variable changed to rel?
-extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
-                        RelOptInfo *foreignrel, List *tlist,
-                        List *remote_conds, List *pathkeys,
-                        List **retrieved_attrs, List **params_list);
+extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo
*root, RelOptInfo *rel,
+                        List *tlist, List *remote_conds, List *pathkeys,
+                        bool is_subquery, List **retrieved_attrs,
+                        List **params_list);
4. I am still not happy with this change
+        /*
+         * Since (1) the expressions in foreignrel's reltarget doesn't contain
+         * any PHVs and (2) foreignrel's local_conds is empty, the tlist
+         * created by build_tlist_to_deparse must be one-to-one with the
+         * expressions.
+         */
+        Assert(list_length(tlist) ==
list_length(foreignrel->reltarget->exprs));
the assertion only checks that the number of elements in both the lists are
same but does not check whether those lists are same i.e. they contain the same
elements in the same order. This equality is crucial to deparsing logic. If
somehow build_tlist_to_deparse() breaks that assumption in future, we have no
way to detect it, unless a regression test fails.

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

#67Robert Haas
robertmhaas@gmail.com
In reply to: Ashutosh Bapat (#66)
Re: Push down more full joins in postgres_fdw

On Mon, Dec 5, 2016 at 6:20 AM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

4. I am still not happy with this change
+        /*
+         * Since (1) the expressions in foreignrel's reltarget doesn't contain
+         * any PHVs and (2) foreignrel's local_conds is empty, the tlist
+         * created by build_tlist_to_deparse must be one-to-one with the
+         * expressions.
+         */
+        Assert(list_length(tlist) ==
list_length(foreignrel->reltarget->exprs));
the assertion only checks that the number of elements in both the lists are
same but does not check whether those lists are same i.e. they contain the same
elements in the same order. This equality is crucial to deparsing logic. If
somehow build_tlist_to_deparse() breaks that assumption in future, we have no
way to detect it, unless a regression test fails.

If there's an easy way to do a more exact comparison, great. But if
we can't get an awesome Assert(), a helpful Assert() is still better
than a kick in the head.

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

#68Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Robert Haas (#67)
Re: Push down more full joins in postgres_fdw

On 2016/12/07 5:27, Robert Haas wrote:

On Mon, Dec 5, 2016 at 6:20 AM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

4. I am still not happy with this change
+        /*
+         * Since (1) the expressions in foreignrel's reltarget doesn't contain
+         * any PHVs and (2) foreignrel's local_conds is empty, the tlist
+         * created by build_tlist_to_deparse must be one-to-one with the
+         * expressions.
+         */
+        Assert(list_length(tlist) ==
list_length(foreignrel->reltarget->exprs));
the assertion only checks that the number of elements in both the lists are
same but does not check whether those lists are same i.e. they contain the same
elements in the same order. This equality is crucial to deparsing logic. If
somehow build_tlist_to_deparse() breaks that assumption in future, we have no
way to detect it, unless a regression test fails.

If there's an easy way to do a more exact comparison, great.

Ashutosh proposed this to do the comparison:

On 2016/11/22 18:28, Ashutosh Bapat wrote:

I guess, the reason why you are doing it this way, is SELECT clause for the
outermost query gets deparsed before FROM clause. For later we call
deparseRangeTblRef(), which builds the tlist. So, while deparsing SELECT
clause, we do not have tlist to build from. In that case, I guess, we have to
build the tlist in get_subselect_alias_id() if it's not available and stick it
in fpinfo. Subsequent calls to get_subselect_alias_id() should find tlist
there. Then in deparseRangeTblRef() assert that there's a tlist in fpinfo
and use it to build the SELECT clause of subquery. That way, we don't build
tlist unless it's needed and also use the same tlist for all searches. Please
use tlist_member() to search into the tlist.

This would probably work, but seems to me a bit complicated. Instead,
I'd like to propose that we build the tlist for each relation being
deparsed as a subquery in a given join tree, right before deparsing the
SELECT clause in deparseSelectStmtForRel, if is_subquery is false and
lower_subquery_rels isn't NULL, and store the tlist into the relation's
fpinfo. That would allow us to build the tlist only when we need it,
and to use tlist_member for the exact comparison. I think it would be
much easier to implement that.

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

#69Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Robert Haas (#67)
Re: Push down more full joins in postgres_fdw

On Wed, Dec 7, 2016 at 1:57 AM, Robert Haas <robertmhaas@gmail.com> wrote:

On Mon, Dec 5, 2016 at 6:20 AM, Ashutosh Bapat
<ashutosh.bapat@enterprisedb.com> wrote:

4. I am still not happy with this change
+        /*
+         * Since (1) the expressions in foreignrel's reltarget doesn't contain
+         * any PHVs and (2) foreignrel's local_conds is empty, the tlist
+         * created by build_tlist_to_deparse must be one-to-one with the
+         * expressions.
+         */
+        Assert(list_length(tlist) ==
list_length(foreignrel->reltarget->exprs));
the assertion only checks that the number of elements in both the lists are
same but does not check whether those lists are same i.e. they contain the same
elements in the same order. This equality is crucial to deparsing logic. If
somehow build_tlist_to_deparse() breaks that assumption in future, we have no
way to detect it, unless a regression test fails.

If there's an easy way to do a more exact comparison, great. But if
we can't get an awesome Assert(), a helpful Assert() is still better
than a kick in the head.

The assert is not a problem in itself, but the reason we have to add
the assert. The problem is explained in [1]/messages/by-id/CAFjFpRfwoSsJr9418b2jA7g0nwagjZSWhPQPUmK9M6z5XSOAqQ@mail.gmail.com, point #9.

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

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

#70Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#68)
Re: Push down more full joins in postgres_fdw

Ashutosh proposed this to do the comparison:

On 2016/11/22 18:28, Ashutosh Bapat wrote:

I guess, the reason why you are doing it this way, is SELECT clause for
the
outermost query gets deparsed before FROM clause. For later we call
deparseRangeTblRef(), which builds the tlist. So, while deparsing SELECT
clause, we do not have tlist to build from. In that case, I guess, we have
to
build the tlist in get_subselect_alias_id() if it's not available and
stick it
in fpinfo. Subsequent calls to get_subselect_alias_id() should find tlist
there. Then in deparseRangeTblRef() assert that there's a tlist in fpinfo
and use it to build the SELECT clause of subquery. That way, we don't
build
tlist unless it's needed and also use the same tlist for all searches.
Please
use tlist_member() to search into the tlist.

This would probably work, but seems to me a bit complicated. Instead, I'd
like to propose that we build the tlist for each relation being deparsed as
a subquery in a given join tree, right before deparsing the SELECT clause in
deparseSelectStmtForRel, if is_subquery is false and lower_subquery_rels
isn't NULL, and store the tlist into the relation's fpinfo. That would
allow us to build the tlist only when we need it, and to use tlist_member
for the exact comparison. I think it would be much easier to implement
that.

IIRC, this is inline with my original proposal of creating tlists
before deparsing anything. Along-with that I also proposed to bubble
this array of tlist up the join hierarchy to avoid recursion [1]/messages/by-id/ad449b25-66ee-4c06-568c-0eb2e1bef9f9@lab.ntt.co.jp point
#15, further elaborated in [2]/messages/by-id/CAFjFpRcn7=DNOXm-PJ_jVDqAmghKVf6tApQC+_RgMZ8tOPExcA@mail.gmail.com

[1]: /messages/by-id/ad449b25-66ee-4c06-568c-0eb2e1bef9f9@lab.ntt.co.jp
[2]: /messages/by-id/CAFjFpRcn7=DNOXm-PJ_jVDqAmghKVf6tApQC+_RgMZ8tOPExcA@mail.gmail.com

We seem to be moving towards that solution, but as discussed we have
left it to committer's judgement.
--
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

#71Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#66)
Re: Push down more full joins in postgres_fdw

On 2016/12/05 20:20, Ashutosh Bapat wrote:

On Mon, Nov 28, 2016 at 3:52 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/11/24 21:45, Etsuro Fujita wrote:

* I removed make_outerrel_subquery/make_innerrel_subquery from fpinfo
and added a new member "is_subquery_rel" to fpinfo, as a flag on whether
to deparse the relation as a subquery.

Replacing make_outerrel_subquery/make_innerrel_subquery with is_subquery_rel
seems to be a bad idea. Whether a relation needs to be deparsed as a subquery
or not depends upon the relation which joins it with another relation. Take for
example a case when a join ABC can pull up the conditions on AB, but ABD can
not. In this case we will mark that AB in ABD needs to be deparsed as a
subquery but will not mark so in ABC. So, if we choose to join ABCD as (ABC)D
we don't need AB to be deparsed as a subquery. If we choose to join ABCD as
(ABD)C, we will need AB to deparsed as a subquery.

This is not true; since (1) currently, a relation needs to be deparsed
as a subquery only when the relation is full-joined with any other
relation, and (2) the join ordering for full joins is preserved, we
wouldn't have such an example. (You might think we would have that when
extending the patch to handle PHVs, but the answer would be no, because
the patch for PHVs would determine whether a relation emitting PHVs
needs to be deparsed as a subquery, depending on the relation itself.
See the patch for PHVs.) I like is_subquery_rel more than
make_outerrel_subquery/make_innerrel_subquery, because it reduces the
number of members in fpinfo. But the choice would be arbitrary, so I
wouldn't object to going back to
make_outerrel_subquery/make_innerrel_subquery.

Some more comments
1. The output of the following query
+SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE);
produces 21 rows all containing a "1". That's not quite good use of expected
output space, given that all that the test tests is the empty
targetlists are deparsed
correctly. You might want to either just test EXPLAIN output or make "between 50
and 60" condition more stringent to reduce the number of rows output.

Will do.

2. Got lost here. I guess, all you need to say is "test deparsing FOR UPDATE
clause with subqueries.". Anyway, the sentence is very hard to read and needs
simplification.
+-- d. test deparsing the remote queries for simple foreign table scans in
+-- an EPQ subplan of a foreign join in which the foreign tables are full
+-- joined and in the remote join query the foreign tables are deparsed as
+-- subqueries

Will do.

3. Why is foreignrel variable changed to rel?
-extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
-                        RelOptInfo *foreignrel, List *tlist,
-                        List *remote_conds, List *pathkeys,
-                        List **retrieved_attrs, List **params_list);
+extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo
*root, RelOptInfo *rel,
+                        List *tlist, List *remote_conds, List *pathkeys,
+                        bool is_subquery, List **retrieved_attrs,
+                        List **params_list);

I changed the name that way to match with the function definition in
deparse.c.

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

#72Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#70)
Re: Push down more full joins in postgres_fdw

On 2016/12/07 15:39, Ashutosh Bapat wrote:

On 2016/11/22 18:28, Ashutosh Bapat wrote:

I guess, the reason why you are doing it this way, is SELECT clause for
the
outermost query gets deparsed before FROM clause. For later we call
deparseRangeTblRef(), which builds the tlist. So, while deparsing SELECT
clause, we do not have tlist to build from. In that case, I guess, we have
to
build the tlist in get_subselect_alias_id() if it's not available and
stick it
in fpinfo. Subsequent calls to get_subselect_alias_id() should find tlist
there. Then in deparseRangeTblRef() assert that there's a tlist in fpinfo
and use it to build the SELECT clause of subquery. That way, we don't
build
tlist unless it's needed and also use the same tlist for all searches.
Please
use tlist_member() to search into the tlist.

I wrote:

This would probably work, but seems to me a bit complicated. Instead, I'd
like to propose that we build the tlist for each relation being deparsed as
a subquery in a given join tree, right before deparsing the SELECT clause in
deparseSelectStmtForRel, if is_subquery is false and lower_subquery_rels
isn't NULL, and store the tlist into the relation's fpinfo. That would
allow us to build the tlist only when we need it, and to use tlist_member
for the exact comparison. I think it would be much easier to implement
that.

IIRC, this is inline with my original proposal of creating tlists
before deparsing anything. Along-with that I also proposed to bubble
this array of tlist up the join hierarchy to avoid recursion [1] point
#15, further elaborated in [2]

[1] /messages/by-id/ad449b25-66ee-4c06-568c-0eb2e1bef9f9@lab.ntt.co.jp
[2] /messages/by-id/CAFjFpRcn7=DNOXm-PJ_jVDqAmghKVf6tApQC+_RgMZ8tOPExcA@mail.gmail.com

We seem to be moving towards that solution, but as discussed we have
left it to committer's judgement.

My proposal here would be a bit different from what you proposed; right
before deparseSelectSql in deparseSelectStmtForRel, build the tlists for
relations present in the given jointree that will be deparsed as
subqueries, by (1) traversing the given jointree and (2) applying
build_tlist_to_deparse to each relation to be deparsed as a subquery and
saving the result in that relation's fpinfo. I think that would be
implemented easily, and the overhead would be small.

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

#73Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#71)
Re: Push down more full joins in postgres_fdw

On Wed, Dec 7, 2016 at 4:51 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/12/05 20:20, Ashutosh Bapat wrote:

On Mon, Nov 28, 2016 at 3:52 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/11/24 21:45, Etsuro Fujita wrote:

* I removed make_outerrel_subquery/make_innerrel_subquery from fpinfo
and added a new member "is_subquery_rel" to fpinfo, as a flag on whether
to deparse the relation as a subquery.

Replacing make_outerrel_subquery/make_innerrel_subquery with
is_subquery_rel
seems to be a bad idea. Whether a relation needs to be deparsed as a
subquery
or not depends upon the relation which joins it with another relation.
Take for
example a case when a join ABC can pull up the conditions on AB, but ABD
can
not. In this case we will mark that AB in ABD needs to be deparsed as a
subquery but will not mark so in ABC. So, if we choose to join ABCD as
(ABC)D
we don't need AB to be deparsed as a subquery. If we choose to join ABCD
as
(ABD)C, we will need AB to deparsed as a subquery.

This is not true; since (1) currently, a relation needs to be deparsed as a
subquery only when the relation is full-joined with any other relation, and
(2) the join ordering for full joins is preserved, we wouldn't have such an
example. (You might think we would have that when extending the patch to
handle PHVs, but the answer would be no, because the patch for PHVs would
determine whether a relation emitting PHVs needs to be deparsed as a
subquery, depending on the relation itself. See the patch for PHVs.) I like
is_subquery_rel more than make_outerrel_subquery/make_innerrel_subquery,
because it reduces the number of members in fpinfo. But the choice would be
arbitrary, so I wouldn't object to going back to
make_outerrel_subquery/make_innerrel_subquery.

I think you are right. And even if we were to deparse a relation as
subquery, when it shouldn't be, we will only loose a syntactic
optimization. So, it shouldn't result in a bug.

3. Why is foreignrel variable changed to rel?
-extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
-                        RelOptInfo *foreignrel, List *tlist,
-                        List *remote_conds, List *pathkeys,
-                        List **retrieved_attrs, List **params_list);
+extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo
*root, RelOptInfo *rel,
+                        List *tlist, List *remote_conds, List *pathkeys,
+                        bool is_subquery, List **retrieved_attrs,
+                        List **params_list);

I changed the name that way to match with the function definition in
deparse.c.

That's something good to do but it's unrelated to this patch. I have
repeated this line several times in this thread :)

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

#74Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#73)
Re: Push down more full joins in postgres_fdw

On 2016/12/07 21:11, Ashutosh Bapat wrote:

On Wed, Dec 7, 2016 at 4:51 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2016/12/05 20:20, Ashutosh Bapat wrote:

3. Why is foreignrel variable changed to rel?
-extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
-                        RelOptInfo *foreignrel, List *tlist,
-                        List *remote_conds, List *pathkeys,
-                        List **retrieved_attrs, List **params_list);
+extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo
*root, RelOptInfo *rel,
+                        List *tlist, List *remote_conds, List *pathkeys,
+                        bool is_subquery, List **retrieved_attrs,
+                        List **params_list);

I changed the name that way to match with the function definition in
deparse.c.

That's something good to do but it's unrelated to this patch. I have
repeated this line several times in this thread :)

OK.

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

#75Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#72)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/12/07 20:23, Etsuro Fujita wrote:

On 2016/12/07 15:39, Ashutosh Bapat wrote:

On 2016/11/22 18:28, Ashutosh Bapat wrote:

I guess, the reason why you are doing it this way, is SELECT clause for
the
outermost query gets deparsed before FROM clause. For later we call
deparseRangeTblRef(), which builds the tlist. So, while deparsing
SELECT
clause, we do not have tlist to build from. In that case, I guess,
we have
to
build the tlist in get_subselect_alias_id() if it's not available and
stick it
in fpinfo. Subsequent calls to get_subselect_alias_id() should find
tlist
there. Then in deparseRangeTblRef() assert that there's a tlist in
fpinfo
and use it to build the SELECT clause of subquery. That way, we don't
build
tlist unless it's needed and also use the same tlist for all searches.
Please
use tlist_member() to search into the tlist.

I wrote:

This would probably work, but seems to me a bit complicated.
Instead, I'd
like to propose that we build the tlist for each relation being
deparsed as
a subquery in a given join tree, right before deparsing the SELECT
clause in
deparseSelectStmtForRel, if is_subquery is false and lower_subquery_rels
isn't NULL, and store the tlist into the relation's fpinfo. That would
allow us to build the tlist only when we need it, and to use
tlist_member
for the exact comparison. I think it would be much easier to implement
that.

IIRC, this is inline with my original proposal of creating tlists
before deparsing anything. Along-with that I also proposed to bubble
this array of tlist up the join hierarchy to avoid recursion [1] point
#15, further elaborated in [2]

[1]
/messages/by-id/ad449b25-66ee-4c06-568c-0eb2e1bef9f9@lab.ntt.co.jp

[2]
/messages/by-id/CAFjFpRcn7=DNOXm-PJ_jVDqAmghKVf6tApQC+_RgMZ8tOPExcA@mail.gmail.com

My proposal here would be a bit different from what you proposed; right
before deparseSelectSql in deparseSelectStmtForRel, build the tlists for
relations present in the given jointree that will be deparsed as
subqueries, by (1) traversing the given jointree and (2) applying
build_tlist_to_deparse to each relation to be deparsed as a subquery and
saving the result in that relation's fpinfo. I think that would be
implemented easily, and the overhead would be small.

I implemented that to address your concern:
* deparseRangeTblRef uses the tlist created as above, to build the
SELECT clause of the subquery. (You said "Then in deparseRangeTblRef()
assert that there's a tlist in fpinfo", but the tlist may be empty, so I
didn't add any assertion to that function.)
* get_relation_column_alias_ids uses tlist_member with the tlist.

I also addressed the comments #1, #2 and #3 in [1]/messages/by-id/CAFjFpRfU4-gxqZ8ahoKM1ZtDJEThe3K8Lb_6beRKa8mmP=v=fA@mail.gmail.com. For #1, I added
"LIMIT 10" to the query. Attached is the new version of the patch.

Other changes:
* As discussed before, renamed grouped_tlist in fpinfo to "tlist" and
used it to store the tlist created as above.
* Also, renamed SS_REL_ALIAS_PREFIX to SUBQUERY_REL_ALIAS_PREFIX
(Likewise for SS_COL_ALIAS_PREFIX).
* Relocated some functions.
* Revised comments a little bit.

Best regards,
Etsuro Fujita

[1]: /messages/by-id/CAFjFpRfU4-gxqZ8ahoKM1ZtDJEThe3K8Lb_6beRKa8mmP=v=fA@mail.gmail.com
/messages/by-id/CAFjFpRfU4-gxqZ8ahoKM1ZtDJEThe3K8Lb_6beRKa8mmP=v=fA@mail.gmail.com

Attachments:

postgres-fdw-subquery-support-v13.patchtext/x-patch; name=postgres-fdw-subquery-support-v13.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SUBQUERY_REL_ALIAS_PREFIX	"s"
+ #define SUBQUERY_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 159,165 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
--- 161,167 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,177 ----
  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, List **params_list);
+ static void appendSubqueryAlias(StringInfo buf, int relno, int ncols);
  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,
***************
*** 175,180 **** static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
--- 180,194 ----
  static Node *deparseSortGroupClause(Index ref, List *tlist,
  					   deparse_expr_cxt *context);
  
+ /*
+  * Helper functions
+  */
+ static void build_subquery_rel_tlists(RelOptInfo *foreignrel);
+ static bool is_subquery_var(Var *node, RelOptInfo *foreignrel,
+ 							int *relno, int *colno);
+ static void get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
+ 										  int *relno, int *colno);
+ 
  
  /*
   * Examine each qual clause in input_conds, and classify them into two groups,
***************
*** 861,867 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
  	 * checking shippability, so just return that.
  	 */
  	if (foreignrel->reloptkind == RELOPT_UPPER_REL)
! 		return fpinfo->grouped_tlist;
  
  	/*
  	 * We require columns specified in foreignrel->reltarget->exprs and those
--- 875,881 ----
  	 * checking shippability, so just return that.
  	 */
  	if (foreignrel->reloptkind == RELOPT_UPPER_REL)
! 		return fpinfo->tlist;
  
  	/*
  	 * We require columns specified in foreignrel->reltarget->exprs and those
***************
*** 881,888 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
   * Deparse SELECT statement for given relation into buf.
   *
   * tlist contains the list of desired columns to be fetched from foreign server.
!  * For a base relation fpinfo->attrs_used is used to construct SELECT clause,
!  * hence the tlist is ignored for a base relation.
   *
   * remote_conds is the list of conditions to be deparsed into the WHERE clause
   * (or, in the case of upper relations, into the HAVING clause).
--- 895,905 ----
   * Deparse SELECT statement for given relation into buf.
   *
   * tlist contains the list of desired columns to be fetched from foreign server.
!  * We use the tlist to construct the SELECT clause, if the relation is a join
!  * or upper relation or if is_subquery is true.  (In the latter case the
!  * relation is deparsed as a subquery.)  Otherwise, we use the relation's
!  * fpinfo->attrs_used to do that, in which case the tlist is ignored for the
!  * relation.
   *
   * remote_conds is the list of conditions to be deparsed into the WHERE clause
   * (or, in the case of upper relations, into the HAVING clause).
***************
*** 901,907 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
  extern void
  deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  						List *tlist, List *remote_conds, List *pathkeys,
! 						List **retrieved_attrs, List **params_list)
  {
  	deparse_expr_cxt context;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
--- 918,925 ----
  extern void
  deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  						List *tlist, List *remote_conds, List *pathkeys,
! 						bool is_subquery, List **retrieved_attrs,
! 						List **params_list)
  {
  	deparse_expr_cxt context;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
***************
*** 924,931 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  		fpinfo->outerrel : rel;
  	context.params_list = params_list;
  
  	/* Construct SELECT clause */
! 	deparseSelectSql(tlist, retrieved_attrs, &context);
  
  	/*
  	 * For upper relations, the WHERE clause is built from the remote
--- 942,956 ----
  		fpinfo->outerrel : rel;
  	context.params_list = params_list;
  
+ 	/*
+ 	 * If we are creating a whole query, build the tlists for relations
+ 	 * in the jointree that will be deparsed as subqueries.
+ 	 */
+ 	if (!is_subquery)
+ 		build_subquery_rel_tlists(context.scanrel);
+ 
  	/* Construct SELECT clause */
! 	deparseSelectSql(tlist, is_subquery, retrieved_attrs, &context);
  
  	/*
  	 * For upper relations, the WHERE clause is built from the remote
***************
*** 974,984 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
   *
!  * tlist is the list of desired columns. Read prologue of
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
--- 999,1011 ----
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
   *
!  * tlist is the list of desired columns. is_subquery is a flag to indicate
!  * whether to deparse the relation as a subquery. Read prologue of
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
! 				 deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
***************
*** 990,1001 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
! 	{
! 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
- 	}
  	else
  	{
  		/*
--- 1017,1031 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
+ 	/*
+ 	 * For a join relation or an upper relation, use deparseExplicitTargetList.
+ 	 * Likewise, for a relation that is being deparsed as a subquery, use that
+ 	 * function. Otherwise, use deparseTargetList.
+ 	 */
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL ||
! 		is_subquery)
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	else
  	{
  		/*
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1185,1203 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery. Locking clause
+ 		 * for such a relation, if needed, is included in the subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->lower_subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1385,1400 ----
  
  	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, params_list);
  
  		/* Deparse inner relation */
  		initStringInfo(&join_sql_i);
! 		deparseRangeTblRef(&join_sql_i, root, fpinfo->innerrel, params_list);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1450,1517 ----
  }
  
  /*
+  * Append FROM clause entry for the given relation to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	/* If is_subquery_rel is true, deparse the relation as a subquery. */
+ 	if (fpinfo->is_subquery_rel)
+ 	{
+ 		List	   *tlist = fpinfo->tlist;
+ 		List	   *retrieved_attrs;
+ 
+ 		/* Deparse the subquery representing the relation. */
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectStmtForRel(buf, root, foreignrel, tlist,
+ 								fpinfo->remote_conds, NIL, true,
+ 								&retrieved_attrs, params_list);
+ 		appendStringInfoChar(buf, ')');
+ 
+ 		/* Append the alias to the subquery. */
+ 		appendSubqueryAlias(buf, fpinfo->relation_index, list_length(tlist));
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Append the relation and column aliases to a subquery.
+  *
+  * 'relno' is the relation alias ID.
+  * 'ncols' is the number of the column aliases to add.
+  */
+ static void
+ appendSubqueryAlias(StringInfo buf, int relno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the relation alias */
+ 	appendStringInfo(buf, " %s%d", SUBQUERY_REL_ALIAS_PREFIX, relno);
+ 
+ 	/* Append the column aliases, if needed */
+ 	if (ncols > 0)
+ 	{
+ 		appendStringInfoChar(buf, '(');
+ 		for (i = 1; i <= ncols; i++)
+ 		{
+ 			if (i > 1)
+ 				appendStringInfoString(buf, ", ");
+ 
+ 			appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, i);
+ 		}
+ 		appendStringInfoChar(buf, ')');
+ 	}
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2155,2179 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	int			relno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var belongs to a foreign relation deparsed as a subquery,
+ 	 * use the relation and column alias provided by the subquery, instead of
+ 	 * the remote name.
+ 	 */
+ 	if (is_subquery_var(node, context->scanrel, &relno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SUBQUERY_REL_ALIAS_PREFIX, relno,
+ 						 SUBQUERY_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
***************
*** 2938,2940 **** deparseSortGroupClause(Index ref, List *tlist, deparse_expr_cxt *context)
--- 3051,3173 ----
  
  	return (Node *) expr;
  }
+ 
+ 
+ /*
+  * Build the tlists for relations in a given jointree that will be deparsed as
+  * subqueries, if any.
+  */
+ static void
+ build_subquery_rel_tlists(RelOptInfo *foreignrel)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return;
+ 	if (fpinfo->lower_subquery_rels == NULL)
+ 		return;
+ 
+ 	/*
+ 	 * If the outer relation is deparsed as a subquery, build the tlist for
+ 	 * the relation and save it into the relation's fpinfo.
+ 	 */
+ 	if (bms_is_subset(outerrel->relids, fpinfo->lower_subquery_rels))
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
+ 
+ 		if (fpinfo2->is_subquery_rel)
+ 			fpinfo2->tlist = build_tlist_to_deparse(outerrel);
+ 	}
+ 	/* Likewise for the inner relation. */
+ 	if (bms_is_subset(innerrel->relids, fpinfo->lower_subquery_rels))
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
+ 
+ 		if (fpinfo2->is_subquery_rel)
+ 			fpinfo2->tlist = build_tlist_to_deparse(innerrel);
+ 	}
+ 
+ 	/* Recurse into the outer relation. */
+ 	build_subquery_rel_tlists(outerrel);
+ 	/* Likewise for the inner relation. */
+ 	build_subquery_rel_tlists(innerrel);
+ }
+ 
+ /*
+  * Returns true if the Var belongs to a foreign relation deparsed as a subquery.
+  * Returns false otherwise.  When returning true, this function sets *relno and
+  * *colno to the IDs for the relation and column alias provided by the subquery,
+  * respectively.
+  */
+ static bool
+ is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 	if (!bms_is_member(node->varno, fpinfo->lower_subquery_rels))
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
+ 
+ 		/*
+ 		 * If the outer relation is deparsed as a subquery, get the IDs for the
+ 		 * relation and column alias to the Var.
+ 		 */
+ 		if (fpinfo2->is_subquery_rel)
+ 		{
+ 			get_relation_column_alias_ids(node, outerrel, relno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the outer relation. */
+ 		return is_subquery_var(node, outerrel, relno, colno);
+ 	}
+ 	else
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
+ 
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * If the inner relation is deparsed as a subquery, get the IDs for the
+ 		 * relation and column alias to the Var.
+ 		 */
+ 		if (fpinfo2->is_subquery_rel)
+ 		{
+ 			get_relation_column_alias_ids(node, innerrel, relno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the inner relation. */
+ 		return is_subquery_var(node, innerrel, relno, colno);
+ 	}
+ }
+ 
+ /*
+  * Get the IDs for the relation and column alias to the Var belonging to the
+  * given foreignrel.  They are returned into *relno and *colno, respectively.
+  */
+ static void
+ get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
+ 							  int *relno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	TargetEntry *tle;
+ 
+ 	/* Get the relation alias ID */
+ 	*relno = fpinfo->relation_index;
+ 
+ 	/* Get the column alias ID */
+ 	tle = tlist_member((Node *) node, fpinfo->tlist);
+ 	if (tle == NULL)
+ 		elog(ERROR, "unexpected expression in subquery output");
+ 	*colno = tle->resno;
+ }
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1365 ----
      | 57
  (8 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+                                                                                                QUERY PLAN                                                                                               
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: 1
+    ->  Foreign Scan
+          Output: 1
+          Relations: (public.ft4) FULL JOIN (public.ft5)
+          Remote SQL: SELECT NULL FROM ((SELECT NULL FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4 FULL JOIN (SELECT NULL FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5 ON (TRUE))
+ (6 rows)
+ 
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+  ?column? 
+ ----------
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+ (10 rows)
+ 
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
+ -- d. test deparsing rowmarked relations as subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                             
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  LockRows
+    Output: "T 3".c1, ft4.c1, ft5.c1, "T 3".ctid, ft4.*, ft5.*
+    ->  Nested Loop
+          Output: "T 3".c1, ft4.c1, ft5.c1, "T 3".ctid, ft4.*, ft5.*
+          ->  Foreign Scan
+                Output: ft4.c1, ft4.*, ft5.c1, ft5.*
+                Relations: (public.ft4) FULL JOIN (public.ft5)
+                Remote SQL: SELECT s8.c1, s8.c2, s9.c1, s9.c2 FROM ((SELECT c1, ROW(c1, c2, c3) FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1, c2) FULL JOIN (SELECT c1, ROW(c1, c2, c3) FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1, c2) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL))) ORDER BY s8.c1 ASC NULLS LAST, s9.c1 ASC NULLS LAST
+                ->  Hash Full Join
+                      Output: ft4.c1, ft4.*, ft5.c1, ft5.*
+                      Hash Cond: (ft4.c1 = ft5.c1)
+                      Filter: ((ft4.c1 IS NULL) OR (ft4.c1 IS NOT NULL))
+                      ->  Foreign Scan on public.ft4
+                            Output: ft4.c1, ft4.*
+                            Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
+                      ->  Hash
+                            Output: ft5.c1, ft5.*
+                            ->  Foreign Scan on public.ft5
+                                  Output: ft5.c1, ft5.*
+                                  Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
+          ->  Materialize
+                Output: "T 3".c1, "T 3".ctid
+                ->  Seq Scan on "S 1"."T 3"
+                      Output: "T 3".c1, "T 3".ctid
+                      Filter: ("T 3".c1 = 50)
+ (25 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  50 | 52 |   
+  50 | 54 | 54
+  50 | 56 |   
+  50 | 58 |   
+  50 | 60 | 60
+  50 |    | 51
+  50 |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3172,3195 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,682 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Initialize info about whether to deparse the relation as a subquery. */
+ 	fpinfo->is_subquery_rel = false;
+ 	/* Initialize info about lower subqueries. */
+ 	fpinfo->lower_subquery_rels = NULL;
+ 	/* Set the relation index. */
+ 	fpinfo->relation_index = baserel->relid;
+ 	/* Initialize the tlist. */
+ 	fpinfo->tlist = NIL;
  }
  
  /*
***************
*** 1239,1245 **** postgresGetForeignPlan(PlannerInfo *root,
  	initStringInfo(&sql);
  	deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
  							remote_conds, best_path->path.pathkeys,
! 							&retrieved_attrs, &params_list);
  
  	/*
  	 * Build the fdw_private list that will be available to the executor.
--- 1248,1254 ----
  	initStringInfo(&sql);
  	deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
  							remote_conds, best_path->path.pathkeys,
! 							false, &retrieved_attrs, &params_list);
  
  	/*
  	 * Build the fdw_private list that will be available to the executor.
***************
*** 2551,2558 **** estimate_path_cost_size(PlannerInfo *root,
  		initStringInfo(&sql);
  		appendStringInfoString(&sql, "EXPLAIN ");
  		deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
! 								remote_conds, pathkeys, &retrieved_attrs,
! 								NULL);
  
  		/* Get the remote estimate */
  		conn = GetConnection(fpinfo->user, false);
--- 2560,2567 ----
  		initStringInfo(&sql);
  		appendStringInfoString(&sql, "EXPLAIN ");
  		deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
! 								remote_conds, pathkeys, false,
! 								&retrieved_attrs, NULL);
  
  		/* Get the remote estimate */
  		conn = GetConnection(fpinfo->user, false);
***************
*** 2708,2714 **** estimate_path_cost_size(PlannerInfo *root,
  			MemSet(&aggcosts, 0, sizeof(AggClauseCosts));
  			if (root->parse->hasAggs)
  			{
! 				get_agg_clause_costs(root, (Node *) fpinfo->grouped_tlist,
  									 AGGSPLIT_SIMPLE, &aggcosts);
  				get_agg_clause_costs(root, (Node *) root->parse->havingQual,
  									 AGGSPLIT_SIMPLE, &aggcosts);
--- 2717,2723 ----
  			MemSet(&aggcosts, 0, sizeof(AggClauseCosts));
  			if (root->parse->hasAggs)
  			{
! 				get_agg_clause_costs(root, (Node *) fpinfo->tlist,
  									 AGGSPLIT_SIMPLE, &aggcosts);
  				get_agg_clause_costs(root, (Node *) root->parse->havingQual,
  									 AGGSPLIT_SIMPLE, &aggcosts);
***************
*** 2718,2724 **** estimate_path_cost_size(PlannerInfo *root,
  			numGroupCols = list_length(root->parse->groupClause);
  			numGroups = estimate_num_groups(root,
  							get_sortgrouplist_exprs(root->parse->groupClause,
! 													fpinfo->grouped_tlist),
  											input_rows, NULL);
  
  			/*
--- 2727,2733 ----
  			numGroupCols = list_length(root->parse->groupClause);
  			numGroups = estimate_num_groups(root,
  							get_sortgrouplist_exprs(root->parse->groupClause,
! 													fpinfo->tlist),
  											input_rows, NULL);
  
  			/*
***************
*** 4146,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->innerrel = innerrel;
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4155,4178 ----
  	fpinfo->innerrel = innerrel;
  	fpinfo->jointype = jointype;
  
+ 	/* Initialize info about whether to deparse the relation as a subquery. */
+ 	fpinfo->is_subquery_rel = false;
+ 
+ 	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use by the deparser.
+ 	 */
+ 	Assert(bms_is_subset(fpinfo_o->lower_subquery_rels, outerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_i->lower_subquery_rels, innerrel->relids));
+ 	fpinfo->lower_subquery_rels = bms_union(fpinfo_o->lower_subquery_rels,
+ 											fpinfo_i->lower_subquery_rels);
+ 
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4183,4189 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4212,4241 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that the
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo of the joining relation so that the deparser can
! 			 * take appropriate action.  Also, save the relids of base
! 			 * relations covered by the joining relation into the fpinfo of
! 			 * this relation.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo_o->is_subquery_rel = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo_i->is_subquery_rel = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									innerrel->relids);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4316,4334 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the join_rel_list list
+ 	 * when we are called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
+ 	/* Initialize the tlist. */
+ 	fpinfo->tlist = NIL;
+ 
  	return true;
  }
  
***************
*** 4613,4619 **** foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
  	apply_pathtarget_labeling_to_tlist(tlist, grouping_target);
  
  	/* Store generated targetlist */
! 	fpinfo->grouped_tlist = tlist;
  
  	/* Safe to pushdown */
  	fpinfo->pushdown_safe = true;
--- 4669,4675 ----
  	apply_pathtarget_labeling_to_tlist(tlist, grouping_target);
  
  	/* Store generated targetlist */
! 	fpinfo->tlist = tlist;
  
  	/* Safe to pushdown */
  	fpinfo->pushdown_safe = true;
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 93,100 **** typedef struct PgFdwRelationInfo
  	JoinType	jointype;
  	List	   *joinclauses;
  
! 	/* Grouping information */
! 	List	   *grouped_tlist;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
--- 93,114 ----
  	JoinType	jointype;
  	List	   *joinclauses;
  
! 	/* Subquery information */
! 	bool		is_subquery_rel;	/* do we deparse this as a subquery? */
! 	Relids		lower_subquery_rels;	/* all relids appearing in lower
! 										 * subqueries */
! 
! 	/*
! 	 * Index of the relation.  It is used for creating an alias to a subquery
! 	 * representing the relation if is_subquery_rel is true.
! 	 */
! 	int			relation_index;
! 
! 	/*
! 	 * Optional tlist describing the contents of the scan tuple from the
! 	 * relation.
! 	 */
! 	List	   *tlist;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
***************
*** 161,167 **** extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
  extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
! 						List *remote_conds, List *pathkeys,
  						List **retrieved_attrs, List **params_list);
  
  /* in shippable.c */
--- 175,181 ----
  extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
! 						List *remote_conds, List *pathkeys, bool is_subquery,
  						List **retrieved_attrs, List **params_list);
  
  /* in shippable.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,416 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- d. test deparsing rowmarked relations as subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 810,821 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
#76Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#75)
2 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2016/12/08 21:08, Etsuro Fujita wrote:

On 2016/12/07 20:23, Etsuro Fujita wrote:

My proposal here would be a bit different from what you proposed; right
before deparseSelectSql in deparseSelectStmtForRel, build the tlists for
relations present in the given jointree that will be deparsed as
subqueries, by (1) traversing the given jointree and (2) applying
build_tlist_to_deparse to each relation to be deparsed as a subquery and
saving the result in that relation's fpinfo. I think that would be
implemented easily, and the overhead would be small.

I implemented that to address your concern:
* deparseRangeTblRef uses the tlist created as above, to build the
SELECT clause of the subquery. (You said "Then in deparseRangeTblRef()
assert that there's a tlist in fpinfo", but the tlist may be empty, so I
didn't add any assertion to that function.)
* get_relation_column_alias_ids uses tlist_member with the tlist.

I also addressed the comments #1, #2 and #3 in [1]. For #1, I added
"LIMIT 10" to the query. Attached is the new version of the patch.

Other changes:
* As discussed before, renamed grouped_tlist in fpinfo to "tlist" and
used it to store the tlist created as above.
* Also, renamed SS_REL_ALIAS_PREFIX to SUBQUERY_REL_ALIAS_PREFIX
(Likewise for SS_COL_ALIAS_PREFIX).
* Relocated some functions.
* Revised comments a little bit.

I updated the patch a bit further: simplified the function name
(s/build_subquery_rel_tlists/build_subquery_tlists/), and revised
comments a little bit. Attached is an updated version
(postgres-fdw-subquery-support-v14.patch). And I rebased another patch
for PHVs against that patch, which is also attached
(postgres-fdw-phv-pushdown-v14.patch).

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v14.patchtext/x-patch; name=postgres-fdw-subquery-support-v14.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SUBQUERY_REL_ALIAS_PREFIX	"s"
+ #define SUBQUERY_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 159,165 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
--- 161,167 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 169,177 ----
  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, List **params_list);
+ static void appendSubqueryAlias(StringInfo buf, int relno, int ncols);
  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,
***************
*** 175,180 **** static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
--- 180,194 ----
  static Node *deparseSortGroupClause(Index ref, List *tlist,
  					   deparse_expr_cxt *context);
  
+ /*
+  * Helper functions
+  */
+ static void build_subquery_tlists(RelOptInfo *foreignrel);
+ static bool is_subquery_var(Var *node, RelOptInfo *foreignrel,
+ 							int *relno, int *colno);
+ static void get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
+ 										  int *relno, int *colno);
+ 
  
  /*
   * Examine each qual clause in input_conds, and classify them into two groups,
***************
*** 861,867 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
  	 * checking shippability, so just return that.
  	 */
  	if (foreignrel->reloptkind == RELOPT_UPPER_REL)
! 		return fpinfo->grouped_tlist;
  
  	/*
  	 * We require columns specified in foreignrel->reltarget->exprs and those
--- 875,881 ----
  	 * checking shippability, so just return that.
  	 */
  	if (foreignrel->reloptkind == RELOPT_UPPER_REL)
! 		return fpinfo->tlist;
  
  	/*
  	 * We require columns specified in foreignrel->reltarget->exprs and those
***************
*** 881,888 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
   * Deparse SELECT statement for given relation into buf.
   *
   * tlist contains the list of desired columns to be fetched from foreign server.
!  * For a base relation fpinfo->attrs_used is used to construct SELECT clause,
!  * hence the tlist is ignored for a base relation.
   *
   * remote_conds is the list of conditions to be deparsed into the WHERE clause
   * (or, in the case of upper relations, into the HAVING clause).
--- 895,905 ----
   * Deparse SELECT statement for given relation into buf.
   *
   * tlist contains the list of desired columns to be fetched from foreign server.
!  * We use the tlist to construct the SELECT clause, if the relation is a join
!  * or upper relation or if is_subquery is true.  (In the latter case the
!  * relation is deparsed as a subquery.)  Otherwise, we use the relation's
!  * fpinfo->attrs_used to do that, in which case the tlist is ignored for the
!  * relation.
   *
   * remote_conds is the list of conditions to be deparsed into the WHERE clause
   * (or, in the case of upper relations, into the HAVING clause).
***************
*** 901,907 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
  extern void
  deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  						List *tlist, List *remote_conds, List *pathkeys,
! 						List **retrieved_attrs, List **params_list)
  {
  	deparse_expr_cxt context;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
--- 918,925 ----
  extern void
  deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  						List *tlist, List *remote_conds, List *pathkeys,
! 						bool is_subquery, List **retrieved_attrs,
! 						List **params_list)
  {
  	deparse_expr_cxt context;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
***************
*** 924,931 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  		fpinfo->outerrel : rel;
  	context.params_list = params_list;
  
  	/* Construct SELECT clause */
! 	deparseSelectSql(tlist, retrieved_attrs, &context);
  
  	/*
  	 * For upper relations, the WHERE clause is built from the remote
--- 942,956 ----
  		fpinfo->outerrel : rel;
  	context.params_list = params_list;
  
+ 	/*
+ 	 * If creating a whole query, build the tlists from subqueries present in
+ 	 * the FROM clause of the query.
+ 	 */
+ 	if (!is_subquery)
+ 		build_subquery_tlists(context.scanrel);
+ 
  	/* Construct SELECT clause */
! 	deparseSelectSql(tlist, is_subquery, retrieved_attrs, &context);
  
  	/*
  	 * For upper relations, the WHERE clause is built from the remote
***************
*** 974,984 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
   *
!  * tlist is the list of desired columns. Read prologue of
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
--- 999,1011 ----
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
   *
!  * tlist is the list of desired columns. is_subquery is a flag to indicate
!  * whether to deparse the relation as a subquery. Read prologue of
   * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
! 				 deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
***************
*** 990,1001 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
! 	{
! 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
- 	}
  	else
  	{
  		/*
--- 1017,1031 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
+ 	/*
+ 	 * For a join relation or an upper relation, use deparseExplicitTargetList.
+ 	 * Likewise, for a relation that is being deparsed as a subquery, use that
+ 	 * function. Otherwise, use deparseTargetList.
+ 	 */
  	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL ||
! 		is_subquery)
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	else
  	{
  		/*
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1185,1203 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery. Locking clause
+ 		 * for such a relation, if needed, is included in the subquery.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->lower_subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1385,1400 ----
  
  	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, params_list);
  
  		/* Deparse inner relation */
  		initStringInfo(&join_sql_i);
! 		deparseRangeTblRef(&join_sql_i, root, fpinfo->innerrel, params_list);
  
  		/*
  		 * For a join relation FROM clause entry is deparsed as
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1450,1517 ----
  }
  
  /*
+  * Append FROM clause entry for the given relation to buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	/* If is_subquery_rel is true, deparse the relation as a subquery. */
+ 	if (fpinfo->is_subquery_rel)
+ 	{
+ 		List	   *tlist = fpinfo->tlist;
+ 		List	   *retrieved_attrs;
+ 
+ 		/* Deparse the subquery representing the relation. */
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectStmtForRel(buf, root, foreignrel, tlist,
+ 								fpinfo->remote_conds, NIL, true,
+ 								&retrieved_attrs, params_list);
+ 		appendStringInfoChar(buf, ')');
+ 
+ 		/* Append the alias to the subquery. */
+ 		appendSubqueryAlias(buf, fpinfo->relation_index, list_length(tlist));
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
+  * Append the relation and column aliases to a subquery.
+  *
+  * 'relno' is the relation alias ID.
+  * 'ncols' is the number of the column aliases to add.
+  */
+ static void
+ appendSubqueryAlias(StringInfo buf, int relno, int ncols)
+ {
+ 	int			i;
+ 
+ 	/* Append the relation alias */
+ 	appendStringInfo(buf, " %s%d", SUBQUERY_REL_ALIAS_PREFIX, relno);
+ 
+ 	/* Append the column aliases, if needed */
+ 	if (ncols > 0)
+ 	{
+ 		appendStringInfoChar(buf, '(');
+ 		for (i = 1; i <= ncols; i++)
+ 		{
+ 			if (i > 1)
+ 				appendStringInfoString(buf, ", ");
+ 
+ 			appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, i);
+ 		}
+ 		appendStringInfoChar(buf, ')');
+ 	}
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2155,2179 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	int			relno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the given Var belongs to a foreign relation deparsed as a subquery,
+ 	 * use the relation and column alias provided by the subquery, instead of
+ 	 * the remote name.
+ 	 */
+ 	if (is_subquery_var(node, context->scanrel, &relno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SUBQUERY_REL_ALIAS_PREFIX, relno,
+ 						 SUBQUERY_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
***************
*** 2938,2940 **** deparseSortGroupClause(Index ref, List *tlist, deparse_expr_cxt *context)
--- 3051,3174 ----
  
  	return (Node *) expr;
  }
+ 
+ 
+ /*
+  * Build the tlists from subqueries present in a given jointree, if any.
+  */
+ static void
+ build_subquery_tlists(RelOptInfo *foreignrel)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return;
+ 	if (fpinfo->lower_subquery_rels == NULL)
+ 		return;
+ 
+ 	/*
+ 	 * If the outer relation is deparsed as a subquery, build the tlist from
+ 	 * the subquery and save it into the relation's fpinfo.
+ 	 */
+ 	if (bms_is_subset(outerrel->relids, fpinfo->lower_subquery_rels))
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
+ 
+ 		if (fpinfo2->is_subquery_rel)
+ 			fpinfo2->tlist = build_tlist_to_deparse(outerrel);
+ 	}
+ 	/* Likewise for the inner relation. */
+ 	if (bms_is_subset(innerrel->relids, fpinfo->lower_subquery_rels))
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
+ 
+ 		if (fpinfo2->is_subquery_rel)
+ 			fpinfo2->tlist = build_tlist_to_deparse(innerrel);
+ 	}
+ 
+ 	/* Recurse into the outer relation. */
+ 	build_subquery_tlists(outerrel);
+ 	/* Likewise for the inner relation. */
+ 	build_subquery_tlists(innerrel);
+ }
+ 
+ /*
+  * Returns true if the Var belongs to a foreign relation deparsed as a subquery.
+  * Returns false otherwise.  When returning true, this function sets *relno and
+  * *colno to the IDs for the relation and column alias provided by the subquery,
+  * respectively.
+  */
+ static bool
+ is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 	if (!bms_is_member(node->varno, fpinfo->lower_subquery_rels))
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
+ 
+ 		/*
+ 		 * If the outer relation is deparsed as a subquery, the Var must be an
+ 		 * output column of the subquery; get the IDs for the relation and
+ 		 * column alias provided by the subquery.
+ 		 */
+ 		if (fpinfo2->is_subquery_rel)
+ 		{
+ 			get_relation_column_alias_ids(node, outerrel, relno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the outer relation. */
+ 		return is_subquery_var(node, outerrel, relno, colno);
+ 	}
+ 	else
+ 	{
+ 		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
+ 
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * If the inner relation is deparsed as a subquery, the Var must be an
+ 		 * output column of the subquery; get the IDs for the relation and
+ 		 * column alias provided by the subquery.
+ 		 */
+ 		if (fpinfo2->is_subquery_rel)
+ 		{
+ 			get_relation_column_alias_ids(node, innerrel, relno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the inner relation. */
+ 		return is_subquery_var(node, innerrel, relno, colno);
+ 	}
+ }
+ 
+ /*
+  * Get the IDs for the relation and column alias to the Var belonging to the
+  * given foreignrel.  They are returned into *relno and *colno, respectively.
+  */
+ static void
+ get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
+ 							  int *relno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	TargetEntry *tle;
+ 
+ 	/* Get the relation alias ID */
+ 	*relno = fpinfo->relation_index;
+ 
+ 	/* Get the column alias ID */
+ 	tle = tlist_member((Node *) node, fpinfo->tlist);
+ 	if (tle == NULL)
+ 		elog(ERROR, "unexpected expression in subquery output");
+ 	*colno = tle->resno;
+ }
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1365 ----
      | 57
  (8 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+                                                                                                QUERY PLAN                                                                                               
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: 1
+    ->  Foreign Scan
+          Output: 1
+          Relations: (public.ft4) FULL JOIN (public.ft5)
+          Remote SQL: SELECT NULL FROM ((SELECT NULL FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4 FULL JOIN (SELECT NULL FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5 ON (TRUE))
+ (6 rows)
+ 
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+  ?column? 
+ ----------
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+ (10 rows)
+ 
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
+ -- d. test deparsing rowmarked relations as subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                             
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  LockRows
+    Output: "T 3".c1, ft4.c1, ft5.c1, "T 3".ctid, ft4.*, ft5.*
+    ->  Nested Loop
+          Output: "T 3".c1, ft4.c1, ft5.c1, "T 3".ctid, ft4.*, ft5.*
+          ->  Foreign Scan
+                Output: ft4.c1, ft4.*, ft5.c1, ft5.*
+                Relations: (public.ft4) FULL JOIN (public.ft5)
+                Remote SQL: SELECT s8.c1, s8.c2, s9.c1, s9.c2 FROM ((SELECT c1, ROW(c1, c2, c3) FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1, c2) FULL JOIN (SELECT c1, ROW(c1, c2, c3) FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1, c2) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL))) ORDER BY s8.c1 ASC NULLS LAST, s9.c1 ASC NULLS LAST
+                ->  Hash Full Join
+                      Output: ft4.c1, ft4.*, ft5.c1, ft5.*
+                      Hash Cond: (ft4.c1 = ft5.c1)
+                      Filter: ((ft4.c1 IS NULL) OR (ft4.c1 IS NOT NULL))
+                      ->  Foreign Scan on public.ft4
+                            Output: ft4.c1, ft4.*
+                            Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
+                      ->  Hash
+                            Output: ft5.c1, ft5.*
+                            ->  Foreign Scan on public.ft5
+                                  Output: ft5.c1, ft5.*
+                                  Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
+          ->  Materialize
+                Output: "T 3".c1, "T 3".ctid
+                ->  Seq Scan on "S 1"."T 3"
+                      Output: "T 3".c1, "T 3".ctid
+                      Filter: ("T 3".c1 = 50)
+ (25 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  50 | 52 |   
+  50 | 54 | 54
+  50 | 56 |   
+  50 | 58 |   
+  50 | 60 | 60
+  50 |    | 51
+  50 |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3172,3195 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,682 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* Initialize info about whether to deparse the relation as a subquery. */
+ 	fpinfo->is_subquery_rel = false;
+ 	/* Initialize info about lower subqueries. */
+ 	fpinfo->lower_subquery_rels = NULL;
+ 	/* Set the relation index. */
+ 	fpinfo->relation_index = baserel->relid;
+ 	/* Initialize the tlist. */
+ 	fpinfo->tlist = NIL;
  }
  
  /*
***************
*** 1239,1245 **** postgresGetForeignPlan(PlannerInfo *root,
  	initStringInfo(&sql);
  	deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
  							remote_conds, best_path->path.pathkeys,
! 							&retrieved_attrs, &params_list);
  
  	/*
  	 * Build the fdw_private list that will be available to the executor.
--- 1248,1254 ----
  	initStringInfo(&sql);
  	deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
  							remote_conds, best_path->path.pathkeys,
! 							false, &retrieved_attrs, &params_list);
  
  	/*
  	 * Build the fdw_private list that will be available to the executor.
***************
*** 2551,2558 **** estimate_path_cost_size(PlannerInfo *root,
  		initStringInfo(&sql);
  		appendStringInfoString(&sql, "EXPLAIN ");
  		deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
! 								remote_conds, pathkeys, &retrieved_attrs,
! 								NULL);
  
  		/* Get the remote estimate */
  		conn = GetConnection(fpinfo->user, false);
--- 2560,2567 ----
  		initStringInfo(&sql);
  		appendStringInfoString(&sql, "EXPLAIN ");
  		deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
! 								remote_conds, pathkeys, false,
! 								&retrieved_attrs, NULL);
  
  		/* Get the remote estimate */
  		conn = GetConnection(fpinfo->user, false);
***************
*** 2708,2714 **** estimate_path_cost_size(PlannerInfo *root,
  			MemSet(&aggcosts, 0, sizeof(AggClauseCosts));
  			if (root->parse->hasAggs)
  			{
! 				get_agg_clause_costs(root, (Node *) fpinfo->grouped_tlist,
  									 AGGSPLIT_SIMPLE, &aggcosts);
  				get_agg_clause_costs(root, (Node *) root->parse->havingQual,
  									 AGGSPLIT_SIMPLE, &aggcosts);
--- 2717,2723 ----
  			MemSet(&aggcosts, 0, sizeof(AggClauseCosts));
  			if (root->parse->hasAggs)
  			{
! 				get_agg_clause_costs(root, (Node *) fpinfo->tlist,
  									 AGGSPLIT_SIMPLE, &aggcosts);
  				get_agg_clause_costs(root, (Node *) root->parse->havingQual,
  									 AGGSPLIT_SIMPLE, &aggcosts);
***************
*** 2718,2724 **** estimate_path_cost_size(PlannerInfo *root,
  			numGroupCols = list_length(root->parse->groupClause);
  			numGroups = estimate_num_groups(root,
  							get_sortgrouplist_exprs(root->parse->groupClause,
! 													fpinfo->grouped_tlist),
  											input_rows, NULL);
  
  			/*
--- 2727,2733 ----
  			numGroupCols = list_length(root->parse->groupClause);
  			numGroups = estimate_num_groups(root,
  							get_sortgrouplist_exprs(root->parse->groupClause,
! 													fpinfo->tlist),
  											input_rows, NULL);
  
  			/*
***************
*** 4146,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->innerrel = innerrel;
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4155,4178 ----
  	fpinfo->innerrel = innerrel;
  	fpinfo->jointype = jointype;
  
+ 	/* Initialize info about whether to deparse the relation as a subquery. */
+ 	fpinfo->is_subquery_rel = false;
+ 
+ 	/*
+ 	 * By default both the joining relations are not required to be deparsed as
+ 	 * subqueries.  But there might be some relations covered by the joining
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use by the deparser.
+ 	 */
+ 	Assert(bms_is_subset(fpinfo_o->lower_subquery_rels, outerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_i->lower_subquery_rels, innerrel->relids));
+ 	fpinfo->lower_subquery_rels = bms_union(fpinfo_o->lower_subquery_rels,
+ 											fpinfo_i->lower_subquery_rels);
+ 
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4183,4189 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4212,4241 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the joining relations has conditions,
! 			 * we need to deparse that relation as a subquery so that the
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo of the joining relation so that the deparser can
! 			 * take appropriate action.  Also, save the relids of base
! 			 * relations covered by the joining relation into the fpinfo of
! 			 * this relation.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo_o->is_subquery_rel = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo_i->is_subquery_rel = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									innerrel->relids);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4316,4334 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the join_rel_list list
+ 	 * when we are called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
+ 	/* Initialize the tlist. */
+ 	fpinfo->tlist = NIL;
+ 
  	return true;
  }
  
***************
*** 4613,4619 **** foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
  	apply_pathtarget_labeling_to_tlist(tlist, grouping_target);
  
  	/* Store generated targetlist */
! 	fpinfo->grouped_tlist = tlist;
  
  	/* Safe to pushdown */
  	fpinfo->pushdown_safe = true;
--- 4669,4675 ----
  	apply_pathtarget_labeling_to_tlist(tlist, grouping_target);
  
  	/* Store generated targetlist */
! 	fpinfo->tlist = tlist;
  
  	/* Safe to pushdown */
  	fpinfo->pushdown_safe = true;
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 93,100 **** typedef struct PgFdwRelationInfo
  	JoinType	jointype;
  	List	   *joinclauses;
  
! 	/* Grouping information */
! 	List	   *grouped_tlist;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
--- 93,114 ----
  	JoinType	jointype;
  	List	   *joinclauses;
  
! 	/* Subquery information */
! 	bool		is_subquery_rel;	/* do we deparse this as a subquery? */
! 	Relids		lower_subquery_rels;	/* all relids appearing in lower
! 										 * subqueries */
! 
! 	/*
! 	 * Index of the relation.  It is used for creating an alias to a subquery
! 	 * representing the relation if is_subquery_rel is true.
! 	 */
! 	int			relation_index;
! 
! 	/*
! 	 * Optional tlist describing the contents of the scan tuple from the
! 	 * relation.
! 	 */
! 	List	   *tlist;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
***************
*** 161,167 **** extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
  extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
! 						List *remote_conds, List *pathkeys,
  						List **retrieved_attrs, List **params_list);
  
  /* in shippable.c */
--- 175,181 ----
  extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
! 						List *remote_conds, List *pathkeys, bool is_subquery,
  						List **retrieved_attrs, List **params_list);
  
  /* in shippable.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,416 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- d. test deparsing rowmarked relations as subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 810,821 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
postgres-fdw-phv-pushdown-v14.patchtext/x-patch; name=postgres-fdw-phv-pushdown-v14.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 49,54 ****
--- 49,55 ----
  #include "nodes/nodeFuncs.h"
  #include "nodes/plannodes.h"
  #include "optimizer/clauses.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/prep.h"
  #include "optimizer/tlist.h"
  #include "optimizer/var.h"
***************
*** 157,162 **** static void deparseRelabelType(RelabelType *node, deparse_expr_cxt *context);
--- 158,164 ----
  static void deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context);
  static void deparseNullTest(NullTest *node, deparse_expr_cxt *context);
  static void deparseArrayExpr(ArrayExpr *node, deparse_expr_cxt *context);
+ static void deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context);
  static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
***************
*** 183,193 **** static Node *deparseSortGroupClause(Index ref, List *tlist,
  /*
   * Helper functions
   */
! static void build_subquery_tlists(RelOptInfo *foreignrel);
! static bool is_subquery_var(Var *node, RelOptInfo *foreignrel,
! 							int *relno, int *colno);
! static void get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
! 										  int *relno, int *colno);
  
  
  /*
--- 185,195 ----
  /*
   * Helper functions
   */
! static void build_subquery_tlists(PlannerInfo *root, RelOptInfo *foreignrel);
! static bool is_subquery_expr(Expr *node, PlannerInfo *root, RelOptInfo *foreignrel,
! 				 int *relno, int *colno);
! static void get_relation_column_alias_ids(Expr *node, RelOptInfo *foreignrel,
! 							  int *relno, int *colno);
  
  
  /*
***************
*** 771,776 **** foreign_expr_walker(Node *node,
--- 773,797 ----
  					state = FDW_COLLATE_UNSAFE;
  			}
  			break;
+ 		case T_PlaceHolderVar:
+ 			{
+ 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 				PlaceHolderInfo *phinfo = find_placeholder_info(glob_cxt->root,
+ 																phv, false);
+ 
+ 				/*
+ 				 * If the PHV's contained expression is computable on the
+ 				 * remote server, we consider the PHV safe to send.
+ 				 */
+ 				if (phv->phlevelsup == 0 &&
+ 					bms_is_subset(phinfo->ph_eval_at,
+ 								  glob_cxt->foreignrel->relids))
+ 					return foreign_expr_walker((Node *) phv->phexpr,
+ 											   glob_cxt, outer_cxt);
+ 				else
+ 					return false;
+ 			}
+ 			break;
  		default:
  
  			/*
***************
*** 865,871 **** deparse_type_name(Oid type_oid, int32 typemod)
   * foreign server.
   */
  List *
! build_tlist_to_deparse(RelOptInfo *foreignrel)
  {
  	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
--- 886,892 ----
   * foreign server.
   */
  List *
! build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel)
  {
  	List	   *tlist = NIL;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
***************
*** 878,892 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
  		return fpinfo->tlist;
  
  	/*
! 	 * We require columns specified in foreignrel->reltarget->exprs and those
! 	 * required for evaluating the local conditions.
  	 */
! 	tlist = add_to_flat_tlist(tlist,
! 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
! 									   PVC_RECURSE_PLACEHOLDERS));
! 	tlist = add_to_flat_tlist(tlist,
! 							  pull_var_clause((Node *) fpinfo->local_conds,
! 											  PVC_RECURSE_PLACEHOLDERS));
  
  	return tlist;
  }
--- 899,971 ----
  		return fpinfo->tlist;
  
  	/*
! 	 * Fetch all expressions in the given relation's reltarget if the
! 	 * reltarget_is_shippable flag is set TRUE.  Otherwise, fetch shipplable
! 	 * expressions in the reltarget plus expressions required for evaluating
! 	 * non-shippable expressions in the reltarget.
  	 */
! 	if (fpinfo->reltarget_is_shippable)
! 		tlist = add_to_flat_tlist(tlist, foreignrel->reltarget->exprs);
! 	else
! 	{
! 		List	   *exprs = NIL;
! 		ListCell   *lc;
! 
! 		/* Note: we have at least one non-shippable PHV in the reltarget. */
! 		foreach(lc, foreignrel->reltarget->exprs)
! 		{
! 			Node	   *node = (Node *) lfirst(lc);
! 
! 			if (IsA(node, Var))
! 				exprs = lappend(exprs, node);
! 			else if (IsA(node, PlaceHolderVar))
! 			{
! 				PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 				PlaceHolderInfo *phinfo = find_placeholder_info(root, phv,
! 																false);
! 
! 				if (bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->outerrel->relids) ||
! 					bms_is_subset(phinfo->ph_eval_at,
! 								  fpinfo->innerrel->relids))
! 				{
! 					/*
! 					 * The PHV coming from an either input should be shippable.
! 					 */
! 					exprs = lappend(exprs, node);
! 				}
! 				else
! 				{
! 					/*
! 					 * The PHV might be shippable, but in any case just fetch
! 					 * expressions required for evaluating the PHV.
! 					 */
! 					List	   *exprs2 = pull_var_clause(node,
! 													PVC_INCLUDE_PLACEHOLDERS);
! 					ListCell   *lc2;
! 
! 					foreach(lc2, exprs2)
! 					{
! 						Node	   *node2 = (Node *) lfirst(lc2);
! 
! 						exprs = lappend(exprs, node2);
! 					}
! 				}
! 			}
! 			else
! 				elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
! 		}
! 
! 		tlist = add_to_flat_tlist(tlist, exprs);
! 	}
! 
! 	/*
! 	 * Fetch expressions required for evaluating local conditions, if any.
! 	 */
! 	if (fpinfo->local_conds)
! 		tlist = add_to_flat_tlist(tlist,
! 								pull_var_clause((Node *) fpinfo->local_conds,
! 												PVC_INCLUDE_PLACEHOLDERS));
  
  	return tlist;
  }
***************
*** 947,953 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  	 * the FROM clause of the query.
  	 */
  	if (!is_subquery)
! 		build_subquery_tlists(context.scanrel);
  
  	/* Construct SELECT clause */
  	deparseSelectSql(tlist, is_subquery, retrieved_attrs, &context);
--- 1026,1032 ----
  	 * the FROM clause of the query.
  	 */
  	if (!is_subquery)
! 		build_subquery_tlists(root, context.scanrel);
  
  	/* Construct SELECT clause */
  	deparseSelectSql(tlist, is_subquery, retrieved_attrs, &context);
***************
*** 1460,1465 **** deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1539,1545 ----
  
  	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
  		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 	Assert(fpinfo->reltarget_is_shippable);
  	Assert(fpinfo->local_conds == NIL);
  
  	/* If is_subquery_rel is true, deparse the relation as a subquery. */
***************
*** 1892,1900 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.  However, we must be
! 		 * careful; the table could be beneath an outer join, in which case it
! 		 * must go to NULL whenever the rest of the row does.
  		 */
  		Oid			fetchval = 0;
  
--- 1972,1978 ----
  	{
  		/*
  		 * All other system attributes are fetched as 0, except for table OID,
! 		 * which is fetched as the local table OID.
  		 */
  		Oid			fetchval = 0;
  
***************
*** 1904,1917 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  			fetchval = rte->relid;
  		}
  
! 		if (qualify_col)
! 		{
! 			appendStringInfoString(buf, "CASE WHEN (");
! 			ADD_REL_QUALIFIER(buf, varno);
! 			appendStringInfo(buf, "*)::text IS NOT NULL THEN %u END", fetchval);
! 		}
! 		else
! 			appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
--- 1982,1988 ----
  			fetchval = rte->relid;
  		}
  
! 		appendStringInfo(buf, "%u", fetchval);
  	}
  	else if (varattno == 0)
  	{
***************
*** 1942,1969 **** deparseColumnRef(StringInfo buf, int varno, int varattno, PlannerInfo *root,
  		attrs_used = bms_add_member(NULL,
  									0 - FirstLowInvalidHeapAttributeNumber);
  
- 		/*
- 		 * In case the whole-row reference is under an outer join then it has
- 		 * to go NULL whenever the rest of the row goes NULL. Deparsing a join
- 		 * query would always involve multiple relations, thus qualify_col
- 		 * would be true.
- 		 */
- 		if (qualify_col)
- 		{
- 			appendStringInfoString(buf, "CASE WHEN (");
- 			ADD_REL_QUALIFIER(buf, varno);
- 			appendStringInfo(buf, "*)::text IS NOT NULL THEN ");
- 		}
- 
  		appendStringInfoString(buf, "ROW(");
  		deparseTargetList(buf, root, varno, rel, false, attrs_used, qualify_col,
  						  &retrieved_attrs);
  		appendStringInfoString(buf, ")");
  
- 		/* Complete the CASE WHEN statement started above. */
- 		if (qualify_col)
- 			appendStringInfo(buf, " END");
- 
  		heap_close(rel, NoLock);
  		bms_free(attrs_used);
  	}
--- 2013,2023 ----
***************
*** 2136,2141 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 2190,2198 ----
  		case T_Aggref:
  			deparseAggref((Aggref *) node, context);
  			break;
+ 		case T_PlaceHolderVar:
+ 			deparseExprInPlaceHolderVar((PlaceHolderVar *) node, context);
+ 			break;
  		default:
  			elog(ERROR, "unsupported expression type for deparse: %d",
  				 (int) nodeTag(node));
***************
*** 2166,2172 **** deparseVar(Var *node, deparse_expr_cxt *context)
  	 * use the relation and column alias provided by the subquery, instead of
  	 * the remote name.
  	 */
! 	if (is_subquery_var(node, context->scanrel, &relno, &colno))
  	{
  		appendStringInfo(context->buf, "%s%d.%s%d",
  						 SUBQUERY_REL_ALIAS_PREFIX, relno,
--- 2223,2230 ----
  	 * use the relation and column alias provided by the subquery, instead of
  	 * the remote name.
  	 */
! 	if (is_subquery_expr((Expr *) node, context->root, context->scanrel,
! 						 &relno, &colno))
  	{
  		appendStringInfo(context->buf, "%s%d.%s%d",
  						 SUBQUERY_REL_ALIAS_PREFIX, relno,
***************
*** 2860,2865 **** appendAggOrderBy(List *orderList, List *targetList, deparse_expr_cxt *context)
--- 2918,2948 ----
  }
  
  /*
+  * Deparse a PlaceHolderVar's contained expression.
+  */
+ static void
+ deparseExprInPlaceHolderVar(PlaceHolderVar *node, deparse_expr_cxt *context)
+ {
+ 	int			relno;
+ 	int			colno;
+ 
+ 	/*
+ 	 * If the given PHV belongs to a foreign relation deparsed as a subquery,
+ 	 * use the relation and column alias provided by the subquery instead.
+ 	 */
+ 	if (is_subquery_expr((Expr *) node, context->root, context->scanrel,
+ 						 &relno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SUBQUERY_REL_ALIAS_PREFIX, relno,
+ 						 SUBQUERY_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
+ 	deparseExpr(node->phexpr, context);
+ }
+ 
+ /*
   * Print the representation of a parameter to be sent to the remote side.
   *
   * Note: we always label the Param's type explicitly rather than relying on
***************
*** 3057,3063 **** deparseSortGroupClause(Index ref, List *tlist, deparse_expr_cxt *context)
   * Build the tlists from subqueries present in a given jointree, if any.
   */
  static void
! build_subquery_tlists(RelOptInfo *foreignrel)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
--- 3140,3146 ----
   * Build the tlists from subqueries present in a given jointree, if any.
   */
  static void
! build_subquery_tlists(PlannerInfo *root, RelOptInfo *foreignrel)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
***************
*** 3077,3083 **** build_subquery_tlists(RelOptInfo *foreignrel)
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
  
  		if (fpinfo2->is_subquery_rel)
! 			fpinfo2->tlist = build_tlist_to_deparse(outerrel);
  	}
  	/* Likewise for the inner relation. */
  	if (bms_is_subset(innerrel->relids, fpinfo->lower_subquery_rels))
--- 3160,3166 ----
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
  
  		if (fpinfo2->is_subquery_rel)
! 			fpinfo2->tlist = build_tlist_to_deparse(root, outerrel);
  	}
  	/* Likewise for the inner relation. */
  	if (bms_is_subset(innerrel->relids, fpinfo->lower_subquery_rels))
***************
*** 3085,3125 **** build_subquery_tlists(RelOptInfo *foreignrel)
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
  
  		if (fpinfo2->is_subquery_rel)
! 			fpinfo2->tlist = build_tlist_to_deparse(innerrel);
  	}
  
  	/* Recurse into the outer relation. */
! 	build_subquery_tlists(outerrel);
  	/* Likewise for the inner relation. */
! 	build_subquery_tlists(innerrel);
  }
  
  /*
!  * Returns true if the Var belongs to a foreign relation deparsed as a subquery.
!  * Returns false otherwise.  When returning true, this function sets *relno and
!  * *colno to the IDs for the relation and column alias provided by the subquery,
!  * respectively.
   */
  static bool
! is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
  	RelOptInfo *innerrel = fpinfo->innerrel;
  
  	if (foreignrel->reloptkind != RELOPT_JOINREL)
  		return false;
- 	if (!bms_is_member(node->varno, fpinfo->lower_subquery_rels))
- 		return false;
  
! 	if (bms_is_member(node->varno, outerrel->relids))
  	{
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
  
  		/*
! 		 * If the outer relation is deparsed as a subquery, the Var must be an
! 		 * output column of the subquery; get the IDs for the relation and
! 		 * column alias provided by the subquery.
  		 */
  		if (fpinfo2->is_subquery_rel)
  		{
--- 3168,3247 ----
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
  
  		if (fpinfo2->is_subquery_rel)
! 			fpinfo2->tlist = build_tlist_to_deparse(root, innerrel);
  	}
  
  	/* Recurse into the outer relation. */
! 	build_subquery_tlists(root, outerrel);
  	/* Likewise for the inner relation. */
! 	build_subquery_tlists(root, innerrel);
  }
  
  /*
!  * Returns true if the given expression belongs to a foreign relation deparsed
!  * as a subquery.  Returns false otherwise.  When returning true, this function
!  * sets *relno and *colno to the IDs for the relation and column alias provided
!  * by the subquery, respectively.
   */
  static bool
! is_subquery_expr(Expr *node, PlannerInfo *root, RelOptInfo *foreignrel,
! 				 int *relno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	RelOptInfo *outerrel = fpinfo->outerrel;
  	RelOptInfo *innerrel = fpinfo->innerrel;
+ 	bool		is_outer_var;
  
  	if (foreignrel->reloptkind != RELOPT_JOINREL)
  		return false;
  
! 	if (IsA(node, Var))
! 	{
! 		Var		   *var = (Var *) node;
! 
! 		/*
! 		 * The Var can't be a subquery output column if it doesn't belong to
! 		 * any foreign relation deparsed as a lower subquery.
! 		 */
! 		if (!bms_is_member(var->varno, fpinfo->lower_subquery_rels))
! 			return false;
! 
! 		is_outer_var = bms_is_member(var->varno, outerrel->relids);
! 	}
! 	else if (IsA(node, PlaceHolderVar))
! 	{
! 		PlaceHolderVar *phv = (PlaceHolderVar *) node;
! 		PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
! 		bool		is_inner_var;
! 
! 		/*
! 		 * The PHV can't be a subquery output column if it doesn't belong to
! 		 * any foreign relation deparsed as a lower subquery.
! 		 */
! 		if (!bms_is_subset(phinfo->ph_eval_at, fpinfo->lower_subquery_rels))
! 			return false;
! 
! 		is_outer_var = bms_is_subset(phinfo->ph_eval_at, outerrel->relids);
! 		is_inner_var = bms_is_subset(phinfo->ph_eval_at, innerrel->relids);
! 
! 		/*
! 		 * The PHV computed here but not in either input can't be a subquery
! 		 * output column.
! 		 */
! 		if (!is_outer_var && !is_inner_var)
! 			return false;
! 	}
! 	else
! 		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
! 
! 	if (is_outer_var)
  	{
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) outerrel->fdw_private;
  
  		/*
! 		 * If the outer relation is deparsed as a subquery, the Var/PHV must
! 		 * be an output column of the subquery; get the IDs for the relation
! 		 * and column alias provided by the subquery.
  		 */
  		if (fpinfo2->is_subquery_rel)
  		{
***************
*** 3128,3145 **** is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
  		}
  
  		/* Otherwise, recurse into the outer relation. */
! 		return is_subquery_var(node, outerrel, relno, colno);
  	}
  	else
  	{
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
  
- 		Assert(bms_is_member(node->varno, innerrel->relids));
- 
  		/*
! 		 * If the inner relation is deparsed as a subquery, the Var must be an
! 		 * output column of the subquery; get the IDs for the relation and
! 		 * column alias provided by the subquery.
  		 */
  		if (fpinfo2->is_subquery_rel)
  		{
--- 3250,3265 ----
  		}
  
  		/* Otherwise, recurse into the outer relation. */
! 		return is_subquery_expr(node, root, outerrel, relno, colno);
  	}
  	else
  	{
  		PgFdwRelationInfo *fpinfo2 = (PgFdwRelationInfo *) innerrel->fdw_private;
  
  		/*
! 		 * If the inner relation is deparsed as a subquery, the Var/PHV must
! 		 * be an output column of the subquery; get the IDs for the relation
! 		 * and column alias provided by the subquery.
  		 */
  		if (fpinfo2->is_subquery_rel)
  		{
***************
*** 3148,3168 **** is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
  		}
  
  		/* Otherwise, recurse into the inner relation. */
! 		return is_subquery_var(node, innerrel, relno, colno);
  	}
  }
  
  /*
!  * Get the IDs for the relation and column alias to the Var belonging to the
!  * given foreignrel.  They are returned into *relno and *colno, respectively.
   */
  static void
! get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
  							  int *relno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	TargetEntry *tle;
  
  	/* Get the relation alias ID */
  	*relno = fpinfo->relation_index;
  
--- 3268,3292 ----
  		}
  
  		/* Otherwise, recurse into the inner relation. */
! 		return is_subquery_expr(node, root, innerrel, relno, colno);
  	}
  }
  
  /*
!  * Get the IDs for the relation and column alias to the given expression
!  * belonging to the given foreignrel.  They are returned into *relno and
!  * *colno, respectively.
   */
  static void
! get_relation_column_alias_ids(Expr *node, RelOptInfo *foreignrel,
  							  int *relno, int *colno)
  {
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
  	TargetEntry *tle;
  
+ 	if (!IsA(node, Var) && !IsA(node, PlaceHolderVar))
+ 		elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node));
+ 
  	/* Get the relation alias ID */
  	*relno = fpinfo->relation_index;
  
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1619,1626 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                                
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1619,1626 ----
  -- tests whole-row reference for row marks
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE OF t1;
!                                                                                                                                                                         QUERY PLAN                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1628,1634 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1628,1634 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1663,1670 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                                                                         QUERY PLAN                                                                                                                                                                                                                        
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1663,1670 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR UPDATE;
!                                                                                                                                                                              QUERY PLAN                                                                                                                                                                              
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1672,1678 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR UPDATE OF r1 FOR UPDATE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1672,1678 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR UPDATE) s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1708,1715 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                                                                QUERY PLAN                                                                                                                                                                                                               
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1708,1715 ----
  -- join two tables with FOR SHARE clause
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE OF t1;
!                                                                                                                                                                        QUERY PLAN                                                                                                                                                                        
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1717,1723 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1717,1723 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1752,1759 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                                                                        QUERY PLAN                                                                                                                                                                                                                       
! --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
--- 1752,1759 ----
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10 FOR SHARE;
!                                                                                                                                                                             QUERY PLAN                                                                                                                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
     ->  LockRows
***************
*** 1761,1767 **** SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT r1."C 1", r1.c3, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r2."C 1", 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST FOR SHARE OF r1 FOR SHARE OF r2
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
--- 1761,1767 ----
           ->  Foreign Scan
                 Output: t1.c1, t2.c1, t1.c3, t1.*, t2.*
                 Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!                Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s2.c1, s2.c2 FROM ((SELECT "C 1", c3, ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s1(c1, c2, c3) INNER JOIN (SELECT "C 1", ROW("C 1", c2, c3, c4, c5, c6, c7, c8) FROM "S 1"."T 1" FOR SHARE) s2(c1, c2) ON (((s1.c1 = s2.c1)))) ORDER BY s1.c2 ASC NULLS LAST, s1.c1 ASC NULLS LAST
                 ->  Merge Join
                       Output: t1.c1, t1.c3, t1.*, t2.c1, t2.*
                       Merge Cond: (t1.c1 = t2.c1)
***************
*** 1831,1844 **** WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                                                    QUERY PLAN                                                                                                                                                                                                    
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT r1.ctid, CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1."C 1", r1.c2, r1.c3, r1.c4, r1.c5, r1.c6, r1.c7, r1.c8) END, r1."C 1", r1.c3, 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."C 1" = r2."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
--- 1831,1844 ----
  -- ctid with whole-row reference
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
!                                                                                                                                                                     QUERY PLAN                                                                                                                                                                     
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Limit
     Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
     ->  Foreign Scan
           Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
           Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
!          Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s1.c4, s2.c1 FROM ((SELECT ctid, ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1", c3 FROM "S 1"."T 1") s1(c1, c2, c3, c4) INNER JOIN (SELECT ROW("C 1", c2, c3, c4, c5, c6, c7, c8), "C 1" FROM "S 1"."T 1") s2(c1, c2) ON (((s1.c3 = s2.c2)))) ORDER BY s1.c4 ASC NULLS LAST, s1.c3 ASC NULLS LAST
  (6 rows)
  
  -- SEMI JOIN, not pushed down
***************
*** 2167,2191 **** SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
     1
  (10 rows)
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                         QUERY PLAN                                                         
! ---------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: (13), ft2.c1
!    Join Filter: (13 = ft2.c1)
!    ->  Foreign Scan on public.ft2
!          Output: ft2.c1
!          Remote SQL: SELECT "C 1" FROM "S 1"."T 1" WHERE (("C 1" >= 10)) AND (("C 1" <= 15)) ORDER BY "C 1" ASC NULLS LAST
!    ->  Materialize
!          Output: (13)
!          ->  Foreign Scan on public.ft1
!                Output: 13
!                Remote SQL: SELECT NULL FROM "S 1"."T 1" WHERE (("C 1" = 13))
! (11 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
--- 2167,2182 ----
     1
  (10 rows)
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
!                                                                                               QUERY PLAN                                                                                              
! ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: (13), ft2.c1
!    Relations: (public.ft2) LEFT JOIN (public.ft1)
!    Remote SQL: SELECT r2."C 1", s4.c1 FROM ("S 1"."T 1" r2 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s4(c1) ON (((13 = r2."C 1")))) WHERE ((r2."C 1" >= 10)) AND ((r2."C 1" <= 15))
! (4 rows)
  
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
   a  | c1 
***************
*** 2198,2221 **** SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 O
      | 15
  (6 rows)
  
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                     QUERY PLAN                                                                                     
! -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Nested Loop Left Join
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Join Filter: (ft4.c1 = ft1.c1)
!    ->  Foreign Scan on public.ft4
!          Output: ft4.c1, ft4.c2, ft4.c3
!          Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 10)) AND ((c1 <= 15))
!    ->  Materialize
!          Output: ft1.c1, ft2.c1, (13)
!          ->  Foreign Scan
!                Output: ft1.c1, ft2.c1, 13
!                Relations: (public.ft1) INNER JOIN (public.ft2)
!                Remote SQL: SELECT r4."C 1", r5."C 1" FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12)))) ORDER BY r4."C 1" ASC NULLS LAST
! (12 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
--- 2189,2203 ----
      | 15
  (6 rows)
  
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
!                                                                                                                                           QUERY PLAN                                                                                                                                          
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, (13), ft1.c1, ft2.c1
!    Relations: (public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2))
!    Remote SQL: SELECT r1.c1, s7.c1, s7.c2, s7.c3 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4."C 1", r5."C 1", 13 FROM ("S 1"."T 1" r4 INNER JOIN "S 1"."T 1" r5 ON (((r5."C 1" = 12)) AND ((r4."C 1" = 12))))) s7(c1, c2, c3) ON (((r1.c1 = s7.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
! (4 rows)
  
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
   c1 | a  | b  | c  
***************
*** 2225,2240 **** SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT
   14 |    |    |   
  (3 rows)
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                                 QUERY PLAN                                                                                                                                 
! ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT CASE WHEN (r1.*)::text IS NOT NULL THEN ROW(r1.c1, r1.c2, r1.c3) END, r1.c1, r1.c2, r1.c3, r2.c1, r2.c2 FROM ("S 1"."T 4" r1 INNER JOIN "S 1"."T 3" r2 ON (((r1.c1 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY r1.c1 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
--- 2207,2287 ----
   14 |    |    |   
  (3 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                               QUERY PLAN                                                                                                                                                                               
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft2.c1, (13), (13), ft2_1.c1
+    Relations: (public.ft2) LEFT JOIN ((public.ft2) LEFT JOIN (public.ft1))
+    Remote SQL: SELECT r1."C 1", s8.c1, s8.c2, s8.c3 FROM ("S 1"."T 1" r1 LEFT JOIN (SELECT r5."C 1", 13, s7.c1 FROM ("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s7(c1) ON (((13 = r5."C 1")))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))) s8(c1, c2, c3) ON (((r1."C 1" = 13)))) WHERE ((r1."C 1" >= 10)) AND ((r1."C 1" <= 15))
+ (4 rows)
+ 
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 
+ ----+----+----+----
+  10 |    |    |   
+  11 |    |    |   
+  12 |    |    |   
+  13 | 13 |    | 10
+  13 | 13 |    | 11
+  13 | 13 |    | 12
+  13 | 13 | 13 | 13
+  13 | 13 |    | 14
+  13 | 13 |    | 15
+  14 |    |    |   
+  15 |    |    |   
+ (11 rows)
+ 
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+                                                                                                                             QUERY PLAN                                                                                                                            
+ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft2.c1, 13, (13), ft2_1.c1
+    Relations: ((public.ft2) LEFT JOIN (public.ft1)) LEFT JOIN (public.ft2)
+    Remote SQL: SELECT r5."C 1", r1."C 1", s7.c1 FROM (("S 1"."T 1" r5 LEFT JOIN (SELECT 13 FROM "S 1"."T 1" WHERE (("C 1" = 13))) s7(c1) ON (((13 = r5."C 1")))) LEFT JOIN "S 1"."T 1" r1 ON (((r1."C 1" = 13)))) WHERE ((r5."C 1" >= 10)) AND ((r5."C 1" <= 15))
+ (4 rows)
+ 
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+  c1 | a2 | b2 | c2 
+ ----+----+----+----
+  13 | 13 |    | 10
+  13 | 13 |    | 11
+  13 | 13 |    | 12
+  13 | 13 | 13 | 13
+  13 | 13 |    | 14
+  13 | 13 |    | 15
+ (6 rows)
+ 
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+                                                                                                                                                                                                                                                 QUERY PLAN                                                                                                                                                                                                                                                
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ((13 IS NOT NULL)), ft4_1.c1, (13), ft1.c1, ft2.c1
+    Relations: (public.ft4) LEFT JOIN ((public.ft4) LEFT JOIN ((public.ft1) INNER JOIN (public.ft2)))
+    Remote SQL: SELECT r1.c1, s11.c1, s11.c2, s11.c3, s11.c4, s11.c5 FROM ("S 1"."T 3" r1 LEFT JOIN (SELECT r4.c1, s10.c1, s10.c2, (s10.c3 IS NOT NULL), s10.c3 FROM ("S 1"."T 3" r4 LEFT JOIN (SELECT r7."C 1", r8."C 1", 13 FROM ("S 1"."T 1" r7 INNER JOIN "S 1"."T 1" r8 ON (((r8."C 1" = 12)) AND ((r7."C 1" = 12))))) s10(c1, c2, c3) ON (((r4.c1 = s10.c1)))) WHERE ((r4.c1 >= 10)) AND ((r4.c1 <= 15))) s11(c1, c2, c3, c4, c5) ON (((r1.c1 = s11.c1)))) WHERE ((r1.c1 >= 10)) AND ((r1.c1 <= 15))
+ (4 rows)
+ 
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+  c1 | a2 | b2 | c2 | d2 | e2 
+ ----+----+----+----+----+----
+  10 | f  | 10 |    |    |   
+  12 | t  | 12 | 13 | 12 | 12
+  14 | f  | 14 |    |    |   
+ (3 rows)
+ 
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
!                                                                                                                               QUERY PLAN                                                                                                                              
! ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Foreign Scan
     Output: ft5.*, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2
     Relations: (public.ft5) INNER JOIN (public.ft4)
!    Remote SQL: SELECT s1.c1, s1.c2, s1.c3, s1.c4, r2.c1, r2.c2 FROM ((SELECT ROW(c1, c2, c3), c1, c2, c3 FROM "S 1"."T 4") s1(c1, c2, c3, c4) INNER JOIN "S 1"."T 3" r2 ON (((s1.c2 = r2.c1)) AND ((r2.c1 >= 10)) AND ((r2.c1 <= 30)))) ORDER BY s1.c2 ASC NULLS LAST
  (4 rows)
  
  SELECT ft5, ft5.c1, ft5.c2, ft5.c3, ft4.c1, ft4.c2 FROM ft5 left join ft4 on ft5.c1 = ft4.c1 WHERE ft4.c1 BETWEEN 10 and 30 ORDER BY ft5.c1, ft4.c1;
***************
*** 4136,4149 **** 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)
--- 4183,4196 ----
  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)
***************
*** 4279,4292 **** 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
!                                                                                                                               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)
--- 4326,4339 ----
  
  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)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 28,33 ****
--- 28,34 ----
  #include "optimizer/clauses.h"
  #include "optimizer/pathnode.h"
  #include "optimizer/paths.h"
+ #include "optimizer/placeholder.h"
  #include "optimizer/planmain.h"
  #include "optimizer/restrictinfo.h"
  #include "optimizer/var.h"
***************
*** 677,682 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 678,743 ----
  	fpinfo->relation_index = baserel->relid;
  	/* Initialize the tlist. */
  	fpinfo->tlist = NIL;
+ 
+ 	/*
+ 	 * Determine whether expressions in the relation's reltarget are shippable
+ 	 * and whether there are any non-Vars in those expressions.
+ 	 *
+ 	 * Note: whole-row Vars and system columns other than ctid and oid are
+ 	 * treated like PHVs.  (See comments in postgres_fdw.h.)
+ 	 *
+ 	 * Note: if the relation is an appendrel child, it isn't in the main join
+ 	 * tree, so those information is not used.
+ 	 */
+ 	if (baserel->reloptkind == RELOPT_BASEREL)
+ 	{
+ 		bool		reltarget_is_shippable = true;
+ 		bool		reltarget_has_non_vars = false;
+ 
+ 		/*
+ 		 * Check to see if any PHVs are requested from the baserel.
+ 		 */
+ 		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))
+ 			{
+ 				reltarget_has_non_vars = true;
+ 
+ 				if (!is_foreign_expr(root, baserel, (Expr *) phinfo->ph_var))
+ 				{
+ 					reltarget_is_shippable = false;
+ 					break;
+ 				}
+ 			}
+ 		}
+ 
+ 		/*
+ 		 * If not, check to see if the baserel is required to emit any
+ 		 * whole-row Vars or system columns other than ctid and oid.
+ 		 */
+ 		if (!reltarget_has_non_vars)
+ 		{
+ 			int			i;
+ 
+ 			Assert(reltarget_is_shippable);
+ 
+ 			for (i = baserel->min_attr; i <= 0; i++)
+ 			{
+ 				if (i == SelfItemPointerAttributeNumber ||
+ 					i == ObjectIdAttributeNumber)
+ 					continue;
+ 
+ 				if (!bms_is_empty(baserel->attr_needed[i - baserel->min_attr]))
+ 					reltarget_has_non_vars = true;
+ 			}
+ 		}
+ 
+ 		fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 		fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 	}
  }
  
  /*
***************
*** 1200,1206 **** postgresGetForeignPlan(PlannerInfo *root,
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
--- 1261,1267 ----
  		local_exprs = fpinfo->local_conds;
  
  		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
***************
*** 2540,2546 **** estimate_path_cost_size(PlannerInfo *root,
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL ||
  			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 			fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
--- 2601,2607 ----
  		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL ||
  			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 			fdw_scan_tlist = build_tlist_to_deparse(root, foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
***************
*** 4056,4061 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4117,4124 ----
  	ListCell   *lc;
  	List	   *joinclauses;
  	List	   *otherclauses;
+ 	bool		reltarget_is_shippable = true;
+ 	bool		reltarget_has_non_vars = false;
  
  	/*
  	 * We support pushing down INNER, LEFT, RIGHT and FULL OUTER joins.
***************
*** 4085,4090 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4148,4161 ----
  	if (fpinfo_o->local_conds || fpinfo_i->local_conds)
  		return false;
  
+ 	/*
+ 	 * If the reltargets of joining relations are not shippable, those are
+ 	 * required to be evaluated locally, so the join can not be pushed down.
+ 	 */
+ 	if (!fpinfo_o->reltarget_is_shippable ||
+ 		!fpinfo_i->reltarget_is_shippable)
+ 		return false;
+ 
  	/* Separate restrict list into join quals and quals on join relation */
  	if (IS_OUTER_JOIN(jointype))
  		extract_actual_join_clauses(extra->restrictlist, &joinclauses, &otherclauses);
***************
*** 4110,4135 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			return false;
  	}
  
- 	/*
- 	 * deparseExplicitTargetList() isn't smart enough to handle anything other
- 	 * than a Var.  In particular, if there's some PlaceHolderVar that would
- 	 * need to be evaluated within this join tree (because there's an upper
- 	 * reference to a quantity that may go to NULL as a result of an outer
- 	 * join), then we can't try to push the join down because we'll fail when
- 	 * we get to deparseExplicitTargetList().  However, a PlaceHolderVar that
- 	 * needs to be evaluated *at the top* of this join tree is OK, because we
- 	 * can do that locally after fetching the results from the remote side.
- 	 */
- 	foreach(lc, root->placeholder_list)
- 	{
- 		PlaceHolderInfo *phinfo = lfirst(lc);
- 		Relids		relids = joinrel->relids;
- 
- 		if (bms_is_subset(phinfo->ph_eval_at, relids) &&
- 			bms_nonempty_difference(relids, phinfo->ph_eval_at))
- 			return false;
- 	}
- 
  	/* Save the join clauses, for later use. */
  	fpinfo->joinclauses = joinclauses;
  
--- 4181,4186 ----
***************
*** 4187,4213 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
  	 */
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
--- 4238,4289 ----
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
+ 	 *
+ 	 * If the joining relation has reltarget_has_non_vars=TRUE, then we need
+ 	 * to deparse the relation as a subquery.  Remember it in the fpinfo of
+ 	 * the joining relation so that the deparser can take appropriate action.
+ 	 * In that case, don't pull up the remote_conds into the joinclauses or
+ 	 * remote_conds of this relation because the remote_conds is transformed
+ 	 * into the subquery's WHERE clause.
  	 */
  	switch (jointype)
  	{
  		case JOIN_INNER:
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo_i->is_subquery_rel = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo_o->is_subquery_rel = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_LEFT:
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo_i->is_subquery_rel = true;
! 			else
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_i->remote_conds));
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo_o->is_subquery_rel = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_o->remote_conds));
  			break;
  
  		case JOIN_RIGHT:
! 			if (fpinfo_o->reltarget_has_non_vars)
! 				fpinfo_o->is_subquery_rel = true;
! 			else
! 				fpinfo->joinclauses = list_concat(fpinfo->joinclauses,
  										  list_copy(fpinfo_o->remote_conds));
! 			if (fpinfo_i->reltarget_has_non_vars)
! 				fpinfo_i->is_subquery_rel = true;
! 			else
! 				fpinfo->remote_conds = list_concat(fpinfo->remote_conds,
  										  list_copy(fpinfo_i->remote_conds));
  			break;
  
***************
*** 4217,4241 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			 * In this case, if any of the joining relations has conditions,
  			 * we need to deparse that relation as a subquery so that the
  			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo of the joining relation so that the deparser can
! 			 * take appropriate action.  Also, save the relids of base
! 			 * relations covered by the joining relation into the fpinfo of
! 			 * this relation.
  			 */
! 			if (fpinfo_o->remote_conds)
! 			{
  				fpinfo_o->is_subquery_rel = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
  				fpinfo_i->is_subquery_rel = true;
- 				fpinfo->lower_subquery_rels =
- 					bms_add_members(fpinfo->lower_subquery_rels,
- 									innerrel->relids);
- 			}
  			break;
  
  		default:
--- 4293,4304 ----
  			 * In this case, if any of the joining relations has conditions,
  			 * we need to deparse that relation as a subquery so that the
  			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo of the joining relation as well.
  			 */
! 			if (fpinfo_o->reltarget_has_non_vars || fpinfo_o->remote_conds)
  				fpinfo_o->is_subquery_rel = true;
! 			if (fpinfo_i->reltarget_has_non_vars || fpinfo_i->remote_conds)
  				fpinfo_i->is_subquery_rel = true;
  			break;
  
  		default:
***************
*** 4244,4249 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4307,4325 ----
  	}
  
  	/*
+ 	 * If the joining relation is deparsed as a subquery, save the relids of
+ 	 * base relations covered by the subquery for later use by the deparser.
+ 	 */
+ 	if (fpinfo_o->is_subquery_rel)
+ 		fpinfo->lower_subquery_rels =
+ 			bms_add_members(fpinfo->lower_subquery_rels,
+ 							outerrel->relids);
+ 	if (fpinfo_i->is_subquery_rel)
+ 		fpinfo->lower_subquery_rels =
+ 			bms_add_members(fpinfo->lower_subquery_rels,
+ 							innerrel->relids);
+ 
+ 	/*
  	 * For an inner join, all restrictions can be treated alike. Treating the
  	 * pushed down conditions as join conditions allows a top level full outer
  	 * join to be deparsed without requiring subqueries.
***************
*** 4259,4264 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4335,4375 ----
  	fpinfo->pushdown_safe = true;
  
  	/*
+ 	 * Determine whether expressions in the relation's reltarget are shippable
+ 	 * and whether there are any non-Vars in those expressions.
+ 	 *
+ 	 * Note: whole-row Vars and system columns other than ctid and oid of a
+ 	 * base relation are handled as output columns of a leaf subquery.  So,
+ 	 * no need to be careful about those columns here.
+ 	 */
+ 	foreach(lc, joinrel->reltarget->exprs)
+ 	{
+ 		Node	   *node = (Node *) lfirst(lc);
+ 
+ 		if (IsA(node, PlaceHolderVar))
+ 		{
+ 			PlaceHolderVar *phv = (PlaceHolderVar *) node;
+ 			PlaceHolderInfo *phinfo = find_placeholder_info(root, phv, false);
+ 
+ 			/* Ignore the PHV if it has bubbled up from an either input. */
+ 			if (bms_is_subset(phinfo->ph_eval_at, outerrel->relids) ||
+ 				bms_is_subset(phinfo->ph_eval_at, innerrel->relids))
+ 				continue;
+ 
+ 			reltarget_has_non_vars = true;
+ 
+ 			if (!is_foreign_expr(root, joinrel, (Expr *) node))
+ 			{
+ 				reltarget_is_shippable = false;
+ 				break;
+ 			}
+ 		}
+ 	}
+ 
+ 	fpinfo->reltarget_is_shippable = reltarget_is_shippable;
+ 	fpinfo->reltarget_has_non_vars = reltarget_has_non_vars;
+ 
+ 	/*
  	 * If user is willing to estimate cost for a scan of either of the joining
  	 * relations using EXPLAIN, he intends to estimate scans on that relation
  	 * more accurately. Then, it makes sense to estimate the cost the join
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 33,38 **** typedef struct PgFdwRelationInfo
--- 33,61 ----
  	bool		pushdown_safe;
  
  	/*
+ 	 * Flags on the reltarget of the relation.
+ 	 *
+ 	 * If each expression in the relation's reltarget is shippable (i.e.,
+ 	 * computable on the remote side), we set reltarget_is_shippalbe to TRUE.
+ 	 * The flag is used to determine whether the relation can be joined with
+ 	 * any other foreign table (or join) on the remote side.  Note that Vars
+ 	 * in the reltarget are shippable, so if any PHV in the reltarget is
+ 	 * shippable, then the flag is set TRUE.  Also, if there are any PHVs in
+ 	 * in the reltarget, we set reltarget_has_non_vars to TRUE.  In that case,
+ 	 * we deparse the relation as a subquery emitting the PHVs when creating
+ 	 * a remote query for doing a join of the relation with any other foreign
+ 	 * table remotely.  Note that whole-row Vars and system columns other than
+ 	 * ctid and oid in the reltarget of a base relation are treated like PHVs,
+ 	 * because (1) we deparse the whole-row Vars as ROW() expressions and the
+ 	 * system columns as 0, except for tableoid, in which case it is deparsed
+ 	 * as a valid value for the local table OID, and (2) the base table could
+ 	 * be beneath an outer join, in which case those columns must go to NULL
+ 	 * whenever the rest of the row does.
+ 	 */
+ 	bool		reltarget_is_shippable; /* are reltarget exprs shippable? */
+ 	bool		reltarget_has_non_vars;	/* are there any non-Var exprs? */
+ 
+ 	/*
  	 * Restriction clauses, divided into safe and unsafe to pushdown subsets.
  	 *
  	 * For a base foreign relation this is a list of clauses along-with
***************
*** 172,178 **** extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys, bool is_subquery,
--- 195,201 ----
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
! extern List *build_tlist_to_deparse(PlannerInfo *root, RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys, bool is_subquery,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 510,526 **** EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- non-Var items in targelist of the nullable rel of a join preventing
! -- push-down in some cases
! -- unable to push {ft1, ft2}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
- 
- -- ok to push {ft1, ft2} but not {ft1, ft2, ft4}
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
--- 510,531 ----
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM ft1 t2, ft2 t3 WHERE t2.c1 = t3.c1 AND t2.c2 = t1.c2) q ORDER BY t1."C 1" OFFSET 10 LIMIT 10;
  
! -- check join pushdown in situations where PHVs are involved
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  SELECT q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15;
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
  SELECT ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+ SELECT ft2.c1, q2.* FROM ft2 LEFT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2) WHERE ft2.c1 BETWEEN 10 AND 15;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+ SELECT ft2.c1, q2.* FROM ft2 RIGHT JOIN (SELECT 13, q.a, ft2.c1 FROM (SELECT 13 FROM ft1 WHERE c1 = 13) q(a) RIGHT JOIN ft2 ON (q.a = ft2.c1) WHERE ft2.c1 BETWEEN 10 AND 15) q2(a2, b2, c2) ON (ft2.c1 = q2.a2);
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
+ SELECT ft4.c1, q2.* FROM ft4 LEFT JOIN (SELECT (q.a IS NOT NULL), ft4.c1, q.* FROM ft4 LEFT JOIN (SELECT 13, ft1.c1, ft2.c1 FROM ft1 RIGHT JOIN ft2 ON (ft1.c1 = ft2.c1) WHERE ft1.c1 = 12) q(a, b, c) ON (ft4.c1 = q.b) WHERE ft4.c1 BETWEEN 10 AND 15) q2(a2, b2, c2, d2, e2) ON (ft4.c1 = q2.b2) WHERE ft4.c1 BETWEEN 10 AND 15;
  
  -- join with nullable side with some columns with null values
  UPDATE ft5 SET c3 = null where c1 % 9 = 0;
#77Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#76)
Re: Push down more full joins in postgres_fdw

I updated the patch a bit further: simplified the function name
(s/build_subquery_rel_tlists/build_subquery_tlists/), and revised comments a
little bit. Attached is an updated version
(postgres-fdw-subquery-support-v14.patch). And I rebased another patch for
PHVs against that patch, which is also attached
(postgres-fdw-phv-pushdown-v14.patch).

Few comments

In build_subquery_tlists(), why don't we handle base relations?
+   if (foreignrel->reloptkind != RELOPT_JOINREL)
+       return;

Also, in this function, if fpinfo->tlist is already set, why do we want to
build it again?

In build_tlist_to_deparse(), if fpinfo->tlist for the given relation is set, we
should just return it rather than constructing it again.

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

#78Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#77)
Re: Push down more full joins in postgres_fdw

On 2017/01/03 17:28, Ashutosh Bapat wrote:

I wrote:

I updated the patch a bit further: simplified the function name
(s/build_subquery_rel_tlists/build_subquery_tlists/), and revised comments a
little bit. Attached is an updated version
(postgres-fdw-subquery-support-v14.patch).

Few comments

Thanks for the comments!

In build_subquery_tlists(), why don't we handle base relations?
+   if (foreignrel->reloptkind != RELOPT_JOINREL)
+       return;

The reason for that is we don't need to handle the baserel cases; the
tlist for a base relation, if needed, would be created while recursing
into a join relation that joins the base relation to other base/join
relation.

Also, in this function, if fpinfo->tlist is already set, why do we want to
build it again?

When this function gets called, fpinfo->tlist isn't set for any base or
join relation that needs to build the tlist, so we always need to build
it for each such relation.

In build_tlist_to_deparse(), if fpinfo->tlist for the given relation is set, we
should just return it rather than constructing it again.

In that function we wouldn't have such cases for base or join relations
needing the tlist.

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

#79Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#78)
Re: Push down more full joins in postgres_fdw

On Thu, Jan 5, 2017 at 5:14 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/03 17:28, Ashutosh Bapat wrote:

I wrote:

I updated the patch a bit further: simplified the function name
(s/build_subquery_rel_tlists/build_subquery_tlists/), and revised
comments a
little bit. Attached is an updated version
(postgres-fdw-subquery-support-v14.patch).

Few comments

Thanks for the comments!

In build_subquery_tlists(), why don't we handle base relations?
+   if (foreignrel->reloptkind != RELOPT_JOINREL)
+       return;

The reason for that is we don't need to handle the baserel cases; the tlist
for a base relation, if needed, would be created while recursing into a join
relation that joins the base relation to other base/join relation.

Right. Sorry, I misunderstood the code. May be a comment would help.

Also, in this function, if fpinfo->tlist is already set, why do we want to
build it again?

When this function gets called, fpinfo->tlist isn't set for any base or join
relation that needs to build the tlist, so we always need to build it for
each such relation.

IIUC, for a relation with use_remote_estimates we will deparse the
query twice and will build the targetlist twice.

In build_tlist_to_deparse(), if fpinfo->tlist for the given relation is
set, we
should just return it rather than constructing it again.

In that function we wouldn't have such cases for base or join relations
needing the tlist.

Same explanation as above.

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

#80Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#79)
Re: Push down more full joins in postgres_fdw

On 2017/01/05 21:11, Ashutosh Bapat wrote:

On Thu, Jan 5, 2017 at 5:14 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/03 17:28, Ashutosh Bapat wrote:

In build_subquery_tlists(), why don't we handle base relations?
+   if (foreignrel->reloptkind != RELOPT_JOINREL)
+       return;

The reason for that is we don't need to handle the baserel cases; the tlist
for a base relation, if needed, would be created while recursing into a join
relation that joins the base relation to other base/join relation.

Right. Sorry, I misunderstood the code. May be a comment would help.

Will add the comment.

Also, in this function, if fpinfo->tlist is already set, why do we want to
build it again?

When this function gets called, fpinfo->tlist isn't set for any base or join
relation that needs to build the tlist, so we always need to build it for
each such relation.

IIUC, for a relation with use_remote_estimates we will deparse the
query twice and will build the targetlist twice.

That's right. We could avoid the duplicate work the way you proposed,
but I was thinking to leave that for another patch. Should we do that
in this patch?

In build_tlist_to_deparse(), if fpinfo->tlist for the given relation is
set, we
should just return it rather than constructing it again.

In that function we wouldn't have such cases for base or join relations
needing the tlist.

Same explanation as above.

Will revise if it's better to do that in this patch.

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

#81Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#80)
Re: Push down more full joins in postgres_fdw

On Thu, Jan 5, 2017 at 5:51 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/05 21:11, Ashutosh Bapat wrote:

On Thu, Jan 5, 2017 at 5:14 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/03 17:28, Ashutosh Bapat wrote:

In build_subquery_tlists(), why don't we handle base relations?
+   if (foreignrel->reloptkind != RELOPT_JOINREL)
+       return;

The reason for that is we don't need to handle the baserel cases; the
tlist
for a base relation, if needed, would be created while recursing into a
join
relation that joins the base relation to other base/join relation.

Right. Sorry, I misunderstood the code. May be a comment would help.

Will add the comment.

Also, in this function, if fpinfo->tlist is already set, why do we want
to
build it again?

When this function gets called, fpinfo->tlist isn't set for any base or
join
relation that needs to build the tlist, so we always need to build it for
each such relation.

IIUC, for a relation with use_remote_estimates we will deparse the
query twice and will build the targetlist twice.

That's right. We could avoid the duplicate work the way you proposed, but I
was thinking to leave that for another patch. Should we do that in this
patch?

If you are agree that the change is needed, it's better to do it in
this patch itself if we can, instead of a one liner patch.

In build_tlist_to_deparse(), if fpinfo->tlist for the given relation is
set, we
should just return it rather than constructing it again.

In that function we wouldn't have such cases for base or join relations
needing the tlist.

Same explanation as above.

Will revise if it's better to do that in this patch.

Thanks.

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

#82Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#81)
Re: Push down more full joins in postgres_fdw

On 2017/01/05 21:38, Ashutosh Bapat wrote:

On Thu, Jan 5, 2017 at 5:51 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/05 21:11, Ashutosh Bapat wrote:

On 2017/01/03 17:28, Ashutosh Bapat wrote:

Also, in this function, if fpinfo->tlist is already set, why do we want
to
build it again?

IIUC, for a relation with use_remote_estimates we will deparse the
query twice and will build the targetlist twice.

That's right. We could avoid the duplicate work the way you proposed, but I
was thinking to leave that for another patch. Should we do that in this
patch?

If you are agree that the change is needed, it's better to do it in
this patch itself if we can, instead of a one liner patch.

While working on this, I noticed a weird case. Consider:

postgres=# explain verbose select 1 from ft1 left join ft2 on (ft1.a =
ft2.a) inner join test on (true);
QUERY PLAN
-------------------------------------------------------------------------------------------------
Nested Loop (cost=100.00..103.06 rows=1 width=4)
Output: 1
-> Foreign Scan (cost=100.00..102.04 rows=1 width=0)
Relations: (public.ft1) LEFT JOIN (public.ft2)
Remote SQL: SELECT NULL FROM (public.t1 r1 LEFT JOIN public.t2
r2 ON (((r1.a = r2.a))))
-> Seq Scan on public.test (cost=0.00..1.01 rows=1 width=0)
Output: test.a, test.b
(7 rows)

In this case the fpinfo->tlist of the foreign join is NIL, so whether or
not the tlist is already built cannot be discriminated by the
fpinfo->tlist. We might need another flag to show that the tlist has
been built already. Since this is an existing issue and we would need
to give careful thought to this, so I'd like to leave this for another
patch.

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

#83Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#82)
Re: Push down more full joins in postgres_fdw

On Wed, Jan 11, 2017 at 5:45 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/05 21:38, Ashutosh Bapat wrote:

On Thu, Jan 5, 2017 at 5:51 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/05 21:11, Ashutosh Bapat wrote:

On 2017/01/03 17:28, Ashutosh Bapat wrote:

Also, in this function, if fpinfo->tlist is already set, why do we
want
to
build it again?

IIUC, for a relation with use_remote_estimates we will deparse the
query twice and will build the targetlist twice.

That's right. We could avoid the duplicate work the way you proposed,
but I
was thinking to leave that for another patch. Should we do that in this
patch?

If you are agree that the change is needed, it's better to do it in
this patch itself if we can, instead of a one liner patch.

While working on this, I noticed a weird case. Consider:

postgres=# explain verbose select 1 from ft1 left join ft2 on (ft1.a =
ft2.a) inner join test on (true);
QUERY PLAN
-------------------------------------------------------------------------------------------------
Nested Loop (cost=100.00..103.06 rows=1 width=4)
Output: 1
-> Foreign Scan (cost=100.00..102.04 rows=1 width=0)
Relations: (public.ft1) LEFT JOIN (public.ft2)
Remote SQL: SELECT NULL FROM (public.t1 r1 LEFT JOIN public.t2 r2
ON (((r1.a = r2.a))))
-> Seq Scan on public.test (cost=0.00..1.01 rows=1 width=0)
Output: test.a, test.b
(7 rows)

In this case the fpinfo->tlist of the foreign join is NIL, so whether or not
the tlist is already built cannot be discriminated by the fpinfo->tlist. We
might need another flag to show that the tlist has been built already.
Since this is an existing issue and we would need to give careful thought to
this, so I'd like to leave this for another patch.

I think in that case, relation's targetlist will also be NIL or
contain no Var node. It wouldn't be expensive to build it again and
again. That's better than maintaining a new flag. This is just a
suggestion, for an additional check, you might want to check
rel->reltarget->exprs for NIL. But I think we don't need it.

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

#84Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#83)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2017/01/12 18:25, Ashutosh Bapat wrote:

On Wed, Jan 11, 2017 at 5:45 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/05 21:11, Ashutosh Bapat wrote:

IIUC, for a relation with use_remote_estimates we will deparse the
query twice and will build the targetlist twice.

While working on this, I noticed a weird case. Consider:

postgres=# explain verbose select 1 from ft1 left join ft2 on (ft1.a =
ft2.a) inner join test on (true);
QUERY PLAN
-------------------------------------------------------------------------------------------------
Nested Loop (cost=100.00..103.06 rows=1 width=4)
Output: 1
-> Foreign Scan (cost=100.00..102.04 rows=1 width=0)
Relations: (public.ft1) LEFT JOIN (public.ft2)
Remote SQL: SELECT NULL FROM (public.t1 r1 LEFT JOIN public.t2 r2
ON (((r1.a = r2.a))))
-> Seq Scan on public.test (cost=0.00..1.01 rows=1 width=0)
Output: test.a, test.b
(7 rows)

In this case the fpinfo->tlist of the foreign join is NIL, so whether or not
the tlist is already built cannot be discriminated by the fpinfo->tlist. We
might need another flag to show that the tlist has been built already.
Since this is an existing issue and we would need to give careful thought to
this, so I'd like to leave this for another patch.

I think in that case, relation's targetlist will also be NIL or
contain no Var node. It wouldn't be expensive to build it again and
again. That's better than maintaining a new flag.

I think that's ugly. A more clean way I'm thinking is: (1) in
postgresGetForeignJoinPaths(), create a tlist by
build_tlist_to_deparse() and save it in fpinfo->tlist before
estimate_path_cost_size() if use_remote_estimates=true, (2) in
estimate_path_cost_size(), use the fpinfo->tlist if
use_remote_estimates=true, and (3) in postgresGetForeignPlan(), use the
fpinfo->tlist as the fdw_scan_tlist if use_remote_estimates=true,
otherwise create a tlist as the fdw_scan_tlist by
build_tlist_to_deparse(), like the attached.

What do you think about that?

Another change is: I simplified build_tlist_to_deparse() a bit and put
that in postgres_fdw.c because that is used only in postgres_fdw.c.

I still think we should discuss this separately because this is an
existing issue and that makes it easy to review the patch. If the
attached is the right way to go, I'll update the join-pushdown patch on
top of the patch.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-tlist-1.patchtext/x-patch; name=postgres-fdw-tlist-1.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 843,883 **** deparse_type_name(Oid type_oid, int32 typemod)
  }
  
  /*
-  * Build the targetlist for given relation to be deparsed as SELECT clause.
-  *
-  * The output targetlist contains the columns that need to be fetched from the
-  * foreign server for the given relation.  If foreignrel is an upper relation,
-  * then the output targetlist can also contain expressions to be evaluated on
-  * foreign server.
-  */
- List *
- build_tlist_to_deparse(RelOptInfo *foreignrel)
- {
- 	List	   *tlist = NIL;
- 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
- 
- 	/*
- 	 * For an upper relation, we have already built the target list while
- 	 * checking shippability, so just return that.
- 	 */
- 	if (foreignrel->reloptkind == RELOPT_UPPER_REL)
- 		return fpinfo->grouped_tlist;
- 
- 	/*
- 	 * We require columns specified in foreignrel->reltarget->exprs and those
- 	 * required for evaluating the local conditions.
- 	 */
- 	tlist = add_to_flat_tlist(tlist,
- 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
- 									   PVC_RECURSE_PLACEHOLDERS));
- 	tlist = add_to_flat_tlist(tlist,
- 							  pull_var_clause((Node *) fpinfo->local_conds,
- 											  PVC_RECURSE_PLACEHOLDERS));
- 
- 	return tlist;
- }
- 
- /*
   * Deparse SELECT statement for given relation into buf.
   *
   * tlist contains the list of desired columns to be fetched from foreign server.
--- 843,848 ----
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 406,411 **** static void conversion_error_callback(void *arg);
--- 406,412 ----
  static bool foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel,
  				JoinType jointype, RelOptInfo *outerrel, RelOptInfo *innerrel,
  				JoinPathExtraData *extra);
+ static List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  static bool foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel);
  static List *get_useful_pathkeys_for_relation(PlannerInfo *root,
  								 RelOptInfo *rel);
***************
*** 1190,1197 **** postgresGetForeignPlan(PlannerInfo *root,
  		remote_conds = fpinfo->remote_conds;
  		local_exprs = fpinfo->local_conds;
  
! 		/* Build the list of columns to be fetched from the foreign server. */
! 		fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
--- 1191,1209 ----
  		remote_conds = fpinfo->remote_conds;
  		local_exprs = fpinfo->local_conds;
  
! 		/* Get the list of columns to be fetched from the foreign server. */
! 		if ((fpinfo->use_remote_estimate &&
! 			 foreignrel->reloptkind == RELOPT_JOINREL) ||
! 			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 		{
! 			/*
! 			 * We have already built the tlist for a join relation as well as
! 			 * for an upper relation.
! 			 */
! 			fdw_scan_tlist = fpinfo->tlist;
! 		}
! 		else
! 			fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  
  		/*
  		 * Ensure that the outer plan produces a tuple whose descriptor
***************
*** 2528,2537 **** estimate_path_cost_size(PlannerInfo *root,
  		classifyConditions(root, foreignrel, param_join_conds,
  						   &remote_param_join_conds, &local_param_join_conds);
  
! 		/* Build the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL ||
  			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 			fdw_scan_tlist = build_tlist_to_deparse(foreignrel);
  		else
  			fdw_scan_tlist = NIL;
  
--- 2540,2555 ----
  		classifyConditions(root, foreignrel, param_join_conds,
  						   &remote_param_join_conds, &local_param_join_conds);
  
! 		/* Get the list of columns to be fetched from the foreign server. */
  		if (foreignrel->reloptkind == RELOPT_JOINREL ||
  			foreignrel->reloptkind == RELOPT_UPPER_REL)
! 		{
! 			/*
! 			 * We have already built the tlist for a join relation as well as
! 			 * for an upper relation.
! 			 */
! 			fdw_scan_tlist = fpinfo->tlist;
! 		}
  		else
  			fdw_scan_tlist = NIL;
  
***************
*** 2708,2714 **** estimate_path_cost_size(PlannerInfo *root,
  			MemSet(&aggcosts, 0, sizeof(AggClauseCosts));
  			if (root->parse->hasAggs)
  			{
! 				get_agg_clause_costs(root, (Node *) fpinfo->grouped_tlist,
  									 AGGSPLIT_SIMPLE, &aggcosts);
  				get_agg_clause_costs(root, (Node *) root->parse->havingQual,
  									 AGGSPLIT_SIMPLE, &aggcosts);
--- 2726,2732 ----
  			MemSet(&aggcosts, 0, sizeof(AggClauseCosts));
  			if (root->parse->hasAggs)
  			{
! 				get_agg_clause_costs(root, (Node *) fpinfo->tlist,
  									 AGGSPLIT_SIMPLE, &aggcosts);
  				get_agg_clause_costs(root, (Node *) root->parse->havingQual,
  									 AGGSPLIT_SIMPLE, &aggcosts);
***************
*** 2718,2724 **** estimate_path_cost_size(PlannerInfo *root,
  			numGroupCols = list_length(root->parse->groupClause);
  			numGroups = estimate_num_groups(root,
  							get_sortgrouplist_exprs(root->parse->groupClause,
! 													fpinfo->grouped_tlist),
  											input_rows, NULL);
  
  			/*
--- 2736,2742 ----
  			numGroupCols = list_length(root->parse->groupClause);
  			numGroups = estimate_num_groups(root,
  							get_sortgrouplist_exprs(root->parse->groupClause,
! 													fpinfo->tlist),
  											input_rows, NULL);
  
  			/*
***************
*** 4276,4281 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4294,4325 ----
  	return true;
  }
  
+ /*
+  * Build the targetlist for given relation to be deparsed as SELECT clause.
+  *
+  * The output targetlist contains the columns that need to be fetched from the
+  * foreign server for the given relation.
+  */
+ static List *
+ build_tlist_to_deparse(RelOptInfo *foreignrel)
+ {
+ 	List	   *tlist = NIL;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	/*
+ 	 * We require columns specified in foreignrel->reltarget->exprs and those
+ 	 * required for evaluating the local conditions.
+ 	 */
+ 	tlist = add_to_flat_tlist(tlist,
+ 					   pull_var_clause((Node *) foreignrel->reltarget->exprs,
+ 									   PVC_RECURSE_PLACEHOLDERS));
+ 	tlist = add_to_flat_tlist(tlist,
+ 							  pull_var_clause((Node *) fpinfo->local_conds,
+ 											  PVC_RECURSE_PLACEHOLDERS));
+ 
+ 	return tlist;
+ }
+ 
  static void
  add_paths_with_pathkeys_for_rel(PlannerInfo *root, RelOptInfo *rel,
  								Path *epq_path)
***************
*** 4407,4412 **** postgresGetForeignJoinPaths(PlannerInfo *root,
--- 4451,4460 ----
  														0, fpinfo->jointype,
  														extra->sjinfo);
  
+ 	/* If we are going to estimate costs remotely, build the tlist here. */
+ 	if (fpinfo->use_remote_estimate)
+ 		fpinfo->tlist = build_tlist_to_deparse(joinrel);
+ 
  	/* Estimate costs for bare join relation */
  	estimate_path_cost_size(root, joinrel, NIL, NIL, &rows,
  							&width, &startup_cost, &total_cost);
***************
*** 4613,4619 **** foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
  	apply_pathtarget_labeling_to_tlist(tlist, grouping_target);
  
  	/* Store generated targetlist */
! 	fpinfo->grouped_tlist = tlist;
  
  	/* Safe to pushdown */
  	fpinfo->pushdown_safe = true;
--- 4661,4667 ----
  	apply_pathtarget_labeling_to_tlist(tlist, grouping_target);
  
  	/* Store generated targetlist */
! 	fpinfo->tlist = tlist;
  
  	/* Safe to pushdown */
  	fpinfo->pushdown_safe = true;
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 93,100 **** typedef struct PgFdwRelationInfo
  	JoinType	jointype;
  	List	   *joinclauses;
  
! 	/* Grouping information */
! 	List	   *grouped_tlist;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
--- 93,103 ----
  	JoinType	jointype;
  	List	   *joinclauses;
  
! 	/*
! 	 * Optional tlist describing the contents of the scan tuple from the
! 	 * relation.
! 	 */
! 	List	   *tlist;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
***************
*** 158,164 **** extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
  				  List **retrieved_attrs);
  extern void deparseStringLiteral(StringInfo buf, const char *val);
  extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
- extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
  						List *remote_conds, List *pathkeys,
--- 161,166 ----
#85Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#84)
Re: Push down more full joins in postgres_fdw

On Fri, Jan 13, 2017 at 12:30 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/12 18:25, Ashutosh Bapat wrote:

On Wed, Jan 11, 2017 at 5:45 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/05 21:11, Ashutosh Bapat wrote:

IIUC, for a relation with use_remote_estimates we will deparse the
query twice and will build the targetlist twice.

While working on this, I noticed a weird case. Consider:

postgres=# explain verbose select 1 from ft1 left join ft2 on (ft1.a =
ft2.a) inner join test on (true);
QUERY PLAN

-------------------------------------------------------------------------------------------------
Nested Loop (cost=100.00..103.06 rows=1 width=4)
Output: 1
-> Foreign Scan (cost=100.00..102.04 rows=1 width=0)
Relations: (public.ft1) LEFT JOIN (public.ft2)
Remote SQL: SELECT NULL FROM (public.t1 r1 LEFT JOIN public.t2
r2
ON (((r1.a = r2.a))))
-> Seq Scan on public.test (cost=0.00..1.01 rows=1 width=0)
Output: test.a, test.b
(7 rows)

In this case the fpinfo->tlist of the foreign join is NIL, so whether or
not
the tlist is already built cannot be discriminated by the fpinfo->tlist.
We
might need another flag to show that the tlist has been built already.
Since this is an existing issue and we would need to give careful thought
to
this, so I'd like to leave this for another patch.

I think in that case, relation's targetlist will also be NIL or
contain no Var node. It wouldn't be expensive to build it again and
again. That's better than maintaining a new flag.

I think that's ugly. A more clean way I'm thinking is: (1) in
postgresGetForeignJoinPaths(), create a tlist by build_tlist_to_deparse()
and save it in fpinfo->tlist before estimate_path_cost_size() if
use_remote_estimates=true, (2) in estimate_path_cost_size(), use the
fpinfo->tlist if use_remote_estimates=true, and (3) in
postgresGetForeignPlan(), use the fpinfo->tlist as the fdw_scan_tlist if
use_remote_estimates=true, otherwise create a tlist as the fdw_scan_tlist by
build_tlist_to_deparse(), like the attached.

What do you think about that?

Another change is: I simplified build_tlist_to_deparse() a bit and put that
in postgres_fdw.c because that is used only in postgres_fdw.c.

I still think we should discuss this separately because this is an existing
issue and that makes it easy to review the patch. If the attached is the
right way to go, I'll update the join-pushdown patch on top of the patch.

I don't think it's right to assume that the targetlist will be
available only when use_remote_estimate is true; for grouping it's
certainly not.

But I don't see this discussion getting anywhere. I will leave it to
the committer's judgement. I think we should pick up your patch on
27th December, update the comment per your mail on 5th Jan. I will
review it once and list down the things left to committer's judgement.

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

#86Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#85)
Re: Push down more full joins in postgres_fdw

On 2017/01/27 20:04, Ashutosh Bapat wrote:

On Fri, Jan 13, 2017 at 12:30 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

A more clean way I'm thinking is: (1) in
postgresGetForeignJoinPaths(), create a tlist by build_tlist_to_deparse()
and save it in fpinfo->tlist before estimate_path_cost_size() if
use_remote_estimates=true, (2) in estimate_path_cost_size(), use the
fpinfo->tlist if use_remote_estimates=true, and (3) in
postgresGetForeignPlan(), use the fpinfo->tlist as the fdw_scan_tlist if
use_remote_estimates=true, otherwise create a tlist as the fdw_scan_tlist by
build_tlist_to_deparse(), like the attached.

What do you think about that?

Another change is: I simplified build_tlist_to_deparse() a bit and put that
in postgres_fdw.c because that is used only in postgres_fdw.c.

I still think we should discuss this separately because this is an existing
issue and that makes it easy to review the patch. If the attached is the
right way to go, I'll update the join-pushdown patch on top of the patch.

I don't think it's right to assume that the targetlist will be
available only when use_remote_estimate is true; for grouping it's
certainly not.

My explanation was not enough. Sorry about that. My proposal described
above was for join relations, not upper relations. We build the
targetlist of an upper relation during postgresGetForeignUpperPaths, so
for grouping I think we should use that targetlist in
estimate_path_cost_size and postgresGetForeignPlan. The patch was
created that way.

But I don't see this discussion getting anywhere. I will leave it to
the committer's judgement.

I'm fine with that.

I think we should pick up your patch on
27th December, update the comment per your mail on 5th Jan. I will
review it once and list down the things left to committer's judgement.

Sorry, I started thinking we went in the wrong direction. I added to
deparseSelectStmtForRel build_subquery_tlists, which creates a tlist for
each subquery present in a given join tree prior to deparsing a whole
remote query. But that's nothing but an overhead; we need to create a
tlist for the top-level query because we use it as fdw_scan_tlist, but
not for subqueries, and we need to create retrieved_attrs for the
top-level query while deparsing the targetlist in
deparseExplicitTargetList, but not for subqueries. So, we should need
some work to avoid such a useless overhead.

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

#87Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Etsuro Fujita (#86)
1 attachment(s)
Re: Push down more full joins in postgres_fdw

On 2017/01/27 21:25, Etsuro Fujita wrote:

On 2017/01/27 20:04, Ashutosh Bapat wrote:

I think we should pick up your patch on
27th December, update the comment per your mail on 5th Jan. I will
review it once and list down the things left to committer's judgement.

Sorry, I started thinking we went in the wrong direction. I added to
deparseSelectStmtForRel build_subquery_tlists, which creates a tlist for
each subquery present in a given join tree prior to deparsing a whole
remote query. But that's nothing but an overhead; we need to create a
tlist for the top-level query because we use it as fdw_scan_tlist, but
not for subqueries, and we need to create retrieved_attrs for the
top-level query while deparsing the targetlist in
deparseExplicitTargetList, but not for subqueries. So, we should need
some work to avoid such a useless overhead.

I think we can avoid the useless overhead by adding a new function,
deparseSubqueryTargetList, that deparses expressions in the given
relation's reltarget, not the tlist, as a SELECT clause of the subquery
representing the given relation. That would also allow us to make the
1-to-1 relationship between the subquery output columns and their
aliases more explicit, which was your original comment. Please find
attached the new version. (The patch doesn't need the patch to avoid
duplicate construction of the tlist, discussed upthread.)

Other changes:
* I went back to make_outerrel_subquery and make_innerrel_subquery,
which are flags to indicate whether to deparse the input relations as
subqueries. is_subquery_rel would work well for handling the cases of
full joins with restrictions on the input relations, but I noticed that
that wouldn't work well when extending to handle the cases where we
deparse the input relations as subqueries to evaluate PHVs remotely.
* Since appendSubqueryAlias in the previous patch is pretty small, I
included the code into deparseRangeTblRef.

Best regards,
Etsuro Fujita

Attachments:

postgres-fdw-subquery-support-v15.patchtext/x-patch; name=postgres-fdw-subquery-support-v15.patchDownload
*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 109,114 **** typedef struct deparse_expr_cxt
--- 109,116 ----
  /* Handy macro to add relation name qualification */
  #define ADD_REL_QUALIFIER(buf, varno)	\
  		appendStringInfo((buf), "%s%d.", REL_ALIAS_PREFIX, (varno))
+ #define SUBQUERY_REL_ALIAS_PREFIX	"s"
+ #define SUBQUERY_COL_ALIAS_PREFIX	"c"
  
  /*
   * Functions to determine whether an expression can be evaluated safely on
***************
*** 132,137 **** static void deparseTargetList(StringInfo buf,
--- 134,140 ----
  				  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,
  					 Index rtindex, Relation rel,
  					 bool trig_after_row,
***************
*** 159,165 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
--- 162,168 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
! static void deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
  				 deparse_expr_cxt *context);
  static void deparseLockingClause(deparse_expr_cxt *context);
  static void appendOrderByClause(List *pathkeys, deparse_expr_cxt *context);
***************
*** 167,172 **** static void appendConditions(List *exprs, deparse_expr_cxt *context);
--- 170,178 ----
  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,
***************
*** 175,180 **** static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
--- 181,194 ----
  static Node *deparseSortGroupClause(Index ref, List *tlist,
  					   deparse_expr_cxt *context);
  
+ /*
+  * Helper functions
+  */
+ static bool is_subquery_var(Var *node, RelOptInfo *foreignrel,
+ 							int *relno, int *colno);
+ static void get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
+ 										  int *relno, int *colno);
+ 
  
  /*
   * Examine each qual clause in input_conds, and classify them into two groups,
***************
*** 896,907 **** build_tlist_to_deparse(RelOptInfo *foreignrel)
   *
   * pathkeys is the list of pathkeys to order the result by.
   *
   * List of columns selected is returned in retrieved_attrs.
   */
  extern void
  deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  						List *tlist, List *remote_conds, List *pathkeys,
! 						List **retrieved_attrs, List **params_list)
  {
  	deparse_expr_cxt context;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
--- 910,925 ----
   *
   * pathkeys is the list of pathkeys to order the result by.
   *
+  * is_subquery is the flag to indicate whether to deparse the specified
+  * relation as a subquery.
+  *
   * List of columns selected is returned in retrieved_attrs.
   */
  extern void
  deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  						List *tlist, List *remote_conds, List *pathkeys,
! 						bool is_subquery, List **retrieved_attrs,
! 						List **params_list)
  {
  	deparse_expr_cxt context;
  	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
***************
*** 925,931 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
  	context.params_list = params_list;
  
  	/* Construct SELECT clause */
! 	deparseSelectSql(tlist, retrieved_attrs, &context);
  
  	/*
  	 * For upper relations, the WHERE clause is built from the remote
--- 943,949 ----
  	context.params_list = params_list;
  
  	/* Construct SELECT clause */
! 	deparseSelectSql(tlist, is_subquery, retrieved_attrs, &context);
  
  	/*
  	 * For upper relations, the WHERE clause is built from the remote
***************
*** 972,984 **** deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *rel,
   * contains just "SELECT ... ".
   *
   * We also create an integer List of the columns being retrieved, which is
!  * returned to *retrieved_attrs.
   *
!  * tlist is the list of desired columns. Read prologue of
!  * deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
--- 990,1005 ----
   * contains just "SELECT ... ".
   *
   * We also create an integer List of the columns being retrieved, which is
!  * returned to *retrieved_attrs, unless we deparse the specified relation
!  * as a subquery.
   *
!  * tlist is the list of desired columns.  is_subquery is the flag to
!  * indicate whether to deparse the specified relation as a subquery.
!  * Read prologue of deparseSelectStmtForRel() for details.
   */
  static void
! deparseSelectSql(List *tlist, bool is_subquery, List **retrieved_attrs,
! 				 deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  	RelOptInfo *foreignrel = context->foreignrel;
***************
*** 990,999 **** deparseSelectSql(List *tlist, List **retrieved_attrs, deparse_expr_cxt *context)
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 		foreignrel->reloptkind == RELOPT_UPPER_REL)
  	{
! 		/* For a join relation use the input tlist */
  		deparseExplicitTargetList(tlist, retrieved_attrs, context);
  	}
  	else
--- 1011,1032 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  
! 	if (is_subquery)
! 	{
! 		/*
! 		 * For a relation that is deparsed as a subquery, emit expressions
! 		 * specified in the relation's reltarget.  Note that since this is
! 		 * for the subquery, no need to care about *retrieved_attrs.
! 		 */
! 		deparseSubqueryTargetList(context);
! 	}
! 	else if (foreignrel->reloptkind == RELOPT_JOINREL ||
! 			 foreignrel->reloptkind == RELOPT_UPPER_REL)
  	{
! 		/*
! 		 * 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
***************
*** 1155,1165 **** deparseLockingClause(deparse_expr_cxt *context)
--- 1188,1206 ----
  	StringInfo	buf = context->buf;
  	PlannerInfo *root = context->root;
  	RelOptInfo *rel = context->scanrel;
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) rel->fdw_private;
  	int			relid = -1;
  
  	while ((relid = bms_next_member(rel->relids, relid)) >= 0)
  	{
  		/*
+ 		 * Ignore relation if it appears in a lower subquery.  Locking clause
+ 		 * for such a relation is included in the subquery if necessary.
+ 		 */
+ 		if (bms_is_member(relid, fpinfo->lower_subquery_rels))
+ 			continue;
+ 
+ 		/*
  		 * Add FOR UPDATE/SHARE if appropriate.  We apply locking during the
  		 * initial row fetch, rather than later on as is done for local
  		 * tables. The extra roundtrips involved in trying to duplicate the
***************
*** 1333,1338 **** deparseExplicitTargetList(List *tlist, List **retrieved_attrs,
--- 1374,1413 ----
  }
  
  /*
+  * Emit expressions specified in the given relation's reltarget.
+  *
+  * This is used for deparsing the given relation as a subquery.
+  */
+ static void
+ deparseSubqueryTargetList(deparse_expr_cxt *context)
+ {
+ 	StringInfo	buf = context->buf;
+ 	RelOptInfo *foreignrel = context->foreignrel;
+ 	bool		first;
+ 	ListCell   *lc;
+ 
+ 	/* Should only be called in these cases. */
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 
+ 	first = true;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		Node	   *node = (Node *) lfirst(lc);
+ 
+ 		if (!first)
+ 			appendStringInfoString(buf, ", ");
+ 		first = false;
+ 
+ 		deparseExpr((Expr *) node, context);
+ 	}
+ 
+ 	/* Don't generate bad syntax if no expressions */
+ 	if (first)
+ 		appendStringInfoString(buf, "NULL");
+ }
+ 
+ /*
   * Construct FROM clause for given relation
   *
   * The function constructs ... JOIN ... ON ... for join relation. For a base
***************
*** 1347,1364 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
  
  	if (foreignrel->reloptkind == RELOPT_JOINREL)
  	{
- 		RelOptInfo *rel_o = fpinfo->outerrel;
- 		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
--- 1422,1439 ----
  
  	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
***************
*** 1414,1419 **** deparseFromExprForRel(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
--- 1489,1551 ----
  }
  
  /*
+  * Append FROM clause entry for the given relation into buf.
+  */
+ static void
+ deparseRangeTblRef(StringInfo buf, PlannerInfo *root, RelOptInfo *foreignrel,
+ 				   bool make_subquery, List **params_list)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 
+ 	/* Should only be called in these cases. */
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL);
+ 
+ 	Assert(fpinfo->local_conds == NIL);
+ 
+ 	/* If make_subquery is true, deparse the relation as a subquery. */
+ 	if (make_subquery)
+ 	{
+ 		List	   *retrieved_attrs;
+ 		int			ncols;
+ 
+ 		/* Deparse the subquery representing the relation. */
+ 		appendStringInfoChar(buf, '(');
+ 		deparseSelectStmtForRel(buf, root, foreignrel, NIL,
+ 								fpinfo->remote_conds, NIL, true,
+ 								&retrieved_attrs, params_list);
+ 		appendStringInfoChar(buf, ')');
+ 
+ 		/* Append the relation alias. */
+ 		appendStringInfo(buf, " %s%d", SUBQUERY_REL_ALIAS_PREFIX,
+ 						 fpinfo->relation_index);
+ 
+ 		/*
+ 		 * Append the column aliases if needed.  Note that the subquery emits
+ 		 * expressions specified in the relation's reltarget (see
+ 		 * deparseSubqueryTargetList).
+ 		 */
+ 		ncols = list_length(foreignrel->reltarget->exprs);
+ 		if (ncols > 0)
+ 		{
+ 			int			i;
+ 
+ 			appendStringInfoChar(buf, '(');
+ 			for (i = 1; i <= ncols; i++)
+ 			{
+ 				if (i > 1)
+ 					appendStringInfoString(buf, ", ");
+ 
+ 				appendStringInfo(buf, "%s%d", SUBQUERY_COL_ALIAS_PREFIX, i);
+ 			}
+ 			appendStringInfoChar(buf, ')');
+ 		}
+ 	}
+ 	else
+ 		deparseFromExprForRel(buf, root, foreignrel, true, params_list);
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 2057,2066 **** static void
--- 2189,2213 ----
  deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	Relids		relids = context->scanrel->relids;
+ 	int			relno;
+ 	int			colno;
  
  	/* Qualify columns when multiple relations are involved. */
  	bool		qualify_col = (bms_num_members(relids) > 1);
  
+ 	/*
+ 	 * If the Var belongs to the foreign relation that is deparsed as a
+ 	 * subquery, use the relation and column alias to the Var provided
+ 	 * by the subquery, instead of the remote name.
+ 	 */
+ 	if (is_subquery_var(node, context->scanrel, &relno, &colno))
+ 	{
+ 		appendStringInfo(context->buf, "%s%d.%s%d",
+ 						 SUBQUERY_REL_ALIAS_PREFIX, relno,
+ 						 SUBQUERY_COL_ALIAS_PREFIX, colno);
+ 		return;
+ 	}
+ 
  	if (bms_is_member(node->varno, relids) && node->varlevelsup == 0)
  		deparseColumnRef(context->buf, node->varno, node->varattno,
  						 context->root, qualify_col);
***************
*** 2938,2940 **** deparseSortGroupClause(Index ref, List *tlist, deparse_expr_cxt *context)
--- 3085,3184 ----
  
  	return (Node *) expr;
  }
+ 
+ 
+ /*
+  * Returns true if given Var is deparsed as a subquery output column, in
+  * which case, *relno and *colno are set to the IDs for the relation and
+  * column alias to the Var provided by the subquery.
+  */
+ static bool
+ is_subquery_var(Var *node, RelOptInfo *foreignrel, int *relno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	RelOptInfo *outerrel = fpinfo->outerrel;
+ 	RelOptInfo *innerrel = fpinfo->innerrel;
+ 
+ 	/* Should only be called in these cases. */
+ 	Assert(foreignrel->reloptkind == RELOPT_BASEREL ||
+ 		   foreignrel->reloptkind == RELOPT_JOINREL ||
+ 		   foreignrel->reloptkind == RELOPT_OTHER_MEMBER_REL);
+ 
+ 	/*
+ 	 * If the given relation isn't a join relation, it doesn't have any lower
+ 	 * subqueries, so the Var isn't a subquery output column.
+ 	 */
+ 	if (foreignrel->reloptkind != RELOPT_JOINREL)
+ 		return false;
+ 
+ 	/*
+ 	 * If the Var doesn't belong to any lower subqueries, it isn't a subquery
+ 	 * output column.
+ 	 */
+ 	if (!bms_is_member(node->varno, fpinfo->lower_subquery_rels))
+ 		return false;
+ 
+ 	if (bms_is_member(node->varno, outerrel->relids))
+ 	{
+ 		/*
+ 		 * If outer relation is deparsed as a subquery, the Var is an output
+ 		 * column of the subquery; get the IDs for the relation/column alias.
+ 		 */
+ 		if (fpinfo->make_outerrel_subquery)
+ 		{
+ 			get_relation_column_alias_ids(node, outerrel, relno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the outer relation. */
+ 		return is_subquery_var(node, outerrel, relno, colno);
+ 	}
+ 	else
+ 	{
+ 		Assert(bms_is_member(node->varno, innerrel->relids));
+ 
+ 		/*
+ 		 * If inner relation is deparsed as a subquery, the Var is an output
+ 		 * column of the subquery; get the IDs for the relation/column alias.
+ 		 */
+ 		if (fpinfo->make_innerrel_subquery)
+ 		{
+ 			get_relation_column_alias_ids(node, innerrel, relno, colno);
+ 			return true;
+ 		}
+ 
+ 		/* Otherwise, recurse into the inner relation. */
+ 		return is_subquery_var(node, innerrel, relno, colno);
+ 	}
+ }
+ 
+ /*
+  * Get the IDs for the relation and column alias to given Var belonging to
+  * given relation, which are returned into *relno and *colno.
+  */
+ static void
+ get_relation_column_alias_ids(Var *node, RelOptInfo *foreignrel,
+ 							  int *relno, int *colno)
+ {
+ 	PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
+ 	int			i;
+ 	ListCell   *lc;
+ 
+ 	/* Get the relation alias ID */
+ 	*relno = fpinfo->relation_index;
+ 
+ 	/* Get the column alias ID */
+ 	i = 1;
+ 	foreach(lc, foreignrel->reltarget->exprs)
+ 	{
+ 		if (equal(lfirst(lc), (Node *) node))
+ 		{
+ 			*colno = i;
+ 			return;
+ 		}
+ 		i++;
+ 	}
+ 
+ 	/* Shouldn't get here */
+ 	elog(ERROR, "unexpected expression in subquery output");
+ }
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 1217,1241 **** SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                            QUERY PLAN                                           
! ------------------------------------------------------------------------------------------------
!  Sort
     Output: ft4.c1, ft5.c1
!    Sort Key: ft4.c1, ft5.c1
!    ->  Hash Full Join
!          Output: ft4.c1, ft5.c1
!          Hash Cond: (ft4.c1 = ft5.c1)
!          ->  Foreign Scan on public.ft4
!                Output: ft4.c1, ft4.c2, ft4.c3
!                Remote SQL: SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
!          ->  Hash
!                Output: ft5.c1
!                ->  Foreign Scan on public.ft5
!                      Output: ft5.c1
!                      Remote SQL: SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
! (14 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
--- 1217,1232 ----
  (10 rows)
  
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
!                                                                                                                                   QUERY PLAN                                                                                                                                   
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: ft4.c1, ft5.c1
!    Relations: (public.ft4) FULL JOIN (public.ft5)
!    Remote SQL: SELECT s4.c1, s5.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s5.c1 ASC NULLS LAST
! (4 rows)
  
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
   c1 | c1 
***************
*** 1250,1255 **** SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL
--- 1241,1365 ----
      | 57
  (8 rows)
  
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+                                                                                                QUERY PLAN                                                                                               
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: 1
+    ->  Foreign Scan
+          Output: 1
+          Relations: (public.ft4) FULL JOIN (public.ft5)
+          Remote SQL: SELECT NULL FROM ((SELECT NULL FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4 FULL JOIN (SELECT NULL FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5 ON (TRUE))
+ (6 rows)
+ 
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+  ?column? 
+ ----------
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+         1
+ (10 rows)
+ 
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                      
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, t2.c1, t3.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4 t2) LEFT JOIN (public.ft5 t3))
+    Remote SQL: SELECT s4.c1, s8.c1, s8.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT r5.c1, r6.c1 FROM ("S 1"."T 3" r5 LEFT JOIN "S 1"."T 4" r6 ON (((r5.c1 = r6.c1)))) WHERE ((r5.c1 >= 50)) AND ((r5.c1 <= 60))) s8(c1, c2) ON (((s4.c1 = s8.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s8.c1 ASC NULLS LAST, s8.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+ (6 rows)
+ 
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                     
+ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: ft4.c1, ft4_1.c1, ft5.c1
+    Relations: (public.ft4) FULL JOIN ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT s4.c1, s10.c1, s10.c2 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT s8.c1, s9.c1 FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL)))) s10(c1, c2) ON (((s4.c1 = s10.c1)))) ORDER BY s4.c1 ASC NULLS LAST, s10.c1 ASC NULLS LAST, s10.c2 ASC NULLS LAST
+ (4 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  52 | 52 |   
+  54 | 54 | 54
+  56 | 56 |   
+  58 | 58 |   
+  60 | 60 | 60
+     |    | 51
+     |    | 57
+ (8 rows)
+ 
+ -- d. test deparsing rowmarked relations as subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+                                                                                                                                                                                              QUERY PLAN                                                                                                                                                                                             
+ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  LockRows
+    Output: "T 3".c1, ft4.c1, ft5.c1, "T 3".ctid, ft4.*, ft5.*
+    ->  Nested Loop
+          Output: "T 3".c1, ft4.c1, ft5.c1, "T 3".ctid, ft4.*, ft5.*
+          ->  Foreign Scan
+                Output: ft4.c1, ft4.*, ft5.c1, ft5.*
+                Relations: (public.ft4) FULL JOIN (public.ft5)
+                Remote SQL: SELECT s8.c1, s8.c2, s9.c1, s9.c2 FROM ((SELECT c1, ROW(c1, c2, c3) FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s8(c1, c2) FULL JOIN (SELECT c1, ROW(c1, c2, c3) FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1, c2) ON (((s8.c1 = s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL))) ORDER BY s8.c1 ASC NULLS LAST, s9.c1 ASC NULLS LAST
+                ->  Hash Full Join
+                      Output: ft4.c1, ft4.*, ft5.c1, ft5.*
+                      Hash Cond: (ft4.c1 = ft5.c1)
+                      Filter: ((ft4.c1 IS NULL) OR (ft4.c1 IS NOT NULL))
+                      ->  Foreign Scan on public.ft4
+                            Output: ft4.c1, ft4.*
+                            Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
+                      ->  Hash
+                            Output: ft5.c1, ft5.*
+                            ->  Foreign Scan on public.ft5
+                                  Output: ft5.c1, ft5.*
+                                  Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
+          ->  Materialize
+                Output: "T 3".c1, "T 3".ctid
+                ->  Seq Scan on "S 1"."T 3"
+                      Output: "T 3".c1, "T 3".ctid
+                      Filter: ("T 3".c1 = 50)
+ (25 rows)
+ 
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+  c1 | a  | b  
+ ----+----+----
+  50 | 50 |   
+  50 | 52 |   
+  50 | 54 | 54
+  50 | 56 |   
+  50 | 58 |   
+  50 | 60 | 60
+  50 |    | 51
+  50 |    | 57
+ (8 rows)
+ 
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 3062,3067 **** select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) gr
--- 3172,3195 ----
                       |   9
  (3 rows)
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+                                                                                                                   QUERY PLAN                                                                                                                   
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Foreign Scan
+    Output: (count(*)), (sum(ft4.c1)), (avg(ft5.c1))
+    Relations: Aggregate on ((public.ft4) FULL JOIN (public.ft5))
+    Remote SQL: SELECT count(*), sum(s4.c1), avg(s5.c1) FROM ((SELECT c1 FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s4(c1) FULL JOIN (SELECT c1 FROM "S 1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s5(c1) ON (((s4.c1 = s5.c1))))
+ (4 rows)
+ 
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+  count | sum |         avg         
+ -------+-----+---------------------
+      8 | 330 | 55.5000000000000000
+ (1 row)
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 668,673 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 668,680 ----
  	if (*refname && strcmp(refname, relname) != 0)
  		appendStringInfo(fpinfo->relation_name, " %s",
  						 quote_identifier(rte->eref->aliasname));
+ 
+ 	/* No outer and inner relations. */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	fpinfo->lower_subquery_rels = NULL;
+ 	/* Set the relation index. */
+ 	fpinfo->relation_index = baserel->relid;
  }
  
  /*
***************
*** 1239,1245 **** postgresGetForeignPlan(PlannerInfo *root,
  	initStringInfo(&sql);
  	deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
  							remote_conds, best_path->path.pathkeys,
! 							&retrieved_attrs, &params_list);
  
  	/*
  	 * Build the fdw_private list that will be available to the executor.
--- 1246,1252 ----
  	initStringInfo(&sql);
  	deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
  							remote_conds, best_path->path.pathkeys,
! 							false, &retrieved_attrs, &params_list);
  
  	/*
  	 * Build the fdw_private list that will be available to the executor.
***************
*** 2551,2558 **** estimate_path_cost_size(PlannerInfo *root,
  		initStringInfo(&sql);
  		appendStringInfoString(&sql, "EXPLAIN ");
  		deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
! 								remote_conds, pathkeys, &retrieved_attrs,
! 								NULL);
  
  		/* Get the remote estimate */
  		conn = GetConnection(fpinfo->user, false);
--- 2558,2565 ----
  		initStringInfo(&sql);
  		appendStringInfoString(&sql, "EXPLAIN ");
  		deparseSelectStmtForRel(&sql, root, foreignrel, fdw_scan_tlist,
! 								remote_conds, pathkeys, false,
! 								&retrieved_attrs, NULL);
  
  		/* Get the remote estimate */
  		conn = GetConnection(fpinfo->user, false);
***************
*** 4147,4156 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	fpinfo->jointype = jointype;
  
  	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step,
! 	 * which is not currently supported by the deparser logic.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
--- 4154,4175 ----
  	fpinfo->jointype = jointype;
  
  	/*
+ 	 * By default, both the input relations are not required to be deparsed
+ 	 * as subqueries, but there might be some relations covered by the input
+ 	 * relations that are required to be deparsed as subqueries, so save the
+ 	 * relids of those relations for later use by the deparser.
+ 	 */
+ 	fpinfo->make_outerrel_subquery = false;
+ 	fpinfo->make_innerrel_subquery = false;
+ 	Assert(bms_is_subset(fpinfo_o->lower_subquery_rels, outerrel->relids));
+ 	Assert(bms_is_subset(fpinfo_i->lower_subquery_rels, innerrel->relids));
+ 	fpinfo->lower_subquery_rels = bms_union(fpinfo_o->lower_subquery_rels,
+ 											fpinfo_i->lower_subquery_rels);
+ 
+ 	/*
  	 * Pull the other remote conditions from the joining relations into join
  	 * clauses or other remote clauses (remote_conds) of this relation
! 	 * wherever possible. This avoids building subqueries at every join step.
  	 *
  	 * For an inner join, clauses from both the relations are added to the
  	 * other remote clauses. For LEFT and RIGHT OUTER join, the clauses from
***************
*** 4161,4168 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other. Consider such full outer join as
! 	 * unshippable because of the reasons mentioned above in this comment.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
--- 4180,4186 ----
  	 *
  	 * For a FULL OUTER JOIN, the other clauses from either relation can not
  	 * be added to the joinclauses or remote_conds, since each relation acts
! 	 * as an outer relation for the other.
  	 *
  	 * The joining sides can not have local conditions, thus no need to test
  	 * shippability of the clauses being pulled up.
***************
*** 4191,4198 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
  			break;
  
  		case JOIN_FULL:
! 			if (fpinfo_i->remote_conds || fpinfo_o->remote_conds)
! 				return false;
  			break;
  
  		default:
--- 4209,4237 ----
  			break;
  
  		case JOIN_FULL:
! 
! 			/*
! 			 * In this case, if any of the input relations has conditions,
! 			 * we need to deparse that relation as a subquery so that the
! 			 * conditions can be evaluated before the join.  Remember it in
! 			 * the fpinfo of this relation so that the deparser can take
! 			 * appropriate action.  Also, save the relids of base relations
! 			 * covered by that relation for later use by the deparser.
! 			 */
! 			if (fpinfo_o->remote_conds)
! 			{
! 				fpinfo->make_outerrel_subquery = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									outerrel->relids);
! 			}
! 			if (fpinfo_i->remote_conds)
! 			{
! 				fpinfo->make_innerrel_subquery = true;
! 				fpinfo->lower_subquery_rels =
! 					bms_add_members(fpinfo->lower_subquery_rels,
! 									innerrel->relids);
! 			}
  			break;
  
  		default:
***************
*** 4273,4278 **** foreign_join_ok(PlannerInfo *root, RelOptInfo *joinrel, JoinType jointype,
--- 4312,4327 ----
  					 get_jointype_name(fpinfo->jointype),
  					 fpinfo_i->relation_name->data);
  
+ 	/*
+ 	 * Set the relation index.  This is defined as the position of this
+ 	 * joinrel in the join_rel_list list plus the length of the rtable list.
+ 	 * Note that since this joinrel is at the end of the join_rel_list list
+ 	 * when we are called, we can get the position by list_length.
+ 	 */
+ 	Assert(fpinfo->relation_index == 0);	/* shouldn't be set yet */
+ 	fpinfo->relation_index =
+ 		list_length(root->parse->rtable) + list_length(root->join_rel_list);
+ 
  	return true;
  }
  
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 95,100 **** typedef struct PgFdwRelationInfo
--- 95,114 ----
  
  	/* Grouping information */
  	List	   *grouped_tlist;
+ 
+ 	/* Subquery information */
+ 	bool		make_outerrel_subquery;	/* do we deparse outerrel as a
+ 										 * subquery? */
+ 	bool		make_innerrel_subquery;	/* do we deparse innerrel as a
+ 										 * subquery? */
+ 	Relids		lower_subquery_rels;	/* all relids appearing in lower
+ 										 * subqueries */
+ 
+ 	/*
+ 	 * Index of the relation.  It is used to create an alias to a subquery
+ 	 * representing the relation.
+ 	 */
+ 	int			relation_index;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
***************
*** 161,167 **** extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
  extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
! 						List *remote_conds, List *pathkeys,
  						List **retrieved_attrs, List **params_list);
  
  /* in shippable.c */
--- 175,181 ----
  extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
  extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
  						RelOptInfo *foreignrel, List *tlist,
! 						List *remote_conds, List *pathkeys, bool is_subquery,
  						List **retrieved_attrs, List **params_list);
  
  /* in shippable.c */
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 391,399 **** EXPLAIN (VERBOSE, COSTS OFF)
--- 391,416 ----
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10;
  -- full outer join with restrictions on the joining relations
+ -- a. the joining relations are both base relations
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
  SELECT t1.c1, t2.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1;
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+ SELECT 1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t2 ON (TRUE) OFFSET 10 LIMIT 10;
+ -- b. one of the joining relations is a base relation and the other is a join
+ -- relation
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM ft4 t2 LEFT JOIN ft5 t3 ON (t2.c1 = t3.c1) WHERE (t2.c1 between 50 and 60)) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- c. test deparsing the remote query as nested subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t1 FULL JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (t1.c1 = ss.a) ORDER BY t1.c1, ss.a, ss.b;
+ -- d. test deparsing rowmarked relations as subqueries
+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
  -- full outer join + inner join
  EXPLAIN (VERBOSE, COSTS OFF)
  SELECT t1.c1, t2.c1, t3.c1 FROM ft4 t1 INNER JOIN ft5 t2 ON (t1.c1 = t2.c1 + 1 and t1.c1 between 50 and 60) FULL JOIN ft4 t3 ON (t2.c1 = t3.c1) ORDER BY t1.c1, t2.c1, t3.c1 LIMIT 10;
***************
*** 793,798 **** explain (verbose, costs off)
--- 810,821 ----
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  select avg(t1.c1), sum(t2.c1) from ft4 t1 full join ft5 t2 on (t1.c1 = t2.c1) group by t2.c1 having (avg(t1.c1) is null and sum(t2.c1) < 10) or sum(t2.c1) is null order by 1 nulls last, 2;
  
+ -- Aggregate over FULL join needing to deparse the joining relations as
+ -- subqueries.
+ explain (verbose, costs off)
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ select count(*), sum(t1.c1), avg(t2.c1) from (select c1 from ft4 where c1 between 50 and 60) t1 full join (select c1 from ft5 where c1 between 50 and 60) t2 on (t1.c1 = t2.c1);
+ 
  -- ORDER BY expression is part of the target list but not pushed down to
  -- foreign server.
  explain (verbose, costs off)
#88Ashutosh Bapat
ashutosh.bapat@enterprisedb.com
In reply to: Etsuro Fujita (#87)
Re: Push down more full joins in postgres_fdw

On Mon, Jan 30, 2017 at 5:00 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/27 21:25, Etsuro Fujita wrote:

On 2017/01/27 20:04, Ashutosh Bapat wrote:

I think we should pick up your patch on
27th December, update the comment per your mail on 5th Jan. I will
review it once and list down the things left to committer's judgement.

Sorry, I started thinking we went in the wrong direction. I added to
deparseSelectStmtForRel build_subquery_tlists, which creates a tlist for
each subquery present in a given join tree prior to deparsing a whole
remote query. But that's nothing but an overhead; we need to create a
tlist for the top-level query because we use it as fdw_scan_tlist, but
not for subqueries, and we need to create retrieved_attrs for the
top-level query while deparsing the targetlist in
deparseExplicitTargetList, but not for subqueries. So, we should need
some work to avoid such a useless overhead.

I think we can avoid the useless overhead by adding a new function,
deparseSubqueryTargetList, that deparses expressions in the given relation's
reltarget, not the tlist, as a SELECT clause of the subquery representing
the given relation. That would also allow us to make the 1-to-1
relationship between the subquery output columns and their aliases more
explicit, which was your original comment. Please find attached the new
version. (The patch doesn't need the patch to avoid duplicate construction
of the tlist, discussed upthread.)

I have not looked at the patch, but the reason we use a tlist instead
of list of expressions is because fdw_scan_tlist is expected in that
form and we don't want two different representations one to deparse
SELECT and one to interpret results from the foreign server. What you
describe above seems to introduce exactly that hazard.

Other changes:
* I went back to make_outerrel_subquery and make_innerrel_subquery, which
are flags to indicate whether to deparse the input relations as subqueries.
is_subquery_rel would work well for handling the cases of full joins with
restrictions on the input relations, but I noticed that that wouldn't work
well when extending to handle the cases where we deparse the input relations
as subqueries to evaluate PHVs remotely.

I had objected to using a single variable instead of two previously
and you had argued against it in [1]/messages/by-id/1eb58ee4-8ffa-7c40-1229-c8973e6498ea@lab.ntt.co.jp. There you had mentioned that PHV
doesn't need two variables, but now you are taking the other stand,
without any apparent reason. Can you please clarify it?

[1]: /messages/by-id/1eb58ee4-8ffa-7c40-1229-c8973e6498ea@lab.ntt.co.jp

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

#89Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Ashutosh Bapat (#88)
Re: Push down more full joins in postgres_fdw

On 2017/01/30 21:05, Ashutosh Bapat wrote:

On Mon, Jan 30, 2017 at 5:00 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

On 2017/01/27 21:25, Etsuro Fujita wrote:

Sorry, I started thinking we went in the wrong direction. I added to
deparseSelectStmtForRel build_subquery_tlists, which creates a tlist for
each subquery present in a given join tree prior to deparsing a whole
remote query. But that's nothing but an overhead; we need to create a
tlist for the top-level query because we use it as fdw_scan_tlist, but
not for subqueries, and we need to create retrieved_attrs for the
top-level query while deparsing the targetlist in
deparseExplicitTargetList, but not for subqueries. So, we should need
some work to avoid such a useless overhead.

I think we can avoid the useless overhead by adding a new function,
deparseSubqueryTargetList, that deparses expressions in the given relation's
reltarget, not the tlist, as a SELECT clause of the subquery representing
the given relation. That would also allow us to make the 1-to-1
relationship between the subquery output columns and their aliases more
explicit, which was your original comment. Please find attached the new
version. (The patch doesn't need the patch to avoid duplicate construction
of the tlist, discussed upthread.)

I have not looked at the patch, but the reason we use a tlist instead
of list of expressions is because fdw_scan_tlist is expected in that
form

Actually, we wouldn't need that to deparse a remote join query; a list
of expressions would be enough.

and we don't want two different representations one to deparse
SELECT and one to interpret results from the foreign server. What you
describe above seems to introduce exactly that hazard.

I agree with you to some extent, BUT:
* I don't think it's a good idea to create a tlist for each base/join
relation that is deparsed as a subquery, to just avoid that hazard. As
I said above, that's nothing but an overhead.
* I think we would need to have two different representations for at
least base relations; we use fpinfo->attrs_used to deparse a simple
foreign table scan query for a base relation, but when deparsing the
base relation as a subquery, we would need to use the list of
expressions in the base relation's reltarget, to deparse a SELECT clause
of the subquery, because we need to deparse a whole-row reference to the
base relation as ROW, not all the user columns expanded, as shown in
this extracted from the regression tests in the patch:

+ EXPLAIN (VERBOSE, COSTS OFF)
+ SELECT t1.c1, ss.a, ss.b FROM (SELECT c1 FROM "S 1"."T 3" WHERE c1 = 
50) t1 INNER JOIN (SELECT t2.c1, t3.c1 FROM (SELECT c1 FROM ft4 WHERE c1 
between 50 and 60) t2 FULL JOIN (SELECT c1 FROM ft5 WHERE c1 between 50 
and 60) t3 ON (t2.c1 = t3.c1) WHERE t2.c1 IS NULL OR t2.c1 IS NOT NULL) 
ss(a, b) ON (TRUE) ORDER BY t1.c1, ss.a, ss.b FOR UPDATE OF t1;
+ 

QUERY PLAN

+ 
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  LockRows
+    Output: "T 3".c1, ft4.c1, ft5.c1, "T 3".ctid, ft4.*, ft5.*
+    ->  Nested Loop
+          Output: "T 3".c1, ft4.c1, ft5.c1, "T 3".ctid, ft4.*, ft5.*
+          ->  Foreign Scan
+                Output: ft4.c1, ft4.*, ft5.c1, ft5.*
+                Relations: (public.ft4) FULL JOIN (public.ft5)
+                Remote SQL: SELECT s8.c1, s8.c2, s9.c1, s9.c2 FROM 
((SELECT c1, ROW(c1, c2, c3) FROM "S 1"."T 3" WHERE ((c1 >= 50)) AND 
((c1 <= 60))) s8(c1, c2) FULL JOIN (SELECT c1, ROW(c1, c2, c3) FROM "S 
1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))) s9(c1, c2) ON (((s8.c1 = 
s9.c1)))) WHERE (((s8.c1 IS NULL) OR (s8.c1 IS NOT NULL))) ORDER BY 
s8.c1 ASC NULLS LAST, s9.c1 ASC NULLS LAST
+                ->  Hash Full Join
+                      Output: ft4.c1, ft4.*, ft5.c1, ft5.*
+                      Hash Cond: (ft4.c1 = ft5.c1)
+                      Filter: ((ft4.c1 IS NULL) OR (ft4.c1 IS NOT NULL))
+                      ->  Foreign Scan on public.ft4
+                            Output: ft4.c1, ft4.*
+                            Remote SQL: SELECT c1, c2, c3 FROM "S 1"."T 
3" WHERE ((c1 >= 50)) AND ((c1 <= 60))
+                      ->  Hash
+                            Output: ft5.c1, ft5.*
+                            ->  Foreign Scan on public.ft5
+                                  Output: ft5.c1, ft5.*
+                                  Remote SQL: SELECT c1, c2, c3 FROM "S 
1"."T 4" WHERE ((c1 >= 50)) AND ((c1 <= 60))
+          ->  Materialize
+                Output: "T 3".c1, "T 3".ctid
+                ->  Seq Scan on "S 1"."T 3"
+                      Output: "T 3".c1, "T 3".ctid
+                      Filter: ("T 3".c1 = 50)
+ (25 rows)

Other changes:
* I went back to make_outerrel_subquery and make_innerrel_subquery, which
are flags to indicate whether to deparse the input relations as subqueries.
is_subquery_rel would work well for handling the cases of full joins with
restrictions on the input relations, but I noticed that that wouldn't work
well when extending to handle the cases where we deparse the input relations
as subqueries to evaluate PHVs remotely.

I had objected to using a single variable instead of two previously
and you had argued against it in [1]. There you had mentioned that PHV
doesn't need two variables, but now you are taking the other stand,
without any apparent reason. Can you please clarify it?

Sorry, I missed some cases. Consider the join {A, B, C} satisfying the
identity 3 in optimizer/README, ie,

(A leftjoin B on (Pab)) leftjoin C on (Pbc)
= A leftjoin (B leftjoin C on (Pbc)) on (Pab)

where predicate Pbc fails for all-null B rows. Assume that B has a PHV
in the relation's reltarget. While considering 2-way joins,
foreign_join_ok would determine to deparse B as a subquery and hence set
the B's is_subquery_rel to true for the join relation {A, B}. However,
if the planner selects the join order {A leftjoin (B leftjoin C on
(Pbc)) on (Pab)} as the cheapest path for the join {A, B, C}, we would
deparse B as a subquery according to the is_subquery_rel flag while
creating the FROM clause entry for the join relation {B, C}. That
wouldn't look good. That would be logically correct, though. To avoid
this, I'd like to go back to the two variables previously proposed.

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

#90Michael Paquier
michael.paquier@gmail.com
In reply to: Etsuro Fujita (#87)
Re: Push down more full joins in postgres_fdw

On Mon, Jan 30, 2017 at 8:30 PM, Etsuro Fujita
<fujita.etsuro@lab.ntt.co.jp> wrote:

Other changes:
* I went back to make_outerrel_subquery and make_innerrel_subquery, which
are flags to indicate whether to deparse the input relations as subqueries.
is_subquery_rel would work well for handling the cases of full joins with
restrictions on the input relations, but I noticed that that wouldn't work
well when extending to handle the cases where we deparse the input relations
as subqueries to evaluate PHVs remotely.
* Since appendSubqueryAlias in the previous patch is pretty small, I
included the code into deparseRangeTblRef.

The patch is very fresh, so 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

#91David Steele
david@pgmasters.net
In reply to: Etsuro Fujita (#87)
Re: Push down more full joins in postgres_fdw

On 1/30/17 6:30 AM, Etsuro Fujita wrote:

On 2017/01/27 21:25, Etsuro Fujita wrote:

On 2017/01/27 20:04, Ashutosh Bapat wrote:

I think we should pick up your patch on
27th December, update the comment per your mail on 5th Jan. I will
review it once and list down the things left to committer's judgement.

Sorry, I started thinking we went in the wrong direction. I added to
deparseSelectStmtForRel build_subquery_tlists, which creates a tlist for
each subquery present in a given join tree prior to deparsing a whole
remote query. But that's nothing but an overhead; we need to create a
tlist for the top-level query because we use it as fdw_scan_tlist, but
not for subqueries, and we need to create retrieved_attrs for the
top-level query while deparsing the targetlist in
deparseExplicitTargetList, but not for subqueries. So, we should need
some work to avoid such a useless overhead.

I think we can avoid the useless overhead by adding a new function,
deparseSubqueryTargetList, that deparses expressions in the given
relation's reltarget, not the tlist, as a SELECT clause of the subquery
representing the given relation. That would also allow us to make the
1-to-1 relationship between the subquery output columns and their
aliases more explicit, which was your original comment. Please find
attached the new version. (The patch doesn't need the patch to avoid
duplicate construction of the tlist, discussed upthread.)

Other changes:
* I went back to make_outerrel_subquery and make_innerrel_subquery,
which are flags to indicate whether to deparse the input relations as
subqueries. is_subquery_rel would work well for handling the cases of
full joins with restrictions on the input relations, but I noticed that
that wouldn't work well when extending to handle the cases where we
deparse the input relations as subqueries to evaluate PHVs remotely.
* Since appendSubqueryAlias in the previous patch is pretty small, I
included the code into deparseRangeTblRef.

This patch does not apply cleanly at cccbdde:

$ patch -p1 < ../other/postgres-fdw-subquery-support-v15.patch
patching file contrib/postgres_fdw/deparse.c
Hunk #11 succeeded at 1371 (offset -3 lines).
Hunk #12 succeeded at 1419 (offset -3 lines).
Hunk #13 succeeded at 1486 (offset -3 lines).
Hunk #14 succeeded at 2186 (offset -3 lines).
Hunk #15 succeeded at 3082 (offset -3 lines).
patching file contrib/postgres_fdw/expected/postgres_fdw.out
patching file contrib/postgres_fdw/postgres_fdw.c
Hunk #1 succeeded at 669 (offset 1 line).
Hunk #2 succeeded at 1245 (offset -1 lines).
Hunk #3 succeeded at 2557 (offset -1 lines).
Hunk #4 succeeded at 4157 (offset 3 lines).
Hunk #5 succeeded at 4183 (offset 3 lines).
Hunk #6 succeeded at 4212 (offset 3 lines).
Hunk #7 succeeded at 4315 (offset 3 lines).
patching file contrib/postgres_fdw/postgres_fdw.h
patching file contrib/postgres_fdw/sql/postgres_fdw.sql

Since these are just offsets I'll leave the patch as "Needs review" but
an updated patch would be appreciated.

Thanks,
--
-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

#92Robert Haas
robertmhaas@gmail.com
In reply to: David Steele (#91)
Re: Push down more full joins in postgres_fdw

On Thu, Mar 16, 2017 at 11:46 AM, David Steele <david@pgmasters.net> wrote:

$ patch -p1 < ../other/postgres-fdw-subquery-support-v15.patch
patching file contrib/postgres_fdw/deparse.c
Hunk #11 succeeded at 1371 (offset -3 lines).
Hunk #12 succeeded at 1419 (offset -3 lines).
Hunk #13 succeeded at 1486 (offset -3 lines).
Hunk #14 succeeded at 2186 (offset -3 lines).
Hunk #15 succeeded at 3082 (offset -3 lines).
patching file contrib/postgres_fdw/expected/postgres_fdw.out
patching file contrib/postgres_fdw/postgres_fdw.c
Hunk #1 succeeded at 669 (offset 1 line).
Hunk #2 succeeded at 1245 (offset -1 lines).
Hunk #3 succeeded at 2557 (offset -1 lines).
Hunk #4 succeeded at 4157 (offset 3 lines).
Hunk #5 succeeded at 4183 (offset 3 lines).
Hunk #6 succeeded at 4212 (offset 3 lines).
Hunk #7 succeeded at 4315 (offset 3 lines).
patching file contrib/postgres_fdw/postgres_fdw.h
patching file contrib/postgres_fdw/sql/postgres_fdw.sql

Since these are just offsets I'll leave the patch as "Needs review" but
an updated patch would be appreciated.

I don't think that's really needed. Offsets don't hurt anything.
Even fuzz is OK. As long as the hunks are applying, I think it's
fine.

Incidentally, I'm reading through this one now.

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

#93Robert Haas
robertmhaas@gmail.com
In reply to: Robert Haas (#92)
Re: Push down more full joins in postgres_fdw

On Thu, Mar 16, 2017 at 12:48 PM, Robert Haas <robertmhaas@gmail.com> wrote:

On Thu, Mar 16, 2017 at 11:46 AM, David Steele <david@pgmasters.net> wrote:

$ patch -p1 < ../other/postgres-fdw-subquery-support-v15.patch
patching file contrib/postgres_fdw/deparse.c
Hunk #11 succeeded at 1371 (offset -3 lines).
Hunk #12 succeeded at 1419 (offset -3 lines).
Hunk #13 succeeded at 1486 (offset -3 lines).
Hunk #14 succeeded at 2186 (offset -3 lines).
Hunk #15 succeeded at 3082 (offset -3 lines).
patching file contrib/postgres_fdw/expected/postgres_fdw.out
patching file contrib/postgres_fdw/postgres_fdw.c
Hunk #1 succeeded at 669 (offset 1 line).
Hunk #2 succeeded at 1245 (offset -1 lines).
Hunk #3 succeeded at 2557 (offset -1 lines).
Hunk #4 succeeded at 4157 (offset 3 lines).
Hunk #5 succeeded at 4183 (offset 3 lines).
Hunk #6 succeeded at 4212 (offset 3 lines).
Hunk #7 succeeded at 4315 (offset 3 lines).
patching file contrib/postgres_fdw/postgres_fdw.h
patching file contrib/postgres_fdw/sql/postgres_fdw.sql

Since these are just offsets I'll leave the patch as "Needs review" but
an updated patch would be appreciated.

I don't think that's really needed. Offsets don't hurt anything.
Even fuzz is OK. As long as the hunks are applying, I think it's
fine.

Incidentally, I'm reading through this one now.

And ... I don't see anything to complain about, so, committed.

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

#94Etsuro Fujita
fujita.etsuro@lab.ntt.co.jp
In reply to: Robert Haas (#93)
Re: Push down more full joins in postgres_fdw

On 2017/03/17 2:35, Robert Haas wrote:

And ... I don't see anything to complain about, so, committed.

Thanks for committing, Robert! Thanks for reviewing, Ashutosh and David!

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