From 8f637aeb39efe65e629f616fbf4362ce9476ea1a Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Wed, 3 Dec 2025 15:07:24 -0500
Subject: [PATCH v44 06/10] Track which relations are modified by a query

Save the OIDs of modified relations in a list in the PlannedStmt. A
later commit will use this information during scans to control whether
or not on-access pruning is allowed to set the visibility map. Setting
the visibility map during a scan is counterproductive if the query is
going to modify the page immediately after.

Relations are considered modified if they are the target of INSERT,
UPDATE, DELETE, or MERGE, or if they have any row mark (including SELECT
FOR UPDATE/SHARE). All row mark types are included, even those which
don't actually modify tuples, because this list is only used as a hint
to avoid unnecessary work.

Author: Melanie Plageman <melanieplageman@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Discussion: https://postgr.es/m/F5CDD1B5-628C-44A1-9F85-3958C626F6A9%40gmail.com
---
 src/backend/executor/execParallel.c    |  1 +
 src/backend/executor/nodeLockRows.c    |  3 ++
 src/backend/executor/nodeModifyTable.c |  9 ++++++
 src/backend/optimizer/plan/planner.c   | 44 +++++++++++++++++++++++++-
 src/include/nodes/plannodes.h          |  6 ++++
 5 files changed, 62 insertions(+), 1 deletion(-)

diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index ac84af294c9..5c1cf51d71c 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -188,6 +188,7 @@ ExecSerializePlan(Plan *plan, EState *estate)
 	pstmt->partPruneInfos = estate->es_part_prune_infos;
 	pstmt->rtable = estate->es_range_table;
 	pstmt->unprunableRelids = estate->es_unpruned_relids;
+	pstmt->modifiedRelOids = estate->es_plannedstmt->modifiedRelOids;
 	pstmt->permInfos = estate->es_rteperminfos;
 	pstmt->resultRelations = NIL;
 	pstmt->appendRelations = NIL;
diff --git a/src/backend/executor/nodeLockRows.c b/src/backend/executor/nodeLockRows.c
index 8d865470780..49b55d15e3e 100644
--- a/src/backend/executor/nodeLockRows.c
+++ b/src/backend/executor/nodeLockRows.c
@@ -113,6 +113,9 @@ lnext:
 		}
 		erm->ermActive = true;
 
+		Assert(list_member_oid(estate->es_plannedstmt->modifiedRelOids,
+							   RelationGetRelid(erm->relation)));
+
 		/* fetch the tuple's ctid */
 		datum = ExecGetJunkAttribute(slot,
 									 aerm->ctidAttNo,
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 4cd5e262e0f..12ecdd383cc 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -896,6 +896,9 @@ ExecInsert(ModifyTableContext *context,
 
 	resultRelationDesc = resultRelInfo->ri_RelationDesc;
 
+	Assert(list_member_oid(estate->es_plannedstmt->modifiedRelOids,
+						   RelationGetRelid(resultRelationDesc)));
+
 	/*
 	 * Open the table's indexes, if we have not done so already, so that we
 	 * can add new index entries for the inserted tuple.
@@ -1523,6 +1526,9 @@ ExecDeleteAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 {
 	EState	   *estate = context->estate;
 
+	Assert(list_member_oid(estate->es_plannedstmt->modifiedRelOids,
+						   RelationGetRelid(resultRelInfo->ri_RelationDesc)));
+
 	return table_tuple_delete(resultRelInfo->ri_RelationDesc, tupleid,
 							  estate->es_output_cid,
 							  estate->es_snapshot,
@@ -2205,6 +2211,9 @@ ExecUpdateAct(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
 	bool		partition_constraint_failed;
 	TM_Result	result;
 
+	Assert(list_member_oid(estate->es_plannedstmt->modifiedRelOids,
+						   RelationGetRelid(resultRelInfo->ri_RelationDesc)));
+
 	updateCxt->crossPartUpdate = false;
 
 	/*
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 42604a0f75c..039796773a9 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -340,8 +340,10 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	RelOptInfo *final_rel;
 	Path	   *best_path;
 	Plan	   *top_plan;
+	List	   *modifiedRelOids = NIL;
 	ListCell   *lp,
-			   *lr;
+			   *lr,
+			   *lc;
 
 	/*
 	 * Set up global state for this planner invocation.  This data is needed
@@ -661,6 +663,46 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->subplans = glob->subplans;
 	result->rewindPlanIDs = glob->rewindPlanIDs;
 	result->rowMarks = glob->finalrowmarks;
+
+	/*
+	 * Compute modifiedRelOids from result relations and row marks.
+	 *
+	 * This is a superset of what the executor will actually modify/lock at
+	 * runtime, because runtime partition pruning may eliminate some result
+	 * relations, and parent row marks are included here but skipped by the
+	 * executor.
+	 *
+	 * For partitioned tables, modifiedRelOids is expanded to include all
+	 * descendant partition OIDs. This is necessary because tuple routing
+	 * lazily expands leaf partitions at execution time.
+	 */
+	foreach(lc, glob->resultRelations)
+	{
+		Index		rti = lfirst_int(lc);
+		RangeTblEntry *rte = rt_fetch(rti, glob->finalrtable);
+
+		modifiedRelOids = list_append_unique_oid(modifiedRelOids, rte->relid);
+
+		if (rte->relkind == RELKIND_PARTITIONED_TABLE)
+		{
+			List	   *children = find_all_inheritors(rte->relid,
+													   NoLock, NULL);
+			ListCell   *lc2;
+
+			foreach(lc2, children)
+				modifiedRelOids = list_append_unique_oid(modifiedRelOids,
+														 lfirst_oid(lc2));
+		}
+	}
+	foreach(lc, glob->finalrowmarks)
+	{
+		PlanRowMark *rc = (PlanRowMark *) lfirst(lc);
+		RangeTblEntry *rte = rt_fetch(rc->rti, glob->finalrtable);
+
+		modifiedRelOids = list_append_unique_oid(modifiedRelOids, rte->relid);
+	}
+	result->modifiedRelOids = modifiedRelOids;
+
 	result->relationOids = glob->relationOids;
 	result->invalItems = glob->invalItems;
 	result->paramExecTypes = glob->paramExecTypes;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index b6185825fcb..6a7008cd50a 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -112,6 +112,12 @@ typedef struct PlannedStmt
 	 */
 	Bitmapset  *unprunableRelids;
 
+	/*
+	 * OIDs of relations modified by the query through
+	 * UPDATE/DELETE/INSERT/MERGE or targeted by SELECT FOR UPDATE/SHARE.
+	 */
+	List	   *modifiedRelOids;
+
 	/*
 	 * list of RTEPermissionInfo nodes for rtable entries needing one
 	 */
-- 
2.43.0

