From d434b39c83db5c3804aab4de657946324c083a2a Mon Sep 17 00:00:00 2001 From: Nikhil Kumar Veldanda 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; +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