From a1c2d5797313c2ef13a8c3b0ae2539577785eba1 Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Wed, 26 Nov 2025 20:19:21 +0900
Subject: [PATCH] Fix row-identity handling for dummy partitioned resultrels
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Stop adding tableoid for child relations that do not have any
row-identity columns.  In cases where all partitions are excluded
by pruning or constraint exclusion, this allows
distribute_row_identity_vars() to detect the empty state
(root->row_identity_vars == NIL) and add the appropriate ctid
column for the dummy partitioned result relation, satisfying the
executor’s requirement that a resultrel always have a row identity.

As part of this, make add_row_identity_columns() return a boolean
to report whether any row-identity columns were added, and skip FDW
children that cannot support the current command.  Adjust expected
EXPLAIN output accordingly and extend file_fdw tests to cover
dummy-root plans with and without pruning.
---
 contrib/file_fdw/expected/file_fdw.out        | 75 +++++++++++++++++++
 contrib/file_fdw/sql/file_fdw.sql             | 34 +++++++++
 src/backend/optimizer/prep/preptlist.c        |  4 +-
 src/backend/optimizer/util/appendinfo.c       | 18 ++++-
 src/backend/optimizer/util/inherit.c          |  6 +-
 src/include/optimizer/appendinfo.h            |  2 +-
 src/test/regress/expected/inherit.out         | 14 ++--
 src/test/regress/expected/merge.out           |  6 +-
 src/test/regress/expected/partition_prune.out |  4 +-
 src/test/regress/expected/returning.out       | 24 +++---
 src/test/regress/expected/updatable_views.out | 20 ++---
 src/test/regress/expected/with.out            | 14 ++--
 12 files changed, 171 insertions(+), 50 deletions(-)

diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index 5121e27dce5..e60177af8c8 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -457,6 +457,81 @@ SELECT tableoid::regclass, * FROM p2;
  p2       | 2 | xyzzy
 (3 rows)
 
+-- Verify that a dummy root partitioned-table result relation works without
+-- error when all child partitions are excluded from the plan (for example,
+-- by constraint exclusion or pruning).  In this case, the executor accepts
+-- a missing ctid for the root result relation since no rows can be produced.
+-- When a foreign-table child is processed before exclusion, a tableoid junk
+-- column may still appear in the targetlist and also wholerow for update.
+-- Dummy-root cases where all children are excluded.
+-- With pruning off, the foreign child is processed first, then excluded
+-- by constraint exclusion. EXPLAIN shows tableoid (rewritten to NULL),
+-- and for UPDATE also wholerow as NULL::record. No ctid.
+DROP TABLE p2;
+SET enable_partition_pruning TO off;
+EXPLAIN (COSTS OFF, VERBOSE) DELETE FROM pt WHERE false;
+           QUERY PLAN           
+--------------------------------
+ Delete on public.pt
+   ->  Result
+         Output: pt.ctid
+         Replaces: Scan on pt
+         One-Time Filter: false
+(5 rows)
+
+-- also cover wholerow for UPDATE; expect NULL::oid and NULL::record
+EXPLAIN (COSTS OFF, VERBOSE) UPDATE pt SET b = 'x' WHERE false;
+             QUERY PLAN             
+------------------------------------
+ Update on public.pt
+   ->  Result
+         Output: 'x'::text, pt.ctid
+         Replaces: Scan on pt
+         One-Time Filter: false
+(5 rows)
+
+-- MERGE behaves the same here; expect NULL::oid
+EXPLAIN (COSTS OFF, VERBOSE) MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a, b)
+  ON false WHEN MATCHED THEN UPDATE SET b = s.b;
+           QUERY PLAN           
+--------------------------------
+ Merge on public.pt t
+   ->  Result
+         Output: t.ctid
+         Replaces: Scan on t
+         One-Time Filter: false
+(5 rows)
+
+-- With pruning on, the foreign child is pruned entirely. The plan has only
+-- the dummy root, and EXPLAIN shows ctid (and for UPDATE, ctid plus target).
+SET enable_partition_pruning TO on;
+EXPLAIN (COSTS OFF, VERBOSE) DELETE FROM pt WHERE false;
+           QUERY PLAN           
+--------------------------------
+ Delete on public.pt
+   ->  Result
+         Output: ctid
+         Replaces: Scan on pt
+         One-Time Filter: false
+(5 rows)
+
+EXPLAIN (COSTS OFF, VERBOSE) UPDATE pt SET b = 'x' WHERE false;
+           QUERY PLAN            
+---------------------------------
+ Update on public.pt
+   ->  Result
+         Output: 'x'::text, ctid
+         Replaces: Scan on pt
+         One-Time Filter: false
+(5 rows)
+
+-- Foreign child not pruned and it does not support DELETE: error.
+EXPLAIN (COSTS OFF, VERBOSE) DELETE FROM pt WHERE a = 1;
+ERROR:  cannot delete from foreign table "p1"
+-- Runtime pruning includes the foreign child in the plan; executor errors
+-- since the foreign child does not support the command.
+EXPLAIN (COSTS OFF, VERBOSE) DELETE FROM pt WHERE (SELECT false);
+ERROR:  cannot delete from foreign table "p1"
 DROP TABLE pt;
 -- generated column tests
 \set filename :abs_srcdir '/data/list1.csv'
diff --git a/contrib/file_fdw/sql/file_fdw.sql b/contrib/file_fdw/sql/file_fdw.sql
index 1a397ad4bd1..25658b1f2dc 100644
--- a/contrib/file_fdw/sql/file_fdw.sql
+++ b/contrib/file_fdw/sql/file_fdw.sql
@@ -242,6 +242,40 @@ UPDATE pt set a = 1 where a = 2; -- ERROR
 SELECT tableoid::regclass, * FROM pt;
 SELECT tableoid::regclass, * FROM p1;
 SELECT tableoid::regclass, * FROM p2;
+
+-- Verify that a dummy root partitioned-table result relation works without
+-- error when all child partitions are excluded from the plan (for example,
+-- by constraint exclusion or pruning).  In this case, the executor accepts
+-- a missing ctid for the root result relation since no rows can be produced.
+-- When a foreign-table child is processed before exclusion, a tableoid junk
+-- column may still appear in the targetlist and also wholerow for update.
+
+-- Dummy-root cases where all children are excluded.
+-- With pruning off, the foreign child is processed first, then excluded
+-- by constraint exclusion. EXPLAIN shows tableoid (rewritten to NULL),
+-- and for UPDATE also wholerow as NULL::record. No ctid.
+DROP TABLE p2;
+SET enable_partition_pruning TO off;
+EXPLAIN (COSTS OFF, VERBOSE) DELETE FROM pt WHERE false;
+-- also cover wholerow for UPDATE; expect NULL::oid and NULL::record
+EXPLAIN (COSTS OFF, VERBOSE) UPDATE pt SET b = 'x' WHERE false;
+-- MERGE behaves the same here; expect NULL::oid
+EXPLAIN (COSTS OFF, VERBOSE) MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a, b)
+  ON false WHEN MATCHED THEN UPDATE SET b = s.b;
+
+-- With pruning on, the foreign child is pruned entirely. The plan has only
+-- the dummy root, and EXPLAIN shows ctid (and for UPDATE, ctid plus target).
+SET enable_partition_pruning TO on;
+EXPLAIN (COSTS OFF, VERBOSE) DELETE FROM pt WHERE false;
+EXPLAIN (COSTS OFF, VERBOSE) UPDATE pt SET b = 'x' WHERE false;
+
+-- Foreign child not pruned and it does not support DELETE: error.
+EXPLAIN (COSTS OFF, VERBOSE) DELETE FROM pt WHERE a = 1;
+
+-- Runtime pruning includes the foreign child in the plan; executor errors
+-- since the foreign child does not support the command.
+EXPLAIN (COSTS OFF, VERBOSE) DELETE FROM pt WHERE (SELECT false);
+
 DROP TABLE pt;
 
 -- generated column tests
diff --git a/src/backend/optimizer/prep/preptlist.c b/src/backend/optimizer/prep/preptlist.c
index ffc9d6c3f30..26090d71dfd 100644
--- a/src/backend/optimizer/prep/preptlist.c
+++ b/src/backend/optimizer/prep/preptlist.c
@@ -122,8 +122,8 @@ preprocess_targetlist(PlannerInfo *root)
 	{
 		/* row-identity logic expects to add stuff to processed_tlist */
 		root->processed_tlist = tlist;
-		add_row_identity_columns(root, result_relation,
-								 target_rte, target_relation);
+		(void) add_row_identity_columns(root, result_relation,
+										target_rte, target_relation);
 		tlist = root->processed_tlist;
 	}
 
diff --git a/src/backend/optimizer/util/appendinfo.c b/src/backend/optimizer/util/appendinfo.c
index 69b8b0c2ae0..f977dfda208 100644
--- a/src/backend/optimizer/util/appendinfo.c
+++ b/src/backend/optimizer/util/appendinfo.c
@@ -951,7 +951,7 @@ add_row_identity_var(PlannerInfo *root, Var *orig_var,
  * FDWs might call add_row_identity_var() for themselves to add nonstandard
  * columns.  (Duplicate requests are fine.)
  */
-void
+bool
 add_row_identity_columns(PlannerInfo *root, Index rtindex,
 						 RangeTblEntry *target_rte,
 						 Relation target_relation)
@@ -977,6 +977,7 @@ add_row_identity_columns(PlannerInfo *root, Index rtindex,
 					  InvalidOid,
 					  0);
 		add_row_identity_var(root, var, rtindex, "ctid");
+		return true;
 	}
 	else if (relkind == RELKIND_FOREIGN_TABLE)
 	{
@@ -987,6 +988,13 @@ add_row_identity_columns(PlannerInfo *root, Index rtindex,
 
 		fdwroutine = GetFdwRoutineForRelation(target_relation, false);
 
+		if (commandType == CMD_MERGE ||
+			(commandType == CMD_UPDATE &&
+			 fdwroutine->ExecForeignUpdate == NULL) ||
+			(commandType == CMD_DELETE &&
+			 fdwroutine->ExecForeignDelete == NULL))
+			return false;
+
 		if (fdwroutine->AddForeignUpdateTargets != NULL)
 			fdwroutine->AddForeignUpdateTargets(root, rtindex,
 												target_rte, target_relation);
@@ -1017,7 +1025,11 @@ add_row_identity_columns(PlannerInfo *root, Index rtindex,
 						  0);
 			add_row_identity_var(root, var, rtindex, "wholerow");
 		}
+
+		return true;
 	}
+
+	return false;
 }
 
 /*
@@ -1075,8 +1087,8 @@ distribute_row_identity_vars(PlannerInfo *root)
 		Relation	target_relation;
 
 		target_relation = table_open(target_rte->relid, NoLock);
-		add_row_identity_columns(root, result_relation,
-								 target_rte, target_relation);
+		(void) add_row_identity_columns(root, result_relation,
+										target_rte, target_relation);
 		table_close(target_relation, NoLock);
 		build_base_rel_tlists(root, root->processed_tlist);
 		/* There are no ROWID_VAR Vars in this case, so we're done. */
diff --git a/src/backend/optimizer/util/inherit.c b/src/backend/optimizer/util/inherit.c
index 6d5225079f8..96c24a8a552 100644
--- a/src/backend/optimizer/util/inherit.c
+++ b/src/backend/optimizer/util/inherit.c
@@ -634,11 +634,11 @@ expand_single_inheritance_child(PlannerInfo *root, RangeTblEntry *parentrte,
 							-1,
 							InvalidOid,
 							0);
-			add_row_identity_var(root, rrvar, childRTindex, "tableoid");
 
 			/* Register any row-identity columns needed by this child. */
-			add_row_identity_columns(root, childRTindex,
-									 childrte, childrel);
+			if (add_row_identity_columns(root, childRTindex,
+										 childrte, childrel))
+				add_row_identity_var(root, rrvar, childRTindex, "tableoid");
 		}
 	}
 }
diff --git a/src/include/optimizer/appendinfo.h b/src/include/optimizer/appendinfo.h
index d06f93b7266..5f3168d612f 100644
--- a/src/include/optimizer/appendinfo.h
+++ b/src/include/optimizer/appendinfo.h
@@ -42,7 +42,7 @@ extern AppendRelInfo **find_appinfos_by_relids(PlannerInfo *root,
 											   Relids relids, int *nappinfos);
 extern void add_row_identity_var(PlannerInfo *root, Var *orig_var,
 								 Index rtindex, const char *rowid_name);
-extern void add_row_identity_columns(PlannerInfo *root, Index rtindex,
+extern bool add_row_identity_columns(PlannerInfo *root, Index rtindex,
 									 RangeTblEntry *target_rte,
 									 Relation target_relation);
 extern void distribute_row_identity_vars(PlannerInfo *root);
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 0490a746555..e8fcae6514f 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -579,7 +579,7 @@ update some_tab set a = a + 1 where false;
 --------------------------------------------------------
  Update on public.some_tab
    ->  Result
-         Output: (some_tab.a + 1), NULL::oid, NULL::tid
+         Output: (some_tab.a + 1), NULL::tid, NULL::oid
          Replaces: Scan on some_tab
          One-Time Filter: false
 (5 rows)
@@ -592,7 +592,7 @@ update some_tab set a = a + 1 where false returning b, a;
  Update on public.some_tab
    Output: some_tab.b, some_tab.a
    ->  Result
-         Output: (some_tab.a + 1), NULL::oid, NULL::tid
+         Output: (some_tab.a + 1), NULL::tid, NULL::oid
          Replaces: Scan on some_tab
          One-Time Filter: false
 (6 rows)
@@ -2054,12 +2054,12 @@ update inhpar i set (f1, f2) = (select i.f1, i.f2 || '-' from int4_tbl limit 1);
    Update on public.inhpar i_1
    Update on public.inhcld i_2
    ->  Result
-         Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), i.tableoid, i.ctid
+         Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), i.ctid, i.tableoid
          ->  Append
                ->  Seq Scan on public.inhpar i_1
-                     Output: i_1.f1, i_1.f2, i_1.tableoid, i_1.ctid
+                     Output: i_1.f1, i_1.f2, i_1.ctid, i_1.tableoid
                ->  Seq Scan on public.inhcld i_2
-                     Output: i_2.f1, i_2.f2, i_2.tableoid, i_2.ctid
+                     Output: i_2.f1, i_2.f2, i_2.ctid, i_2.tableoid
          SubPlan multiexpr_1
            ->  Limit
                  Output: (i.f1), (((i.f2)::text || '-'::text))
@@ -2103,14 +2103,14 @@ update inhpar i set (f1, f2) = (select i.f1, i.f2 || '-' from int4_tbl limit 1);
    Update on public.inhcld2 i_2
    ->  Append
          ->  Seq Scan on public.inhcld1 i_1
-               Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), i_1.tableoid, i_1.ctid
+               Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), i_1.ctid, i_1.tableoid
                SubPlan multiexpr_1
                  ->  Limit
                        Output: (i_1.f1), (((i_1.f2)::text || '-'::text))
                        ->  Seq Scan on public.int4_tbl
                              Output: i_1.f1, ((i_1.f2)::text || '-'::text)
          ->  Seq Scan on public.inhcld2 i_2
-               Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), i_2.tableoid, i_2.ctid
+               Output: (SubPlan multiexpr_1).col1, (SubPlan multiexpr_1).col2, (rescan SubPlan multiexpr_1), i_2.ctid, i_2.tableoid
 (13 rows)
 
 update inhpar i set (f1, f2) = (select i.f1, i.f2 || '-' from int4_tbl limit 1);
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index 9cb1d87066a..10b27b01532 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -2387,15 +2387,15 @@ MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
  Merge on public.pa_target t
    Merge on public.pa_targetp t_1
    ->  Hash Left Join
-         Output: s.sid, s.ctid, t_1.tableoid, t_1.ctid
+         Output: s.sid, s.ctid, t_1.ctid, t_1.tableoid
          Inner Unique: true
          Hash Cond: (s.sid = t_1.tid)
          ->  Seq Scan on public.pa_source s
                Output: s.sid, s.ctid
          ->  Hash
-               Output: t_1.tid, t_1.tableoid, t_1.ctid
+               Output: t_1.tid, t_1.ctid, t_1.tableoid
                ->  Seq Scan on public.pa_targetp t_1
-                     Output: t_1.tid, t_1.tableoid, t_1.ctid
+                     Output: t_1.tid, t_1.ctid, t_1.tableoid
 (12 rows)
 
 MERGE INTO pa_target t USING pa_source s ON t.tid = s.sid
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index deacdd75807..24c6ac408f3 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -4585,7 +4585,7 @@ explain (verbose, costs off) execute update_part_abc_view (1, 'd');
    ->  Append
          Subplans Removed: 1
          ->  Seq Scan on public.part_abc_1
-               Output: $2, part_abc_1.tableoid, part_abc_1.ctid
+               Output: $2, part_abc_1.ctid, part_abc_1.tableoid
                Filter: ((part_abc_1.b <> 'a'::text) AND (part_abc_1.a = $1))
 (8 rows)
 
@@ -4604,7 +4604,7 @@ explain (verbose, costs off) execute update_part_abc_view (2, 'a');
    ->  Append
          Subplans Removed: 1
          ->  Seq Scan on public.part_abc_2
-               Output: $2, part_abc_2.tableoid, part_abc_2.ctid
+               Output: $2, part_abc_2.ctid, part_abc_2.tableoid
                Filter: ((part_abc_2.b <> 'a'::text) AND (part_abc_2.a = $1))
 (8 rows)
 
diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out
index cfaaf015bb3..2b2161f245c 100644
--- a/src/test/regress/expected/returning.out
+++ b/src/test/regress/expected/returning.out
@@ -504,9 +504,9 @@ UPDATE foo SET f4 = 100 WHERE f1 = 5
    Output: (old.tableoid)::regclass, old.ctid, old.f1, old.f2, old.f3, old.f4, old.*, (new.tableoid)::regclass, new.ctid, new.f1, new.f2, new.f3, new.f4, new.*, (((old.f4)::text || '->'::text) || (new.f4)::text)
    Update on pg_temp.foo foo_1
    ->  Result
-         Output: '100'::bigint, foo_1.tableoid, foo_1.ctid
+         Output: '100'::bigint, foo_1.ctid, foo_1.tableoid
          ->  Seq Scan on pg_temp.foo foo_1
-               Output: foo_1.tableoid, foo_1.ctid
+               Output: foo_1.ctid, foo_1.tableoid
                Filter: (foo_1.f1 = 5)
 (8 rows)
 
@@ -530,7 +530,7 @@ DELETE FROM foo WHERE f1 = 5
    Output: (old.tableoid)::regclass, old.ctid, old.f1, old.f2, old.f3, old.f4, (new.tableoid)::regclass, new.ctid, new.f1, new.f2, new.f3, new.f4, foo_1.f1, foo_1.f2, foo_1.f3, foo_1.f4
    Delete on pg_temp.foo foo_1
    ->  Seq Scan on pg_temp.foo foo_1
-         Output: foo_1.tableoid, foo_1.ctid
+         Output: foo_1.ctid, foo_1.tableoid
          Filter: (foo_1.f1 = 5)
 (6 rows)
 
@@ -586,9 +586,9 @@ UPDATE foo SET f4 = 100 WHERE f1 = 5
    Output: (SubPlan expr_1), (SubPlan expr_2), (SubPlan expr_3)
    Update on pg_temp.foo foo_1
    ->  Result
-         Output: '100'::bigint, foo_1.tableoid, foo_1.ctid
+         Output: '100'::bigint, foo_1.ctid, foo_1.tableoid
          ->  Seq Scan on pg_temp.foo foo_1
-               Output: foo_1.tableoid, foo_1.ctid
+               Output: foo_1.ctid, foo_1.tableoid
                Filter: (foo_1.f1 = 5)
    SubPlan expr_1
      ->  Result
@@ -626,7 +626,7 @@ DELETE FROM foo WHERE f1 = 5
    Output: (SubPlan expr_1), (SubPlan expr_2)
    Delete on pg_temp.foo foo_1
    ->  Seq Scan on pg_temp.foo foo_1
-         Output: foo_1.tableoid, foo_1.ctid
+         Output: foo_1.ctid, foo_1.tableoid
          Filter: (foo_1.f1 = 5)
    SubPlan expr_1
      ->  Aggregate
@@ -662,9 +662,9 @@ DELETE FROM foo WHERE f1 = 4 RETURNING old.*,new.*, *;
    Output: old.f1, old.f2, old.f3, old.f4, new.f1, new.f2, new.f3, new.f4, foo_2.f1, foo_2.f2, foo_2.f3, foo_2.f4
    Update on pg_temp.foo foo_2
    ->  Nested Loop
-         Output: (foo_2.f2 || ' (deleted)'::text), '-1'::integer, '-1'::bigint, foo_1.ctid, foo_1.tableoid, foo_2.tableoid, foo_2.ctid
+         Output: (foo_2.f2 || ' (deleted)'::text), '-1'::integer, '-1'::bigint, foo_1.ctid, foo_1.tableoid, foo_2.ctid, foo_2.tableoid
          ->  Seq Scan on pg_temp.foo foo_2
-               Output: foo_2.f2, foo_2.f1, foo_2.tableoid, foo_2.ctid
+               Output: foo_2.f2, foo_2.f1, foo_2.ctid, foo_2.tableoid
                Filter: (foo_2.f1 = 4)
          ->  Seq Scan on pg_temp.foo foo_1
                Output: foo_1.ctid, foo_1.f1, foo_1.tableoid
@@ -687,17 +687,17 @@ UPDATE joinview SET f3 = f3 + 1 WHERE f3 = 57
    Output: old.f1, old.f2, old.f3, old.f4, joinme.other, new.f1, new.f2, new.f3, new.f4, joinme.other, foo_1.f1, foo_1.f2, foo_1.f3, foo_1.f4, joinme.other, (new.f3 - old.f3)
    Update on pg_temp.foo foo_1
    ->  Hash Join
-         Output: foo_2.f1, (foo_2.f3 + 1), joinme.ctid, foo_2.ctid, joinme_1.ctid, joinme.other, foo_1.tableoid, foo_1.ctid, foo_2.tableoid
+         Output: foo_2.f1, (foo_2.f3 + 1), joinme.ctid, foo_2.ctid, joinme_1.ctid, joinme.other, foo_1.ctid, foo_1.tableoid, foo_2.tableoid
          Hash Cond: (foo_1.f2 = joinme.f2j)
          ->  Hash Join
-               Output: foo_1.f2, foo_1.tableoid, foo_1.ctid, joinme_1.ctid, joinme_1.f2j
+               Output: foo_1.f2, foo_1.ctid, foo_1.tableoid, joinme_1.ctid, joinme_1.f2j
                Hash Cond: (joinme_1.f2j = foo_1.f2)
                ->  Seq Scan on pg_temp.joinme joinme_1
                      Output: joinme_1.ctid, joinme_1.f2j
                ->  Hash
-                     Output: foo_1.f2, foo_1.tableoid, foo_1.ctid
+                     Output: foo_1.f2, foo_1.ctid, foo_1.tableoid
                      ->  Seq Scan on pg_temp.foo foo_1
-                           Output: foo_1.f2, foo_1.tableoid, foo_1.ctid
+                           Output: foo_1.f2, foo_1.ctid, foo_1.tableoid
          ->  Hash
                Output: joinme.ctid, joinme.other, joinme.f2j, foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid
                ->  Hash Join
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 03df7e75b7b..d059e70e0c5 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -3248,10 +3248,10 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
    Update on public.t12 t1_3
    Update on public.t111 t1_4
    ->  Result
-         Output: 100, t1.tableoid, t1.ctid
+         Output: 100, t1.ctid, t1.tableoid
          ->  Append
                ->  Index Scan using t1_a_idx on public.t1 t1_1
-                     Output: t1_1.tableoid, t1_1.ctid
+                     Output: t1_1.ctid, t1_1.tableoid
                      Index Cond: ((t1_1.a > 5) AND (t1_1.a < 7))
                      Filter: ((t1_1.a <> 6) AND EXISTS(SubPlan exists_1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
                      SubPlan exists_1
@@ -3261,15 +3261,15 @@ UPDATE v1 SET a=100 WHERE snoop(a) AND leakproof(a) AND a < 7 AND a != 6;
                              ->  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.tableoid, t1_2.ctid
+                     Output: t1_2.ctid, t1_2.tableoid
                      Index Cond: ((t1_2.a > 5) AND (t1_2.a < 7))
                      Filter: ((t1_2.a <> 6) AND EXISTS(SubPlan exists_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.tableoid, t1_3.ctid
+                     Output: t1_3.ctid, t1_3.tableoid
                      Index Cond: ((t1_3.a > 5) AND (t1_3.a < 7))
                      Filter: ((t1_3.a <> 6) AND EXISTS(SubPlan exists_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.tableoid, t1_4.ctid
+                     Output: t1_4.ctid, t1_4.tableoid
                      Index Cond: ((t1_4.a > 5) AND (t1_4.a < 7))
                      Filter: ((t1_4.a <> 6) AND EXISTS(SubPlan exists_1) AND snoop(t1_4.a) AND leakproof(t1_4.a))
 (30 rows)
@@ -3295,10 +3295,10 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
    Update on public.t12 t1_3
    Update on public.t111 t1_4
    ->  Result
-         Output: (t1.a + 1), t1.tableoid, t1.ctid
+         Output: (t1.a + 1), t1.ctid, t1.tableoid
          ->  Append
                ->  Index Scan using t1_a_idx on public.t1 t1_1
-                     Output: t1_1.a, t1_1.tableoid, t1_1.ctid
+                     Output: t1_1.a, t1_1.ctid, t1_1.tableoid
                      Index Cond: ((t1_1.a > 5) AND (t1_1.a = 8))
                      Filter: (EXISTS(SubPlan exists_1) AND snoop(t1_1.a) AND leakproof(t1_1.a))
                      SubPlan exists_1
@@ -3308,15 +3308,15 @@ UPDATE v1 SET a=a+1 WHERE snoop(a) AND leakproof(a) AND a = 8;
                              ->  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.tableoid, t1_2.ctid
+                     Output: t1_2.a, t1_2.ctid, t1_2.tableoid
                      Index Cond: ((t1_2.a > 5) AND (t1_2.a = 8))
                      Filter: (EXISTS(SubPlan exists_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.tableoid, t1_3.ctid
+                     Output: t1_3.a, t1_3.ctid, t1_3.tableoid
                      Index Cond: ((t1_3.a > 5) AND (t1_3.a = 8))
                      Filter: (EXISTS(SubPlan exists_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.tableoid, t1_4.ctid
+                     Output: t1_4.a, t1_4.ctid, t1_4.tableoid
                      Index Cond: ((t1_4.a > 5) AND (t1_4.a = 8))
                      Filter: (EXISTS(SubPlan exists_1) AND snoop(t1_4.a) AND leakproof(t1_4.a))
 (30 rows)
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index f4caedf272f..b949c95ae58 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -3597,21 +3597,21 @@ DELETE FROM a_star USING wcte WHERE aa = q2;
            ->  Result
                  Output: '42'::bigint, '47'::bigint
    ->  Hash Join
-         Output: wcte.*, a_star.tableoid, a_star.ctid
+         Output: wcte.*, a_star.ctid, a_star.tableoid
          Hash Cond: (a_star.aa = wcte.q2)
          ->  Append
                ->  Seq Scan on public.a_star a_star_1
-                     Output: a_star_1.aa, a_star_1.tableoid, a_star_1.ctid
+                     Output: a_star_1.aa, a_star_1.ctid, a_star_1.tableoid
                ->  Seq Scan on public.b_star a_star_2
-                     Output: a_star_2.aa, a_star_2.tableoid, a_star_2.ctid
+                     Output: a_star_2.aa, a_star_2.ctid, a_star_2.tableoid
                ->  Seq Scan on public.c_star a_star_3
-                     Output: a_star_3.aa, a_star_3.tableoid, a_star_3.ctid
+                     Output: a_star_3.aa, a_star_3.ctid, a_star_3.tableoid
                ->  Seq Scan on public.d_star a_star_4
-                     Output: a_star_4.aa, a_star_4.tableoid, a_star_4.ctid
+                     Output: a_star_4.aa, a_star_4.ctid, a_star_4.tableoid
                ->  Seq Scan on public.e_star a_star_5
-                     Output: a_star_5.aa, a_star_5.tableoid, a_star_5.ctid
+                     Output: a_star_5.aa, a_star_5.ctid, a_star_5.tableoid
                ->  Seq Scan on public.f_star a_star_6
-                     Output: a_star_6.aa, a_star_6.tableoid, a_star_6.ctid
+                     Output: a_star_6.aa, a_star_6.ctid, a_star_6.tableoid
          ->  Hash
                Output: wcte.*, wcte.q2
                ->  CTE Scan on wcte
-- 
2.47.3

