Skip to content

Commit 28317de

Browse files
amitlandeanrasheed
andcommitted
Ensure first ModifyTable rel initialized if all are pruned
Commit cbc1279 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
1 parent 06fb561 commit 28317de

File tree

13 files changed

+214
-18
lines changed

13 files changed

+214
-18
lines changed

src/backend/commands/explain.c

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4575,10 +4575,20 @@ show_modifytable_info(ModifyTableState *mtstate, List *ancestors,
45754575
break;
45764576
}
45774577

4578-
/* Should we explicitly label target relations? */
4578+
/*
4579+
* Should we explicitly label target relations?
4580+
*
4581+
* If there's only one target relation, do not list it if it's the
4582+
* relation named in the query, or if it has been pruned. (Normally
4583+
* mtstate->resultRelInfo doesn't include pruned relations, but a single
4584+
* pruned target relation may be present, if all other target relations
4585+
* have been pruned. See ExecInitModifyTable().)
4586+
*/
45794587
labeltargets = (mtstate->mt_nrels > 1 ||
45804588
(mtstate->mt_nrels == 1 &&
4581-
mtstate->resultRelInfo[0].ri_RangeTableIndex != node->nominalRelation));
4589+
mtstate->resultRelInfo[0].ri_RangeTableIndex != node->nominalRelation &&
4590+
bms_is_member(mtstate->resultRelInfo[0].ri_RangeTableIndex,
4591+
mtstate->ps.state->es_unpruned_relids)));
45824592

45834593
if (labeltargets)
45844594
ExplainOpenGroup("Target Tables", "Target Tables", false, es);

src/backend/executor/execMain.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ InitPlan(QueryDesc *queryDesc, int eflags)
10061006
case ROW_MARK_SHARE:
10071007
case ROW_MARK_KEYSHARE:
10081008
case ROW_MARK_REFERENCE:
1009-
relation = ExecGetRangeTableRelation(estate, rc->rti);
1009+
relation = ExecGetRangeTableRelation(estate, rc->rti, false);
10101010
break;
10111011
case ROW_MARK_COPY:
10121012
/* no physical table access is required */

src/backend/executor/execPartition.c

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1819,6 +1819,7 @@ adjust_partition_colnos_using_map(List *colnos, AttrMap *attrMap)
18191819
void
18201820
ExecDoInitialPruning(EState *estate)
18211821
{
1822+
PlannedStmt *stmt = estate->es_plannedstmt;
18221823
ListCell *lc;
18231824
List *locked_relids = NIL;
18241825

@@ -1867,6 +1868,34 @@ ExecDoInitialPruning(EState *estate)
18671868
validsubplans);
18681869
}
18691870

1871+
/*
1872+
* Lock the first result relation of each ModifyTable node, even if it was
1873+
* pruned. This is required for ExecInitModifyTable(), which keeps its
1874+
* first result relation if all other result relations have been pruned,
1875+
* because some executor paths (e.g., in nodeModifyTable.c and
1876+
* execPartition.c) rely on there being at least one result relation.
1877+
*
1878+
* There's room for improvement here --- we actually only need to do this
1879+
* if all other result relations of the ModifyTable node were pruned, but
1880+
* we don't have an easy way to tell that here.
1881+
*/
1882+
if (stmt->resultRelations && ExecShouldLockRelations(estate))
1883+
{
1884+
foreach(lc, stmt->firstResultRels)
1885+
{
1886+
Index firstResultRel = lfirst_int(lc);
1887+
1888+
if (!bms_is_member(firstResultRel, estate->es_unpruned_relids))
1889+
{
1890+
RangeTblEntry *rte = exec_rt_fetch(firstResultRel, estate);
1891+
1892+
Assert(rte->rtekind == RTE_RELATION && rte->rellockmode != NoLock);
1893+
LockRelationOid(rte->relid, rte->rellockmode);
1894+
locked_relids = lappend_int(locked_relids, firstResultRel);
1895+
}
1896+
}
1897+
}
1898+
18701899
/*
18711900
* Release the useless locks if the plan won't be executed. This is the
18721901
* same as what CheckCachedPlan() in plancache.c does.
@@ -2076,7 +2105,7 @@ CreatePartitionPruneState(EState *estate, PartitionPruneInfo *pruneinfo,
20762105
* because that entry will be held open and locked for the
20772106
* duration of this executor run.
20782107
*/
2079-
partrel = ExecGetRangeTableRelation(estate, pinfo->rtindex);
2108+
partrel = ExecGetRangeTableRelation(estate, pinfo->rtindex, false);
20802109

20812110
/* Remember for InitExecPartitionPruneContext(). */
20822111
pprune->partrel = partrel;

src/backend/executor/execUtils.c

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -746,7 +746,7 @@ ExecOpenScanRelation(EState *estate, Index scanrelid, int eflags)
746746
Relation rel;
747747

748748
/* Open the relation. */
749-
rel = ExecGetRangeTableRelation(estate, scanrelid);
749+
rel = ExecGetRangeTableRelation(estate, scanrelid, false);
750750

751751
/*
752752
* Complain if we're attempting a scan of an unscannable relation, except
@@ -815,18 +815,22 @@ ExecInitRangeTable(EState *estate, List *rangeTable, List *permInfos,
815815
*
816816
* The Relations will be closed in ExecEndPlan().
817817
*
818-
* Note: The caller must ensure that 'rti' refers to an unpruned relation
819-
* (i.e., it is a member of estate->es_unpruned_relids) before calling this
820-
* function. Attempting to open a pruned relation will result in an error.
818+
* If isResultRel is true, the relation is being used as a result relation.
819+
* Such a relation might have been pruned, which is OK for result relations,
820+
* but not for scan relations; see the details in ExecInitModifyTable(). If
821+
* isResultRel is false, the caller must ensure that 'rti' refers to an
822+
* unpruned relation (i.e., it is a member of estate->es_unpruned_relids)
823+
* before calling this function. Attempting to open a pruned relation for
824+
* scanning will result in an error.
821825
*/
822826
Relation
823-
ExecGetRangeTableRelation(EState *estate, Index rti)
827+
ExecGetRangeTableRelation(EState *estate, Index rti, bool isResultRel)
824828
{
825829
Relation rel;
826830

827831
Assert(rti > 0 && rti <= estate->es_range_table_size);
828832

829-
if (!bms_is_member(rti, estate->es_unpruned_relids))
833+
if (!isResultRel && !bms_is_member(rti, estate->es_unpruned_relids))
830834
elog(ERROR, "trying to open a pruned relation");
831835

832836
rel = estate->es_relations[rti - 1];
@@ -880,7 +884,7 @@ ExecInitResultRelation(EState *estate, ResultRelInfo *resultRelInfo,
880884
{
881885
Relation resultRelationDesc;
882886

883-
resultRelationDesc = ExecGetRangeTableRelation(estate, rti);
887+
resultRelationDesc = ExecGetRangeTableRelation(estate, rti, true);
884888
InitResultRelInfo(resultRelInfo,
885889
resultRelationDesc,
886890
rti,

src/backend/executor/nodeModifyTable.c

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4471,6 +4471,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
44714471
ModifyTableState *mtstate;
44724472
Plan *subplan = outerPlan(node);
44734473
CmdType operation = node->operation;
4474+
int total_nrels = list_length(node->resultRelations);
44744475
int nrels;
44754476
List *resultRelations = NIL;
44764477
List *withCheckOptionLists = NIL;
@@ -4490,13 +4491,35 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
44904491
/*
44914492
* Only consider unpruned relations for initializing their ResultRelInfo
44924493
* struct and other fields such as withCheckOptions, etc.
4494+
*
4495+
* Note: We must avoid pruning every result relation. This is important
4496+
* for MERGE, since even if every result relation is pruned from the
4497+
* subplan, there might still be NOT MATCHED rows, for which there may be
4498+
* INSERT actions to perform. To allow these actions to be found, at
4499+
* least one result relation must be kept. Also, when inserting into a
4500+
* partitioned table, ExecInitPartitionInfo() needs a ResultRelInfo struct
4501+
* as a reference for building the ResultRelInfo of the target partition.
4502+
* In either case, it doesn't matter which result relation is kept, so we
4503+
* just keep the first one, if all others have been pruned. See also,
4504+
* ExecDoInitialPruning(), which ensures that this first result relation
4505+
* has been locked.
44934506
*/
44944507
i = 0;
44954508
foreach(l, node->resultRelations)
44964509
{
44974510
Index rti = lfirst_int(l);
4511+
bool keep_rel;
4512+
4513+
keep_rel = bms_is_member(rti, estate->es_unpruned_relids);
4514+
if (!keep_rel && i == total_nrels - 1 && resultRelations == NIL)
4515+
{
4516+
/* all result relations pruned; keep the first one */
4517+
keep_rel = true;
4518+
rti = linitial_int(node->resultRelations);
4519+
i = 0;
4520+
}
44984521

4499-
if (bms_is_member(rti, estate->es_unpruned_relids))
4522+
if (keep_rel)
45004523
{
45014524
resultRelations = lappend_int(resultRelations, rti);
45024525
if (node->withCheckOptionLists)
@@ -4537,6 +4560,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
45374560
i++;
45384561
}
45394562
nrels = list_length(resultRelations);
4563+
Assert(nrels > 0);
45404564

45414565
/*
45424566
* create state structure
@@ -4735,7 +4759,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
47354759
*/
47364760
mtstate->mt_resultOidAttno =
47374761
ExecFindJunkAttributeInTlist(subplan->targetlist, "tableoid");
4738-
Assert(AttributeNumberIsValid(mtstate->mt_resultOidAttno) || nrels == 1);
4762+
Assert(AttributeNumberIsValid(mtstate->mt_resultOidAttno) || total_nrels == 1);
47394763
mtstate->mt_lastResultOid = InvalidOid; /* force lookup at first tuple */
47404764
mtstate->mt_lastResultIndex = 0; /* must be zero if no such attr */
47414765

@@ -4832,7 +4856,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
48324856
if (node->onConflictAction != ONCONFLICT_NONE)
48334857
{
48344858
/* insert may only have one relation, inheritance is not expanded */
4835-
Assert(nrels == 1);
4859+
Assert(total_nrels == 1);
48364860
resultRelInfo->ri_onConflictArbiterIndexes = node->arbiterIndexes;
48374861
}
48384862

@@ -4979,7 +5003,7 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
49795003
if (operation == CMD_INSERT)
49805004
{
49815005
/* insert may only have one relation, inheritance is not expanded */
4982-
Assert(nrels == 1);
5006+
Assert(total_nrels == 1);
49835007
resultRelInfo = mtstate->resultRelInfo;
49845008
if (!resultRelInfo->ri_usesFdwDirectModify &&
49855009
resultRelInfo->ri_FdwRoutine != NULL &&

src/backend/optimizer/plan/planner.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
562562
glob->prunableRelids);
563563
result->permInfos = glob->finalrteperminfos;
564564
result->resultRelations = glob->resultRelations;
565+
result->firstResultRels = glob->firstResultRels;
565566
result->appendRelations = glob->appendRelations;
566567
result->subplans = glob->subplans;
567568
result->rewindPlanIDs = glob->rewindPlanIDs;

src/backend/optimizer/plan/setrefs.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,9 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
12481248
lappend_int(root->glob->resultRelations,
12491249
splan->rootRelation);
12501250
}
1251+
root->glob->firstResultRels =
1252+
lappend_int(root->glob->firstResultRels,
1253+
linitial_int(splan->resultRelations));
12511254
}
12521255
break;
12531256
case T_Append:

src/include/executor/execPartition.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ extern void ExecCleanupTupleRouting(ModifyTableState *mtstate,
4343
* subpart_map contains indexes into PartitionPruningData.partrelprunedata[].
4444
*
4545
* partrel Partitioned table Relation; obtained by
46-
* ExecGetRangeTableRelation(estate, rti), where
47-
* rti is PartitionedRelPruneInfo.rtindex.
46+
* ExecGetRangeTableRelation(estate, rti, false),
47+
* where rti is PartitionedRelPruneInfo.rtindex.
4848
* nparts Length of subplan_map[] and subpart_map[].
4949
* subplan_map Subplan index by partition index, or -1.
5050
* subpart_map Subpart index by partition index, or -1.

src/include/executor/executor.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,8 @@ exec_rt_fetch(Index rti, EState *estate)
680680
return (RangeTblEntry *) list_nth(estate->es_range_table, rti - 1);
681681
}
682682

683-
extern Relation ExecGetRangeTableRelation(EState *estate, Index rti);
683+
extern Relation ExecGetRangeTableRelation(EState *estate, Index rti,
684+
bool isResultRel);
684685
extern void ExecInitResultRelation(EState *estate, ResultRelInfo *resultRelInfo,
685686
Index rti);
686687

src/include/nodes/pathnodes.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ typedef struct PlannerGlobal
138138
/* "flat" list of integer RT indexes */
139139
List *resultRelations;
140140

141+
/* "flat" list of integer RT indexes (one per ModifyTable node) */
142+
List *firstResultRels;
143+
141144
/* "flat" list of AppendRelInfos */
142145
List *appendRelations;
143146

src/include/nodes/plannodes.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ typedef struct PlannedStmt
102102
/* integer list of RT indexes, or NIL */
103103
List *resultRelations;
104104

105+
/*
106+
* rtable indexes of first target relation in each ModifyTable node in the
107+
* plan for INSERT/UPDATE/DELETE/MERGE
108+
*/
109+
/* integer list of RT indexes, or NIL */
110+
List *firstResultRels;
111+
105112
/* list of AppendRelInfo nodes */
106113
List *appendRelations;
107114

src/test/regress/expected/partition_prune.out

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4662,6 +4662,88 @@ table part_abc_view;
46624662
2 | c | t
46634663
(1 row)
46644664

4665+
-- MERGE ... INSERT when all pruned from MERGE source.
4666+
begin;
4667+
explain (costs off)
4668+
merge into part_abc_view pt
4669+
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
4670+
when not matched then insert values (1, 'd', false) returning pt.a;
4671+
QUERY PLAN
4672+
------------------------------------------------
4673+
Merge on part_abc
4674+
-> Nested Loop Left Join
4675+
-> Seq Scan on part_abc_2 pt2
4676+
Filter: ((stable_one() + 1) = a)
4677+
-> Materialize
4678+
-> Append
4679+
Subplans Removed: 2
4680+
(7 rows)
4681+
4682+
merge into part_abc_view pt
4683+
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
4684+
when not matched then insert values (1, 'd', false) returning pt.a;
4685+
a
4686+
---
4687+
1
4688+
(1 row)
4689+
4690+
table part_abc_view;
4691+
a | b | c
4692+
---+---+---
4693+
1 | d | f
4694+
2 | c | t
4695+
(2 rows)
4696+
4697+
rollback;
4698+
-- A case with multiple ModifyTable nodes.
4699+
begin;
4700+
create table part_abc_log (action text, a int, b text, c bool);
4701+
explain (costs off)
4702+
with t as (
4703+
merge into part_abc_view pt
4704+
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
4705+
when not matched then insert values (1, 'd', false) returning merge_action(), pt.*
4706+
)
4707+
insert into part_abc_log select * from t returning *;
4708+
QUERY PLAN
4709+
--------------------------------------------------------
4710+
Insert on part_abc_log
4711+
CTE t
4712+
-> Merge on part_abc
4713+
-> Nested Loop Left Join
4714+
-> Seq Scan on part_abc_2 pt2
4715+
Filter: ((stable_one() + 1) = a)
4716+
-> Materialize
4717+
-> Append
4718+
Subplans Removed: 2
4719+
-> CTE Scan on t
4720+
(10 rows)
4721+
4722+
with t as (
4723+
merge into part_abc_view pt
4724+
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
4725+
when not matched then insert values (1, 'd', false) returning merge_action(), pt.*
4726+
)
4727+
insert into part_abc_log select * from t returning *;
4728+
action | a | b | c
4729+
--------+---+---+---
4730+
INSERT | 1 | d | f
4731+
(1 row)
4732+
4733+
table part_abc_view;
4734+
a | b | c
4735+
---+---+---
4736+
1 | d | f
4737+
2 | c | t
4738+
(2 rows)
4739+
4740+
table part_abc_log;
4741+
action | a | b | c
4742+
--------+---+---+---
4743+
INSERT | 1 | d | f
4744+
(1 row)
4745+
4746+
rollback;
46654747
-- A case with nested MergeAppend with its own PartitionPruneInfo.
46664748
create index on part_abc (a);
46674749
alter table part_abc add d int;

0 commit comments

Comments
 (0)