From 368688a89a80e34d340758a8854a4b4e1d16818d Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 15 Oct 2026 11:54:57 +0000
Subject: [PATCH v1 4/4] Use bulk-write strategy for TOAST during heap rewrites

Heap rewrites use a separate insertion path, so their TOAST writes do not benefit
from preceding commit XXXX.

When a rewrite first needs to store TOAST data, create a BAS_BULKWRITE strategy
and a BulkInsertState for the destination relation. The TOAST state then shares
that strategy. Continue using the existing BulkWriteState for the main relation.

Add a VACUUM FULL test that checks TOAST extensions are counted in the bulkwrite
I/O context.

Reported-by: Justin Pryzby <pryzby@telsasoft.com>
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by:
Discussion: https://postgr.es/m/
Discussion: https://postgr.es/m/20220622143841.GS22452%40telsasoft.com
---
 src/backend/access/heap/rewriteheap.c | 17 +++++++++++++-
 src/test/regress/expected/stats.out   | 33 +++++++++++++++++++++++++++
 src/test/regress/sql/stats.sql        | 20 ++++++++++++++++
 3 files changed, 69 insertions(+), 1 deletion(-)
  24.5% src/backend/access/heap/
  41.0% src/test/regress/expected/
  34.4% src/test/regress/sql/

diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index 90ddfd11908..438dedc6c51 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -133,6 +133,8 @@ typedef struct RewriteStateData
 	Relation	rs_old_rel;		/* source heap */
 	Relation	rs_new_rel;		/* destination heap */
 	BulkWriteState *rs_bulkstate;	/* writer for the destination */
+	BufferAccessStrategy rs_toast_strategy; /* ring for TOAST writes */
+	BulkInsertState rs_toast_bistate;	/* parent state for TOAST writes */
 	BulkWriteBuffer rs_buffer;	/* page currently being built */
 	BlockNumber rs_blockno;		/* block where page will go */
 	bool		rs_logical_rewrite; /* do we need to do logical rewriting */
@@ -320,6 +322,11 @@ end_heap_rewrite(RewriteState state)
 	}
 
 	smgr_bulk_finish(state->rs_bulkstate);
+	if (state->rs_toast_bistate != NULL)
+	{
+		FreeBulkInsertState(state->rs_toast_bistate);
+		FreeAccessStrategy(state->rs_toast_strategy);
+	}
 
 	logical_end_heap_rewrite(state);
 
@@ -627,8 +634,16 @@ raw_heap_insert(RewriteState state, HeapTuple tup)
 		 */
 		options |= HEAP_INSERT_NO_LOGICAL;
 
+		if (state->rs_toast_bistate == NULL)
+		{
+			state->rs_toast_strategy = GetAccessStrategy(BAS_BULKWRITE);
+			state->rs_toast_bistate = GetBulkInsertState(state->rs_new_rel,
+														 state->rs_toast_strategy);
+		}
+
 		heaptup = heap_toast_insert_or_update(state->rs_new_rel, tup, NULL,
-											  options, NULL);
+											  options,
+											  state->rs_toast_bistate);
 	}
 	else
 		heaptup = tup;
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index 1f041617aee..62308fe6c2b 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -1955,6 +1955,39 @@ SELECT :io_sum_bulkwrite_toast_multi_extends_after >
  t
 (1 row)
 
+-- Check that a TOAST bulk insertion state survives per-tuple context resets
+-- during a table rewrite.
+ALTER TABLE test_io_bulkwrite_toast
+  ADD COLUMN rewrite_id int GENERATED ALWAYS AS (id) STORED;
+SELECT count(*), bool_and(rewrite_id = id) AS rewrite_ok
+  FROM test_io_bulkwrite_toast;
+ count | rewrite_ok 
+-------+------------
+     9 | t
+(1 row)
+
+-- Test that TOAST writes performed during a heap rewrite use the
+-- BAS_BULKWRITE strategy.
+SELECT sum(extends) AS io_sum_rewrite_toast_extends_before
+  FROM pg_stat_get_backend_io(pg_backend_pid())
+  WHERE context = 'bulkwrite' \gset
+VACUUM FULL test_io_bulkwrite_toast;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(extends) AS io_sum_rewrite_toast_extends_after
+  FROM pg_stat_get_backend_io(pg_backend_pid())
+  WHERE context = 'bulkwrite' \gset
+SELECT :io_sum_rewrite_toast_extends_after >
+  :io_sum_rewrite_toast_extends_before;
+ ?column? 
+----------
+ t
+(1 row)
+
 DROP TABLE test_io_bulkwrite_toast;
 -- Test IO stats reset
 SELECT pg_stat_have_stats('io', 0, 0);
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 7c11c07b4ab..41702fc76c3 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -950,6 +950,26 @@ SELECT sum(extends) AS io_sum_bulkwrite_toast_multi_extends_after
   WHERE context = 'bulkwrite' \gset
 SELECT :io_sum_bulkwrite_toast_multi_extends_after >
   :io_sum_bulkwrite_toast_multi_extends_before;
+
+-- Check that a TOAST bulk insertion state survives per-tuple context resets
+-- during a table rewrite.
+ALTER TABLE test_io_bulkwrite_toast
+  ADD COLUMN rewrite_id int GENERATED ALWAYS AS (id) STORED;
+SELECT count(*), bool_and(rewrite_id = id) AS rewrite_ok
+  FROM test_io_bulkwrite_toast;
+
+-- Test that TOAST writes performed during a heap rewrite use the
+-- BAS_BULKWRITE strategy.
+SELECT sum(extends) AS io_sum_rewrite_toast_extends_before
+  FROM pg_stat_get_backend_io(pg_backend_pid())
+  WHERE context = 'bulkwrite' \gset
+VACUUM FULL test_io_bulkwrite_toast;
+SELECT pg_stat_force_next_flush();
+SELECT sum(extends) AS io_sum_rewrite_toast_extends_after
+  FROM pg_stat_get_backend_io(pg_backend_pid())
+  WHERE context = 'bulkwrite' \gset
+SELECT :io_sum_rewrite_toast_extends_after >
+  :io_sum_rewrite_toast_extends_before;
 DROP TABLE test_io_bulkwrite_toast;
 
 -- Test IO stats reset
-- 
2.34.1

