diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index f88e0a2..c0022da 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -7566,14 +7566,11 @@ SELECT t1.a,t2.b,t3.c FROM fprt1 t1 INNER JOIN fprt2 t2 ON (t1.a = t2.b) INNER J
 -- left outer join + nullable clasue
 EXPLAIN (COSTS OFF)
 SELECT t1.a,t2.b,t2.c FROM fprt1 t1 LEFT JOIN (SELECT * FROM fprt2 WHERE a < 10) t2 ON (t1.a = t2.b and t1.b = t2.a) WHERE t1.a < 10 ORDER BY 1,2,3;
-                                    QUERY PLAN                                     
------------------------------------------------------------------------------------
- Sort
-   Sort Key: t1.a, ftprt2_p1.b, ftprt2_p1.c
-   ->  Append
-         ->  Foreign Scan
-               Relations: (public.ftprt1_p1 t1) LEFT JOIN (public.ftprt2_p1 fprt2)
-(5 rows)
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
+ Foreign Scan
+   Relations: (public.ftprt1_p1 t1) LEFT JOIN (public.ftprt2_p1 fprt2)
+(2 rows)
 
 SELECT t1.a,t2.b,t2.c FROM fprt1 t1 LEFT JOIN (SELECT * FROM fprt2 WHERE a < 10) t2 ON (t1.a = t2.b and t1.b = t2.a) WHERE t1.a < 10 ORDER BY 1,2,3;
  a | b |  c   
diff --git a/src/backend/executor/execAmi.c b/src/backend/executor/execAmi.c
index 9e78421..11950ec 100644
--- a/src/backend/executor/execAmi.c
+++ b/src/backend/executor/execAmi.c
@@ -443,6 +443,21 @@ ExecSupportsMarkRestore(Path *pathnode)
 				return false;	/* childless Result */
 			}
 
+		case T_Append:
+			{
+				AppendPath *appendPath = castNode(AppendPath, pathnode);
+
+				/*
+				 * AppendPaths acting as a proxy will never appear in the
+				 * final plan, so just return the mark/restore capability of
+				 * the proxied subpath.
+				 */
+				if (IS_PROXY_PATH(appendPath))
+					return ExecSupportsMarkRestore(
+									(Path *) linitial(appendPath->subpaths));
+
+				return false;
+			}
 		default:
 			break;
 	}
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index fd1a583..128f2d8 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -136,6 +136,8 @@ static void recurse_push_qual(Node *setOp, Query *topquery,
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
 static void add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 						List *live_childrels);
+static void generate_proxy_paths(PlannerInfo *root, RelOptInfo *rel,
+					 RelOptInfo *childrel);
 
 
 /*
@@ -1545,12 +1547,31 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 	/*
 	 * If we found unparameterized paths for all children, build an unordered,
 	 * unparameterized Append path for the rel.  (Note: this is correct even
-	 * if we have zero or one live subpath due to constraint exclusion.)
+	 * if we have no live subpaths due to constraint exclusion.)
 	 */
 	if (subpaths_valid)
-		add_path(rel, (Path *) create_append_path(rel, subpaths, NIL,
-												  NULL, 0, false,
-												  partitioned_rels, -1));
+	{
+		/*
+		 * If only a single subpath exists then we've no need for the Append
+		 * at all. Here we'll generate a "proxy" path in rel for each of the
+		 * subpath rel's paths.
+		 */
+		if (list_length(subpaths) == 1)
+		{
+			generate_proxy_paths(root, rel,
+								 ((Path *) linitial(subpaths))->parent);
+
+			/* no need to generate any other paths */
+			return;
+		}
+		else
+		{
+			add_path(rel, (Path *) create_append_path(rel, subpaths, NIL,
+													  NULL, 0, false,
+													  partitioned_rels, -1,
+													  NIL, NIL, false));
+		}
+	}
 
 	/*
 	 * Consider an append of unordered, unparameterized partial paths.  Make
@@ -1593,7 +1614,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		appendpath = create_append_path(rel, NIL, partial_subpaths, NULL,
 										parallel_workers,
 										enable_parallel_append,
-										partitioned_rels, -1);
+										partitioned_rels, -1,
+										NIL, NIL, false);
 
 		/*
 		 * Make sure any subsequent partial paths use the same row count
@@ -1642,7 +1664,8 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		appendpath = create_append_path(rel, pa_nonpartial_subpaths,
 										pa_partial_subpaths,
 										NULL, parallel_workers, true,
-										partitioned_rels, partial_rows);
+										partitioned_rels, partial_rows,
+										NIL, NIL, false);
 		add_partial_path(rel, (Path *) appendpath);
 	}
 
@@ -1694,10 +1717,61 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel,
 		}
 
 		if (subpaths_valid)
-			add_path(rel, (Path *)
-					 create_append_path(rel, subpaths, NIL,
-										required_outer, 0, false,
-										partitioned_rels, -1));
+		{
+			if (list_length(subpaths) == 1)
+				generate_proxy_paths(root, rel,
+									 ((Path *) linitial(subpaths))->parent);
+			else
+				add_path(rel, (Path *)
+						 create_append_path(rel, subpaths, NIL,
+											required_outer, 0, false,
+											partitioned_rels, -1,
+											NIL, NIL, false));
+		}
+	}
+}
+
+/*
+ * generate_proxy_paths
+ *		Add all of childrel's paths to rel as proxy paths.
+ */
+static void
+generate_proxy_paths(PlannerInfo *root, RelOptInfo *rel, RelOptInfo *childrel)
+{
+	ListCell   *l;
+	List	   *oldtlist;
+	List	   *newtlist;
+
+	/*
+	 * Make a copy of the current target list.  Each path can share the same
+	 * copy.
+	 */
+	oldtlist = list_copy(rel->reltarget->exprs);
+
+	/* Perform translation to determine what the new tlist will be */
+	newtlist = (List *) adjust_appendrel_attrs_multilevel(root,
+											(Node *) rel->reltarget->exprs,
+											childrel->relids,
+											rel->relids);
+
+	foreach(l, childrel->pathlist)
+	{
+		Path *path = (Path *) lfirst(l);
+
+		add_path(rel, (Path *) create_append_path(rel,
+					list_make1(path), NIL, PATH_REQ_OUTER(path),
+					path->parallel_workers, false, NULL, -1,
+					oldtlist, newtlist, true));
+	}
+
+	foreach(l, childrel->partial_pathlist)
+	{
+		Path *path = (Path *) lfirst(l);
+
+		add_partial_path(rel, (Path *) create_append_path(rel,
+							list_make1(path), NIL, PATH_REQ_OUTER(path),
+							path->parallel_workers, false, NULL, -1,
+							oldtlist, newtlist, true));
 	}
 }
 
@@ -1962,8 +2036,8 @@ set_dummy_rel_pathlist(RelOptInfo *rel)
 	rel->partial_pathlist = NIL;
 
 	add_path(rel, (Path *) create_append_path(rel, NIL, NIL, NULL,
-											  0, false, NIL, -1));
-
+											  0, false, NIL, -1,
+											  NIL, NIL, false));
 	/*
 	 * We set the cheapest path immediately, to ensure that IS_DUMMY_REL()
 	 * will recognize the relation as dummy if anyone asks.  This is redundant
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index 70a925c..97a77fe 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -2094,6 +2094,55 @@ match_eclasses_to_foreign_key_col(PlannerInfo *root,
 
 
 /*
+ * promote_child_rel_equivalences
+ *		Remove em_is_child marker from any eclass members belonging to
+ *		childrelids
+ */
+void
+promote_child_rel_equivalences(PlannerInfo *root, Relids childrelids)
+{
+	ListCell   *lc1;
+
+	foreach(lc1, root->eq_classes)
+	{
+		EquivalenceClass *cur_ec = (EquivalenceClass *) lfirst(lc1);
+		ListCell   *lc2;
+
+		/*
+		 * No need to search in eclasses with volatile expressions, there will
+		 * be no child exprs in here.
+		 */
+		if (cur_ec->ec_has_volatile)
+			continue;
+
+		foreach(lc2, cur_ec->ec_members)
+		{
+			EquivalenceMember *cur_em = (EquivalenceMember *) lfirst(lc2);
+
+			if (!cur_em->em_is_child)
+				continue;		/* ignore non-child members */
+
+			if (cur_em->em_is_const)
+				continue;		/* ignore consts here */
+
+			/* skip if it doesn't reference these childrelids */
+			if (!bms_overlap(cur_em->em_relids, childrelids))
+				continue;
+
+			cur_em->em_is_child = false;
+
+			/*
+			 * The eclass will need to be updated to say which relids it
+			 * contains members for.  These were previously not set due to the
+			 * member belonging to a child rel.
+			 */
+			cur_ec->ec_relids = bms_add_members(cur_ec->ec_relids,
+												cur_em->em_relids);
+		}
+	}
+}
+
+/*
  * add_child_rel_equivalences
  *	  Search for EC members that reference the parent_rel, and
  *	  add transformed members referencing the child_rel.
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index a35d068..3ca4e8b 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -1231,7 +1231,8 @@ mark_dummy_rel(RelOptInfo *rel)
 
 	/* Set up the dummy path */
 	add_path(rel, (Path *) create_append_path(rel, NIL, NIL, NULL,
-											  0, false, NIL, -1));
+											  0, false, NIL, -1, NIL,
+											  NIL, false));
 
 	/* Set or update cheapest_total_path and related fields */
 	set_cheapest(rel);
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 86e7e74..8df673e 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -35,6 +35,7 @@
 #include "optimizer/planmain.h"
 #include "optimizer/planner.h"
 #include "optimizer/predtest.h"
+#include "optimizer/prep.h"
 #include "optimizer/restrictinfo.h"
 #include "optimizer/subselect.h"
 #include "optimizer/tlist.h"
@@ -73,12 +74,13 @@ static Plan *create_plan_recurse(PlannerInfo *root, Path *best_path,
 static Plan *create_scan_plan(PlannerInfo *root, Path *best_path,
 				 int flags);
 static List *build_path_tlist(PlannerInfo *root, Path *path);
+static List *finalize_plan_tlist(PlannerInfo *root, List *tlist);
 static bool use_physical_tlist(PlannerInfo *root, Path *path, int flags);
 static List *get_gating_quals(PlannerInfo *root, List *quals);
 static Plan *create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 				   List *gating_quals);
 static Plan *create_join_plan(PlannerInfo *root, JoinPath *best_path);
-static Plan *create_append_plan(PlannerInfo *root, AppendPath *best_path);
+static Plan *create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags);
 static Plan *create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path);
 static Result *create_result_plan(PlannerInfo *root, ResultPath *best_path);
 static ProjectSet *create_project_set_plan(PlannerInfo *root, ProjectSetPath *best_path);
@@ -385,7 +387,7 @@ create_plan_recurse(PlannerInfo *root, Path *best_path, int flags)
 			break;
 		case T_Append:
 			plan = create_append_plan(root,
-									  (AppendPath *) best_path);
+									  (AppendPath *) best_path, flags);
 			break;
 		case T_MergeAppend:
 			plan = create_merge_append_plan(root,
@@ -749,6 +751,12 @@ build_path_tlist(PlannerInfo *root, Path *path)
 		if (path->param_info)
 			node = replace_nestloop_params(root, node);
 
+		/*
+		 * Perform any translations required from any Append nodes which
+		 * were removed.
+		 */
+		node = replace_translatable_exprs(root, node);
+
 		tle = makeTargetEntry((Expr *) node,
 							  resno,
 							  NULL,
@@ -763,6 +771,18 @@ build_path_tlist(PlannerInfo *root, Path *path)
 }
 
 /*
+ * finalize_plan_tlist
+ *		Finalize the plan's targetlist. This must be used in places where
+ *		build_path_tlist is called before create_plan_recurse is called for
+ *		any of the plan's subplans.
+ */
+static List *
+finalize_plan_tlist(PlannerInfo *root, List *tlist)
+{
+	return (List *) replace_translatable_exprs(root, (Node *) tlist);
+}
+
+/*
  * use_physical_tlist
  *		Decide whether to use a tlist matching relation structure,
  *		rather than only those Vars actually referenced.
@@ -795,9 +815,13 @@ use_physical_tlist(PlannerInfo *root, Path *path, int flags)
 	/*
 	 * Can't do it with inheritance cases either (mainly because Append
 	 * doesn't project; this test may be unnecessary now that
-	 * create_append_plan instructs its children to return an exact tlist).
+	 * create_append_plan instructs its children to return an exact tlist),
+	 * however, we must allow any inherited children that have been pulled up
+	 * from below and Append.
 	 */
-	if (rel->reloptkind != RELOPT_BASEREL)
+	if (rel->reloptkind != RELOPT_BASEREL &&
+		(rel->reloptkind != RELOPT_OTHER_MEMBER_REL ||
+		!bms_is_member(rel->relid, root->translated_childrelids)))
 		return false;
 
 	/*
@@ -1011,14 +1035,56 @@ create_join_plan(PlannerInfo *root, JoinPath *best_path)
  *	  Returns a Plan node.
  */
 static Plan *
-create_append_plan(PlannerInfo *root, AppendPath *best_path)
+create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags)
 {
 	Append	   *plan;
-	List	   *tlist = build_path_tlist(root, &best_path->path);
+	List	   *tlist;
 	List	   *subplans = NIL;
 	ListCell   *subpaths;
 
 	/*
+	 * If the AppendPath is acting as a proxy to a single subpath then we
+	 * don't create the Append node at all. Instead we'll just generate the
+	 * plan node for the only subpath.
+	 */
+	if (IS_PROXY_PATH(best_path))
+	{
+		Path	   *subpath = (Path *) linitial(best_path->subpaths);
+		Plan	   *plan;
+
+		/* Just verify we've only one, otherwise the planner messed up */
+		Assert(list_length(best_path->subpaths) == 1);
+
+		/*
+		 * Make the required changes to the child relation to allow it to
+		 * be scanned in place of its parent.
+		 */
+		promote_child_relation(root, best_path->path.parent, subpath->parent,
+							   best_path->translate_from,
+							   best_path->translate_to);
+
+		/* Generate a new PathTarget using the Append relation's one */
+		subpath->pathtarget = copy_pathtarget(best_path->path.pathtarget);
+		subpath->pathtarget->exprs = (List *) replace_translatable_exprs(root,
+										(Node *) subpath->pathtarget->exprs);
+
+		plan = create_plan_recurse(root, subpath, flags);
+
+		/*
+		 * If we require an exact tlist then we'd better use a translated
+		 * version of the, would be, Append node's tlist.
+		 */
+		if (flags & CP_EXACT_TLIST)
+			plan->targetlist = build_path_tlist(root, &best_path->path);
+
+		copy_generic_path_info(plan, (Path *) subpath);
+
+		return plan;
+	}
+
+	tlist = build_path_tlist(root, &best_path->path);
+
+	/*
 	 * The subpaths list could be empty, if every child was proven empty by
 	 * constraint exclusion.  In that case generate a dummy plan that returns
 	 * no rows.
@@ -1054,13 +1120,6 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path)
 		subplans = lappend(subplans, subplan);
 	}
 
-	/*
-	 * XXX ideally, if there's just one child, we'd not bother to generate an
-	 * Append node but just return the single child.  At the moment this does
-	 * not work because the varno of the child scan plan won't match the
-	 * parent-rel Vars it'll be asked to emit.
-	 */
-
 	plan = make_append(subplans, best_path->first_partial_path,
 					   tlist, best_path->partitioned_rels);
 
@@ -1173,6 +1232,14 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path)
 	node->partitioned_rels = best_path->partitioned_rels;
 	node->mergeplans = subplans;
 
+	/*
+	 * Since we called build_path_tlist() before create_plan_recurse some
+	 * Append nodes may have been removed and new translation Vars added.
+	 * We'll need to perform a final translation to perform any further
+	 * translations which were added since build_path_tlist was called.
+	 */
+	plan->targetlist = finalize_plan_tlist(root, plan->targetlist);
+
 	return (Plan *) node;
 }
 
@@ -1196,6 +1263,8 @@ create_result_plan(PlannerInfo *root, ResultPath *best_path)
 	/* best_path->quals is just bare clauses */
 	quals = order_qual_clauses(root, best_path->quals);
 
+	quals = (List *) replace_translatable_exprs(root, (Node *) quals);
+
 	plan = make_result(tlist, (Node *) quals, NULL);
 
 	copy_generic_path_info(&plan->plan, (Path *) best_path);
@@ -1302,7 +1371,8 @@ create_unique_plan(PlannerInfo *root, UniquePath *best_path, int flags)
 	 * sorting or stuff has to be added.
 	 */
 	in_operators = best_path->in_operators;
-	uniq_exprs = best_path->uniq_exprs;
+	uniq_exprs = (List *) replace_translatable_exprs(root,
+											(Node *) best_path->uniq_exprs);
 
 	/* initialize modified subplan tlist as just the "required" vars */
 	newtlist = build_path_tlist(root, &best_path->path);
@@ -1551,6 +1621,15 @@ create_gather_merge_plan(PlannerInfo *root, GatherMergePath *best_path)
 	/* use parallel mode for parallel plans. */
 	root->glob->parallelModeNeeded = true;
 
+	/*
+	 * Since we called build_path_tlist() before create_plan_recurse some
+	 * Append nodes may have been removed and new translation Vars added.
+	 * We'll need to perform a final translation to perform any further
+	 * translations which were added since build_path_tlist was called.
+	 */
+	gm_plan->plan.targetlist = finalize_plan_tlist(root,
+												   gm_plan->plan.targetlist);
+
 	return gm_plan;
 }
 
@@ -1696,6 +1775,8 @@ create_group_plan(PlannerInfo *root, GroupPath *best_path)
 
 	quals = order_qual_clauses(root, best_path->qual);
 
+	quals = (List *) replace_translatable_exprs(root, (Node *) quals);
+
 	plan = make_group(tlist,
 					  quals,
 					  list_length(best_path->groupClause),
@@ -1761,6 +1842,8 @@ create_agg_plan(PlannerInfo *root, AggPath *best_path)
 
 	quals = order_qual_clauses(root, best_path->qual);
 
+	quals = (List *) replace_translatable_exprs(root, (Node *) quals);
+
 	plan = make_agg(tlist, quals,
 					best_path->aggstrategy,
 					best_path->aggsplit,
@@ -1954,13 +2037,17 @@ create_groupingsets_plan(PlannerInfo *root, GroupingSetsPath *best_path)
 		RollupData *rollup = linitial(rollups);
 		AttrNumber *top_grpColIdx;
 		int			numGroupCols;
+		List	   *quals;
 
 		top_grpColIdx = remap_groupColIdx(root, rollup->groupClause);
 
 		numGroupCols = list_length((List *) linitial(rollup->gsets));
 
+		quals = (List *) replace_translatable_exprs(root,
+													(Node *) best_path->qual);
+
 		plan = make_agg(build_path_tlist(root, &best_path->path),
-						best_path->qual,
+						quals,
 						best_path->aggstrategy,
 						AGGSPLIT_SIMPLE,
 						numGroupCols,
@@ -2000,6 +2087,13 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 		Plan	   *plan;
 
 		/*
+		 * This minmaxagg may have been below a single path Append node which
+		 * has been removed.  Apply expr translations, if required.
+		 */
+		mminfo->target = (Expr *) replace_translatable_exprs(root,
+													(Node *) mminfo->target);
+
+		/*
 		 * Generate the plan for the subquery. We already have a Path, but we
 		 * have to convert it to a Plan and attach a LIMIT node above it.
 		 * Since we are entering a different planner context (subroot),
@@ -3055,6 +3149,11 @@ create_tidscan_plan(PlannerInfo *root, TidPath *best_path,
 		ortidquals = list_make1(make_orclause(ortidquals));
 	scan_clauses = list_difference(scan_clauses, ortidquals);
 
+	/*
+	 * tidquals won't need to be passed through replace_translatable_exprs()
+	 * as ctid is the same varattno in every relation.
+	 */
+
 	scan_plan = make_tidscan(tlist,
 							 scan_clauses,
 							 scan_relid,
@@ -3731,6 +3830,8 @@ create_nestloop_plan(PlannerInfo *root,
 		{
 			root->curOuterParams = list_delete_cell(root->curOuterParams,
 													cell, prev);
+			nlp->paramval = (Var *) replace_translatable_exprs(root,
+													(Node *) nlp->paramval);
 			nestParams = lappend(nestParams, nlp);
 		}
 		else if (IsA(nlp->paramval, PlaceHolderVar) &&
@@ -3743,12 +3844,20 @@ create_nestloop_plan(PlannerInfo *root,
 		{
 			root->curOuterParams = list_delete_cell(root->curOuterParams,
 													cell, prev);
+			nlp->paramval = (Var *) replace_translatable_exprs(root,
+													(Node *) nlp->paramval);
 			nestParams = lappend(nestParams, nlp);
 		}
 		else
 			prev = cell;
 	}
 
+	/* perform translation of join clauses, if required */
+	joinclauses = (List *) replace_translatable_exprs(root,
+														(Node *) joinclauses);
+	otherclauses = (List *) replace_translatable_exprs(root,
+													(Node *) otherclauses);
+
 	join_plan = make_nestloop(tlist,
 							  joinclauses,
 							  otherclauses,
@@ -3758,6 +3867,15 @@ create_nestloop_plan(PlannerInfo *root,
 							  best_path->jointype,
 							  best_path->inner_unique);
 
+	/*
+	 * Since we called build_path_tlist() before create_plan_recurse some
+	 * Append nodes may have been removed and new translation Vars added.
+	 * We'll need to perform a final translation to perform any further
+	 * translations which were added since build_path_tlist was called.
+	 */
+	join_plan->join.plan.targetlist = finalize_plan_tlist(root,
+											join_plan->join.plan.targetlist);
+
 	copy_generic_path_info(&join_plan->join.plan, &best_path->path);
 
 	return join_plan;
@@ -4051,6 +4169,14 @@ create_mergejoin_plan(PlannerInfo *root,
 	 * than we need for the current mergejoin.
 	 */
 
+	/* perform translation of join clauses, if required */
+	mergeclauses = (List *) replace_translatable_exprs(root,
+													(Node *) mergeclauses);
+	joinclauses = (List *) replace_translatable_exprs(root,
+														(Node *) joinclauses);
+	otherclauses = (List *) replace_translatable_exprs(root,
+													(Node *) otherclauses);
+
 	/*
 	 * Now we can build the mergejoin node.
 	 */
@@ -4068,6 +4194,15 @@ create_mergejoin_plan(PlannerInfo *root,
 							   best_path->jpath.inner_unique,
 							   best_path->skip_mark_restore);
 
+	/*
+	 * Since we called build_path_tlist() before create_plan_recurse some
+	 * Append nodes may have been removed and new translation Vars added.
+	 * We'll need to perform a final translation to perform any further
+	 * translations which were added since build_path_tlist was called.
+	 */
+	join_plan->join.plan.targetlist = finalize_plan_tlist(root,
+											join_plan->join.plan.targetlist);
+
 	/* Costs of sort and material steps are included in path cost already */
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
@@ -4194,6 +4329,13 @@ create_hashjoin_plan(PlannerInfo *root,
 	copy_plan_costsize(&hash_plan->plan, inner_plan);
 	hash_plan->plan.startup_cost = hash_plan->plan.total_cost;
 
+	/* perform translation of join clauses, if required */
+	hashclauses = (List *) replace_translatable_exprs(root,
+														(Node *) hashclauses);
+	joinclauses = (List *) replace_translatable_exprs(root,
+														(Node *) joinclauses);
+	otherclauses = (List *) replace_translatable_exprs(root,
+														(Node *) otherclauses);
 	/*
 	 * If parallel-aware, the executor will also need an estimate of the total
 	 * number of rows expected from all participants so that it can size the
@@ -4214,6 +4356,15 @@ create_hashjoin_plan(PlannerInfo *root,
 							  best_path->jpath.jointype,
 							  best_path->jpath.inner_unique);
 
+	/*
+	 * Since we called build_path_tlist() before create_plan_recurse some
+	 * Append nodes may have been removed and new translation Vars added.
+	 * We'll need to perform a final translation to perform any further
+	 * translations which were added since build_path_tlist was called.
+	 */
+	join_plan->join.plan.targetlist = finalize_plan_tlist(root,
+											join_plan->join.plan.targetlist);
+
 	copy_generic_path_info(&join_plan->join.plan, &best_path->jpath.path);
 
 	return join_plan;
@@ -6601,14 +6752,24 @@ is_projection_capable_path(Path *path)
 		case T_RecursiveUnion:
 			return false;
 		case T_Append:
+			{
+				AppendPath *apath = (AppendPath *) path;
+				/*
+				 * For Appends acting as a proxy to a single subpath, just
+				 * return the projection capability of that subpath.
+				 */
+				if (IS_PROXY_PATH(apath))
+					return is_projection_capable_path(
+										(Path *) linitial(apath->subpaths));
 
-			/*
-			 * Append can't project, but if it's being used to represent a
-			 * dummy path, claim that it can project.  This prevents us from
-			 * converting a rel from dummy to non-dummy status by applying a
-			 * projection to its dummy path.
-			 */
-			return IS_DUMMY_PATH(path);
+				/*
+				 * Otherwise, Append can't project, but if it's being used to
+				 * represent a dummy path, claim that it can project.  This
+				 * prevents us from converting a rel from dummy to non-dummy
+				 * status by applying a projection to its dummy path.
+				 */
+				return IS_DUMMY_PATH(path);
+			}
 		case T_ProjectSet:
 
 			/*
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 5387043..18cd1bc 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -3689,7 +3689,10 @@ create_grouping_paths(PlannerInfo *root,
 								   0,
 								   false,
 								   NIL,
-								   -1);
+								   -1,
+								   NIL,
+								   NIL,
+								   false);
 			path->pathtarget = target;
 		}
 		else
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 45d82da..b0c3894 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -918,6 +918,9 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	subroot->hasRecursion = false;
 	subroot->wt_param_id = -1;
 	subroot->non_recursive_path = NULL;
+	subroot->translate_from_exprs = NIL;
+	subroot->translate_to_exprs = NIL;
+	subroot->translated_childrelids = NULL;
 
 	/* No CTEs to worry about */
 	Assert(subquery->cteList == NIL);
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index e6b1534..4c239eb 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -592,7 +592,8 @@ generate_union_path(SetOperationStmt *op, PlannerInfo *root,
 	 * Append the child results together.
 	 */
 	path = (Path *) create_append_path(result_rel, pathlist, NIL,
-									   NULL, 0, false, NIL, -1);
+									   NULL, 0, false, NIL, -1,
+									   NIL, NIL, false);
 	/* We have to manually jam the right tlist into the path; ick */
 	path->pathtarget = create_pathtarget(root, tlist);
 
@@ -704,7 +705,8 @@ generate_nonunion_path(SetOperationStmt *op, PlannerInfo *root,
 	 * Append the child results together.
 	 */
 	path = (Path *) create_append_path(result_rel, pathlist, NIL,
-									   NULL, 0, false, NIL, -1);
+									   NULL, 0, false, NIL, -1,
+									   NIL, NIL, false);
 
 	/* We have to manually jam the right tlist into the path; ick */
 	path->pathtarget = create_pathtarget(root, tlist);
@@ -1939,6 +1941,90 @@ translate_col_privs(const Bitmapset *parent_privs,
 }
 
 /*
+ * promote_child_relation
+ *		Make the required changes to allow a child relation to be used instead
+ *		of an Append node.
+ */
+void
+promote_child_relation(PlannerInfo *root, RelOptInfo *parent,
+					   RelOptInfo *child,
+					   List *translate_from_exprs,
+					   List *translate_to_exprs)
+{
+	Bitmapset  *cur_relids;
+
+	/*
+	 * First we must record the translation expressions in the PlannerInfo.
+	 * These need to be found when the expression translation is being done
+	 * when the final plan is being assembled.
+	 */
+	root->translate_from_exprs = list_concat(root->translate_from_exprs,
+									list_copy(translate_from_exprs));
+
+	root->translate_to_exprs = list_concat(root->translate_to_exprs,
+									list_copy(translate_to_exprs));
+
+	/*
+	 * Record this child as having been promoted.  Some places treat child
+	 * relations in a special way, and this will give them a VIP ticket to
+	 * adulthood, where required.
+	 */
+	root->translated_childrelids =
+				bms_add_members(root->translated_childrelids, child->relids);
+
+	cur_relids = child->relids;
+
+	do
+	{
+		AppendRelInfo **appinfos;
+		int			nappinfos;
+		int			i;
+
+		appinfos = find_appinfos_by_relids(root, cur_relids, &nappinfos);
+
+		/* free any bitmapset we used in the last iteration */
+		if (cur_relids != child->relids)
+			bms_free(cur_relids);
+
+		cur_relids = NULL;
+
+		for (i = 0; i < nappinfos; i++)
+		{
+			AppendRelInfo *appinfo = appinfos[i];
+
+			RelOptInfo *parent = find_base_rel(root, appinfo->parent_relid);
+			RelOptInfo *child = find_base_rel(root, appinfo->child_relid);
+
+			/*
+			 * Some childrel equivalences may not exist due to some eclasses
+			 * having been added since add_child_rel_equivalences was called
+			 * originally.  Calling this again will add child members for any
+			 * newly added eclasses.
+			 */
+			add_child_rel_equivalences(root, appinfo, parent, child);
+
+			cur_relids = bms_add_member(cur_relids, appinfo->parent_relid);
+		}
+
+		pfree(appinfos);
+
+		/*
+		 * There may be multiple levels between the parent and child, so keep
+		 * going until we reach the top-level parent.
+		 */
+	} while (!bms_equal(cur_relids, parent->relids));
+
+	bms_free(cur_relids);
+
+	/*
+	 * Finally, we remove em_is_child markers for child eclass members
+	 * belonging to this child rel.  This is required so we can find eclass
+	 * members for sort PathKeys.
+	 */
+	promote_child_rel_equivalences(root, child->relids);
+}
+
+/*
  * adjust_appendrel_attrs
  *	  Copy the specified query or expression and translate Vars referring to a
  *	  parent rel to refer to the corresponding child rel instead.  We also
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 89f27ce..2c0d3fa 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -159,6 +159,7 @@ static Query *substitute_actual_srf_parameters(Query *expr,
 static Node *substitute_actual_srf_parameters_mutator(Node *node,
 										 substitute_actual_srf_parameters_context *context);
 static bool tlist_matches_coltypelist(List *tlist, List *coltypelist);
+static Node *replace_translatable_exprs_mutator(Node *node, PlannerInfo *root);
 
 
 /*****************************************************************************
@@ -5308,3 +5309,48 @@ tlist_matches_coltypelist(List *tlist, List *coltypelist)
 
 	return true;
 }
+
+/*
+ * replace_translatable_exprs
+ *		Append paths with a single subpath are special because its possible
+ *		to disregard the AppendPath and just build the plan using that single
+ *		subpath instead.  Expr translation is required to translate the
+ *		targetlists of nodes above the Append, this function performs that
+ *		translation.
+ *
+ * For now, we really only ever expect to translate from a Var. However, the
+ * Var may get translated into something other than a Var.
+ */
+Node *
+replace_translatable_exprs(PlannerInfo *root, Node *expr)
+{
+	if (root->translate_from_exprs != NIL)
+		return replace_translatable_exprs_mutator(expr, root);
+	return expr;
+}
+
+static Node *
+replace_translatable_exprs_mutator(Node *node, PlannerInfo *root)
+{
+	if (node == NULL)
+		return NULL;
+
+	if (IsA(node, Var))
+	{
+		ListCell *l1;
+		ListCell *l2;
+
+		forboth(l1, root->translate_from_exprs, l2, root->translate_to_exprs)
+		{
+			if (equal(node, lfirst(l1)))
+			{
+				/* XXX do we need to do copyObject? */
+				return (Node *) copyObject(lfirst(l2));
+			}
+		}
+		return node;
+	}
+	return expression_tree_mutator(node,
+								   replace_translatable_exprs_mutator,
+								   (void *) root);
+}
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index fe3b458..b8456f0 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1208,17 +1208,24 @@ create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, List *tidquals,
  *	  pathnode.
  *
  * Note that we must handle subpaths = NIL, representing a dummy access path.
+ * If isproxy is true, then we expect only a single subpath. translate_from
+ * and translate_to can be set to allow translation of Exprs between the
+ * subpath and the Append Rel.
  */
 AppendPath *
 create_append_path(RelOptInfo *rel,
 				   List *subpaths, List *partial_subpaths,
 				   Relids required_outer,
 				   int parallel_workers, bool parallel_aware,
-				   List *partitioned_rels, double rows)
+				   List *partitioned_rels, double rows,
+				   List *translate_from, List *translate_to,
+				   bool isproxy)
 {
 	AppendPath *pathnode = makeNode(AppendPath);
 	ListCell   *l;
 
+	/* Might both be NIL, but must contain the same number of elements. */
+	Assert(list_length(translate_from) == list_length(translate_to));
 	Assert(!parallel_aware || parallel_workers > 0);
 
 	pathnode->path.pathtype = T_Append;
@@ -1229,8 +1236,26 @@ create_append_path(RelOptInfo *rel,
 	pathnode->path.parallel_aware = parallel_aware;
 	pathnode->path.parallel_safe = rel->consider_parallel;
 	pathnode->path.parallel_workers = parallel_workers;
-	pathnode->path.pathkeys = NIL;	/* result is always considered unsorted */
 	pathnode->partitioned_rels = list_copy(partitioned_rels);
+	pathnode->translate_from = translate_from;
+	pathnode->translate_to = translate_to;
+	pathnode->subpaths = subpaths;
+	pathnode->isproxy = isproxy;
+
+	/*
+	 * If this is a proxy Append, there can be only one subpath, so we're able
+	 * to use its PathKeys.
+	 */
+	if (isproxy)
+	{
+		Assert(list_length(subpaths) == 1);
+		pathnode->path.pathkeys = ((Path *) linitial(subpaths))->pathkeys;
+	}
+	else
+	{
+		Assert(translate_from == NIL);
+		pathnode->path.pathkeys = NIL;
+	}
 
 	/*
 	 * For parallel append, non-partial paths are sorted by descending total
@@ -3572,7 +3597,10 @@ reparameterize_path(PlannerInfo *root, Path *path,
 									   apath->path.parallel_workers,
 									   apath->path.parallel_aware,
 									   apath->partitioned_rels,
-									   -1);
+									   -1,
+									   apath->translate_from,
+									   apath->translate_to,
+									   apath->isproxy);
 			}
 		default:
 			break;
diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h
index 6bf68f3..3683ddb 100644
--- a/src/include/nodes/relation.h
+++ b/src/include/nodes/relation.h
@@ -317,6 +317,16 @@ typedef struct PlannerInfo
 
 	/* optional private data for join_search_hook, e.g., GEQO */
 	void	   *join_search_private;
+
+	/*
+	 * Used for expr translation for some proxy path Append nodes during
+	 * createplan.
+	 */
+	List	   *translate_from_exprs;
+	List	   *translate_to_exprs;
+
+	Relids		translated_childrelids; /* All child rels that have been
+										 * promoted to parents */
 } PlannerInfo;
 
 
@@ -1262,6 +1272,18 @@ typedef struct CustomPath
  * elements.  These cases are optimized during create_append_plan.
  * In particular, an AppendPath with no subpaths is a "dummy" path that
  * is created to represent the case that a relation is provably empty.
+ *
+ * An AppendPath with a single subpath can be set up to become a "proxy" path.
+ * This allows a Path which belongs to one relation to be added to the pathlist
+ * of some other relation.  This is intended as generic infrastructure, but its
+ * primary use case is to allow Appends with only a single subpath to be
+ * removed from the final plan.
+ *
+ * A path's targetlist naturally will contain Vars belonging to its parent
+ * rel, so we must also provide a mechanism to allow the translation of any
+ * Vars which reference the original Append relation's Vars to allow them to
+ * be translated into the proxied path Vars. translate_from and translate_to
+ * serve this purpose.  They must only be set when isproxy is true.
  */
 typedef struct AppendPath
 {
@@ -1269,9 +1291,12 @@ typedef struct AppendPath
 	/* RT indexes of non-leaf tables in a partition tree */
 	List	   *partitioned_rels;
 	List	   *subpaths;		/* list of component Paths */
-
 	/* Index of first partial path in subpaths */
 	int			first_partial_path;
+
+	List	   *translate_from;
+	List	   *translate_to;
+	bool		isproxy;
 } AppendPath;
 
 #define IS_DUMMY_PATH(p) \
@@ -1282,6 +1307,10 @@ typedef struct AppendPath
 	((r)->cheapest_total_path != NULL && \
 	 IS_DUMMY_PATH((r)->cheapest_total_path))
 
+/* Append path is acting as a proxy for its single subpath */
+#define IS_PROXY_PATH(p) \
+	(IsA((p), AppendPath) && ((AppendPath *) (p))->isproxy)
+
 /*
  * MergeAppendPath represents a MergeAppend plan, ie, the merging of sorted
  * results from several member plans to produce similarly-sorted output.
diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h
index ba4fa4b..10e0d50 100644
--- a/src/include/optimizer/clauses.h
+++ b/src/include/optimizer/clauses.h
@@ -85,4 +85,6 @@ extern Node *estimate_expression_value(PlannerInfo *root, Node *node);
 extern Query *inline_set_returning_function(PlannerInfo *root,
 							  RangeTblEntry *rte);
 
+extern Node *replace_translatable_exprs(PlannerInfo *root, Node *expr);
+
 #endif							/* CLAUSES_H */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index ef7173f..a2dad71 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -68,7 +68,9 @@ extern AppendPath *create_append_path(RelOptInfo *rel,
 				   List *subpaths, List *partial_subpaths,
 				   Relids required_outer,
 				   int parallel_workers, bool parallel_aware,
-				   List *partitioned_rels, double rows);
+				   List *partitioned_rels, double rows,
+				   List *translate_from, List *translate_to,
+				   bool isproxy);
 extern MergeAppendPath *create_merge_append_path(PlannerInfo *root,
 						 RelOptInfo *rel,
 						 List *subpaths,
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 0072b7a..2b7515f 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -156,6 +156,8 @@ extern bool exprs_known_equal(PlannerInfo *root, Node *item1, Node *item2);
 extern EquivalenceClass *match_eclasses_to_foreign_key_col(PlannerInfo *root,
 								  ForeignKeyOptInfo *fkinfo,
 								  int colno);
+extern void promote_child_rel_equivalences(PlannerInfo *root,
+												Relids childrelids);
 extern void add_child_rel_equivalences(PlannerInfo *root,
 						   AppendRelInfo *appinfo,
 						   RelOptInfo *parent_rel,
diff --git a/src/include/optimizer/prep.h b/src/include/optimizer/prep.h
index 89b7ef3..bc2552f 100644
--- a/src/include/optimizer/prep.h
+++ b/src/include/optimizer/prep.h
@@ -49,6 +49,11 @@ extern RelOptInfo *plan_set_operations(PlannerInfo *root);
 
 extern void expand_inherited_tables(PlannerInfo *root);
 
+extern void promote_child_relation(PlannerInfo *root, RelOptInfo *parent,
+					   RelOptInfo *child,
+					   List *translate_from_exprs,
+					   List *translate_to_exprs);
+
 extern Node *adjust_appendrel_attrs(PlannerInfo *root, Node *node,
 					   int nappinfos, AppendRelInfo **appinfos);
 
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index a79f891..c294d65 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1680,12 +1680,11 @@ explain (costs off) select * from list_parted;
 (4 rows)
 
 explain (costs off) select * from list_parted where a is null;
-           QUERY PLAN           
---------------------------------
- Append
-   ->  Seq Scan on part_null_xy
-         Filter: (a IS NULL)
-(3 rows)
+        QUERY PLAN        
+--------------------------
+ Seq Scan on part_null_xy
+   Filter: (a IS NULL)
+(2 rows)
 
 explain (costs off) select * from list_parted where a is not null;
            QUERY PLAN            
@@ -1722,12 +1721,11 @@ explain (costs off) select * from list_parted where a = 'ab' or a in (null, 'cd'
 (7 rows)
 
 explain (costs off) select * from list_parted where a = 'ab';
-                QUERY PLAN                
-------------------------------------------
- Append
-   ->  Seq Scan on part_ab_cd
-         Filter: ((a)::text = 'ab'::text)
-(3 rows)
+             QUERY PLAN             
+------------------------------------
+ Seq Scan on part_ab_cd
+   Filter: ((a)::text = 'ab'::text)
+(2 rows)
 
 create table range_list_parted (
 	a	int,
@@ -1807,12 +1805,11 @@ explain (costs off) select * from range_list_parted where a is null;
 
 /* Should only select rows from the null-accepting partition */
 explain (costs off) select * from range_list_parted where b is null;
-             QUERY PLAN             
-------------------------------------
- Append
-   ->  Seq Scan on part_40_inf_null
-         Filter: (b IS NULL)
-(3 rows)
+          QUERY PLAN          
+------------------------------
+ Seq Scan on part_40_inf_null
+   Filter: (b IS NULL)
+(2 rows)
 
 explain (costs off) select * from range_list_parted where a is not null and a < 67;
                    QUERY PLAN                   
@@ -1933,12 +1930,11 @@ explain (costs off) select * from mcrparted where a > -1;	-- scans all partition
 (15 rows)
 
 explain (costs off) select * from mcrparted where a = 20 and abs(b) = 10 and c > 10;	-- scans mcrparted4
-                        QUERY PLAN                         
------------------------------------------------------------
- Append
-   ->  Seq Scan on mcrparted4
-         Filter: ((c > 10) AND (a = 20) AND (abs(b) = 10))
-(3 rows)
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Seq Scan on mcrparted4
+   Filter: ((c > 10) AND (a = 20) AND (abs(b) = 10))
+(2 rows)
 
 explain (costs off) select * from mcrparted where a = 20 and c > 20; -- scans mcrparted3, mcrparte4, mcrparte5, mcrparted_def
                QUERY PLAN                
@@ -1962,22 +1958,18 @@ create table parted_minmax1 partition of parted_minmax for values from (1) to (1
 create index parted_minmax1i on parted_minmax1 (a, b);
 insert into parted_minmax values (1,'12345');
 explain (costs off) select min(a), max(a) from parted_minmax where b = '12345';
-                                              QUERY PLAN                                               
--------------------------------------------------------------------------------------------------------
+                                           QUERY PLAN                                            
+-------------------------------------------------------------------------------------------------
  Result
    InitPlan 1 (returns $0)
      ->  Limit
-           ->  Merge Append
-                 Sort Key: parted_minmax1.a
-                 ->  Index Only Scan using parted_minmax1i on parted_minmax1
-                       Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
+           ->  Index Only Scan using parted_minmax1i on parted_minmax1
+                 Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
    InitPlan 2 (returns $1)
      ->  Limit
-           ->  Merge Append
-                 Sort Key: parted_minmax1_1.a DESC
-                 ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax1_1
-                       Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
-(13 rows)
+           ->  Index Only Scan Backward using parted_minmax1i on parted_minmax1 parted_minmax1_1
+                 Index Cond: ((a IS NOT NULL) AND (b = '12345'::text))
+(9 rows)
 
 select min(a), max(a) from parted_minmax where b = '12345';
  min | max 
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 27ab852..78664fa 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -193,19 +193,18 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT 50 phv, * FROM prt1 WHERE prt1.b = 0)
 -- Join with pruned partitions from joining relations
 EXPLAIN (COSTS OFF)
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a < 450 AND t2.b > 250 AND t1.b = 0 ORDER BY t1.a, t2.b;
-                        QUERY PLAN                         
------------------------------------------------------------
+                     QUERY PLAN                      
+-----------------------------------------------------
  Sort
    Sort Key: t1.a
-   ->  Append
-         ->  Hash Join
-               Hash Cond: (t2.b = t1.a)
-               ->  Seq Scan on prt2_p2 t2
-                     Filter: (b > 250)
-               ->  Hash
-                     ->  Seq Scan on prt1_p2 t1
-                           Filter: ((a < 450) AND (b = 0))
-(10 rows)
+   ->  Hash Join
+         Hash Cond: (t2.b = t1.a)
+         ->  Seq Scan on prt2_p2 t2
+               Filter: (b > 250)
+         ->  Hash
+               ->  Seq Scan on prt1_p2 t1
+                     Filter: ((a < 450) AND (b = 0))
+(9 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1 t1, prt2 t2 WHERE t1.a = t2.b AND t1.a < 450 AND t2.b > 250 AND t1.b = 0 ORDER BY t1.a, t2.b;
   a  |  c   |  b  |  c   
@@ -1391,10 +1390,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_l t1, prt2_l t2 WHERE t1.a = t2.b AND t1
                      ->  Seq Scan on prt2_l_p3_p1 t2_3
                      ->  Seq Scan on prt2_l_p3_p2 t2_4
                ->  Hash
-                     ->  Append
-                           ->  Seq Scan on prt1_l_p3_p1 t1_3
-                                 Filter: (b = 0)
-(29 rows)
+                     ->  Seq Scan on prt1_l_p3_p1 t1_3
+                           Filter: (b = 0)
+(28 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_l t1, prt2_l t2 WHERE t1.a = t2.b AND t1.b = 0 ORDER BY t1.a, t2.b;
   a  |  c   |  b  |  c   
@@ -1437,10 +1435,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_l t1 LEFT JOIN prt2_l t2 ON t1.a = t2.b
                      ->  Seq Scan on prt2_l_p3_p1 t2_3
                      ->  Seq Scan on prt2_l_p3_p2 t2_4
                ->  Hash
-                     ->  Append
-                           ->  Seq Scan on prt1_l_p3_p1 t1_3
-                                 Filter: (b = 0)
-(30 rows)
+                     ->  Seq Scan on prt1_l_p3_p1 t1_3
+                           Filter: (b = 0)
+(29 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_l t1 LEFT JOIN prt2_l t2 ON t1.a = t2.b AND t1.c = t2.c WHERE t1.b = 0 ORDER BY t1.a, t2.b;
   a  |  c   |  b  |  c   
@@ -1492,10 +1489,9 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_l t1 RIGHT JOIN prt2_l t2 ON t1.a = t2.b
                            ->  Seq Scan on prt1_l_p3_p1 t1_3
                            ->  Seq Scan on prt1_l_p3_p2 t1_4
                      ->  Hash
-                           ->  Append
-                                 ->  Seq Scan on prt2_l_p3_p1 t2_3
-                                       Filter: (a = 0)
-(31 rows)
+                           ->  Seq Scan on prt2_l_p3_p1 t2_3
+                                 Filter: (a = 0)
+(30 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_l t1 RIGHT JOIN prt2_l t2 ON t1.a = t2.b AND t1.c = t2.c WHERE t2.a = 0 ORDER BY t1.a, t2.b;
   a  |  c   |  b  |  c   
@@ -1541,14 +1537,12 @@ SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE prt1_l.b = 0) t1
                            Filter: (a = 0)
          ->  Hash Full Join
                Hash Cond: ((prt1_l_p3_p1.a = prt2_l_p3_p1.b) AND ((prt1_l_p3_p1.c)::text = (prt2_l_p3_p1.c)::text))
-               ->  Append
-                     ->  Seq Scan on prt1_l_p3_p1
-                           Filter: (b = 0)
+               ->  Seq Scan on prt1_l_p3_p1
+                     Filter: (b = 0)
                ->  Hash
-                     ->  Append
-                           ->  Seq Scan on prt2_l_p3_p1
-                                 Filter: (a = 0)
-(33 rows)
+                     ->  Seq Scan on prt2_l_p3_p1
+                           Filter: (a = 0)
+(31 rows)
 
 SELECT t1.a, t1.c, t2.b, t2.c FROM (SELECT * FROM prt1_l WHERE prt1_l.b = 0) t1 FULL JOIN (SELECT * FROM prt2_l WHERE prt2_l.a = 0) t2 ON (t1.a = t2.b AND t1.c = t2.c) ORDER BY t1.a, t2.b;
   a  |  c   |  b  |  c   
@@ -1610,9 +1604,8 @@ SELECT * FROM prt1_l t1 LEFT JOIN LATERAL
                                  ->  Seq Scan on prt1_l_p2_p2 t2_2
                                        Filter: ((t1_2.a = a) AND ((t1_2.c)::text = (c)::text))
                ->  Nested Loop Left Join
-                     ->  Append
-                           ->  Seq Scan on prt1_l_p3_p1 t1_3
-                                 Filter: (b = 0)
+                     ->  Seq Scan on prt1_l_p3_p1 t1_3
+                           Filter: (b = 0)
                      ->  Hash Join
                            Hash Cond: ((t3_3.b = t2_3.a) AND ((t3_3.c)::text = (t2_3.c)::text))
                            ->  Append
@@ -1624,7 +1617,7 @@ SELECT * FROM prt1_l t1 LEFT JOIN LATERAL
                                              Filter: ((t1_3.a = a) AND ((t1_3.c)::text = (c)::text))
                                        ->  Seq Scan on prt1_l_p3_p2 t2_4
                                              Filter: ((t1_3.a = a) AND ((t1_3.c)::text = (c)::text))
-(46 rows)
+(45 rows)
 
 SELECT * FROM prt1_l t1 LEFT JOIN LATERAL
 			  (SELECT t2.a AS t2a, t2.c AS t2c, t2.b AS t2b, t3.b AS t3b, least(t1.a,t2.a,t3.b) FROM prt1_l t2 JOIN prt2_l t3 ON (t2.a = t3.b AND t2.c = t3.c)) ss
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index aabb024..c02f82c 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -43,20 +43,18 @@ explain (costs off) select * from lp where a > 'a' and a <= 'd';
 (7 rows)
 
 explain (costs off) select * from lp where a = 'a';
-            QUERY PLAN             
------------------------------------
- Append
-   ->  Seq Scan on lp_ad
-         Filter: (a = 'a'::bpchar)
-(3 rows)
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on lp_ad
+   Filter: (a = 'a'::bpchar)
+(2 rows)
 
 explain (costs off) select * from lp where 'a' = a;	/* commuted */
-            QUERY PLAN             
------------------------------------
- Append
-   ->  Seq Scan on lp_ad
-         Filter: ('a'::bpchar = a)
-(3 rows)
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on lp_ad
+   Filter: ('a'::bpchar = a)
+(2 rows)
 
 explain (costs off) select * from lp where a is not null;
            QUERY PLAN            
@@ -75,12 +73,11 @@ explain (costs off) select * from lp where a is not null;
 (11 rows)
 
 explain (costs off) select * from lp where a is null;
-         QUERY PLAN          
------------------------------
- Append
-   ->  Seq Scan on lp_null
-         Filter: (a IS NULL)
-(3 rows)
+      QUERY PLAN       
+-----------------------
+ Seq Scan on lp_null
+   Filter: (a IS NULL)
+(2 rows)
 
 explain (costs off) select * from lp where a = 'a' or a = 'c';
                         QUERY PLAN                        
@@ -150,12 +147,11 @@ create table coll_pruning_a partition of coll_pruning for values in ('a');
 create table coll_pruning_b partition of coll_pruning for values in ('b');
 create table coll_pruning_def partition of coll_pruning default;
 explain (costs off) select * from coll_pruning where a collate "C" = 'a' collate "C";
-                 QUERY PLAN                  
----------------------------------------------
- Append
-   ->  Seq Scan on coll_pruning_a
-         Filter: (a = 'a'::text COLLATE "C")
-(3 rows)
+              QUERY PLAN               
+---------------------------------------
+ Seq Scan on coll_pruning_a
+   Filter: (a = 'a'::text COLLATE "C")
+(2 rows)
 
 -- collation doesn't match the partitioning collation, no pruning occurs
 explain (costs off) select * from coll_pruning where a collate "POSIX" = 'a' collate "POSIX";
@@ -192,20 +188,18 @@ create table rlp5 partition of rlp for values from (31) to (maxvalue) partition
 create table rlp5_default partition of rlp5 default;
 create table rlp5_1 partition of rlp5 for values from (31) to (40);
 explain (costs off) select * from rlp where a < 1;
-       QUERY PLAN        
--------------------------
- Append
-   ->  Seq Scan on rlp1
-         Filter: (a < 1)
-(3 rows)
+    QUERY PLAN     
+-------------------
+ Seq Scan on rlp1
+   Filter: (a < 1)
+(2 rows)
 
 explain (costs off) select * from rlp where 1 > a;	/* commuted */
-       QUERY PLAN        
--------------------------
- Append
-   ->  Seq Scan on rlp1
-         Filter: (1 > a)
-(3 rows)
+    QUERY PLAN     
+-------------------
+ Seq Scan on rlp1
+   Filter: (1 > a)
+(2 rows)
 
 explain (costs off) select * from rlp where a <= 1;
               QUERY PLAN               
@@ -220,20 +214,18 @@ explain (costs off) select * from rlp where a <= 1;
 (7 rows)
 
 explain (costs off) select * from rlp where a = 1;
-       QUERY PLAN        
--------------------------
- Append
-   ->  Seq Scan on rlp2
-         Filter: (a = 1)
-(3 rows)
+    QUERY PLAN     
+-------------------
+ Seq Scan on rlp2
+   Filter: (a = 1)
+(2 rows)
 
 explain (costs off) select * from rlp where a = 1::bigint;		/* same as above */
-            QUERY PLAN             
------------------------------------
- Append
-   ->  Seq Scan on rlp2
-         Filter: (a = '1'::bigint)
-(3 rows)
+         QUERY PLAN          
+-----------------------------
+ Seq Scan on rlp2
+   Filter: (a = '1'::bigint)
+(2 rows)
 
 explain (costs off) select * from rlp where a = 1::numeric;	/* no pruning */
                   QUERY PLAN                   
@@ -386,20 +378,18 @@ explain (costs off) select * from rlp where a = 16;
 (9 rows)
 
 explain (costs off) select * from rlp where a = 16 and b in ('not', 'in', 'here');
-                                 QUERY PLAN                                 
-----------------------------------------------------------------------------
- Append
-   ->  Seq Scan on rlp3_default
-         Filter: ((a = 16) AND ((b)::text = ANY ('{not,in,here}'::text[])))
-(3 rows)
+                              QUERY PLAN                              
+----------------------------------------------------------------------
+ Seq Scan on rlp3_default
+   Filter: ((a = 16) AND ((b)::text = ANY ('{not,in,here}'::text[])))
+(2 rows)
 
 explain (costs off) select * from rlp where a = 16 and b < 'ab';
-                       QUERY PLAN                        
----------------------------------------------------------
- Append
-   ->  Seq Scan on rlp3_default
-         Filter: (((b)::text < 'ab'::text) AND (a = 16))
-(3 rows)
+                    QUERY PLAN                     
+---------------------------------------------------
+ Seq Scan on rlp3_default
+   Filter: (((b)::text < 'ab'::text) AND (a = 16))
+(2 rows)
 
 explain (costs off) select * from rlp where a = 16 and b <= 'ab';
                         QUERY PLAN                        
@@ -412,12 +402,11 @@ explain (costs off) select * from rlp where a = 16 and b <= 'ab';
 (5 rows)
 
 explain (costs off) select * from rlp where a = 16 and b is null;
-                 QUERY PLAN                 
---------------------------------------------
- Append
-   ->  Seq Scan on rlp3nullxy
-         Filter: ((b IS NULL) AND (a = 16))
-(3 rows)
+              QUERY PLAN              
+--------------------------------------
+ Seq Scan on rlp3nullxy
+   Filter: ((b IS NULL) AND (a = 16))
+(2 rows)
 
 explain (costs off) select * from rlp where a = 16 and b is not null;
                    QUERY PLAN                   
@@ -434,12 +423,11 @@ explain (costs off) select * from rlp where a = 16 and b is not null;
 (9 rows)
 
 explain (costs off) select * from rlp where a is null;
-             QUERY PLAN             
-------------------------------------
- Append
-   ->  Seq Scan on rlp_default_null
-         Filter: (a IS NULL)
-(3 rows)
+          QUERY PLAN          
+------------------------------
+ Seq Scan on rlp_default_null
+   Filter: (a IS NULL)
+(2 rows)
 
 explain (costs off) select * from rlp where a is not null;
               QUERY PLAN               
@@ -488,12 +476,11 @@ explain (costs off) select * from rlp where a > 30;
 (7 rows)
 
 explain (costs off) select * from rlp where a = 30;	/* only default is scanned */
-            QUERY PLAN            
-----------------------------------
- Append
-   ->  Seq Scan on rlp_default_30
-         Filter: (a = 30)
-(3 rows)
+         QUERY PLAN         
+----------------------------
+ Seq Scan on rlp_default_30
+   Filter: (a = 30)
+(2 rows)
 
 explain (costs off) select * from rlp where a <= 31;
               QUERY PLAN               
@@ -530,12 +517,11 @@ explain (costs off) select * from rlp where a <= 31;
 (29 rows)
 
 explain (costs off) select * from rlp where a = 1 or a = 7;
-              QUERY PLAN              
---------------------------------------
- Append
-   ->  Seq Scan on rlp2
-         Filter: ((a = 1) OR (a = 7))
-(3 rows)
+           QUERY PLAN           
+--------------------------------
+ Seq Scan on rlp2
+   Filter: ((a = 1) OR (a = 7))
+(2 rows)
 
 explain (costs off) select * from rlp where a = 1 or b = 'ab';
                       QUERY PLAN                       
@@ -580,12 +566,11 @@ explain (costs off) select * from rlp where a > 20 and a < 27;
 (7 rows)
 
 explain (costs off) select * from rlp where a = 29;
-           QUERY PLAN           
---------------------------------
- Append
-   ->  Seq Scan on rlp4_default
-         Filter: (a = 29)
-(3 rows)
+        QUERY PLAN        
+--------------------------
+ Seq Scan on rlp4_default
+   Filter: (a = 29)
+(2 rows)
 
 explain (costs off) select * from rlp where a >= 29;
               QUERY PLAN               
@@ -605,12 +590,11 @@ explain (costs off) select * from rlp where a >= 29;
 
 -- redundant clauses are eliminated
 explain (costs off) select * from rlp where a > 1 and a = 10;	/* only default */
-               QUERY PLAN               
-----------------------------------------
- Append
-   ->  Seq Scan on rlp_default_10
-         Filter: ((a > 1) AND (a = 10))
-(3 rows)
+            QUERY PLAN            
+----------------------------------
+ Seq Scan on rlp_default_10
+   Filter: ((a > 1) AND (a = 10))
+(2 rows)
 
 explain (costs off) select * from rlp where a > 1 and a >=15;	/* rlp3 onwards, including default */
                QUERY PLAN                
@@ -799,20 +783,18 @@ explain (costs off) select * from mc3p where a <= 10 and abs(b) < 10;
 (9 rows)
 
 explain (costs off) select * from mc3p where a = 11 and abs(b) = 0;
-                 QUERY PLAN                  
----------------------------------------------
- Append
-   ->  Seq Scan on mc3p_default
-         Filter: ((a = 11) AND (abs(b) = 0))
-(3 rows)
+              QUERY PLAN               
+---------------------------------------
+ Seq Scan on mc3p_default
+   Filter: ((a = 11) AND (abs(b) = 0))
+(2 rows)
 
 explain (costs off) select * from mc3p where a = 20 and abs(b) = 10 and c = 100;
-                         QUERY PLAN                         
-------------------------------------------------------------
- Append
-   ->  Seq Scan on mc3p6
-         Filter: ((a = 20) AND (c = 100) AND (abs(b) = 10))
-(3 rows)
+                      QUERY PLAN                      
+------------------------------------------------------
+ Seq Scan on mc3p6
+   Filter: ((a = 20) AND (c = 100) AND (abs(b) = 10))
+(2 rows)
 
 explain (costs off) select * from mc3p where a > 20;
            QUERY PLAN           
@@ -962,12 +944,11 @@ explain (costs off) select * from mc2p where a < 2;
 (9 rows)
 
 explain (costs off) select * from mc2p where a = 2 and b < 1;
-              QUERY PLAN               
----------------------------------------
- Append
-   ->  Seq Scan on mc2p3
-         Filter: ((b < 1) AND (a = 2))
-(3 rows)
+           QUERY PLAN            
+---------------------------------
+ Seq Scan on mc2p3
+   Filter: ((b < 1) AND (a = 2))
+(2 rows)
 
 explain (costs off) select * from mc2p where a > 1;
            QUERY PLAN           
@@ -986,12 +967,11 @@ explain (costs off) select * from mc2p where a > 1;
 (11 rows)
 
 explain (costs off) select * from mc2p where a = 1 and b > 1;
-              QUERY PLAN               
----------------------------------------
- Append
-   ->  Seq Scan on mc2p2
-         Filter: ((b > 1) AND (a = 1))
-(3 rows)
+           QUERY PLAN            
+---------------------------------
+ Seq Scan on mc2p2
+   Filter: ((b > 1) AND (a = 1))
+(2 rows)
 
 -- boolean partitioning
 create table boolpart (a bool) partition by list (a);
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index f1ae40d..5aad8b4 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -1057,15 +1057,14 @@ NOTICE:  f_leak => awesome science fiction
 (4 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-                             QUERY PLAN                             
---------------------------------------------------------------------
- Append
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Seq Scan on part_document_fiction
+   Filter: ((cid < 55) AND (dlevel <= $0) AND f_leak(dtitle))
    InitPlan 1 (returns $0)
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
-   ->  Seq Scan on part_document_fiction
-         Filter: ((cid < 55) AND (dlevel <= $0) AND f_leak(dtitle))
-(6 rows)
+(5 rows)
 
 -- pp1 ERROR
 INSERT INTO part_document VALUES (100, 11, 5, 'regress_rls_dave', 'testing pp1'); -- fail
@@ -1136,15 +1135,14 @@ NOTICE:  f_leak => awesome science fiction
 (4 rows)
 
 EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-                             QUERY PLAN                             
---------------------------------------------------------------------
- Append
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Seq Scan on part_document_fiction
+   Filter: ((cid < 55) AND (dlevel <= $0) AND f_leak(dtitle))
    InitPlan 1 (returns $0)
      ->  Index Scan using uaccount_pkey on uaccount
            Index Cond: (pguser = CURRENT_USER)
-   ->  Seq Scan on part_document_fiction
-         Filter: ((cid < 55) AND (dlevel <= $0) AND f_leak(dtitle))
-(6 rows)
+(5 rows)
 
 -- viewpoint from regress_rls_carol
 SET SESSION AUTHORIZATION regress_rls_carol;
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 92d427a..da70438 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -812,11 +812,10 @@ explain (costs off)
    UNION ALL
    SELECT 2 AS t, * FROM tenk1 b) c
  WHERE t = 2;
-        QUERY PLAN         
----------------------------
- Append
-   ->  Seq Scan on tenk1 b
-(2 rows)
+     QUERY PLAN      
+---------------------
+ Seq Scan on tenk1 b
+(1 row)
 
 -- Test that we push quals into UNION sub-selects only when it's safe
 explain (costs off)
