postgres.git / summary / log / commit / refs

commit    39a0db101105eab3f4044d11c609c58b9459ea16
Author:   David Rowley <drowley@postgresql.org>
Date:     Wed Sep 23 13:20:02 2026 +0000

    Fix incorrect block accounting in TID Range Scans
    
    heapgettup_advance_block() (or heapgettup() and heapgettup_pagemode() in
    versions before v16) would incorrectly decrement rs_numblocks when
    advancing to the next block.  Ordinarily this was fine, but if the TID
    Range scan plan was used by a scrollable cursor and the cursor was fetched
    backwards enough to result in another block being required, then
    rs_numblocks may have reached 0 prematurely resulting in the scan ending
    prematurely.
    
    Reported-by: Jeff Davis <pgsql@j-davis.com>
    Author: Ewan Young <kdbase.hack@gmail.com>
    Author: David Rowley <dgrowleyml@gmail.com>
    Discussion: https://postgr.es/m/6b50233fcc0f7d6f4845f083777fa9702d9612e4.camel@j-davis.com
    Backpatch-through: 14


src/backend/access/heap/heapam.c | 20 +++++++++++--------- src/test/regress/expected/tidrangescan.out | 26 ++++++++++++++++++++++++++ src/test/regress/sql/tidrangescan.sql | 12 ++++++++++++ 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index d72b41ef92f..9ee86340bdc 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -847,7 +847,11 @@ heapgettup_advance_block(HeapScanDesc scan, BlockNumber block, ScanDirection dir /* check if the limit imposed by heap_setscanlimits() is met */ if (scan->rs_numblocks != InvalidBlockNumber) { - if (--scan->rs_numblocks == 0) + BlockNumber endblock; + + endblock = (scan->rs_startblock + scan->rs_numblocks) % + scan->rs_nblocks; + if (block == endblock) return InvalidBlockNumber; } @@ -855,17 +859,15 @@ heapgettup_advance_block(HeapScanDesc scan, BlockNumber block, ScanDirection dir } else { - /* we're done if the last block is the start position */ + /* + * We're done if the last block is the start position. No need to + * check if rs_numblocks was set by heap_setscanlimits() as that only + * changes the end block. The start block is the same with or without + * scan limits. + */ if (block == scan->rs_startblock) return InvalidBlockNumber; - /* check if the limit imposed by heap_setscanlimits() is met */ - if (scan->rs_numblocks != InvalidBlockNumber) - { - if (--scan->rs_numblocks == 0) - return InvalidBlockNumber; - } - /* wrap to the end of the heap when the last page was page 0 */ if (block == 0) block = scan->rs_nblocks; diff --git a/src/test/regress/expected/tidrangescan.out b/src/test/regress/expected/tidrangescan.out index 721f3b94e04..3ccef0fd0c2 100644 --- a/src/test/regress/expected/tidrangescan.out +++ b/src/test/regress/expected/tidrangescan.out @@ -295,6 +295,32 @@ FETCH LAST c; (0,10) (1 row) +COMMIT; +-- Ensure TID Range Scans with scrollable cursors count blocks correctly. +BEGIN; +DECLARE c SCROLL CURSOR FOR SELECT ctid FROM tidrangescan WHERE ctid >= '(0,1)' AND ctid <= '(2,10)'; +MOVE FORWARD 25 c; +MOVE BACKWARD 4 c; +FETCH BACKWARD 1 c; + ctid +-------- + (1,10) +(1 row) + +MOVE BACKWARD ALL c; +FETCH FORWARD 1 c; + ctid +------- + (0,1) +(1 row) + +MOVE FORWARD ALL c; +FETCH BACKWARD 1 c; + ctid +-------- + (2,10) +(1 row) + COMMIT; DROP TABLE tidrangescan; RESET enable_seqscan; diff --git a/src/test/regress/sql/tidrangescan.sql b/src/test/regress/sql/tidrangescan.sql index ac09ebb6262..e367292e885 100644 --- a/src/test/regress/sql/tidrangescan.sql +++ b/src/test/regress/sql/tidrangescan.sql @@ -96,6 +96,18 @@ FETCH FIRST c; FETCH LAST c; COMMIT; +-- Ensure TID Range Scans with scrollable cursors count blocks correctly. +BEGIN; +DECLARE c SCROLL CURSOR FOR SELECT ctid FROM tidrangescan WHERE ctid >= '(0,1)' AND ctid <= '(2,10)'; +MOVE FORWARD 25 c; +MOVE BACKWARD 4 c; +FETCH BACKWARD 1 c; +MOVE BACKWARD ALL c; +FETCH FORWARD 1 c; +MOVE FORWARD ALL c; +FETCH BACKWARD 1 c; +COMMIT; + DROP TABLE tidrangescan; RESET enable_seqscan; [parent: 71672056784f]