mirror of
https://git.postgresql.org/git/postgresql.git
synced 2025-01-24 18:55:04 +08:00
Fix failure in WHERE CURRENT OF after rewinding the referenced cursor.
In a case where we have multiple relation-scan nodes in a cursor plan, such as a scan of an inheritance tree, it's possible to fetch from a given scan node, then rewind the cursor and fetch some row from an earlier scan node. In such a case, execCurrent.c mistakenly thought that the later scan node was still active, because ExecReScan hadn't done anything to make it look not-active. We'd get some sort of failure in the case of a SeqScan node, because the node's scan tuple slot would be pointing at a HeapTuple whose t_self gets reset to invalid by heapam.c. But it seems possible that for other relation scan node types we'd actually return a valid tuple TID to the caller, resulting in updating or deleting a tuple that shouldn't have been considered current. To fix, forcibly clear the ScanTupleSlot in ExecScanReScan. Another issue here, which seems only latent at the moment but could easily become a live bug in future, is that rewinding a cursor does not necessarily lead to *immediately* applying ExecReScan to every scan-level node in the plan tree. Upper-level nodes will think that they can postpone that call if their child node is already marked with chgParam flags. I don't see a way for that to happen today in a plan tree that's simple enough for execCurrent.c's search_plan_tree to understand, but that's one heck of a fragile assumption. So, add some logic in search_plan_tree to detect chgParam flags being set on nodes that it descended to/through, and assume that that means we should consider lower scan nodes to be logically reset even if their ReScan call hasn't actually happened yet. Per bug #15395 from Matvey Arye. This has been broken for a long time, so back-patch to all supported branches. Discussion: https://postgr.es/m/153764171023.14986.280404050547008575@wrigleys.postgresql.org
This commit is contained in:
parent
2f39106a20
commit
89b280e139
@ -23,7 +23,8 @@
|
||||
|
||||
|
||||
static char *fetch_cursor_param_value(ExprContext *econtext, int paramId);
|
||||
static ScanState *search_plan_tree(PlanState *node, Oid table_oid);
|
||||
static ScanState *search_plan_tree(PlanState *node, Oid table_oid,
|
||||
bool *pending_rescan);
|
||||
|
||||
|
||||
/*
|
||||
@ -156,8 +157,10 @@ execCurrentOf(CurrentOfExpr *cexpr,
|
||||
* aggregation.
|
||||
*/
|
||||
ScanState *scanstate;
|
||||
bool pending_rescan = false;
|
||||
|
||||
scanstate = search_plan_tree(queryDesc->planstate, table_oid);
|
||||
scanstate = search_plan_tree(queryDesc->planstate, table_oid,
|
||||
&pending_rescan);
|
||||
if (!scanstate)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_CURSOR_STATE),
|
||||
@ -177,8 +180,12 @@ execCurrentOf(CurrentOfExpr *cexpr,
|
||||
errmsg("cursor \"%s\" is not positioned on a row",
|
||||
cursor_name)));
|
||||
|
||||
/* Now OK to return false if we found an inactive scan */
|
||||
if (TupIsNull(scanstate->ss_ScanTupleSlot))
|
||||
/*
|
||||
* Now OK to return false if we found an inactive scan. It is
|
||||
* inactive either if it's not positioned on a row, or there's a
|
||||
* rescan pending for it.
|
||||
*/
|
||||
if (TupIsNull(scanstate->ss_ScanTupleSlot) || pending_rescan)
|
||||
return false;
|
||||
|
||||
/*
|
||||
@ -291,10 +298,20 @@ fetch_cursor_param_value(ExprContext *econtext, int paramId)
|
||||
*
|
||||
* Search through a PlanState tree for a scan node on the specified table.
|
||||
* Return NULL if not found or multiple candidates.
|
||||
*
|
||||
* If a candidate is found, set *pending_rescan to true if that candidate
|
||||
* or any node above it has a pending rescan action, i.e. chgParam != NULL.
|
||||
* That indicates that we shouldn't consider the node to be positioned on a
|
||||
* valid tuple, even if its own state would indicate that it is. (Caller
|
||||
* must initialize *pending_rescan to false, and should not trust its state
|
||||
* if multiple candidates are found.)
|
||||
*/
|
||||
static ScanState *
|
||||
search_plan_tree(PlanState *node, Oid table_oid)
|
||||
search_plan_tree(PlanState *node, Oid table_oid,
|
||||
bool *pending_rescan)
|
||||
{
|
||||
ScanState *result = NULL;
|
||||
|
||||
if (node == NULL)
|
||||
return NULL;
|
||||
switch (nodeTag(node))
|
||||
@ -314,7 +331,7 @@ search_plan_tree(PlanState *node, Oid table_oid)
|
||||
ScanState *sstate = (ScanState *) node;
|
||||
|
||||
if (RelationGetRelid(sstate->ss_currentRelation) == table_oid)
|
||||
return sstate;
|
||||
result = sstate;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -325,13 +342,13 @@ search_plan_tree(PlanState *node, Oid table_oid)
|
||||
case T_AppendState:
|
||||
{
|
||||
AppendState *astate = (AppendState *) node;
|
||||
ScanState *result = NULL;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < astate->as_nplans; i++)
|
||||
{
|
||||
ScanState *elem = search_plan_tree(astate->appendplans[i],
|
||||
table_oid);
|
||||
table_oid,
|
||||
pending_rescan);
|
||||
|
||||
if (!elem)
|
||||
continue;
|
||||
@ -339,7 +356,7 @@ search_plan_tree(PlanState *node, Oid table_oid)
|
||||
return NULL; /* multiple matches */
|
||||
result = elem;
|
||||
}
|
||||
return result;
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -348,13 +365,13 @@ search_plan_tree(PlanState *node, Oid table_oid)
|
||||
case T_MergeAppendState:
|
||||
{
|
||||
MergeAppendState *mstate = (MergeAppendState *) node;
|
||||
ScanState *result = NULL;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < mstate->ms_nplans; i++)
|
||||
{
|
||||
ScanState *elem = search_plan_tree(mstate->mergeplans[i],
|
||||
table_oid);
|
||||
table_oid,
|
||||
pending_rescan);
|
||||
|
||||
if (!elem)
|
||||
continue;
|
||||
@ -362,7 +379,7 @@ search_plan_tree(PlanState *node, Oid table_oid)
|
||||
return NULL; /* multiple matches */
|
||||
result = elem;
|
||||
}
|
||||
return result;
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -371,18 +388,31 @@ search_plan_tree(PlanState *node, Oid table_oid)
|
||||
*/
|
||||
case T_ResultState:
|
||||
case T_LimitState:
|
||||
return search_plan_tree(node->lefttree, table_oid);
|
||||
result = search_plan_tree(node->lefttree,
|
||||
table_oid,
|
||||
pending_rescan);
|
||||
break;
|
||||
|
||||
/*
|
||||
* SubqueryScan too, but it keeps the child in a different place
|
||||
*/
|
||||
case T_SubqueryScanState:
|
||||
return search_plan_tree(((SubqueryScanState *) node)->subplan,
|
||||
table_oid);
|
||||
result = search_plan_tree(((SubqueryScanState *) node)->subplan,
|
||||
table_oid,
|
||||
pending_rescan);
|
||||
break;
|
||||
|
||||
default:
|
||||
/* Otherwise, assume we can't descend through it */
|
||||
break;
|
||||
}
|
||||
return NULL;
|
||||
|
||||
/*
|
||||
* If we found a candidate at or below this node, then this node's
|
||||
* chgParam indicates a pending rescan that will affect the candidate.
|
||||
*/
|
||||
if (result && node->chgParam != NULL)
|
||||
*pending_rescan = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -263,6 +263,12 @@ ExecScanReScan(ScanState *node)
|
||||
{
|
||||
EState *estate = node->ps.state;
|
||||
|
||||
/*
|
||||
* We must clear the scan tuple so that observers (e.g., execCurrent.c)
|
||||
* can tell that this plan node is not positioned on a tuple.
|
||||
*/
|
||||
ExecClearTuple(node->ss_ScanTupleSlot);
|
||||
|
||||
/* Rescan EvalPlanQual tuple if we're inside an EvalPlanQual recheck */
|
||||
if (estate->es_epqScanDone != NULL)
|
||||
{
|
||||
|
@ -1269,6 +1269,76 @@ SELECT stringu1 FROM onek WHERE stringu1 = 'DZAAAA';
|
||||
----------
|
||||
(0 rows)
|
||||
|
||||
ROLLBACK;
|
||||
-- Check behavior with rewinding to a previous child scan node,
|
||||
-- as per bug #15395
|
||||
BEGIN;
|
||||
CREATE TABLE current_check (currentid int, payload text);
|
||||
CREATE TABLE current_check_1 () INHERITS (current_check);
|
||||
CREATE TABLE current_check_2 () INHERITS (current_check);
|
||||
INSERT INTO current_check_1 SELECT i, 'p' || i FROM generate_series(1,9) i;
|
||||
INSERT INTO current_check_2 SELECT i, 'P' || i FROM generate_series(10,19) i;
|
||||
DECLARE c1 SCROLL CURSOR FOR SELECT * FROM current_check;
|
||||
-- This tests the fetch-backwards code path
|
||||
FETCH ABSOLUTE 12 FROM c1;
|
||||
currentid | payload
|
||||
-----------+---------
|
||||
12 | P12
|
||||
(1 row)
|
||||
|
||||
FETCH ABSOLUTE 8 FROM c1;
|
||||
currentid | payload
|
||||
-----------+---------
|
||||
8 | p8
|
||||
(1 row)
|
||||
|
||||
DELETE FROM current_check WHERE CURRENT OF c1 RETURNING *;
|
||||
currentid | payload
|
||||
-----------+---------
|
||||
8 | p8
|
||||
(1 row)
|
||||
|
||||
-- This tests the ExecutorRewind code path
|
||||
FETCH ABSOLUTE 13 FROM c1;
|
||||
currentid | payload
|
||||
-----------+---------
|
||||
13 | P13
|
||||
(1 row)
|
||||
|
||||
FETCH ABSOLUTE 1 FROM c1;
|
||||
currentid | payload
|
||||
-----------+---------
|
||||
1 | p1
|
||||
(1 row)
|
||||
|
||||
DELETE FROM current_check WHERE CURRENT OF c1 RETURNING *;
|
||||
currentid | payload
|
||||
-----------+---------
|
||||
1 | p1
|
||||
(1 row)
|
||||
|
||||
SELECT * FROM current_check;
|
||||
currentid | payload
|
||||
-----------+---------
|
||||
2 | p2
|
||||
3 | p3
|
||||
4 | p4
|
||||
5 | p5
|
||||
6 | p6
|
||||
7 | p7
|
||||
9 | p9
|
||||
10 | P10
|
||||
11 | P11
|
||||
12 | P12
|
||||
13 | P13
|
||||
14 | P14
|
||||
15 | P15
|
||||
16 | P16
|
||||
17 | P17
|
||||
18 | P18
|
||||
19 | P19
|
||||
(17 rows)
|
||||
|
||||
ROLLBACK;
|
||||
-- Make sure snapshot management works okay, per bug report in
|
||||
-- 235395b90909301035v7228ce63q392931f15aa74b31@mail.gmail.com
|
||||
|
@ -472,6 +472,30 @@ DELETE FROM onek WHERE CURRENT OF c1;
|
||||
SELECT stringu1 FROM onek WHERE stringu1 = 'DZAAAA';
|
||||
ROLLBACK;
|
||||
|
||||
-- Check behavior with rewinding to a previous child scan node,
|
||||
-- as per bug #15395
|
||||
BEGIN;
|
||||
CREATE TABLE current_check (currentid int, payload text);
|
||||
CREATE TABLE current_check_1 () INHERITS (current_check);
|
||||
CREATE TABLE current_check_2 () INHERITS (current_check);
|
||||
INSERT INTO current_check_1 SELECT i, 'p' || i FROM generate_series(1,9) i;
|
||||
INSERT INTO current_check_2 SELECT i, 'P' || i FROM generate_series(10,19) i;
|
||||
|
||||
DECLARE c1 SCROLL CURSOR FOR SELECT * FROM current_check;
|
||||
|
||||
-- This tests the fetch-backwards code path
|
||||
FETCH ABSOLUTE 12 FROM c1;
|
||||
FETCH ABSOLUTE 8 FROM c1;
|
||||
DELETE FROM current_check WHERE CURRENT OF c1 RETURNING *;
|
||||
|
||||
-- This tests the ExecutorRewind code path
|
||||
FETCH ABSOLUTE 13 FROM c1;
|
||||
FETCH ABSOLUTE 1 FROM c1;
|
||||
DELETE FROM current_check WHERE CURRENT OF c1 RETURNING *;
|
||||
|
||||
SELECT * FROM current_check;
|
||||
ROLLBACK;
|
||||
|
||||
-- Make sure snapshot management works okay, per bug report in
|
||||
-- 235395b90909301035v7228ce63q392931f15aa74b31@mail.gmail.com
|
||||
BEGIN;
|
||||
|
Loading…
Reference in New Issue
Block a user