From d434b39c83db5c3804aab4de657946324c083a2a Mon Sep 17 00:00:00 2001
From: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Date: Sat, 16 Aug 2025 09:12:32 +0000
Subject: [PATCH v1] Add isolation test for TOAST value deduplication during
 CLUSTER

This test exercises the corner case in toast_save_datum() where CLUSTER operations encounter duplicate TOAST references and correctly reuse existing TOAST data instead of creating redundant copies.

During table rewrites like CLUSTER, both live and recently-dead versions of a row may reference the same TOAST value. When copying the second or later version of such a row, the system checks if the TOAST OID already exists in the new toast table using toastrel_valueid_exists(). If found, it sets data_todo = 0 to skip redundant data storage, ensuring only one copy of the TOAST value exists in the new table.

The test creates a scenario where:
- Session 1 updates rows while holding a transaction lock
- Session 2 attempts CLUSTER, which waits for the lock
- When CLUSTER proceeds, it encounters the duplicate TOAST references
- The test verifies TOAST chunk IDs are preserved via deduplication
---
 .../expected/cluster-toast-value-reuse.out    | 29 +++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/cluster-toast-value-reuse.spec      | 64 +++++++++++++++++++
 3 files changed, 94 insertions(+)
 create mode 100644 src/test/isolation/expected/cluster-toast-value-reuse.out
 create mode 100644 src/test/isolation/specs/cluster-toast-value-reuse.spec

diff --git a/src/test/isolation/expected/cluster-toast-value-reuse.out b/src/test/isolation/expected/cluster-toast-value-reuse.out
new file mode 100644
index 00000000000..84cfc00c84e
--- /dev/null
+++ b/src/test/isolation/expected/cluster-toast-value-reuse.out
@@ -0,0 +1,29 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_begin s1_update s2_store_chunk_ids s2_cluster s1_commit s2_verify_chunk_ids
+step s1_begin: BEGIN;
+step s1_update: 
+  UPDATE cluster_toast_value_reuse
+     SET flag = 1 WHERE TRUE;
+
+step s2_store_chunk_ids: 
+  -- Store the primary keys and their associated chunk IDs before CLUSTER
+  CREATE TABLE chunk_id_comparison AS
+  SELECT c.id, pg_column_toast_chunk_id(c.value) as chunk_id
+  FROM cluster_toast_value_reuse c;
+
+step s2_cluster: CLUSTER cluster_toast_value_reuse; <waiting ...>
+step s1_commit: COMMIT;
+step s2_cluster: <... completed>
+step s2_verify_chunk_ids: 
+  -- Verify that chunk IDs are the same before and after CLUSTER (indicating reuse)
+  SELECT COUNT(*) = 0 AS chunk_ids_preserved
+  FROM chunk_id_comparison orig
+  JOIN cluster_toast_value_reuse c ON orig.id = c.id
+  WHERE orig.chunk_id != pg_column_toast_chunk_id(c.value);
+
+chunk_ids_preserved
+-------------------
+t                  
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4411d3c86dd..cb8a3bfbcbf 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -117,3 +117,4 @@ test: serializable-parallel-2
 test: serializable-parallel-3
 test: matview-write-skew
 test: lock-nowait
+test: cluster-toast-value-reuse
diff --git a/src/test/isolation/specs/cluster-toast-value-reuse.spec b/src/test/isolation/specs/cluster-toast-value-reuse.spec
new file mode 100644
index 00000000000..c123da9f720
--- /dev/null
+++ b/src/test/isolation/specs/cluster-toast-value-reuse.spec
@@ -0,0 +1,64 @@
+# Hold an UPDATE open, run CLUSTER in another session, then COMMIT. Which triggers data_todo = 0; code path in toast_save_datum
+
+# ---------- global setup ----------
+setup
+{
+  DROP TABLE IF EXISTS cluster_toast_value_reuse CASCADE;
+  DROP TABLE IF EXISTS chunk_id_comparison CASCADE;
+
+  CREATE TABLE cluster_toast_value_reuse
+  (
+    id    serial PRIMARY KEY,
+    flag  integer,
+    value text
+  );
+
+  -- Make sure 'value' is large enough to be TOASTed.
+  ALTER TABLE cluster_toast_value_reuse ALTER COLUMN value SET STORAGE EXTERNAL;
+
+  -- Define the clustering index.
+  CLUSTER "cluster_toast_value_reuse_pkey" ON cluster_toast_value_reuse;
+
+  -- Seed data: one row with big string to force TOAST tuple and trigger the todo=0 code path.
+  INSERT INTO cluster_toast_value_reuse(flag, value) 
+  VALUES (0, repeat(md5('1'), 120) || repeat('x', 8000));
+
+  CLUSTER cluster_toast_value_reuse;
+}
+
+teardown
+{
+  DROP TABLE IF EXISTS cluster_toast_value_reuse;
+  DROP TABLE IF EXISTS chunk_id_comparison;
+}
+
+# ---------- sessions ----------
+# Session 1: starts a txn and updates some rows, then commits later.
+session s1
+step s1_begin  { BEGIN; }
+step s1_update {
+  UPDATE cluster_toast_value_reuse
+     SET flag = 1 WHERE TRUE;
+}
+step s1_commit { COMMIT; }
+
+# Session 2: runs CLUSTER while s1 holds locks.
+session s2
+step s2_store_chunk_ids {
+  -- Store the primary keys and their associated chunk IDs before CLUSTER
+  CREATE TABLE chunk_id_comparison AS
+  SELECT c.id, pg_column_toast_chunk_id(c.value) as chunk_id
+  FROM cluster_toast_value_reuse c;
+}
+step s2_cluster { CLUSTER cluster_toast_value_reuse; }
+step s2_verify_chunk_ids {
+  -- Verify that chunk IDs are the same before and after CLUSTER (indicating reuse)
+  SELECT COUNT(*) = 0 AS chunk_ids_preserved
+  FROM chunk_id_comparison orig
+  JOIN cluster_toast_value_reuse c ON orig.id = c.id
+  WHERE orig.chunk_id != pg_column_toast_chunk_id(c.value);
+}
+
+# ---------- single interleaving ----------
+# Do the update in s1, store chunk IDs, then attempt CLUSTER in s2 (will wait), then commit s1, then verify chunk IDs.
+permutation s1_begin s1_update s2_store_chunk_ids s2_cluster s1_commit s2_verify_chunk_ids
-- 
2.47.3

