0002-Add-compressed-TOAST-test-to-repack_toast.patch

application/octet-stream

Filename: 0002-Add-compressed-TOAST-test-to-repack_toast.patch
Type: application/octet-stream
Part: 1
Message: [PATCH] Compressed TOAST data corruption with REPACK CONCURRENTLY

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: unified
Series: patch 0002
File+
src/test/modules/injection_points/expected/repack_toast.out 57 0
src/test/modules/injection_points/specs/repack_toast.spec 59 0
diff --git a/src/test/modules/injection_points/expected/repack_toast.out b/src/test/modules/injection_points/expected/repack_toast.out
index b56dde1..0eefc88 100644
--- a/src/test/modules/injection_points/expected/repack_toast.out
+++ b/src/test/modules/injection_points/expected/repack_toast.out
@@ -63,3 +63,60 @@ injection_points_detach
                        
 (1 row)
 
+
+starting permutation: repack_compressed compressed_update wakeup_before_lock verify_compressed
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step repack_compressed: 
+	REPACK (CONCURRENTLY) repack_ctest;
+ <waiting ...>
+step compressed_update: 
+	UPDATE repack_ctest SET j = gen_compressible(99) WHERE i = 2;
+
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step repack_compressed: <... completed>
+step verify_compressed: 
+	INSERT INTO post_repack_data
+	SELECT i, j FROM repack_ctest ORDER BY i;
+
+	SELECT e.i,
+	       length(e.j) AS expect_len,
+	       length(p.j) AS actual_len,
+	       (length(e.j) = length(p.j)) AS len_ok,
+	       (md5(e.j) = md5(p.j)) AS hash_ok
+	FROM expected_data e
+	JOIN post_repack_data p USING (i)
+	ORDER BY e.i;
+
+	SELECT count(*) AS mismatches
+	FROM expected_data e
+	JOIN post_repack_data p USING (i)
+	WHERE length(e.j) != length(p.j) OR md5(e.j) != md5(p.j);
+
+i|expect_len|actual_len|len_ok|hash_ok
+-+----------+----------+------+-------
+1|     16000|     16000|t     |t      
+2|     16000|     16000|t     |t      
+3|     16000|     16000|t     |t      
+(3 rows)
+
+mismatches
+----------
+         0
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/specs/repack_toast.spec b/src/test/modules/injection_points/specs/repack_toast.spec
index b878b19..fb8a557 100644
--- a/src/test/modules/injection_points/specs/repack_toast.spec
+++ b/src/test/modules/injection_points/specs/repack_toast.spec
@@ -13,6 +13,14 @@ setup
 		FROM generate_series(1, 2048) s(x);
 	$$;
 
+	-- 500 concatenated md5 hashes = 16000 hex chars; compresses but stays > 2KB.
+	CREATE FUNCTION gen_compressible(seed int)
+	RETURNS text
+	LANGUAGE sql IMMUTABLE as $$
+		SELECT string_agg(md5((seed * 1000 + x)::text), '')
+		FROM generate_series(1, 500) x;
+	$$;
+
 	CREATE TABLE repack_test(i int PRIMARY KEY, j text);
 	INSERT INTO repack_test(i, j) VALUES (1, get_long_string()),
 		(2, get_long_string()), (3, get_long_string());
@@ -21,13 +29,30 @@ setup
 
 	CREATE TABLE data_s1(i int, j text);
 	CREATE TABLE data_s2(i int, j text);
+
+	CREATE TABLE repack_ctest(i int PRIMARY KEY, j text);
+	INSERT INTO repack_ctest(i, j)
+	VALUES (1, gen_compressible(1)),
+	       (2, gen_compressible(2)),
+	       (3, gen_compressible(3));
+
+	CREATE TABLE expected_data AS
+	SELECT 1 AS i, gen_compressible(1) AS j
+	UNION ALL SELECT 2, gen_compressible(99)
+	UNION ALL SELECT 3, gen_compressible(3);
+
+	CREATE TABLE post_repack_data(i int, j text);
 }
 
 teardown
 {
 	DROP TABLE repack_test;
+	DROP TABLE repack_ctest;
+	DROP TABLE expected_data;
+	DROP TABLE post_repack_data;
 	DROP EXTENSION injection_points;
 	DROP FUNCTION get_long_string();
+	DROP FUNCTION gen_compressible(int);
 
 	DROP TABLE relfilenodes;
 	DROP TABLE data_s1;
@@ -67,6 +92,29 @@ step check1
 	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
 	WHERE d1.i ISNULL OR d2.i ISNULL;
 }
+step repack_compressed
+{
+	REPACK (CONCURRENTLY) repack_ctest;
+}
+step verify_compressed
+{
+	INSERT INTO post_repack_data
+	SELECT i, j FROM repack_ctest ORDER BY i;
+
+	SELECT e.i,
+	       length(e.j) AS expect_len,
+	       length(p.j) AS actual_len,
+	       (length(e.j) = length(p.j)) AS len_ok,
+	       (md5(e.j) = md5(p.j)) AS hash_ok
+	FROM expected_data e
+	JOIN post_repack_data p USING (i)
+	ORDER BY e.i;
+
+	SELECT count(*) AS mismatches
+	FROM expected_data e
+	JOIN post_repack_data p USING (i)
+	WHERE length(e.j) != length(p.j) OR md5(e.j) != md5(p.j);
+}
 teardown
 {
     SELECT injection_points_detach('repack-concurrently-before-lock');
@@ -101,6 +149,10 @@ step wakeup_before_lock
 {
 	SELECT injection_points_wakeup('repack-concurrently-before-lock');
 }
+step compressed_update
+{
+	UPDATE repack_ctest SET j = gen_compressible(99) WHERE i = 2;
+}
 
 # Test if data changes introduced while one session is performing REPACK
 # CONCURRENTLY find their way into the table.
@@ -110,3 +162,10 @@ permutation
 	check2
 	wakeup_before_lock
 	check1
+
+# Test that compressed TOAST values survive REPACK CONCURRENTLY.
+permutation
+	repack_compressed
+	compressed_update
+	wakeup_before_lock
+	verify_compressed