Ensure first ModifyTable rel initialized if all are pruned
authorAmit Langote <amitlan@postgresql.org>
Wed, 19 Mar 2025 03:14:24 +0000 (12:14 +0900)
committerAmit Langote <amitlan@postgresql.org>
Wed, 19 Mar 2025 03:14:24 +0000 (12:14 +0900)
Commit cbc127917e introduced tracking of unpruned relids to avoid
processing pruned relations, and changed ExecInitModifyTable() to
initialize only unpruned result relations. As a result, MERGE
statements that prune all target partitions can now lead to crashes
or incorrect behavior during execution.

The crash occurs because some executor code paths rely on
ModifyTableState.resultRelInfo[0] being present and initialized,
even when no result relations remain after pruning. For example,
ExecMerge() and ExecMergeNotMatched() use the first resultRelInfo
to determine the appropriate action. Similarly,
ExecInitPartitionInfo() assumes that at least one result relation
exists.

To preserve these assumptions, ExecInitModifyTable() now includes the
first result relation in the initialized result relation list if all
result relations for that ModifyTable were pruned. To enable that,
ExecDoInitialPruning() ensures the first relation is locked if it was
pruned and locking is necessary.

To support this exception to the pruning logic, PlannedStmt now
includes a list of RT indexes identifying the first result relation
of each ModifyTable node in the plan. This allows
ExecDoInitialPruning() to check whether each such relation was
pruned and, if so, lock it if necessary.

Bug: #18830
Reported-by: Robins Tharakan <tharakan@gmail.com>
Diagnozed-by: Tender Wang <tndrwang@gmail.com>
Diagnozed-by: Dean Rasheed <dean.a.rasheed@gmail.com>
Co-authored-by: Dean Rasheed <dean.a.rasheed@gmail.com>
Reviewed-by: Tender Wang <tndrwang@gmail.com>
Reviewed-by: Dean Rasheed <dean.a.rasheed@gmail.com>
Discussion: https://postgr.es/m/18830-1f31ea1dc930d444%40postgresql.org

13 files changed:
src/backend/commands/explain.c
src/backend/executor/execMain.c
src/backend/executor/execPartition.c
src/backend/executor/execUtils.c
src/backend/executor/nodeModifyTable.c
src/backend/optimizer/plan/planner.c
src/backend/optimizer/plan/setrefs.c
src/include/executor/execPartition.h
src/include/executor/executor.h
src/include/nodes/pathnodes.h
src/include/nodes/plannodes.h
src/test/regress/expected/partition_prune.out
src/test/regress/sql/partition_prune.sql

index 22616cf7add94e976fe0ef3d9ca955fa973076e1..33a16d2d8e2d7cd8e1dd4450b793eecc9c21aba5 100644 (file)
@@ -4575,10 +4575,20 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
            break;
    }
 
-   /* Should we explicitly label target relations? */
+   /*
+    * Should we explicitly label target relations?
+    *
+    * If there's only one target relation, do not list it if it's the
+    * relation named in the query, or if it has been pruned.  (Normally
+    * mtstate->resultRelInfo doesn't include pruned relations, but a single
+    * pruned target relation may be present, if all other target relations
+    * have been pruned.  See ExecInitModifyTable().)
+    */
    labeltargets = (mtstate->mt_nrels > 1 ||
                    (mtstate->mt_nrels == 1 &&
-                    mtstate->resultRelInfo[0].ri_RangeTableIndex != node->nominalRelation));
+                    mtstate->resultRelInfo[0].ri_RangeTableIndex != node->nominalRelation &&
+                    bms_is_member(mtstate->resultRelInfo[0].ri_RangeTableIndex,
+                                  mtstate->ps.state->es_unpruned_relids)));
 
    if (labeltargets)
        ExplainOpenGroup("Target Tables", "Target Tables", false, es);
index 0493b7d536544b0fb9bea618fb7c29edab0cf56a..e9bd98c7738c0c8b86a343cb8648293c3d51c441 100644 (file)
@@ -1006,7 +1006,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
                case ROW_MARK_SHARE:
                case ROW_MARK_KEYSHARE:
                case ROW_MARK_REFERENCE:
-                   relation = ExecGetRangeTableRelation(estate, rc->rti);
+                   relation = ExecGetRangeTableRelation(estate, rc->rti, false);
                    break;
                case ROW_MARK_COPY:
                    /* no physical table access is required */
index 5cd5e2eeb80b3f53d568e42f0457425d0397d768..84ccd7d457de910aeb18937fdcff289ca9140b3b 100644 (file)
@@ -1819,6 +1819,7 @@ adjust_partition_colnos_using_map(List *colnos, AttrMap *attrMap)
 void
 ExecDoInitialPruning(EState *estate)
 {
+   PlannedStmt *stmt = estate->es_plannedstmt;
    ListCell   *lc;
    List       *locked_relids = NIL;
 
@@ -1867,6 +1868,34 @@ ExecDoInitialPruning(EState *estate)
                                                validsubplans);
    }
 
+   /*
+    * Lock the first result relation of each ModifyTable node, even if it was
+    * pruned.  This is required for ExecInitModifyTable(), which keeps its
+    * first result relation if all other result relations have been pruned,
+    * because some executor paths (e.g., in nodeModifyTable.c and
+    * execPartition.c) rely on there being at least one result relation.
+    *
+    * There's room for improvement here --- we actually only need to do this
+    * if all other result relations of the ModifyTable node were pruned, but
+    * we don't have an easy way to tell that here.
+    */
+   if (stmt->resultRelations && ExecShouldLockRelations(estate))
+   {
+       foreach(lc, stmt->firstResultRels)
+       {
+           Index       firstResultRel = lfirst_int(lc);
+
+           if (!bms_is_member(firstResultRel, estate->es_unpruned_relids))
+           {
+               RangeTblEntry *rte = exec_rt_fetch(firstResultRel, estate);
+
+               Assert(rte->rtekind == RTE_RELATION && rte->rellockmode != NoLock);
+               LockRelationOid(rte->relid, rte->rellockmode);
+               locked_relids = lappend_int(locked_relids, firstResultRel);
+           }
+       }
+   }
+
    /*
     * Release the useless locks if the plan won't be executed.  This is the
     * same as what CheckCachedPlan() in plancache.c does.
@@ -2076,7 +2105,7 @@ CreatePartitionPruneState(EState *estate, PartitionPruneInfo *pruneinfo,
             * because that entry will be held open and locked for the
             * duration of this executor run.
             */
-           partrel = ExecGetRangeTableRelation(estate, pinfo->rtindex);
+           partrel = ExecGetRangeTableRelation(estate, pinfo->rtindex, false);
 
            /* Remember for InitExecPartitionPruneContext(). */
            pprune->partrel = partrel;
index 39d6f4d819ed0583be0592e2ae2ebdd65cdb7d33..55ab18fb826948bc11a3dff652934f6fc5b92c0b 100644 (file)
@@ -746,7 +746,7 @@ ExecOpenScanRelation(EState *estate, Index scanrelid, int eflags)
    Relation    rel;
 
    /* Open the relation. */
-   rel = ExecGetRangeTableRelation(estate, scanrelid);
+   rel = ExecGetRangeTableRelation(estate, scanrelid, false);
 
    /*
     * Complain if we're attempting a scan of an unscannable relation, except
@@ -815,18 +815,22 @@ ExecInitRangeTable(EState *estate, List *rangeTable, List *permInfos,
  *
  * The Relations will be closed in ExecEndPlan().
  *
- * Note: The caller must ensure that 'rti' refers to an unpruned relation
- * (i.e., it is a member of estate->es_unpruned_relids) before calling this
- * function. Attempting to open a pruned relation will result in an error.
+ * If isResultRel is true, the relation is being used as a result relation.
+ * Such a relation might have been pruned, which is OK for result relations,
+ * but not for scan relations; see the details in ExecInitModifyTable(). If
+ * isResultRel is false, the caller must ensure that 'rti' refers to an
+ * unpruned relation (i.e., it is a member of estate->es_unpruned_relids)
+ * before calling this function. Attempting to open a pruned relation for
+ * scanning will result in an error.
  */
 Relation
-ExecGetRangeTableRelation(EState *estate, Index rti)
+ExecGetRangeTableRelation(EState *estate, Index rti, bool isResultRel)
 {
    Relation    rel;
 
    Assert(rti > 0 && rti <= estate->es_range_table_size);
 
-   if (!bms_is_member(rti, estate->es_unpruned_relids))
+   if (!isResultRel && !bms_is_member(rti, estate->es_unpruned_relids))
        elog(ERROR, "trying to open a pruned relation");
 
    rel = estate->es_relations[rti - 1];
@@ -880,7 +884,7 @@ ExecInitResultRelation(EState *estate, ResultRelInfo *resultRelInfo,
 {
    Relation    resultRelationDesc;
 
-   resultRelationDesc = ExecGetRangeTableRelation(estate, rti);
+   resultRelationDesc = ExecGetRangeTableRelation(estate, rti, true);
    InitResultRelInfo(resultRelInfo,
                      resultRelationDesc,
                      rti,
index b0fe50075adf5597bfe4ff8dc5da094389518ace..87c820276a8f79b0214e2479ff0dc2a3a130fc1d 100644 (file)
@@ -4471,6 +4471,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
    ModifyTableState *mtstate;
    Plan       *subplan = outerPlan(node);
    CmdType     operation = node->operation;
+   int         total_nrels = list_length(node->resultRelations);
    int         nrels;
    List       *resultRelations = NIL;
    List       *withCheckOptionLists = NIL;
@@ -4490,13 +4491,35 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
    /*
     * Only consider unpruned relations for initializing their ResultRelInfo
     * struct and other fields such as withCheckOptions, etc.
+    *
+    * Note: We must avoid pruning every result relation.  This is important
+    * for MERGE, since even if every result relation is pruned from the
+    * subplan, there might still be NOT MATCHED rows, for which there may be
+    * INSERT actions to perform.  To allow these actions to be found, at
+    * least one result relation must be kept.  Also, when inserting into a
+    * partitioned table, ExecInitPartitionInfo() needs a ResultRelInfo struct
+    * as a reference for building the ResultRelInfo of the target partition.
+    * In either case, it doesn't matter which result relation is kept, so we
+    * just keep the first one, if all others have been pruned.  See also,
+    * ExecDoInitialPruning(), which ensures that this first result relation
+    * has been locked.
     */
    i = 0;
    foreach(l, node->resultRelations)
    {
        Index       rti = lfirst_int(l);
+       bool        keep_rel;
+
+       keep_rel = bms_is_member(rti, estate->es_unpruned_relids);
+       if (!keep_rel && i == total_nrels - 1 && resultRelations == NIL)
+       {
+           /* all result relations pruned; keep the first one */
+           keep_rel = true;
+           rti = linitial_int(node->resultRelations);
+           i = 0;
+       }
 
-       if (bms_is_member(rti, estate->es_unpruned_relids))
+       if (keep_rel)
        {
            resultRelations = lappend_int(resultRelations, rti);
            if (node->withCheckOptionLists)
@@ -4537,6 +4560,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
        i++;
    }
    nrels = list_length(resultRelations);
+   Assert(nrels > 0);
 
    /*
     * create state structure
@@ -4735,7 +4759,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
     */
    mtstate->mt_resultOidAttno =
        ExecFindJunkAttributeInTlist(subplan->targetlist, "tableoid");
-   Assert(AttributeNumberIsValid(mtstate->mt_resultOidAttno) || nrels == 1);
+   Assert(AttributeNumberIsValid(mtstate->mt_resultOidAttno) || total_nrels == 1);
    mtstate->mt_lastResultOid = InvalidOid; /* force lookup at first tuple */
    mtstate->mt_lastResultIndex = 0;    /* must be zero if no such attr */
 
@@ -4832,7 +4856,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
    if (node->onConflictAction != ONCONFLICT_NONE)
    {
        /* insert may only have one relation, inheritance is not expanded */
-       Assert(nrels == 1);
+       Assert(total_nrels == 1);
        resultRelInfo->ri_onConflictArbiterIndexes = node->arbiterIndexes;
    }
 
@@ -4979,7 +5003,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
    if (operation == CMD_INSERT)
    {
        /* insert may only have one relation, inheritance is not expanded */
-       Assert(nrels == 1);
+       Assert(total_nrels == 1);
        resultRelInfo = mtstate->resultRelInfo;
        if (!resultRelInfo->ri_usesFdwDirectModify &&
            resultRelInfo->ri_FdwRoutine != NULL &&
index a4d523dcb0ffa441e6f960b20bd8a1990630a93d..141177e74135c33686993b454a73790635864ad6 100644 (file)
@@ -562,6 +562,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
                                              glob->prunableRelids);
    result->permInfos = glob->finalrteperminfos;
    result->resultRelations = glob->resultRelations;
+   result->firstResultRels = glob->firstResultRels;
    result->appendRelations = glob->appendRelations;
    result->subplans = glob->subplans;
    result->rewindPlanIDs = glob->rewindPlanIDs;
index 999a5a8ab5a2e1c716b43d782edd057cf0f23bbc..150e9f060ee0073e2946b5a880c224c6d254cb2f 100644 (file)
@@ -1248,6 +1248,9 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
                        lappend_int(root->glob->resultRelations,
                                    splan->rootRelation);
                }
+               root->glob->firstResultRels =
+                   lappend_int(root->glob->firstResultRels,
+                               linitial_int(splan->resultRelations));
            }
            break;
        case T_Append:
index 626613012f91f124314f1386090376f56d794ea4..3b3f46aced05f747b74d00ac1827a4cc70a2e0e8 100644 (file)
@@ -43,8 +43,8 @@ extern void ExecCleanupTupleRouting(ModifyTableState *mtstate,
  * subpart_map contains indexes into PartitionPruningData.partrelprunedata[].
  *
  * partrel                     Partitioned table Relation; obtained by
- *                                 ExecGetRangeTableRelation(estate, rti), where
- *                             rti is PartitionedRelPruneInfo.rtindex.
+ *                                 ExecGetRangeTableRelation(estate, rti, false),
+ *                                 where rti is PartitionedRelPruneInfo.rtindex.
  * nparts                      Length of subplan_map[] and subpart_map[].
  * subplan_map                 Subplan index by partition index, or -1.
  * subpart_map                 Subpart index by partition index, or -1.
index 0d2ffabda684a4a35d93c454da00d809856a18aa..0db5d18ba2232b1ea1bef50eefb01c4239a3762a 100644 (file)
@@ -680,7 +680,8 @@ exec_rt_fetch(Index rti, EState *estate)
    return (RangeTblEntry *) list_nth(estate->es_range_table, rti - 1);
 }
 
-extern Relation ExecGetRangeTableRelation(EState *estate, Index rti);
+extern Relation ExecGetRangeTableRelation(EState *estate, Index rti,
+                                         bool isResultRel);
 extern void ExecInitResultRelation(EState *estate, ResultRelInfo *resultRelInfo,
                                   Index rti);
 
index fbf05322c75f42864bb5e3f7b35e2fdb635bc59c..c24a1fc8514c2e7a9d443194a8ed1e7e690a948b 100644 (file)
@@ -138,6 +138,9 @@ typedef struct PlannerGlobal
    /* "flat" list of integer RT indexes */
    List       *resultRelations;
 
+   /* "flat" list of integer RT indexes (one per ModifyTable node) */
+   List       *firstResultRels;
+
    /* "flat" list of AppendRelInfos */
    List       *appendRelations;
 
index 22841211f4811883db9d70844433da1a9f70e70c..f78bffd90cf8f76cc79f994464c8a752808418bb 100644 (file)
@@ -102,6 +102,13 @@ typedef struct PlannedStmt
    /* integer list of RT indexes, or NIL */
    List       *resultRelations;
 
+   /*
+    * rtable indexes of first target relation in each ModifyTable node in the
+    * plan for INSERT/UPDATE/DELETE/MERGE
+    */
+   /* integer list of RT indexes, or NIL */
+   List       *firstResultRels;
+
    /* list of AppendRelInfo nodes */
    List       *appendRelations;
 
index 8097f4e92825dc1e34a80f03b0ba4dc4db5cd200..0bf35260b46e640b16b85ca298d827dad13d7a47 100644 (file)
@@ -4662,6 +4662,88 @@ table part_abc_view;
  2 | c | t
 (1 row)
 
+-- MERGE ... INSERT when all pruned from MERGE source.
+begin;
+explain (costs off)
+merge into part_abc_view pt
+using (select stable_one() + 1 as pid) as q join part_abc_2 pt2 on (q.pid = pt2.a) on pt.a = stable_one() + 2
+when not matched then insert values (1, 'd', false) returning pt.a;
+                   QUERY PLAN                   
+------------------------------------------------
+ Merge on part_abc
+   ->  Nested Loop Left Join
+         ->  Seq Scan on part_abc_2 pt2
+               Filter: ((stable_one() + 1) = a)
+         ->  Materialize
+               ->  Append
+                     Subplans Removed: 2
+(7 rows)
+
+merge into part_abc_view pt
+using (select stable_one() + 1 as pid) as q join part_abc_2 pt2 on (q.pid = pt2.a) on pt.a = stable_one() + 2
+when not matched then insert values (1, 'd', false) returning pt.a;
+ a 
+---
+ 1
+(1 row)
+
+table part_abc_view;
+ a | b | c 
+---+---+---
+ 1 | d | f
+ 2 | c | t
+(2 rows)
+
+rollback;
+-- A case with multiple ModifyTable nodes.
+begin;
+create table part_abc_log (action text, a int, b text, c bool);
+explain (costs off)
+with t as (
+  merge into part_abc_view pt
+  using (select stable_one() + 1 as pid) as q join part_abc_2 pt2 on (q.pid = pt2.a) on pt.a = stable_one() + 2
+  when not matched then insert values (1, 'd', false) returning merge_action(), pt.*
+)
+insert into part_abc_log select * from t returning *;
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Insert on part_abc_log
+   CTE t
+     ->  Merge on part_abc
+           ->  Nested Loop Left Join
+                 ->  Seq Scan on part_abc_2 pt2
+                       Filter: ((stable_one() + 1) = a)
+                 ->  Materialize
+                       ->  Append
+                             Subplans Removed: 2
+   ->  CTE Scan on t
+(10 rows)
+
+with t as (
+  merge into part_abc_view pt
+  using (select stable_one() + 1 as pid) as q join part_abc_2 pt2 on (q.pid = pt2.a) on pt.a = stable_one() + 2
+  when not matched then insert values (1, 'd', false) returning merge_action(), pt.*
+)
+insert into part_abc_log select * from t returning *;
+ action | a | b | c 
+--------+---+---+---
+ INSERT | 1 | d | f
+(1 row)
+
+table part_abc_view;
+ a | b | c 
+---+---+---
+ 1 | d | f
+ 2 | c | t
+(2 rows)
+
+table part_abc_log;
+ action | a | b | c 
+--------+---+---+---
+ INSERT | 1 | d | f
+(1 row)
+
+rollback;
 -- A case with nested MergeAppend with its own PartitionPruneInfo.
 create index on part_abc (a);
 alter table part_abc add d int;
index 4a2c74b089990780e9cf1d57e90a4ad165ab0949..f6db9479f54ad7a7cad8e0e5231586eb3221dffc 100644 (file)
@@ -1401,6 +1401,38 @@ using (select stable_one() + 2 as pid) as q join part_abc_1 pt1 on (q.pid = pt1.
 when matched then delete returning pt.a;
 table part_abc_view;
 
+-- MERGE ... INSERT when all pruned from MERGE source.
+begin;
+explain (costs off)
+merge into part_abc_view pt
+using (select stable_one() + 1 as pid) as q join part_abc_2 pt2 on (q.pid = pt2.a) on pt.a = stable_one() + 2
+when not matched then insert values (1, 'd', false) returning pt.a;
+merge into part_abc_view pt
+using (select stable_one() + 1 as pid) as q join part_abc_2 pt2 on (q.pid = pt2.a) on pt.a = stable_one() + 2
+when not matched then insert values (1, 'd', false) returning pt.a;
+table part_abc_view;
+rollback;
+
+-- A case with multiple ModifyTable nodes.
+begin;
+create table part_abc_log (action text, a int, b text, c bool);
+explain (costs off)
+with t as (
+  merge into part_abc_view pt
+  using (select stable_one() + 1 as pid) as q join part_abc_2 pt2 on (q.pid = pt2.a) on pt.a = stable_one() + 2
+  when not matched then insert values (1, 'd', false) returning merge_action(), pt.*
+)
+insert into part_abc_log select * from t returning *;
+with t as (
+  merge into part_abc_view pt
+  using (select stable_one() + 1 as pid) as q join part_abc_2 pt2 on (q.pid = pt2.a) on pt.a = stable_one() + 2
+  when not matched then insert values (1, 'd', false) returning merge_action(), pt.*
+)
+insert into part_abc_log select * from t returning *;
+table part_abc_view;
+table part_abc_log;
+rollback;
+
 -- A case with nested MergeAppend with its own PartitionPruneInfo.
 create index on part_abc (a);
 alter table part_abc add d int;