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]