*** a/contrib/postgres_fdw/deparse.c
--- b/contrib/postgres_fdw/deparse.c
***************
*** 44,51 ****
--- 44,54 ----
  #include "catalog/pg_proc.h"
  #include "catalog/pg_type.h"
  #include "commands/defrem.h"
+ #include "nodes/makefuncs.h"
  #include "nodes/nodeFuncs.h"
+ #include "nodes/plannodes.h"
  #include "optimizer/clauses.h"
+ #include "optimizer/prep.h"
  #include "optimizer/var.h"
  #include "parser/parsetree.h"
  #include "utils/builtins.h"
***************
*** 92,97 **** typedef struct deparse_expr_cxt
--- 95,102 ----
  	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 */
+ 	List	   *outertlist;		/* outer child's target list */
+ 	List	   *innertlist;		/* inner child's target list */
  } deparse_expr_cxt;
  
  /*
***************
*** 111,117 **** static void deparseTargetList(StringInfo buf,
  				  Index rtindex,
  				  Relation rel,
  				  Bitmapset *attrs_used,
! 				  List **retrieved_attrs);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 Index rtindex, Relation rel,
  					 bool trig_after_row,
--- 116,123 ----
  				  Index rtindex,
  				  Relation rel,
  				  Bitmapset *attrs_used,
! 				  List **retrieved_attrs,
! 				  bool alias);
  static void deparseReturningList(StringInfo buf, PlannerInfo *root,
  					 Index rtindex, Relation rel,
  					 bool trig_after_row,
***************
*** 139,144 **** static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod,
--- 145,151 ----
  				 deparse_expr_cxt *context);
  static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod,
  					   deparse_expr_cxt *context);
+ static const char *get_jointype_name(JoinType jointype);
  
  
  /*
***************
*** 261,267 **** foreign_expr_walker(Node *node,
  				 * Param's collation, ie it's not safe for it to have a
  				 * non-default collation.
  				 */
! 				if (var->varno == glob_cxt->foreignrel->relid &&
  					var->varlevelsup == 0)
  				{
  					/* Var belongs to foreign table */
--- 268,274 ----
  				 * Param's collation, ie it's not safe for it to have a
  				 * non-default collation.
  				 */
! 				if (bms_is_member(var->varno, glob_cxt->foreignrel->relids) &&
  					var->varlevelsup == 0)
  				{
  					/* Var belongs to foreign table */
***************
*** 703,720 **** deparse_type_name(Oid type_oid, int32 typemod)
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
   */
  void
  deparseSelectSql(StringInfo buf,
  				 PlannerInfo *root,
  				 RelOptInfo *baserel,
  				 Bitmapset *attrs_used,
! 				 List **retrieved_attrs)
  {
  	RangeTblEntry *rte = planner_rt_fetch(baserel->relid, root);
  	Relation	rel;
  
  	/*
  	 * Core code already has some lock on each rel being planned, so we can
  	 * use NoLock here.
  	 */
--- 710,798 ----
   *
   * We also create an integer List of the columns being retrieved, which is
   * returned to *retrieved_attrs.
+  *
+  * The relations is a string buffer for "Relations" portion of EXPLAIN output,
+  * or NULL if caller doesn't need it.  Note that it should have been
+  * initialized by caller.
+  *
+  * The alias is a flag to add aliases of columns and tables.  This should be
+  * false in the initial call, and will be set true when this function is called
+  * for building a part of a join query.
   */
  void
  deparseSelectSql(StringInfo buf,
  				 PlannerInfo *root,
  				 RelOptInfo *baserel,
  				 Bitmapset *attrs_used,
! 				 List *remote_conds,
! 				 List *pathkeys,
! 				 List **params_list,
! 				 List **fdw_scan_tlist,
! 				 List **retrieved_attrs,
! 				 StringInfo relations,
! 				 bool alias)
  {
+ 	PgFdwRelationInfo  *fpinfo = (PgFdwRelationInfo *) baserel->fdw_private;
  	RangeTblEntry *rte = planner_rt_fetch(baserel->relid, root);
  	Relation	rel;
  
  	/*
+ 	 * If given relation was a join relation, recursively construct statement
+ 	 * by putting each outer and inner relations in FROM clause as a subquery
+ 	 * with aliasing.
+ 	 */
+ 	if (baserel->reloptkind == RELOPT_JOINREL)
+ 	{
+ 		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		sql_o;
+ 		StringInfoData		sql_i;
+ 		List			   *ret_attrs_tmp;	/* not used */
+ 		StringInfoData		relations_o;
+ 		StringInfoData		relations_i;
+ 		const char		   *jointype_str;
+ 
+ 		/*
+ 		 * Deparse query for outer and inner relation, and combine them into
+ 		 * a query.
+ 		 *
+ 		 * Here we don't pass fdw_scan_tlist because targets of underlying
+ 		 * relations are already put in joinrel->reltargetlist, and
+ 		 * deparseJoinRel() takes all care about it.
+ 		 */
+ 		initStringInfo(&sql_o);
+ 		initStringInfo(&relations_o);
+ 		deparseSelectSql(&sql_o, root, rel_o, fpinfo_o->attrs_used,
+ 						 fpinfo_o->remote_conds, NIL, params_list,
+ 						 NULL, &ret_attrs_tmp, &relations_o, true);
+ 		initStringInfo(&sql_i);
+ 		initStringInfo(&relations_i);
+ 		deparseSelectSql(&sql_i, root, rel_i, fpinfo_i->attrs_used,
+ 						 fpinfo_i->remote_conds, NIL, params_list,
+ 						 NULL, &ret_attrs_tmp, &relations_i, true);
+ 
+ 		/* For EXPLAIN output */
+ 		jointype_str = get_jointype_name(fpinfo->jointype);
+ 		if (relations)
+ 			appendStringInfo(relations, "(%s) %s JOIN (%s)",
+ 							 relations_o.data, jointype_str, relations_i.data);
+ 
+ 		deparseJoinSql(buf, root, baserel,
+ 					   fpinfo->outerrel,
+ 					   fpinfo->innerrel,
+ 					   sql_o.data,
+ 					   sql_i.data,
+ 					   fpinfo->jointype,
+ 					   fpinfo->joinclauses,
+ 					   fpinfo->otherclauses,
+ 					   fdw_scan_tlist,
+ 					   retrieved_attrs);
+ 		return;
+ 	}
+ 
+ 	/*
  	 * Core code already has some lock on each rel being planned, so we can
  	 * use NoLock here.
  	 */
***************
*** 725,731 **** deparseSelectSql(StringInfo buf,
  	 */
  	appendStringInfoString(buf, "SELECT ");
  	deparseTargetList(buf, root, baserel->relid, rel, attrs_used,
! 					  retrieved_attrs);
  
  	/*
  	 * Construct FROM clause
--- 803,809 ----
  	 */
  	appendStringInfoString(buf, "SELECT ");
  	deparseTargetList(buf, root, baserel->relid, rel, attrs_used,
! 					  retrieved_attrs, alias);
  
  	/*
  	 * Construct FROM clause
***************
*** 733,738 **** deparseSelectSql(StringInfo buf,
--- 811,901 ----
  	appendStringInfoString(buf, " FROM ");
  	deparseRelation(buf, rel);
  
+ 	/*
+ 	 * Return local relation name for EXPLAIN output.
+ 	 * We can't know VERBOSE option is specified or not, so always add shcema
+ 	 * name.
+ 	 */
+ 	if (relations)
+ 	{
+ 		const char	   *namespace;
+ 		const char	   *relname;
+ 		const char	   *refname;
+ 
+ 		namespace = get_namespace_name(get_rel_namespace(rte->relid));
+ 		relname = get_rel_name(rte->relid);
+ 		refname = rte->eref->aliasname;
+ 		appendStringInfo(relations, "%s.%s",
+ 						 quote_identifier(namespace),
+ 						 quote_identifier(relname));
+ 		if (*refname && strcmp(refname, relname) != 0)
+ 			appendStringInfo(relations, " %s",
+ 							 quote_identifier(rte->eref->aliasname));
+ 	}
+ 
+ 	/*
+ 	 * Construct WHERE clause
+ 	 */
+ 	if (remote_conds)
+ 		appendConditions(buf, root, baserel, NULL, NULL, remote_conds,
+ 						 " WHERE ", params_list);
+ 
+ 	/* Add ORDER BY clause if we found any useful pathkeys */
+ 	if (pathkeys)
+ 		appendOrderByClause(buf, root, baserel, pathkeys);
+ 
+ 	/*
+ 	 * 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.
+ 	 */
+ 	if (baserel->relid == root->parse->resultRelation &&
+ 		(root->parse->commandType == CMD_UPDATE ||
+ 		 root->parse->commandType == CMD_DELETE))
+ 	{
+ 		/* Relation is UPDATE/DELETE target, so use FOR UPDATE */
+ 		appendStringInfoString(buf, " FOR UPDATE");
+ 	}
+ 	else
+ 	{
+ 		PlanRowMark *rc = get_plan_rowmark(root->rowMarks, baserel->relid);
+ 
+ 		if (rc)
+ 		{
+ 			/*
+ 			 * Relation is specified as a FOR UPDATE/SHARE target, so handle
+ 			 * that.  (But we could also see LCS_NONE, meaning this isn't a
+ 			 * target relation after all.)
+ 			 *
+ 			 * For now, just ignore any [NO] KEY specification, since (a)
+ 			 * it's not clear what that means for a remote table that we
+ 			 * don't have complete information about, and (b) it wouldn't
+ 			 * work anyway on older remote servers.  Likewise, we don't
+ 			 * worry about NOWAIT.
+ 			 */
+ 			switch (rc->strength)
+ 			{
+ 				case LCS_NONE:
+ 					/* No locking needed */
+ 					break;
+ 				case LCS_FORKEYSHARE:
+ 				case LCS_FORSHARE:
+ 					appendStringInfoString(buf, " FOR SHARE");
+ 					break;
+ 				case LCS_FORNOKEYUPDATE:
+ 				case LCS_FORUPDATE:
+ 					appendStringInfoString(buf, " FOR UPDATE");
+ 					break;
+ 			}
+ 		}
+ 	}
+ 
  	heap_close(rel, NoLock);
  }
  
***************
*** 749,755 **** deparseTargetList(StringInfo buf,
  				  Index rtindex,
  				  Relation rel,
  				  Bitmapset *attrs_used,
! 				  List **retrieved_attrs)
  {
  	TupleDesc	tupdesc = RelationGetDescr(rel);
  	bool		have_wholerow;
--- 912,919 ----
  				  Index rtindex,
  				  Relation rel,
  				  Bitmapset *attrs_used,
! 				  List **retrieved_attrs,
! 				  bool alias)
  {
  	TupleDesc	tupdesc = RelationGetDescr(rel);
  	bool		have_wholerow;
***************
*** 780,785 **** deparseTargetList(StringInfo buf,
--- 944,952 ----
  			first = false;
  
  			deparseColumnRef(buf, rtindex, i, root);
+ 			if (alias)
+ 				appendStringInfo(buf, " a%d",
+ 								 i - FirstLowInvalidHeapAttributeNumber);
  
  			*retrieved_attrs = lappend_int(*retrieved_attrs, i);
  		}
***************
*** 797,802 **** deparseTargetList(StringInfo buf,
--- 964,972 ----
  		first = false;
  
  		appendStringInfoString(buf, "ctid");
+ 		if (alias)
+ 			appendStringInfo(buf, " a%d",
+ 							 SelfItemPointerAttributeNumber - FirstLowInvalidHeapAttributeNumber);
  
  		*retrieved_attrs = lappend_int(*retrieved_attrs,
  									   SelfItemPointerAttributeNumber);
***************
*** 808,818 **** deparseTargetList(StringInfo buf,
  }
  
  /*
!  * Deparse WHERE clauses in given list of RestrictInfos and append them to buf.
   *
   * baserel is the foreign table we're planning for.
   *
!  * If no WHERE clause already exists in the buffer, is_first should be true.
   *
   * If params is not NULL, it receives a list of Params and other-relation Vars
   * used in the clauses; these values must be transmitted to the remote server
--- 978,990 ----
  }
  
  /*
!  * Deparse conditions, such as WHERE clause and ON clause of JOIN, in the given
!  * list, consist of RestrictInfo or Expr, and append string representation of
!  * them to buf.
   *
   * baserel is the foreign table we're planning for.
   *
!  * prefix is placed before the conditions, if any.
   *
   * If params is not NULL, it receives a list of Params and other-relation Vars
   * used in the clauses; these values must be transmitted to the remote server
***************
*** 822,837 **** deparseTargetList(StringInfo buf,
   * so Params and other-relation Vars should be replaced by dummy values.
   */
  void
! appendWhereClause(StringInfo buf,
! 				  PlannerInfo *root,
! 				  RelOptInfo *baserel,
! 				  List *exprs,
! 				  bool is_first,
! 				  List **params)
  {
  	deparse_expr_cxt context;
  	int			nestlevel;
  	ListCell   *lc;
  
  	if (params)
  		*params = NIL;			/* initialize result list to empty */
--- 994,1012 ----
   * so Params and other-relation Vars should be replaced by dummy values.
   */
  void
! appendConditions(StringInfo buf,
! 				 PlannerInfo *root,
! 				 RelOptInfo *baserel,
! 				 List *outertlist,
! 				 List *innertlist,
! 				 List *exprs,
! 				 const char *prefix,
! 				 List **params)
  {
  	deparse_expr_cxt context;
  	int			nestlevel;
  	ListCell   *lc;
+ 	bool		is_first = prefix == NULL ? false : true;
  
  	if (params)
  		*params = NIL;			/* initialize result list to empty */
***************
*** 841,862 **** appendWhereClause(StringInfo buf,
  	context.foreignrel = baserel;
  	context.buf = buf;
  	context.params_list = params;
  
  	/* Make sure any constants in the exprs are printed portably */
  	nestlevel = set_transmission_modes();
  
  	foreach(lc, exprs)
  	{
  		RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
  
  		/* Connect expressions with "AND" and parenthesize each condition. */
  		if (is_first)
! 			appendStringInfoString(buf, " WHERE ");
  		else
  			appendStringInfoString(buf, " AND ");
  
  		appendStringInfoChar(buf, '(');
! 		deparseExpr(ri->clause, &context);
  		appendStringInfoChar(buf, ')');
  
  		is_first = false;
--- 1016,1051 ----
  	context.foreignrel = baserel;
  	context.buf = buf;
  	context.params_list = params;
+ 	context.outertlist = outertlist;
+ 	context.innertlist = innertlist;
  
  	/* Make sure any constants in the exprs are printed portably */
  	nestlevel = set_transmission_modes();
  
  	foreach(lc, exprs)
  	{
+ 		Node	   *node = (Node *) lfirst(lc);
  		RestrictInfo *ri = (RestrictInfo *) lfirst(lc);
+ 		Expr	   *expr = (Expr *) lfirst(lc);
+ 
+ 		if (IsA(node, RestrictInfo))
+ 		{
+ 			expr = ri->clause;
+ 		}
+ 		else
+ 		{
+ 			expr = ri->clause;
+ 			expr = (Expr *) node;
+ 		}
  
  		/* Connect expressions with "AND" and parenthesize each condition. */
  		if (is_first)
! 			appendStringInfoString(buf, prefix);
  		else
  			appendStringInfoString(buf, " AND ");
  
  		appendStringInfoChar(buf, '(');
! 		deparseExpr(expr, &context);
  		appendStringInfoChar(buf, ')');
  
  		is_first = false;
***************
*** 866,871 **** appendWhereClause(StringInfo buf,
--- 1055,1351 ----
  }
  
  /*
+  * Returns position index (start with 1) of given var in given target list, or
+  * 0 when not found.
+  */
+ static int
+ find_var_pos(Var *node, List *tlist)
+ {
+ 	int		pos = 1;
+ 	ListCell *lc;
+ 
+ 	foreach(lc, tlist)
+ 	{
+ 		Var *var = (Var *) lfirst(lc);
+ 
+ 		if (equal(var, node))
+ 		{
+ 			return pos;
+ 		}
+ 		pos++;
+ 	}
+ 
+ 	return 0;
+ }
+ 
+ /*
+  * Deparse given Var into buf.
+  */
+ static void
+ deparseJoinVar(Var *node, deparse_expr_cxt *context)
+ {
+ 	char		side;
+ 	int			pos;
+ 
+ 	pos = find_var_pos(node, context->outertlist);
+ 	if (pos > 0)
+ 		side = 'l';
+ 	else
+ 	{
+ 		side = 'r';
+ 		pos = find_var_pos(node, context->innertlist);
+ 	}
+ 
+ 	/*
+ 	 * We treat whole-row reference same as ordinary attribute references,
+ 	 * because such transformation should be done in lower level.
+ 	 */
+ 	appendStringInfo(context->buf, "%c.a%d", side, pos);
+ }
+ 
+ /*
+  * Deparse column alias list for a subquery in FROM clause.
+  */
+ static void
+ deparseColumnAliases(StringInfo buf, List *tlist)
+ {
+ 	int			pos;
+ 	ListCell   *lc;
+ 
+ 	pos = 1;
+ 	foreach(lc, tlist)
+ 	{
+ 		/* Deparse column alias for the subquery */
+ 		if (pos > 1)
+ 			appendStringInfoString(buf, ", ");
+ 		appendStringInfo(buf, "a%d", pos);
+ 		pos++;
+ 	}
+ }
+ 
+ /*
+  * Deparse "wrapper" SQL for a query which projects target lists in proper
+  * order and contents.  Note that this treatment is necessary only for queries
+  * used in FROM clause of a join query.
+  *
+  * Even if the SQL is enough simple (no ctid, no whole-row reference), the order
+  * of output column might different from underlying scan, so we always need to
+  * wrap the queries for join sources.
+  *
+  */
+ static const char *
+ deparseProjectionSql(PlannerInfo *root,
+ 					 RelOptInfo *baserel,
+ 					 const char *sql,
+ 					 char side)
+ {
+ 	StringInfoData wholerow;
+ 	StringInfoData buf;
+ 	ListCell   *lc;
+ 	bool		first;
+ 	bool		have_wholerow = false;
+ 
+ 	/*
+ 	 * We have nothing to do if the targetlist contains no special reference,
+ 	 * such as whole-row and ctid.
+ 	 */
+ 	foreach(lc, baserel->reltargetlist)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 		if (var->varattno == 0)
+ 		{
+ 			have_wholerow = true;
+ 			break;
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * Construct whole-row reference with ROW() syntax
+ 	 */
+ 	if (have_wholerow)
+ 	{
+ 		RangeTblEntry *rte;
+ 		Relation		rel;
+ 		TupleDesc		tupdesc;
+ 		int				i;
+ 
+ 		/* Obtain TupleDesc for deparsing all valid columns */
+ 		rte = planner_rt_fetch(baserel->relid, root);
+ 		rel = heap_open(rte->relid, NoLock);
+ 		tupdesc = rel->rd_att;
+ 
+ 		/* Print all valid columns in ROW() to generate whole-row value */
+ 		initStringInfo(&wholerow);
+ 		appendStringInfoString(&wholerow, "ROW(");
+ 		first = true;
+ 		for (i = 1; i <= tupdesc->natts; i++)
+ 		{
+ 			Form_pg_attribute attr = tupdesc->attrs[i - 1];
+ 
+ 			/* Ignore dropped columns. */
+ 			if (attr->attisdropped)
+ 				continue;
+ 
+ 			if (!first)
+ 				appendStringInfoString(&wholerow, ", ");
+ 			first = false;
+ 
+ 			appendStringInfo(&wholerow, "%c.a%d", side,
+ 							 i - FirstLowInvalidHeapAttributeNumber);
+ 		}
+ 		appendStringInfoString(&wholerow, ")");
+ 
+ 		heap_close(rel, NoLock);
+ 	}
+ 
+ 	/*
+ 	 * Construct a SELECT statement which has the original query in its FROM
+ 	 * clause, and have target list entries in its SELECT clause.  The number
+ 	 * used in column aliases are attnum - FirstLowInvalidHeapAttributeNumber in
+ 	 * order to make all numbers positive even for system columns which have
+ 	 * minus value as attnum.
+ 	 */
+ 	initStringInfo(&buf);
+ 	appendStringInfoString(&buf, "SELECT ");
+ 	first = true;
+ 	foreach(lc, baserel->reltargetlist)
+ 	{
+ 		Var *var = (Var *) lfirst(lc);
+ 
+ 		if (!first)
+ 			appendStringInfoString(&buf, ", ");
+ 	
+ 		if (var->varattno == 0)
+ 			appendStringInfo(&buf, "%s", wholerow.data);
+ 		else
+ 			appendStringInfo(&buf, "%c.a%d", side,
+ 							 var->varattno - FirstLowInvalidHeapAttributeNumber);
+ 
+ 		first = false;
+ 	}
+ 	appendStringInfo(&buf, " FROM (%s) %c", sql, side);
+ 
+ 	return buf.data;
+ }
+ 
+ static const char *
+ get_jointype_name(JoinType jointype)
+ {
+ 	if (jointype == JOIN_INNER)
+ 		return "INNER";
+ 	else if (jointype == JOIN_LEFT)
+ 		return "LEFT";
+ 	else if (jointype == JOIN_RIGHT)
+ 		return "RIGHT";
+ 	else if (jointype == JOIN_FULL)
+ 		return "FULL";
+ 
+ 	/* not reached */
+ 	elog(ERROR, "unsupported join type %d", jointype);
+ }
+ 
+ /*
+  * Construct a SELECT statement which contains join clause.
+  *
+  * We also create an TargetEntry List of the columns being retrieved, which is
+  * returned to *fdw_scan_tlist.
+  *
+  * path_o, tl_o, sql_o are respectively path, targetlist, and remote query
+  * statement of the outer child relation.  postfix _i means those for the inner
+  * child relation.  jointype and joinclauses are information of join method.
+  * fdw_scan_tlist is output parameter to pass target list of the pseudo scan to
+  * caller.
+  */
+ void
+ deparseJoinSql(StringInfo buf,
+ 			   PlannerInfo *root,
+ 			   RelOptInfo *baserel,
+ 			   RelOptInfo *outerrel,
+ 			   RelOptInfo *innerrel,
+ 			   const char *sql_o,
+ 			   const char *sql_i,
+ 			   JoinType jointype,
+ 			   List *joinclauses,
+ 			   List *otherclauses,
+ 			   List **fdw_scan_tlist,
+ 			   List **retrieved_attrs)
+ {
+ 	StringInfoData selbuf;		/* buffer for SELECT clause */
+ 	StringInfoData abuf_o;		/* buffer for column alias list of outer */
+ 	StringInfoData abuf_i;		/* buffer for column alias list of inner */
+ 	int			i;
+ 	ListCell   *lc;
+ 	const char *jointype_str;
+ 	deparse_expr_cxt context;
+ 
+ 	context.root = root;
+ 	context.foreignrel = baserel;
+ 	context.buf = &selbuf;
+ 	context.params_list = NULL;
+ 	context.outertlist = outerrel->reltargetlist;
+ 	context.innertlist = innerrel->reltargetlist;
+ 
+ 	jointype_str = get_jointype_name(jointype);
+ 	*retrieved_attrs = NIL;
+ 
+ 	/* print SELECT clause of the join scan */
+ 	initStringInfo(&selbuf);
+ 	i = 0;
+ 	foreach(lc, baserel->reltargetlist)
+ 	{
+ 		Var		   *var = (Var *) lfirst(lc);
+ 		TargetEntry *tle;
+ 
+ 		if (i > 0)
+ 			appendStringInfoString(&selbuf, ", ");
+ 		deparseJoinVar(var, &context);
+ 
+ 		tle = makeTargetEntry((Expr *) var, i + 1, NULL, false);
+ 		if (fdw_scan_tlist)
+ 			*fdw_scan_tlist = lappend(*fdw_scan_tlist, tle);
+ 
+ 		*retrieved_attrs = lappend_int(*retrieved_attrs, i + 1);
+ 
+ 		i++;
+ 	}
+ 	if (i == 0)
+ 		appendStringInfoString(&selbuf, "NULL");
+ 
+ 	/*
+ 	 * Do pseudo-projection for an underlying scan on a foreign table, if a) the
+ 	 * relation is a base relation, and b) its targetlist contains whole-row
+ 	 * reference.
+ 	 */
+ 	if (outerrel->reloptkind == RELOPT_BASEREL)
+ 		sql_o = deparseProjectionSql(root, outerrel, sql_o, 'l');
+ 	if (innerrel->reloptkind == RELOPT_BASEREL)
+ 		sql_i = deparseProjectionSql(root, innerrel, sql_i, 'r');
+ 
+ 	/* Deparse column alias portion of subquery in FROM clause. */
+ 	initStringInfo(&abuf_o);
+ 	deparseColumnAliases(&abuf_o, outerrel->reltargetlist);
+ 	initStringInfo(&abuf_i);
+ 	deparseColumnAliases(&abuf_i, innerrel->reltargetlist);
+ 
+ 	/* Construct SELECT statement */
+ 	appendStringInfo(buf, "SELECT %s FROM", selbuf.data);
+ 	appendStringInfo(buf, " (%s) l (%s) %s JOIN (%s) r (%s)",
+ 					 sql_o, abuf_o.data, jointype_str, sql_i, abuf_i.data);
+ 	/* Append ON clause */
+ 	if (joinclauses)
+ 		appendConditions(buf, root, baserel,
+ 						 outerrel->reltargetlist, innerrel->reltargetlist,
+ 						 joinclauses,
+ 						 " ON ", NULL);
+ 	/* Append WHERE clause */
+ 	if (otherclauses)
+ 		appendConditions(buf, root, baserel,
+ 						 outerrel->reltargetlist, innerrel->reltargetlist,
+ 						 otherclauses,
+ 						 " WHERE ", NULL);
+ }
+ 
+ /*
   * deparse remote INSERT statement
   *
   * The statement text is appended to buf, and we also create an integer List
***************
*** 1025,1031 **** deparseReturningList(StringInfo buf, PlannerInfo *root,
  	{
  		appendStringInfoString(buf, " RETURNING ");
  		deparseTargetList(buf, root, rtindex, rel, attrs_used,
! 						  retrieved_attrs);
  	}
  	else
  		*retrieved_attrs = NIL;
--- 1505,1511 ----
  	{
  		appendStringInfoString(buf, " RETURNING ");
  		deparseTargetList(buf, root, rtindex, rel, attrs_used,
! 						  retrieved_attrs, false);
  	}
  	else
  		*retrieved_attrs = NIL;
***************
*** 1292,1297 **** deparseExpr(Expr *node, deparse_expr_cxt *context)
--- 1772,1779 ----
  /*
   * Deparse given Var node into context->buf.
   *
+  * If context has valid innerrel, this is invoked for a join conditions.
+  *
   * 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
***************
*** 1302,1340 **** deparseVar(Var *node, deparse_expr_cxt *context)
  {
  	StringInfo	buf = context->buf;
  
! 	if (node->varno == context->foreignrel->relid &&
! 		node->varlevelsup == 0)
  	{
! 		/* Var belongs to foreign table */
! 		deparseColumnRef(buf, node->varno, node->varattno, context->root);
  	}
  	else
  	{
! 		/* Treat like a Param */
! 		if (context->params_list)
  		{
! 			int			pindex = 0;
! 			ListCell   *lc;
! 
! 			/* find its index in params_list */
! 			foreach(lc, *context->params_list)
  			{
! 				pindex++;
! 				if (equal(node, (Node *) lfirst(lc)))
! 					break;
  			}
! 			if (lc == NULL)
  			{
! 				/* not in list, so add it */
! 				pindex++;
! 				*context->params_list = lappend(*context->params_list, node);
  			}
- 
- 			printRemoteParam(pindex, node->vartype, node->vartypmod, context);
- 		}
- 		else
- 		{
- 			printRemotePlaceholder(node->vartype, node->vartypmod, context);
  		}
  	}
  }
--- 1784,1829 ----
  {
  	StringInfo	buf = context->buf;
  
! 	if (context->foreignrel->reloptkind == RELOPT_JOINREL)
  	{
! 		deparseJoinVar(node, context);
  	}
  	else
  	{
! 		if (node->varno == context->foreignrel->relid &&
! 			node->varlevelsup == 0)
  		{
! 			/* Var belongs to foreign table */
! 			deparseColumnRef(buf, node->varno, node->varattno, context->root);
! 		}
! 		else
! 		{
! 			/* Treat like a Param */
! 			if (context->params_list)
  			{
! 				int			pindex = 0;
! 				ListCell   *lc;
! 
! 				/* find its index in params_list */
! 				foreach(lc, *context->params_list)
! 				{
! 					pindex++;
! 					if (equal(node, (Node *) lfirst(lc)))
! 						break;
! 				}
! 				if (lc == NULL)
! 				{
! 					/* not in list, so add it */
! 					pindex++;
! 					*context->params_list = lappend(*context->params_list, node);
! 				}
! 
! 				printRemoteParam(pindex, node->vartype, node->vartypmod, context);
  			}
! 			else
  			{
! 				printRemotePlaceholder(node->vartype, node->vartypmod, context);
  			}
  		}
  	}
  }
*** a/contrib/postgres_fdw/expected/postgres_fdw.out
--- b/contrib/postgres_fdw/expected/postgres_fdw.out
***************
*** 9,19 **** DO $d$
--- 9,24 ----
              OPTIONS (dbname '$$||current_database()||$$',
                       port '$$||current_setting('port')||$$'
              )$$;
+         EXECUTE $$CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgres_fdw
+             OPTIONS (dbname '$$||current_database()||$$',
+                      port '$$||current_setting('port')||$$'
+             )$$;
      END;
  $d$;
  CREATE USER MAPPING FOR public SERVER testserver1
  	OPTIONS (user 'value', password 'value');
  CREATE USER MAPPING FOR CURRENT_USER SERVER loopback;
+ CREATE USER MAPPING FOR CURRENT_USER SERVER loopback2;
  -- ===================================================================
  -- create objects used through FDW loopback server
  -- ===================================================================
***************
*** 35,40 **** CREATE TABLE "S 1"."T 2" (
--- 40,57 ----
  	c2 text,
  	CONSTRAINT t2_pkey PRIMARY KEY (c1)
  );
+ CREATE TABLE "S 1"."T 3" (
+ 	c1 int NOT NULL,
+ 	c2 int NOT NULL,
+ 	c3 text,
+ 	CONSTRAINT t3_pkey PRIMARY KEY (c1)
+ );
+ CREATE TABLE "S 1"."T 4" (
+ 	c1 int NOT NULL,
+ 	c2 int NOT NULL,
+ 	c4 text,
+ 	CONSTRAINT t4_pkey PRIMARY KEY (c1)
+ );
  INSERT INTO "S 1"."T 1"
  	SELECT id,
  	       id % 10,
***************
*** 49,56 **** INSERT INTO "S 1"."T 2"
--- 66,87 ----
  	SELECT id,
  	       'AAA' || to_char(id, 'FM000')
  	FROM generate_series(1, 100) id;
+ INSERT INTO "S 1"."T 3"
+ 	SELECT id,
+ 	       id + 1,
+ 	       'AAA' || to_char(id, 'FM000')
+ 	FROM generate_series(1, 100) id;
+ DELETE FROM "S 1"."T 3" WHERE c1 % 2 != 0;	-- delete for outer join tests
+ INSERT INTO "S 1"."T 4"
+ 	SELECT id,
+ 	       id + 1,
+ 	       'AAA' || to_char(id, 'FM000')
+ 	FROM generate_series(1, 100) id;
+ DELETE FROM "S 1"."T 4" WHERE c1 % 3 != 0;	-- delete for outer join tests
  ANALYZE "S 1"."T 1";
  ANALYZE "S 1"."T 2";
+ ANALYZE "S 1"."T 3";
+ ANALYZE "S 1"."T 4";
  -- ===================================================================
  -- create foreign tables
  -- ===================================================================
***************
*** 78,83 **** CREATE FOREIGN TABLE ft2 (
--- 109,134 ----
  	c8 user_enum
  ) SERVER loopback;
  ALTER FOREIGN TABLE ft2 DROP COLUMN cx;
+ CREATE FOREIGN TABLE ft4 (
+ 	c1 int NOT NULL,
+ 	c2 int NOT NULL,
+ 	c3 text
+ ) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 3');
+ CREATE FOREIGN TABLE ft5 (
+ 	c1 int NOT NULL,
+ 	c2 int NOT NULL,
+ 	c3 text
+ ) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 4');
+ CREATE FOREIGN TABLE ft6 (
+ 	c1 int NOT NULL,
+ 	c2 int NOT NULL,
+ 	c3 text
+ ) SERVER loopback2 OPTIONS (schema_name 'S 1', table_name 'T 4');
+ CREATE USER view_owner;
+ GRANT ALL ON ft5 TO view_owner;
+ CREATE VIEW v_ft5 AS SELECT * FROM ft5;
+ ALTER VIEW v_ft5 OWNER TO view_owner;
+ CREATE USER MAPPING FOR view_owner SERVER loopback;
  -- ===================================================================
  -- tests for validator
  -- ===================================================================
***************
*** 127,138 **** ALTER FOREIGN TABLE ft2 OPTIONS (schema_name 'S 1', table_name 'T 1');
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
  ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
  \det+
!                              List of foreign tables
!  Schema | Table |  Server  |              FDW Options              | Description 
! --------+-------+----------+---------------------------------------+-------------
!  public | ft1   | loopback | (schema_name 'S 1', table_name 'T 1') | 
!  public | ft2   | loopback | (schema_name 'S 1', table_name 'T 1') | 
! (2 rows)
  
  -- Now we should be able to run ANALYZE.
  -- To exercise multiple code paths, we use local stats on ft1
--- 178,192 ----
  ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
  ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (column_name 'C 1');
  \det+
!                               List of foreign tables
!  Schema | Table |  Server   |              FDW Options              | Description 
! --------+-------+-----------+---------------------------------------+-------------
!  public | ft1   | loopback  | (schema_name 'S 1', table_name 'T 1') | 
!  public | ft2   | loopback  | (schema_name 'S 1', table_name 'T 1') | 
!  public | ft4   | loopback  | (schema_name 'S 1', table_name 'T 3') | 
!  public | ft5   | loopback  | (schema_name 'S 1', table_name 'T 4') | 
!  public | ft6   | loopback2 | (schema_name 'S 1', table_name 'T 4') | 
! (5 rows)
  
  -- Now we should be able to run ANALYZE.
  -- To exercise multiple code paths, we use local stats on ft1
***************
*** 281,302 **** SELECT COUNT(*) FROM ft1 t1;
    1000
  (1 row)
  
- -- join two tables
- SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
-  c1  
- -----
-  101
-  102
-  103
-  104
-  105
-  106
-  107
-  108
-  109
-  110
- (10 rows)
- 
  -- subquery
  SELECT * FROM ft1 t1 WHERE t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 <= 10) ORDER BY c1;
   c1 | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
--- 335,340 ----
***************
*** 446,462 **** EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE c8 = 'foo';  -- can't
  -- parameterized remote path
  EXPLAIN (VERBOSE, COSTS false)
    SELECT * FROM ft2 a, ft2 b WHERE a.c1 = 47 AND b.c1 = a.c2;
!                                                  QUERY PLAN                                                  
! -------------------------------------------------------------------------------------------------------------
!  Nested Loop
     Output: a.c1, a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8, b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8
!    ->  Foreign Scan on public.ft2 a
!          Output: a.c1, a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8
!          Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = 47))
!    ->  Foreign Scan on public.ft2 b
!          Output: b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8
!          Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (($1::integer = "C 1"))
! (8 rows)
  
  SELECT * FROM ft2 a, ft2 b WHERE a.c1 = 47 AND b.c1 = a.c2;
   c1 | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  | c1 | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
--- 484,496 ----
  -- parameterized remote path
  EXPLAIN (VERBOSE, COSTS false)
    SELECT * FROM ft2 a, ft2 b WHERE a.c1 = 47 AND b.c1 = a.c2;
!                                                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                                                      
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
!  Foreign Scan
     Output: a.c1, a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8, b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8
!    Relations: (public.ft2 a) INNER JOIN (public.ft2 b)
!    Remote SQL: SELECT l.a1, l.a2, l.a3, l.a4, l.a5, l.a6, l.a7, l.a8, r.a1, r.a2, r.a3, r.a4, r.a5, r.a6, r.a7, r.a8 FROM (SELECT l.a9, l.a10, l.a12, l.a13, l.a14, l.a15, l.a16, l.a17 FROM (SELECT "C 1" a9, c2 a10, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17 FROM "S 1"."T 1" WHERE (("C 1" = 47))) l) l (a1, a2, a3, a4, a5, a6, a7, a8) INNER JOIN (SELECT r.a9, r.a10, r.a12, r.a13, r.a14, r.a15, r.a16, r.a17 FROM (SELECT "C 1" a9, c2 a10, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17 FROM "S 1"."T 1") r) r (a1, a2, a3, a4, a5, a6, a7, a8) ON ((l.a2 = r.a1))
! (4 rows)
  
  SELECT * FROM ft2 a, ft2 b WHERE a.c1 = 47 AND b.c1 = a.c2;
   c1 | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  | c1 | c2 |  c3   |              c4              |            c5            | c6 |     c7     | c8  
***************
*** 757,762 **** SELECT count(c3) FROM ft1 t1 WHERE t1.c1 === t1.c2;
--- 791,1434 ----
  (1 row)
  
  -- ===================================================================
+ -- JOIN queries
+ -- ===================================================================
+ -- join two tables
+ EXPLAIN (COSTS false, VERBOSE)
+ 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
+    ->  Sort
+          Output: t1.c1, t2.c1, t1.c3
+          Sort Key: t1.c3, t1.c1
+          ->  Foreign Scan
+                Output: t1.c1, t2.c1, t1.c3
+                Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
+                Remote SQL: SELECT l.a1, l.a2, r.a1 FROM (SELECT l.a10, l.a12 FROM (SELECT "C 1" a10, c3 a12 FROM "S 1"."T 1") l) l (a1, a2) INNER JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1))
+ (9 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 (COSTS false, VERBOSE)
+ 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
+          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 l.a1, l.a2, l.a3, r.a1 FROM (SELECT l.a1, l.a2, r.a1, r.a2 FROM (SELECT l.a10, l.a12 FROM (SELECT "C 1" a10, c3 a12 FROM "S 1"."T 1") l) l (a1, a2) INNER JOIN (SELECT r.a10, r.a9 FROM (SELECT "C 1" a9, c2 a10 FROM "S 1"."T 1") r) r (a1, a2) ON ((l.a1 = r.a2))) l (a1, a2, a3, a4) INNER JOIN (SELECT r.a11, r.a9 FROM (SELECT c1 a9, c3 a11 FROM "S 1"."T 3") r) r (a1, a2) ON ((l.a1 = r.a2))
+ (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
+  32 |  2 | AAA032
+  34 |  4 | AAA034
+  36 |  6 | AAA036
+  38 |  8 | AAA038
+  40 |  0 | AAA040
+ (10 rows)
+ 
+ -- left outer join
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
+                                                                                               QUERY PLAN                                                                                               
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: t1.c1, t2.c1
+    ->  Sort
+          Output: t1.c1, t2.c1
+          Sort Key: t1.c1, t2.c1
+          ->  Foreign Scan
+                Output: t1.c1, t2.c1
+                Relations: (public.ft4 t1) LEFT JOIN (public.ft5 t2)
+                Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a9 FROM (SELECT c1 a9 FROM "S 1"."T 3") l) l (a1) LEFT JOIN (SELECT r.a9 FROM (SELECT c1 a9 FROM "S 1"."T 4") r) r (a1) ON ((l.a1 = r.a1))
+ (9 rows)
+ 
+ SELECT t1.c1, t2.c1 FROM ft4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
+  c1 | c1 
+ ----+----
+  22 |   
+  24 | 24
+  26 |   
+  28 |   
+  30 | 30
+  32 |   
+  34 |   
+  36 | 36
+  38 |   
+  40 |   
+ (10 rows)
+ 
+ -- right outer join
+ EXPLAIN (COSTS false, VERBOSE)
+ 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;
+                                                                                               QUERY PLAN                                                                                               
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: t1.c1, t2.c1
+    ->  Sort
+          Output: t1.c1, t2.c1
+          Sort Key: t2.c1, t1.c1
+          ->  Foreign Scan
+                Output: t1.c1, t2.c1
+                Relations: (public.ft4 t2) LEFT JOIN (public.ft5 t1)
+                Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a9 FROM (SELECT c1 a9 FROM "S 1"."T 3") l) l (a1) LEFT JOIN (SELECT r.a9 FROM (SELECT c1 a9 FROM "S 1"."T 4") r) r (a1) ON ((r.a1 = l.a1))
+ (9 rows)
+ 
+ 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;
+  c1 | c1 
+ ----+----
+     | 22
+  24 | 24
+     | 26
+     | 28
+  30 | 30
+     | 32
+     | 34
+  36 | 36
+     | 38
+     | 40
+ (10 rows)
+ 
+ -- full outer join
+ EXPLAIN (COSTS false, VERBOSE)
+ 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;
+                                                                                               QUERY PLAN                                                                                               
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: t1.c1, t2.c1
+    ->  Sort
+          Output: t1.c1, t2.c1
+          Sort Key: t1.c1, t2.c1
+          ->  Foreign Scan
+                Output: t1.c1, t2.c1
+                Relations: (public.ft4 t1) FULL JOIN (public.ft5 t2)
+                Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a9 FROM (SELECT c1 a9 FROM "S 1"."T 3") l) l (a1) FULL JOIN (SELECT r.a9 FROM (SELECT c1 a9 FROM "S 1"."T 4") r) r (a1) ON ((l.a1 = r.a1))
+ (9 rows)
+ 
+ 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;
+  c1  | c1 
+ -----+----
+   92 |   
+   94 |   
+   96 | 96
+   98 |   
+  100 |   
+      |  3
+      |  9
+      | 15
+      | 21
+      | 27
+ (10 rows)
+ 
+ -- full outer join + WHERE clause, only matched rows
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 = t2.c1 OR t1.c1 IS NULL) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
+                                                                                                                    QUERY PLAN                                                                                                                    
+ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: t1.c1, t2.c1
+    ->  Sort
+          Output: t1.c1, t2.c1
+          Sort Key: t1.c1, t2.c1
+          ->  Foreign Scan
+                Output: t1.c1, t2.c1
+                Relations: (public.ft4 t1) FULL JOIN (public.ft5 t2)
+                Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a9 FROM (SELECT c1 a9 FROM "S 1"."T 3") l) l (a1) FULL JOIN (SELECT r.a9 FROM (SELECT c1 a9 FROM "S 1"."T 4") r) r (a1) ON ((l.a1 = r.a1)) WHERE (((l.a1 = r.a1) OR (l.a1 IS NULL)))
+ (9 rows)
+ 
+ SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 = t2.c1 OR t1.c1 IS NULL) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
+  c1 | c1 
+ ----+----
+  66 | 66
+  72 | 72
+  78 | 78
+  84 | 84
+  90 | 90
+  96 | 96
+     |  3
+     |  9
+     | 15
+     | 21
+ (10 rows)
+ 
+ -- join at WHERE clause 
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON true WHERE (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+                                                                                                                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
+                Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
+                Remote SQL: SELECT l.a1, l.a2, r.a1 FROM (SELECT l.a10, l.a12 FROM (SELECT "C 1" a10, c3 a12 FROM "S 1"."T 1") l) l (a1, a2) INNER JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1))
+ (9 rows)
+ 
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON true WHERE (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 in CTE
+ EXPLAIN (COSTS false, VERBOSE)
+ 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 l.a1, l.a2, r.a1 FROM (SELECT l.a10, l.a12 FROM (SELECT "C 1" a10, c3 a12 FROM "S 1"."T 1") l) l (a1, a2) INNER JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1))
+    ->  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 
+ ------+------
+   101 |  101
+   102 |  102
+   103 |  103
+   104 |  104
+   105 |  105
+   106 |  106
+   107 |  107
+   108 |  108
+   109 |  109
+   110 |  110
+ (10 rows)
+ 
+ -- ctid with whole-row reference
+ EXPLAIN (COSTS false, VERBOSE)
+ 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
+    ->  Sort
+          Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
+          Sort Key: t1.c3, t1.c1
+          ->  Foreign Scan
+                Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3
+                Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
+                Remote SQL: SELECT l.a1, l.a2, l.a3, l.a4, r.a1 FROM (SELECT l.a7, ROW(l.a10, l.a11, l.a12, l.a13, l.a14, l.a15, l.a16, l.a17), l.a10, l.a12 FROM (SELECT "C 1" a10, c2 a11, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17, ctid a7 FROM "S 1"."T 1") l) l (a1, a2, a3, a4) INNER JOIN (SELECT ROW(r.a9, r.a10, r.a12, r.a13, r.a14, r.a15, r.a16, r.a17), r.a9 FROM (SELECT "C 1" a9, c2 a10, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17 FROM "S 1"."T 1") r) r (a1, a2) ON ((l.a3 = r.a2))
+ (9 rows)
+ 
+ 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;
+   ctid  |                                             t1                                             |                                             t2                                             | c1  
+ --------+--------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------+-----
+  (1,4)  | (101,1,00101,"Fri Jan 02 00:00:00 1970 PST","Fri Jan 02 00:00:00 1970",1,"1         ",foo) | (101,1,00101,"Fri Jan 02 00:00:00 1970 PST","Fri Jan 02 00:00:00 1970",1,"1         ",foo) | 101
+  (1,5)  | (102,2,00102,"Sat Jan 03 00:00:00 1970 PST","Sat Jan 03 00:00:00 1970",2,"2         ",foo) | (102,2,00102,"Sat Jan 03 00:00:00 1970 PST","Sat Jan 03 00:00:00 1970",2,"2         ",foo) | 102
+  (1,6)  | (103,3,00103,"Sun Jan 04 00:00:00 1970 PST","Sun Jan 04 00:00:00 1970",3,"3         ",foo) | (103,3,00103,"Sun Jan 04 00:00:00 1970 PST","Sun Jan 04 00:00:00 1970",3,"3         ",foo) | 103
+  (1,7)  | (104,4,00104,"Mon Jan 05 00:00:00 1970 PST","Mon Jan 05 00:00:00 1970",4,"4         ",foo) | (104,4,00104,"Mon Jan 05 00:00:00 1970 PST","Mon Jan 05 00:00:00 1970",4,"4         ",foo) | 104
+  (1,8)  | (105,5,00105,"Tue Jan 06 00:00:00 1970 PST","Tue Jan 06 00:00:00 1970",5,"5         ",foo) | (105,5,00105,"Tue Jan 06 00:00:00 1970 PST","Tue Jan 06 00:00:00 1970",5,"5         ",foo) | 105
+  (1,9)  | (106,6,00106,"Wed Jan 07 00:00:00 1970 PST","Wed Jan 07 00:00:00 1970",6,"6         ",foo) | (106,6,00106,"Wed Jan 07 00:00:00 1970 PST","Wed Jan 07 00:00:00 1970",6,"6         ",foo) | 106
+  (1,10) | (107,7,00107,"Thu Jan 08 00:00:00 1970 PST","Thu Jan 08 00:00:00 1970",7,"7         ",foo) | (107,7,00107,"Thu Jan 08 00:00:00 1970 PST","Thu Jan 08 00:00:00 1970",7,"7         ",foo) | 107
+  (1,11) | (108,8,00108,"Fri Jan 09 00:00:00 1970 PST","Fri Jan 09 00:00:00 1970",8,"8         ",foo) | (108,8,00108,"Fri Jan 09 00:00:00 1970 PST","Fri Jan 09 00:00:00 1970",8,"8         ",foo) | 108
+  (1,12) | (109,9,00109,"Sat Jan 10 00:00:00 1970 PST","Sat Jan 10 00:00:00 1970",9,"9         ",foo) | (109,9,00109,"Sat Jan 10 00:00:00 1970 PST","Sat Jan 10 00:00:00 1970",9,"9         ",foo) | 109
+  (1,13) | (110,0,00110,"Sun Jan 11 00:00:00 1970 PST","Sun Jan 11 00:00:00 1970",0,"0         ",foo) | (110,0,00110,"Sun Jan 11 00:00:00 1970 PST","Sun Jan 11 00:00:00 1970",0,"0         ",foo) | 110
+ (10 rows)
+ 
+ -- partially unsafe to push down, not pushed down
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON t2.c1 = t2.c1 JOIN ft4 t3 ON t2.c1 = t3.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10;
+                                                                                                             QUERY PLAN                                                                                                             
+ -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: t1.c1
+    ->  Nested Loop
+          Output: t1.c1
+          ->  Foreign Scan on public.ft1 t1
+                Output: t1.c1
+                Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC
+          ->  Materialize
+                ->  Foreign Scan
+                      Relations: (public.ft2 t2) INNER JOIN (public.ft4 t3)
+                      Remote SQL: SELECT NULL FROM (SELECT l.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1" WHERE (("C 1" = "C 1"))) l) l (a1) INNER JOIN (SELECT r.a9 FROM (SELECT c1 a9 FROM "S 1"."T 3") r) r (a1) ON ((l.a1 = r.a1))
+ (11 rows)
+ 
+ SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON t2.c1 = t2.c1 JOIN ft4 t3 ON t2.c1 = t3.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10;
+  c1 
+ ----
+   1
+   1
+   1
+   1
+   1
+   1
+   1
+   1
+   1
+   1
+ (10 rows)
+ 
+ -- SEMI JOIN, not pushed down
+ EXPLAIN (COSTS false, VERBOSE)
+ 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
+          Output: t1.c1
+          Merge Cond: (t1.c1 = t2.c1)
+          ->  Foreign Scan on public.ft1 t1
+                Output: t1.c1
+                Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC
+          ->  Materialize
+                Output: t2.c1
+                ->  Foreign Scan on public.ft2 t2
+                      Output: t2.c1
+                      Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC
+ (13 rows)
+ 
+ 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;
+  c1  
+ -----
+  101
+  102
+  103
+  104
+  105
+  106
+  107
+  108
+  109
+  110
+ (10 rows)
+ 
+ -- ANTI JOIN, not pushed down
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
+                                  QUERY PLAN                                 
+ ----------------------------------------------------------------------------
+  Limit
+    Output: t1.c1
+    ->  Merge Anti Join
+          Output: t1.c1
+          Merge Cond: (t1.c1 = t2.c2)
+          ->  Foreign Scan on public.ft1 t1
+                Output: t1.c1
+                Remote SQL: SELECT "C 1" FROM "S 1"."T 1" ORDER BY "C 1" ASC
+          ->  Sort
+                Output: t2.c2
+                Sort Key: t2.c2
+                ->  Foreign Scan on public.ft2 t2
+                      Output: t2.c2
+                      Remote SQL: SELECT c2 FROM "S 1"."T 1"
+ (14 rows)
+ 
+ SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
+  c1  
+ -----
+  110
+  111
+  112
+  113
+  114
+  115
+  116
+  117
+  118
+  119
+ (10 rows)
+ 
+ -- CROSS JOIN, not pushed down
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+                              QUERY PLAN                              
+ ---------------------------------------------------------------------
+  Limit
+    Output: t1.c1, t2.c1
+    ->  Sort
+          Output: t1.c1, t2.c1
+          Sort Key: t1.c1, t2.c1
+          ->  Nested Loop
+                Output: t1.c1, t2.c1
+                ->  Foreign Scan on public.ft1 t1
+                      Output: t1.c1
+                      Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+                ->  Materialize
+                      Output: t2.c1
+                      ->  Foreign Scan on public.ft2 t2
+                            Output: t2.c1
+                            Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+ (15 rows)
+ 
+ SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+  c1 | c1  
+ ----+-----
+   1 | 101
+   1 | 102
+   1 | 103
+   1 | 104
+   1 | 105
+   1 | 106
+   1 | 107
+   1 | 108
+   1 | 109
+   1 | 110
+ (10 rows)
+ 
+ -- different server
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+                                  QUERY PLAN                                 
+ ----------------------------------------------------------------------------
+  Limit
+    Output: t1.c1, t2.c1
+    ->  Merge Join
+          Output: t1.c1, t2.c1
+          Merge Cond: (t1.c1 = t2.c1)
+          ->  Foreign Scan on public.ft5 t1
+                Output: t1.c1, t1.c2, t1.c3
+                Remote SQL: SELECT c1 FROM "S 1"."T 4" ORDER BY c1 ASC
+          ->  Materialize
+                Output: t2.c1, t2.c2, t2.c3
+                ->  Foreign Scan on public.ft6 t2
+                      Output: t2.c1, t2.c2, t2.c3
+                      Remote SQL: SELECT c1 FROM "S 1"."T 4" ORDER BY c1 ASC
+ (13 rows)
+ 
+ SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+  c1 | c1 
+ ----+----
+ (0 rows)
+ 
+ -- different effective user for permission check
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN v_ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+                                  QUERY PLAN                                 
+ ----------------------------------------------------------------------------
+  Limit
+    Output: t1.c1, ft5.c1
+    ->  Merge Join
+          Output: t1.c1, ft5.c1
+          Merge Cond: (t1.c1 = ft5.c1)
+          ->  Foreign Scan on public.ft5 t1
+                Output: t1.c1, t1.c2, t1.c3
+                Remote SQL: SELECT c1 FROM "S 1"."T 4" ORDER BY c1 ASC
+          ->  Materialize
+                Output: ft5.c1, ft5.c2, ft5.c3
+                ->  Foreign Scan on public.ft5
+                      Output: ft5.c1, ft5.c2, ft5.c3
+                      Remote SQL: SELECT c1 FROM "S 1"."T 4" ORDER BY c1 ASC
+ (13 rows)
+ 
+ SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN v_ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+  c1 | c1 
+ ----+----
+ (0 rows)
+ 
+ -- unsafe join conditions
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c8 = t2.c8) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+                                          QUERY PLAN                                         
+ --------------------------------------------------------------------------------------------
+  Limit
+    Output: t1.c1, t2.c1, t1.c3
+    ->  Nested Loop
+          Output: t1.c1, t2.c1, t1.c3
+          Join Filter: (t1.c8 = t2.c8)
+          ->  Foreign Scan on public.ft1 t1
+                Output: t1.c1, t1.c3, t1.c8
+                Remote SQL: SELECT "C 1", c3, c8 FROM "S 1"."T 1" ORDER BY c3 ASC, "C 1" ASC
+          ->  Materialize
+                Output: t2.c1, t2.c8
+                ->  Foreign Scan on public.ft2 t2
+                      Output: t2.c1, t2.c8
+                      Remote SQL: SELECT "C 1", c8 FROM "S 1"."T 1"
+ (13 rows)
+ 
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c8 = t2.c8) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+  c1 | c1  
+ ----+-----
+   1 | 101
+   1 | 102
+   1 | 103
+   1 | 104
+   1 | 105
+   1 | 106
+   1 | 107
+   1 | 108
+   1 | 109
+   1 | 110
+ (10 rows)
+ 
+ -- local filter (unsafe conditions on one side)
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = 'foo' ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+                                  QUERY PLAN                                  
+ -----------------------------------------------------------------------------
+  Limit
+    Output: t1.c1, t2.c1, t1.c3
+    ->  Sort
+          Output: t1.c1, t2.c1, t1.c3
+          Sort Key: t1.c3, t1.c1
+          ->  Hash Join
+                Output: t1.c1, t2.c1, t1.c3
+                Hash Cond: (t2.c1 = t1.c1)
+                ->  Foreign Scan on public.ft2 t2
+                      Output: t2.c1
+                      Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
+                ->  Hash
+                      Output: t1.c1, t1.c3
+                      ->  Foreign Scan on public.ft1 t1
+                            Output: t1.c1, t1.c3
+                            Filter: (t1.c8 = 'foo'::user_enum)
+                            Remote SQL: SELECT "C 1", c3, c8 FROM "S 1"."T 1"
+ (17 rows)
+ 
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = 'foo' 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 (COSTS false, VERBOSE)
+ 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
+          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 l.a1, r.a1 FROM (SELECT l.a10 FROM (SELECT "C 1" a10 FROM "S 1"."T 1") l) l (a1) INNER JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1))
+                            ->  Foreign Scan
+                                  Output: t1_1.c1, t2_1.c1
+                                  Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
+                                  Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a10 FROM (SELECT "C 1" a10 FROM "S 1"."T 1") l) l (a1) INNER JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1))
+ (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 two foreign tables and two local tables
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON t1.c1 = t2.c1 JOIN "S 1"."T 1" t3 ON t1.c1 = t3."C 1" JOIN "S 1"."T 2" t4 ON t1.c1 = t4.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10;
+                                                                                                      QUERY PLAN                                                                                                      
+ ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+  Limit
+    Output: t1.c1, t2.c1
+    ->  Sort
+          Output: t1.c1, t2.c1
+          Sort Key: t1.c1
+          ->  Hash Join
+                Output: t1.c1, t2.c1
+                Hash Cond: (t1.c1 = t3."C 1")
+                ->  Foreign Scan
+                      Output: t1.c1, t2.c1
+                      Relations: (public.ft1 t1) LEFT JOIN (public.ft2 t2)
+                      Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a10 FROM (SELECT "C 1" a10 FROM "S 1"."T 1") l) l (a1) LEFT JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1))
+                ->  Hash
+                      Output: t3."C 1", t4.c1
+                      ->  Merge Join
+                            Output: t3."C 1", t4.c1
+                            Merge Cond: (t3."C 1" = t4.c1)
+                            ->  Index Only Scan using t1_pkey on "S 1"."T 1" t3
+                                  Output: t3."C 1"
+                            ->  Sort
+                                  Output: t4.c1
+                                  Sort Key: t4.c1
+                                  ->  Seq Scan on "S 1"."T 2" t4
+                                        Output: t4.c1
+ (24 rows)
+ 
+ SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON t1.c1 = t2.c1 JOIN "S 1"."T 1" t3 ON t1.c1 = t3."C 1" JOIN "S 1"."T 2" t4 ON t1.c1 = t4.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10;
+  c1 | c1 
+ ----+----
+  11 | 11
+  12 | 12
+  13 | 13
+  14 | 14
+  15 | 15
+  16 | 16
+  17 | 17
+  18 | 18
+  19 | 19
+  20 | 20
+ (10 rows)
+ 
+ -- ===================================================================
  -- parameterized queries
  -- ===================================================================
  -- simple join
***************
*** 1355,1376 **** 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;
!                                                                             QUERY PLAN                                                                             
! -------------------------------------------------------------------------------------------------------------------------------------------------------------------
   Update on public.ft2
     Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3, c7 = $4 WHERE ctid = $1
!    ->  Hash Join
           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.*
!          Hash Cond: (ft2.c2 = ft1.c1)
!          ->  Foreign Scan on public.ft2
!                Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid
!                Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c8, ctid FROM "S 1"."T 1" FOR UPDATE
!          ->  Hash
!                Output: ft1.*, ft1.c1
!                ->  Foreign Scan on public.ft1
!                      Output: ft1.*, ft1.c1
!                      Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))
! (13 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
--- 2027,2041 ----
  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;
!                                                                                                                                                                                                                                                                        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 l.a1, l.a2, l.a3, l.a4, l.a5, l.a6, l.a7, l.a8, r.a1 FROM (SELECT l.a9, l.a10, l.a12, l.a13, l.a14, l.a15, l.a17, l.a7 FROM (SELECT "C 1" a9, c2 a10, c3 a12, c4 a13, c5 a14, c6 a15, c8 a17, ctid a7 FROM "S 1"."T 1" FOR UPDATE) l) l (a1, a2, a3, a4, a5, a6, a7, a8) INNER JOIN (SELECT ROW(r.a10, r.a11, r.a12, r.a13, r.a14, r.a15, r.a16, r.a17), r.a10 FROM (SELECT "C 1" a10, c2 a11, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))) r) r (a1, a2) ON ((l.a2 = r.a2))
! (6 rows)
  
  UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT
    FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9;
***************
*** 1496,1517 **** 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;
!                                                       QUERY PLAN                                                      
! ----------------------------------------------------------------------------------------------------------------------
   Delete on public.ft2
     Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1
!    ->  Hash Join
           Output: ft2.ctid, ft1.*
!          Hash Cond: (ft2.c2 = ft1.c1)
!          ->  Foreign Scan on public.ft2
!                Output: ft2.ctid, ft2.c2
!                Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" FOR UPDATE
!          ->  Hash
!                Output: ft1.*, ft1.c1
!                ->  Foreign Scan on public.ft1
!                      Output: ft1.*, ft1.c1
!                      Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))
! (13 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
--- 2161,2175 ----
  
  EXPLAIN (verbose, costs off)
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
!                                                                                                                                                                                         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 l.a1, r.a1 FROM (SELECT l.a7, l.a10 FROM (SELECT c2 a10, ctid a7 FROM "S 1"."T 1" FOR UPDATE) l) l (a1, a2) INNER JOIN (SELECT ROW(r.a10, r.a11, r.a12, r.a13, r.a14, r.a15, r.a16, r.a17), r.a10 FROM (SELECT "C 1" a10, c2 a11, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))) r) r (a1, a2) ON ((l.a2 = r.a2))
! (6 rows)
  
  DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2;
  SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1;
***************
*** 3786,3788 **** QUERY:  CREATE FOREIGN TABLE t5 (
--- 4444,4449 ----
  OPTIONS (schema_name 'import_source', table_name 't5');
  CONTEXT:  importing foreign table "t5"
  ROLLBACK;
+ -- Cleanup
+ DROP OWNED BY view_owner;
+ DROP USER view_owner;
*** a/contrib/postgres_fdw/postgres_fdw.c
--- b/contrib/postgres_fdw/postgres_fdw.c
***************
*** 28,34 ****
  #include "optimizer/pathnode.h"
  #include "optimizer/paths.h"
  #include "optimizer/planmain.h"
- #include "optimizer/prep.h"
  #include "optimizer/restrictinfo.h"
  #include "optimizer/var.h"
  #include "parser/parsetree.h"
--- 28,33 ----
***************
*** 68,74 **** enum FdwScanPrivateIndex
  	/* SQL statement to execute remotely (as a String node) */
  	FdwScanPrivateSelectSql,
  	/* Integer list of attribute numbers retrieved by the SELECT */
! 	FdwScanPrivateRetrievedAttrs
  };
  
  /*
--- 67,79 ----
  	/* SQL statement to execute remotely (as a String node) */
  	FdwScanPrivateSelectSql,
  	/* Integer list of attribute numbers retrieved by the SELECT */
! 	FdwScanPrivateRetrievedAttrs,
! 	/* Integer value of server for the scan */
! 	FdwScanPrivateServerOid,
! 	/* Integer value of user mapping for the scan */
! 	FdwScanPrivateUserMappingOid,
! 	/* Names of relation scanned, added when the scan is join */
! 	FdwScanPrivateRelations,
  };
  
  /*
***************
*** 98,104 **** enum FdwModifyPrivateIndex
   */
  typedef struct PgFdwScanState
  {
! 	Relation	rel;			/* relcache entry for the foreign table */
  	AttInMetadata *attinmeta;	/* attribute datatype conversion metadata */
  
  	/* extracted fdw_private data */
--- 103,110 ----
   */
  typedef struct PgFdwScanState
  {
! 	const char *relname;		/* name of relation being scanned */
! 	TupleDesc	tupdesc;		/* tuple descriptor of the scan */
  	AttInMetadata *attinmeta;	/* attribute datatype conversion metadata */
  
  	/* extracted fdw_private data */
***************
*** 164,169 **** typedef struct PgFdwAnalyzeState
--- 170,177 ----
  	AttInMetadata *attinmeta;	/* attribute datatype conversion metadata */
  	List	   *retrieved_attrs;	/* attr numbers retrieved by query */
  
+ 	char	   *query;			/* text of SELECT command */
+ 
  	/* collected sample rows */
  	HeapTuple  *rows;			/* array of size targrows */
  	int			targrows;		/* target # of sample rows */
***************
*** 184,190 **** typedef struct PgFdwAnalyzeState
   */
  typedef struct ConversionLocation
  {
! 	Relation	rel;			/* foreign table's relcache entry */
  	AttrNumber	cur_attno;		/* attribute number being processed, or 0 */
  } ConversionLocation;
  
--- 192,201 ----
   */
  typedef struct ConversionLocation
  {
! 	const char *relname;		/* name of relation being processed, or NULL for
! 								   a foreign join */
! 	const char *query;			/* query being processed */
! 	TupleDesc	tupdesc;		/* tuple descriptor for attribute names */
  	AttrNumber	cur_attno;		/* attribute number being processed, or 0 */
  } ConversionLocation;
  
***************
*** 247,252 **** static TupleTableSlot *postgresExecForeignDelete(EState *estate,
--- 258,265 ----
  static void postgresEndForeignModify(EState *estate,
  						 ResultRelInfo *resultRelInfo);
  static int	postgresIsForeignRelUpdatable(Relation rel);
+ static bool postgresRecheckForeignScan(ForeignScanState *node,
+ 									   TupleTableSlot *slot);
  static void postgresExplainForeignScan(ForeignScanState *node,
  						   ExplainState *es);
  static void postgresExplainForeignModify(ModifyTableState *mtstate,
***************
*** 259,264 **** static bool postgresAnalyzeForeignTable(Relation relation,
--- 272,283 ----
  							BlockNumber *totalpages);
  static List *postgresImportForeignSchema(ImportForeignSchemaStmt *stmt,
  							Oid serverOid);
+ static void postgresGetForeignJoinPaths(PlannerInfo *root,
+ 										RelOptInfo *joinrel,
+ 										RelOptInfo *outerrel,
+ 										RelOptInfo *innerrel,
+ 										JoinType jointype,
+ 										JoinPathExtraData *extra);
  
  /*
   * Helper functions
***************
*** 295,306 **** static void analyze_row_processor(PGresult *res, int row,
  					  PgFdwAnalyzeState *astate);
  static HeapTuple make_tuple_from_result_row(PGresult *res,
  						   int row,
! 						   Relation rel,
  						   AttInMetadata *attinmeta,
  						   List *retrieved_attrs,
  						   MemoryContext temp_context);
  static void conversion_error_callback(void *arg);
  
  
  /*
   * Foreign-data wrapper handler function: return a struct with pointers
--- 314,354 ----
  					  PgFdwAnalyzeState *astate);
  static HeapTuple make_tuple_from_result_row(PGresult *res,
  						   int row,
! 						   const char *relname,
! 						   const char *query,
! 						   TupleDesc tupdesc,
  						   AttInMetadata *attinmeta,
  						   List *retrieved_attrs,
  						   MemoryContext temp_context);
  static void conversion_error_callback(void *arg);
+ static Path *get_unsorted_unparameterized_path(List *paths);
+ 
+ /*
+  * Describe Bitmapset as comma-separated integer list.
+  * For debug purpose.
+  * XXX Can this become a member of bitmapset.c?
+  */
+ static char *
+ bms_to_str(Bitmapset *bmp)
+ {
+ 	StringInfoData buf;
+ 	bool		first = true;
+ 	int			x;
  
+ 	initStringInfo(&buf);
+ 
+ 	x = -1;
+ 	while ((x = bms_next_member(bmp, x)) >= 0)
+ 	{
+ 		if (!first)
+ 			appendStringInfoString(&buf, ", ");
+ 		appendStringInfo(&buf, "%d", x);
+ 
+ 		first = false;
+ 	}
+ 
+ 	return buf.data;
+ }
  
  /*
   * Foreign-data wrapper handler function: return a struct with pointers
***************
*** 330,335 **** postgres_fdw_handler(PG_FUNCTION_ARGS)
--- 378,386 ----
  	routine->EndForeignModify = postgresEndForeignModify;
  	routine->IsForeignRelUpdatable = postgresIsForeignRelUpdatable;
  
+ 	/* Functions for SELECT FOR UPDATE/SHARE row locking */
+ 	routine->RecheckForeignScan = postgresRecheckForeignScan;
+ 
  	/* Support functions for EXPLAIN */
  	routine->ExplainForeignScan = postgresExplainForeignScan;
  	routine->ExplainForeignModify = postgresExplainForeignModify;
***************
*** 340,345 **** postgres_fdw_handler(PG_FUNCTION_ARGS)
--- 391,399 ----
  	/* Support functions for IMPORT FOREIGN SCHEMA */
  	routine->ImportForeignSchema = postgresImportForeignSchema;
  
+ 	/* Support functions for join push-down */
+ 	routine->GetForeignJoinPaths = postgresGetForeignJoinPaths;
+ 
  	PG_RETURN_POINTER(routine);
  }
  
***************
*** 365,373 **** postgresGetForeignRelSize(PlannerInfo *root,
--- 419,431 ----
  	fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo));
  	baserel->fdw_private = (void *) fpinfo;
  
+ 	/* This scan can be pushed down to the remote. */
+ 	fpinfo->pushdown_safe = true;
+ 
  	/* Look up foreign-table catalog info. */
  	fpinfo->table = GetForeignTable(foreigntableid);
  	fpinfo->server = GetForeignServer(fpinfo->table->serverid);
+ 	fpinfo->umid = baserel->umid;
  
  	/*
  	 * Extract user-settable option values.  Note that per-table setting of
***************
*** 404,425 **** postgresGetForeignRelSize(PlannerInfo *root,
  	}
  
  	/*
- 	 * If the table or the server is configured to use remote estimates,
- 	 * identify which user to do remote access as during planning.  This
- 	 * should match what ExecCheckRTEPerms() does.  If we fail due to lack of
- 	 * permissions, the query would have failed at runtime anyway.
- 	 */
- 	if (fpinfo->use_remote_estimate)
- 	{
- 		RangeTblEntry *rte = planner_rt_fetch(baserel->relid, root);
- 		Oid			userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
- 
- 		fpinfo->user = GetUserMapping(userid, fpinfo->server->serverid);
- 	}
- 	else
- 		fpinfo->user = NULL;
- 
- 	/*
  	 * Identify which baserestrictinfo clauses can be sent to the remote
  	 * server and which can't.
  	 */
--- 462,467 ----
***************
*** 788,793 **** postgresGetForeignPlan(PlannerInfo *root,
--- 830,837 ----
  	List	   *retrieved_attrs;
  	StringInfoData sql;
  	ListCell   *lc;
+ 	List	   *fdw_scan_tlist = NIL;
+ 	StringInfoData relations;
  
  	/*
  	 * Separate the scan_clauses into those that can be executed remotely and
***************
*** 834,910 **** postgresGetForeignPlan(PlannerInfo *root,
  			local_exprs = lappend(local_exprs, rinfo->clause);
  	}
  
  	/*
  	 * Build the query string to be sent for execution, and identify
  	 * expressions to be sent as parameters.
  	 */
  	initStringInfo(&sql);
! 	deparseSelectSql(&sql, root, baserel, fpinfo->attrs_used,
! 					 &retrieved_attrs);
! 	if (remote_conds)
! 		appendWhereClause(&sql, root, baserel, remote_conds,
! 						  true, &params_list);
! 
! 	/* Add ORDER BY clause if we found any useful pathkeys */
! 	if (best_path->path.pathkeys)
! 		appendOrderByClause(&sql, root, baserel, best_path->path.pathkeys);
! 
! 	/*
! 	 * 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.
! 	 */
! 	if (baserel->relid == root->parse->resultRelation &&
! 		(root->parse->commandType == CMD_UPDATE ||
! 		 root->parse->commandType == CMD_DELETE))
! 	{
! 		/* Relation is UPDATE/DELETE target, so use FOR UPDATE */
! 		appendStringInfoString(&sql, " FOR UPDATE");
! 	}
! 	else
! 	{
! 		PlanRowMark *rc = get_plan_rowmark(root->rowMarks, baserel->relid);
! 
! 		if (rc)
! 		{
! 			/*
! 			 * Relation is specified as a FOR UPDATE/SHARE target, so handle
! 			 * that.  (But we could also see LCS_NONE, meaning this isn't a
! 			 * target relation after all.)
! 			 *
! 			 * For now, just ignore any [NO] KEY specification, since (a) it's
! 			 * not clear what that means for a remote table that we don't have
! 			 * complete information about, and (b) it wouldn't work anyway on
! 			 * older remote servers.  Likewise, we don't worry about NOWAIT.
! 			 */
! 			switch (rc->strength)
! 			{
! 				case LCS_NONE:
! 					/* No locking needed */
! 					break;
! 				case LCS_FORKEYSHARE:
! 				case LCS_FORSHARE:
! 					appendStringInfoString(&sql, " FOR SHARE");
! 					break;
! 				case LCS_FORNOKEYUPDATE:
! 				case LCS_FORUPDATE:
! 					appendStringInfoString(&sql, " FOR UPDATE");
! 					break;
! 			}
! 		}
! 	}
  
  	/*
  	 * Build the fdw_private list that will be available to the executor.
  	 * Items in the list must match enum FdwScanPrivateIndex, above.
  	 */
! 	fdw_private = list_make2(makeString(sql.data),
! 							 retrieved_attrs);
  
  	/*
  	 * Create the ForeignScan node from target list, filtering expressions,
--- 878,921 ----
  			local_exprs = lappend(local_exprs, rinfo->clause);
  	}
  
+ 	if (scan_relid == 0)
+ 	{
+ 		Assert(scan_clauses == NIL);
+ 		Assert(remote_conds == NIL);
+ 		Assert(remote_exprs == NIL);
+ 	}
+ 
  	/*
  	 * Build the query string to be sent for execution, and identify
  	 * expressions to be sent as parameters.
  	 */
  	initStringInfo(&sql);
! 	if (baserel->reloptkind == RELOPT_JOINREL)
! 		initStringInfo(&relations);
! 	deparseSelectSql(&sql, root, baserel, fpinfo->attrs_used, remote_conds,
! 					 best_path->path.pathkeys,
! 					 &params_list, &fdw_scan_tlist, &retrieved_attrs,
! 					 baserel->reloptkind == RELOPT_JOINREL ? &relations : NULL,
! 					 false);
  
  	/*
  	 * Build the fdw_private list that will be available to the executor.
  	 * Items in the list must match enum FdwScanPrivateIndex, above.
  	 */
! 	fdw_private = list_make4(makeString(sql.data),
! 							 retrieved_attrs,
! 							 makeInteger(fpinfo->server->serverid),
! 							 makeInteger(fpinfo->umid));
! 	if (baserel->reloptkind == RELOPT_JOINREL)
! 		fdw_private = lappend(fdw_private, makeString(relations.data));
! 
! 	/* Adjust outer_plan */
! 	if (scan_relid == 0)
! 	{
! 		Assert(outer_plan != NULL);
! 		outer_plan->targetlist = fdw_scan_tlist;
! 		outer_plan->qual = fpinfo->otherclauses;
! 	}
  
  	/*
  	 * Create the ForeignScan node from target list, filtering expressions,
***************
*** 919,925 **** postgresGetForeignPlan(PlannerInfo *root,
  							scan_relid,
  							params_list,
  							fdw_private,
! 							NIL,	/* no custom tlist */
  							remote_exprs,
  							outer_plan);
  }
--- 930,936 ----
  							scan_relid,
  							params_list,
  							fdw_private,
! 							fdw_scan_tlist,
  							remote_exprs,
  							outer_plan);
  }
***************
*** 934,942 **** postgresBeginForeignScan(ForeignScanState *node, int eflags)
  	ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
  	EState	   *estate = node->ss.ps.state;
  	PgFdwScanState *fsstate;
! 	RangeTblEntry *rte;
! 	Oid			userid;
! 	ForeignTable *table;
  	ForeignServer *server;
  	UserMapping *user;
  	int			numParams;
--- 945,952 ----
  	ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan;
  	EState	   *estate = node->ss.ps.state;
  	PgFdwScanState *fsstate;
! 	Oid			serverid;
! 	Oid			umid;
  	ForeignServer *server;
  	UserMapping *user;
  	int			numParams;
***************
*** 956,977 **** postgresBeginForeignScan(ForeignScanState *node, int eflags)
  	node->fdw_state = (void *) fsstate;
  
  	/*
- 	 * Identify which user to do the remote access as.  This should match what
- 	 * ExecCheckRTEPerms() does.
- 	 */
- 	rte = rt_fetch(fsplan->scan.scanrelid, estate->es_range_table);
- 	userid = rte->checkAsUser ? rte->checkAsUser : GetUserId();
- 
- 	/* Get info about foreign table. */
- 	fsstate->rel = node->ss.ss_currentRelation;
- 	table = GetForeignTable(RelationGetRelid(fsstate->rel));
- 	server = GetForeignServer(table->serverid);
- 	user = GetUserMapping(userid, server->serverid);
- 
- 	/*
  	 * Get connection to the foreign server.  Connection manager will
  	 * establish new connection if necessary.
  	 */
  	fsstate->conn = GetConnection(server, user, false);
  
  	/* Assign a unique ID for my cursor */
--- 966,978 ----
  	node->fdw_state = (void *) fsstate;
  
  	/*
  	 * Get connection to the foreign server.  Connection manager will
  	 * establish new connection if necessary.
  	 */
+ 	serverid = intVal(list_nth(fsplan->fdw_private, FdwScanPrivateServerOid));
+ 	umid = intVal(list_nth(fsplan->fdw_private, FdwScanPrivateUserMappingOid));
+ 	server = GetForeignServer(serverid);
+ 	user = GetUserMappingById(umid);
  	fsstate->conn = GetConnection(server, user, false);
  
  	/* Assign a unique ID for my cursor */
***************
*** 996,1003 **** postgresBeginForeignScan(ForeignScanState *node, int eflags)
  											  ALLOCSET_SMALL_INITSIZE,
  											  ALLOCSET_SMALL_MAXSIZE);
  
! 	/* Get info we'll need for input data conversion. */
! 	fsstate->attinmeta = TupleDescGetAttInMetadata(RelationGetDescr(fsstate->rel));
  
  	/* Prepare for output conversion of parameters used in remote query. */
  	numParams = list_length(fsplan->fdw_exprs);
--- 997,1014 ----
  											  ALLOCSET_SMALL_INITSIZE,
  											  ALLOCSET_SMALL_MAXSIZE);
  
! 	/* Get info we'll need for input data conversion and error report. */
! 	if (fsplan->scan.scanrelid > 0)
! 	{
! 		fsstate->relname = RelationGetRelationName(node->ss.ss_currentRelation);
! 		fsstate->tupdesc = RelationGetDescr(node->ss.ss_currentRelation);
! 	}
! 	else
! 	{
! 		fsstate->relname = NULL;
! 		fsstate->tupdesc = node->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
! 	}
! 	fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc);
  
  	/* Prepare for output conversion of parameters used in remote query. */
  	numParams = list_length(fsplan->fdw_exprs);
***************
*** 1718,1723 **** postgresIsForeignRelUpdatable(Relation rel)
--- 1729,1761 ----
  }
  
  /*
+  * postgresRecheckForeignScan
+  *		Execute a local join execution plan for a foreign join
+  */
+ static bool
+ postgresRecheckForeignScan(ForeignScanState *node, TupleTableSlot *slot)
+ {
+ 	Index		scanrelid = ((Scan *) node->ss.ps.plan)->scanrelid;
+ 	PlanState  *outerPlan = outerPlanState(node);
+ 	TupleTableSlot *result;
+ 
+ 	if (scanrelid > 0)
+ 		return true;
+ 
+ 	Assert(outerPlan != NULL);
+ 
+ 	/* Execute a local join execution plan */
+ 	result = ExecProcNode(outerPlan);
+ 	if (TupIsNull(result))
+ 		return false;
+ 
+ 	/* Store result in the given slot */
+ 	ExecCopySlot(slot, result);
+ 
+ 	return true;
+ }
+ 
+ /*
   * postgresExplainForeignScan
   *		Produce extra output for EXPLAIN of a ForeignScan on a foreign table
   */
***************
*** 1726,1735 **** postgresExplainForeignScan(ForeignScanState *node, ExplainState *es)
  {
  	List	   *fdw_private;
  	char	   *sql;
  
  	if (es->verbose)
  	{
- 		fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
  		sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql));
  		ExplainPropertyText("Remote SQL", sql, es);
  	}
--- 1764,1788 ----
  {
  	List	   *fdw_private;
  	char	   *sql;
+ 	char	   *relations;
+ 
+ 	fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private;
+ 
+ 	/*
+ 	 * Add names of relation handled by the foreign scan when the scan is a
+ 	 * join
+ 	 */
+ 	if (list_length(fdw_private) > FdwScanPrivateRelations)
+ 	{
+ 		relations = strVal(list_nth(fdw_private, FdwScanPrivateRelations));
+ 		ExplainPropertyText("Relations", relations, es);
+ 	}
  
+ 	/*
+ 	 * Add remote query, when VERBOSE option is specified.
+ 	 */
  	if (es->verbose)
  	{
  		sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql));
  		ExplainPropertyText("Remote SQL", sql, es);
  	}
***************
*** 1789,1798 **** estimate_path_cost_size(PlannerInfo *root,
--- 1842,1853 ----
  	 */
  	if (fpinfo->use_remote_estimate)
  	{
+ 		List	   *remote_conds;
  		List	   *remote_join_conds;
  		List	   *local_join_conds;
  		StringInfoData sql;
  		List	   *retrieved_attrs;
+ 		UserMapping *user;
  		PGconn	   *conn;
  		Selectivity local_sel;
  		QualCost	local_cost;
***************
*** 1804,1830 **** estimate_path_cost_size(PlannerInfo *root,
  		classifyConditions(root, baserel, join_conds,
  						   &remote_join_conds, &local_join_conds);
  
  		/*
  		 * Construct EXPLAIN query including the desired SELECT, FROM, and
  		 * WHERE clauses.  Params and other-relation Vars are replaced by
  		 * dummy values.
  		 */
  		initStringInfo(&sql);
  		appendStringInfoString(&sql, "EXPLAIN ");
! 		deparseSelectSql(&sql, root, baserel, fpinfo->attrs_used,
! 						 &retrieved_attrs);
! 		if (fpinfo->remote_conds)
! 			appendWhereClause(&sql, root, baserel, fpinfo->remote_conds,
! 							  true, NULL);
! 		if (remote_join_conds)
! 			appendWhereClause(&sql, root, baserel, remote_join_conds,
! 							  (fpinfo->remote_conds == NIL), NULL);
! 
! 		if (pathkeys)
! 			appendOrderByClause(&sql, root, baserel, pathkeys);
  
  		/* Get the remote estimate */
! 		conn = GetConnection(fpinfo->server, fpinfo->user, false);
  		get_remote_estimate(sql.data, conn, &rows, &width,
  							&startup_cost, &total_cost);
  		ReleaseConnection(conn);
--- 1859,1882 ----
  		classifyConditions(root, baserel, join_conds,
  						   &remote_join_conds, &local_join_conds);
  
+ 		remote_conds = copyObject(fpinfo->remote_conds);
+ 		remote_conds = list_concat(remote_conds, remote_join_conds);
+ 
  		/*
  		 * Construct EXPLAIN query including the desired SELECT, FROM, and
  		 * WHERE clauses.  Params and other-relation Vars are replaced by
  		 * dummy values.
+ 		 * Here we waste params_list and fdw_scan_tlist because they are
+ 		 * unnecessary for EXPLAIN.
  		 */
  		initStringInfo(&sql);
  		appendStringInfoString(&sql, "EXPLAIN ");
! 		deparseSelectSql(&sql, root, baserel, fpinfo->attrs_used, remote_conds,
! 						 pathkeys, NULL, NULL, &retrieved_attrs, NULL, false);
  
  		/* Get the remote estimate */
! 		user = GetUserMappingById(fpinfo->umid);
! 		conn = GetConnection(fpinfo->server, user, false);
  		get_remote_estimate(sql.data, conn, &rows, &width,
  							&startup_cost, &total_cost);
  		ReleaseConnection(conn);
***************
*** 2136,2142 **** fetch_more_data(ForeignScanState *node)
  		{
  			fsstate->tuples[i] =
  				make_tuple_from_result_row(res, i,
! 										   fsstate->rel,
  										   fsstate->attinmeta,
  										   fsstate->retrieved_attrs,
  										   fsstate->temp_cxt);
--- 2188,2196 ----
  		{
  			fsstate->tuples[i] =
  				make_tuple_from_result_row(res, i,
! 										   fsstate->relname,
! 										   fsstate->query,
! 										   fsstate->tupdesc,
  										   fsstate->attinmeta,
  										   fsstate->retrieved_attrs,
  										   fsstate->temp_cxt);
***************
*** 2354,2360 **** store_returning_result(PgFdwModifyState *fmstate,
  		HeapTuple	newtup;
  
  		newtup = make_tuple_from_result_row(res, 0,
! 											fmstate->rel,
  											fmstate->attinmeta,
  											fmstate->retrieved_attrs,
  											fmstate->temp_cxt);
--- 2408,2416 ----
  		HeapTuple	newtup;
  
  		newtup = make_tuple_from_result_row(res, 0,
! 										RelationGetRelationName(fmstate->rel),
! 											fmstate->query,
! 											RelationGetDescr(fmstate->rel),
  											fmstate->attinmeta,
  											fmstate->retrieved_attrs,
  											fmstate->temp_cxt);
***************
*** 2504,2509 **** postgresAcquireSampleRowsFunc(Relation relation, int elevel,
--- 2560,2566 ----
  	initStringInfo(&sql);
  	appendStringInfo(&sql, "DECLARE c%u CURSOR FOR ", cursor_number);
  	deparseAnalyzeSql(&sql, relation, &astate.retrieved_attrs);
+ 	astate.query = sql.data;
  
  	/* In what follows, do not risk leaking any PGresults. */
  	PG_TRY();
***************
*** 2645,2651 **** analyze_row_processor(PGresult *res, int row, PgFdwAnalyzeState *astate)
  		oldcontext = MemoryContextSwitchTo(astate->anl_cxt);
  
  		astate->rows[pos] = make_tuple_from_result_row(res, row,
! 													   astate->rel,
  													   astate->attinmeta,
  													 astate->retrieved_attrs,
  													   astate->temp_cxt);
--- 2702,2710 ----
  		oldcontext = MemoryContextSwitchTo(astate->anl_cxt);
  
  		astate->rows[pos] = make_tuple_from_result_row(res, row,
! 										   RelationGetRelationName(astate->rel),
! 													   astate->query,
! 											   RelationGetDescr(astate->rel),
  													   astate->attinmeta,
  													 astate->retrieved_attrs,
  													   astate->temp_cxt);
***************
*** 2919,2924 **** postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
--- 2978,3308 ----
  }
  
  /*
+  * Construct PgFdwRelationInfo from two join sources
+  */
+ static void
+ merge_fpinfo(RelOptInfo *outerrel,
+ 			 RelOptInfo *innerrel,
+ 			 PgFdwRelationInfo *fpinfo,
+ 			 JoinType jointype,
+ 			 double rows,
+ 			 int width)
+ {
+ 	PgFdwRelationInfo *fpinfo_o;
+ 	PgFdwRelationInfo *fpinfo_i;
+ 
+ 	fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ 	fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+ 
+ 	/* Mark that this join can be pushed down safely */
+ 	fpinfo->pushdown_safe = true;
+ 
+ 	/* Join relation must have conditions come from sources */
+ 	fpinfo->remote_conds = list_concat(copyObject(fpinfo_o->remote_conds),
+ 									   copyObject(fpinfo_i->remote_conds));
+ 	fpinfo->local_conds = list_concat(copyObject(fpinfo_o->local_conds),
+ 									  copyObject(fpinfo_i->local_conds));
+ 
+ 	/* Only for simple foreign table scan */
+ 	fpinfo->attrs_used = NULL;
+ 
+ 	/* rows and width will be set later */
+ 	fpinfo->rows = rows;
+ 	fpinfo->width = width;
+ 
+ 	/* A join have local conditions for outer and inner, so sum up them. */
+ 	fpinfo->local_conds_cost.startup = fpinfo_o->local_conds_cost.startup +
+ 									   fpinfo_i->local_conds_cost.startup;
+ 	fpinfo->local_conds_cost.per_tuple = fpinfo_o->local_conds_cost.per_tuple +
+ 										 fpinfo_i->local_conds_cost.per_tuple;
+ 
+ 	/* Don't consider correlation between local filters. */
+ 	fpinfo->local_conds_sel = fpinfo_o->local_conds_sel *
+ 							  fpinfo_i->local_conds_sel;
+ 
+ 	fpinfo->use_remote_estimate = false;
+ 
+ 	/*
+ 	 * These two comes default or per-server setting, so outer and inner must
+ 	 * have same value.
+ 	 */
+ 	fpinfo->fdw_startup_cost = fpinfo_o->fdw_startup_cost;
+ 	fpinfo->fdw_tuple_cost = fpinfo_o->fdw_tuple_cost;
+ 
+ 	/*
+ 	 * TODO estimate more accurately
+ 	 */
+ 	fpinfo->startup_cost = fpinfo->fdw_startup_cost +
+ 						   fpinfo->local_conds_cost.startup;
+ 	fpinfo->total_cost = fpinfo->startup_cost +
+ 						 (fpinfo->fdw_tuple_cost +
+ 						  fpinfo->local_conds_cost.per_tuple +
+ 						  cpu_tuple_cost) * fpinfo->rows;
+ 
+ 	/* serverid and userid are respectively identical */
+ 	fpinfo->server = fpinfo_o->server;
+ 	fpinfo->umid = fpinfo_o->umid;
+ 
+ 	fpinfo->outerrel = outerrel;
+ 	fpinfo->innerrel = innerrel;
+ 	fpinfo->jointype = jointype;
+ 
+ 	/* This join can be pushed down safely */
+ 	fpinfo->pushdown_safe = true;
+ 
+ 	/* joinclauses and otherclauses will be set later */
+ }
+ 
+ /*
+  * Get a copy of unsorted, unparameterized path
+  */
+ static Path *
+ get_unsorted_unparameterized_path(List *paths)
+ {
+ 	ListCell   *l;
+ 
+ 	foreach(l, paths)
+ 	{
+ 		Path	   *path = (Path *) lfirst(l);
+ 
+ 		if (path->pathkeys == NIL && path->param_info == NULL)
+ 		{
+ 			switch (path->pathtype)
+ 			{
+ 				case T_MergeJoin:
+ 					{
+ 						MergePath  *retval = makeNode(MergePath);
+ 						*retval = *((MergePath *) path);
+ 						return (Path *) retval;
+ 					}
+ 				case T_HashJoin:
+ 					{
+ 						HashPath   *retval = makeNode(HashPath);
+ 						*retval = *((HashPath *) path);
+ 						return (Path *) retval;
+ 					}
+ 				case T_NestLoop:
+ 					{
+ 						NestPath   *retval = makeNode(NestPath);
+ 						*retval = *((NestPath *) path);
+ 						return (Path *) retval;
+ 					}
+ 				default:
+ 					elog(ERROR, "unrecognized node type: %d",
+ 						 (int) path->pathtype);
+ 					return NULL;
+ 			}
+ 		}
+ 	}
+ 	return NULL;
+ }
+ 
+ /*
+  * postgresGetForeignJoinPaths
+  *		Add possible ForeignPath to joinrel.
+  *
+  * Joins satisfy conditions below can be pushed down to the remote PostgreSQL
+  * server.
+  *
+  * 1) Join type is INNER or OUTER (one of LEFT/RIGHT/FULL)
+  * 2) Both outer and inner portions are safe to push-down
+  * 3) All foreign tables in the join belong to the same foreign server
+  * 4) All join conditions are safe to push down
+  * 5) No relation has local filter (this can be relaxed for INNER JOIN with
+  * no volatile function/operator, but as of now we want safer way)
+  */
+ static void
+ postgresGetForeignJoinPaths(PlannerInfo *root,
+ 							RelOptInfo *joinrel,
+ 							RelOptInfo *outerrel,
+ 							RelOptInfo *innerrel,
+ 							JoinType jointype,
+ 							JoinPathExtraData *extra)
+ {
+ 	PgFdwRelationInfo *fpinfo;
+ 	PgFdwRelationInfo *fpinfo_o;
+ 	PgFdwRelationInfo *fpinfo_i;
+ 	ForeignPath	   *joinpath;
+ 	double			rows;
+ 	Cost			startup_cost;
+ 	Cost			total_cost;
+ 	Path		   *subpath;
+ 
+ 	ListCell	   *lc;
+ 	List		   *joinclauses;
+ 	List		   *otherclauses;
+ 
+ 	/*
+ 	 * Skip if this join combination has been considered already.
+ 	 */
+ 	if (joinrel->fdw_private)
+ 	{
+ 		ereport(DEBUG3, (errmsg("combination already considered")));
+ 		return;
+ 	}
+ 
+ 	/*
+ 	 * Create unfinished PgFdwRelationInfo entry which is used to indicate that
+ 	 * the join relaiton is already considered but the join can't be pushed
+ 	 * down.  Once we know that this join can be pushed down, we fill the entry
+ 	 * and make it valid by calling merge_fpinfo.
+ 	 *
+ 	 * This unfinished entry prevents redandunt checks for a join combination
+ 	 * which is already known as unsafe to push down.
+ 	 */
+ 	fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo));
+ 	fpinfo->pushdown_safe = false;
+ 	joinrel->fdw_private = fpinfo;
+ 
+ 	/*
+ 	 * We support all outer joins in addition to inner join.  CROSS JOIN is
+ 	 * an INNER JOIN with no conditions internally, so will be checked later.
+ 	 */
+ 	if (jointype != JOIN_INNER && jointype != JOIN_LEFT &&
+ 		jointype != JOIN_RIGHT && jointype != JOIN_FULL)
+ 	{
+ 		ereport(DEBUG3, (errmsg("unsupported join type (SEMI, ANTI)")));
+ 		return;
+ 	}
+ 
+ 	/*
+ 	 * Having valid PgFdwRelationInfo marked as "safe to push down" in
+ 	 * RelOptInfo#fdw_private indicates that scanning against the relation can
+ 	 * be pushed down.  If either of them doesn't have PgFdwRelationInfo or it
+ 	 * is not marked as safe, give up to push down this join relation.
+ 	 */
+ 	fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private;
+ 	if (!fpinfo_o || !fpinfo_o->pushdown_safe)
+ 	{
+ 		ereport(DEBUG3, (errmsg("outer is not safe to push-down")));
+ 		return;
+ 	}
+ 	fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private;
+ 	if (!fpinfo_i || !fpinfo_i->pushdown_safe)
+ 	{
+ 		ereport(DEBUG3, (errmsg("inner is not safe to push-down")));
+ 		return;
+ 	}
+ 
+ 	/*
+ 	 * All relations in the join must belong to same server.  Having a valid
+ 	 * fdw_private means that all relations in the relations belong to the
+ 	 * server the fdw_private has, so what we should do is just compare
+ 	 * serverid of outer/inner relations.
+ 	 */
+ 	if (fpinfo_o->server->serverid != fpinfo_i->server->serverid)
+ 	{
+ 		ereport(DEBUG3, (errmsg("server unmatch")));
+ 		return;
+ 	}
+ 
+ 	/*
+ 	 * No source relation can have local conditions.  This can be relaxed
+ 	 * if the join is an inner join and local conditions don't contain
+ 	 * volatile function/operator, but as of now we leave it as future
+ 	 * enhancement.
+ 	 */
+ 	if (fpinfo_o->local_conds != NULL || fpinfo_i->local_conds != NULL)
+ 	{
+ 		ereport(DEBUG3, (errmsg("join with local filter")));
+ 		return;
+ 	}
+ 
+ 	/*
+ 	 * Separate restrictlist into two lists, join conditions and remote filters.
+ 	 */
+ 	joinclauses = extra->restrictlist;
+ 	if (IS_OUTER_JOIN(jointype))
+ 	{
+ 		extract_actual_join_clauses(joinclauses, &joinclauses, &otherclauses);
+ 	}
+ 	else
+ 	{
+ 		joinclauses = extract_actual_clauses(joinclauses, false);
+ 		otherclauses = NIL;
+ 	}
+ 
+ 	/*
+ 	 * Note that CROSS JOIN (cartesian product) is transformed to JOIN_INNER
+ 	 * with empty joinclauses.  Pushing down CROSS JOIN usually produces more
+ 	 * result than retrieving each tables separately, so we don't push down
+ 	 * such joins.
+ 	 */
+ 	if (jointype == JOIN_INNER && joinclauses == NIL)
+ 	{
+ 		ereport(DEBUG3, (errmsg("unsupported join type (CROSS)")));
+ 		return;
+ 	}
+ 
+ 	/*
+ 	 * Join condition must be safe to push down.
+ 	 */
+ 	foreach(lc, joinclauses)
+ 	{
+ 		Expr *expr = (Expr *) lfirst(lc);
+ 
+ 		if (!is_foreign_expr(root, joinrel, expr))
+ 		{
+ 			ereport(DEBUG3, (errmsg("join quals contains unsafe conditions")));
+ 			return;
+ 		}
+ 	}
+ 
+ 	/*
+ 	 * Other condition for the join must be safe to push down.
+ 	 */
+ 	foreach(lc, otherclauses)
+ 	{
+ 		Expr *expr = (Expr *) lfirst(lc);
+ 
+ 		if (!is_foreign_expr(root, joinrel, expr))
+ 		{
+ 			ereport(DEBUG3, (errmsg("filter contains unsafe conditions")));
+ 			return;
+ 		}
+ 	}
+ 
+ 	/* Here we know that this join can be pushed-down to remote side. */
+ 
+ 	/* Construct fpinfo for the join relation */
+ 	merge_fpinfo(outerrel, innerrel, fpinfo, jointype, joinrel->rows,
+ 				 joinrel->width); 
+ 	fpinfo->joinclauses = joinclauses;
+ 	fpinfo->otherclauses = otherclauses;
+ 
+ 	/* TODO determine more accurate cost and rows of the join. */
+ 	rows = joinrel->rows;
+ 	startup_cost = fpinfo->startup_cost;
+ 	total_cost = fpinfo->total_cost;
+ 
+ 	/* Get an alternative path for this foreign join */
+ 	subpath = get_unsorted_unparameterized_path(joinrel->pathlist);
+ 	if (subpath == NULL)
+ 		elog(ERROR, "could not get any alternative path for a foreign join");
+ 
+ 	/*
+ 	 * Create a new join path and add it to the joinrel which represents a join
+ 	 * between foreign tables.
+ 	 */
+ 	joinpath = create_foreignscan_path(root,
+ 									   joinrel,
+ 									   rows,
+ 									   startup_cost,
+ 									   total_cost,
+ 									   NIL,		/* no pathkeys */
+ 									   NULL,	/* no required_outer */
+ 									   subpath,
+ 									   NIL);	/* no fdw_private */
+ 
+ 	/* Add generated path into joinrel by add_path(). */
+ 	add_path(joinrel, (Path *) joinpath);
+ 	elog(DEBUG3, "join path added for (%s) join (%s)",
+ 		 bms_to_str(outerrel->relids), bms_to_str(innerrel->relids));
+ 
+ 	/* TODO consider parameterized paths */
+ }
+ 
+ /*
   * Create a tuple from the specified row of the PGresult.
   *
   * rel is the local representation of the foreign table, attinmeta is
***************
*** 2929,2941 **** postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
  static HeapTuple
  make_tuple_from_result_row(PGresult *res,
  						   int row,
! 						   Relation rel,
  						   AttInMetadata *attinmeta,
  						   List *retrieved_attrs,
  						   MemoryContext temp_context)
  {
  	HeapTuple	tuple;
- 	TupleDesc	tupdesc = RelationGetDescr(rel);
  	Datum	   *values;
  	bool	   *nulls;
  	ItemPointer ctid = NULL;
--- 3313,3326 ----
  static HeapTuple
  make_tuple_from_result_row(PGresult *res,
  						   int row,
! 						   const char *relname,
! 						   const char *query,
! 						   TupleDesc tupdesc,
  						   AttInMetadata *attinmeta,
  						   List *retrieved_attrs,
  						   MemoryContext temp_context)
  {
  	HeapTuple	tuple;
  	Datum	   *values;
  	bool	   *nulls;
  	ItemPointer ctid = NULL;
***************
*** 2962,2968 **** make_tuple_from_result_row(PGresult *res,
  	/*
  	 * Set up and install callback to report where conversion error occurs.
  	 */
! 	errpos.rel = rel;
  	errpos.cur_attno = 0;
  	errcallback.callback = conversion_error_callback;
  	errcallback.arg = (void *) &errpos;
--- 3347,3355 ----
  	/*
  	 * Set up and install callback to report where conversion error occurs.
  	 */
! 	errpos.relname = relname;
! 	errpos.query = query;
! 	errpos.tupdesc = tupdesc;
  	errpos.cur_attno = 0;
  	errcallback.callback = conversion_error_callback;
  	errcallback.arg = (void *) &errpos;
***************
*** 3052,3064 **** make_tuple_from_result_row(PGresult *res,
  static void
  conversion_error_callback(void *arg)
  {
  	ConversionLocation *errpos = (ConversionLocation *) arg;
! 	TupleDesc	tupdesc = RelationGetDescr(errpos->rel);
  
  	if (errpos->cur_attno > 0 && errpos->cur_attno <= tupdesc->natts)
! 		errcontext("column \"%s\" of foreign table \"%s\"",
! 				   NameStr(tupdesc->attrs[errpos->cur_attno - 1]->attname),
! 				   RelationGetRelationName(errpos->rel));
  }
  
  /*
--- 3439,3479 ----
  static void
  conversion_error_callback(void *arg)
  {
+ 	const char *attname;
+ 	const char *relname;
  	ConversionLocation *errpos = (ConversionLocation *) arg;
! 	TupleDesc	tupdesc = errpos->tupdesc;
! 	StringInfoData buf;
! 
! 	if (errpos->relname)
! 	{
! 		/* error occurred in a scan against a foreign table */ 
! 		initStringInfo(&buf);
! 		if (errpos->cur_attno > 0)
! 			appendStringInfo(&buf, "column \"%s\"",
! 					 NameStr(tupdesc->attrs[errpos->cur_attno - 1]->attname));
! 		else if (errpos->cur_attno == SelfItemPointerAttributeNumber)
! 			appendStringInfoString(&buf, "column \"ctid\"");
! 		attname = buf.data;
! 
! 		initStringInfo(&buf);
! 		appendStringInfo(&buf, "foreign table \"%s\"", errpos->relname);
! 		relname = buf.data;
! 	}
! 	else
! 	{
! 		/* error occurred in a scan against a foreign join */ 
! 		initStringInfo(&buf);
! 		appendStringInfo(&buf, "column %d", errpos->cur_attno - 1);
! 		attname = buf.data;
! 
! 		initStringInfo(&buf);
! 		appendStringInfo(&buf, "foreign join \"%s\"", errpos->query);
! 		relname = buf.data;
! 	}
  
  	if (errpos->cur_attno > 0 && errpos->cur_attno <= tupdesc->natts)
! 		errcontext("%s of %s", attname, relname);
  }
  
  /*
*** a/contrib/postgres_fdw/postgres_fdw.h
--- b/contrib/postgres_fdw/postgres_fdw.h
***************
*** 15,20 ****
--- 15,21 ----
  
  #include "foreign/foreign.h"
  #include "lib/stringinfo.h"
+ #include "nodes/plannodes.h"
  #include "nodes/relation.h"
  #include "utils/relcache.h"
  
***************
*** 26,31 ****
--- 27,38 ----
   */
  typedef struct PgFdwRelationInfo
  {
+ 	/*
+ 	 * True means that the relation can be pushed down.  Always true for
+ 	 * simple foreign scan.
+ 	 */
+ 	bool		pushdown_safe;
+ 
  	/* baserestrictinfo clauses, broken down into safe and unsafe subsets. */
  	List	   *remote_conds;
  	List	   *local_conds;
***************
*** 52,58 **** typedef struct PgFdwRelationInfo
  	/* Cached catalog information. */
  	ForeignTable *table;
  	ForeignServer *server;
! 	UserMapping *user;			/* only set in use_remote_estimate mode */
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
--- 59,72 ----
  	/* Cached catalog information. */
  	ForeignTable *table;
  	ForeignServer *server;
! 	Oid			umid;
! 
! 	/* Join information */
! 	RelOptInfo *outerrel;
! 	RelOptInfo *innerrel;
! 	JoinType	jointype;
! 	List	   *joinclauses;
! 	List	   *otherclauses;
  } PgFdwRelationInfo;
  
  /* in postgres_fdw.c */
***************
*** 88,100 **** extern void deparseSelectSql(StringInfo buf,
  				 PlannerInfo *root,
  				 RelOptInfo *baserel,
  				 Bitmapset *attrs_used,
! 				 List **retrieved_attrs);
! extern void appendWhereClause(StringInfo buf,
  				  PlannerInfo *root,
  				  RelOptInfo *baserel,
  				  List *exprs,
! 				  bool is_first,
  				  List **params);
  extern void deparseInsertSql(StringInfo buf, PlannerInfo *root,
  				 Index rtindex, Relation rel,
  				 List *targetAttrs, bool doNothing, List *returningList,
--- 102,134 ----
  				 PlannerInfo *root,
  				 RelOptInfo *baserel,
  				 Bitmapset *attrs_used,
! 				 List *remote_conds,
! 				 List *pathkeys,
! 				 List **params_list,
! 				 List **fdw_scan_tlist,
! 				 List **retrieved_attrs,
! 				 StringInfo relations,
! 				 bool alias);
! extern void appendConditions(StringInfo buf,
  				  PlannerInfo *root,
  				  RelOptInfo *baserel,
+ 				  List *outertlist,
+ 				  List *innertlist,
  				  List *exprs,
! 				  const char *prefix,
  				  List **params);
+ extern void deparseJoinSql(StringInfo sql,
+ 			   PlannerInfo *root,
+ 			   RelOptInfo *baserel,
+ 			   RelOptInfo *outerrel,
+ 			   RelOptInfo *innerrel,
+ 			   const char *sql_o,
+ 			   const char *sql_i,
+ 			   JoinType jointype,
+ 			   List *joinclauses,
+ 			   List *otherclauses,
+ 			   List **fdw_scan_tlist,
+ 			   List **retrieved_attrs);
  extern void deparseInsertSql(StringInfo buf, PlannerInfo *root,
  				 Index rtindex, Relation rel,
  				 List *targetAttrs, bool doNothing, List *returningList,
*** a/contrib/postgres_fdw/sql/postgres_fdw.sql
--- b/contrib/postgres_fdw/sql/postgres_fdw.sql
***************
*** 11,22 **** DO $d$
--- 11,27 ----
              OPTIONS (dbname '$$||current_database()||$$',
                       port '$$||current_setting('port')||$$'
              )$$;
+         EXECUTE $$CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgres_fdw
+             OPTIONS (dbname '$$||current_database()||$$',
+                      port '$$||current_setting('port')||$$'
+             )$$;
      END;
  $d$;
  
  CREATE USER MAPPING FOR public SERVER testserver1
  	OPTIONS (user 'value', password 'value');
  CREATE USER MAPPING FOR CURRENT_USER SERVER loopback;
+ CREATE USER MAPPING FOR CURRENT_USER SERVER loopback2;
  
  -- ===================================================================
  -- create objects used through FDW loopback server
***************
*** 39,44 **** CREATE TABLE "S 1"."T 2" (
--- 44,61 ----
  	c2 text,
  	CONSTRAINT t2_pkey PRIMARY KEY (c1)
  );
+ CREATE TABLE "S 1"."T 3" (
+ 	c1 int NOT NULL,
+ 	c2 int NOT NULL,
+ 	c3 text,
+ 	CONSTRAINT t3_pkey PRIMARY KEY (c1)
+ );
+ CREATE TABLE "S 1"."T 4" (
+ 	c1 int NOT NULL,
+ 	c2 int NOT NULL,
+ 	c4 text,
+ 	CONSTRAINT t4_pkey PRIMARY KEY (c1)
+ );
  
  INSERT INTO "S 1"."T 1"
  	SELECT id,
***************
*** 54,62 **** INSERT INTO "S 1"."T 2"
--- 71,93 ----
  	SELECT id,
  	       'AAA' || to_char(id, 'FM000')
  	FROM generate_series(1, 100) id;
+ INSERT INTO "S 1"."T 3"
+ 	SELECT id,
+ 	       id + 1,
+ 	       'AAA' || to_char(id, 'FM000')
+ 	FROM generate_series(1, 100) id;
+ DELETE FROM "S 1"."T 3" WHERE c1 % 2 != 0;	-- delete for outer join tests
+ INSERT INTO "S 1"."T 4"
+ 	SELECT id,
+ 	       id + 1,
+ 	       'AAA' || to_char(id, 'FM000')
+ 	FROM generate_series(1, 100) id;
+ DELETE FROM "S 1"."T 4" WHERE c1 % 3 != 0;	-- delete for outer join tests
  
  ANALYZE "S 1"."T 1";
  ANALYZE "S 1"."T 2";
+ ANALYZE "S 1"."T 3";
+ ANALYZE "S 1"."T 4";
  
  -- ===================================================================
  -- create foreign tables
***************
*** 87,92 **** CREATE FOREIGN TABLE ft2 (
--- 118,146 ----
  ) SERVER loopback;
  ALTER FOREIGN TABLE ft2 DROP COLUMN cx;
  
+ CREATE FOREIGN TABLE ft4 (
+ 	c1 int NOT NULL,
+ 	c2 int NOT NULL,
+ 	c3 text
+ ) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 3');
+ 
+ CREATE FOREIGN TABLE ft5 (
+ 	c1 int NOT NULL,
+ 	c2 int NOT NULL,
+ 	c3 text
+ ) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 4');
+ 
+ CREATE FOREIGN TABLE ft6 (
+ 	c1 int NOT NULL,
+ 	c2 int NOT NULL,
+ 	c3 text
+ ) SERVER loopback2 OPTIONS (schema_name 'S 1', table_name 'T 4');
+ CREATE USER view_owner;
+ GRANT ALL ON ft5 TO view_owner;
+ CREATE VIEW v_ft5 AS SELECT * FROM ft5;
+ ALTER VIEW v_ft5 OWNER TO view_owner;
+ CREATE USER MAPPING FOR view_owner SERVER loopback;
+ 
  -- ===================================================================
  -- tests for validator
  -- ===================================================================
***************
*** 168,175 **** EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE c1 = 102 FOR SHARE;
  SELECT * FROM ft1 t1 WHERE c1 = 102 FOR SHARE;
  -- aggregate
  SELECT COUNT(*) FROM ft1 t1;
- -- join two tables
- SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
  -- subquery
  SELECT * FROM ft1 t1 WHERE t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 <= 10) ORDER BY c1;
  -- subquery+MAX
--- 222,227 ----
***************
*** 257,262 **** EXPLAIN (VERBOSE, COSTS false)
--- 309,394 ----
  SELECT count(c3) FROM ft1 t1 WHERE t1.c1 === t1.c2;
  
  -- ===================================================================
+ -- JOIN queries
+ -- ===================================================================
+ -- join two tables
+ EXPLAIN (COSTS false, VERBOSE)
+ 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;
+ 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;
+ -- join three tables
+ EXPLAIN (COSTS false, VERBOSE)
+ 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;
+ 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;
+ -- left outer join
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
+ SELECT t1.c1, t2.c1 FROM ft4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
+ -- right outer join
+ EXPLAIN (COSTS false, VERBOSE)
+ 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;
+ 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;
+ -- full outer join
+ EXPLAIN (COSTS false, VERBOSE)
+ 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 + WHERE clause, only matched rows
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 = t2.c1 OR t1.c1 IS NULL) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
+ SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 = t2.c1 OR t1.c1 IS NULL) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10;
+ -- join at WHERE clause 
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON true WHERE (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON true WHERE (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- join in CTE
+ EXPLAIN (COSTS false, VERBOSE)
+ 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;
+ 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;
+ -- ctid with whole-row reference
+ EXPLAIN (COSTS false, VERBOSE)
+ 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;
+ 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;
+ -- partially unsafe to push down, not pushed down
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON t2.c1 = t2.c1 JOIN ft4 t3 ON t2.c1 = t3.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10;
+ SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON t2.c1 = t2.c1 JOIN ft4 t3 ON t2.c1 = t3.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10;
+ -- SEMI JOIN, not pushed down
+ EXPLAIN (COSTS false, VERBOSE)
+ 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;
+ 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;
+ -- ANTI JOIN, not pushed down
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
+ SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10;
+ -- CROSS JOIN, not pushed down
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+ SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+ -- different server
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+ SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+ -- different effective user for permission check
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN v_ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+ SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN v_ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10;
+ -- unsafe join conditions
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c8 = t2.c8) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c8 = t2.c8) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- local filter (unsafe conditions on one side)
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = 'foo' ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = 'foo' ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
+ -- Aggregate after UNION, for testing setrefs
+ EXPLAIN (COSTS false, VERBOSE)
+ 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;
+ 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;
+ -- join two foreign tables and two local tables
+ EXPLAIN (COSTS false, VERBOSE)
+ SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON t1.c1 = t2.c1 JOIN "S 1"."T 1" t3 ON t1.c1 = t3."C 1" JOIN "S 1"."T 2" t4 ON t1.c1 = t4.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10;
+ SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON t1.c1 = t2.c1 JOIN "S 1"."T 1" t3 ON t1.c1 = t3."C 1" JOIN "S 1"."T 2" t4 ON t1.c1 = t4.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10;
+ 
+ -- ===================================================================
  -- parameterized queries
  -- ===================================================================
  -- simple join
***************
*** 880,882 **** DROP TYPE "Colors" CASCADE;
--- 1012,1018 ----
  IMPORT FOREIGN SCHEMA import_source LIMIT TO (t5)
    FROM SERVER loopback INTO import_dest5;  -- ERROR
  ROLLBACK;
+ 
+ -- Cleanup
+ DROP OWNED BY view_owner;
+ DROP USER view_owner;
