diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index cff23b0211..83d81886cc 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -46,6 +46,7 @@
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
 #include "nodes/plannodes.h"
+#include "optimizer/inherit.h"
 #include "optimizer/optimizer.h"
 #include "optimizer/prep.h"
 #include "optimizer/tlist.h"
@@ -1275,7 +1276,7 @@ deparseLockingClause(deparse_expr_cxt *context)
 		 * that DECLARE CURSOR ... FOR UPDATE is supported, which it isn't
 		 * before 8.3.
 		 */
-		if (relid == root->parse->resultRelation &&
+		if (is_result_relation(root, relid) &&
 			(root->parse->commandType == CMD_UPDATE ||
 			 root->parse->commandType == CMD_DELETE))
 		{
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index b46e7e623f..a4cd127e0e 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -6354,7 +6354,7 @@ UPDATE rw_view SET b = b + 5;
    Foreign Update on public.foreign_tbl parent_tbl_1
      Remote SQL: UPDATE public.child_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b
    ->  Foreign Scan on public.foreign_tbl parent_tbl_1
-         Output: (parent_tbl_1.b + 5), parent_tbl_1.ctid, parent_tbl_1.*
+         Output: (parent_tbl_1.b + 5), parent_tbl_1.ctid, 0, parent_tbl_1.*
          Remote SQL: SELECT a, b, ctid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE
 (6 rows)
 
@@ -6369,7 +6369,7 @@ UPDATE rw_view SET b = b + 15;
    Foreign Update on public.foreign_tbl parent_tbl_1
      Remote SQL: UPDATE public.child_tbl SET b = $2 WHERE ctid = $1 RETURNING a, b
    ->  Foreign Scan on public.foreign_tbl parent_tbl_1
-         Output: (parent_tbl_1.b + 15), parent_tbl_1.ctid, parent_tbl_1.*
+         Output: (parent_tbl_1.b + 15), parent_tbl_1.ctid, 0, parent_tbl_1.*
          Remote SQL: SELECT a, b, ctid FROM public.child_tbl WHERE ((a < b)) FOR UPDATE
 (6 rows)
 
@@ -7256,33 +7256,19 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                                       QUERY PLAN                                       
 ---------------------------------------------------------------------------------------
  Update on public.bar
-   Update on public.bar
-   Foreign Update on public.bar2 bar_1
+   Update on public.bar bar_1
+   Foreign Update on public.bar2 bar_2
      Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1
    ->  Hash Join
-         Output: (bar.f2 + 100), bar.ctid, foo.ctid, foo.*, foo.tableoid
+         Output: (bar.f2 + 100), bar.ctid, foo.ctid, (0), bar.*, foo.*, foo.tableoid
          Inner Unique: true
          Hash Cond: (bar.f1 = foo.f1)
-         ->  Seq Scan on public.bar
-               Output: bar.f2, bar.ctid, bar.f1
-         ->  Hash
-               Output: foo.ctid, foo.f1, foo.*, foo.tableoid
-               ->  HashAggregate
-                     Output: foo.ctid, foo.f1, foo.*, foo.tableoid
-                     Group Key: foo.f1
-                     ->  Append
-                           ->  Seq Scan on public.foo foo_1
-                                 Output: foo_1.ctid, foo_1.f1, foo_1.*, foo_1.tableoid
-                           ->  Foreign Scan on public.foo2 foo_2
-                                 Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
-                                 Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-   ->  Hash Join
-         Output: (bar_1.f2 + 100), bar_1.ctid, bar_1.*, foo.ctid, foo.*, foo.tableoid
-         Inner Unique: true
-         Hash Cond: (bar_1.f1 = foo.f1)
-         ->  Foreign Scan on public.bar2 bar_1
-               Output: bar_1.f2, bar_1.ctid, bar_1.*, bar_1.f1
-               Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
+         ->  Append
+               ->  Seq Scan on public.bar bar_1
+                     Output: bar_1.f2, bar_1.ctid, bar_1.f1, 0, bar_1.*
+               ->  Foreign Scan on public.bar2 bar_2
+                     Output: bar_2.f2, bar_2.ctid, bar_2.f1, 1, bar_2.*
+                     Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
          ->  Hash
                Output: foo.ctid, foo.f1, foo.*, foo.tableoid
                ->  HashAggregate
@@ -7294,7 +7280,7 @@ update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
                            ->  Foreign Scan on public.foo2 foo_2
                                  Output: foo_2.ctid, foo_2.f1, foo_2.*, foo_2.tableoid
                                  Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct1
-(39 rows)
+(25 rows)
 
 update bar set f2 = f2 + 100 where f1 in (select f1 from foo);
 select tableoid::regclass, * from bar order by 1,2;
@@ -7314,39 +7300,24 @@ update bar set f2 = f2 + 100
 from
   ( select f1 from foo union all select f1+3 from foo ) ss
 where bar.f1 = ss.f1;
-                                      QUERY PLAN                                      
---------------------------------------------------------------------------------------
+                                         QUERY PLAN                                         
+--------------------------------------------------------------------------------------------
  Update on public.bar
-   Update on public.bar
-   Foreign Update on public.bar2 bar_1
+   Update on public.bar bar_1
+   Foreign Update on public.bar2 bar_2
      Remote SQL: UPDATE public.loct2 SET f2 = $2 WHERE ctid = $1
-   ->  Hash Join
-         Output: (bar.f2 + 100), bar.ctid, (ROW(foo.f1))
-         Hash Cond: (foo.f1 = bar.f1)
-         ->  Append
-               ->  Seq Scan on public.foo
-                     Output: ROW(foo.f1), foo.f1
-               ->  Foreign Scan on public.foo2 foo_1
-                     Output: ROW(foo_1.f1), foo_1.f1
-                     Remote SQL: SELECT f1 FROM public.loct1
-               ->  Seq Scan on public.foo foo_2
-                     Output: ROW((foo_2.f1 + 3)), (foo_2.f1 + 3)
-               ->  Foreign Scan on public.foo2 foo_3
-                     Output: ROW((foo_3.f1 + 3)), (foo_3.f1 + 3)
-                     Remote SQL: SELECT f1 FROM public.loct1
-         ->  Hash
-               Output: bar.f2, bar.ctid, bar.f1
-               ->  Seq Scan on public.bar
-                     Output: bar.f2, bar.ctid, bar.f1
    ->  Merge Join
-         Output: (bar_1.f2 + 100), bar_1.ctid, bar_1.*, (ROW(foo.f1))
-         Merge Cond: (bar_1.f1 = foo.f1)
+         Output: (bar.f2 + 100), bar.ctid, (ROW(foo.f1)), (0), bar.*
+         Merge Cond: (bar.f1 = foo.f1)
          ->  Sort
-               Output: bar_1.f2, bar_1.ctid, bar_1.*, bar_1.f1
-               Sort Key: bar_1.f1
-               ->  Foreign Scan on public.bar2 bar_1
-                     Output: bar_1.f2, bar_1.ctid, bar_1.*, bar_1.f1
-                     Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
+               Output: bar.f2, bar.ctid, bar.f1, (0), bar.*
+               Sort Key: bar.f1
+               ->  Append
+                     ->  Seq Scan on public.bar bar_1
+                           Output: bar_1.f2, bar_1.ctid, bar_1.f1, 0, bar_1.*
+                     ->  Foreign Scan on public.bar2 bar_2
+                           Output: bar_2.f2, bar_2.ctid, bar_2.f1, 1, bar_2.*
+                           Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
          ->  Sort
                Output: (ROW(foo.f1)), foo.f1
                Sort Key: foo.f1
@@ -7361,7 +7332,7 @@ where bar.f1 = ss.f1;
                      ->  Foreign Scan on public.foo2 foo_3
                            Output: ROW((foo_3.f1 + 3)), (foo_3.f1 + 3)
                            Remote SQL: SELECT f1 FROM public.loct1
-(45 rows)
+(30 rows)
 
 update bar set f2 = f2 + 100
 from
@@ -7487,18 +7458,19 @@ ERROR:  WHERE CURRENT OF is not supported for this table type
 rollback;
 explain (verbose, costs off)
 delete from foo where f1 < 5 returning *;
-                                   QUERY PLAN                                   
---------------------------------------------------------------------------------
+                                      QUERY PLAN                                      
+--------------------------------------------------------------------------------------
  Delete on public.foo
-   Output: foo.f1, foo.f2
-   Delete on public.foo
-   Foreign Delete on public.foo2 foo_1
-   ->  Index Scan using i_foo_f1 on public.foo
-         Output: foo.ctid
-         Index Cond: (foo.f1 < 5)
-   ->  Foreign Delete on public.foo2 foo_1
-         Remote SQL: DELETE FROM public.loct1 WHERE ((f1 < 5)) RETURNING f1, f2
-(9 rows)
+   Output: foo_1.f1, foo_1.f2
+   Delete on public.foo foo_1
+   Foreign Delete on public.foo2 foo_2
+   ->  Append
+         ->  Index Scan using i_foo_f1 on public.foo foo_1
+               Output: foo_1.ctid, 0
+               Index Cond: (foo_1.f1 < 5)
+         ->  Foreign Delete on public.foo2 foo_2
+               Remote SQL: DELETE FROM public.loct1 WHERE ((f1 < 5)) RETURNING f1, f2
+(10 rows)
 
 delete from foo where f1 < 5 returning *;
  f1 | f2 
@@ -7512,17 +7484,20 @@ delete from foo where f1 < 5 returning *;
 
 explain (verbose, costs off)
 update bar set f2 = f2 + 100 returning *;
-                                  QUERY PLAN                                  
-------------------------------------------------------------------------------
+                                        QUERY PLAN                                        
+------------------------------------------------------------------------------------------
  Update on public.bar
-   Output: bar.f1, bar.f2
-   Update on public.bar
-   Foreign Update on public.bar2 bar_1
-   ->  Seq Scan on public.bar
-         Output: (bar.f2 + 100), bar.ctid
-   ->  Foreign Update on public.bar2 bar_1
-         Remote SQL: UPDATE public.loct2 SET f2 = (f2 + 100) RETURNING f1, f2
-(8 rows)
+   Output: bar_1.f1, bar_1.f2
+   Update on public.bar bar_1
+   Foreign Update on public.bar2 bar_2
+   ->  Result
+         Output: (bar.f2 + 100), bar.ctid, (0), bar.*
+         ->  Append
+               ->  Seq Scan on public.bar bar_1
+                     Output: bar_1.f2, bar_1.ctid, 0, bar_1.*
+               ->  Foreign Update on public.bar2 bar_2
+                     Remote SQL: UPDATE public.loct2 SET f2 = (f2 + 100) RETURNING f1, f2
+(11 rows)
 
 update bar set f2 = f2 + 100 returning *;
  f1 | f2  
@@ -7547,15 +7522,18 @@ update bar set f2 = f2 + 100;
                                                QUERY PLAN                                               
 --------------------------------------------------------------------------------------------------------
  Update on public.bar
-   Update on public.bar
-   Foreign Update on public.bar2 bar_1
+   Update on public.bar bar_1
+   Foreign Update on public.bar2 bar_2
      Remote SQL: UPDATE public.loct2 SET f1 = $2, f2 = $3, f3 = $4 WHERE ctid = $1 RETURNING f1, f2, f3
-   ->  Seq Scan on public.bar
-         Output: (bar.f2 + 100), bar.ctid
-   ->  Foreign Scan on public.bar2 bar_1
-         Output: (bar_1.f2 + 100), bar_1.ctid, bar_1.*
-         Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
-(9 rows)
+   ->  Result
+         Output: (bar.f2 + 100), bar.ctid, (0), bar.*
+         ->  Append
+               ->  Seq Scan on public.bar bar_1
+                     Output: bar_1.f2, bar_1.ctid, 0, bar_1.*
+               ->  Foreign Scan on public.bar2 bar_2
+                     Output: bar_2.f2, bar_2.ctid, 1, bar_2.*
+                     Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 FOR UPDATE
+(12 rows)
 
 update bar set f2 = f2 + 100;
 NOTICE:  trig_row_before(23, skidoo) BEFORE ROW UPDATE ON bar2
@@ -7572,19 +7550,20 @@ NOTICE:  trig_row_after(23, skidoo) AFTER ROW UPDATE ON bar2
 NOTICE:  OLD: (7,277,77),NEW: (7,377,77)
 explain (verbose, costs off)
 delete from bar where f2 < 400;
-                                         QUERY PLAN                                          
----------------------------------------------------------------------------------------------
+                                            QUERY PLAN                                             
+---------------------------------------------------------------------------------------------------
  Delete on public.bar
-   Delete on public.bar
-   Foreign Delete on public.bar2 bar_1
+   Delete on public.bar bar_1
+   Foreign Delete on public.bar2 bar_2
      Remote SQL: DELETE FROM public.loct2 WHERE ctid = $1 RETURNING f1, f2, f3
-   ->  Seq Scan on public.bar
-         Output: bar.ctid
-         Filter: (bar.f2 < 400)
-   ->  Foreign Scan on public.bar2 bar_1
-         Output: bar_1.ctid, bar_1.*
-         Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 WHERE ((f2 < 400)) FOR UPDATE
-(10 rows)
+   ->  Append
+         ->  Seq Scan on public.bar bar_1
+               Output: bar_1.ctid, 0, bar_1.*
+               Filter: (bar_1.f2 < 400)
+         ->  Foreign Scan on public.bar2 bar_2
+               Output: bar_2.ctid, 1, bar_2.*
+               Remote SQL: SELECT f1, f2, f3, ctid FROM public.loct2 WHERE ((f2 < 400)) FOR UPDATE
+(11 rows)
 
 delete from bar where f2 < 400;
 NOTICE:  trig_row_before(23, skidoo) BEFORE ROW DELETE ON bar2
@@ -7615,23 +7594,28 @@ analyze remt1;
 analyze remt2;
 explain (verbose, costs off)
 update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a returning *;
-                                                                  QUERY PLAN                                                                   
------------------------------------------------------------------------------------------------------------------------------------------------
+                                          QUERY PLAN                                          
+----------------------------------------------------------------------------------------------
  Update on public.parent
-   Output: parent.a, parent.b, remt2.a, remt2.b
-   Update on public.parent
-   Foreign Update on public.remt1 parent_1
+   Output: parent_1.a, parent_1.b, remt2.a, remt2.b
+   Update on public.parent parent_1
+   Foreign Update on public.remt1 parent_2
+     Remote SQL: UPDATE public.loct1 SET b = $2 WHERE ctid = $1 RETURNING a, b
    ->  Nested Loop
-         Output: (parent.b || remt2.b), parent.ctid, remt2.*, remt2.a, remt2.b
+         Output: (parent.b || remt2.b), parent.ctid, remt2.*, remt2.a, remt2.b, (0), parent.*
          Join Filter: (parent.a = remt2.a)
-         ->  Seq Scan on public.parent
-               Output: parent.b, parent.ctid, parent.a
-         ->  Foreign Scan on public.remt2
+         ->  Append
+               ->  Seq Scan on public.parent parent_1
+                     Output: parent_1.b, parent_1.ctid, parent_1.a, 0, parent_1.*
+               ->  Foreign Scan on public.remt1 parent_2
+                     Output: parent_2.b, parent_2.ctid, parent_2.a, 1, parent_2.*
+                     Remote SQL: SELECT a, b, ctid FROM public.loct1 FOR UPDATE
+         ->  Materialize
                Output: remt2.b, remt2.*, remt2.a
-               Remote SQL: SELECT a, b FROM public.loct2
-   ->  Foreign Update
-         Remote SQL: UPDATE public.loct1 r4 SET b = (r4.b || r2.b) FROM public.loct2 r2 WHERE ((r4.a = r2.a)) RETURNING r4.a, r4.b, r2.a, r2.b
-(14 rows)
+               ->  Foreign Scan on public.remt2
+                     Output: remt2.b, remt2.*, remt2.a
+                     Remote SQL: SELECT a, b FROM public.loct2
+(19 rows)
 
 update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a returning *;
  a |   b    | a |  b  
@@ -7642,23 +7626,28 @@ update parent set b = parent.b || remt2.b from remt2 where parent.a = remt2.a re
 
 explain (verbose, costs off)
 delete from parent using remt2 where parent.a = remt2.a returning parent;
-                                                    QUERY PLAN                                                    
-------------------------------------------------------------------------------------------------------------------
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
  Delete on public.parent
-   Output: parent.*
-   Delete on public.parent
-   Foreign Delete on public.remt1 parent_1
+   Output: parent_1.*
+   Delete on public.parent parent_1
+   Foreign Delete on public.remt1 parent_2
+     Remote SQL: DELETE FROM public.loct1 WHERE ctid = $1 RETURNING a, b
    ->  Nested Loop
-         Output: parent.ctid, remt2.*
+         Output: parent.ctid, remt2.*, (0)
          Join Filter: (parent.a = remt2.a)
-         ->  Seq Scan on public.parent
-               Output: parent.ctid, parent.a
-         ->  Foreign Scan on public.remt2
+         ->  Append
+               ->  Seq Scan on public.parent parent_1
+                     Output: parent_1.ctid, parent_1.a, 0
+               ->  Foreign Scan on public.remt1 parent_2
+                     Output: parent_2.ctid, parent_2.a, 1
+                     Remote SQL: SELECT a, ctid FROM public.loct1 FOR UPDATE
+         ->  Materialize
                Output: remt2.*, remt2.a
-               Remote SQL: SELECT a, b FROM public.loct2
-   ->  Foreign Delete
-         Remote SQL: DELETE FROM public.loct1 r4 USING public.loct2 r2 WHERE ((r4.a = r2.a)) RETURNING r4.a, r4.b
-(14 rows)
+               ->  Foreign Scan on public.remt2
+                     Output: remt2.*, remt2.a
+                     Remote SQL: SELECT a, b FROM public.loct2
+(19 rows)
 
 delete from parent using remt2 where parent.a = remt2.a returning parent;
    parent   
@@ -7810,13 +7799,11 @@ create table locp (a int check (a in (2)), b text);
 alter table utrtest attach partition remp for values in (1);
 alter table utrtest attach partition locp for values in (2);
 insert into utrtest values (1, 'foo');
-insert into utrtest values (2, 'qux');
 select tableoid::regclass, * FROM utrtest;
  tableoid | a |  b  
 ----------+---+-----
  remp     | 1 | foo
- locp     | 2 | qux
-(2 rows)
+(1 row)
 
 select tableoid::regclass, * FROM remp;
  tableoid | a |  b  
@@ -7825,18 +7812,21 @@ select tableoid::regclass, * FROM remp;
 (1 row)
 
 select tableoid::regclass, * FROM locp;
- tableoid | a |  b  
-----------+---+-----
- locp     | 2 | qux
-(1 row)
+ tableoid | a | b 
+----------+---+---
+(0 rows)
 
 -- It's not allowed to move a row from a partition that is foreign to another
 update utrtest set a = 2 where b = 'foo' returning *;
 ERROR:  new row for relation "loct" violates check constraint "loct_a_check"
 DETAIL:  Failing row contains (2, foo).
 CONTEXT:  remote SQL command: UPDATE public.loct SET a = 2 WHERE ((b = 'foo'::text)) RETURNING a, b
--- But the reverse is allowed
+-- But the reverse is allowed provided the target foreign partition is itself
+-- not an UPDATE target
+insert into utrtest values (2, 'qux');
 update utrtest set a = 1 where b = 'qux' returning *;
+ERROR:  cannot route tuples into foreign table to be updated "remp"
+update utrtest set a = 1 where a = 2 returning *;
  a |  b  
 ---+-----
  1 | qux
@@ -7868,32 +7858,6 @@ create trigger loct_br_insert_trigger before insert on loct
 	for each row execute procedure br_insert_trigfunc();
 delete from utrtest;
 insert into utrtest values (2, 'qux');
--- Check case where the foreign partition is a subplan target rel
-explain (verbose, costs off)
-update utrtest set a = 1 where a = 1 or a = 2 returning *;
-                                          QUERY PLAN                                          
-----------------------------------------------------------------------------------------------
- Update on public.utrtest
-   Output: utrtest_1.a, utrtest_1.b
-   Foreign Update on public.remp utrtest_1
-   Update on public.locp utrtest_2
-   ->  Foreign Update on public.remp utrtest_1
-         Remote SQL: UPDATE public.loct SET a = 1 WHERE (((a = 1) OR (a = 2))) RETURNING a, b
-   ->  Seq Scan on public.locp utrtest_2
-         Output: 1, utrtest_2.ctid
-         Filter: ((utrtest_2.a = 1) OR (utrtest_2.a = 2))
-(9 rows)
-
--- The new values are concatenated with ' triggered !'
-update utrtest set a = 1 where a = 1 or a = 2 returning *;
- a |        b        
----+-----------------
- 1 | qux triggered !
-(1 row)
-
-delete from utrtest;
-insert into utrtest values (2, 'qux');
--- Check case where the foreign partition isn't a subplan target rel
 explain (verbose, costs off)
 update utrtest set a = 1 where a = 2 returning *;
                QUERY PLAN                
@@ -7902,7 +7866,7 @@ update utrtest set a = 1 where a = 2 returning *;
    Output: utrtest_1.a, utrtest_1.b
    Update on public.locp utrtest_1
    ->  Seq Scan on public.locp utrtest_1
-         Output: 1, utrtest_1.ctid
+         Output: 1, utrtest_1.ctid, 0
          Filter: (utrtest_1.a = 2)
 (6 rows)
 
@@ -7914,132 +7878,6 @@ update utrtest set a = 1 where a = 2 returning *;
 (1 row)
 
 drop trigger loct_br_insert_trigger on loct;
--- We can move rows to a foreign partition that has been updated already,
--- but can't move rows to a foreign partition that hasn't been updated yet
-delete from utrtest;
-insert into utrtest values (1, 'foo');
-insert into utrtest values (2, 'qux');
--- Test the former case:
--- with a direct modification plan
-explain (verbose, costs off)
-update utrtest set a = 1 returning *;
-                           QUERY PLAN                            
------------------------------------------------------------------
- Update on public.utrtest
-   Output: utrtest_1.a, utrtest_1.b
-   Foreign Update on public.remp utrtest_1
-   Update on public.locp utrtest_2
-   ->  Foreign Update on public.remp utrtest_1
-         Remote SQL: UPDATE public.loct SET a = 1 RETURNING a, b
-   ->  Seq Scan on public.locp utrtest_2
-         Output: 1, utrtest_2.ctid
-(8 rows)
-
-update utrtest set a = 1 returning *;
- a |  b  
----+-----
- 1 | foo
- 1 | qux
-(2 rows)
-
-delete from utrtest;
-insert into utrtest values (1, 'foo');
-insert into utrtest values (2, 'qux');
--- with a non-direct modification plan
-explain (verbose, costs off)
-update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *;
-                                    QUERY PLAN                                    
-----------------------------------------------------------------------------------
- Update on public.utrtest
-   Output: utrtest_1.a, utrtest_1.b, "*VALUES*".column1
-   Foreign Update on public.remp utrtest_1
-     Remote SQL: UPDATE public.loct SET a = $2 WHERE ctid = $1 RETURNING a, b
-   Update on public.locp utrtest_2
-   ->  Hash Join
-         Output: 1, utrtest_1.ctid, utrtest_1.*, "*VALUES*".*, "*VALUES*".column1
-         Hash Cond: (utrtest_1.a = "*VALUES*".column1)
-         ->  Foreign Scan on public.remp utrtest_1
-               Output: utrtest_1.ctid, utrtest_1.*, utrtest_1.a
-               Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE
-         ->  Hash
-               Output: "*VALUES*".*, "*VALUES*".column1
-               ->  Values Scan on "*VALUES*"
-                     Output: "*VALUES*".*, "*VALUES*".column1
-   ->  Hash Join
-         Output: 1, utrtest_2.ctid, "*VALUES*".*, "*VALUES*".column1
-         Hash Cond: (utrtest_2.a = "*VALUES*".column1)
-         ->  Seq Scan on public.locp utrtest_2
-               Output: utrtest_2.ctid, utrtest_2.a
-         ->  Hash
-               Output: "*VALUES*".*, "*VALUES*".column1
-               ->  Values Scan on "*VALUES*"
-                     Output: "*VALUES*".*, "*VALUES*".column1
-(24 rows)
-
-update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *;
-ERROR:  invalid attribute number 5
--- Change the definition of utrtest so that the foreign partition get updated
--- after the local partition
-delete from utrtest;
-alter table utrtest detach partition remp;
-drop foreign table remp;
-alter table loct drop constraint loct_a_check;
-alter table loct add check (a in (3));
-create foreign table remp (a int check (a in (3)), b text) server loopback options (table_name 'loct');
-alter table utrtest attach partition remp for values in (3);
-insert into utrtest values (2, 'qux');
-insert into utrtest values (3, 'xyzzy');
--- Test the latter case:
--- with a direct modification plan
-explain (verbose, costs off)
-update utrtest set a = 3 returning *;
-                           QUERY PLAN                            
------------------------------------------------------------------
- Update on public.utrtest
-   Output: utrtest_1.a, utrtest_1.b
-   Update on public.locp utrtest_1
-   Foreign Update on public.remp utrtest_2
-   ->  Seq Scan on public.locp utrtest_1
-         Output: 3, utrtest_1.ctid
-   ->  Foreign Update on public.remp utrtest_2
-         Remote SQL: UPDATE public.loct SET a = 3 RETURNING a, b
-(8 rows)
-
-update utrtest set a = 3 returning *; -- ERROR
-ERROR:  cannot route tuples into foreign table to be updated "remp"
--- with a non-direct modification plan
-explain (verbose, costs off)
-update utrtest set a = 3 from (values (2), (3)) s(x) where a = s.x returning *;
-                                    QUERY PLAN                                    
-----------------------------------------------------------------------------------
- Update on public.utrtest
-   Output: utrtest_1.a, utrtest_1.b, "*VALUES*".column1
-   Update on public.locp utrtest_1
-   Foreign Update on public.remp utrtest_2
-     Remote SQL: UPDATE public.loct SET a = $2 WHERE ctid = $1 RETURNING a, b
-   ->  Hash Join
-         Output: 3, utrtest_1.ctid, "*VALUES*".*, "*VALUES*".column1
-         Hash Cond: (utrtest_1.a = "*VALUES*".column1)
-         ->  Seq Scan on public.locp utrtest_1
-               Output: utrtest_1.ctid, utrtest_1.a
-         ->  Hash
-               Output: "*VALUES*".*, "*VALUES*".column1
-               ->  Values Scan on "*VALUES*"
-                     Output: "*VALUES*".*, "*VALUES*".column1
-   ->  Hash Join
-         Output: 3, utrtest_2.ctid, utrtest_2.*, "*VALUES*".*, "*VALUES*".column1
-         Hash Cond: (utrtest_2.a = "*VALUES*".column1)
-         ->  Foreign Scan on public.remp utrtest_2
-               Output: utrtest_2.ctid, utrtest_2.*, utrtest_2.a
-               Remote SQL: SELECT a, b, ctid FROM public.loct FOR UPDATE
-         ->  Hash
-               Output: "*VALUES*".*, "*VALUES*".column1
-               ->  Values Scan on "*VALUES*"
-                     Output: "*VALUES*".*, "*VALUES*".column1
-(24 rows)
-
-update utrtest set a = 3 from (values (2), (3)) s(x) where a = s.x returning *; -- ERROR
-ERROR:  cannot route tuples into foreign table to be updated "remp"
 drop table utrtest;
 drop table loct;
 -- Test copy tuple routing
@@ -9422,7 +9260,7 @@ CREATE TABLE batch_cp_up_test1 PARTITION OF batch_cp_upd_test
 	FOR VALUES IN (2);
 INSERT INTO batch_cp_upd_test VALUES (1), (2);
 -- The following moves a row from the local partition to the foreign one
-UPDATE batch_cp_upd_test t SET a = 1 FROM (VALUES (1), (2)) s(a) WHERE t.a = s.a;
+UPDATE batch_cp_upd_test t SET a = 1 FROM (VALUES (1), (2)) s(a) WHERE t.a = s.a AND t.a = 2;
 SELECT tableoid::regclass, * FROM batch_cp_upd_test;
        tableoid       | a 
 ----------------------+---
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6ba6786c8b..4ed583b35b 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -28,6 +28,7 @@
 #include "nodes/nodeFuncs.h"
 #include "optimizer/clauses.h"
 #include "optimizer/cost.h"
+#include "optimizer/inherit.h"
 #include "optimizer/optimizer.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
@@ -1854,7 +1855,7 @@ postgresBeginForeignModify(ModifyTableState *mtstate,
 									rte,
 									resultRelInfo,
 									mtstate->operation,
-									mtstate->mt_plans[subplan_index]->plan,
+									outerPlanState(mtstate)->plan,
 									query,
 									target_attrs,
 									values_end_len,
@@ -2054,8 +2055,7 @@ postgresBeginForeignInsert(ModifyTableState *mtstate,
 	 */
 	if (plan && plan->operation == CMD_UPDATE &&
 		(resultRelInfo->ri_usesFdwDirectModify ||
-		 resultRelInfo->ri_FdwState) &&
-		resultRelInfo > mtstate->resultRelInfo + mtstate->mt_whichplan)
+		 resultRelInfo->ri_FdwState))
 		ereport(ERROR,
 				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 				 errmsg("cannot route tuples into foreign table to be updated \"%s\"",
@@ -2251,6 +2251,82 @@ postgresRecheckForeignScan(ForeignScanState *node, TupleTableSlot *slot)
 	return true;
 }
 
+/*
+ * modifytable_result_subplan_pushable
+ *		Helper routine for postgresPlanDirectModify to find subplan
+ *		corresponding to subplan_index'th result relation of the given
+ *		ModifyTable node and check if it's pushable, returning true if
+ *		so and setting *subplan_p to thus found subplan
+ *
+ * *subplan_p will be set to NULL if a pushable subplan can't be located.
+ */
+static bool
+modifytable_result_subplan_pushable(PlannerInfo *root,
+									ModifyTable *plan,
+									int subplan_index,
+									Plan **subplan_p)
+{
+	Plan   *subplan = outerPlan(plan);
+
+	/*
+	 * In a non-inherited update, check the top-level plan itself.
+	 */
+	if (IsA(subplan, ForeignScan))
+	{
+		*subplan_p = subplan;
+		return true;
+	}
+
+	/*
+	 * In an inherited update, unless the result relation is joined to another
+	 * relation, the top-level plan would be an Append/MergeAppend with result
+	 * relation subplans underneath, and in some cases even a Result node on
+	 * top of the Append/MergeAppend.  These nodes atop result relation
+	 * subplans can be ignored as no-op as far determining if the subplan can
+	 * be pushed to remote side is concerned, because their job is to for the
+	 * most part passing the tuples fetched from the subplan along to the
+	 * ModifyTable node which performs the actual update/delete operation.
+	 * It's true that Result node isn't entirely no-op, because it is added
+	 * to compute the query's targetlist, but if the targetlist is pushable,
+	 * it can be safely ignored too.
+	 */
+	if (IsA(subplan, Append))
+	{
+		Append	   *appendplan = (Append *) subplan;
+
+		subplan = (Plan *) list_nth(appendplan->appendplans, subplan_index);
+	}
+	else if (IsA(subplan, Result) && IsA(outerPlan(subplan), Append))
+	{
+		Append	   *appendplan = (Append *) outerPlan(subplan);
+
+		subplan = (Plan *) list_nth(appendplan->appendplans, subplan_index);
+	}
+	else if (IsA(subplan, MergeAppend))
+	{
+		MergeAppend	   *maplan = (MergeAppend *) subplan;
+
+		subplan = (Plan *) list_nth(maplan->mergeplans, subplan_index);
+	}
+	else if (IsA(subplan, Result) && IsA(outerPlan(subplan), MergeAppend))
+	{
+		MergeAppend	   *maplan = (MergeAppend *) outerPlan(subplan);
+
+		subplan = (Plan *) list_nth(maplan->mergeplans, subplan_index);
+	}
+
+	if (IsA(subplan, ForeignScan))
+	{
+		*subplan_p = subplan;
+		return true;
+	}
+
+	/* Caller won't use it, but set anyway. */
+	*subplan_p = NULL;
+
+	return false;
+}
+
 /*
  * postgresPlanDirectModify
  *		Consider a direct foreign table modification
@@ -2272,6 +2348,7 @@ postgresPlanDirectModify(PlannerInfo *root,
 	Relation	rel;
 	StringInfoData sql;
 	ForeignScan *fscan;
+	List	   *processed_tlist = NIL;
 	List	   *targetAttrs = NIL;
 	List	   *remote_exprs;
 	List	   *params_list = NIL;
@@ -2289,12 +2366,14 @@ postgresPlanDirectModify(PlannerInfo *root,
 		return false;
 
 	/*
-	 * It's unsafe to modify a foreign table directly if there are any local
-	 * joins needed.
+	 * The following checks if the subplan corresponding to this result
+	 * relation is pushable, if so, returns the ForeignScan node for the
+	 * pushable subplan.
 	 */
-	subplan = (Plan *) list_nth(plan->plans, subplan_index);
-	if (!IsA(subplan, ForeignScan))
+	if (!modifytable_result_subplan_pushable(root, plan, subplan_index,
+											 &subplan))
 		return false;
+	Assert(IsA(subplan, ForeignScan));
 	fscan = (ForeignScan *) subplan;
 
 	/*
@@ -2313,6 +2392,11 @@ postgresPlanDirectModify(PlannerInfo *root,
 	}
 	else
 		foreignrel = root->simple_rel_array[resultRelation];
+
+	/* Sanity check. */
+	if (!bms_is_member(resultRelation, foreignrel->relids))
+		elog(ERROR, "invalid subplan for result relation %u", resultRelation);
+
 	rte = root->simple_rte_array[resultRelation];
 	fpinfo = (PgFdwRelationInfo *) foreignrel->fdw_private;
 
@@ -2325,11 +2409,12 @@ postgresPlanDirectModify(PlannerInfo *root,
 		ListCell *lc, *lc2;
 
 		/*
-		 * The expressions of concern are the first N columns of the subplan
-		 * targetlist, where N is the length of root->update_colnos.
+		 * The expressions of concern are the first N columns of the processed
+		 * targetlist, where N is the length of the rel's update_colnos.
 		 */
-		targetAttrs = root->update_colnos;
-		forboth(lc, subplan->targetlist, lc2, targetAttrs)
+		get_result_update_info(root, resultRelation,
+							   &processed_tlist, &targetAttrs);
+		forboth(lc, processed_tlist, lc2, targetAttrs)
 		{
 			TargetEntry *tle = lfirst_node(TargetEntry, lc);
 			AttrNumber attno = lfirst_int(lc2);
@@ -2392,7 +2477,7 @@ postgresPlanDirectModify(PlannerInfo *root,
 		case CMD_UPDATE:
 			deparseDirectUpdateSql(&sql, root, resultRelation, rel,
 								   foreignrel,
-								   ((Plan *) fscan)->targetlist,
+								   processed_tlist,
 								   targetAttrs,
 								   remote_exprs, &params_list,
 								   returningList, &retrieved_attrs);
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 2b525ea44a..46bf4411f8 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -2079,7 +2079,6 @@ alter table utrtest attach partition remp for values in (1);
 alter table utrtest attach partition locp for values in (2);
 
 insert into utrtest values (1, 'foo');
-insert into utrtest values (2, 'qux');
 
 select tableoid::regclass, * FROM utrtest;
 select tableoid::regclass, * FROM remp;
@@ -2088,8 +2087,11 @@ select tableoid::regclass, * FROM locp;
 -- It's not allowed to move a row from a partition that is foreign to another
 update utrtest set a = 2 where b = 'foo' returning *;
 
--- But the reverse is allowed
+-- But the reverse is allowed provided the target foreign partition is itself
+-- not an UPDATE target
+insert into utrtest values (2, 'qux');
 update utrtest set a = 1 where b = 'qux' returning *;
+update utrtest set a = 1 where a = 2 returning *;
 
 select tableoid::regclass, * FROM utrtest;
 select tableoid::regclass, * FROM remp;
@@ -2104,17 +2106,6 @@ create trigger loct_br_insert_trigger before insert on loct
 
 delete from utrtest;
 insert into utrtest values (2, 'qux');
-
--- Check case where the foreign partition is a subplan target rel
-explain (verbose, costs off)
-update utrtest set a = 1 where a = 1 or a = 2 returning *;
--- The new values are concatenated with ' triggered !'
-update utrtest set a = 1 where a = 1 or a = 2 returning *;
-
-delete from utrtest;
-insert into utrtest values (2, 'qux');
-
--- Check case where the foreign partition isn't a subplan target rel
 explain (verbose, costs off)
 update utrtest set a = 1 where a = 2 returning *;
 -- The new values are concatenated with ' triggered !'
@@ -2122,51 +2113,6 @@ update utrtest set a = 1 where a = 2 returning *;
 
 drop trigger loct_br_insert_trigger on loct;
 
--- We can move rows to a foreign partition that has been updated already,
--- but can't move rows to a foreign partition that hasn't been updated yet
-
-delete from utrtest;
-insert into utrtest values (1, 'foo');
-insert into utrtest values (2, 'qux');
-
--- Test the former case:
--- with a direct modification plan
-explain (verbose, costs off)
-update utrtest set a = 1 returning *;
-update utrtest set a = 1 returning *;
-
-delete from utrtest;
-insert into utrtest values (1, 'foo');
-insert into utrtest values (2, 'qux');
-
--- with a non-direct modification plan
-explain (verbose, costs off)
-update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *;
-update utrtest set a = 1 from (values (1), (2)) s(x) where a = s.x returning *;
-
--- Change the definition of utrtest so that the foreign partition get updated
--- after the local partition
-delete from utrtest;
-alter table utrtest detach partition remp;
-drop foreign table remp;
-alter table loct drop constraint loct_a_check;
-alter table loct add check (a in (3));
-create foreign table remp (a int check (a in (3)), b text) server loopback options (table_name 'loct');
-alter table utrtest attach partition remp for values in (3);
-insert into utrtest values (2, 'qux');
-insert into utrtest values (3, 'xyzzy');
-
--- Test the latter case:
--- with a direct modification plan
-explain (verbose, costs off)
-update utrtest set a = 3 returning *;
-update utrtest set a = 3 returning *; -- ERROR
-
--- with a non-direct modification plan
-explain (verbose, costs off)
-update utrtest set a = 3 from (values (2), (3)) s(x) where a = s.x returning *;
-update utrtest set a = 3 from (values (2), (3)) s(x) where a = s.x returning *; -- ERROR
-
 drop table utrtest;
 drop table loct;
 
@@ -2923,7 +2869,7 @@ CREATE TABLE batch_cp_up_test1 PARTITION OF batch_cp_upd_test
 INSERT INTO batch_cp_upd_test VALUES (1), (2);
 
 -- The following moves a row from the local partition to the foreign one
-UPDATE batch_cp_upd_test t SET a = 1 FROM (VALUES (1), (2)) s(a) WHERE t.a = s.a;
+UPDATE batch_cp_upd_test t SET a = 1 FROM (VALUES (1), (2)) s(a) WHERE t.a = s.a AND t.a = 2;
 SELECT tableoid::regclass, * FROM batch_cp_upd_test;
 
 -- Clean up
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index 6989957d50..351ad4c4c9 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -445,7 +445,9 @@ AddForeignUpdateTargets(Query *parsetree,
      extra values to be fetched.  Each such entry must be marked
      <structfield>resjunk</structfield> = <literal>true</literal>, and must have a distinct
      <structfield>resname</structfield> that will identify it at execution time.
-     Avoid using names matching <literal>ctid<replaceable>N</replaceable></literal>,
+     Avoid using names matching <literal>resultrelindex</literal>,
+     <literal>ctid</literal>,
+     <literal>ctid<replaceable>N</replaceable></literal>,
      <literal>wholerow</literal>, or
      <literal>wholerow<replaceable>N</replaceable></literal>, as the core system can
      generate junk columns of these names.
@@ -495,8 +497,8 @@ PlanForeignModify(PlannerInfo *root,
      <literal>resultRelation</literal> identifies the target foreign table by its
      range table index.  <literal>subplan_index</literal> identifies which target of
      the <structname>ModifyTable</structname> plan node this is, counting from zero;
-     use this if you want to index into <literal>plan-&gt;plans</literal> or other
-     substructure of the <literal>plan</literal> node.
+     use this if you want to index into per-target-relation substructures of the
+     <literal>plan</literal> node.
     </para>
 
     <para>
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 2ed696d429..74dbb709fe 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -666,6 +666,7 @@ CopyFrom(CopyFromState cstate)
 	mtstate->ps.plan = NULL;
 	mtstate->ps.state = estate;
 	mtstate->operation = CMD_INSERT;
+	mtstate->mt_nrels = 1;
 	mtstate->resultRelInfo = resultRelInfo;
 	mtstate->rootResultRelInfo = resultRelInfo;
 
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index afc45429ba..0b1808d503 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -2078,7 +2078,6 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	haschildren = planstate->initPlan ||
 		outerPlanState(planstate) ||
 		innerPlanState(planstate) ||
-		IsA(plan, ModifyTable) ||
 		IsA(plan, Append) ||
 		IsA(plan, MergeAppend) ||
 		IsA(plan, BitmapAnd) ||
@@ -2111,11 +2110,6 @@ ExplainNode(PlanState *planstate, List *ancestors,
 	/* special child plans */
 	switch (nodeTag(plan))
 	{
-		case T_ModifyTable:
-			ExplainMemberNodes(((ModifyTableState *) planstate)->mt_plans,
-							   ((ModifyTableState *) planstate)->mt_nplans,
-							   ancestors, es);
-			break;
 		case T_Append:
 			ExplainMemberNodes(((AppendState *) planstate)->appendplans,
 							   ((AppendState *) planstate)->as_nplans,
@@ -3715,14 +3709,14 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 	}
 
 	/* Should we explicitly label target relations? */
-	labeltargets = (mtstate->mt_nplans > 1 ||
-					(mtstate->mt_nplans == 1 &&
+	labeltargets = (mtstate->mt_nrels > 1 ||
+					(mtstate->mt_nrels == 1 &&
 					 mtstate->resultRelInfo[0].ri_RangeTableIndex != node->nominalRelation));
 
 	if (labeltargets)
 		ExplainOpenGroup("Target Tables", "Target Tables", false, es);
 
-	for (j = 0; j < mtstate->mt_nplans; j++)
+	for (j = 0; j < mtstate->mt_nrels; j++)
 	{
 		ResultRelInfo *resultRelInfo = mtstate->resultRelInfo + j;
 		FdwRoutine *fdwroutine = resultRelInfo->ri_FdwRoutine;
@@ -3817,10 +3811,10 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 			double		insert_path;
 			double		other_path;
 
-			InstrEndLoop(mtstate->mt_plans[0]->instrument);
+			InstrEndLoop(outerPlanState(mtstate)->instrument);
 
 			/* count the number of source rows */
-			total = mtstate->mt_plans[0]->instrument->ntuples;
+			total = outerPlanState(mtstate)->instrument->ntuples;
 			other_path = mtstate->ps.instrument->ntuples2;
 			insert_path = total - other_path;
 
@@ -3836,7 +3830,7 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
 }
 
 /*
- * Explain the constituent plans of a ModifyTable, Append, MergeAppend,
+ * Explain the constituent plans of an Append, MergeAppend,
  * BitmapAnd, or BitmapOr node.
  *
  * The ancestors list should already contain the immediate parent of these
diff --git a/src/backend/executor/README b/src/backend/executor/README
index 18b2ac1865..4958452730 100644
--- a/src/backend/executor/README
+++ b/src/backend/executor/README
@@ -32,10 +32,14 @@ includes a RETURNING clause, the ModifyTable node delivers the computed
 RETURNING rows as output, otherwise it returns nothing.  Handling INSERT
 is pretty straightforward: the tuples returned from the plan tree below
 ModifyTable are inserted into the correct result relation.  For UPDATE,
-the plan tree returns the computed tuples to be updated, plus a "junk"
-(hidden) CTID column identifying which table row is to be replaced by each
-one.  For DELETE, the plan tree need only deliver a CTID column, and the
-ModifyTable node visits each of those rows and marks the row deleted.
+the plan tree returns the new values of the updated columns, plus "junk"
+(hidden) column(s) identifying which table row is to be updated.  The
+ModifyTable node must fetch that row to extract values for the unchanged
+columns, combine the values into a new row, and apply the update.  (For a
+heap table, the row-identity junk column is a CTID, but other things may
+be used for other table types.)  For DELETE, the plan tree need only deliver
+junk row-identity column(s), and the ModifyTable node visits each of those
+rows and marks the row deleted.
 
 XXX a great deal more documentation needs to be written here...
 
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index ea1530e032..163242f54e 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -2416,7 +2416,8 @@ EvalPlanQualInit(EPQState *epqstate, EState *parentestate,
 /*
  * EvalPlanQualSetPlan -- set or change subplan of an EPQState.
  *
- * We need this so that ModifyTable can deal with multiple subplans.
+ * We used to need this so that ModifyTable could deal with multiple subplans.
+ * It could now be refactored out of existence.
  */
 void
 EvalPlanQualSetPlan(EPQState *epqstate, Plan *subplan, List *auxrowmarks)
diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index 619aaffae4..558060e080 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -82,7 +82,7 @@
  *
  * subplan_resultrel_htab
  *		Hash table to store subplan ResultRelInfos by Oid.  This is used to
- *		cache ResultRelInfos from subplans of an UPDATE ModifyTable node;
+ *		cache ResultRelInfos from targets of an UPDATE ModifyTable node;
  *		NULL in other cases.  Some of these may be useful for tuple routing
  *		to save having to build duplicates.
  *
@@ -527,12 +527,12 @@ ExecHashSubPlanResultRelsByOid(ModifyTableState *mtstate,
 	ctl.entrysize = sizeof(SubplanResultRelHashElem);
 	ctl.hcxt = CurrentMemoryContext;
 
-	htab = hash_create("PartitionTupleRouting table", mtstate->mt_nplans,
+	htab = hash_create("PartitionTupleRouting table", mtstate->mt_nrels,
 					   &ctl, HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
 	proute->subplan_resultrel_htab = htab;
 
 	/* Hash all subplans by their Oid */
-	for (i = 0; i < mtstate->mt_nplans; i++)
+	for (i = 0; i < mtstate->mt_nrels; i++)
 	{
 		ResultRelInfo *rri = &mtstate->resultRelInfo[i];
 		bool		found;
@@ -628,10 +628,10 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 		 */
 		Assert((node->operation == CMD_INSERT &&
 				list_length(node->withCheckOptionLists) == 1 &&
-				list_length(node->plans) == 1) ||
+				list_length(node->resultRelations) == 1) ||
 			   (node->operation == CMD_UPDATE &&
 				list_length(node->withCheckOptionLists) ==
-				list_length(node->plans)));
+				list_length(node->resultRelations)));
 
 		/*
 		 * Use the WCO list of the first plan as a reference to calculate
@@ -687,10 +687,10 @@ ExecInitPartitionInfo(ModifyTableState *mtstate, EState *estate,
 		/* See the comment above for WCO lists. */
 		Assert((node->operation == CMD_INSERT &&
 				list_length(node->returningLists) == 1 &&
-				list_length(node->plans) == 1) ||
+				list_length(node->resultRelations) == 1) ||
 			   (node->operation == CMD_UPDATE &&
 				list_length(node->returningLists) ==
-				list_length(node->plans)));
+				list_length(node->resultRelations)));
 
 		/*
 		 * Use the RETURNING list of the first plan as a reference to
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index b9064bfe66..3042d14747 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -19,14 +19,10 @@
  *		ExecReScanModifyTable - rescan the ModifyTable node
  *
  *	 NOTES
- *		Each ModifyTable node contains a list of one or more subplans,
- *		much like an Append node.  There is one subplan per result relation.
- *		The key reason for this is that in an inherited UPDATE command, each
- *		result relation could have a different schema (more or different
- *		columns) requiring a different plan tree to produce it.  In an
- *		inherited DELETE, all the subplans should produce the same output
- *		rowtype, but we might still find that different plans are appropriate
- *		for different child relations.
+ *		The ModifyTable node receives input from its outerPlan, which is
+ *		the data to insert for INSERT cases, or the changed columns' new
+ *		values plus row-locating info for UPDATE cases, or just the
+ *		row-locating info for DELETE cases.
  *
  *		If the query specifies RETURNING, then the ModifyTable returns a
  *		RETURNING tuple after completing each row insert, update, or delete.
@@ -372,10 +368,8 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo,
 /*
  * ExecGetInsertNewTuple
  *		This prepares a "new" tuple ready to be inserted into given result
- *		relation by removing any junk columns of the plan's output tuple.
- *
- * Note: currently, this is really dead code, because INSERT cases don't
- * receive any junk columns so there's never a projection to be done.
+ *		relation, by removing any junk columns of the plan's output tuple
+ *		and (if necessary) coercing the tuple to the right tuple format.
  */
 static TupleTableSlot *
 ExecGetInsertNewTuple(ResultRelInfo *relinfo,
@@ -384,9 +378,29 @@ ExecGetInsertNewTuple(ResultRelInfo *relinfo,
 	ProjectionInfo *newProj = relinfo->ri_projectNew;
 	ExprContext   *econtext;
 
+	/*
+	 * If there's no projection to be done, just make sure the slot is of the
+	 * right type for the target rel.  If the planSlot is the right type we
+	 * can use it as-is, else copy the data into ri_newTupleSlot.
+	 */
 	if (newProj == NULL)
-		return planSlot;
+	{
+		if (relinfo->ri_newTupleSlot->tts_ops != planSlot->tts_ops)
+		{
+			ExecCopySlot(relinfo->ri_newTupleSlot, planSlot);
+			return relinfo->ri_newTupleSlot;
+		}
+		else
+			return planSlot;
+	}
 
+	/*
+	 * Else project; since the projection output slot is ri_newTupleSlot,
+	 * this will also fix any slot-type problem.
+	 *
+	 * Note: currently, this is dead code, because INSERT cases don't receive
+	 * any junk columns so there's never a projection to be done.
+	 */
 	econtext = newProj->pi_exprContext;
 	econtext->ecxt_outertuple = planSlot;
 	return ExecProject(newProj);
@@ -396,8 +410,10 @@ ExecGetInsertNewTuple(ResultRelInfo *relinfo,
  * ExecGetUpdateNewTuple
  *		This prepares a "new" tuple by combining an UPDATE subplan's output
  *		tuple (which contains values of changed columns) with unchanged
- *		columns taken from the old tuple.  The subplan tuple might also
- *		contain junk columns, which are ignored.
+ *		columns taken from the old tuple.
+ *
+ * The subplan tuple might also contain junk columns, which are ignored.
+ * Note that the projection also ensures we have a slot of the right type.
  */
 TupleTableSlot *
 ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
@@ -407,7 +423,6 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo,
 	ProjectionInfo *newProj = relinfo->ri_projectNew;
 	ExprContext   *econtext;
 
-	Assert(newProj != NULL);
 	Assert(planSlot != NULL && !TTS_EMPTY(planSlot));
 	Assert(oldSlot != NULL && !TTS_EMPTY(oldSlot));
 
@@ -1249,9 +1264,7 @@ static bool
 ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 						 ResultRelInfo *resultRelInfo,
 						 ItemPointer tupleid, HeapTuple oldtuple,
-						 TupleTableSlot *slot,
-						 TupleTableSlot *oldSlot,
-						 TupleTableSlot *planSlot,
+						 TupleTableSlot *slot, TupleTableSlot *planSlot,
 						 EPQState *epqstate, bool canSetTag,
 						 TupleTableSlot **retry_slot,
 						 TupleTableSlot **inserted_tuple)
@@ -1327,7 +1340,8 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 		else
 		{
 			/* Fetch the most recent version of old tuple. */
-			ExecClearTuple(oldSlot);
+			TupleTableSlot *oldSlot = resultRelInfo->ri_oldTupleSlot;
+
 			if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc,
 											   tupleid,
 											   SnapshotAny,
@@ -1340,7 +1354,7 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
 	}
 
 	/*
-	 * resultRelInfo is one of the per-subplan resultRelInfos.  So we should
+	 * resultRelInfo is one of the per-relation resultRelInfos.  So we should
 	 * convert the tuple into root's tuple descriptor if needed, since
 	 * ExecInsert() starts the search from root.
 	 */
@@ -1384,10 +1398,10 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate,
  *		foreign table triggers; it is NULL when the foreign table has
  *		no relevant triggers.
  *
- *		slot contains the new tuple value to be stored, while oldSlot
- *		contains the old tuple being replaced.  planSlot is the output
- *		of the ModifyTable's subplan; we use it to access values from
- *		other input tables (for RETURNING), row-ID junk columns, etc.
+ *		slot contains the new tuple value to be stored.
+ *		planSlot is the output of the ModifyTable's subplan; we use it
+ *		to access values from other input tables (for RETURNING),
+ *		row-ID junk columns, etc.
  *
  *		Returns RETURNING result if any, otherwise NULL.
  * ----------------------------------------------------------------
@@ -1398,7 +1412,6 @@ ExecUpdate(ModifyTableState *mtstate,
 		   ItemPointer tupleid,
 		   HeapTuple oldtuple,
 		   TupleTableSlot *slot,
-		   TupleTableSlot *oldSlot,
 		   TupleTableSlot *planSlot,
 		   EPQState *epqstate,
 		   EState *estate,
@@ -1536,8 +1549,8 @@ lreplace:;
 			 * the tuple we're trying to move has been concurrently updated.
 			 */
 			retry = !ExecCrossPartitionUpdate(mtstate, resultRelInfo, tupleid,
-											  oldtuple, slot, oldSlot,
-											  planSlot, epqstate, canSetTag,
+											  oldtuple, slot, planSlot,
+											  epqstate, canSetTag,
 											  &retry_slot, &inserted_tuple);
 			if (retry)
 			{
@@ -1616,6 +1629,7 @@ lreplace:;
 				{
 					TupleTableSlot *inputslot;
 					TupleTableSlot *epqslot;
+					TupleTableSlot *oldSlot;
 
 					if (IsolationUsesXactSnapshot())
 						ereport(ERROR,
@@ -1650,7 +1664,7 @@ lreplace:;
 								return NULL;
 
 							/* Fetch the most recent version of old tuple. */
-							ExecClearTuple(oldSlot);
+							oldSlot = resultRelInfo->ri_oldTupleSlot;
 							if (!table_tuple_fetch_row_version(resultRelationDesc,
 															   tupleid,
 															   SnapshotAny,
@@ -1953,7 +1967,7 @@ ExecOnConflictUpdate(ModifyTableState *mtstate,
 	/* Execute UPDATE with projection */
 	*returning = ExecUpdate(mtstate, resultRelInfo, conflictTid, NULL,
 							resultRelInfo->ri_onConflict->oc_ProjSlot,
-							existing, planSlot,
+							planSlot,
 							&mtstate->mt_epqstate, mtstate->ps.state,
 							canSetTag);
 
@@ -2132,6 +2146,7 @@ ExecModifyTable(PlanState *pstate)
 	PlanState  *subplanstate;
 	TupleTableSlot *slot;
 	TupleTableSlot *planSlot;
+	TupleTableSlot *oldSlot;
 	ItemPointer tupleid;
 	ItemPointerData tuple_ctid;
 	HeapTupleData oldtupdata;
@@ -2173,11 +2188,11 @@ ExecModifyTable(PlanState *pstate)
 	}
 
 	/* Preload local variables */
-	resultRelInfo = node->resultRelInfo + node->mt_whichplan;
-	subplanstate = node->mt_plans[node->mt_whichplan];
+	resultRelInfo = node->resultRelInfo;
+	subplanstate = outerPlanState(node);
 
 	/*
-	 * Fetch rows from subplan(s), and execute the required table modification
+	 * Fetch rows from subplan, and execute the required table modification
 	 * for each row.
 	 */
 	for (;;)
@@ -2200,29 +2215,28 @@ ExecModifyTable(PlanState *pstate)
 
 		planSlot = ExecProcNode(subplanstate);
 
+		/* No more tuples to process? */
 		if (TupIsNull(planSlot))
-		{
-			/* advance to next subplan if any */
-			node->mt_whichplan++;
-			if (node->mt_whichplan < node->mt_nplans)
-			{
-				resultRelInfo++;
-				subplanstate = node->mt_plans[node->mt_whichplan];
-				EvalPlanQualSetPlan(&node->mt_epqstate, subplanstate->plan,
-									node->mt_arowmarks[node->mt_whichplan]);
-				continue;
-			}
-			else
-				break;
-		}
+			break;
 
 		/*
-		 * Ensure input tuple is the right format for the target relation.
+		 * When there are multiple result relations, each tuple contains a
+		 * junk column that gives the index of the rel from which it came.
+		 * Extract it and select the correct result relation.
 		 */
-		if (node->mt_scans[node->mt_whichplan]->tts_ops != planSlot->tts_ops)
+		if (AttributeNumberIsValid(node->mt_resultIndexAttno))
 		{
-			ExecCopySlot(node->mt_scans[node->mt_whichplan], planSlot);
-			planSlot = node->mt_scans[node->mt_whichplan];
+			Datum	datum;
+			bool	isNull;
+			int			resultindex;
+
+			datum = ExecGetJunkAttribute(planSlot, node->mt_resultIndexAttno,
+										 &isNull);
+			if (isNull)
+				elog(ERROR, "resultrelindex is NULL");
+			resultindex = DatumGetInt32(datum);
+			Assert(resultindex >= 0 && resultindex < node->mt_nrels);
+			resultRelInfo = node->resultRelInfo + resultindex;
 		}
 
 		/*
@@ -2333,39 +2347,34 @@ ExecModifyTable(PlanState *pstate)
 								  estate, node->canSetTag);
 				break;
 			case CMD_UPDATE:
+				/*
+				 * Make the new tuple by combining plan's output tuple with
+				 * the old tuple being updated.
+				 */
+				oldSlot = resultRelInfo->ri_oldTupleSlot;
+				if (oldtuple != NULL)
 				{
-					TupleTableSlot *oldSlot = resultRelInfo->ri_oldTupleSlot;
-
-					/*
-					 * Make the new tuple by combining plan's output tuple
-					 * with the old tuple being updated.
-					 */
-					ExecClearTuple(oldSlot);
-					if (oldtuple != NULL)
-					{
-						/* Foreign table update, store the wholerow attr. */
-						ExecForceStoreHeapTuple(oldtuple, oldSlot, false);
-					}
-					else
-					{
-						/* Fetch the most recent version of old tuple. */
-						Relation	relation = resultRelInfo->ri_RelationDesc;
-
-						Assert(tupleid != NULL);
-						if (!table_tuple_fetch_row_version(relation, tupleid,
-														   SnapshotAny,
-														   oldSlot))
-							elog(ERROR, "failed to fetch tuple being updated");
-					}
-					slot = ExecGetUpdateNewTuple(resultRelInfo, planSlot,
-												 oldSlot);
-
-					/* Now apply the update. */
-					slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple,
-									  slot, oldSlot, planSlot,
-									  &node->mt_epqstate, estate,
-									  node->canSetTag);
+					/* Use the wholerow junk attr as the old tuple. */
+					ExecForceStoreHeapTuple(oldtuple, oldSlot, false);
 				}
+				else
+				{
+					/* Fetch the most recent version of old tuple. */
+					Relation	relation = resultRelInfo->ri_RelationDesc;
+
+					Assert(tupleid != NULL);
+					if (!table_tuple_fetch_row_version(relation, tupleid,
+													   SnapshotAny,
+													   oldSlot))
+						elog(ERROR, "failed to fetch tuple being updated");
+				}
+				slot = ExecGetUpdateNewTuple(resultRelInfo, planSlot,
+											 oldSlot);
+
+				/* Now apply the update. */
+				slot = ExecUpdate(node, resultRelInfo, tupleid, oldtuple, slot,
+								  planSlot, &node->mt_epqstate, estate,
+								  node->canSetTag);
 				break;
 			case CMD_DELETE:
 				slot = ExecDelete(node, resultRelInfo, tupleid, oldtuple,
@@ -2425,12 +2434,12 @@ ModifyTableState *
 ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 {
 	ModifyTableState *mtstate;
+	Plan	   *subplan = outerPlan(node);
 	CmdType		operation = node->operation;
-	int			nplans = list_length(node->plans);
+	int			nrels = list_length(node->resultRelations);
 	ResultRelInfo *resultRelInfo;
-	Plan	   *subplan;
-	ListCell   *l,
-			   *l1;
+	List *arowmarks;
+	ListCell   *l;
 	int			i;
 	Relation	rel;
 	bool		update_tuple_routing_needed = node->partColsUpdated;
@@ -2450,10 +2459,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	mtstate->canSetTag = node->canSetTag;
 	mtstate->mt_done = false;
 
-	mtstate->mt_plans = (PlanState **) palloc0(sizeof(PlanState *) * nplans);
+	mtstate->mt_nrels = nrels;
 	mtstate->resultRelInfo = (ResultRelInfo *)
-		palloc(nplans * sizeof(ResultRelInfo));
-	mtstate->mt_scans = (TupleTableSlot **) palloc0(sizeof(TupleTableSlot *) * nplans);
+		palloc(nrels * sizeof(ResultRelInfo));
 
 	/*----------
 	 * Resolve the target relation. This is the same as:
@@ -2482,9 +2490,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 							   linitial_int(node->resultRelations));
 	}
 
-	mtstate->mt_arowmarks = (List **) palloc0(sizeof(List *) * nplans);
-	mtstate->mt_nplans = nplans;
-
 	/* set up epqstate with dummy subplan data for the moment */
 	EvalPlanQualInit(&mtstate->mt_epqstate, estate, NULL, NIL, node->epqParam);
 	mtstate->fireBSTriggers = true;
@@ -2497,23 +2502,17 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		ExecSetupTransitionCaptureState(mtstate, estate);
 
 	/*
-	 * call ExecInitNode on each of the plans to be executed and save the
-	 * results into the array "mt_plans".  This is also a convenient place to
-	 * verify that the proposed target relations are valid and open their
-	 * indexes for insertion of new index entries.
+	 * Open all the result relations and initialize the ResultRelInfo structs.
+	 * (But root relation was initialized above, if it's part of the array.)
+	 * We must do this before initializing the subplan, because direct-modify
+	 * FDWs expect their ResultRelInfos to be available.
 	 */
 	resultRelInfo = mtstate->resultRelInfo;
 	i = 0;
-	forboth(l, node->resultRelations, l1, node->plans)
+	foreach(l, node->resultRelations)
 	{
 		Index		resultRelation = lfirst_int(l);
 
-		subplan = (Plan *) lfirst(l1);
-
-		/*
-		 * This opens result relation and fills ResultRelInfo. (root relation
-		 * was initialized already.)
-		 */
 		if (resultRelInfo != mtstate->rootResultRelInfo)
 			ExecInitResultRelation(estate, resultRelInfo, resultRelation);
 
@@ -2526,6 +2525,22 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		 */
 		CheckValidResultRel(resultRelInfo, operation);
 
+		resultRelInfo++;
+		i++;
+	}
+
+	/*
+	 * Now we may initialize the subplan.
+	 */
+	outerPlanState(mtstate) = ExecInitNode(subplan, estate, eflags);
+
+	/*
+	 * Do additional per-result-relation initialization.
+	 */
+	for (i = 0; i < nrels; i++)
+	{
+		resultRelInfo = &mtstate->resultRelInfo[i];
+
 		/*
 		 * If there are indices on the result relation, open them and save
 		 * descriptors in the result relation info, so that we can add new
@@ -2551,12 +2566,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 			operation == CMD_UPDATE)
 			update_tuple_routing_needed = true;
 
-		/* Now init the plan for this result rel */
-		mtstate->mt_plans[i] = ExecInitNode(subplan, estate, eflags);
-		mtstate->mt_scans[i] =
-			ExecInitExtraTupleSlot(mtstate->ps.state, ExecGetResultType(mtstate->mt_plans[i]),
-								   table_slot_callbacks(resultRelInfo->ri_RelationDesc));
-
 		/* Also let FDWs init themselves for foreign-table result rels */
 		if (!resultRelInfo->ri_usesFdwDirectModify &&
 			resultRelInfo->ri_FdwRoutine != NULL &&
@@ -2588,11 +2597,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 			resultRelInfo->ri_ChildToRootMap =
 				convert_tuples_by_name(RelationGetDescr(resultRelInfo->ri_RelationDesc),
 									   RelationGetDescr(mtstate->rootResultRelInfo->ri_RelationDesc));
-		resultRelInfo++;
-		i++;
 	}
 
-	/* Get the target relation */
+	/* Get the root target relation */
 	rel = mtstate->rootResultRelInfo->ri_RelationDesc;
 
 	/*
@@ -2708,8 +2715,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		TupleDesc	relationDesc;
 		TupleDesc	tupDesc;
 
-		/* insert may only have one plan, inheritance is not expanded */
-		Assert(nplans == 1);
+		/* insert may only have one relation, inheritance is not expanded */
+		Assert(nrels == 1);
 
 		/* already exists if created by RETURNING processing above */
 		if (mtstate->ps.ps_ExprContext == NULL)
@@ -2761,34 +2768,24 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	 * EvalPlanQual mechanism needs to be told about them.  Locate the
 	 * relevant ExecRowMarks.
 	 */
+	arowmarks = NIL;
 	foreach(l, node->rowMarks)
 	{
 		PlanRowMark *rc = lfirst_node(PlanRowMark, l);
 		ExecRowMark *erm;
+		ExecAuxRowMark *aerm;
 
 		/* ignore "parent" rowmarks; they are irrelevant at runtime */
 		if (rc->isParent)
 			continue;
 
-		/* find ExecRowMark (same for all subplans) */
+		/* Find ExecRowMark and build ExecAuxRowMark */
 		erm = ExecFindRowMark(estate, rc->rti, false);
-
-		/* build ExecAuxRowMark for each subplan */
-		for (i = 0; i < nplans; i++)
-		{
-			ExecAuxRowMark *aerm;
-
-			subplan = mtstate->mt_plans[i]->plan;
-			aerm = ExecBuildAuxRowMark(erm, subplan->targetlist);
-			mtstate->mt_arowmarks[i] = lappend(mtstate->mt_arowmarks[i], aerm);
-		}
+		aerm = ExecBuildAuxRowMark(erm, subplan->targetlist);
+		arowmarks = lappend(arowmarks, aerm);
 	}
 
-	/* select first subplan */
-	mtstate->mt_whichplan = 0;
-	subplan = (Plan *) linitial(node->plans);
-	EvalPlanQualSetPlan(&mtstate->mt_epqstate, subplan,
-						mtstate->mt_arowmarks[0]);
+	EvalPlanQualSetPlan(&mtstate->mt_epqstate, subplan, arowmarks);
 
 	/*
 	 * Initialize projection(s) to create tuples suitable for result rel(s).
@@ -2801,15 +2798,14 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	 *
 	 * If there are multiple result relations, each one needs its own
 	 * projection.  Note multiple rels are only possible for UPDATE/DELETE, so
-	 * we can't be fooled by some needing a filter and some not.
+	 * we can't be fooled by some needing a projection and some not.
 	 *
 	 * This section of code is also a convenient place to verify that the
 	 * output of an INSERT or UPDATE matches the target table(s).
 	 */
-	for (i = 0; i < nplans; i++)
+	for (i = 0; i < nrels; i++)
 	{
 		resultRelInfo = &mtstate->resultRelInfo[i];
-		subplan = mtstate->mt_plans[i]->plan;
 
 		/*
 		 * Prepare to generate tuples suitable for the target relation.
@@ -2818,6 +2814,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		{
 			List	   *insertTargetList = NIL;
 			bool		need_projection = false;
+
 			foreach(l, subplan->targetlist)
 			{
 				TargetEntry *tle = (TargetEntry *) lfirst(l);
@@ -2827,14 +2824,24 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 				else
 					need_projection = true;
 			}
+
+			/*
+			 * The junk-free list must produce a tuple suitable for the result
+			 * relation.
+			 */
+			ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
+								insertTargetList);
+
+			/* We'll need a slot matching the table's format. */
+			resultRelInfo->ri_newTupleSlot =
+				table_slot_create(resultRelInfo->ri_RelationDesc,
+								  &mtstate->ps.state->es_tupleTable);
+
+			/* Build ProjectionInfo if needed (it probably isn't). */
 			if (need_projection)
 			{
 				TupleDesc	relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc);
 
-				resultRelInfo->ri_newTupleSlot =
-					table_slot_create(resultRelInfo->ri_RelationDesc,
-									  &mtstate->ps.state->es_tupleTable);
-
 				/* need an expression context to do the projection */
 				if (mtstate->ps.ps_ExprContext == NULL)
 					ExecAssignExprContext(estate, &mtstate->ps);
@@ -2846,13 +2853,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 											&mtstate->ps,
 											relDesc);
 			}
-
-			/*
-			 * The junk-free list must produce a tuple suitable for the result
-			 * relation.
-			 */
-			ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc,
-								insertTargetList);
 		}
 		else if (operation == CMD_UPDATE)
 		{
@@ -2863,7 +2863,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 
 			/*
 			 * For UPDATE, we use the old tuple to fill up missing values in
-			 * the tuple produced by the plan to get the new tuple.
+			 * the tuple produced by the plan to get the new tuple.  We need
+			 * two slots, both matching the table's desired format.
 			 */
 			resultRelInfo->ri_oldTupleSlot =
 				table_slot_create(resultRelInfo->ri_RelationDesc,
@@ -2931,6 +2932,16 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 		}
 	}
 
+	/*
+	 * If this is an inherited update/delete, there will be a junk attribute
+	 * named "resultrelindex" present in the subplan's targetlist.  It will be
+	 * used to identify the result relation for a given tuple to be updated/
+	 * deleted.
+	 */
+	mtstate->mt_resultIndexAttno =
+		ExecFindJunkAttributeInTlist(subplan->targetlist, "resultrelindex");
+	Assert(AttributeNumberIsValid(mtstate->mt_resultIndexAttno) || nrels == 1);
+
 	/*
 	 * Determine if the FDW supports batch insert and determine the batch
 	 * size (a FDW may support batching, but it may be disabled for the
@@ -2942,7 +2953,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
 	if (operation == CMD_INSERT)
 	{
 		resultRelInfo = mtstate->resultRelInfo;
-		for (i = 0; i < nplans; i++)
+		for (i = 0; i < nrels; i++)
 		{
 			if (!resultRelInfo->ri_usesFdwDirectModify &&
 				resultRelInfo->ri_FdwRoutine != NULL &&
@@ -2991,7 +3002,7 @@ ExecEndModifyTable(ModifyTableState *node)
 	/*
 	 * Allow any FDWs to shut down
 	 */
-	for (i = 0; i < node->mt_nplans; i++)
+	for (i = 0; i < node->mt_nrels; i++)
 	{
 		ResultRelInfo *resultRelInfo = node->resultRelInfo + i;
 
@@ -3031,10 +3042,9 @@ ExecEndModifyTable(ModifyTableState *node)
 	EvalPlanQualEnd(&node->mt_epqstate);
 
 	/*
-	 * shut down subplans
+	 * shut down subplan
 	 */
-	for (i = 0; i < node->mt_nplans; i++)
-		ExecEndNode(node->mt_plans[i]);
+	ExecEndNode(outerPlanState(node));
 }
 
 void
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 1ec586729b..832bfb1095 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -206,7 +206,6 @@ _copyModifyTable(const ModifyTable *from)
 	COPY_SCALAR_FIELD(rootRelation);
 	COPY_SCALAR_FIELD(partColsUpdated);
 	COPY_NODE_FIELD(resultRelations);
-	COPY_NODE_FIELD(plans);
 	COPY_NODE_FIELD(updateColnosLists);
 	COPY_NODE_FIELD(withCheckOptionLists);
 	COPY_NODE_FIELD(returningLists);
@@ -2393,6 +2392,7 @@ _copyAppendRelInfo(const AppendRelInfo *from)
 	COPY_SCALAR_FIELD(parent_reltype);
 	COPY_SCALAR_FIELD(child_reltype);
 	COPY_NODE_FIELD(translated_vars);
+	COPY_NODE_FIELD(translated_fake_vars);
 	COPY_SCALAR_FIELD(num_child_cols);
 	COPY_POINTER_FIELD(parent_colnos, from->num_child_cols * sizeof(AttrNumber));
 	COPY_SCALAR_FIELD(parent_reloid);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3292dda342..643a8a73e3 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -907,6 +907,7 @@ _equalAppendRelInfo(const AppendRelInfo *a, const AppendRelInfo *b)
 	COMPARE_SCALAR_FIELD(parent_reltype);
 	COMPARE_SCALAR_FIELD(child_reltype);
 	COMPARE_NODE_FIELD(translated_vars);
+	COMPARE_NODE_FIELD(translated_fake_vars);
 	COMPARE_SCALAR_FIELD(num_child_cols);
 	COMPARE_POINTER_FIELD(parent_colnos, a->num_child_cols * sizeof(AttrNumber));
 	COMPARE_SCALAR_FIELD(parent_reloid);
diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 38226530c6..5f7b2fae27 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -2276,6 +2276,9 @@ expression_tree_walker(Node *node,
 				if (expression_tree_walker((Node *) appinfo->translated_vars,
 										   walker, context))
 					return true;
+				if (expression_tree_walker((Node *) appinfo->translated_fake_vars,
+										   walker, context))
+					return true;
 			}
 			break;
 		case T_PlaceHolderInfo:
@@ -3197,6 +3200,7 @@ expression_tree_mutator(Node *node,
 
 				FLATCOPY(newnode, appinfo, AppendRelInfo);
 				MUTATE(newnode->translated_vars, appinfo->translated_vars, List *);
+				MUTATE(newnode->translated_fake_vars, appinfo->translated_fake_vars, List *);
 				/* Assume nothing need be done with parent_colnos[] */
 				return (Node *) newnode;
 			}
@@ -4002,12 +4006,6 @@ planstate_tree_walker(PlanState *planstate,
 	/* special child plans */
 	switch (nodeTag(plan))
 	{
-		case T_ModifyTable:
-			if (planstate_walk_members(((ModifyTableState *) planstate)->mt_plans,
-									   ((ModifyTableState *) planstate)->mt_nplans,
-									   walker, context))
-				return true;
-			break;
 		case T_Append:
 			if (planstate_walk_members(((AppendState *) planstate)->appendplans,
 									   ((AppendState *) planstate)->as_nplans,
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 99fb38c05a..83adf9d82b 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -407,7 +407,6 @@ _outModifyTable(StringInfo str, const ModifyTable *node)
 	WRITE_UINT_FIELD(rootRelation);
 	WRITE_BOOL_FIELD(partColsUpdated);
 	WRITE_NODE_FIELD(resultRelations);
-	WRITE_NODE_FIELD(plans);
 	WRITE_NODE_FIELD(updateColnosLists);
 	WRITE_NODE_FIELD(withCheckOptionLists);
 	WRITE_NODE_FIELD(returningLists);
@@ -2136,14 +2135,13 @@ _outModifyTablePath(StringInfo str, const ModifyTablePath *node)
 
 	_outPathInfo(str, (const Path *) node);
 
+	WRITE_NODE_FIELD(subpath);
 	WRITE_ENUM_FIELD(operation, CmdType);
 	WRITE_BOOL_FIELD(canSetTag);
 	WRITE_UINT_FIELD(nominalRelation);
 	WRITE_UINT_FIELD(rootRelation);
 	WRITE_BOOL_FIELD(partColsUpdated);
 	WRITE_NODE_FIELD(resultRelations);
-	WRITE_NODE_FIELD(subpaths);
-	WRITE_NODE_FIELD(subroots);
 	WRITE_NODE_FIELD(updateColnosLists);
 	WRITE_NODE_FIELD(withCheckOptionLists);
 	WRITE_NODE_FIELD(returningLists);
@@ -2261,6 +2259,7 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
 	WRITE_NODE_FIELD(full_join_clauses);
 	WRITE_NODE_FIELD(join_info_list);
 	WRITE_NODE_FIELD(append_rel_list);
+	WRITE_NODE_FIELD(inherit_result_rels);
 	WRITE_NODE_FIELD(rowMarks);
 	WRITE_NODE_FIELD(placeholder_list);
 	WRITE_NODE_FIELD(fkey_list);
@@ -2271,6 +2270,7 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
 	WRITE_NODE_FIELD(sort_pathkeys);
 	WRITE_NODE_FIELD(processed_tlist);
 	WRITE_NODE_FIELD(update_colnos);
+	WRITE_NODE_FIELD(inherit_junk_tlist);
 	WRITE_NODE_FIELD(minmax_aggs);
 	WRITE_FLOAT_FIELD(total_table_pages, "%.0f");
 	WRITE_FLOAT_FIELD(tuple_fraction, "%.4f");
@@ -2286,6 +2286,7 @@ _outPlannerInfo(StringInfo str, const PlannerInfo *node)
 	WRITE_BITMAPSET_FIELD(curOuterRels);
 	WRITE_NODE_FIELD(curOuterParams);
 	WRITE_BOOL_FIELD(partColsUpdated);
+	WRITE_INT_FIELD(lastResultRelIndex);
 }
 
 static void
@@ -2568,11 +2569,24 @@ _outAppendRelInfo(StringInfo str, const AppendRelInfo *node)
 	WRITE_OID_FIELD(parent_reltype);
 	WRITE_OID_FIELD(child_reltype);
 	WRITE_NODE_FIELD(translated_vars);
+	WRITE_NODE_FIELD(translated_fake_vars);
 	WRITE_INT_FIELD(num_child_cols);
 	WRITE_ATTRNUMBER_ARRAY(parent_colnos, node->num_child_cols);
 	WRITE_OID_FIELD(parent_reloid);
 }
 
+static void
+_outInheritResultRelInfo(StringInfo str, const InheritResultRelInfo *node)
+{
+	WRITE_NODE_TYPE("INHERITRESULTRELINFO");
+
+	WRITE_UINT_FIELD(resultRelation);
+	WRITE_NODE_FIELD(withCheckOptions);
+	WRITE_NODE_FIELD(returningList);
+	WRITE_NODE_FIELD(processed_tlist);
+	WRITE_NODE_FIELD(update_colnos);
+}
+
 static void
 _outPlaceHolderInfo(StringInfo str, const PlaceHolderInfo *node)
 {
@@ -4222,6 +4236,9 @@ outNode(StringInfo str, const void *obj)
 			case T_AppendRelInfo:
 				_outAppendRelInfo(str, obj);
 				break;
+			case T_InheritResultRelInfo:
+				_outInheritResultRelInfo(str, obj);
+				break;
 			case T_PlaceHolderInfo:
 				_outPlaceHolderInfo(str, obj);
 				break;
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 0b6331d3da..feb64db702 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1407,6 +1407,7 @@ _readAppendRelInfo(void)
 	READ_OID_FIELD(parent_reltype);
 	READ_OID_FIELD(child_reltype);
 	READ_NODE_FIELD(translated_vars);
+	READ_NODE_FIELD(translated_fake_vars);
 	READ_INT_FIELD(num_child_cols);
 	READ_ATTRNUMBER_ARRAY(parent_colnos, local_node->num_child_cols);
 	READ_OID_FIELD(parent_reloid);
@@ -1682,7 +1683,6 @@ _readModifyTable(void)
 	READ_UINT_FIELD(rootRelation);
 	READ_BOOL_FIELD(partColsUpdated);
 	READ_NODE_FIELD(resultRelations);
-	READ_NODE_FIELD(plans);
 	READ_NODE_FIELD(updateColnosLists);
 	READ_NODE_FIELD(withCheckOptionLists);
 	READ_NODE_FIELD(returningLists);
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index d73ac562eb..de765eb709 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -1049,10 +1049,25 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
 			adjust_appendrel_attrs(root,
 								   (Node *) rel->joininfo,
 								   1, &appinfo);
-		childrel->reltarget->exprs = (List *)
-			adjust_appendrel_attrs(root,
-								   (Node *) rel->reltarget->exprs,
-								   1, &appinfo);
+
+		/*
+		 * If the child is a result relation, the executor expects that any
+		 * wholerow Vars in the targetlist are of its reltype, not parent's
+		 * reltype.  So use adjust_target_appendrel_attrs() to translate the
+		 * reltarget expressions, because it does not wrap a translated
+		 * wholerow Var with ConcertRowtypeExpr to convert it back to the
+		 * parent's reltype.
+		 */
+		if (is_result_relation(root, childRTindex))
+			childrel->reltarget->exprs = (List *)
+				adjust_target_appendrel_attrs(root,
+											  (Node *) rel->reltarget->exprs,
+											  appinfo);
+		else
+			childrel->reltarget->exprs = (List *)
+				adjust_appendrel_attrs(root,
+									   (Node *) rel->reltarget->exprs,
+									   1, &appinfo);
 
 		/*
 		 * We have to make child entries in the EquivalenceClass data
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index ff536e6b24..8a65de783c 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -27,6 +27,7 @@
 #include "nodes/nodeFuncs.h"
 #include "nodes/supportnodes.h"
 #include "optimizer/cost.h"
+#include "optimizer/inherit.h"
 #include "optimizer/optimizer.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
@@ -3397,7 +3398,7 @@ check_index_predicates(PlannerInfo *root, RelOptInfo *rel)
 	 * and pass them through to EvalPlanQual via a side channel; but for now,
 	 * we just don't remove implied quals at all for target relations.
 	 */
-	is_target_rel = (rel->relid == root->parse->resultRelation ||
+	is_target_rel = (is_result_relation(root, rel->relid) ||
 					 get_plan_rowmark(root->rowMarks, rel->relid) != NULL);
 
 	/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 4bb482879f..291636b5cf 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -301,7 +301,7 @@ static ModifyTable *make_modifytable(PlannerInfo *root,
 									 CmdType operation, bool canSetTag,
 									 Index nominalRelation, Index rootRelation,
 									 bool partColsUpdated,
-									 List *resultRelations, List *subplans, List *subroots,
+									 List *resultRelations, Plan *subplan,
 									 List *updateColnosLists,
 									 List *withCheckOptionLists, List *returningLists,
 									 List *rowMarks, OnConflictExpr *onconflict, int epqParam);
@@ -2257,7 +2257,6 @@ create_groupingsets_plan(PlannerInfo *root, GroupingSetsPath *best_path)
 	 * create_modifytable_plan).  Fortunately we can't be because there would
 	 * never be grouping in an UPDATE/DELETE; but let's Assert that.
 	 */
-	Assert(root->inhTargetKind == INHKIND_NONE);
 	Assert(root->grouping_map == NULL);
 	root->grouping_map = grouping_map;
 
@@ -2419,12 +2418,7 @@ create_minmaxagg_plan(PlannerInfo *root, MinMaxAggPath *best_path)
 	 * with InitPlan output params.  (We can't just do that locally in the
 	 * MinMaxAgg node, because path nodes above here may have Agg references
 	 * as well.)  Save the mmaggregates list to tell setrefs.c to do that.
-	 *
-	 * This doesn't work if we're in an inheritance subtree (see notes in
-	 * create_modifytable_plan).  Fortunately we can't be because there would
-	 * never be aggregates in an UPDATE/DELETE; but let's Assert that.
 	 */
-	Assert(root->inhTargetKind == INHKIND_NONE);
 	Assert(root->minmax_aggs == NIL);
 	root->minmax_aggs = best_path->mmaggregates;
 
@@ -2641,34 +2635,11 @@ static ModifyTable *
 create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 {
 	ModifyTable *plan;
-	List	   *subplans = NIL;
-	ListCell   *subpaths,
-			   *subroots,
-			   *lc;
-
-	/* Build the plan for each input path */
-	forboth(subpaths, best_path->subpaths,
-			subroots, best_path->subroots)
-	{
-		Path	   *subpath = (Path *) lfirst(subpaths);
-		PlannerInfo *subroot = (PlannerInfo *) lfirst(subroots);
-		Plan	   *subplan;
-
-		/*
-		 * In an inherited UPDATE/DELETE, reference the per-child modified
-		 * subroot while creating Plans from Paths for the child rel.  This is
-		 * a kluge, but otherwise it's too hard to ensure that Plan creation
-		 * functions (particularly in FDWs) don't depend on the contents of
-		 * "root" matching what they saw at Path creation time.  The main
-		 * downside is that creation functions for Plans that might appear
-		 * below a ModifyTable cannot expect to modify the contents of "root"
-		 * and have it "stick" for subsequent processing such as setrefs.c.
-		 * That's not great, but it seems better than the alternative.
-		 */
-		subplan = create_plan_recurse(subroot, subpath, CP_EXACT_TLIST);
+	Path	   *subpath = best_path->subpath;
+	Plan	   *subplan;
 
-		subplans = lappend(subplans, subplan);
-	}
+	/* Build the plan. */
+	subplan = create_plan_recurse(root, subpath, CP_EXACT_TLIST);
 
 	plan = make_modifytable(root,
 							best_path->operation,
@@ -2677,8 +2648,7 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 							best_path->rootRelation,
 							best_path->partColsUpdated,
 							best_path->resultRelations,
-							subplans,
-							best_path->subroots,
+							subplan,
 							best_path->updateColnosLists,
 							best_path->withCheckOptionLists,
 							best_path->returningLists,
@@ -2688,11 +2658,10 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 
 	copy_generic_path_info(&plan->plan, &best_path->path);
 
-	forboth(lc, subplans,
-			subroots, best_path->subroots)
+	if (plan->operation == CMD_UPDATE)
 	{
-		Plan	   *subplan = (Plan *) lfirst(lc);
-		PlannerInfo *subroot = (PlannerInfo *) lfirst(subroots);
+		ListCell   *l;
+		AttrNumber	resno = 1;
 
 		/*
 		 * Fix up the resnos of query's TLEs to make them match their ordinal
@@ -2704,25 +2673,19 @@ create_modifytable_plan(PlannerInfo *root, ModifyTablePath *best_path)
 		 * resnos in processed_tlist and resnos in subplan targetlist are
 		 * exactly same, but maybe we can just remove the assert?
 		 */
-		if (plan->operation == CMD_UPDATE)
+		foreach(l, root->processed_tlist)
 		{
-			ListCell   *l;
-			AttrNumber	resno = 1;
+			TargetEntry *tle = lfirst(l);
 
-			foreach(l, subroot->processed_tlist)
-			{
-				TargetEntry *tle = lfirst(l);
-
-				tle = flatCopyTargetEntry(tle);
-				tle->resno = resno++;
-				lfirst(l) = tle;
-			}
+			tle = flatCopyTargetEntry(tle);
+			tle->resno = resno++;
+			lfirst(l) = tle;
 		}
-
-		/* Transfer resname/resjunk labeling, too, to keep executor happy */
-		apply_tlist_labeling(subplan->targetlist, subroot->processed_tlist);
 	}
 
+	/* Transfer resname/resjunk labeling, too, to keep executor happy */
+	apply_tlist_labeling(subplan->targetlist, root->processed_tlist);
+
 	return plan;
 }
 
@@ -6914,7 +6877,7 @@ make_modifytable(PlannerInfo *root,
 				 CmdType operation, bool canSetTag,
 				 Index nominalRelation, Index rootRelation,
 				 bool partColsUpdated,
-				 List *resultRelations, List *subplans, List *subroots,
+				 List *resultRelations, Plan *subplan,
 				 List *updateColnosLists,
 				 List *withCheckOptionLists, List *returningLists,
 				 List *rowMarks, OnConflictExpr *onconflict, int epqParam)
@@ -6923,11 +6886,8 @@ make_modifytable(PlannerInfo *root,
 	List	   *fdw_private_list;
 	Bitmapset  *direct_modify_plans;
 	ListCell   *lc;
-	ListCell   *lc2;
 	int			i;
 
-	Assert(list_length(resultRelations) == list_length(subplans));
-	Assert(list_length(resultRelations) == list_length(subroots));
 	Assert(operation == CMD_UPDATE ?
 		   list_length(resultRelations) == list_length(updateColnosLists) :
 		   updateColnosLists == NIL);
@@ -6936,7 +6896,7 @@ make_modifytable(PlannerInfo *root,
 	Assert(returningLists == NIL ||
 		   list_length(resultRelations) == list_length(returningLists));
 
-	node->plan.lefttree = NULL;
+	node->plan.lefttree = subplan;
 	node->plan.righttree = NULL;
 	node->plan.qual = NIL;
 	/* setrefs.c will fill in the targetlist, if needed */
@@ -6948,7 +6908,6 @@ make_modifytable(PlannerInfo *root,
 	node->rootRelation = rootRelation;
 	node->partColsUpdated = partColsUpdated;
 	node->resultRelations = resultRelations;
-	node->plans = subplans;
 	if (!onconflict)
 	{
 		node->onConflictAction = ONCONFLICT_NONE;
@@ -6988,10 +6947,9 @@ make_modifytable(PlannerInfo *root,
 	fdw_private_list = NIL;
 	direct_modify_plans = NULL;
 	i = 0;
-	forboth(lc, resultRelations, lc2, subroots)
+	foreach(lc, resultRelations)
 	{
 		Index		rti = lfirst_int(lc);
-		PlannerInfo *subroot = lfirst_node(PlannerInfo, lc2);
 		FdwRoutine *fdwroutine;
 		List	   *fdw_private;
 		bool		direct_modify;
@@ -7003,16 +6961,16 @@ make_modifytable(PlannerInfo *root,
 		 * so it's not a baserel; and there are also corner cases for
 		 * updatable views where the target rel isn't a baserel.)
 		 */
-		if (rti < subroot->simple_rel_array_size &&
-			subroot->simple_rel_array[rti] != NULL)
+		if (rti < root->simple_rel_array_size &&
+			root->simple_rel_array[rti] != NULL)
 		{
-			RelOptInfo *resultRel = subroot->simple_rel_array[rti];
+			RelOptInfo *resultRel = root->simple_rel_array[rti];
 
 			fdwroutine = resultRel->fdwroutine;
 		}
 		else
 		{
-			RangeTblEntry *rte = planner_rt_fetch(rti, subroot);
+			RangeTblEntry *rte = planner_rt_fetch(rti, root);
 
 			Assert(rte->rtekind == RTE_RELATION);
 			if (rte->relkind == RELKIND_FOREIGN_TABLE)
@@ -7035,16 +6993,16 @@ make_modifytable(PlannerInfo *root,
 			fdwroutine->IterateDirectModify != NULL &&
 			fdwroutine->EndDirectModify != NULL &&
 			withCheckOptionLists == NIL &&
-			!has_row_triggers(subroot, rti, operation) &&
-			!has_stored_generated_columns(subroot, rti))
-			direct_modify = fdwroutine->PlanDirectModify(subroot, node, rti, i);
+			!has_row_triggers(root, rti, operation) &&
+			!has_stored_generated_columns(root, rti))
+			direct_modify = fdwroutine->PlanDirectModify(root, node, rti, i);
 		if (direct_modify)
 			direct_modify_plans = bms_add_member(direct_modify_plans, i);
 
 		if (!direct_modify &&
 			fdwroutine != NULL &&
 			fdwroutine->PlanForeignModify != NULL)
-			fdw_private = fdwroutine->PlanForeignModify(subroot, node, rti, i);
+			fdw_private = fdwroutine->PlanForeignModify(root, node, rti, i);
 		else
 			fdw_private = NIL;
 		fdw_private_list = lappend(fdw_private_list, fdw_private);
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index ccb9166a8e..2bd7842b45 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -129,9 +129,7 @@ typedef struct
 /* Local functions */
 static Node *preprocess_expression(PlannerInfo *root, Node *expr, int kind);
 static void preprocess_qual_conditions(PlannerInfo *root, Node *jtnode);
-static void inheritance_planner(PlannerInfo *root);
-static void grouping_planner(PlannerInfo *root, bool inheritance_update,
-							 double tuple_fraction);
+static void grouping_planner(PlannerInfo *root, double tuple_fraction);
 static grouping_sets_data *preprocess_grouping_sets(PlannerInfo *root);
 static List *remap_to_groupclause_idx(List *groupClause, List *gsets,
 									  int *tleref_to_colnum_map);
@@ -616,15 +614,16 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	root->eq_classes = NIL;
 	root->ec_merging_done = false;
 	root->append_rel_list = NIL;
+	root->inherit_result_rels = NIL;
 	root->rowMarks = NIL;
 	memset(root->upper_rels, 0, sizeof(root->upper_rels));
 	memset(root->upper_targets, 0, sizeof(root->upper_targets));
 	root->processed_tlist = NIL;
 	root->update_colnos = NIL;
+	root->inherit_junk_tlist = NIL;
 	root->grouping_map = NULL;
 	root->minmax_aggs = NIL;
 	root->qual_security_level = 0;
-	root->inhTargetKind = INHKIND_NONE;
 	root->hasPseudoConstantQuals = false;
 	root->hasAlternativeSubPlans = false;
 	root->hasRecursion = hasRecursion;
@@ -634,6 +633,8 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 		root->wt_param_id = -1;
 	root->non_recursive_path = NULL;
 	root->partColsUpdated = false;
+	root->inherit_result_rels = NIL;
+	root->lastResultRelIndex = 0;
 
 	/*
 	 * If there is a WITH list, process each WITH query and either convert it
@@ -832,6 +833,7 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	root->append_rel_list = (List *)
 		preprocess_expression(root, (Node *) root->append_rel_list,
 							  EXPRKIND_APPINFO);
+	/* We assume we don't need to preprocess inherit_result_rels contents */
 
 	/* Also need to preprocess expressions within RTEs */
 	foreach(l, parse->rtable)
@@ -999,15 +1001,8 @@ subquery_planner(PlannerGlobal *glob, Query *parse,
 	if (hasResultRTEs)
 		remove_useless_result_rtes(root);
 
-	/*
-	 * Do the main planning.  If we have an inherited target relation, that
-	 * needs special processing, else go straight to grouping_planner.
-	 */
-	if (parse->resultRelation &&
-		rt_fetch(parse->resultRelation, parse->rtable)->inh)
-		inheritance_planner(root);
-	else
-		grouping_planner(root, false, tuple_fraction);
+	/* Do the main planning. */
+	grouping_planner(root, tuple_fraction);
 
 	/*
 	 * Capture the set of outer-level param IDs we have access to, for use in
@@ -1181,631 +1176,6 @@ preprocess_phv_expression(PlannerInfo *root, Expr *expr)
 	return (Expr *) preprocess_expression(root, (Node *) expr, EXPRKIND_PHV);
 }
 
-/*
- * inheritance_planner
- *	  Generate Paths in the case where the result relation is an
- *	  inheritance set.
- *
- * We have to handle this case differently from cases where a source relation
- * is an inheritance set. Source inheritance is expanded at the bottom of the
- * plan tree (see allpaths.c), but target inheritance has to be expanded at
- * the top.  The reason is that for UPDATE, each target relation needs a
- * different targetlist matching its own column set.  Fortunately,
- * the UPDATE/DELETE target can never be the nullable side of an outer join,
- * so it's OK to generate the plan this way.
- *
- * Returns nothing; the useful output is in the Paths we attach to
- * the (UPPERREL_FINAL, NULL) upperrel stored in *root.
- *
- * Note that we have not done set_cheapest() on the final rel; it's convenient
- * to leave this to the caller.
- */
-static void
-inheritance_planner(PlannerInfo *root)
-{
-	Query	   *parse = root->parse;
-	int			top_parentRTindex = parse->resultRelation;
-	List	   *select_rtable;
-	List	   *select_appinfos;
-	List	   *child_appinfos;
-	List	   *old_child_rtis;
-	List	   *new_child_rtis;
-	Bitmapset  *subqueryRTindexes;
-	Index		next_subquery_rti;
-	int			nominalRelation = -1;
-	Index		rootRelation = 0;
-	List	   *final_rtable = NIL;
-	List	   *final_rowmarks = NIL;
-	List	   *final_appendrels = NIL;
-	int			save_rel_array_size = 0;
-	RelOptInfo **save_rel_array = NULL;
-	AppendRelInfo **save_append_rel_array = NULL;
-	List	   *subpaths = NIL;
-	List	   *subroots = NIL;
-	List	   *resultRelations = NIL;
-	List	   *updateColnosLists = NIL;
-	List	   *withCheckOptionLists = NIL;
-	List	   *returningLists = NIL;
-	List	   *rowMarks;
-	RelOptInfo *final_rel;
-	ListCell   *lc;
-	ListCell   *lc2;
-	Index		rti;
-	RangeTblEntry *parent_rte;
-	Bitmapset  *parent_relids;
-	Query	  **parent_parses;
-
-	/* Should only get here for UPDATE or DELETE */
-	Assert(parse->commandType == CMD_UPDATE ||
-		   parse->commandType == CMD_DELETE);
-
-	/*
-	 * We generate a modified instance of the original Query for each target
-	 * relation, plan that, and put all the plans into a list that will be
-	 * controlled by a single ModifyTable node.  All the instances share the
-	 * same rangetable, but each instance must have its own set of subquery
-	 * RTEs within the finished rangetable because (1) they are likely to get
-	 * scribbled on during planning, and (2) it's not inconceivable that
-	 * subqueries could get planned differently in different cases.  We need
-	 * not create duplicate copies of other RTE kinds, in particular not the
-	 * target relations, because they don't have either of those issues.  Not
-	 * having to duplicate the target relations is important because doing so
-	 * (1) would result in a rangetable of length O(N^2) for N targets, with
-	 * at least O(N^3) work expended here; and (2) would greatly complicate
-	 * management of the rowMarks list.
-	 *
-	 * To begin with, generate a bitmapset of the relids of the subquery RTEs.
-	 */
-	subqueryRTindexes = NULL;
-	rti = 1;
-	foreach(lc, parse->rtable)
-	{
-		RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);
-
-		if (rte->rtekind == RTE_SUBQUERY)
-			subqueryRTindexes = bms_add_member(subqueryRTindexes, rti);
-		rti++;
-	}
-
-	/*
-	 * If the parent RTE is a partitioned table, we should use that as the
-	 * nominal target relation, because the RTEs added for partitioned tables
-	 * (including the root parent) as child members of the inheritance set do
-	 * not appear anywhere else in the plan, so the confusion explained below
-	 * for non-partitioning inheritance cases is not possible.
-	 */
-	parent_rte = rt_fetch(top_parentRTindex, parse->rtable);
-	Assert(parent_rte->inh);
-	if (parent_rte->relkind == RELKIND_PARTITIONED_TABLE)
-	{
-		nominalRelation = top_parentRTindex;
-		rootRelation = top_parentRTindex;
-	}
-
-	/*
-	 * Before generating the real per-child-relation plans, do a cycle of
-	 * planning as though the query were a SELECT.  The objective here is to
-	 * find out which child relations need to be processed, using the same
-	 * expansion and pruning logic as for a SELECT.  We'll then pull out the
-	 * RangeTblEntry-s generated for the child rels, and make use of the
-	 * AppendRelInfo entries for them to guide the real planning.  (This is
-	 * rather inefficient; we could perhaps stop short of making a full Path
-	 * tree.  But this whole function is inefficient and slated for
-	 * destruction, so let's not contort query_planner for that.)
-	 */
-	{
-		PlannerInfo *subroot;
-
-		/*
-		 * Flat-copy the PlannerInfo to prevent modification of the original.
-		 */
-		subroot = makeNode(PlannerInfo);
-		memcpy(subroot, root, sizeof(PlannerInfo));
-
-		/*
-		 * Make a deep copy of the parsetree for this planning cycle to mess
-		 * around with, and change it to look like a SELECT.  (Hack alert: the
-		 * target RTE still has updatedCols set if this is an UPDATE, so that
-		 * expand_partitioned_rtentry will correctly update
-		 * subroot->partColsUpdated.)
-		 */
-		subroot->parse = copyObject(root->parse);
-
-		subroot->parse->commandType = CMD_SELECT;
-		subroot->parse->resultRelation = 0;
-
-		/*
-		 * Ensure the subroot has its own copy of the original
-		 * append_rel_list, since it'll be scribbled on.  (Note that at this
-		 * point, the list only contains AppendRelInfos for flattened UNION
-		 * ALL subqueries.)
-		 */
-		subroot->append_rel_list = copyObject(root->append_rel_list);
-
-		/*
-		 * Better make a private copy of the rowMarks, too.
-		 */
-		subroot->rowMarks = copyObject(root->rowMarks);
-
-		/* There shouldn't be any OJ info to translate, as yet */
-		Assert(subroot->join_info_list == NIL);
-		/* and we haven't created PlaceHolderInfos, either */
-		Assert(subroot->placeholder_list == NIL);
-
-		/* Generate Path(s) for accessing this result relation */
-		grouping_planner(subroot, true, 0.0 /* retrieve all tuples */ );
-
-		/* Extract the info we need. */
-		select_rtable = subroot->parse->rtable;
-		select_appinfos = subroot->append_rel_list;
-
-		/*
-		 * We need to propagate partColsUpdated back, too.  (The later
-		 * planning cycles will not set this because they won't run
-		 * expand_partitioned_rtentry for the UPDATE target.)
-		 */
-		root->partColsUpdated = subroot->partColsUpdated;
-	}
-
-	/*----------
-	 * Since only one rangetable can exist in the final plan, we need to make
-	 * sure that it contains all the RTEs needed for any child plan.  This is
-	 * complicated by the need to use separate subquery RTEs for each child.
-	 * We arrange the final rtable as follows:
-	 * 1. All original rtable entries (with their original RT indexes).
-	 * 2. All the relation RTEs generated for children of the target table.
-	 * 3. Subquery RTEs for children after the first.  We need N * (K - 1)
-	 *    RT slots for this, if there are N subqueries and K child tables.
-	 * 4. Additional RTEs generated during the child planning runs, such as
-	 *    children of inheritable RTEs other than the target table.
-	 * We assume that each child planning run will create an identical set
-	 * of type-4 RTEs.
-	 *
-	 * So the next thing to do is append the type-2 RTEs (the target table's
-	 * children) to the original rtable.  We look through select_appinfos
-	 * to find them.
-	 *
-	 * To identify which AppendRelInfos are relevant as we thumb through
-	 * select_appinfos, we need to look for both direct and indirect children
-	 * of top_parentRTindex, so we use a bitmap of known parent relids.
-	 * expand_inherited_rtentry() always processes a parent before any of that
-	 * parent's children, so we should see an intermediate parent before its
-	 * children.
-	 *----------
-	 */
-	child_appinfos = NIL;
-	old_child_rtis = NIL;
-	new_child_rtis = NIL;
-	parent_relids = bms_make_singleton(top_parentRTindex);
-	foreach(lc, select_appinfos)
-	{
-		AppendRelInfo *appinfo = lfirst_node(AppendRelInfo, lc);
-		RangeTblEntry *child_rte;
-
-		/* append_rel_list contains all append rels; ignore others */
-		if (!bms_is_member(appinfo->parent_relid, parent_relids))
-			continue;
-
-		/* remember relevant AppendRelInfos for use below */
-		child_appinfos = lappend(child_appinfos, appinfo);
-
-		/* extract RTE for this child rel */
-		child_rte = rt_fetch(appinfo->child_relid, select_rtable);
-
-		/* and append it to the original rtable */
-		parse->rtable = lappend(parse->rtable, child_rte);
-
-		/* remember child's index in the SELECT rtable */
-		old_child_rtis = lappend_int(old_child_rtis, appinfo->child_relid);
-
-		/* and its new index in the final rtable */
-		new_child_rtis = lappend_int(new_child_rtis, list_length(parse->rtable));
-
-		/* if child is itself partitioned, update parent_relids */
-		if (child_rte->inh)
-		{
-			Assert(child_rte->relkind == RELKIND_PARTITIONED_TABLE);
-			parent_relids = bms_add_member(parent_relids, appinfo->child_relid);
-		}
-	}
-
-	/*
-	 * It's possible that the RTIs we just assigned for the child rels in the
-	 * final rtable are different from what they were in the SELECT query.
-	 * Adjust the AppendRelInfos so that they will correctly map RT indexes to
-	 * the final indexes.  We can do this left-to-right since no child rel's
-	 * final RT index could be greater than what it had in the SELECT query.
-	 */
-	forboth(lc, old_child_rtis, lc2, new_child_rtis)
-	{
-		int			old_child_rti = lfirst_int(lc);
-		int			new_child_rti = lfirst_int(lc2);
-
-		if (old_child_rti == new_child_rti)
-			continue;			/* nothing to do */
-
-		Assert(old_child_rti > new_child_rti);
-
-		ChangeVarNodes((Node *) child_appinfos,
-					   old_child_rti, new_child_rti, 0);
-	}
-
-	/*
-	 * Now set up rangetable entries for subqueries for additional children
-	 * (the first child will just use the original ones).  These all have to
-	 * look more or less real, or EXPLAIN will get unhappy; so we just make
-	 * them all clones of the original subqueries.
-	 */
-	next_subquery_rti = list_length(parse->rtable) + 1;
-	if (subqueryRTindexes != NULL)
-	{
-		int			n_children = list_length(child_appinfos);
-
-		while (n_children-- > 1)
-		{
-			int			oldrti = -1;
-
-			while ((oldrti = bms_next_member(subqueryRTindexes, oldrti)) >= 0)
-			{
-				RangeTblEntry *subqrte;
-
-				subqrte = rt_fetch(oldrti, parse->rtable);
-				parse->rtable = lappend(parse->rtable, copyObject(subqrte));
-			}
-		}
-	}
-
-	/*
-	 * The query for each child is obtained by translating the query for its
-	 * immediate parent, since the AppendRelInfo data we have shows deltas
-	 * between parents and children.  We use the parent_parses array to
-	 * remember the appropriate query trees.  This is indexed by parent relid.
-	 * Since the maximum number of parents is limited by the number of RTEs in
-	 * the SELECT query, we use that number to allocate the array.  An extra
-	 * entry is needed since relids start from 1.
-	 */
-	parent_parses = (Query **) palloc0((list_length(select_rtable) + 1) *
-									   sizeof(Query *));
-	parent_parses[top_parentRTindex] = parse;
-
-	/*
-	 * And now we can get on with generating a plan for each child table.
-	 */
-	foreach(lc, child_appinfos)
-	{
-		AppendRelInfo *appinfo = lfirst_node(AppendRelInfo, lc);
-		Index		this_subquery_rti = next_subquery_rti;
-		Query	   *parent_parse;
-		PlannerInfo *subroot;
-		RangeTblEntry *child_rte;
-		RelOptInfo *sub_final_rel;
-		Path	   *subpath;
-
-		/*
-		 * expand_inherited_rtentry() always processes a parent before any of
-		 * that parent's children, so the parent query for this relation
-		 * should already be available.
-		 */
-		parent_parse = parent_parses[appinfo->parent_relid];
-		Assert(parent_parse != NULL);
-
-		/*
-		 * We need a working copy of the PlannerInfo so that we can control
-		 * propagation of information back to the main copy.
-		 */
-		subroot = makeNode(PlannerInfo);
-		memcpy(subroot, root, sizeof(PlannerInfo));
-
-		/*
-		 * Generate modified query with this rel as target.  We first apply
-		 * adjust_appendrel_attrs, which copies the Query and changes
-		 * references to the parent RTE to refer to the current child RTE,
-		 * then fool around with subquery RTEs.
-		 */
-		subroot->parse = (Query *)
-			adjust_appendrel_attrs(subroot,
-								   (Node *) parent_parse,
-								   1, &appinfo);
-
-		/*
-		 * If there are securityQuals attached to the parent, move them to the
-		 * child rel (they've already been transformed properly for that).
-		 */
-		parent_rte = rt_fetch(appinfo->parent_relid, subroot->parse->rtable);
-		child_rte = rt_fetch(appinfo->child_relid, subroot->parse->rtable);
-		child_rte->securityQuals = parent_rte->securityQuals;
-		parent_rte->securityQuals = NIL;
-
-		/*
-		 * HACK: setting this to a value other than INHKIND_NONE signals to
-		 * relation_excluded_by_constraints() to treat the result relation as
-		 * being an appendrel member.
-		 */
-		subroot->inhTargetKind =
-			(rootRelation != 0) ? INHKIND_PARTITIONED : INHKIND_INHERITED;
-
-		/*
-		 * If this child is further partitioned, remember it as a parent.
-		 * Since a partitioned table does not have any data, we don't need to
-		 * create a plan for it, and we can stop processing it here.  We do,
-		 * however, need to remember its modified PlannerInfo for use when
-		 * processing its children, since we'll update their varnos based on
-		 * the delta from immediate parent to child, not from top to child.
-		 *
-		 * Note: a very non-obvious point is that we have not yet added
-		 * duplicate subquery RTEs to the subroot's rtable.  We mustn't,
-		 * because then its children would have two sets of duplicates,
-		 * confusing matters.
-		 */
-		if (child_rte->inh)
-		{
-			Assert(child_rte->relkind == RELKIND_PARTITIONED_TABLE);
-			parent_parses[appinfo->child_relid] = subroot->parse;
-			continue;
-		}
-
-		/*
-		 * Set the nominal target relation of the ModifyTable node if not
-		 * already done.  If the target is a partitioned table, we already set
-		 * nominalRelation to refer to the partition root, above.  For
-		 * non-partitioned inheritance cases, we'll use the first child
-		 * relation (even if it's excluded) as the nominal target relation.
-		 * Because of the way expand_inherited_rtentry works, that should be
-		 * the RTE representing the parent table in its role as a simple
-		 * member of the inheritance set.
-		 *
-		 * It would be logically cleaner to *always* use the inheritance
-		 * parent RTE as the nominal relation; but that RTE is not otherwise
-		 * referenced in the plan in the non-partitioned inheritance case.
-		 * Instead the duplicate child RTE created by expand_inherited_rtentry
-		 * is used elsewhere in the plan, so using the original parent RTE
-		 * would give rise to confusing use of multiple aliases in EXPLAIN
-		 * output for what the user will think is the "same" table.  OTOH,
-		 * it's not a problem in the partitioned inheritance case, because
-		 * there is no duplicate RTE for the parent.
-		 */
-		if (nominalRelation < 0)
-			nominalRelation = appinfo->child_relid;
-
-		/*
-		 * As above, each child plan run needs its own append_rel_list and
-		 * rowmarks, which should start out as pristine copies of the
-		 * originals.  There can't be any references to UPDATE/DELETE target
-		 * rels in them; but there could be subquery references, which we'll
-		 * fix up in a moment.
-		 */
-		subroot->append_rel_list = copyObject(root->append_rel_list);
-		subroot->rowMarks = copyObject(root->rowMarks);
-
-		/*
-		 * If this isn't the first child Query, adjust Vars and jointree
-		 * entries to reference the appropriate set of subquery RTEs.
-		 */
-		if (final_rtable != NIL && subqueryRTindexes != NULL)
-		{
-			int			oldrti = -1;
-
-			while ((oldrti = bms_next_member(subqueryRTindexes, oldrti)) >= 0)
-			{
-				Index		newrti = next_subquery_rti++;
-
-				ChangeVarNodes((Node *) subroot->parse, oldrti, newrti, 0);
-				ChangeVarNodes((Node *) subroot->append_rel_list,
-							   oldrti, newrti, 0);
-				ChangeVarNodes((Node *) subroot->rowMarks, oldrti, newrti, 0);
-			}
-		}
-
-		/* There shouldn't be any OJ info to translate, as yet */
-		Assert(subroot->join_info_list == NIL);
-		/* and we haven't created PlaceHolderInfos, either */
-		Assert(subroot->placeholder_list == NIL);
-
-		/* Generate Path(s) for accessing this result relation */
-		grouping_planner(subroot, true, 0.0 /* retrieve all tuples */ );
-
-		/*
-		 * Select cheapest path in case there's more than one.  We always run
-		 * modification queries to conclusion, so we care only for the
-		 * cheapest-total path.
-		 */
-		sub_final_rel = fetch_upper_rel(subroot, UPPERREL_FINAL, NULL);
-		set_cheapest(sub_final_rel);
-		subpath = sub_final_rel->cheapest_total_path;
-
-		/*
-		 * If this child rel was excluded by constraint exclusion, exclude it
-		 * from the result plan.
-		 */
-		if (IS_DUMMY_REL(sub_final_rel))
-			continue;
-
-		/*
-		 * If this is the first non-excluded child, its post-planning rtable
-		 * becomes the initial contents of final_rtable; otherwise, copy its
-		 * modified subquery RTEs into final_rtable, to ensure we have sane
-		 * copies of those.  Also save the first non-excluded child's version
-		 * of the rowmarks list; we assume all children will end up with
-		 * equivalent versions of that.  Likewise for append_rel_list.
-		 */
-		if (final_rtable == NIL)
-		{
-			final_rtable = subroot->parse->rtable;
-			final_rowmarks = subroot->rowMarks;
-			final_appendrels = subroot->append_rel_list;
-		}
-		else
-		{
-			Assert(list_length(final_rtable) ==
-				   list_length(subroot->parse->rtable));
-			if (subqueryRTindexes != NULL)
-			{
-				int			oldrti = -1;
-
-				while ((oldrti = bms_next_member(subqueryRTindexes, oldrti)) >= 0)
-				{
-					Index		newrti = this_subquery_rti++;
-					RangeTblEntry *subqrte;
-					ListCell   *newrticell;
-
-					subqrte = rt_fetch(newrti, subroot->parse->rtable);
-					newrticell = list_nth_cell(final_rtable, newrti - 1);
-					lfirst(newrticell) = subqrte;
-				}
-			}
-		}
-
-		/*
-		 * We need to collect all the RelOptInfos from all child plans into
-		 * the main PlannerInfo, since setrefs.c will need them.  We use the
-		 * last child's simple_rel_array, so we have to propagate forward the
-		 * RelOptInfos that were already built in previous children.
-		 */
-		Assert(subroot->simple_rel_array_size >= save_rel_array_size);
-		for (rti = 1; rti < save_rel_array_size; rti++)
-		{
-			RelOptInfo *brel = save_rel_array[rti];
-
-			if (brel)
-				subroot->simple_rel_array[rti] = brel;
-		}
-		save_rel_array_size = subroot->simple_rel_array_size;
-		save_rel_array = subroot->simple_rel_array;
-		save_append_rel_array = subroot->append_rel_array;
-
-		/*
-		 * Make sure any initplans from this rel get into the outer list. Note
-		 * we're effectively assuming all children generate the same
-		 * init_plans.
-		 */
-		root->init_plans = subroot->init_plans;
-
-		/* Build list of sub-paths */
-		subpaths = lappend(subpaths, subpath);
-
-		/* Build list of modified subroots, too */
-		subroots = lappend(subroots, subroot);
-
-		/* Build list of target-relation RT indexes */
-		resultRelations = lappend_int(resultRelations, appinfo->child_relid);
-
-		/* Accumulate lists of UPDATE target columns */
-		if (parse->commandType == CMD_UPDATE)
-			updateColnosLists = lappend(updateColnosLists,
-										subroot->update_colnos);
-
-		/* Build lists of per-relation WCO and RETURNING targetlists */
-		if (parse->withCheckOptions)
-			withCheckOptionLists = lappend(withCheckOptionLists,
-										   subroot->parse->withCheckOptions);
-		if (parse->returningList)
-			returningLists = lappend(returningLists,
-									 subroot->parse->returningList);
-
-		Assert(!parse->onConflict);
-	}
-
-	/* Result path must go into outer query's FINAL upperrel */
-	final_rel = fetch_upper_rel(root, UPPERREL_FINAL, NULL);
-
-	/*
-	 * We don't currently worry about setting final_rel's consider_parallel
-	 * flag in this case, nor about allowing FDWs or create_upper_paths_hook
-	 * to get control here.
-	 */
-
-	if (subpaths == NIL)
-	{
-		/*
-		 * We managed to exclude every child rel, so generate a dummy path
-		 * representing the empty set.  Although it's clear that no data will
-		 * be updated or deleted, we will still need to have a ModifyTable
-		 * node so that any statement triggers are executed.  (This could be
-		 * cleaner if we fixed nodeModifyTable.c to support zero child nodes,
-		 * but that probably wouldn't be a net win.)
-		 */
-		Path	   *dummy_path;
-
-		/* tlist processing never got done, either */
-		root->processed_tlist = preprocess_targetlist(root);
-		final_rel->reltarget = create_pathtarget(root, root->processed_tlist);
-
-		/* Make a dummy path, cf set_dummy_rel_pathlist() */
-		dummy_path = (Path *) create_append_path(NULL, final_rel, NIL, NIL,
-												 NIL, NULL, 0, false,
-												 -1);
-
-		/* These lists must be nonempty to make a valid ModifyTable node */
-		subpaths = list_make1(dummy_path);
-		subroots = list_make1(root);
-		resultRelations = list_make1_int(parse->resultRelation);
-		if (parse->commandType == CMD_UPDATE)
-			updateColnosLists = lappend(updateColnosLists,
-										root->update_colnos);
-		if (parse->withCheckOptions)
-			withCheckOptionLists = list_make1(parse->withCheckOptions);
-		if (parse->returningList)
-			returningLists = list_make1(parse->returningList);
-		/* Disable tuple routing, too, just to be safe */
-		root->partColsUpdated = false;
-	}
-	else
-	{
-		/*
-		 * Put back the final adjusted rtable into the original copy of the
-		 * Query.  (We mustn't do this if we found no non-excluded children,
-		 * since we never saved an adjusted rtable at all.)
-		 */
-		parse->rtable = final_rtable;
-		root->simple_rel_array_size = save_rel_array_size;
-		root->simple_rel_array = save_rel_array;
-		root->append_rel_array = save_append_rel_array;
-
-		/* Must reconstruct original's simple_rte_array, too */
-		root->simple_rte_array = (RangeTblEntry **)
-			palloc0((list_length(final_rtable) + 1) * sizeof(RangeTblEntry *));
-		rti = 1;
-		foreach(lc, final_rtable)
-		{
-			RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);
-
-			root->simple_rte_array[rti++] = rte;
-		}
-
-		/* Put back adjusted rowmarks and appendrels, too */
-		root->rowMarks = final_rowmarks;
-		root->append_rel_list = final_appendrels;
-	}
-
-	/*
-	 * If there was a FOR [KEY] UPDATE/SHARE clause, the LockRows node will
-	 * have dealt with fetching non-locked marked rows, else we need to have
-	 * ModifyTable do that.
-	 */
-	if (parse->rowMarks)
-		rowMarks = NIL;
-	else
-		rowMarks = root->rowMarks;
-
-	/* Create Path representing a ModifyTable to do the UPDATE/DELETE work */
-	add_path(final_rel, (Path *)
-			 create_modifytable_path(root, final_rel,
-									 parse->commandType,
-									 parse->canSetTag,
-									 nominalRelation,
-									 rootRelation,
-									 root->partColsUpdated,
-									 resultRelations,
-									 subpaths,
-									 subroots,
-									 updateColnosLists,
-									 withCheckOptionLists,
-									 returningLists,
-									 rowMarks,
-									 NULL,
-									 assign_special_exec_param(root)));
-}
-
 /*--------------------
  * grouping_planner
  *	  Perform planning steps related to grouping, aggregation, etc.
@@ -1813,11 +1183,6 @@ inheritance_planner(PlannerInfo *root)
  * This function adds all required top-level processing to the scan/join
  * Path(s) produced by query_planner.
  *
- * If inheritance_update is true, we're being called from inheritance_planner
- * and should not include a ModifyTable step in the resulting Path(s).
- * (inheritance_planner will create a single ModifyTable node covering all the
- * target tables.)
- *
  * tuple_fraction is the fraction of tuples we expect will be retrieved.
  * tuple_fraction is interpreted as follows:
  *	  0: expect all tuples to be retrieved (normal case)
@@ -1835,8 +1200,7 @@ inheritance_planner(PlannerInfo *root)
  *--------------------
  */
 static void
-grouping_planner(PlannerInfo *root, bool inheritance_update,
-				 double tuple_fraction)
+grouping_planner(PlannerInfo *root, double tuple_fraction)
 {
 	Query	   *parse = root->parse;
 	int64		offset_est = 0;
@@ -2317,17 +1681,112 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 											  offset_est, count_est);
 		}
 
-		/*
-		 * If this is an INSERT/UPDATE/DELETE, and we're not being called from
-		 * inheritance_planner, add the ModifyTable node.
-		 */
-		if (parse->commandType != CMD_SELECT && !inheritance_update)
+		/* If this is an INSERT/UPDATE/DELETE, add the ModifyTable node. */
+		if (parse->commandType != CMD_SELECT)
 		{
 			Index		rootRelation;
-			List *updateColnosLists;
-			List	   *withCheckOptionLists;
-			List	   *returningLists;
+			List	   *resultRelations = NIL;
+			List	   *updateColnosLists = NIL;
+			List	   *withCheckOptionLists = NIL;
+			List	   *returningLists = NIL;
 			List	   *rowMarks;
+			ListCell *l;
+
+			if (root->inherit_result_rels)
+			{
+				/* Inherited UPDATE/DELETE */
+				foreach(l, root->inherit_result_rels)
+				{
+					InheritResultRelInfo *resultInfo = lfirst(l);
+					Index	resultRelation = resultInfo->resultRelation;
+
+					/* Add only leaf children to ModifyTable. */
+					if (planner_rt_fetch(resultInfo->resultRelation,
+										 root)->inh)
+						continue;
+
+					/*
+					 * Also exclude any leaf rels that have turned dummy since
+					 * being added to the list, for example, by being excluded
+					 * by constraint exclusion.
+					 */
+					if (IS_DUMMY_REL(find_base_rel(root, resultRelation)))
+						continue;
+
+					resultRelations = lappend_int(resultRelations,
+												  resultInfo->resultRelation);
+					if (parse->commandType == CMD_UPDATE)
+						updateColnosLists = lappend(updateColnosLists,
+													resultInfo->update_colnos);
+					if (resultInfo->withCheckOptions)
+						withCheckOptionLists = lappend(withCheckOptionLists,
+													   resultInfo->withCheckOptions);
+					if (resultInfo->returningList)
+						returningLists = lappend(returningLists,
+												 resultInfo->returningList);
+				}
+
+				/*
+				 * We managed to exclude every child rel, so generate a dummy
+				 * path representing the empty set.  Although it's clear that
+				 * no data will be updated or deleted, we will still need to
+				 * have a ModifyTable node so that any statement triggers are
+				 * executed.  (This could be cleaner if we fixed
+				 * nodeModifyTable.c to support zero target relations, but
+				 * that probably wouldn't be a net win.)
+				 */
+				if (resultRelations == NIL)
+				{
+					InheritResultRelInfo *resultInfo = linitial(root->inherit_result_rels);
+					RelOptInfo *rel = find_base_rel(root, resultInfo->resultRelation);
+					List	   *newlist;
+
+					resultRelations = list_make1_int(resultInfo->resultRelation);
+					if (parse->commandType == CMD_UPDATE)
+						updateColnosLists = list_make1(resultInfo->update_colnos);
+					if (resultInfo->withCheckOptions)
+						withCheckOptionLists = list_make1(resultInfo->withCheckOptions);
+					if (resultInfo->returningList)
+						returningLists = list_make1(resultInfo->returningList);
+
+					/*
+					 * Must remove special junk attributes from the targetlist
+					 * that were added for child relations, because they are
+					 * no longer necessary and in fact may not even be
+					 * computable using root parent relation.
+					 */
+					newlist = NIL;
+					foreach(l, root->processed_tlist)
+					{
+						TargetEntry *tle = lfirst(l);
+
+						if (!list_member(root->inherit_junk_tlist, tle))
+							newlist = lappend(newlist, tle);
+					}
+					root->processed_tlist = newlist;
+					rel->reltarget = create_pathtarget(root,
+													   root->processed_tlist);
+					/*
+					 * Override the existing path with a dummy Append path,
+					 * because the old path still references the old
+					 * reltarget.
+					 */
+					path = (Path *) create_append_path(NULL, rel, NIL, NIL,
+													   NIL, NULL, 0, false,
+													   -1);
+				}
+			}
+			else
+			{
+				/* Single-relation UPDATE/DELETE or INSERT. */
+				resultRelations = list_make1_int(parse->resultRelation);
+				if (parse->commandType == CMD_UPDATE)
+					updateColnosLists = list_make1(root->update_colnos);
+				if (parse->withCheckOptions)
+					withCheckOptionLists = list_make1(parse->withCheckOptions);
+				if (parse->returningList)
+					returningLists = list_make1(parse->returningList);
+			}
 
 			/*
 			 * If target is a partition root table, we need to mark the
@@ -2339,26 +1798,6 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 			else
 				rootRelation = 0;
 
-			/* Set up the UPDATE target columns list-of-lists, if needed. */
-			if (parse->commandType == CMD_UPDATE)
-				updateColnosLists = list_make1(root->update_colnos);
-			else
-				updateColnosLists = NIL;
-
-			/*
-			 * Set up the WITH CHECK OPTION and RETURNING lists-of-lists, if
-			 * needed.
-			 */
-			if (parse->withCheckOptions)
-				withCheckOptionLists = list_make1(parse->withCheckOptions);
-			else
-				withCheckOptionLists = NIL;
-
-			if (parse->returningList)
-				returningLists = list_make1(parse->returningList);
-			else
-				returningLists = NIL;
-
 			/*
 			 * If there was a FOR [KEY] UPDATE/SHARE clause, the LockRows node
 			 * will have dealt with fetching non-locked marked rows, else we
@@ -2371,14 +1810,13 @@ grouping_planner(PlannerInfo *root, bool inheritance_update,
 
 			path = (Path *)
 				create_modifytable_path(root, final_rel,
+										path,
 										parse->commandType,
 										parse->canSetTag,
 										parse->resultRelation,
 										rootRelation,
-										false,
-										list_make1_int(parse->resultRelation),
-										list_make1(path),
-										list_make1(root),
+										root->partColsUpdated,
+										resultRelations,
 										updateColnosLists,
 										withCheckOptionLists,
 										returningLists,
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 42f088ad71..76f07aebbd 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -297,6 +297,7 @@ set_plan_references(PlannerInfo *root, Plan *plan)
 		 * Neither the executor nor EXPLAIN currently need that data.
 		 */
 		appinfo->translated_vars = NIL;
+		appinfo->translated_fake_vars = NIL;
 
 		glob->appendRelations = lappend(glob->appendRelations, appinfo);
 	}
@@ -897,26 +898,21 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 				{
 					List	   *newRL = NIL;
 					ListCell   *lcrl,
-							   *lcrr,
-							   *lcp;
+							   *lcrr;
 
 					/*
-					 * Pass each per-subplan returningList through
+					 * Pass each per-resultrel returningList through
 					 * set_returning_clause_references().
 					 */
 					Assert(list_length(splan->returningLists) == list_length(splan->resultRelations));
-					Assert(list_length(splan->returningLists) == list_length(splan->plans));
-					forthree(lcrl, splan->returningLists,
-							 lcrr, splan->resultRelations,
-							 lcp, splan->plans)
+					forboth(lcrl, splan->returningLists, lcrr, splan->resultRelations)
 					{
 						List	   *rlist = (List *) lfirst(lcrl);
 						Index		resultrel = lfirst_int(lcrr);
-						Plan	   *subplan = (Plan *) lfirst(lcp);
 
 						rlist = set_returning_clause_references(root,
 																rlist,
-																subplan,
+																outerPlan(splan),
 																resultrel,
 																rtoffset);
 						newRL = lappend(newRL, rlist);
@@ -982,12 +978,6 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 					rc->rti += rtoffset;
 					rc->prti += rtoffset;
 				}
-				foreach(l, splan->plans)
-				{
-					lfirst(l) = set_plan_refs(root,
-											  (Plan *) lfirst(l),
-											  rtoffset);
-				}
 
 				/*
 				 * Append this ModifyTable node's final result relation RT
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index f3e46e0959..b12ab7de2d 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -2533,7 +2533,6 @@ finalize_plan(PlannerInfo *root, Plan *plan,
 		case T_ModifyTable:
 			{
 				ModifyTable *mtplan = (ModifyTable *) plan;
-				ListCell   *l;
 
 				/* Force descendant scan nodes to reference epqParam */
 				locally_added_param = mtplan->epqParam;
@@ -2548,16 +2547,6 @@ finalize_plan(PlannerInfo *root, Plan *plan,
 				finalize_primnode((Node *) mtplan->onConflictWhere,
 								  &context);
 				/* exclRelTlist contains only Vars, doesn't need examination */
-				foreach(l, mtplan->plans)
-				{
-					context.paramids =
-						bms_add_members(context.paramids,
-										finalize_plan(root,
-													  (Plan *) lfirst(l),
-													  gather_param,
-													  valid_params,
-													  scan_params));
-				}
 			}
 			break;
 
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index e18553ac7c..0cf5f6d0d6 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -921,15 +921,16 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	subroot->eq_classes = NIL;
 	subroot->ec_merging_done = false;
 	subroot->append_rel_list = NIL;
+	subroot->inherit_result_rels = NIL;
 	subroot->rowMarks = NIL;
 	memset(subroot->upper_rels, 0, sizeof(subroot->upper_rels));
 	memset(subroot->upper_targets, 0, sizeof(subroot->upper_targets));
 	subroot->processed_tlist = NIL;
 	subroot->update_colnos = NIL;
+	subroot->inherit_junk_tlist = NIL;
 	subroot->grouping_map = NULL;
 	subroot->minmax_aggs = NIL;
 	subroot->qual_security_level = 0;
-	subroot->inhTargetKind = INHKIND_NONE;
 	subroot->hasRecursion = false;
 	subroot->wt_param_id = -1;
 	subroot->non_recursive_path = NULL;
@@ -1014,6 +1015,7 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
 	rtoffset = list_length(parse->rtable);
 	OffsetVarNodes((Node *) subquery, rtoffset, 0);
 	OffsetVarNodes((Node *) subroot->append_rel_list, rtoffset, 0);
+	Assert(subroot->inherit_result_rels == NIL);
 
 	/*
 	 * Upper-level vars in subquery are now one level closer to their parent
@@ -2057,6 +2059,8 @@ perform_pullup_replace_vars(PlannerInfo *root,
 	 * parent appendrel --- there isn't any outer join between.  Elsewhere,
 	 * use PHVs for safety.  (This analysis could be made tighter but it seems
 	 * unlikely to be worth much trouble.)
+	 *
+	 * XXX what of translated_fake_vars?
 	 */
 	foreach(lc, root->append_rel_list)
 	{
@@ -3514,6 +3518,8 @@ fix_append_rel_relids(List *append_rel_list, int varno, Relids subrelids)
 		/* Also fix up any PHVs in its translated vars */
 		substitute_phv_relids((Node *) appinfo->translated_vars,
 							  varno, subrelids);
+
+		/* XXX what of translated_fake_vars? */
 	}
 }
 
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index 488e8cfd4d..91f2d54da5 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -111,6 +111,10 @@ preprocess_targetlist(PlannerInfo *root)
 	 * to identify the rows to be updated or deleted.  Note that this step
 	 * scribbles on parse->targetList, which is not very desirable, but we
 	 * keep it that way to avoid changing APIs used by FDWs.
+	 *
+	 * If target relation has inheritance children, junk column(s) needed
+	 * by the individual leaf child relations are added by
+	 * inherit.c: add_child_junk_attrs().
 	 */
 	if (command_type == CMD_UPDATE || command_type == CMD_DELETE)
 		rewriteTargetListUD(parse, target_rte, target_relation);
diff --git a/src/backend/optimizer/util/appendinfo.c b/src/backend/optimizer/util/appendinfo.c
index 86922a273c..fb7cf149b1 100644
--- a/src/backend/optimizer/util/appendinfo.c
+++ b/src/backend/optimizer/util/appendinfo.c
@@ -29,6 +29,7 @@ typedef struct
 	PlannerInfo *root;
 	int			nappinfos;
 	AppendRelInfo **appinfos;
+	bool		need_parent_wholerow;
 } adjust_appendrel_attrs_context;
 
 static void make_inh_translation_list(Relation oldrelation,
@@ -37,8 +38,6 @@ static void make_inh_translation_list(Relation oldrelation,
 									  AppendRelInfo *appinfo);
 static Node *adjust_appendrel_attrs_mutator(Node *node,
 											adjust_appendrel_attrs_context *context);
-static List *adjust_inherited_tlist(List *tlist,
-									AppendRelInfo *context);
 
 
 /*
@@ -200,42 +199,42 @@ adjust_appendrel_attrs(PlannerInfo *root, Node *node, int nappinfos,
 	context.root = root;
 	context.nappinfos = nappinfos;
 	context.appinfos = appinfos;
+	context.need_parent_wholerow = true;
 
 	/* If there's nothing to adjust, don't call this function. */
 	Assert(nappinfos >= 1 && appinfos != NULL);
 
-	/*
-	 * Must be prepared to start with a Query or a bare expression tree.
-	 */
-	if (node && IsA(node, Query))
-	{
-		Query	   *newnode;
-		int			cnt;
+	/* Should never be translating a Query tree. */
+	Assert (node == NULL || !IsA(node, Query));
+	result = adjust_appendrel_attrs_mutator(node, &context);
 
-		newnode = query_tree_mutator((Query *) node,
-									 adjust_appendrel_attrs_mutator,
-									 (void *) &context,
-									 QTW_IGNORE_RC_SUBQUERIES);
-		for (cnt = 0; cnt < nappinfos; cnt++)
-		{
-			AppendRelInfo *appinfo = appinfos[cnt];
+	return result;
+}
 
-			if (newnode->resultRelation == appinfo->parent_relid)
-			{
-				newnode->resultRelation = appinfo->child_relid;
-				/* Fix tlist resnos too, if it's inherited UPDATE */
-				if (newnode->commandType == CMD_UPDATE)
-					newnode->targetList =
-						adjust_inherited_tlist(newnode->targetList,
-											   appinfo);
-				break;
-			}
-		}
+/*
+ * adjust_target_appendrel_attrs
+ *		like adjust_appendrel_attrs, but treats wholerow Vars a bit
+ *		differently in that it doesn't convert any child table
+ *		wholerows contained in 'node' back to the parent reltype.
+ */
+Node *
+adjust_target_appendrel_attrs(PlannerInfo *root, Node *node,
+							  AppendRelInfo *appinfo)
+{
+	Node	   *result;
+	adjust_appendrel_attrs_context context;
 
-		result = (Node *) newnode;
-	}
-	else
-		result = adjust_appendrel_attrs_mutator(node, &context);
+	context.root = root;
+	context.nappinfos = 1;
+	context.appinfos = &appinfo;
+	context.need_parent_wholerow = false;
+
+	/* If there's nothing to adjust, don't call this function. */
+	Assert(appinfo != NULL);
+
+	/* Should never be translating a Query tree. */
+	Assert (node == NULL || !IsA(node, Query));
+	result = adjust_appendrel_attrs_mutator(node, &context);
 
 	return result;
 }
@@ -277,11 +276,16 @@ adjust_appendrel_attrs_mutator(Node *node,
 			{
 				Node	   *newnode;
 
+				/*
+				 * If this Var appears to have a unusual attno assigned, it
+				 * must be one of the "fake" vars added to a parent target
+				 * relation's reltarget; see add_inherit_junk_var().
+				 */
 				if (var->varattno > list_length(appinfo->translated_vars))
-					elog(ERROR, "attribute %d of relation \"%s\" does not exist",
-						 var->varattno, get_rel_name(appinfo->parent_reloid));
-				newnode = copyObject(list_nth(appinfo->translated_vars,
-											  var->varattno - 1));
+					newnode = translate_fake_parent_var(var, appinfo);
+				else
+					newnode = copyObject(list_nth(appinfo->translated_vars,
+												  var->varattno - 1));
 				if (newnode == NULL)
 					elog(ERROR, "attribute %d of relation \"%s\" does not exist",
 						 var->varattno, get_rel_name(appinfo->parent_reloid));
@@ -298,7 +302,10 @@ adjust_appendrel_attrs_mutator(Node *node,
 				if (OidIsValid(appinfo->child_reltype))
 				{
 					Assert(var->vartype == appinfo->parent_reltype);
-					if (appinfo->parent_reltype != appinfo->child_reltype)
+					/* Make sure the Var node has the right type ID, too */
+					var->vartype = appinfo->child_reltype;
+					if (appinfo->parent_reltype != appinfo->child_reltype &&
+						context->need_parent_wholerow)
 					{
 						ConvertRowtypeExpr *r = makeNode(ConvertRowtypeExpr);
 
@@ -306,8 +313,6 @@ adjust_appendrel_attrs_mutator(Node *node,
 						r->resulttype = appinfo->parent_reltype;
 						r->convertformat = COERCE_IMPLICIT_CAST;
 						r->location = -1;
-						/* Make sure the Var node has the right type ID, too */
-						var->vartype = appinfo->child_reltype;
 						return (Node *) r;
 					}
 				}
@@ -361,44 +366,6 @@ adjust_appendrel_attrs_mutator(Node *node,
 		}
 		return (Node *) cexpr;
 	}
-	if (IsA(node, RangeTblRef))
-	{
-		RangeTblRef *rtr = (RangeTblRef *) copyObject(node);
-
-		for (cnt = 0; cnt < nappinfos; cnt++)
-		{
-			AppendRelInfo *appinfo = appinfos[cnt];
-
-			if (rtr->rtindex == appinfo->parent_relid)
-			{
-				rtr->rtindex = appinfo->child_relid;
-				break;
-			}
-		}
-		return (Node *) rtr;
-	}
-	if (IsA(node, JoinExpr))
-	{
-		/* Copy the JoinExpr node with correct mutation of subnodes */
-		JoinExpr   *j;
-		AppendRelInfo *appinfo;
-
-		j = (JoinExpr *) expression_tree_mutator(node,
-												 adjust_appendrel_attrs_mutator,
-												 (void *) context);
-		/* now fix JoinExpr's rtindex (probably never happens) */
-		for (cnt = 0; cnt < nappinfos; cnt++)
-		{
-			appinfo = appinfos[cnt];
-
-			if (j->rtindex == appinfo->parent_relid)
-			{
-				j->rtindex = appinfo->child_relid;
-				break;
-			}
-		}
-		return (Node *) j;
-	}
 	if (IsA(node, PlaceHolderVar))
 	{
 		/* Copy the PlaceHolderVar node with correct mutation of subnodes */
@@ -487,6 +454,10 @@ adjust_appendrel_attrs_mutator(Node *node,
 	Assert(!IsA(node, SubLink));
 	Assert(!IsA(node, Query));
 
+	/* We should never see these Query substructures. */
+	Assert(!IsA(node, RangeTblRef));
+	Assert(!IsA(node, JoinExpr));
+
 	return expression_tree_mutator(node, adjust_appendrel_attrs_mutator,
 								   (void *) context);
 }
@@ -620,103 +591,6 @@ adjust_child_relids_multilevel(PlannerInfo *root, Relids relids,
 	return result;
 }
 
-/*
- * Adjust the targetlist entries of an inherited UPDATE operation
- *
- * The expressions have already been fixed, but we have to make sure that
- * the target resnos match the child table (they may not, in the case of
- * a column that was added after-the-fact by ALTER TABLE).  In some cases
- * this can force us to re-order the tlist to preserve resno ordering.
- * (We do all this work in special cases so that preptlist.c is fast for
- * the typical case.)
- *
- * The given tlist has already been through expression_tree_mutator;
- * therefore the TargetEntry nodes are fresh copies that it's okay to
- * scribble on.
- *
- * Note that this is not needed for INSERT because INSERT isn't inheritable.
- */
-static List *
-adjust_inherited_tlist(List *tlist, AppendRelInfo *context)
-{
-	bool		changed_it = false;
-	ListCell   *tl;
-	List	   *new_tlist;
-	bool		more;
-	int			attrno;
-
-	/* This should only happen for an inheritance case, not UNION ALL */
-	Assert(OidIsValid(context->parent_reloid));
-
-	/* Scan tlist and update resnos to match attnums of child rel */
-	foreach(tl, tlist)
-	{
-		TargetEntry *tle = (TargetEntry *) lfirst(tl);
-		Var		   *childvar;
-
-		if (tle->resjunk)
-			continue;			/* ignore junk items */
-
-		/* Look up the translation of this column: it must be a Var */
-		if (tle->resno <= 0 ||
-			tle->resno > list_length(context->translated_vars))
-			elog(ERROR, "attribute %d of relation \"%s\" does not exist",
-				 tle->resno, get_rel_name(context->parent_reloid));
-		childvar = (Var *) list_nth(context->translated_vars, tle->resno - 1);
-		if (childvar == NULL || !IsA(childvar, Var))
-			elog(ERROR, "attribute %d of relation \"%s\" does not exist",
-				 tle->resno, get_rel_name(context->parent_reloid));
-
-		if (tle->resno != childvar->varattno)
-		{
-			tle->resno = childvar->varattno;
-			changed_it = true;
-		}
-	}
-
-	/*
-	 * If we changed anything, re-sort the tlist by resno, and make sure
-	 * resjunk entries have resnos above the last real resno.  The sort
-	 * algorithm is a bit stupid, but for such a seldom-taken path, small is
-	 * probably better than fast.
-	 */
-	if (!changed_it)
-		return tlist;
-
-	new_tlist = NIL;
-	more = true;
-	for (attrno = 1; more; attrno++)
-	{
-		more = false;
-		foreach(tl, tlist)
-		{
-			TargetEntry *tle = (TargetEntry *) lfirst(tl);
-
-			if (tle->resjunk)
-				continue;		/* ignore junk items */
-
-			if (tle->resno == attrno)
-				new_tlist = lappend(new_tlist, tle);
-			else if (tle->resno > attrno)
-				more = true;
-		}
-	}
-
-	foreach(tl, tlist)
-	{
-		TargetEntry *tle = (TargetEntry *) lfirst(tl);
-
-		if (!tle->resjunk)
-			continue;			/* here, ignore non-junk items */
-
-		tle->resno = attrno;
-		new_tlist = lappend(new_tlist, tle);
-		attrno++;
-	}
-
-	return new_tlist;
-}
-
 /*
  * find_appinfos_by_relids
  * 		Find AppendRelInfo structures for all relations specified by relids.
diff --git a/src/backend/optimizer/util/inherit.c b/src/backend/optimizer/util/inherit.c
index be1c9ddd96..50d2deb2a0 100644
--- a/src/backend/optimizer/util/inherit.c
+++ b/src/backend/optimizer/util/inherit.c
@@ -21,6 +21,7 @@
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
+#include "nodes/nodeFuncs.h"
 #include "optimizer/appendinfo.h"
 #include "optimizer/inherit.h"
 #include "optimizer/optimizer.h"
@@ -29,9 +30,12 @@
 #include "optimizer/planner.h"
 #include "optimizer/prep.h"
 #include "optimizer/restrictinfo.h"
+#include "optimizer/tlist.h"
 #include "parser/parsetree.h"
 #include "partitioning/partdesc.h"
 #include "partitioning/partprune.h"
+#include "rewrite/rewriteHandler.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 
 
@@ -49,6 +53,16 @@ static Bitmapset *translate_col_privs(const Bitmapset *parent_privs,
 									  List *translated_vars);
 static void expand_appendrel_subquery(PlannerInfo *root, RelOptInfo *rel,
 									  RangeTblEntry *rte, Index rti);
+static void add_inherit_result_relation_info(PlannerInfo *root, Index rti,
+							Relation relation,
+							InheritResultRelInfo *parentInfo);
+static void adjust_inherited_tlist(List *tlist, AppendRelInfo *context);
+static void add_child_junk_attrs(PlannerInfo *root,
+					Index childRTindex, Relation childrelation,
+					RelOptInfo *rel, Relation relation);
+static void add_inherit_junk_var(PlannerInfo *root, char *attrname, Node *child_expr,
+					   AppendRelInfo *appinfo,
+					   RelOptInfo *parentrelinfo, Relation parentrelation);
 
 
 /*
@@ -85,6 +99,8 @@ expand_inherited_rtentry(PlannerInfo *root, RelOptInfo *rel,
 	PlanRowMark *oldrc;
 	bool		old_isParent = false;
 	int			old_allMarkTypes = 0;
+	ListCell   *l;
+	List	   *newvars = NIL;
 
 	Assert(rte->inh);			/* else caller error */
 
@@ -128,6 +144,22 @@ expand_inherited_rtentry(PlannerInfo *root, RelOptInfo *rel,
 		old_allMarkTypes = oldrc->allMarkTypes;
 	}
 
+	/*
+	 * Make an InheritResultRelInfo for the root parent if it's an
+	 * UPDATE/DELETE result relation.
+	 */
+	if (rti == root->parse->resultRelation &&
+		root->parse->commandType != CMD_INSERT)
+	{
+		/* Make an array indexable by RT indexes for easy lookup. */
+		root->inherit_result_rel_array = (InheritResultRelInfo **)
+			palloc0(root->simple_rel_array_size *
+					sizeof(InheritResultRelInfo *));
+
+		add_inherit_result_relation_info(root, root->parse->resultRelation,
+										 oldrelation, NULL);
+	}
+
 	/* Scan the inheritance set and expand it */
 	if (oldrelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
 	{
@@ -151,7 +183,6 @@ expand_inherited_rtentry(PlannerInfo *root, RelOptInfo *rel,
 		 * children, so it's not possible for both cases to apply.)
 		 */
 		List	   *inhOIDs;
-		ListCell   *l;
 
 		/* Scan for all members of inheritance set, acquire needed locks */
 		inhOIDs = find_all_inheritors(parentOID, lockmode, NULL);
@@ -226,7 +257,6 @@ expand_inherited_rtentry(PlannerInfo *root, RelOptInfo *rel,
 		Var		   *var;
 		TargetEntry *tle;
 		char		resname[32];
-		List	   *newvars = NIL;
 
 		/* The old PlanRowMark should already have necessitated adding TID */
 		Assert(old_allMarkTypes & ~(1 << ROW_MARK_COPY));
@@ -265,13 +295,24 @@ expand_inherited_rtentry(PlannerInfo *root, RelOptInfo *rel,
 			root->processed_tlist = lappend(root->processed_tlist, tle);
 			newvars = lappend(newvars, var);
 		}
+	}
 
-		/*
-		 * Add the newly added Vars to parent's reltarget.  We needn't worry
-		 * about the children's reltargets, they'll be made later.
-		 */
+	/*
+	 * Also pull any appendrel parent junk vars added due to child result
+	 * relations.
+	 */
+	if (rti == root->parse->resultRelation &&
+		list_length(root->inherit_junk_tlist) > 0)
+		newvars = list_concat(newvars,
+							  pull_var_clause((Node *)
+											  root->inherit_junk_tlist, 0));
+
+	/*
+	 * Add the newly added Vars to parent's reltarget.  We needn't worry
+	 * about the children's reltargets, they'll be made later.
+	 */
+	if (newvars != NIL)
 		add_vars_to_targetlist(root, newvars, bms_make_singleton(0), false);
-	}
 
 	table_close(oldrelation, NoLock);
 }
@@ -381,10 +422,23 @@ expand_partitioned_rtentry(PlannerInfo *root, RelOptInfo *relinfo,
 
 		/* If this child is itself partitioned, recurse */
 		if (childrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+		{
 			expand_partitioned_rtentry(root, childrelinfo,
 									   childrte, childRTindex,
 									   childrel, top_parentrc, lockmode);
 
+			/*
+			 * Add junk attributes needed by this child relation or really by
+			 * its children.  We must do this after having added all the leaf
+			 * children of this relation, because the add_child_junk_attrs()
+			 * call below simply propagates their junk attributes that are in
+			 * the form of this child relation's vars up to its own parent.
+			 */
+			if (is_result_relation(root, childRTindex))
+				add_child_junk_attrs(root, childRTindex, childrel,
+									 relinfo, parentrel);
+		}
+
 		/* Close child relation, but keep locks */
 		table_close(childrel, NoLock);
 	}
@@ -585,6 +639,27 @@ expand_single_inheritance_child(PlannerInfo *root, RangeTblEntry *parentrte,
 
 		root->rowMarks = lappend(root->rowMarks, childrc);
 	}
+
+	/*
+	 * If this appears to be a child of an UPDATE/DELETE result relation, we
+	 * need to remember some additional information.
+	 */
+	if (is_result_relation(root, parentRTindex))
+	{
+		InheritResultRelInfo *parentInfo = root->inherit_result_rel_array[parentRTindex];
+		RelOptInfo *parentrelinfo = root->simple_rel_array[parentRTindex];
+
+		add_inherit_result_relation_info(root, childRTindex, childrel,
+										 parentInfo);
+
+		/*
+		 * Add junk attributes needed by this leaf child result relation, if
+		 * one.
+		 */
+		if (childrte->relkind != RELKIND_PARTITIONED_TABLE)
+			add_child_junk_attrs(root, childRTindex, childrel, parentrelinfo,
+								 parentrel);
+	}
 }
 
 /*
@@ -805,3 +880,581 @@ apply_child_basequals(PlannerInfo *root, RelOptInfo *parentrel,
 
 	return true;
 }
+
+/*
+ * add_inherit_result_relation
+ *		Adds information to PlannerInfo about an inherited UPDATE/DELETE
+ *		result relation
+ */
+static void
+add_inherit_result_relation_info(PlannerInfo *root, Index rti,
+								 Relation relation,
+								 InheritResultRelInfo *parentInfo)
+{
+	InheritResultRelInfo *resultInfo = makeNode(InheritResultRelInfo);
+
+	if (parentInfo == NULL)
+	{
+		/* Root result relation. */
+		resultInfo->resultRelation = rti;
+		resultInfo->withCheckOptions = root->parse->withCheckOptions;
+		resultInfo->returningList = root->parse->returningList;
+		if (root->parse->commandType == CMD_UPDATE)
+		{
+			resultInfo->processed_tlist = root->processed_tlist;
+			resultInfo->update_colnos = root->update_colnos;
+		}
+	}
+	else
+	{
+		/* Child result relation. */
+		AppendRelInfo *appinfo = root->append_rel_array[rti];
+
+		Assert(appinfo != NULL);
+
+		resultInfo->resultRelation = rti;
+
+		if (parentInfo->withCheckOptions)
+			resultInfo->withCheckOptions = (List *)
+				adjust_appendrel_attrs(root,
+									   (Node *) parentInfo->withCheckOptions,
+									   1, &appinfo);
+		if (parentInfo->returningList)
+			resultInfo->returningList = (List *)
+				adjust_appendrel_attrs(root,
+									   (Node *) parentInfo->returningList,
+									   1, &appinfo);
+
+		/* Build UPDATE targetlist for this child. */
+		if (root->parse->commandType == CMD_UPDATE)
+		{
+			List*update_colnos = NIL;
+			ListCell *lc;
+
+			/*
+			 * First fix up any Vars in the parent's version of the top-level
+			 * targetlist.
+			 */
+			resultInfo->processed_tlist = (List *)
+				adjust_appendrel_attrs(root,
+									   (Node *) parentInfo->processed_tlist,
+									   1, &appinfo);
+
+			/*
+			 * adjust_appendrel_attrs() doesn't modify TLE resnos, so we need
+			 * to do that here to make processed_tlist's resnos match the
+			 * child.  Then we can extract update_colnos.
+			 */
+			adjust_inherited_tlist(resultInfo->processed_tlist, appinfo);
+
+			/* XXX this duplicates make_update_colnos() */
+			foreach(lc, resultInfo->processed_tlist)
+			{
+				TargetEntry *tle = (TargetEntry *) lfirst(lc);
+
+				if (!tle->resjunk)
+					update_colnos = lappend_int(update_colnos, tle->resno);
+			}
+			resultInfo->update_colnos = update_colnos;
+		}
+	}
+
+	root->inherit_result_rels = lappend(root->inherit_result_rels, resultInfo);
+	Assert(root->inherit_result_rel_array);
+	Assert(root->inherit_result_rel_array[rti] == NULL);
+	root->inherit_result_rel_array[rti] = resultInfo;
+}
+
+/*
+ * Adjust the targetlist entries of an inherited UPDATE operation
+ *
+ * The input tlist is that of an UPDATE targeting the given parent table.
+ * Expressions of the individual target entries have already been fixed to
+ * convert any parent table Vars in them into child table Vars, but the
+ * target resnos still match the parent attnos, which we fix here to match
+ * the corresponding child table attnos.
+ *
+ * Note the list is modified in-place.
+ */
+static void
+adjust_inherited_tlist(List *tlist, AppendRelInfo *context)
+{
+	ListCell   *tl;
+
+	/* This should only happen for an inheritance case, not UNION ALL */
+	Assert(OidIsValid(context->parent_reloid));
+
+	/* Scan tlist and update resnos to match attnums of child rel */
+	foreach(tl, tlist)
+	{
+		TargetEntry *tle = (TargetEntry *) lfirst(tl);
+		Var		   *childvar;
+
+		if (tle->resjunk)
+			continue;			/* ignore junk items */
+
+		/* Look up the translation of this column: it must be a Var */
+		if (tle->resno <= 0 ||
+			tle->resno > list_length(context->translated_vars))
+			elog(ERROR, "attribute %d of relation \"%s\" does not exist",
+				 tle->resno, get_rel_name(context->parent_reloid));
+		childvar = (Var *) list_nth(context->translated_vars, tle->resno - 1);
+		if (childvar == NULL || !IsA(childvar, Var))
+			elog(ERROR, "attribute %d of relation \"%s\" does not exist",
+				 tle->resno, get_rel_name(context->parent_reloid));
+
+		tle->resno = childvar->varattno;
+	}
+}
+
+/*
+ * add_child_junk_attrs
+ *		Adds junk attributes needed by leaf child result relations to
+ *		identify tuples to be updated/deleted, and for each tuple also
+ *		the result relation to perform the operation on
+ *
+ * While preprocess_targetlist() would have added junk attributes needed to
+ * identify rows to be updated/deleted based on whatever the root parent says
+ * they are, not all leaf result relations may be able to use the same junk
+ * attributes.  For example, in the case of leaf result relations that are
+ * foreign tables, junk attributes to use are determined by their FDW's
+ * AddForeignUpdateTargets().
+ *
+ * Even though leaf result relations are scanned at the bottom of the plan
+ * tree, any junk attributes needed must be present in the top-level tlist,
+ * so to add a junk attribute for a given leaf result relation really means
+ * adding corresponding column of the top parent relation to the top-level
+ * targetlist from where it will be propagated back down to the leaf result
+ * relation.  In some cases, a leaf relation's junk attribute may be such that
+ * no column of the root parent can be mapped to it, in which case we must add
+ * "fake" parent columns to the targetlist and set things up to map those
+ * columns' vars to desired junk attribute expressions in the reltargets of
+ * leaf result relation that need them.  This logic of how leaf-level junk
+ * attributes are mapped to top-level level vars and back is present in
+ * add_inherit_junk_var().
+ *
+ * The leaf-level junk attribute that is added to identify the leaf result
+ * relation for each tuple to be updated/deleted is really a Const node
+ * containing an integer value that gives the index of the leaf result
+ * relation in the subquery's list of result relations.  This adds an
+ * entry named "resultrelindex" to the top-level tlist which wraps a fake
+ * parent var that maps back to the Const node for each leaf result
+ * relation.
+ */
+static void
+add_child_junk_attrs(PlannerInfo *root,
+					 Index childRTindex, Relation childrelation,
+					 RelOptInfo *parentrelinfo, Relation parentrelation)
+{
+	AppendRelInfo  *appinfo = root->append_rel_array[childRTindex];
+	ListCell	   *lc;
+	List		   *child_junk_attrs = NIL;
+
+	/*
+	 * For a non-leaf child relation, we simply need to bubble up to its
+	 * parent any entries containing its vars that would be added for junk
+	 * attributes of its own children.
+	 */
+	if (childrelation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+	{
+		foreach(lc, root->inherit_junk_tlist)
+		{
+			TargetEntry *tle = lfirst(lc);
+			Var   *var = castNode(Var, tle->expr);
+
+			if (var->varno == childRTindex)
+				child_junk_attrs = lappend(child_junk_attrs, tle);
+		}
+	}
+	else
+	{
+		/* Leaf child case. */
+		RangeTblEntry  *childrte = root->simple_rte_array[childRTindex];
+		Query			parsetree;
+		Node		   *childexpr;
+		TargetEntry	   *tle;
+
+		/* The "resultrelindex" column. */
+		childexpr = (Node *) makeConst(INT4OID, -1, InvalidOid, sizeof(int32),
+									   Int32GetDatum(root->lastResultRelIndex++),
+									   false, true);
+		/* XXX resno in this is wrong */
+		tle = makeTargetEntry((Expr *) childexpr, 1, pstrdup("resultrelindex"), true);
+		child_junk_attrs = lappend(child_junk_attrs, tle);
+
+		/*
+		 * Now call rewriteTargetListUD() to add junk attributes into the
+		 * parsetree.  We pass a slightly altered version of the original
+		 * parsetree to show the child result relation as the main target
+		 * relation.  It is assumed here that rewriteTargetListUD and any
+		 * code downstream to it do not inspect the parsetree beside to
+		 * figure out the varno to assign to the Vars that will be added
+		 * to the targetlist.
+		 *
+		 * XXX instead of this, should refactor rewriteTargetListUD to pull
+		 * out whatever behavior is actually needed
+		 */
+		memcpy(&parsetree, root->parse, sizeof(Query));
+		parsetree.resultRelation = childRTindex;
+		parsetree.targetList = NIL;
+		rewriteTargetListUD(&parsetree, childrte, childrelation);
+		child_junk_attrs = list_concat(child_junk_attrs,
+									   parsetree.targetList);
+	}
+
+	/* Add parent vars for each of the child junk attributes. */
+	foreach(lc, child_junk_attrs)
+	{
+		TargetEntry *tle = lfirst(lc);
+
+		Assert(tle->resjunk);
+
+		add_inherit_junk_var(root, tle->resname, (Node *) tle->expr,
+							 appinfo, parentrelinfo, parentrelation);
+	}
+}
+
+/*
+ * add_inherit_junk_var
+ *		Checks if the query's top-level tlist (root->processed_tlist) or
+ *		root->inherit_junk_tlist contains an entry for a junk attribute
+ *		with given name and if the parent var therein translates to
+ *		given child junk expression
+ *
+ * If not, add the parent var to appropriate list -- top-level tlist if parent
+ * is top-level parent, root->inherit_junk_tlist otherwise.
+ *
+ * If the parent var found or added is not for a real column or is a "fake"
+ * var, which will be the case if no real column of the parent translates to
+ * provided child expression, then add mapping information in provided
+ * AppendRelInfo to translate such fake parent var to provided child
+ * expression.
+ */
+static void
+add_inherit_junk_var(PlannerInfo *root, char *attrname, Node *child_expr,
+					 AppendRelInfo *appinfo,
+					 RelOptInfo *parentrelinfo, Relation parentrelation)
+{
+	AttrNumber	max_parent_attno = RelationGetNumberOfAttributes(parentrelation);
+	AttrNumber	max_child_attno = appinfo->num_child_cols;
+	ListCell   *lc;
+	Var		   *parent_var = NULL;
+	Index		parent_varno = parentrelinfo->relid;
+	AttrNumber	parent_attno;
+
+	Assert(appinfo && parent_varno == appinfo->parent_relid);
+
+	/*
+	 * The way we decide if a given parent var found in the targetlist is the
+	 * one that will give the desired child var back upon translation is to
+	 * check whether the child var refers to an inherited user column or a
+	 * system column that is same as the one that the parent var refers to.
+	 * If the child var refers to a fake column, parent var must likewise
+	 * refer to a fake column itself.
+	 *
+	 * There is a special case where the desired child expression is a Const
+	 * node wrapped in an entry named "resultrelindex", in which case, simply
+	 * finding an entry with that name containing a parent's var suffices.
+	 *
+	 * If no such parent var is found, we will add one.
+	 */
+	foreach(lc, list_concat_copy(root->inherit_junk_tlist,
+								 root->processed_tlist))
+	{
+		TargetEntry *tle = lfirst(lc);
+		Var	   *var = (Var *) tle->expr;
+		Var	   *child_var = (Var *) child_expr;
+
+		if (!tle->resjunk)
+			continue;
+
+		/* Ignore RETURNING expressions in the top-level tlist. */
+		if (tle->resname == NULL)
+			continue;
+
+		if (strcmp(attrname, tle->resname) != 0)
+			continue;
+
+		if (!IsA(var,  Var))
+			elog(ERROR, "junk column \"%s\" is not a Var", attrname);
+
+		/* Ignore junk vars of other relations. */
+		if (var->varno != parent_varno)
+			continue;
+
+		/* special case */
+		if (strcmp(attrname, "resultrelindex") == 0)
+		{
+			/* The parent var had better not be a normal user column. */
+			Assert(var->varattno > max_parent_attno);
+			parent_var = var;
+			break;
+		}
+
+		if (!IsA(child_expr, Var))
+			elog(ERROR, "junk column \"%s\" of child relation %u is not a Var",
+				 attrname, appinfo->child_relid);
+
+		/*
+		 * So we found parent var referring to the column that the child wants
+		 * added, but check if that's really the case.
+		 */
+		if (var->vartype == child_var->vartype &&
+			var->vartypmod == child_var->vartypmod &&
+
+			(/* child var refers to same system column as parent var */
+			 (child_var->varattno <= 0 &&
+			  child_var->varattno == var->varattno) ||
+
+			 /* child var refers to same user column as parent var */
+			 (child_var->varattno > 0 &&
+			  child_var->varattno <= max_child_attno &&
+			  var->varattno == appinfo->parent_colnos[child_var->varattno]) ||
+
+			 /* both child var and parent var refer to "fake" column */
+			 (child_var->varattno > max_child_attno &&
+			  var->varattno > max_parent_attno)))
+		{
+			parent_var = var;
+			break;
+		}
+
+		/*
+		 * Getting here means that did find a parent column with the given
+		 * name but it's not equivalent to the child column we're trying
+		 * to add to the targetlist.  Adding a second var with child's type
+		 * would not be correct.
+		 */
+		elog(ERROR, "junk column \"%s\" of child relation %u conflicts with parent junk column with same name",
+			 attrname, appinfo->child_relid);
+	}
+
+	/*
+	 * If no parent column matching the child column found in the targetlist,
+	 * add.
+	 */
+	if (parent_var == NULL)
+	{
+		TargetEntry *tle;
+		bool		fake_column = true;
+		AttrNumber	resno;
+		Oid			parent_vartype = exprType((Node *) child_expr);
+		int32		parent_vartypmod = exprTypmod((Node *) child_expr);
+		Oid			parent_varcollid = exprCollation((Node *) child_expr);
+
+		/*
+		 * If the child expression is either an inherited user column, or
+		 * wholerow, or ctid, it can be mapped to a parent var.  If the child
+		 * expression does not refer to a column, or a column that parent does
+		 * not contain, then we will need to make a "fake" parent column to
+		 * stand for the child expression.  We will set things up below using
+		 * the child's AppendRelInfo such that when translated, the fake parent
+		 * column becomes the child expression.  Note that these fake columns
+		 * don't leave the planner, because the parent's reltarget is never
+		 * actually computed during execution (see set_dummy_tlist_references()
+		 * and how it applies to Append and similar plan nodes).
+		 */
+		if (IsA(child_expr, Var))
+		{
+			Var   *child_var = (Var *) child_expr;
+
+			if (child_var->varattno > 0 &&
+				child_var->varattno <= appinfo->num_child_cols &&
+				appinfo->parent_colnos[child_var->varattno] > 0)
+			{
+				/* A user-defined parent column. */
+				parent_attno = appinfo->parent_colnos[child_var->varattno];
+				fake_column = false;
+			}
+			else if (child_var->varattno == 0)
+			{
+				/* wholerow */
+				parent_attno = 0;
+				parent_vartype = parentrelation->rd_rel->reltype;
+				fake_column = false;
+			}
+			else if (child_var->varattno == SelfItemPointerAttributeNumber)
+			{
+				/* ctid */
+				parent_attno = SelfItemPointerAttributeNumber;
+				fake_column = false;
+			}
+		}
+
+		/*
+		 * A fake parent column is represented by a Var with fake varattno.
+		 * We use attribute numbers starting from parent's max_attr + 1.
+		 */
+		if (fake_column)
+		{
+			int		array_size;
+
+			parent_attno = parentrelinfo->max_attr + 1;
+
+			/* Must expand attr_needed array for the new fake Var. */
+			array_size = parentrelinfo->max_attr - parentrelinfo->min_attr + 1;
+			parentrelinfo->attr_needed = (Relids *)
+					repalloc(parentrelinfo->attr_needed,
+							 (array_size + 1) * sizeof(Relids));
+			parentrelinfo->attr_widths = (int32 *)
+					repalloc(parentrelinfo->attr_widths,
+							 (array_size + 1) * sizeof(int32));
+			parentrelinfo->attr_needed[array_size] = NULL;
+			parentrelinfo->attr_widths[array_size] = 0;
+			parentrelinfo->max_attr += 1;
+		}
+
+		parent_var = makeVar(parent_varno, parent_attno, parent_vartype,
+							 parent_vartypmod, parent_varcollid, 0);
+
+		/*
+		 * Only the top-level parent's vars will make it into the top-level
+		 * tlist, so choose resno likewise.  Other TLEs containing vars of
+		 * intermediate parents only serve as placeholders for remembering
+		 * child junk attribute names and expressions so as to avoid re-adding
+		 * duplicates as the code at the beginning of this function does, so
+		 * their resnos don't need to be correct.
+		 */
+		if (parent_varno == root->parse->resultRelation)
+			resno = list_length(root->processed_tlist) + 1;
+		else
+			resno = 1;
+		tle = makeTargetEntry((Expr *) parent_var, resno, attrname, true);
+
+		root->inherit_junk_tlist = lappend(root->inherit_junk_tlist, tle);
+		if (parent_varno == root->parse->resultRelation)
+			root->processed_tlist = lappend(root->processed_tlist, tle);
+	}
+
+	/*
+	 * While appinfo->translated_vars contains child column vars mapped from
+	 * real parent column vars, we maintain a list of child expressions that
+	 * are mapped from fake parent vars in appinfo->translated_fake_vars.
+	 */
+	parent_attno = parent_var->varattno;
+	if (parent_attno > max_parent_attno)
+	{
+		int		fake_var_offset = max_parent_attno - parent_attno - 1;
+
+		/*
+		 * For parent's fake columns with attribute number smaller than the
+		 * current fake attno, we assume that they are not mapped to any
+		 * expression of this child, which is indicated by having a NULL in
+		 * the map.
+		 */
+		if (fake_var_offset > 0)
+		{
+			int		offset;
+
+			Assert(list_length(appinfo->translated_fake_vars) > 0);
+			for (offset = 0; offset < fake_var_offset; offset++)
+			{
+				/*
+				 * Don't accidentally overwrite other expressions of this
+				 * child.
+				 */
+				if (list_nth(appinfo->translated_fake_vars, offset) != NULL)
+					continue;
+
+				appinfo->translated_fake_vars =
+					lappend(appinfo->translated_fake_vars, NULL);
+			}
+
+			if (list_nth(appinfo->translated_fake_vars, offset) != NULL)
+				elog(ERROR, "fake attno %u of parent relation %u already mapped",
+					 parent_var->varattno, parent_varno);
+		}
+
+		appinfo->translated_fake_vars = lappend(appinfo->translated_fake_vars,
+												child_expr);
+	}
+}
+
+/*
+ * translate_fake_parent_var
+ * 		For a "fake" parent var, return corresponding child expression in
+ * 		appinfo->translated_fake_vars if one has been added, NULL const node
+ * 		otherwise
+ */
+Node *
+translate_fake_parent_var(Var *var, AppendRelInfo *appinfo)
+{
+	int		max_parent_attno = list_length(appinfo->translated_vars);
+	int		offset = var->varattno - max_parent_attno - 1;
+	Node   *result = NULL;
+
+	if (offset < list_length(appinfo->translated_fake_vars))
+		result = (Node *) list_nth(appinfo->translated_fake_vars, offset);
+
+	/*
+	 * It's possible for some fake parent vars to map to a valid expression
+	 * in only some child relations but not in others.  In that case, we
+	 * return a NULL const node for those other relations.
+	 */
+	if (result == NULL)
+		return (Node *) makeNullConst(var->vartype, var->vartypmod,
+									  var->varcollid);
+
+	return result;
+}
+
+/*
+ * is_result_relation
+ * 		Is passed-in relation a result relation of this query?
+ *
+ * While root->parse->resultRelation gives the query's original target
+ * relation, other target relations resulting from adding inheritance child
+ * relations of the main target relation are tracked elsewhere.  This
+ * function will return true for the RT index of any target relation.
+ */
+bool
+is_result_relation(PlannerInfo *root, Index relid)
+{
+	InheritResultRelInfo *resultInfo;
+
+	if (relid == root->parse->resultRelation)
+		return true;
+
+	/*
+	 * There can be only one result relation in a given subquery before
+	 * inheritance child relation are added.
+	 */
+	if (root->inherit_result_rel_array == NULL)
+		return false;
+
+	resultInfo = root->inherit_result_rel_array[relid];
+	if (resultInfo == NULL)
+		return false;
+
+	return (relid == resultInfo->resultRelation);
+}
+
+/*
+ * get_result_update_info
+ *		Returns update targetlist and column numbers of a given result relation
+ *
+ * Note: Don't call for a relation that is not certainly a result relation!
+ */
+void
+get_result_update_info(PlannerInfo *root, Index result_relation,
+					   List **processed_tlist,
+					   List **update_colnos)
+{
+	Assert(is_result_relation(root, result_relation));
+
+	if (result_relation == root->parse->resultRelation)
+	{
+		*processed_tlist = root->processed_tlist;
+		*update_colnos = root->update_colnos;
+	}
+	else
+	{
+		InheritResultRelInfo *resultInfo;
+
+		Assert(root->inherit_result_rel_array != NULL);
+		resultInfo = root->inherit_result_rel_array[result_relation];
+		Assert(resultInfo != NULL);
+		*processed_tlist = resultInfo->processed_tlist;
+		*update_colnos = resultInfo->update_colnos;
+	}
+}
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index a97929c13f..6b17cf098c 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -3539,6 +3539,7 @@ create_lockrows_path(PlannerInfo *root, RelOptInfo *rel,
  *	  Creates a pathnode that represents performing INSERT/UPDATE/DELETE mods
  *
  * 'rel' is the parent relation associated with the result
+ * 'subpath' is a Path producing source data
  * 'operation' is the operation type
  * 'canSetTag' is true if we set the command tag/es_processed
  * 'nominalRelation' is the parent RT index for use of EXPLAIN
@@ -3546,8 +3547,6 @@ create_lockrows_path(PlannerInfo *root, RelOptInfo *rel,
  * 'partColsUpdated' is true if any partitioning columns are being updated,
  *		either from the target relation or a descendent partitioned table.
  * 'resultRelations' is an integer list of actual RT indexes of target rel(s)
- * 'subpaths' is a list of Path(s) producing source data (one per rel)
- * 'subroots' is a list of PlannerInfo structs (one per rel)
  * 'updateColnosLists' is a list of UPDATE target column number lists
  *		(one sublist per rel); or NIL if not an UPDATE
  * 'withCheckOptionLists' is a list of WCO lists (one per rel)
@@ -3558,22 +3557,18 @@ create_lockrows_path(PlannerInfo *root, RelOptInfo *rel,
  */
 ModifyTablePath *
 create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
+						Path *subpath,
 						CmdType operation, bool canSetTag,
 						Index nominalRelation, Index rootRelation,
 						bool partColsUpdated,
-						List *resultRelations, List *subpaths,
-						List *subroots,
+						List *resultRelations,
 						List *updateColnosLists,
 						List *withCheckOptionLists, List *returningLists,
 						List *rowMarks, OnConflictExpr *onconflict,
 						int epqParam)
 {
 	ModifyTablePath *pathnode = makeNode(ModifyTablePath);
-	double		total_size;
-	ListCell   *lc;
 
-	Assert(list_length(resultRelations) == list_length(subpaths));
-	Assert(list_length(resultRelations) == list_length(subroots));
 	Assert(operation == CMD_UPDATE ?
 		   list_length(resultRelations) == list_length(updateColnosLists) :
 		   updateColnosLists == NIL);
@@ -3594,7 +3589,7 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 	pathnode->path.pathkeys = NIL;
 
 	/*
-	 * Compute cost & rowcount as sum of subpath costs & rowcounts.
+	 * Compute cost & rowcount as subpath cost & rowcount (if RETURNING)
 	 *
 	 * Currently, we don't charge anything extra for the actual table
 	 * modification work, nor for the WITH CHECK OPTIONS or RETURNING
@@ -3603,42 +3598,32 @@ create_modifytable_path(PlannerInfo *root, RelOptInfo *rel,
 	 * costs to change any higher-level planning choices.  But we might want
 	 * to make it look better sometime.
 	 */
-	pathnode->path.startup_cost = 0;
-	pathnode->path.total_cost = 0;
-	pathnode->path.rows = 0;
-	total_size = 0;
-	foreach(lc, subpaths)
+	pathnode->path.startup_cost = subpath->startup_cost;
+	pathnode->path.total_cost = subpath->total_cost;
+	if (returningLists != NIL)
 	{
-		Path	   *subpath = (Path *) lfirst(lc);
-
-		if (lc == list_head(subpaths))	/* first node? */
-			pathnode->path.startup_cost = subpath->startup_cost;
-		pathnode->path.total_cost += subpath->total_cost;
-		if (returningLists != NIL)
-		{
-			pathnode->path.rows += subpath->rows;
-			total_size += subpath->pathtarget->width * subpath->rows;
-		}
+		pathnode->path.rows = subpath->rows;
+		/*
+		 * Set width to match the subpath output.  XXX this is totally wrong:
+		 * we should return an average of the RETURNING tlist widths.  But
+		 * it's what happened historically, and improving it is a task for
+		 * another day.  (Again, it's mostly window dressing.)
+		 */
+		pathnode->path.pathtarget->width = subpath->pathtarget->width;
+	}
+	else
+	{
+		pathnode->path.rows = 0;
+		pathnode->path.pathtarget->width = 0;
 	}
 
-	/*
-	 * Set width to the average width of the subpath outputs.  XXX this is
-	 * totally wrong: we should return an average of the RETURNING tlist
-	 * widths.  But it's what happened historically, and improving it is a task
-	 * for another day.
-	 */
-	if (pathnode->path.rows > 0)
-		total_size /= pathnode->path.rows;
-	pathnode->path.pathtarget->width = rint(total_size);
-
+	pathnode->subpath = subpath;
 	pathnode->operation = operation;
 	pathnode->canSetTag = canSetTag;
 	pathnode->nominalRelation = nominalRelation;
 	pathnode->rootRelation = rootRelation;
 	pathnode->partColsUpdated = partColsUpdated;
 	pathnode->resultRelations = resultRelations;
-	pathnode->subpaths = subpaths;
-	pathnode->subroots = subroots;
 	pathnode->updateColnosLists = updateColnosLists;
 	pathnode->withCheckOptionLists = withCheckOptionLists;
 	pathnode->returningLists = returningLists;
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 6c39bf893f..d0fb3b6834 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1453,18 +1453,11 @@ relation_excluded_by_constraints(PlannerInfo *root,
 
 			/*
 			 * When constraint_exclusion is set to 'partition' we only handle
-			 * appendrel members.  Normally, they are RELOPT_OTHER_MEMBER_REL
-			 * relations, but we also consider inherited target relations as
-			 * appendrel members for the purposes of constraint exclusion
-			 * (since, indeed, they were appendrel members earlier in
-			 * inheritance_planner).
-			 *
-			 * In both cases, partition pruning was already applied, so there
-			 * is no need to consider the rel's partition constraints here.
+			 * appendrel members.  Partition pruning has already been applied,
+			 * so there is no need to consider the rel's partition constraints
+			 * here.
 			 */
-			if (rel->reloptkind == RELOPT_OTHER_MEMBER_REL ||
-				(rel->relid == root->parse->resultRelation &&
-				 root->inhTargetKind != INHKIND_NONE))
+			if (rel->reloptkind == RELOPT_OTHER_MEMBER_REL)
 				break;			/* appendrel member, so process it */
 			return false;
 
@@ -1477,9 +1470,7 @@ relation_excluded_by_constraints(PlannerInfo *root,
 			 * its partition constraints haven't been considered yet, so
 			 * include them in the processing here.
 			 */
-			if (rel->reloptkind == RELOPT_BASEREL &&
-				!(rel->relid == root->parse->resultRelation &&
-				  root->inhTargetKind != INHKIND_NONE))
+			if (rel->reloptkind == RELOPT_BASEREL)
 				include_partition = true;
 			break;				/* always try to exclude */
 	}
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 345c877aeb..9d5a4d2adb 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -108,6 +108,9 @@ setup_simple_rel_arrays(PlannerInfo *root)
 		root->simple_rte_array[rti++] = rte;
 	}
 
+	/* inherit_result_rel_array is not made here */
+	root->inherit_result_rel_array = NULL;
+
 	/* append_rel_array is not needed if there are no AppendRelInfos */
 	if (root->append_rel_list == NIL)
 	{
@@ -183,6 +186,15 @@ expand_planner_arrays(PlannerInfo *root, int add_size)
 			palloc0(sizeof(AppendRelInfo *) * new_size);
 	}
 
+	if (root->inherit_result_rel_array)
+	{
+		root->inherit_result_rel_array = (InheritResultRelInfo **)
+			repalloc(root->inherit_result_rel_array,
+					 sizeof(InheritResultRelInfo *) * new_size);
+		MemSet(root->inherit_result_rel_array + root->simple_rel_array_size,
+			   0, sizeof(InheritResultRelInfo *) * add_size);
+	}
+
 	root->simple_rel_array_size = new_size;
 }
 
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index f9175987f8..bb62708f49 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1661,8 +1661,9 @@ rewriteTargetListUD(Query *parsetree, RangeTblEntry *target_rte,
 		/*
 		 * For UPDATE, we need to make the FDW fetch unchanged columns by
 		 * asking it to fetch a whole-row Var.  That's because the top-level
-		 * targetlist only contains entries for changed columns.  (Actually,
-		 * we only really need this for UPDATEs that are not pushed to the
+		 * targetlist only contains entries for changed columns, but
+		 * ExecUpdate will need to build the complete new tuple.  (Actually,
+		 * we only really need this in UPDATEs that are not pushed to the
 		 * remote side, but it's hard to tell if that will be the case at the
 		 * point when this function is called.)
 		 *
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index f0de2a25c9..03c22c80c3 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -4572,16 +4572,12 @@ set_deparse_plan(deparse_namespace *dpns, Plan *plan)
 	 * We special-case Append and MergeAppend to pretend that the first child
 	 * plan is the OUTER referent; we have to interpret OUTER Vars in their
 	 * tlists according to one of the children, and the first one is the most
-	 * natural choice.  Likewise special-case ModifyTable to pretend that the
-	 * first child plan is the OUTER referent; this is to support RETURNING
-	 * lists containing references to non-target relations.
+	 * natural choice.
 	 */
 	if (IsA(plan, Append))
 		dpns->outer_plan = linitial(((Append *) plan)->appendplans);
 	else if (IsA(plan, MergeAppend))
 		dpns->outer_plan = linitial(((MergeAppend *) plan)->mergeplans);
-	else if (IsA(plan, ModifyTable))
-		dpns->outer_plan = linitial(((ModifyTable *) plan)->plans);
 	else
 		dpns->outer_plan = outerPlan(plan);
 
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 7af6d48525..2b79a87fe4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -666,10 +666,7 @@ typedef struct ExecRowMark
  * Each LockRows and ModifyTable node keeps a list of the rowmarks it needs to
  * deal with.  In addition to a pointer to the related entry in es_rowmarks,
  * this struct carries the column number(s) of the resjunk columns associated
- * with the rowmark (see comments for PlanRowMark for more detail).  In the
- * case of ModifyTable, there has to be a separate ExecAuxRowMark list for
- * each child plan, because the resjunk columns could be at different physical
- * column positions in different subplans.
+ * with the rowmark (see comments for PlanRowMark for more detail).
  */
 typedef struct ExecAuxRowMark
 {
@@ -1071,9 +1068,8 @@ typedef struct PlanState
  * EvalPlanQualSlot), and/or found using the rowmark mechanism (non-locking
  * rowmarks by the EPQ machinery itself, locking ones by the caller).
  *
- * While the plan to be checked may be changed using EvalPlanQualSetPlan() -
- * e.g. so all source plans for a ModifyTable node can be processed - all such
- * plans need to share the same EState.
+ * While the plan to be checked may be changed using EvalPlanQualSetPlan(),
+ * all such plans need to share the same EState.
  */
 typedef struct EPQState
 {
@@ -1167,23 +1163,26 @@ typedef struct ModifyTableState
 	CmdType		operation;		/* INSERT, UPDATE, or DELETE */
 	bool		canSetTag;		/* do we set the command tag/es_processed? */
 	bool		mt_done;		/* are we done? */
-	PlanState **mt_plans;		/* subplans (one per target rel) */
-	int			mt_nplans;		/* number of plans in the array */
-	int			mt_whichplan;	/* which one is being executed (0..n-1) */
-	TupleTableSlot **mt_scans;	/* input tuple corresponding to underlying
-								 * plans */
-	ResultRelInfo *resultRelInfo;	/* per-subplan target relations */
+	int			mt_nrels;		/* number of entries in resultRelInfo[] */
+	ResultRelInfo *resultRelInfo;	/* info about target relation(s) */
 
 	/*
 	 * Target relation mentioned in the original statement, used to fire
-	 * statement-level triggers and as the root for tuple routing.
+	 * statement-level triggers and as the root for tuple routing.  (This
+	 * might point to one of the resultRelInfo[] entries, but it can also be a
+	 * distinct struct.)
 	 */
 	ResultRelInfo *rootResultRelInfo;
 
-	List	  **mt_arowmarks;	/* per-subplan ExecAuxRowMark lists */
 	EPQState	mt_epqstate;	/* for evaluating EvalPlanQual rechecks */
 	bool		fireBSTriggers; /* do we need to fire stmt triggers? */
 
+	/*
+	 * For inherited UPDATE and DELETE, resno of the "resultrelindex" junk
+	 * attribute in the subplan's targetlist; zero in other cases.
+	 */
+	int			mt_resultIndexAttno;
+
 	/*
 	 * Slot for storing tuples in the root partitioned table's rowtype during
 	 * an UPDATE of a partitioned table.
diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h
index e22df890ef..fadd413867 100644
--- a/src/include/nodes/nodes.h
+++ b/src/include/nodes/nodes.h
@@ -270,6 +270,7 @@ typedef enum NodeTag
 	T_PlaceHolderVar,
 	T_SpecialJoinInfo,
 	T_AppendRelInfo,
+	T_InheritResultRelInfo,
 	T_PlaceHolderInfo,
 	T_MinMaxAggInfo,
 	T_PlannerParamItem,
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index bed9f4da09..da46151e46 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -77,18 +77,6 @@ typedef enum UpperRelationKind
 	/* NB: UPPERREL_FINAL must be last enum entry; it's used to size arrays */
 } UpperRelationKind;
 
-/*
- * This enum identifies which type of relation is being planned through the
- * inheritance planner.  INHKIND_NONE indicates the inheritance planner
- * was not used.
- */
-typedef enum InheritanceKind
-{
-	INHKIND_NONE,
-	INHKIND_INHERITED,
-	INHKIND_PARTITIONED
-} InheritanceKind;
-
 /*----------
  * PlannerGlobal
  *		Global information for planning/optimization
@@ -212,6 +200,14 @@ struct PlannerInfo
 	 */
 	struct AppendRelInfo **append_rel_array;
 
+	/*
+	 * Same length as other "simple" rel arrays and holds pointers to
+	 * InheritResultRelInfo for this subquery's result relations indexed by RT
+	 * index, or NULL if the rel is not a result relation.  This array is not
+	 * allocated unless the query is an inherited UPDATE/DELETE.
+	 */
+	struct InheritResultRelInfo **inherit_result_rel_array;
+
 	/*
 	 * all_baserels is a Relids set of all base relids (but not "other"
 	 * relids) in the query; that is, the Relids identifier of the final join
@@ -283,6 +279,8 @@ struct PlannerInfo
 	 */
 	List	   *append_rel_list;	/* list of AppendRelInfos */
 
+	List	   *inherit_result_rels;	/* List of InheritResultRelInfo */
+
 	List	   *rowMarks;		/* list of PlanRowMarks */
 
 	List	   *placeholder_list;	/* list of PlaceHolderInfos */
@@ -326,6 +324,9 @@ struct PlannerInfo
 	 */
 	List	   *update_colnos;
 
+	/* Scratch space for inherit.c: add_inherit_junk_var() */
+	List	   *inherit_junk_tlist;	/* List of TargetEntry */
+
 	/* Fields filled during create_plan() for use in setrefs.c */
 	AttrNumber *grouping_map;	/* for GroupingFunc fixup */
 	List	   *minmax_aggs;	/* List of MinMaxAggInfos */
@@ -341,9 +342,6 @@ struct PlannerInfo
 	Index		qual_security_level;	/* minimum security_level for quals */
 	/* Note: qual_security_level is zero if there are no securityQuals */
 
-	InheritanceKind inhTargetKind;	/* indicates if the target relation is an
-									 * inheritance child or partition or a
-									 * partitioned table */
 	bool		hasJoinRTEs;	/* true if any RTEs are RTE_JOIN kind */
 	bool		hasLateralRTEs; /* true if any RTEs are marked LATERAL */
 	bool		hasHavingQual;	/* true if havingQual was non-null */
@@ -374,6 +372,9 @@ struct PlannerInfo
 
 	/* Does this query modify any partition key columns? */
 	bool		partColsUpdated;
+
+	/* Highest result relation index assigned in this subquery */
+	int			lastResultRelIndex;
 };
 
 
@@ -1833,20 +1834,19 @@ typedef struct LockRowsPath
  * ModifyTablePath represents performing INSERT/UPDATE/DELETE modifications
  *
  * We represent most things that will be in the ModifyTable plan node
- * literally, except we have child Path(s) not Plan(s).  But analysis of the
+ * literally, except we have a child Path not Plan.  But analysis of the
  * OnConflictExpr is deferred to createplan.c, as is collection of FDW data.
  */
 typedef struct ModifyTablePath
 {
 	Path		path;
+	Path	   *subpath;		/* Path producing source data */
 	CmdType		operation;		/* INSERT, UPDATE, or DELETE */
 	bool		canSetTag;		/* do we set the command tag/es_processed? */
 	Index		nominalRelation;	/* Parent RT index for use of EXPLAIN */
 	Index		rootRelation;	/* Root RT index, if target is partitioned */
-	bool		partColsUpdated;	/* some part key in hierarchy updated */
+	bool		partColsUpdated;	/* some part key in hierarchy updated? */
 	List	   *resultRelations;	/* integer list of RT indexes */
-	List	   *subpaths;		/* Path(s) producing source data */
-	List	   *subroots;		/* per-target-table PlannerInfos */
 	List	   *updateColnosLists; /* per-target-table update_colnos lists */
 	List	   *withCheckOptionLists;	/* per-target-table WCO lists */
 	List	   *returningLists; /* per-target-table RETURNING tlists */
@@ -2286,6 +2286,13 @@ typedef struct AppendRelInfo
 	 */
 	List	   *translated_vars;	/* Expressions in the child's Vars */
 
+	/*
+	 * The following contains expressions that the child relation is expected
+	 * to output for each "fake" parent Var that add_inherit_junk_var() adds
+	 * to the parent's reltarget; also see translate_fake_parent_var().
+	 */
+	List	   *translated_fake_vars;
+
 	/*
 	 * This array simplifies translations in the reverse direction, from
 	 * child's column numbers to parent's.  The entry at [ccolno - 1] is the
@@ -2303,6 +2310,40 @@ typedef struct AppendRelInfo
 	Oid			parent_reloid;	/* OID of parent relation */
 } AppendRelInfo;
 
+/*
+ * InheritResultRelInfo
+ *		Information about result relations of an inherited UPDATE/DELETE
+ *		operation
+ *
+ * If the main target relation is an inheritance parent, we build an
+ * InheritResultRelInfo for it and for every child result relation resulting
+ * from expanding it.  This is to store the information relevant to each
+ * result relation that must be added to the ModifyTable, such as its update
+ * targetlist, WITH CHECK OPTIONS, and RETURNING expression lists.  For the
+ * main result relation (root inheritance parent), that information is same
+ * as what's in Query and PlannerInfo.  For child result relations, we make
+ * copies of those expressions with appropriate translation of any Vars.
+ * Also, update_colnos for a given child relation has been adjusted to show
+ * that relation's attribute numbers.
+ *
+ * While it's okay for the code outside of the core planner to look at
+ * update_colnos, processed_tlist is only kept around for internal planner use.
+ * For example, an FDW's PlanDirectModify() may look at update_colnos to check
+ * if the assigned expressions are pushable.
+ */
+typedef struct InheritResultRelInfo
+{
+	NodeTag		type;
+
+	Index		resultRelation;
+	List	   *withCheckOptions;
+	List	   *returningList;
+
+	/* Only valid for UPDATE. */
+	List	   *processed_tlist;
+	List	   *update_colnos;
+} InheritResultRelInfo;
+
 /*
  * For each distinct placeholder expression generated during planning, we
  * store a PlaceHolderInfo node in the PlannerInfo node's placeholder_list.
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 7d74bd92b8..f371390f7f 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -196,7 +196,7 @@ typedef struct ProjectSet
 
 /* ----------------
  *	 ModifyTable node -
- *		Apply rows produced by subplan(s) to result table(s),
+ *		Apply rows produced by outer plan to result table(s),
  *		by inserting, updating, or deleting.
  *
  * If the originally named target table is a partitioned table, both
@@ -206,7 +206,7 @@ typedef struct ProjectSet
  * EXPLAIN should claim is the INSERT/UPDATE/DELETE target.
  *
  * Note that rowMarks and epqParam are presumed to be valid for all the
- * subplan(s); they can't contain any info that varies across subplans.
+ * table(s); they can't contain any info that varies across tables.
  * ----------------
  */
 typedef struct ModifyTable
@@ -216,9 +216,8 @@ typedef struct ModifyTable
 	bool		canSetTag;		/* do we set the command tag/es_processed? */
 	Index		nominalRelation;	/* Parent RT index for use of EXPLAIN */
 	Index		rootRelation;	/* Root RT index, if target is partitioned */
-	bool		partColsUpdated;	/* some part key in hierarchy updated */
+	bool		partColsUpdated;	/* some part key in hierarchy updated? */
 	List	   *resultRelations;	/* integer list of RT indexes */
-	List	   *plans;			/* plan(s) producing source data */
 	List	   *updateColnosLists; /* per-target-table update_colnos lists */
 	List	   *withCheckOptionLists;	/* per-target-table WCO lists */
 	List	   *returningLists; /* per-target-table RETURNING tlists */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index d4ce037088..193fbf0e0e 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1386,10 +1386,14 @@ typedef struct InferenceElem
  * column for the item; so there may be missing or out-of-order resnos.
  * It is even legal to have duplicated resnos; consider
  *		UPDATE table SET arraycol[1] = ..., arraycol[2] = ..., ...
- * The two meanings come together in the executor, because the planner
- * transforms INSERT/UPDATE tlists into a normalized form with exactly
- * one entry for each column of the destination table.  Before that's
- * happened, however, it is risky to assume that resno == position.
+ * In an INSERT, the rewriter and planner will normalize the tlist by
+ * reordering it into physical column order and filling in default values
+ * for any columns not assigned values by the original query.  In an UPDATE,
+ * no such thing ever happens; the tlist elements are eventually renumbered
+ * to match their ordinal positions, but this has nothing to do with which
+ * table column will be updated.  (Look to the update column numbers list,
+ * which parallels the finished tlist, to find that out.)
+ *
  * Generally get_tle_by_resno() should be used rather than list_nth()
  * to fetch tlist entries by resno, and only in SELECT should you assume
  * that resno is a unique identifier.
diff --git a/src/include/optimizer/appendinfo.h b/src/include/optimizer/appendinfo.h
index 4cbf8c26cc..a52333a364 100644
--- a/src/include/optimizer/appendinfo.h
+++ b/src/include/optimizer/appendinfo.h
@@ -22,6 +22,8 @@ extern AppendRelInfo *make_append_rel_info(Relation parentrel,
 										   Index parentRTindex, Index childRTindex);
 extern Node *adjust_appendrel_attrs(PlannerInfo *root, Node *node,
 									int nappinfos, AppendRelInfo **appinfos);
+extern Node *adjust_target_appendrel_attrs(PlannerInfo *root, Node *node,
+									AppendRelInfo *appinfo);
 extern Node *adjust_appendrel_attrs_multilevel(PlannerInfo *root, Node *node,
 											   Relids child_relids,
 											   Relids top_parent_relids);
@@ -31,5 +33,6 @@ extern Relids adjust_child_relids_multilevel(PlannerInfo *root, Relids relids,
 											 Relids child_relids, Relids top_parent_relids);
 extern AppendRelInfo **find_appinfos_by_relids(PlannerInfo *root,
 											   Relids relids, int *nappinfos);
+extern Node *translate_fake_parent_var(Var *var, AppendRelInfo *appinfo);
 
 #endif							/* APPENDINFO_H */
diff --git a/src/include/optimizer/inherit.h b/src/include/optimizer/inherit.h
index e9472f2f73..1c27d9123d 100644
--- a/src/include/optimizer/inherit.h
+++ b/src/include/optimizer/inherit.h
@@ -24,4 +24,10 @@ extern bool apply_child_basequals(PlannerInfo *root, RelOptInfo *parentrel,
 								  RelOptInfo *childrel, RangeTblEntry *childRTE,
 								  AppendRelInfo *appinfo);
 
+extern bool is_result_relation(PlannerInfo *root, Index relid);
+
+extern void get_result_update_info(PlannerInfo *root, Index result_relation,
+								   List **processed_tlist,
+								   List **update_colnos);
+
 #endif							/* INHERIT_H */
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 9673a4a638..d539bc2783 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -260,11 +260,11 @@ extern LockRowsPath *create_lockrows_path(PlannerInfo *root, RelOptInfo *rel,
 										  Path *subpath, List *rowMarks, int epqParam);
 extern ModifyTablePath *create_modifytable_path(PlannerInfo *root,
 												RelOptInfo *rel,
+												Path *subpath,
 												CmdType operation, bool canSetTag,
 												Index nominalRelation, Index rootRelation,
 												bool partColsUpdated,
-												List *resultRelations, List *subpaths,
-												List *subroots,
+												List *resultRelations,
 												List *updateColnosLists,
 												List *withCheckOptionLists, List *returningLists,
 												List *rowMarks, OnConflictExpr *onconflict,
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 94e43c3410..d4b112c5e4 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -545,27 +545,25 @@ create table some_tab_child () inherits (some_tab);
 insert into some_tab_child values(1,2);
 explain (verbose, costs off)
 update some_tab set a = a + 1 where false;
-           QUERY PLAN           
---------------------------------
+                   QUERY PLAN                    
+-------------------------------------------------
  Update on public.some_tab
-   Update on public.some_tab
    ->  Result
-         Output: (a + 1), ctid
+         Output: (some_tab.a + 1), some_tab.ctid
          One-Time Filter: false
-(5 rows)
+(4 rows)
 
 update some_tab set a = a + 1 where false;
 explain (verbose, costs off)
 update some_tab set a = a + 1 where false returning b, a;
-           QUERY PLAN           
---------------------------------
+                   QUERY PLAN                    
+-------------------------------------------------
  Update on public.some_tab
-   Output: b, a
-   Update on public.some_tab
+   Output: some_tab.b, some_tab.a
    ->  Result
-         Output: (a + 1), ctid
+         Output: (some_tab.a + 1), some_tab.ctid
          One-Time Filter: false
-(6 rows)
+(5 rows)
 
 update some_tab set a = a + 1 where false returning b, a;
  b | a 
@@ -670,7 +668,7 @@ explain update parted_tab set a = 2 where false;
                        QUERY PLAN                       
 --------------------------------------------------------
  Update on parted_tab  (cost=0.00..0.00 rows=0 width=0)
-   ->  Result  (cost=0.00..0.00 rows=0 width=0)
+   ->  Result  (cost=0.00..0.00 rows=0 width=10)
          One-Time Filter: false
 (3 rows)
 
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index ff157ceb1c..73c0f3e04b 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -212,7 +212,7 @@ explain (costs off, format json) insert into insertconflicttest values (0, 'Bilb
        "Plans": [                                                      +
          {                                                             +
            "Node Type": "Result",                                      +
-           "Parent Relationship": "Member",                            +
+           "Parent Relationship": "Outer",                             +
            "Parallel Aware": false                                     +
          }                                                             +
        ]                                                               +
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 0057f41caa..27f7525b3e 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -1926,37 +1926,27 @@ WHERE EXISTS (
     FROM int4_tbl,
          LATERAL (SELECT int4_tbl.f1 FROM int8_tbl LIMIT 2) ss
     WHERE prt1_l.c IS NULL);
-                          QUERY PLAN                           
----------------------------------------------------------------
+                        QUERY PLAN                        
+----------------------------------------------------------
  Delete on prt1_l
    Delete on prt1_l_p1 prt1_l_1
    Delete on prt1_l_p3_p1 prt1_l_2
    Delete on prt1_l_p3_p2 prt1_l_3
    ->  Nested Loop Semi Join
-         ->  Seq Scan on prt1_l_p1 prt1_l_1
-               Filter: (c IS NULL)
-         ->  Nested Loop
-               ->  Seq Scan on int4_tbl
-               ->  Subquery Scan on ss
-                     ->  Limit
-                           ->  Seq Scan on int8_tbl
-   ->  Nested Loop Semi Join
-         ->  Seq Scan on prt1_l_p3_p1 prt1_l_2
-               Filter: (c IS NULL)
-         ->  Nested Loop
-               ->  Seq Scan on int4_tbl
-               ->  Subquery Scan on ss_1
-                     ->  Limit
-                           ->  Seq Scan on int8_tbl int8_tbl_1
-   ->  Nested Loop Semi Join
-         ->  Seq Scan on prt1_l_p3_p2 prt1_l_3
-               Filter: (c IS NULL)
-         ->  Nested Loop
-               ->  Seq Scan on int4_tbl
-               ->  Subquery Scan on ss_2
-                     ->  Limit
-                           ->  Seq Scan on int8_tbl int8_tbl_2
-(28 rows)
+         ->  Append
+               ->  Seq Scan on prt1_l_p1 prt1_l_1
+                     Filter: (c IS NULL)
+               ->  Seq Scan on prt1_l_p3_p1 prt1_l_2
+                     Filter: (c IS NULL)
+               ->  Seq Scan on prt1_l_p3_p2 prt1_l_3
+                     Filter: (c IS NULL)
+         ->  Materialize
+               ->  Nested Loop
+                     ->  Seq Scan on int4_tbl
+                     ->  Subquery Scan on ss
+                           ->  Limit
+                                 ->  Seq Scan on int8_tbl
+(18 rows)
 
 --
 -- negative testcases
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index bde29e38a9..c4e827caec 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2463,74 +2463,43 @@ deallocate ab_q6;
 insert into ab values (1,2);
 explain (analyze, costs off, summary off, timing off)
 update ab_a1 set b = 3 from ab where ab.a = 1 and ab.a = ab_a1.a;
-                                     QUERY PLAN                                      
--------------------------------------------------------------------------------------
+                                        QUERY PLAN                                         
+-------------------------------------------------------------------------------------------
  Update on ab_a1 (actual rows=0 loops=1)
    Update on ab_a1_b1 ab_a1_1
    Update on ab_a1_b2 ab_a1_2
    Update on ab_a1_b3 ab_a1_3
-   ->  Nested Loop (actual rows=0 loops=1)
-         ->  Append (actual rows=1 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
-                     Recheck Cond: (a = 1)
-                     Heap Blocks: exact=1
-                     ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
-         ->  Materialize (actual rows=0 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_a1_1 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
    ->  Nested Loop (actual rows=1 loops=1)
          ->  Append (actual rows=1 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+               ->  Bitmap Heap Scan on ab_a1_b1 ab_a1_1 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
                            Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
-                     Recheck Cond: (a = 1)
-                     Heap Blocks: exact=1
-                     ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-         ->  Materialize (actual rows=1 loops=1)
                ->  Bitmap Heap Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
                      Recheck Cond: (a = 1)
                      Heap Blocks: exact=1
                      ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
-   ->  Nested Loop (actual rows=0 loops=1)
-         ->  Append (actual rows=1 loops=1)
-               ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
-                     Recheck Cond: (a = 1)
-                     Heap Blocks: exact=1
-                     ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-               ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
-                     Recheck Cond: (a = 1)
-                     ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
-                           Index Cond: (a = 1)
-         ->  Materialize (actual rows=0 loops=1)
                ->  Bitmap Heap Scan on ab_a1_b3 ab_a1_3 (actual rows=0 loops=1)
                      Recheck Cond: (a = 1)
                      ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
                            Index Cond: (a = 1)
-(65 rows)
+         ->  Materialize (actual rows=1 loops=1)
+               ->  Append (actual rows=1 loops=1)
+                     ->  Bitmap Heap Scan on ab_a1_b1 ab_1 (actual rows=0 loops=1)
+                           Recheck Cond: (a = 1)
+                           ->  Bitmap Index Scan on ab_a1_b1_a_idx (actual rows=0 loops=1)
+                                 Index Cond: (a = 1)
+                     ->  Bitmap Heap Scan on ab_a1_b2 ab_2 (actual rows=1 loops=1)
+                           Recheck Cond: (a = 1)
+                           Heap Blocks: exact=1
+                           ->  Bitmap Index Scan on ab_a1_b2_a_idx (actual rows=1 loops=1)
+                                 Index Cond: (a = 1)
+                     ->  Bitmap Heap Scan on ab_a1_b3 ab_3 (actual rows=0 loops=1)
+                           Recheck Cond: (a = 1)
+                           ->  Bitmap Index Scan on ab_a1_b3_a_idx (actual rows=1 loops=1)
+                                 Index Cond: (a = 1)
+(34 rows)
 
 table ab;
  a | b 
@@ -2551,29 +2520,12 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);
    Update on ab_a1_b3 ab_a1_3
    InitPlan 1 (returns $0)
      ->  Result (actual rows=1 loops=1)
-   ->  Nested Loop (actual rows=1 loops=1)
-         ->  Seq Scan on ab_a1_b1 ab_a1_1 (actual rows=1 loops=1)
-         ->  Materialize (actual rows=1 loops=1)
-               ->  Append (actual rows=1 loops=1)
-                     ->  Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1 loops=1)
-                           Filter: (b = $0)
-                     ->  Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
-                           Filter: (b = $0)
-                     ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
-                           Filter: (b = $0)
-   ->  Nested Loop (actual rows=1 loops=1)
-         ->  Seq Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
-         ->  Materialize (actual rows=1 loops=1)
-               ->  Append (actual rows=1 loops=1)
-                     ->  Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1 loops=1)
-                           Filter: (b = $0)
-                     ->  Seq Scan on ab_a2_b2 ab_a2_2 (never executed)
-                           Filter: (b = $0)
-                     ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
-                           Filter: (b = $0)
-   ->  Nested Loop (actual rows=1 loops=1)
-         ->  Seq Scan on ab_a1_b3 ab_a1_3 (actual rows=1 loops=1)
-         ->  Materialize (actual rows=1 loops=1)
+   ->  Nested Loop (actual rows=3 loops=1)
+         ->  Append (actual rows=3 loops=1)
+               ->  Seq Scan on ab_a1_b1 ab_a1_1 (actual rows=1 loops=1)
+               ->  Seq Scan on ab_a1_b2 ab_a1_2 (actual rows=1 loops=1)
+               ->  Seq Scan on ab_a1_b3 ab_a1_3 (actual rows=1 loops=1)
+         ->  Materialize (actual rows=1 loops=3)
                ->  Append (actual rows=1 loops=1)
                      ->  Seq Scan on ab_a2_b1 ab_a2_1 (actual rows=1 loops=1)
                            Filter: (b = $0)
@@ -2581,7 +2533,7 @@ update ab_a1 set b = 3 from ab_a2 where ab_a2.b = (select 1);
                            Filter: (b = $0)
                      ->  Seq Scan on ab_a2_b3 ab_a2_3 (never executed)
                            Filter: (b = $0)
-(36 rows)
+(19 rows)
 
 select tableoid::regclass, * from ab;
  tableoid | a | b 
@@ -3420,28 +3372,30 @@ explain (costs off) select * from pp_lp where a = 1;
 (5 rows)
 
 explain (costs off) update pp_lp set value = 10 where a = 1;
-            QUERY PLAN            
-----------------------------------
+               QUERY PLAN               
+----------------------------------------
  Update on pp_lp
    Update on pp_lp1 pp_lp_1
    Update on pp_lp2 pp_lp_2
-   ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
-   ->  Seq Scan on pp_lp2 pp_lp_2
-         Filter: (a = 1)
-(7 rows)
+   ->  Append
+         ->  Seq Scan on pp_lp1 pp_lp_1
+               Filter: (a = 1)
+         ->  Seq Scan on pp_lp2 pp_lp_2
+               Filter: (a = 1)
+(8 rows)
 
 explain (costs off) delete from pp_lp where a = 1;
-            QUERY PLAN            
-----------------------------------
+               QUERY PLAN               
+----------------------------------------
  Delete on pp_lp
    Delete on pp_lp1 pp_lp_1
    Delete on pp_lp2 pp_lp_2
-   ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
-   ->  Seq Scan on pp_lp2 pp_lp_2
-         Filter: (a = 1)
-(7 rows)
+   ->  Append
+         ->  Seq Scan on pp_lp1 pp_lp_1
+               Filter: (a = 1)
+         ->  Seq Scan on pp_lp2 pp_lp_2
+               Filter: (a = 1)
+(8 rows)
 
 set constraint_exclusion = 'off'; -- this should not affect the result.
 explain (costs off) select * from pp_lp where a = 1;
@@ -3455,28 +3409,30 @@ explain (costs off) select * from pp_lp where a = 1;
 (5 rows)
 
 explain (costs off) update pp_lp set value = 10 where a = 1;
-            QUERY PLAN            
-----------------------------------
+               QUERY PLAN               
+----------------------------------------
  Update on pp_lp
    Update on pp_lp1 pp_lp_1
    Update on pp_lp2 pp_lp_2
-   ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
-   ->  Seq Scan on pp_lp2 pp_lp_2
-         Filter: (a = 1)
-(7 rows)
+   ->  Append
+         ->  Seq Scan on pp_lp1 pp_lp_1
+               Filter: (a = 1)
+         ->  Seq Scan on pp_lp2 pp_lp_2
+               Filter: (a = 1)
+(8 rows)
 
 explain (costs off) delete from pp_lp where a = 1;
-            QUERY PLAN            
-----------------------------------
+               QUERY PLAN               
+----------------------------------------
  Delete on pp_lp
    Delete on pp_lp1 pp_lp_1
    Delete on pp_lp2 pp_lp_2
-   ->  Seq Scan on pp_lp1 pp_lp_1
-         Filter: (a = 1)
-   ->  Seq Scan on pp_lp2 pp_lp_2
-         Filter: (a = 1)
-(7 rows)
+   ->  Append
+         ->  Seq Scan on pp_lp1 pp_lp_1
+               Filter: (a = 1)
+         ->  Seq Scan on pp_lp2 pp_lp_2
+               Filter: (a = 1)
+(8 rows)
 
 drop table pp_lp;
 -- Ensure enable_partition_prune does not affect non-partitioned tables.
@@ -3500,28 +3456,31 @@ explain (costs off) select * from inh_lp where a = 1;
 (5 rows)
 
 explain (costs off) update inh_lp set value = 10 where a = 1;
-             QUERY PLAN             
-------------------------------------
+                   QUERY PLAN                   
+------------------------------------------------
  Update on inh_lp
-   Update on inh_lp
-   Update on inh_lp1 inh_lp_1
-   ->  Seq Scan on inh_lp
-         Filter: (a = 1)
-   ->  Seq Scan on inh_lp1 inh_lp_1
-         Filter: (a = 1)
-(7 rows)
+   Update on inh_lp inh_lp_1
+   Update on inh_lp1 inh_lp_2
+   ->  Result
+         ->  Append
+               ->  Seq Scan on inh_lp inh_lp_1
+                     Filter: (a = 1)
+               ->  Seq Scan on inh_lp1 inh_lp_2
+                     Filter: (a = 1)
+(9 rows)
 
 explain (costs off) delete from inh_lp where a = 1;
-             QUERY PLAN             
-------------------------------------
+                QUERY PLAN                
+------------------------------------------
  Delete on inh_lp
-   Delete on inh_lp
-   Delete on inh_lp1 inh_lp_1
-   ->  Seq Scan on inh_lp
-         Filter: (a = 1)
-   ->  Seq Scan on inh_lp1 inh_lp_1
-         Filter: (a = 1)
-(7 rows)
+   Delete on inh_lp inh_lp_1
+   Delete on inh_lp1 inh_lp_2
+   ->  Append
+         ->  Seq Scan on inh_lp inh_lp_1
+               Filter: (a = 1)
+         ->  Seq Scan on inh_lp1 inh_lp_2
+               Filter: (a = 1)
+(8 rows)
 
 -- Ensure we don't exclude normal relations when we only expect to exclude
 -- inheritance children
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 9506aaef82..b02a682471 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -1632,19 +1632,21 @@ EXPLAIN (COSTS OFF) EXECUTE p2(2);
 --
 SET SESSION AUTHORIZATION regress_rls_bob;
 EXPLAIN (COSTS OFF) UPDATE t1 SET b = b || b WHERE f_leak(b);
-                  QUERY PLAN                   
------------------------------------------------
+                        QUERY PLAN                         
+-----------------------------------------------------------
  Update on t1
-   Update on t1
-   Update on t2 t1_1
-   Update on t3 t1_2
-   ->  Seq Scan on t1
-         Filter: (((a % 2) = 0) AND f_leak(b))
-   ->  Seq Scan on t2 t1_1
-         Filter: (((a % 2) = 0) AND f_leak(b))
-   ->  Seq Scan on t3 t1_2
-         Filter: (((a % 2) = 0) AND f_leak(b))
-(10 rows)
+   Update on t1 t1_1
+   Update on t2 t1_2
+   Update on t3 t1_3
+   ->  Result
+         ->  Append
+               ->  Seq Scan on t1 t1_1
+                     Filter: (((a % 2) = 0) AND f_leak(b))
+               ->  Seq Scan on t2 t1_2
+                     Filter: (((a % 2) = 0) AND f_leak(b))
+               ->  Seq Scan on t3 t1_3
+                     Filter: (((a % 2) = 0) AND f_leak(b))
+(12 rows)
 
 UPDATE t1 SET b = b || b WHERE f_leak(b);
 NOTICE:  f_leak => bbb
@@ -1722,31 +1724,27 @@ NOTICE:  f_leak => cde
 NOTICE:  f_leak => yyyyyy
 EXPLAIN (COSTS OFF) UPDATE t1 SET b=t1.b FROM t2
 WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
-                           QUERY PLAN                            
------------------------------------------------------------------
+                              QUERY PLAN                               
+-----------------------------------------------------------------------
  Update on t1
-   Update on t1
-   Update on t2 t1_1
-   Update on t3 t1_2
-   ->  Nested Loop
-         ->  Seq Scan on t1
-               Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
-         ->  Seq Scan on t2
-               Filter: ((a = 3) AND ((a % 2) = 1) AND f_leak(b))
-   ->  Nested Loop
-         ->  Seq Scan on t2 t1_1
-               Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
-         ->  Seq Scan on t2
-               Filter: ((a = 3) AND ((a % 2) = 1) AND f_leak(b))
+   Update on t1 t1_1
+   Update on t2 t1_2
+   Update on t3 t1_3
    ->  Nested Loop
-         ->  Seq Scan on t3 t1_2
-               Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
          ->  Seq Scan on t2
                Filter: ((a = 3) AND ((a % 2) = 1) AND f_leak(b))
-(19 rows)
+         ->  Append
+               ->  Seq Scan on t1 t1_1
+                     Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
+               ->  Seq Scan on t2 t1_2
+                     Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
+               ->  Seq Scan on t3 t1_3
+                     Filter: ((a = 3) AND ((a % 2) = 0) AND f_leak(b))
+(14 rows)
 
 UPDATE t1 SET b=t1.b FROM t2
 WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
+NOTICE:  f_leak => cde
 EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t1
 WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
                               QUERY PLAN                               
@@ -1795,46 +1793,30 @@ NOTICE:  f_leak => cde
 EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
 WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
 AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
-                              QUERY PLAN                               
------------------------------------------------------------------------
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
  Update on t1 t1_1
-   Update on t1 t1_1
-   Update on t2 t1_1_1
-   Update on t3 t1_1_2
+   Update on t1 t1_1_1
+   Update on t2 t1_1_2
+   Update on t3 t1_1_3
    ->  Nested Loop
          Join Filter: (t1_1.b = t1_2.b)
-         ->  Seq Scan on t1 t1_1
-               Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-         ->  Append
-               ->  Seq Scan on t1 t1_2_1
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t2 t1_2_2
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t3 t1_2_3
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-   ->  Nested Loop
-         Join Filter: (t1_1_1.b = t1_2.b)
-         ->  Seq Scan on t2 t1_1_1
-               Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
          ->  Append
-               ->  Seq Scan on t1 t1_2_1
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t2 t1_2_2
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t3 t1_2_3
+               ->  Seq Scan on t1 t1_1_1
                      Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-   ->  Nested Loop
-         Join Filter: (t1_1_2.b = t1_2.b)
-         ->  Seq Scan on t3 t1_1_2
-               Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-         ->  Append
-               ->  Seq Scan on t1 t1_2_1
+               ->  Seq Scan on t2 t1_1_2
                      Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t2 t1_2_2
+               ->  Seq Scan on t3 t1_1_3
                      Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-               ->  Seq Scan on t3 t1_2_3
-                     Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
-(37 rows)
+         ->  Materialize
+               ->  Append
+                     ->  Seq Scan on t1 t1_2_1
+                           Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
+                     ->  Seq Scan on t2 t1_2_2
+                           Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
+                     ->  Seq Scan on t3 t1_2_3
+                           Filter: ((a = 4) AND ((a % 2) = 0) AND f_leak(b))
+(21 rows)
 
 UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
 WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
@@ -1842,8 +1824,6 @@ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
 NOTICE:  f_leak => daddad_updt
 NOTICE:  f_leak => daddad_updt
 NOTICE:  f_leak => defdef
-NOTICE:  f_leak => defdef
-NOTICE:  f_leak => daddad_updt
 NOTICE:  f_leak => defdef
  id  | a |      b      | id  | a |      b      |        t1_1         |        t1_2         
 -----+---+-------------+-----+---+-------------+---------------------+---------------------
@@ -1880,19 +1860,20 @@ EXPLAIN (COSTS OFF) DELETE FROM only t1 WHERE f_leak(b);
 (3 rows)
 
 EXPLAIN (COSTS OFF) DELETE FROM t1 WHERE f_leak(b);
-                  QUERY PLAN                   
------------------------------------------------
+                     QUERY PLAN                      
+-----------------------------------------------------
  Delete on t1
-   Delete on t1
-   Delete on t2 t1_1
-   Delete on t3 t1_2
-   ->  Seq Scan on t1
-         Filter: (((a % 2) = 0) AND f_leak(b))
-   ->  Seq Scan on t2 t1_1
-         Filter: (((a % 2) = 0) AND f_leak(b))
-   ->  Seq Scan on t3 t1_2
-         Filter: (((a % 2) = 0) AND f_leak(b))
-(10 rows)
+   Delete on t1 t1_1
+   Delete on t2 t1_2
+   Delete on t3 t1_3
+   ->  Append
+         ->  Seq Scan on t1 t1_1
+               Filter: (((a % 2) = 0) AND f_leak(b))
+         ->  Seq Scan on t2 t1_2
+               Filter: (((a % 2) = 0) AND f_leak(b))
+         ->  Seq Scan on t3 t1_3
+               Filter: (((a % 2) = 0) AND f_leak(b))
+(11 rows)
 
 DELETE FROM only t1 WHERE f_leak(b) RETURNING tableoid::regclass, *, t1;
 NOTICE:  f_leak => bbbbbb_updt
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 770eab38b5..d759d3a896 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -1607,26 +1607,21 @@ UPDATE rw_view1 SET a = a + 1000 FROM other_tbl_parent WHERE a = id;
                                QUERY PLAN                                
 -------------------------------------------------------------------------
  Update on base_tbl_parent
-   Update on base_tbl_parent
-   Update on base_tbl_child base_tbl_parent_1
-   ->  Hash Join
-         Hash Cond: (other_tbl_parent.id = base_tbl_parent.a)
-         ->  Append
-               ->  Seq Scan on other_tbl_parent other_tbl_parent_1
-               ->  Seq Scan on other_tbl_child other_tbl_parent_2
-         ->  Hash
-               ->  Seq Scan on base_tbl_parent
+   Update on base_tbl_parent base_tbl_parent_1
+   Update on base_tbl_child base_tbl_parent_2
    ->  Merge Join
-         Merge Cond: (base_tbl_parent_1.a = other_tbl_parent.id)
+         Merge Cond: (base_tbl_parent.a = other_tbl_parent.id)
          ->  Sort
-               Sort Key: base_tbl_parent_1.a
-               ->  Seq Scan on base_tbl_child base_tbl_parent_1
+               Sort Key: base_tbl_parent.a
+               ->  Append
+                     ->  Seq Scan on base_tbl_parent base_tbl_parent_1
+                     ->  Seq Scan on base_tbl_child base_tbl_parent_2
          ->  Sort
                Sort Key: other_tbl_parent.id
                ->  Append
                      ->  Seq Scan on other_tbl_parent other_tbl_parent_1
                      ->  Seq Scan on other_tbl_child other_tbl_parent_2
-(20 rows)
+(15 rows)
 
 UPDATE rw_view1 SET a = a + 1000 FROM other_tbl_parent WHERE a = id;
 SELECT * FROM ONLY base_tbl_parent ORDER BY a;
@@ -2332,36 +2327,39 @@ SELECT * FROM v1 WHERE a=8;
 
 EXPLAIN (VERBOSE, COSTS OFF)
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
-                                       QUERY PLAN                                        
------------------------------------------------------------------------------------------
+                                             QUERY PLAN                                              
+-----------------------------------------------------------------------------------------------------
  Update on public.t1
-   Update on public.t1
-   Update on public.t11 t1_1
-   Update on public.t12 t1_2
-   Update on public.t111 t1_3
-   ->  Index Scan using t1_a_idx on public.t1
-         Output: 100, t1.ctid
-         Index Cond: ((t1.a > 5) AND (t1.a < 7))
-         Filter: ((t1.a <> 6) AND (SubPlan 1) AND snoop(t1.a) AND leakproof(t1.a))
-         SubPlan 1
-           ->  Append
-                 ->  Seq Scan on public.t12 t12_1
-                       Filter: (t12_1.a = t1.a)
-                 ->  Seq Scan on public.t111 t12_2
-                       Filter: (t12_2.a = t1.a)
-   ->  Index Scan using t11_a_idx on public.t11 t1_1
-         Output: 100, t1_1.ctid
-         Index Cond: ((t1_1.a > 5) AND (t1_1.a < 7))
-         Filter: ((t1_1.a <> 6) AND (SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
-   ->  Index Scan using t12_a_idx on public.t12 t1_2
-         Output: 100, t1_2.ctid
-         Index Cond: ((t1_2.a > 5) AND (t1_2.a < 7))
-         Filter: ((t1_2.a <> 6) AND (SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
-   ->  Index Scan using t111_a_idx on public.t111 t1_3
-         Output: 100, t1_3.ctid
-         Index Cond: ((t1_3.a > 5) AND (t1_3.a < 7))
-         Filter: ((t1_3.a <> 6) AND (SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
-(27 rows)
+   Update on public.t1 t1_1
+   Update on public.t11 t1_2
+   Update on public.t12 t1_3
+   Update on public.t111 t1_4
+   ->  Result
+         Output: 100, t1.ctid, (0)
+         ->  Append
+               ->  Index Scan using t1_a_idx on public.t1 t1_1
+                     Output: t1_1.ctid, 0
+                     Index Cond: ((t1_1.a > 5) AND (t1_1.a < 7))
+                     Filter: ((t1_1.a <> 6) AND (SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
+                     SubPlan 1
+                       ->  Append
+                             ->  Seq Scan on public.t12 t12_1
+                                   Filter: (t12_1.a = t1_1.a)
+                             ->  Seq Scan on public.t111 t12_2
+                                   Filter: (t12_2.a = t1_1.a)
+               ->  Index Scan using t11_a_idx on public.t11 t1_2
+                     Output: t1_2.ctid, 1
+                     Index Cond: ((t1_2.a > 5) AND (t1_2.a < 7))
+                     Filter: ((t1_2.a <> 6) AND (SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
+               ->  Index Scan using t12_a_idx on public.t12 t1_3
+                     Output: t1_3.ctid, 2
+                     Index Cond: ((t1_3.a > 5) AND (t1_3.a < 7))
+                     Filter: ((t1_3.a <> 6) AND (SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
+               ->  Index Scan using t111_a_idx on public.t111 t1_4
+                     Output: t1_4.ctid, 3
+                     Index Cond: ((t1_4.a > 5) AND (t1_4.a < 7))
+                     Filter: ((t1_4.a <> 6) AND (SubPlan 1) AND snoop(t1_4.a) AND leakproof(t1_4.a))
+(30 rows)
 
 UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
 SELECT * FROM v1 WHERE a=100; -- Nothing should have been changed to 100
@@ -2376,36 +2374,39 @@ SELECT * FROM t1 WHERE a=100; -- Nothing should have been changed to 100
 
 EXPLAIN (VERBOSE, COSTS OFF)
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
-                              QUERY PLAN                               
------------------------------------------------------------------------
+                                    QUERY PLAN                                     
+-----------------------------------------------------------------------------------
  Update on public.t1
-   Update on public.t1
-   Update on public.t11 t1_1
-   Update on public.t12 t1_2
-   Update on public.t111 t1_3
-   ->  Index Scan using t1_a_idx on public.t1
-         Output: (t1.a + 1), t1.ctid
-         Index Cond: ((t1.a > 5) AND (t1.a = 8))
-         Filter: ((SubPlan 1) AND snoop(t1.a) AND leakproof(t1.a))
-         SubPlan 1
-           ->  Append
-                 ->  Seq Scan on public.t12 t12_1
-                       Filter: (t12_1.a = t1.a)
-                 ->  Seq Scan on public.t111 t12_2
-                       Filter: (t12_2.a = t1.a)
-   ->  Index Scan using t11_a_idx on public.t11 t1_1
-         Output: (t1_1.a + 1), t1_1.ctid
-         Index Cond: ((t1_1.a > 5) AND (t1_1.a = 8))
-         Filter: ((SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
-   ->  Index Scan using t12_a_idx on public.t12 t1_2
-         Output: (t1_2.a + 1), t1_2.ctid
-         Index Cond: ((t1_2.a > 5) AND (t1_2.a = 8))
-         Filter: ((SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
-   ->  Index Scan using t111_a_idx on public.t111 t1_3
-         Output: (t1_3.a + 1), t1_3.ctid
-         Index Cond: ((t1_3.a > 5) AND (t1_3.a = 8))
-         Filter: ((SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
-(27 rows)
+   Update on public.t1 t1_1
+   Update on public.t11 t1_2
+   Update on public.t12 t1_3
+   Update on public.t111 t1_4
+   ->  Result
+         Output: (t1.a + 1), t1.ctid, (0)
+         ->  Append
+               ->  Index Scan using t1_a_idx on public.t1 t1_1
+                     Output: t1_1.a, t1_1.ctid, 0
+                     Index Cond: ((t1_1.a > 5) AND (t1_1.a = 8))
+                     Filter: ((SubPlan 1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
+                     SubPlan 1
+                       ->  Append
+                             ->  Seq Scan on public.t12 t12_1
+                                   Filter: (t12_1.a = t1_1.a)
+                             ->  Seq Scan on public.t111 t12_2
+                                   Filter: (t12_2.a = t1_1.a)
+               ->  Index Scan using t11_a_idx on public.t11 t1_2
+                     Output: t1_2.a, t1_2.ctid, 1
+                     Index Cond: ((t1_2.a > 5) AND (t1_2.a = 8))
+                     Filter: ((SubPlan 1) AND snoop(t1_2.a) AND leakproof(t1_2.a))
+               ->  Index Scan using t12_a_idx on public.t12 t1_3
+                     Output: t1_3.a, t1_3.ctid, 2
+                     Index Cond: ((t1_3.a > 5) AND (t1_3.a = 8))
+                     Filter: ((SubPlan 1) AND snoop(t1_3.a) AND leakproof(t1_3.a))
+               ->  Index Scan using t111_a_idx on public.t111 t1_4
+                     Output: t1_4.a, t1_4.ctid, 3
+                     Index Cond: ((t1_4.a > 5) AND (t1_4.a = 8))
+                     Filter: ((SubPlan 1) AND snoop(t1_4.a) AND leakproof(t1_4.a))
+(30 rows)
 
 UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
 NOTICE:  snooped value: 8
diff --git a/src/test/regress/expected/update.out b/src/test/regress/expected/update.out
index dece036069..dc34ac67b3 100644
--- a/src/test/regress/expected/update.out
+++ b/src/test/regress/expected/update.out
@@ -308,8 +308,8 @@ ALTER TABLE part_b_10_b_20 ATTACH PARTITION part_c_1_100 FOR VALUES FROM (1) TO
 
 -- The order of subplans should be in bound order
 EXPLAIN (costs off) UPDATE range_parted set c = c - 50 WHERE c > 97;
-                   QUERY PLAN                    
--------------------------------------------------
+                      QUERY PLAN                       
+-------------------------------------------------------
  Update on range_parted
    Update on part_a_1_a_10 range_parted_1
    Update on part_a_10_a_20 range_parted_2
@@ -318,21 +318,22 @@ EXPLAIN (costs off) UPDATE range_parted set c = c - 50 WHERE c > 97;
    Update on part_d_1_15 range_parted_5
    Update on part_d_15_20 range_parted_6
    Update on part_b_20_b_30 range_parted_7
-   ->  Seq Scan on part_a_1_a_10 range_parted_1
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_a_10_a_20 range_parted_2
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_b_1_b_10 range_parted_3
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_c_1_100 range_parted_4
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_d_1_15 range_parted_5
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_d_15_20 range_parted_6
-         Filter: (c > '97'::numeric)
-   ->  Seq Scan on part_b_20_b_30 range_parted_7
-         Filter: (c > '97'::numeric)
-(22 rows)
+   ->  Append
+         ->  Seq Scan on part_a_1_a_10 range_parted_1
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_a_10_a_20 range_parted_2
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_b_1_b_10 range_parted_3
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_c_1_100 range_parted_4
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_d_1_15 range_parted_5
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_d_15_20 range_parted_6
+               Filter: (c > '97'::numeric)
+         ->  Seq Scan on part_b_20_b_30 range_parted_7
+               Filter: (c > '97'::numeric)
+(23 rows)
 
 -- fail, row movement happens only within the partition subtree.
 UPDATE part_c_100_200 set c = c - 20, d = c WHERE c = 105;
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 9a6b716ddc..1cec874d62 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -2909,44 +2909,32 @@ DELETE FROM a USING wcte WHERE aa = q2;
                      QUERY PLAN                     
 ----------------------------------------------------
  Delete on public.a
-   Delete on public.a
-   Delete on public.b a_1
-   Delete on public.c a_2
-   Delete on public.d a_3
+   Delete on public.a a_1
+   Delete on public.b a_2
+   Delete on public.c a_3
+   Delete on public.d a_4
    CTE wcte
      ->  Insert on public.int8_tbl
            Output: int8_tbl.q2
            ->  Result
                  Output: '42'::bigint, '47'::bigint
-   ->  Nested Loop
-         Output: a.ctid, wcte.*
-         Join Filter: (a.aa = wcte.q2)
-         ->  Seq Scan on public.a
-               Output: a.ctid, a.aa
-         ->  CTE Scan on wcte
+   ->  Hash Join
+         Output: a.ctid, wcte.*, (0)
+         Hash Cond: (a.aa = wcte.q2)
+         ->  Append
+               ->  Seq Scan on public.a a_1
+                     Output: a_1.ctid, a_1.aa, 0
+               ->  Seq Scan on public.b a_2
+                     Output: a_2.ctid, a_2.aa, 1
+               ->  Seq Scan on public.c a_3
+                     Output: a_3.ctid, a_3.aa, 2
+               ->  Seq Scan on public.d a_4
+                     Output: a_4.ctid, a_4.aa, 3
+         ->  Hash
                Output: wcte.*, wcte.q2
-   ->  Nested Loop
-         Output: a_1.ctid, wcte.*
-         Join Filter: (a_1.aa = wcte.q2)
-         ->  Seq Scan on public.b a_1
-               Output: a_1.ctid, a_1.aa
-         ->  CTE Scan on wcte
-               Output: wcte.*, wcte.q2
-   ->  Nested Loop
-         Output: a_2.ctid, wcte.*
-         Join Filter: (a_2.aa = wcte.q2)
-         ->  Seq Scan on public.c a_2
-               Output: a_2.ctid, a_2.aa
-         ->  CTE Scan on wcte
-               Output: wcte.*, wcte.q2
-   ->  Nested Loop
-         Output: a_3.ctid, wcte.*
-         Join Filter: (a_3.aa = wcte.q2)
-         ->  Seq Scan on public.d a_3
-               Output: a_3.ctid, a_3.aa
-         ->  CTE Scan on wcte
-               Output: wcte.*, wcte.q2
-(38 rows)
+               ->  CTE Scan on wcte
+                     Output: wcte.*, wcte.q2
+(26 rows)
 
 -- error cases
 -- data-modifying WITH tries to use its own output
