From f57e214d1cbb748b1e9be79e1b30d85fcbc9e340 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Sun, 17 Aug 2025 15:20:01 +0900
Subject: [PATCH] Add isolation test for TOAST value reuse during CLUSTER

This test exercises the corner case in toast_save_datum() where CLUSTER
operations encounter duplicated TOAST references, reusing the 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 a TOAST value already
exists in the new TOAST table using toastrel_valueid_exists().  If
found, toast_save_datum() sets data_todo = 0 so as redundant data is not
stored, ensuring only one copy of the TOAST value exists in the new
table.

The test relies on a combination of UPDATE, CLUSTER, and checks of the
TOAST values used before and after the relation rewrite, to make sure
that the same values are reused across the rewrite.

This is a continuation of 69f75d671475 to make sure that this corner
case keeps working should we mess with this area of the code.

Author: Nikhil Kumar Veldanda <veldanda.nikhilkumar17@gmail.com>
Discussion: https://postgr.es/m/CAFAfj_E+kw5P713S8_jZyVgQAGVFfzFiTUJPrgo-TTtJJoazQw@mail.gmail.com
---
 .../expected/cluster-toast-value-reuse.out    | 29 ++++++++
 src/test/isolation/isolation_schedule         |  1 +
 .../specs/cluster-toast-value-reuse.spec      | 69 +++++++++++++++++++
 3 files changed, 99 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 0000000000..cb14ddcee3
--- /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 SET flag = 1 WHERE TRUE;
+step s2_store_chunk_ids: 
+  CREATE TABLE cluster_chunk_id AS
+    SELECT c.id, pg_column_toast_chunk_id(c.value) AS chunk_id
+    FROM cluster_toast_value c;
+  SELECT count(*) FROM cluster_chunk_id;
+
+count
+-----
+    1
+(1 row)
+
+step s2_cluster: CLUSTER cluster_toast_value; <waiting ...>
+step s1_commit: COMMIT;
+step s2_cluster: <... completed>
+step s2_verify_chunk_ids: 
+  SELECT o.id AS chunk_ids_preserved
+    FROM cluster_chunk_id o
+    JOIN cluster_toast_value c ON o.id = c.id
+    WHERE o.chunk_id != pg_column_toast_chunk_id(c.value);
+
+chunk_ids_preserved
+-------------------
+(0 rows)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4411d3c86d..9f1e997d81 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -111,6 +111,7 @@ test: partition-key-update-4
 test: plpgsql-toast
 test: cluster-conflict
 test: cluster-conflict-partition
+test: cluster-toast-value-reuse
 test: truncate-conflict
 test: serializable-parallel
 test: serializable-parallel-2
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 0000000000..1930eedb85
--- /dev/null
+++ b/src/test/isolation/specs/cluster-toast-value-reuse.spec
@@ -0,0 +1,69 @@
+# Tests with CLUSTER for toast values
+
+# This test does a relation rewrite, with toast values reused to make the
+# rewrite cheaper (see data_todo = 0 case in toast_save_datum()).
+#
+# A first session updates the table with an attribute not toasted.  CLUSTER
+# is then executed in a second session.  The comparison of the values
+# allocated for the toasted values are done using a CTAS.  The allocated
+# chunk_ids are saved before the rewrite, and compared after the rewrite.
+
+# ---------- global setup ----------
+setup
+{
+  DROP TABLE IF EXISTS cluster_toast_value CASCADE;
+  DROP TABLE IF EXISTS cluster_chunk_id CASCADE;
+
+  CREATE TABLE cluster_toast_value (
+    id    serial PRIMARY KEY,
+    flag  integer,
+    value text);
+
+  -- Make sure 'value' is large enough to be toasted.
+  ALTER TABLE cluster_toast_value ALTER COLUMN value SET STORAGE EXTERNAL;
+
+  -- Clustering index.
+  CLUSTER cluster_toast_value_pkey ON cluster_toast_value;
+
+  -- Seed data: one row with big string to force TOAST tuple and trigger the todo=0 code path.
+  INSERT INTO cluster_toast_value(flag, value)
+    VALUES (0, repeat(md5('1'), 120) || repeat('x', 8000));
+
+  CLUSTER cluster_toast_value;
+}
+
+teardown
+{
+  DROP TABLE IF EXISTS cluster_toast_value;
+  DROP TABLE IF EXISTS cluster_chunk_id;
+}
+
+session s1
+step s1_begin  { BEGIN; }
+step s1_update { UPDATE cluster_toast_value SET flag = 1 WHERE TRUE; }
+step s1_commit { COMMIT; }
+
+session s2
+# Store the primary key values and their associated chunk IDs.  This makes
+# sure that some data is captured.
+step s2_store_chunk_ids {
+  CREATE TABLE cluster_chunk_id AS
+    SELECT c.id, pg_column_toast_chunk_id(c.value) AS chunk_id
+    FROM cluster_toast_value c;
+  SELECT count(*) FROM cluster_chunk_id;
+}
+step s2_cluster { CLUSTER cluster_toast_value; }
+
+# Verify that toast values allocated are the same, indicating reuse.
+# This query reports the tuples with toast values that do not match.
+step s2_verify_chunk_ids {
+  SELECT o.id AS chunk_ids_preserved
+    FROM cluster_chunk_id o
+    JOIN cluster_toast_value c ON o.id = c.id
+    WHERE o.chunk_id != pg_column_toast_chunk_id(c.value);
+}
+
+# Run UPDATE with its transaction still open, then store the chunk IDs.
+# CLUSTER will wait until the first transaction commit.  Finally, the chunk
+# IDs are compared.
+permutation s1_begin s1_update s2_store_chunk_ids s2_cluster s1_commit s2_verify_chunk_ids
-- 
2.39.5

