From 1bcf981c29f54b77a07c25a7b3eb06d90164bd8a Mon Sep 17 00:00:00 2001 From: Ayush Tiwari Date: Wed, 20 May 2026 05:06:57 +0000 Subject: [PATCH v1] Re-index ModifyTable FDW arrays when pruning result relations ExecInitModifyTable() copies parallel per-result-relation lists from the plan node into a new "kept" set after dropping pruned result relations. That re-indexing was already done for withCheckOptionLists, returningLists, updateColnosLists, mergeActionLists and mergeJoinConditions, but two members were missed: * node->fdwPrivLists, indexed by list_nth() when calling BeginForeignModify(), and * node->fdwDirectModifyPlans, indexed by bms_is_member() when setting ri_usesFdwDirectModify. Both were still read using the *kept* loop variable i against the *original* (pre-pruning) indexing, so on a partitioned UPDATE/DELETE that uses a generic plan (PREPARE/EXECUTE under plan_cache_mode = force_generic_plan) and runtime partition pruning, a foreign partition whose original index no longer matched its kept position caused BeginForeignModify() to receive the wrong fdw_private and segfault inside the FDW. Build re-indexed kept copies for these two arrays in the same loop as the other parallel lists and use them at the call sites. Add a postgres_fdw regression case using PREPARE/EXECUTE under force_generic_plan that exercises the failing path. Reported-by: Chi Zhang <798604270@qq.com> Discussion: https://postgr.es/m/19484-...@postgresql.org --- .../postgres_fdw/expected/postgres_fdw.out | 26 +++++++++++++++++++ contrib/postgres_fdw/sql/postgres_fdw.sql | 22 ++++++++++++++++ src/backend/executor/nodeModifyTable.c | 18 +++++++++++-- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index e90289e4ab1..872d871a675 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -9337,6 +9337,32 @@ select tableoid::regclass, * FROM locp; -- The executor should not let unexercised FDWs shut down update utrtest set a = 1 where b = 'foo'; +-- Runtime pruning of result relations must keep ModifyTable's per-relation +-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept +-- resultRelations. Otherwise BeginForeignModify() reads the wrong +-- fdw_private and segfaults. +create table fdw_part_update (a int not null, b int) partition by list (a); +create table fdw_part_update_p1 partition of fdw_part_update for values in (1); +create table fdw_part_update_remote (a int not null, b int); +create foreign table fdw_part_update_p2 partition of fdw_part_update + for values in (2) + server loopback options (table_name 'fdw_part_update_remote'); +insert into fdw_part_update_p1 values (1, 10); +insert into fdw_part_update_remote values (2, 20); +set plan_cache_mode = force_generic_plan; +prepare fdw_part_upd(int) as + update fdw_part_update set b = b + 1 where a = $1 + returning tableoid::regclass, a, b; +execute fdw_part_upd(2); + tableoid | a | b +--------------------+---+---- + fdw_part_update_p2 | 2 | 21 +(1 row) + +deallocate fdw_part_upd; +reset plan_cache_mode; +drop table fdw_part_update; +drop table fdw_part_update_remote; -- Test that remote triggers work with update tuple routing create trigger loct_br_insert_trigger before insert on loct for each row execute procedure br_insert_trigfunc(); diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index dfc58beb0d2..c80aaf1c1b4 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -2723,6 +2723,28 @@ select tableoid::regclass, * FROM locp; -- The executor should not let unexercised FDWs shut down update utrtest set a = 1 where b = 'foo'; +-- Runtime pruning of result relations must keep ModifyTable's per-relation +-- FDW arrays (fdwPrivLists, fdwDirectModifyPlans) aligned with the kept +-- resultRelations. Otherwise BeginForeignModify() reads the wrong +-- fdw_private and segfaults. +create table fdw_part_update (a int not null, b int) partition by list (a); +create table fdw_part_update_p1 partition of fdw_part_update for values in (1); +create table fdw_part_update_remote (a int not null, b int); +create foreign table fdw_part_update_p2 partition of fdw_part_update + for values in (2) + server loopback options (table_name 'fdw_part_update_remote'); +insert into fdw_part_update_p1 values (1, 10); +insert into fdw_part_update_remote values (2, 20); +set plan_cache_mode = force_generic_plan; +prepare fdw_part_upd(int) as + update fdw_part_update set b = b + 1 where a = $1 + returning tableoid::regclass, a, b; +execute fdw_part_upd(2); +deallocate fdw_part_upd; +reset plan_cache_mode; +drop table fdw_part_update; +drop table fdw_part_update_remote; + -- Test that remote triggers work with update tuple routing create trigger loct_br_insert_trigger before insert on loct for each row execute procedure br_insert_trigfunc(); diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 478cb01783c..f69060cb5ab 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -5108,6 +5108,9 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) List *resultRelations = NIL; List *withCheckOptionLists = NIL; List *returningLists = NIL; + /* fdwPrivLists/fdwDirectModifyPlans are re-indexed to match resultRelations */ + List *fdwPrivLists = NIL; + Bitmapset *fdwDirectModifyPlans = NULL; List *updateColnosLists = NIL; List *mergeActionLists = NIL; List *mergeJoinConditions = NIL; @@ -5153,6 +5156,8 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) if (keep_rel) { + int new_index = list_length(resultRelations); + resultRelations = lappend_int(resultRelations, rti); if (node->withCheckOptionLists) { @@ -5170,6 +5175,15 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) returningLists = lappend(returningLists, returningList); } + if (node->fdwPrivLists) + { + List *fdwPrivList = (List *) list_nth(node->fdwPrivLists, i); + + fdwPrivLists = lappend(fdwPrivLists, fdwPrivList); + } + if (bms_is_member(i, node->fdwDirectModifyPlans)) + fdwDirectModifyPlans = bms_add_member(fdwDirectModifyPlans, + new_index); if (node->updateColnosLists) { List *updateColnosList = list_nth(node->updateColnosLists, i); @@ -5291,7 +5305,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) /* Initialize the usesFdwDirectModify flag */ resultRelInfo->ri_usesFdwDirectModify = - bms_is_member(i, node->fdwDirectModifyPlans); + bms_is_member(i, fdwDirectModifyPlans); /* * Verify result relation is a valid target for the current operation @@ -5320,7 +5334,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) resultRelInfo->ri_FdwRoutine != NULL && resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL) { - List *fdw_private = (List *) list_nth(node->fdwPrivLists, i); + List *fdw_private = (List *) list_nth(fdwPrivLists, i); resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate, resultRelInfo, -- 2.43.0