Skip to content

Commit ba408fc

Browse files
committed
Fix some anomalies with NO SCROLL cursors.
We have long forbidden fetching backwards from a NO SCROLL cursor, but the prohibition didn't extend to cases in which we rewind the query altogether and then re-fetch forwards. I think the reason is that this logic was mainly meant to protect plan nodes that can't be run in the reverse direction. However, re-reading the query output is problematic if the query is volatile (which includes SELECT FOR UPDATE, not just queries with volatile functions): the re-read can produce different results, which confuses the cursor navigation logic completely. Another reason for disliking this approach is that some code paths will either fetch backwards or rewind-and-fetch-forwards depending on the distance to the target row; so that seemingly identical use-cases may or may not draw the "cursor can only scan forward" error. Hence, let's clean things up by disallowing rewind as well as fetch-backwards in a NO SCROLL cursor. Ordinarily we'd only make such a definitional change in HEAD, but there is a third reason to consider this change now. Commit ba2c6d6 created some new user-visible anomalies for non-scrollable cursors WITH HOLD, in that navigation in the cursor result got confused if the cursor had been partially read before committing. The only good way to resolve those anomalies is to forbid rewinding such a cursor, which allows removal of the incorrect cursor state manipulations that ba2c6d6 added to PersistHoldablePortal. To minimize the behavioral change in the back branches (including v14), refuse to rewind a NO SCROLL cursor only when it has a holdStore, ie has been held over from a previous transaction due to WITH HOLD. This should avoid breaking most applications that have been sloppy about whether to declare cursors as scrollable. We'll enforce the prohibition across-the-board beginning in v15. Back-patch to v11, as ba2c6d6 was. Discussion: https://postgr.es/m/3712911.1631207435@sss.pgh.pa.us
1 parent 2e75e96 commit ba408fc

File tree

4 files changed

+107
-11
lines changed

4 files changed

+107
-11
lines changed

src/backend/commands/portalcmds.c

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -382,17 +382,22 @@ PersistHoldablePortal(Portal portal)
382382
* can be processed. Otherwise, store only the not-yet-fetched rows.
383383
* (The latter is not only more efficient, but avoids semantic
384384
* problems if the query's output isn't stable.)
385+
*
386+
* In the no-scroll case, tuple indexes in the tuplestore will not
387+
* match the cursor's nominal position (portalPos). Currently this
388+
* causes no difficulty because we only navigate in the tuplestore by
389+
* relative position, except for the tuplestore_skiptuples call below
390+
* and the tuplestore_rescan call in DoPortalRewind, both of which are
391+
* disabled for no-scroll cursors. But someday we might need to track
392+
* the offset between the holdStore and the cursor's nominal position
393+
* explicitly.
385394
*/
386395
if (portal->cursorOptions & CURSOR_OPT_SCROLL)
387396
{
388397
ExecutorRewind(queryDesc);
389398
}
390399
else
391400
{
392-
/* Disallow moving backwards from here */
393-
portal->atStart = true;
394-
portal->portalPos = 0;
395-
396401
/*
397402
* If we already reached end-of-query, set the direction to
398403
* NoMovement to avoid trying to fetch any tuples. (This check
@@ -448,10 +453,17 @@ PersistHoldablePortal(Portal portal)
448453
{
449454
tuplestore_rescan(portal->holdStore);
450455

451-
if (!tuplestore_skiptuples(portal->holdStore,
452-
portal->portalPos,
453-
true))
454-
elog(ERROR, "unexpected end of tuple stream");
456+
/*
457+
* In the no-scroll case, the start of the tuplestore is exactly
458+
* where we want to be, so no repositioning is wanted.
459+
*/
460+
if (portal->cursorOptions & CURSOR_OPT_SCROLL)
461+
{
462+
if (!tuplestore_skiptuples(portal->holdStore,
463+
portal->portalPos,
464+
true))
465+
elog(ERROR, "unexpected end of tuple stream");
466+
}
455467
}
456468
}
457469
PG_CATCH();

src/backend/tcop/pquery.c

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,9 +1493,8 @@ PortalRunFetch(Portal portal,
14931493
* DoPortalRunFetch
14941494
* Guts of PortalRunFetch --- the portal context is already set up
14951495
*
1496-
* count <= 0 is interpreted as a no-op: the destination gets started up
1497-
* and shut down, but nothing else happens. Also, count == FETCH_ALL is
1498-
* interpreted as "all rows". (cf FetchStmt.howMany)
1496+
* Here, count < 0 typically reverses the direction. Also, count == FETCH_ALL
1497+
* is interpreted as "all rows". (cf FetchStmt.howMany)
14991498
*
15001499
* Returns number of rows processed (suitable for use in result tag)
15011500
*/
@@ -1512,6 +1511,15 @@ DoPortalRunFetch(Portal portal,
15121511
portal->strategy == PORTAL_ONE_MOD_WITH ||
15131512
portal->strategy == PORTAL_UTIL_SELECT);
15141513

1514+
/*
1515+
* Note: we disallow backwards fetch (including re-fetch of current row)
1516+
* for NO SCROLL cursors, but we interpret that very loosely: you can use
1517+
* any of the FetchDirection options, so long as the end result is to move
1518+
* forwards by at least one row. Currently it's sufficient to check for
1519+
* NO SCROLL in DoPortalRewind() and in the forward == false path in
1520+
* PortalRunSelect(); but someday we might prefer to account for that
1521+
* restriction explicitly here.
1522+
*/
15151523
switch (fdirection)
15161524
{
15171525
case FETCH_FORWARD:
@@ -1689,6 +1697,25 @@ DoPortalRewind(Portal portal)
16891697
{
16901698
QueryDesc *queryDesc;
16911699

1700+
/*
1701+
* No work is needed if we've not advanced nor attempted to advance the
1702+
* cursor (and we don't want to throw a NO SCROLL error in this case).
1703+
*/
1704+
if (portal->atStart && !portal->atEnd)
1705+
return;
1706+
1707+
/*
1708+
* Otherwise, cursor should allow scrolling. However, we're only going to
1709+
* enforce that policy fully beginning in v15. In older branches, insist
1710+
* on this only if the portal has a holdStore. That prevents users from
1711+
* seeing that the holdStore may not have all the rows of the query.
1712+
*/
1713+
if ((portal->cursorOptions & CURSOR_OPT_NO_SCROLL) && portal->holdStore)
1714+
ereport(ERROR,
1715+
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
1716+
errmsg("cursor can only scan forward"),
1717+
errhint("Declare it with SCROLL option to enable backward scan.")));
1718+
16921719
/* Rewind holdStore, if we have one */
16931720
if (portal->holdStore)
16941721
{

src/test/regress/expected/portals.out

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,43 @@ SELECT name, statement, is_holdable, is_binary, is_scrollable FROM pg_cursors;
763763
(1 row)
764764

765765
CLOSE foo25;
766+
BEGIN;
767+
DECLARE foo25ns NO SCROLL CURSOR WITH HOLD FOR SELECT * FROM tenk2;
768+
FETCH FROM foo25ns;
769+
unique1 | unique2 | two | four | ten | twenty | hundred | thousand | twothousand | fivethous | tenthous | odd | even | stringu1 | stringu2 | string4
770+
---------+---------+-----+------+-----+--------+---------+----------+-------------+-----------+----------+-----+------+----------+----------+---------
771+
8800 | 0 | 0 | 0 | 0 | 0 | 0 | 800 | 800 | 3800 | 8800 | 0 | 1 | MAAAAA | AAAAAA | AAAAxx
772+
(1 row)
773+
774+
FETCH FROM foo25ns;
775+
unique1 | unique2 | two | four | ten | twenty | hundred | thousand | twothousand | fivethous | tenthous | odd | even | stringu1 | stringu2 | string4
776+
---------+---------+-----+------+-----+--------+---------+----------+-------------+-----------+----------+-----+------+----------+----------+---------
777+
1891 | 1 | 1 | 3 | 1 | 11 | 91 | 891 | 1891 | 1891 | 1891 | 182 | 183 | TUAAAA | BAAAAA | HHHHxx
778+
(1 row)
779+
780+
COMMIT;
781+
FETCH FROM foo25ns;
782+
unique1 | unique2 | two | four | ten | twenty | hundred | thousand | twothousand | fivethous | tenthous | odd | even | stringu1 | stringu2 | string4
783+
---------+---------+-----+------+-----+--------+---------+----------+-------------+-----------+----------+-----+------+----------+----------+---------
784+
3420 | 2 | 0 | 0 | 0 | 0 | 20 | 420 | 1420 | 3420 | 3420 | 40 | 41 | OBAAAA | CAAAAA | OOOOxx
785+
(1 row)
786+
787+
FETCH ABSOLUTE 4 FROM foo25ns;
788+
unique1 | unique2 | two | four | ten | twenty | hundred | thousand | twothousand | fivethous | tenthous | odd | even | stringu1 | stringu2 | string4
789+
---------+---------+-----+------+-----+--------+---------+----------+-------------+-----------+----------+-----+------+----------+----------+---------
790+
9850 | 3 | 0 | 2 | 0 | 10 | 50 | 850 | 1850 | 4850 | 9850 | 100 | 101 | WOAAAA | DAAAAA | VVVVxx
791+
(1 row)
792+
793+
FETCH ABSOLUTE 4 FROM foo25ns; -- fail
794+
ERROR: cursor can only scan forward
795+
HINT: Declare it with SCROLL option to enable backward scan.
796+
SELECT name, statement, is_holdable, is_binary, is_scrollable FROM pg_cursors;
797+
name | statement | is_holdable | is_binary | is_scrollable
798+
---------+---------------------------------------------------------------------+-------------+-----------+---------------
799+
foo25ns | DECLARE foo25ns NO SCROLL CURSOR WITH HOLD FOR SELECT * FROM tenk2; | t | f | f
800+
(1 row)
801+
802+
CLOSE foo25ns;
766803
--
767804
-- ROLLBACK should close holdable cursors
768805
--

src/test/regress/sql/portals.sql

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,26 @@ SELECT name, statement, is_holdable, is_binary, is_scrollable FROM pg_cursors;
217217

218218
CLOSE foo25;
219219

220+
BEGIN;
221+
222+
DECLARE foo25ns NO SCROLL CURSOR WITH HOLD FOR SELECT * FROM tenk2;
223+
224+
FETCH FROM foo25ns;
225+
226+
FETCH FROM foo25ns;
227+
228+
COMMIT;
229+
230+
FETCH FROM foo25ns;
231+
232+
FETCH ABSOLUTE 4 FROM foo25ns;
233+
234+
FETCH ABSOLUTE 4 FROM foo25ns; -- fail
235+
236+
SELECT name, statement, is_holdable, is_binary, is_scrollable FROM pg_cursors;
237+
238+
CLOSE foo25ns;
239+
220240
--
221241
-- ROLLBACK should close holdable cursors
222242
--

0 commit comments

Comments
 (0)