From 582b60ee6ec9bb7e48d252254bd2f868a0a76b9f Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 23 Oct 2026 08:41:39 +0900 Subject: [PATCH] Set HEAP_XMAX_INVALID for tuples updated by aborted subtransaction HeapTupleSatisfiesVacuumHorizon() did not set HEAP_XMAX_INVALID on a tuple inserted, then updated by a subtransaction that rolled back. rewriteheap.c relies on this hint to tell if such tuples should be treated as updated and would drop them, causing rewrites to lose some tuples. HeapTupleSatisfiesVacuumHorizon() is updated to set the hint HEAP_XMAX_INVALID, so as heapam is able to redirect the tuple to its correct route, and does not lose it. This issue impacted both CLUSTER (down to v14) and REPACK [USING] (down to v19). Discussion: https://postgr.es/m/ Backpatch-through: 14 --- src/backend/access/heap/heapam_visibility.c | 2 ++ src/test/regress/expected/cluster.out | 28 +++++++++++++++++++++ src/test/regress/sql/cluster.sql | 19 ++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/backend/access/heap/heapam_visibility.c b/src/backend/access/heap/heapam_visibility.c index 361b76e50652..905444ae0360 100644 --- a/src/backend/access/heap/heapam_visibility.c +++ b/src/backend/access/heap/heapam_visibility.c @@ -1178,6 +1178,8 @@ HeapTupleSatisfiesVacuumHorizon(HeapTuple htup, Buffer buffer, TransactionId *de if (TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple))) return HEAPTUPLE_DELETE_IN_PROGRESS; /* deleting subtransaction must have aborted */ + SetHintBits(tuple, buffer, HEAP_XMAX_INVALID, + InvalidTransactionId); return HEAPTUPLE_INSERT_IN_PROGRESS; } else if (TransactionIdIsInProgress(HeapTupleHeaderGetRawXmin(tuple))) diff --git a/src/test/regress/expected/cluster.out b/src/test/regress/expected/cluster.out index d99c90cced59..280bb8d2d23f 100644 --- a/src/test/regress/expected/cluster.out +++ b/src/test/regress/expected/cluster.out @@ -880,6 +880,33 @@ ORDER BY o.relname; clstr_3 (2 rows) +-- CLUSTER with rollbacked subtransaction, where the same row is inserted, +-- updated, then rollbacked. +BEGIN; +-- Index scan required for CLUSTER, force it. +SET LOCAL random_page_cost = 0.0001; +SET LOCAL seq_page_cost = 1000; +CREATE TABLE cluster_aborted_upd (a int, b int); +CREATE INDEX cluster_aborted_upd_idx ON cluster_aborted_upd (b); +INSERT INTO cluster_aborted_upd VALUES (1, 3); +SAVEPOINT sp; +UPDATE cluster_aborted_upd SET a = 2; +ROLLBACK TO sp; +CLUSTER cluster_aborted_upd USING cluster_aborted_upd_idx; +COMMIT; +-- One row expected. +SELECT count(*) FROM cluster_aborted_upd; + count +------- + 1 +(1 row) + +SELECT * FROM cluster_aborted_upd; + a | b +---+--- + 1 | 3 +(1 row) + -- clean up DROP TABLE clustertest; DROP TABLE clstr_1; @@ -888,4 +915,5 @@ DROP TABLE clstr_3; DROP TABLE clstr_4; DROP TABLE clstr_expression; DROP TABLE clstrpart; +DROP TABLE cluster_aborted_upd; DROP USER regress_clstr_user; diff --git a/src/test/regress/sql/cluster.sql b/src/test/regress/sql/cluster.sql index eb0da1cf22e9..b636f93aa547 100644 --- a/src/test/regress/sql/cluster.sql +++ b/src/test/regress/sql/cluster.sql @@ -454,6 +454,24 @@ JOIN relnodes_new n ON o.relname = n.relname WHERE o.relfilenode <> n.relfilenode ORDER BY o.relname; +-- CLUSTER with rollbacked subtransaction, where the same row is inserted, +-- updated, then rollbacked. +BEGIN; +-- Index scan required for CLUSTER, force it. +SET LOCAL random_page_cost = 0.0001; +SET LOCAL seq_page_cost = 1000; +CREATE TABLE cluster_aborted_upd (a int, b int); +CREATE INDEX cluster_aborted_upd_idx ON cluster_aborted_upd (b); +INSERT INTO cluster_aborted_upd VALUES (1, 3); +SAVEPOINT sp; +UPDATE cluster_aborted_upd SET a = 2; +ROLLBACK TO sp; +CLUSTER cluster_aborted_upd USING cluster_aborted_upd_idx; +COMMIT; +-- One row expected. +SELECT count(*) FROM cluster_aborted_upd; +SELECT * FROM cluster_aborted_upd; + -- clean up DROP TABLE clustertest; DROP TABLE clstr_1; @@ -462,5 +480,6 @@ DROP TABLE clstr_3; DROP TABLE clstr_4; DROP TABLE clstr_expression; DROP TABLE clstrpart; +DROP TABLE cluster_aborted_upd; DROP USER regress_clstr_user; -- 2.56.0