From ffd8366a6ad0d2cf65ddfc407a09de4ad4415bac Mon Sep 17 00:00:00 2001
From: Konstantin Knizhnik <knizhnik@garret.ru>
Date: Thu, 12 Nov 2026 14:46:55 +0200
Subject: [PATCH v8] Fix "missing chunk" errors during heap rewrites and index
 builds

A transaction in another database can hold back the xmin of the backend
running CLUSTER, VACUUM FULL, REPACK or a non-concurrent CREATE INDEX,
as GetSnapshotData() does not filter by database.  ComputeXidHorizons()
does, so a concurrent VACUUM or on-access pruning can remove the TOAST
chunks of a tuple deleted or updated by a committed transaction while
HeapTupleSatisfiesVacuum() in the rewrite still classifies the tuple as
HEAPTUPLE_RECENTLY_DEAD.  Fetching its TOAST data then fails with
"missing chunk number 0 for toast value ...".

Such a tuple is invisible to every snapshot that can still look at the
relation, so it is safe to skip it.  This commit adds a TOAST_MISSING_OK
flag that makes the TOAST fetch paths (table_relation_fetch_toast_slice,
toast_fetch_datum, detoast_external_attr_extended, toast_tuple_init and
heap_toast_insert_or_update) report missing chunks to the caller instead
of raising an error, and uses it only for RECENTLY_DEAD tuples:

- In a heap rewrite, a RECENTLY_DEAD tuple whose TOAST data is gone is
  not written and is counted as removed.  This covers both directly
  written tuples and update-chain members stashed in rewriteheap.c,
  whose result is reported by end_heap_rewrite().
- When the rewrite sorts the tuples, the TOAST data of RECENTLY_DEAD
  tuples is inlined before the sort, so that missing chunks are detected
  while the tuple's visibility is still known.
- In a non-concurrent index build, the varlena attributes referenced by
  the index (columns, expressions including whole-row references, and
  predicate) are inlined for tuples that are not alive, and the tuple is
  skipped from the index if its TOAST data is gone.

REPACK CONCURRENTLY and CREATE INDEX CONCURRENTLY use MVCC snapshots and
are not affected.

An isolation test is added to contrib/dblink, which is used to hold an
XID in another database.

Reported-by: Alexander Lakhin <exclusion@gmail.com>
Reported-by: Ekaterina Testova <e.testova@postgrespro.ru>
Author: Michael Paquier <michael@paquier.xyz>
Co-authored-by: Matthias van de Meent <boekewurm+postgres@gmail.com>
Co-authored-by: Rui Zhao <zhaorui126@gmail.com>
Discussion: https://postgr.es/m/18351-f6e06364b3a2e669@postgresql.org
Discussion: https://postgr.es/m/19519-fe02d8ff679d834d@postgresql.org
Discussion: https://postgr.es/m/1779078055.630771314@fmail2.qdit
---
 contrib/dblink/.gitignore                     |   2 +
 contrib/dblink/Makefile                       |   2 +
 .../dblink/expected/rewrite_stale_xmin.out    | 752 ++++++++++++++++++
 contrib/dblink/meson.build                    |   5 +
 contrib/dblink/specs/rewrite_stale_xmin.spec  | 249 ++++++
 src/backend/access/common/detoast.c           |  43 +-
 src/backend/access/heap/heapam.c              |   4 +-
 src/backend/access/heap/heapam_handler.c      | 240 +++++-
 src/backend/access/heap/heaptoast.c           |  48 +-
 src/backend/access/heap/rewriteheap.c         |  71 +-
 src/backend/access/table/toast_helper.c       |  41 +-
 src/include/access/detoast.h                  |   9 +
 src/include/access/heaptoast.h                |  16 +-
 src/include/access/rewriteheap.h              |   6 +-
 src/include/access/tableam.h                  |  26 +-
 src/include/access/toast_helper.h             |  11 +-
 16 files changed, 1467 insertions(+), 58 deletions(-)
 create mode 100644 contrib/dblink/expected/rewrite_stale_xmin.out
 create mode 100644 contrib/dblink/specs/rewrite_stale_xmin.spec

diff --git a/contrib/dblink/.gitignore b/contrib/dblink/.gitignore
index 5dcb3ff9723..33bc8d8ffeb 100644
--- a/contrib/dblink/.gitignore
+++ b/contrib/dblink/.gitignore
@@ -2,3 +2,5 @@
 /log/
 /results/
 /tmp_check/
+/output_iso/
+/tmp_check_iso/
diff --git a/contrib/dblink/Makefile b/contrib/dblink/Makefile
index fde0b49ddbb..a0ba4128081 100644
--- a/contrib/dblink/Makefile
+++ b/contrib/dblink/Makefile
@@ -13,6 +13,8 @@ PGFILEDESC = "dblink - connect to other PostgreSQL databases"
 
 REGRESS = dblink
 REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
+ISOLATION = rewrite_stale_xmin
+ISOLATION_OPTS = --load-extension=dblink
 TAP_TESTS = 1
 
 ifdef USE_PGXS
diff --git a/contrib/dblink/expected/rewrite_stale_xmin.out b/contrib/dblink/expected/rewrite_stale_xmin.out
new file mode 100644
index 00000000000..168c6ea3f4b
--- /dev/null
+++ b/contrib/dblink/expected/rewrite_stale_xmin.out
@@ -0,0 +1,752 @@
+Parsed test spec with 3 sessions
+
+starting permutation: s1_hold_xmin s2_begin s3_delete s3_vacuum_main_only s2_commit s3_vacuum_toast s3_cluster s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_delete: 
+    DELETE FROM rewrite_test WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_cluster: 
+    CLUSTER rewrite_test USING rewrite_test_pkey;
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
+
+starting permutation: s1_hold_xmin s2_begin s3_delete s3_vacuum_main_only s2_commit s3_vacuum_toast s3_cluster_sort s3_check_rows s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_delete: 
+    DELETE FROM rewrite_test WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_cluster_sort: 
+    SET enable_indexscan = off;
+    CLUSTER rewrite_test USING rewrite_test_pkey;
+    RESET enable_indexscan;
+
+step s3_check_rows: 
+    SELECT id, length(data), left(data, 1) FROM rewrite_test ORDER BY id;
+    SELECT reltuples FROM pg_class WHERE oid = 'rewrite_test'::regclass;
+
+id|length|left
+--+------+----
+ 2|  2500|y   
+(1 row)
+
+reltuples
+---------
+        1
+(1 row)
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
+
+starting permutation: s1_hold_xmin s2_begin s3_delete s3_vacuum_main_only s2_commit s3_vacuum_toast s3_vacuum_full s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_delete: 
+    DELETE FROM rewrite_test WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_vacuum_full: 
+    VACUUM FULL rewrite_test;
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
+
+starting permutation: s1_hold_xmin s2_begin s3_delete s3_vacuum_main_only s2_commit s3_vacuum_toast s3_repack s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_delete: 
+    DELETE FROM rewrite_test WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_repack: 
+    REPACK rewrite_test;
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
+
+starting permutation: s1_hold_xmin s2_begin s3_delete s3_vacuum_main_only s2_commit s3_vacuum_toast s3_create_index s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_delete: 
+    DELETE FROM rewrite_test WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_create_index: 
+    CREATE INDEX rewrite_test_data_idx ON rewrite_test (data);
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
+
+starting permutation: s1_hold_xmin s2_begin s3_delete s3_vacuum_main_only s2_commit s3_vacuum_toast s3_create_index_wholerow s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_delete: 
+    DELETE FROM rewrite_test WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_create_index_wholerow: 
+    CREATE INDEX rewrite_test_row_idx ON rewrite_test ((rewrite_test IS NOT NULL));
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
+
+starting permutation: s1_hold_xmin s2_begin s3_update s3_vacuum_main_only s2_commit s3_vacuum_toast s3_vacuum_full s3_check_rows s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_update: 
+    UPDATE rewrite_test SET data = repeat('z', 2500) WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_vacuum_full: 
+    VACUUM FULL rewrite_test;
+
+step s3_check_rows: 
+    SELECT id, length(data), left(data, 1) FROM rewrite_test ORDER BY id;
+    SELECT reltuples FROM pg_class WHERE oid = 'rewrite_test'::regclass;
+
+id|length|left
+--+------+----
+ 1|  2500|z   
+ 2|  2500|y   
+(2 rows)
+
+reltuples
+---------
+        2
+(1 row)
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
+
+starting permutation: s1_hold_xmin s2_begin s3_update s3_vacuum_main_only s2_commit s3_vacuum_toast s3_repack s3_check_rows s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_update: 
+    UPDATE rewrite_test SET data = repeat('z', 2500) WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_repack: 
+    REPACK rewrite_test;
+
+step s3_check_rows: 
+    SELECT id, length(data), left(data, 1) FROM rewrite_test ORDER BY id;
+    SELECT reltuples FROM pg_class WHERE oid = 'rewrite_test'::regclass;
+
+id|length|left
+--+------+----
+ 1|  2500|z   
+ 2|  2500|y   
+(2 rows)
+
+reltuples
+---------
+        2
+(1 row)
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
+
+starting permutation: s1_hold_xmin s2_begin s3_update s3_update_again s3_vacuum_main_only s2_commit s3_vacuum_toast s3_vacuum_full s3_check_rows s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_update: 
+    UPDATE rewrite_test SET data = repeat('z', 2500) WHERE id = 1;
+
+step s3_update_again: 
+    UPDATE rewrite_test SET data = repeat('w', 2500) WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_vacuum_full: 
+    VACUUM FULL rewrite_test;
+
+step s3_check_rows: 
+    SELECT id, length(data), left(data, 1) FROM rewrite_test ORDER BY id;
+    SELECT reltuples FROM pg_class WHERE oid = 'rewrite_test'::regclass;
+
+id|length|left
+--+------+----
+ 1|  2500|w   
+ 2|  2500|y   
+(2 rows)
+
+reltuples
+---------
+        2
+(1 row)
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
+
+starting permutation: s1_hold_xmin s2_begin s3_update s3_update_again s3_vacuum_main_only s2_commit s3_vacuum_toast s3_repack s3_check_rows s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_update: 
+    UPDATE rewrite_test SET data = repeat('z', 2500) WHERE id = 1;
+
+step s3_update_again: 
+    UPDATE rewrite_test SET data = repeat('w', 2500) WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_repack: 
+    REPACK rewrite_test;
+
+step s3_check_rows: 
+    SELECT id, length(data), left(data, 1) FROM rewrite_test ORDER BY id;
+    SELECT reltuples FROM pg_class WHERE oid = 'rewrite_test'::regclass;
+
+id|length|left
+--+------+----
+ 1|  2500|w   
+ 2|  2500|y   
+(2 rows)
+
+reltuples
+---------
+        2
+(1 row)
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
+
+starting permutation: s1_hold_xmin s2_begin s3_update s3_update_again s3_vacuum_main_only s2_commit s3_vacuum_toast s3_cluster_sort s3_check_rows s1_release
+step s1_hold_xmin: 
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+
+dblink_connect
+--------------
+OK            
+(1 row)
+
+dblink_exec
+-----------
+BEGIN      
+(1 row)
+
+dblink_exec 
+------------
+CREATE TABLE
+(1 row)
+
+step s2_begin: 
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+
+?column?
+--------
+       1
+(1 row)
+
+step s3_update: 
+    UPDATE rewrite_test SET data = repeat('z', 2500) WHERE id = 1;
+
+step s3_update_again: 
+    UPDATE rewrite_test SET data = repeat('w', 2500) WHERE id = 1;
+
+step s3_vacuum_main_only: 
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+
+step s2_commit: COMMIT;
+step s3_vacuum_toast: 
+    VACUUM pg_toast.rewrite_test_toast;
+
+step s3_cluster_sort: 
+    SET enable_indexscan = off;
+    CLUSTER rewrite_test USING rewrite_test_pkey;
+    RESET enable_indexscan;
+
+step s3_check_rows: 
+    SELECT id, length(data), left(data, 1) FROM rewrite_test ORDER BY id;
+    SELECT reltuples FROM pg_class WHERE oid = 'rewrite_test'::regclass;
+
+id|length|left
+--+------+----
+ 1|  2500|w   
+ 2|  2500|y   
+(2 rows)
+
+reltuples
+---------
+        2
+(1 row)
+
+step s1_release: 
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+
+dblink_exec
+-----------
+COMMIT     
+(1 row)
+
+dblink_disconnect
+-----------------
+OK               
+(1 row)
+
diff --git a/contrib/dblink/meson.build b/contrib/dblink/meson.build
index e2489f41229..86fbd372b88 100644
--- a/contrib/dblink/meson.build
+++ b/contrib/dblink/meson.build
@@ -36,6 +36,11 @@ tests += {
     ],
     'regress_args': ['--dlpath', meson.project_build_root() / 'src/test/regress'],
   },
+  'isolation': {
+    'specs': [
+      'rewrite_stale_xmin',
+    ],
+  },
   'tap': {
     'tests': [
       't/001_auth_scram.pl',
diff --git a/contrib/dblink/specs/rewrite_stale_xmin.spec b/contrib/dblink/specs/rewrite_stale_xmin.spec
new file mode 100644
index 00000000000..63ba262f359
--- /dev/null
+++ b/contrib/dblink/specs/rewrite_stale_xmin.spec
@@ -0,0 +1,249 @@
+# Test that heap rewrites and index builds cope with RECENTLY_DEAD tuples
+# whose TOAST data has already been removed.
+#
+# A transaction in another database holds back the xmin of the rewriting
+# backend, but not the removal horizon of this database.  The main table and
+# its TOAST table are vacuumed separately, with a same-database snapshot that
+# keeps the dead tuple released in between, so the tuple survives while its
+# TOAST chunks are removed.
+
+setup
+{
+    CREATE DATABASE regress_other_db;
+}
+
+setup
+{
+    CREATE EXTENSION IF NOT EXISTS dblink;
+
+    CREATE TABLE rewrite_test (id int PRIMARY KEY, data text)
+        WITH (autovacuum_enabled = off);
+    ALTER TABLE rewrite_test ALTER COLUMN data SET STORAGE EXTERNAL;
+    INSERT INTO rewrite_test VALUES (1, repeat('x', 2500));
+    INSERT INTO rewrite_test VALUES (2, repeat('y', 2500));
+
+    -- Rename the toast table to a known name so we can VACUUM it directly.
+    SET allow_system_table_mods TO true;
+    DO $$DECLARE r record;
+      BEGIN
+        SELECT INTO r reltoastrelid::regclass::text AS table_name
+          FROM pg_class WHERE oid = 'rewrite_test'::regclass;
+        EXECUTE 'ALTER TABLE ' || r.table_name || ' RENAME TO rewrite_test_toast;';
+      END$$;
+    RESET allow_system_table_mods;
+}
+
+teardown
+{
+    DROP DATABASE regress_other_db;
+}
+
+# Session s1: use dblink to hold a transaction in another database.
+# This must start before the DELETE so its XID is older, ensuring
+# GetSnapshotData() holds back any rewrites.
+session s1
+step s1_hold_xmin
+{
+    SELECT dblink_connect('holder',
+        'dbname=regress_other_db port=' || current_setting('port'));
+    SELECT dblink_exec('holder', 'BEGIN');
+    SELECT dblink_exec('holder', 'CREATE TEMP TABLE xid_holder(x int)');
+}
+step s1_release
+{
+    SELECT dblink_exec('holder', 'COMMIT');
+    SELECT dblink_disconnect('holder');
+}
+
+# Session s2: holds xmin in the SAME database temporarily, so the
+# main-table vacuum preserves the dead tuple as RECENTLY_DEAD.
+session s2
+step s2_begin
+{
+    BEGIN ISOLATION LEVEL REPEATABLE READ;
+    SELECT 1;
+}
+step s2_commit { COMMIT; }
+
+# Session s3: performs DML, split vacuum, and the rewrite.
+session s3
+step s3_delete
+{
+    DELETE FROM rewrite_test WHERE id = 1;
+}
+step s3_update
+{
+    UPDATE rewrite_test SET data = repeat('z', 2500) WHERE id = 1;
+}
+step s3_update_again
+{
+    UPDATE rewrite_test SET data = repeat('w', 2500) WHERE id = 1;
+}
+step s3_check_rows
+{
+    SELECT id, length(data), left(data, 1) FROM rewrite_test ORDER BY id;
+    SELECT reltuples FROM pg_class WHERE oid = 'rewrite_test'::regclass;
+}
+step s3_vacuum_main_only
+{
+    VACUUM (PROCESS_TOAST false) rewrite_test;
+}
+step s3_vacuum_toast
+{
+    VACUUM pg_toast.rewrite_test_toast;
+}
+step s3_cluster
+{
+    CLUSTER rewrite_test USING rewrite_test_pkey;
+}
+# Use the seqscan-and-sort strategy, which fetches TOAST data before sorting
+step s3_cluster_sort
+{
+    SET enable_indexscan = off;
+    CLUSTER rewrite_test USING rewrite_test_pkey;
+    RESET enable_indexscan;
+}
+step s3_vacuum_full
+{
+    VACUUM FULL rewrite_test;
+}
+step s3_repack
+{
+    REPACK rewrite_test;
+}
+step s3_create_index
+{
+    CREATE INDEX rewrite_test_data_idx ON rewrite_test (data);
+}
+step s3_create_index_wholerow
+{
+    CREATE INDEX rewrite_test_row_idx ON rewrite_test ((rewrite_test IS NOT NULL));
+}
+
+teardown { DROP TABLE IF EXISTS rewrite_test; }
+
+# CLUSTER
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_delete
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_cluster
+    s1_release
+
+# CLUSTER, sorting
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_delete
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_cluster_sort
+    s3_check_rows
+    s1_release
+
+# VACUUM FULL
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_delete
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_vacuum_full
+    s1_release
+
+# REPACK
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_delete
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_repack
+    s1_release
+
+# CREATE INDEX
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_delete
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_create_index
+    s1_release
+
+# CREATE INDEX on a whole-row expression
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_delete
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_create_index_wholerow
+    s1_release
+
+# Preserve the live successor when a stashed prior version loses its TOAST.
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_update
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_vacuum_full
+    s3_check_rows
+    s1_release
+
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_update
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_repack
+    s3_check_rows
+    s1_release
+
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_update
+    s3_update_again
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_vacuum_full
+    s3_check_rows
+    s1_release
+
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_update
+    s3_update_again
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_repack
+    s3_check_rows
+    s1_release
+
+permutation
+    s1_hold_xmin
+    s2_begin
+    s3_update
+    s3_update_again
+    s3_vacuum_main_only
+    s2_commit
+    s3_vacuum_toast
+    s3_cluster_sort
+    s3_check_rows
+    s1_release
diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c
index 8223cc219c1..b27b6bb9780 100644
--- a/src/backend/access/common/detoast.c
+++ b/src/backend/access/common/detoast.c
@@ -17,6 +17,7 @@
 #include "access/table.h"
 #include "access/tableam.h"
 #include "access/toast_compression.h"
+#include "access/toast_helper.h"
 #include "access/toast_internals.h"
 #include "common/int.h"
 #include "common/pg_lzcompress.h"
@@ -24,7 +25,7 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 
-static varlena *toast_fetch_datum(varlena *attr);
+static varlena *toast_fetch_datum(varlena *attr, uint32 flags);
 static varlena *toast_fetch_datum_slice(varlena *attr,
 										int32 sliceoffset,
 										int32 slicelength);
@@ -61,7 +62,7 @@ detoast_external_attr(varlena *attr)
 		/*
 		 * This is an external stored plain value
 		 */
-		result = toast_fetch_datum(attr);
+		result = toast_fetch_datum(attr, 0);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -112,6 +113,24 @@ detoast_external_attr(varlena *attr)
 }
 
 
+/* ----------
+ * detoast_external_attr_extended -
+ *
+ *	Like detoast_external_attr, but returns NULL instead of raising an
+ *	error if the chunks of an on-disk value are missing.  Only meant for
+ *	RECENTLY_DEAD tuples, see TOAST_MISSING_OK.
+ * ----------
+ */
+varlena *
+detoast_external_attr_extended(varlena *attr)
+{
+	if (VARATT_IS_EXTERNAL_ONDISK(attr))
+		return toast_fetch_datum(attr, TOAST_MISSING_OK);
+	else
+		return detoast_external_attr(attr);
+}
+
+
 /* ----------
  * detoast_attr -
  *
@@ -130,7 +149,7 @@ detoast_attr(varlena *attr)
 		/*
 		 * This is an externally stored datum --- fetch it back from there
 		 */
-		attr = toast_fetch_datum(attr);
+		attr = toast_fetch_datum(attr, 0);
 		/* If it's compressed, decompress it */
 		if (VARATT_IS_COMPRESSED(attr))
 		{
@@ -277,7 +296,7 @@ detoast_attr_slice(varlena *attr,
 			preslice = toast_fetch_datum_slice(attr, 0, max_size);
 		}
 		else
-			preslice = toast_fetch_datum(attr);
+			preslice = toast_fetch_datum(attr, 0);
 	}
 	else if (VARATT_IS_EXTERNAL_INDIRECT(attr))
 	{
@@ -352,10 +371,12 @@ detoast_attr_slice(varlena *attr,
  *
  *	Reconstruct an in memory Datum from the chunks saved
  *	in the toast relation
+ *
+ *	If flags includes TOAST_MISSING_OK, returns NULL when chunks are missing.
  * ----------
  */
 static varlena *
-toast_fetch_datum(varlena *attr)
+toast_fetch_datum(varlena *attr, uint32 flags)
 {
 	Relation	toastrel;
 	varlena    *result;
@@ -389,8 +410,14 @@ toast_fetch_datum(varlena *attr)
 	toastrel = table_open(toastrelid, AccessShareLock);
 
 	/* Fetch all chunks */
-	table_relation_fetch_toast_slice(toastrel, valueid,
-									 attrsize, 0, attrsize, result);
+	if (!table_relation_fetch_toast_slice(toastrel, valueid,
+										  attrsize, 0, attrsize, result,
+										  flags))
+	{
+		pfree(result);
+		table_close(toastrel, AccessShareLock);
+		return NULL;
+	}
 
 	/* Close toast table */
 	table_close(toastrel, AccessShareLock);
@@ -475,7 +502,7 @@ toast_fetch_datum_slice(varlena *attr, int32 sliceoffset,
 	/* Fetch all chunks */
 	table_relation_fetch_toast_slice(toastrel, valueid,
 									 attrsize, sliceoffset, slicelength,
-									 result);
+									 result, 0);
 
 	/* Close toast table */
 	table_close(toastrel, AccessShareLock);
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 3bb33699349..0b3c4ae85ed 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -2266,7 +2266,7 @@ heap_prepare_insert(Relation relation, HeapTuple tup, TransactionId xid,
 		return tup;
 	}
 	else if (HeapTupleHasExternal(tup) || tup->t_len > TOAST_TUPLE_THRESHOLD)
-		return heap_toast_insert_or_update(relation, tup, NULL, options);
+		return heap_toast_insert_or_update(relation, tup, NULL, options, 0);
 	else
 		return tup;
 }
@@ -3983,7 +3983,7 @@ l2:
 			 * tuples too. We never skip the FSM here.
 			 */
 			heaptup = heap_toast_insert_or_update(relation, newtup, &oldtup,
-												  walLogical ? 0 : HEAP_INSERT_NO_LOGICAL);
+												  walLogical ? 0 : HEAP_INSERT_NO_LOGICAL, 0);
 			newtupsize = MAXALIGN(heaptup->t_len);
 		}
 		else
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 05a81e4557a..bc10b4fe891 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -19,6 +19,7 @@
  */
 #include "postgres.h"
 
+#include "access/detoast.h"
 #include "access/genam.h"
 #include "access/heapam.h"
 #include "access/heaptoast.h"
@@ -26,6 +27,7 @@
 #include "access/rewriteheap.h"
 #include "access/syncscan.h"
 #include "access/tableam.h"
+#include "access/toast_helper.h"
 #include "access/tsmapi.h"
 #include "access/visibilitymap.h"
 #include "access/xact.h"
@@ -36,6 +38,7 @@
 #include "commands/progress.h"
 #include "executor/executor.h"
 #include "miscadmin.h"
+#include "optimizer/optimizer.h"
 #include "pgstat.h"
 #include "storage/bufmgr.h"
 #include "storage/bufpage.h"
@@ -48,9 +51,14 @@
 #include "utils/rel.h"
 #include "utils/tuplesort.h"
 
-static void reform_and_rewrite_tuple(HeapTuple tuple,
+static bool reform_and_rewrite_tuple(HeapTuple tuple,
 									 Relation OldHeap, Relation NewHeap,
-									 Datum *values, bool *isnull, RewriteState rwstate);
+									 Datum *values, bool *isnull, RewriteState rwstate,
+									 bool recently_dead);
+static HeapTuple heap_inline_recently_dead_toast(HeapTuple tuple,
+												 TupleDesc tupdesc,
+												 const Bitmapset *attrs,
+												 Datum *values, bool *isnull);
 static void heap_insert_for_repack(HeapTuple tuple, Relation OldHeap,
 								   Relation NewHeap, Datum *values, bool *isnull,
 								   BulkInsertState bistate);
@@ -703,6 +711,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 		HeapTuple	tuple;
 		Buffer		buf;
 		bool		isdead;
+		bool		recently_dead = false;
 
 		CHECK_FOR_INTERRUPTS();
 
@@ -789,6 +798,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 					break;
 				case HEAPTUPLE_RECENTLY_DEAD:
 					*tups_recently_dead += 1;
+					recently_dead = true;
 					pg_fallthrough;
 				case HEAPTUPLE_LIVE:
 					/* Live or recently dead, must copy it */
@@ -822,6 +832,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 							 RelationGetRelationName(OldHeap));
 					/* treat as recently dead */
 					*tups_recently_dead += 1;
+					recently_dead = true;
 					isdead = false;
 					break;
 				default:
@@ -850,7 +861,30 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 		*num_tuples += 1;
 		if (tuplesort != NULL)
 		{
-			tuplesort_putheaptuple(tuplesort, tuple);
+			HeapTuple	sorttup = tuple;
+
+			/*
+			 * The tuples are written out only after the sort, by which time
+			 * the TOAST data of a recently-dead tuple may be gone, so fetch
+			 * it now.  If it is already gone, the tuple is dead to everyone
+			 * who could still read it, so skip it.
+			 */
+			if (recently_dead && HeapTupleHasExternal(tuple))
+			{
+				sorttup = heap_inline_recently_dead_toast(tuple, oldTupDesc,
+														  NULL, values, isnull);
+				if (sorttup == NULL)
+				{
+					*tups_vacuumed += 1;
+					*num_tuples -= 1;
+					*tups_recently_dead -= 1;
+					continue;
+				}
+			}
+
+			tuplesort_putheaptuple(tuplesort, sorttup);
+			if (sorttup != tuple)
+				heap_freetuple(sorttup);
 
 			/*
 			 * In scan-and-sort mode, report increase in number of tuples
@@ -868,8 +902,19 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 			int64		ct_val[2];
 
 			if (!concurrent)
-				reform_and_rewrite_tuple(tuple, OldHeap, NewHeap,
-										 values, isnull, rwstate);
+			{
+				if (!reform_and_rewrite_tuple(tuple, OldHeap, NewHeap,
+											  values, isnull, rwstate,
+											  recently_dead))
+				{
+					/* TOAST data of a recently-dead tuple is gone */
+					Assert(recently_dead);
+					*tups_vacuumed += 1;
+					*num_tuples -= 1;
+					*tups_recently_dead -= 1;
+					continue;
+				}
+			}
 			else
 				heap_insert_for_repack(tuple, OldHeap, NewHeap,
 									   values, isnull, bistate);
@@ -920,11 +965,12 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 				break;
 
 			n_tuples += 1;
+			/* TOAST data of recently-dead tuples was inlined before the sort */
 			if (!concurrent)
-				reform_and_rewrite_tuple(tuple,
-										 OldHeap, NewHeap,
-										 values, isnull,
-										 rwstate);
+				(void) reform_and_rewrite_tuple(tuple,
+												OldHeap, NewHeap,
+												values, isnull,
+												rwstate, false);
 			else
 				heap_insert_for_repack(tuple, OldHeap, NewHeap,
 									   values, isnull, bistate);
@@ -939,7 +985,14 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 
 	/* Write out any remaining tuples, and fsync if needed */
 	if (rwstate)
-		end_heap_rewrite(rwstate);
+	{
+		double		tuples_dropped = end_heap_rewrite(rwstate);
+
+		/* Stashed tuples with missing TOAST were scanned as recently dead. */
+		*tups_vacuumed += tuples_dropped;
+		*num_tuples -= tuples_dropped;
+		*tups_recently_dead -= tuples_dropped;
+	}
 	if (bistate)
 		FreeBulkInsertState(bistate);
 
@@ -1156,6 +1209,9 @@ heapam_index_build_range_scan(Relation heapRelation,
 	BlockNumber previous_blkno = InvalidBlockNumber;
 	BlockNumber root_blkno = InvalidBlockNumber;
 	OffsetNumber root_offsets[MaxHeapTuplesPerPage];
+	Bitmapset  *detoast_index_attrs = NULL;
+	Datum	   *inline_values = NULL;
+	bool	   *inline_isnull = NULL;
 
 	/*
 	 * sanity checks
@@ -1252,6 +1308,50 @@ heapam_index_build_range_scan(Relation heapRelation,
 		   !TransactionIdIsValid(OldestXmin));
 	Assert(snapshot == SnapshotAny || !anyvisible);
 
+	/*
+	 * Collect the varlena columns that the index columns, expressions and
+	 * predicate reference, so that their TOAST data can be fetched for
+	 * RECENTLY_DEAD tuples; see below.  A whole-row reference needs all of
+	 * them.  The set contains plain attribute numbers.
+	 */
+	{
+		TupleDesc	desc = RelationGetDescr(heapRelation);
+		Bitmapset  *refattrs = NULL;
+		bool		wholerow;
+
+		for (int i = 0; i < indexInfo->ii_NumIndexAttrs; i++)
+		{
+			AttrNumber	attnum = indexInfo->ii_IndexAttrNumbers[i];
+
+			if (attnum != 0)
+				refattrs = bms_add_member(refattrs,
+										  attnum - FirstLowInvalidHeapAttributeNumber);
+		}
+		pull_varattnos((Node *) indexInfo->ii_Expressions, 1, &refattrs);
+		pull_varattnos((Node *) indexInfo->ii_Predicate, 1, &refattrs);
+
+		wholerow = bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber,
+								 refattrs);
+
+		for (AttrNumber attnum = 1; attnum <= desc->natts; attnum++)
+		{
+			if (TupleDescCompactAttr(desc, attnum - 1)->attlen != -1)
+				continue;
+			if (wholerow ||
+				bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber,
+							  refattrs))
+				detoast_index_attrs = bms_add_member(detoast_index_attrs,
+													 attnum);
+		}
+		bms_free(refattrs);
+
+		if (detoast_index_attrs != NULL)
+		{
+			inline_values = palloc_array(Datum, desc->natts);
+			inline_isnull = palloc_array(bool, desc->natts);
+		}
+	}
+
 	/* Publish number of blocks to scan */
 	if (progress)
 	{
@@ -1590,6 +1690,29 @@ heapam_index_build_range_scan(Relation heapRelation,
 		/* Set up for predicate or expression evaluation */
 		ExecStoreBufferHeapTuple(heapTuple, slot, hscan->rs_cbuf);
 
+		/*
+		 * The TOAST data of a RECENTLY_DEAD tuple can be removed while we
+		 * look at it; see heap_inline_recently_dead_toast().  Fetch what the
+		 * index needs up front, and skip the tuple if it is already gone.
+		 */
+		if (!tupleIsAlive && detoast_index_attrs != NULL &&
+			HeapTupleHasExternal(heapTuple))
+		{
+			HeapTuple	inlined;
+
+			inlined = heap_inline_recently_dead_toast(heapTuple,
+													  RelationGetDescr(heapRelation),
+													  detoast_index_attrs,
+													  inline_values, inline_isnull);
+			if (inlined == NULL)
+				continue;
+			if (inlined != heapTuple)
+			{
+				ExecForceStoreHeapTuple(inlined, slot, true);
+				slot->tts_tableOid = RelationGetRelid(heapRelation);
+			}
+		}
+
 		/*
 		 * In a partial index, discard tuples that don't satisfy the
 		 * predicate.
@@ -1698,6 +1821,12 @@ heapam_index_build_range_scan(Relation heapRelation,
 	indexInfo->ii_ExpressionsState = NIL;
 	indexInfo->ii_PredicateState = NULL;
 
+	bms_free(detoast_index_attrs);
+	if (inline_values)
+		pfree(inline_values);
+	if (inline_isnull)
+		pfree(inline_isnull);
+
 	return reltuples;
 }
 
@@ -2338,20 +2467,105 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate,
  * SET WITHOUT OIDS.
  *
  * So, we must reconstruct the tuple from component Datums.
+ *
+ * recently_dead must be set if the tuple is RECENTLY_DEAD.  Returns false if
+ * the tuple was skipped because its TOAST data is gone; see
+ * rewrite_heap_tuple().
  */
-static void
+static bool
 reform_and_rewrite_tuple(HeapTuple tuple,
 						 Relation OldHeap, Relation NewHeap,
-						 Datum *values, bool *isnull, RewriteState rwstate)
+						 Datum *values, bool *isnull, RewriteState rwstate,
+						 bool recently_dead)
 {
 	HeapTuple	newtuple;
+	bool		written;
 
 	newtuple = reform_tuple(tuple, OldHeap, NewHeap, values, isnull);
 
 	/* The heap rewrite module does the rest */
-	rewrite_heap_tuple(rwstate, tuple, newtuple);
+	written = rewrite_heap_tuple(rwstate, tuple, newtuple, recently_dead);
 
 	heap_freetuple(newtuple);
+	return written;
+}
+
+/*
+ * Fetch the out-of-line TOAST values of a RECENTLY_DEAD tuple, and return a
+ * copy of the tuple with those values stored inline, keeping the original
+ * tuple's identity and visibility information.
+ *
+ * Other backends may already consider a RECENTLY_DEAD tuple fully dead, so
+ * its TOAST data can be removed by VACUUM or pruning of the TOAST relation at
+ * any time.  If any value is already gone, return NULL: nobody can see the
+ * tuple anymore, and the caller should treat it as dead.
+ *
+ * attrs is the set of attribute numbers to fetch, or NULL for all of them.
+ * Returns the tuple itself if there is nothing to fetch.  values and isnull
+ * are caller-provided workspace of tupdesc->natts entries.
+ */
+static HeapTuple
+heap_inline_recently_dead_toast(HeapTuple tuple, TupleDesc tupdesc,
+								const Bitmapset *attrs,
+								Datum *values, bool *isnull)
+{
+	Bitmapset  *fetched = NULL;
+	HeapTuple	result = NULL;
+	int			i;
+
+	heap_deform_tuple(tuple, tupdesc, values, isnull);
+
+	for (i = 0; i < tupdesc->natts; i++)
+	{
+		CompactAttribute *att = TupleDescCompactAttr(tupdesc, i);
+		varlena    *value;
+
+		if (att->attlen != -1 || isnull[i])
+			continue;
+		if (attrs != NULL && !bms_is_member(i + 1, attrs))
+			continue;
+
+		/* reform_tuple() throws away dropped columns anyway */
+		if (att->attisdropped)
+		{
+			isnull[i] = true;
+			continue;
+		}
+
+		value = (varlena *) DatumGetPointer(values[i]);
+		if (!VARATT_IS_EXTERNAL_ONDISK(value))
+			continue;
+
+		value = detoast_external_attr_extended(value);
+		if (value == NULL)
+			goto cleanup;
+
+		values[i] = PointerGetDatum(value);
+		fetched = bms_add_member(fetched, i);
+	}
+
+	if (fetched == NULL)
+		return tuple;
+
+	result = heap_form_tuple(tupdesc, values, isnull);
+
+	result->t_data->t_choice = tuple->t_data->t_choice;
+	result->t_data->t_ctid = tuple->t_data->t_ctid;
+	result->t_data->t_infomask2 &= ~HEAP2_XACT_MASK;
+	result->t_data->t_infomask2 |=
+		tuple->t_data->t_infomask2 & HEAP2_XACT_MASK;
+	result->t_data->t_infomask &= ~HEAP_XACT_MASK;
+	result->t_data->t_infomask |= tuple->t_data->t_infomask & HEAP_XACT_MASK;
+	result->t_self = tuple->t_self;
+	result->t_tableOid = tuple->t_tableOid;
+
+cleanup:
+	i = -1;
+	while ((i = bms_next_member(fetched, i)) >= 0)
+		pfree(DatumGetPointer(values[i]));
+	bms_free(fetched);
+
+	return result;
 }
 
 /*
diff --git a/src/backend/access/heap/heaptoast.c b/src/backend/access/heap/heaptoast.c
index e329662b683..1097e076bb9 100644
--- a/src/backend/access/heap/heaptoast.c
+++ b/src/backend/access/heap/heaptoast.c
@@ -84,9 +84,11 @@ heap_toast_delete(Relation rel, HeapTuple oldtup, bool is_speculative)
  *	newtup: the candidate new tuple to be inserted
  *	oldtup: the old row version for UPDATE, or NULL for INSERT
  *	options: options to be passed to heap_insert() for toast rows
+ *	flags: TOAST fetch flags, see toast_helper.h
  * Result:
  *	either newtup if no toasting is needed, or a palloc'd modified tuple
- *	that is what should actually get stored
+ *	that is what should actually get stored, or NULL if flags includes
+ *	TOAST_MISSING_OK and an external value of newtup could not be fetched
  *
  * NOTE: neither newtup nor oldtup will be modified.  This is a change
  * from the pre-8.1 API of this routine.
@@ -94,7 +96,7 @@ heap_toast_delete(Relation rel, HeapTuple oldtup, bool is_speculative)
  */
 HeapTuple
 heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
-							uint32 options)
+							uint32 options, uint32 flags)
 {
 	HeapTuple	result_tuple;
 	TupleDesc	tupleDesc;
@@ -154,7 +156,8 @@ heap_toast_insert_or_update(Relation rel, HeapTuple newtup, HeapTuple oldtup,
 		ttc.ttc_oldisnull = toast_oldisnull;
 	}
 	ttc.ttc_attr = toast_attr;
-	toast_tuple_init(&ttc);
+	if (!toast_tuple_init(&ttc, flags))
+		return NULL;
 
 	/* ----------
 	 * Compress and/or save external until data fits into target length
@@ -636,11 +639,15 @@ toast_build_flattened_tuple(TupleDesc tupleDesc,
  * sliceoffset is the byte offset within the TOAST value from which to fetch.
  * slicelength is the number of bytes to be fetched from the TOAST value.
  * result is the varlena into which the results should be written.
+ * flags are TOAST fetch flags, see toast_helper.h.
+ *
+ * Returns true on success.  If flags includes TOAST_MISSING_OK, returns false
+ * instead of raising an error when chunks are missing or inconsistent.
  */
-void
+bool
 heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 					   int32 sliceoffset, int32 slicelength,
-					   varlena *result)
+					   varlena *result, uint32 flags)
 {
 	Relation   *toastidxs;
 	ScanKeyData toastkey[3];
@@ -753,27 +760,45 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 		 * Some checks on the data we've found
 		 */
 		if (curchunk != expectedchunk)
+		{
+			if (flags & TOAST_MISSING_OK)
+				goto toast_missing;
+
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
 					 errmsg_internal("unexpected chunk number %d (expected %d) for toast value %" PRIu64 " in %s",
 									 curchunk, expectedchunk, valueid,
 									 RelationGetRelationName(toastrel))));
+		}
+
 		if (curchunk > endchunk)
+		{
+			if (flags & TOAST_MISSING_OK)
+				goto toast_missing;
+
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
 					 errmsg_internal("unexpected chunk number %d (out of range %d..%d) for toast value %" PRIu64 " in %s",
 									 curchunk,
 									 startchunk, endchunk, valueid,
 									 RelationGetRelationName(toastrel))));
+		}
+
 		expected_size = curchunk < totalchunks - 1 ? max_chunk_size
 			: attrsize - ((totalchunks - 1) * max_chunk_size);
+
 		if (chunksize != expected_size)
+		{
+			if (flags & TOAST_MISSING_OK)
+				goto toast_missing;
+
 			ereport(ERROR,
 					(errcode(ERRCODE_DATA_CORRUPTED),
 					 errmsg_internal("unexpected chunk size %d (expected %d) in chunk %d of %d for toast value %" PRIu64 " in %s",
 									 chunksize, expected_size,
 									 curchunk, totalchunks, valueid,
 									 RelationGetRelationName(toastrel))));
+		}
 
 		/*
 		 * Copy the data into proper place in our result
@@ -797,13 +822,26 @@ heap_fetch_toast_slice(Relation toastrel, Oid8 valueid, int32 attrsize,
 	 * Final checks that we successfully fetched the datum
 	 */
 	if (expectedchunk != (endchunk + 1))
+	{
+		if (flags & TOAST_MISSING_OK)
+			goto toast_missing;
+
 		ereport(ERROR,
 				(errcode(ERRCODE_DATA_CORRUPTED),
 				 errmsg_internal("missing chunk number %d for toast value %" PRIu64 " in %s",
 								 expectedchunk, valueid,
 								 RelationGetRelationName(toastrel))));
+	}
 
 	/* End scan and close indexes. */
 	systable_endscan_ordered(toastscan);
 	toast_close_indexes(toastidxs, num_indexes, AccessShareLock);
+
+	return true;
+
+toast_missing:
+	systable_endscan_ordered(toastscan);
+	toast_close_indexes(toastidxs, num_indexes, AccessShareLock);
+
+	return false;
 }
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index be0f6646b83..2ab3f77edfc 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -108,6 +108,7 @@
 #include "access/heapam_xlog.h"
 #include "access/heaptoast.h"
 #include "access/rewriteheap.h"
+#include "access/toast_helper.h"
 #include "access/transam.h"
 #include "access/xact.h"
 #include "access/xloginsert.h"
@@ -149,6 +150,7 @@ typedef struct RewriteStateData
 	XLogRecPtr	rs_begin_lsn;	/* XLogInsertLsn when starting the rewrite */
 	HTAB	   *rs_unresolved_tups; /* unmatched A tuples */
 	HTAB	   *rs_old_new_tid_map; /* unmatched B tuples */
+	double		rs_tuples_dropped;	/* # stashed tuples skipped, TOAST gone */
 	HTAB	   *rs_logical_mappings;	/* logical remapping files */
 	uint32		rs_num_rewrite_mappings;	/* # in memory mappings */
 } RewriteStateData;
@@ -172,6 +174,7 @@ typedef struct
 {
 	TidHashKey	key;			/* expected xmin/old location of B tuple */
 	ItemPointerData old_tid;	/* A's location in the old heap */
+	bool		recently_dead;	/* A was RECENTLY_DEAD */
 	HeapTuple	tuple;			/* A's tuple contents */
 } UnresolvedTupData;
 
@@ -211,7 +214,8 @@ typedef struct RewriteMappingDataEntry
 
 
 /* prototypes for internal functions */
-static void raw_heap_insert(RewriteState state, HeapTuple tup);
+static bool raw_heap_insert(RewriteState state, HeapTuple tup,
+							bool recently_dead);
 
 /* internal logical remapping prototypes */
 static void logical_begin_heap_rewrite(RewriteState state);
@@ -293,12 +297,16 @@ begin_heap_rewrite(Relation old_heap, Relation new_heap, TransactionId oldest_xm
  * End a rewrite.
  *
  * state and any other resources are freed.
+ *
+ * Returns the number of stashed tuples that were skipped because their TOAST
+ * data was gone; see rewrite_heap_tuple().
  */
-void
+double
 end_heap_rewrite(RewriteState state)
 {
 	HASH_SEQ_STATUS seq_status;
 	UnresolvedTup unresolved;
+	double		tuples_dropped;
 
 	/*
 	 * Write any remaining tuples in the UnresolvedTups table. If we have any
@@ -309,7 +317,8 @@ end_heap_rewrite(RewriteState state)
 	while ((unresolved = hash_seq_search(&seq_status)) != NULL)
 	{
 		ItemPointerSetInvalid(&unresolved->tuple->t_data->t_ctid);
-		raw_heap_insert(state, unresolved->tuple);
+		if (!raw_heap_insert(state, unresolved->tuple, unresolved->recently_dead))
+			state->rs_tuples_dropped += 1;
 	}
 
 	/* Write the last page, if any */
@@ -323,8 +332,12 @@ end_heap_rewrite(RewriteState state)
 
 	logical_end_heap_rewrite(state);
 
+	tuples_dropped = state->rs_tuples_dropped;
+
 	/* Deleting the context frees everything */
 	MemoryContextDelete(state->rs_cxt);
+
+	return tuples_dropped;
 }
 
 /*
@@ -337,10 +350,18 @@ end_heap_rewrite(RewriteState state)
  * state		opaque state as returned by begin_heap_rewrite
  * old_tuple	original tuple in the old heap
  * new_tuple	new, rewritten tuple to be inserted to new heap
+ * recently_dead	old_tuple is RECENTLY_DEAD
+ *
+ * The TOAST data of a RECENTLY_DEAD tuple may already have been removed; see
+ * raw_heap_insert().  Returns false if new_tuple was skipped for that reason,
+ * in which case the caller should count it as dead.  Skipped older versions
+ * of the same update chain, which were stashed earlier and get written out
+ * here or in end_heap_rewrite(), are counted in rs_tuples_dropped instead.
  */
-void
+bool
 rewrite_heap_tuple(RewriteState state,
-				   HeapTuple old_tuple, HeapTuple new_tuple)
+				   HeapTuple old_tuple, HeapTuple new_tuple,
+				   bool recently_dead)
 {
 	MemoryContext old_cxt;
 	ItemPointerData old_tid;
@@ -438,13 +459,14 @@ rewrite_heap_tuple(RewriteState state,
 
 			unresolved->old_tid = old_tuple->t_self;
 			unresolved->tuple = heap_copytuple(new_tuple);
+			unresolved->recently_dead = recently_dead;
 
 			/*
 			 * We can't do anything more now, since we don't know where the
 			 * tuple will be written.
 			 */
 			MemoryContextSwitchTo(old_cxt);
-			return;
+			return true;
 		}
 	}
 
@@ -462,7 +484,24 @@ rewrite_heap_tuple(RewriteState state,
 		ItemPointerData new_tid;
 
 		/* Insert the tuple and find out where it's put in new_heap */
-		raw_heap_insert(state, new_tuple);
+		if (!raw_heap_insert(state, new_tuple, recently_dead))
+		{
+			if (!free_new)
+			{
+				MemoryContextSwitchTo(old_cxt);
+				return false;
+			}
+
+			/*
+			 * This is a stashed older version, and the caller's tuple has
+			 * already been written.  Any still older version waiting for this
+			 * one is written out by end_heap_rewrite().
+			 */
+			state->rs_tuples_dropped += 1;
+			heap_freetuple(new_tuple);
+			MemoryContextSwitchTo(old_cxt);
+			return true;
+		}
 		new_tid = new_tuple->t_self;
 
 		logical_rewrite_heap_tuple(state, old_tid, new_tuple);
@@ -503,6 +542,7 @@ rewrite_heap_tuple(RewriteState state,
 				new_tuple = unresolved->tuple;
 				free_new = true;
 				old_tid = unresolved->old_tid;
+				recently_dead = unresolved->recently_dead;
 				new_tuple->t_data->t_ctid = new_tid;
 
 				/*
@@ -539,6 +579,7 @@ rewrite_heap_tuple(RewriteState state,
 	}
 
 	MemoryContextSwitchTo(old_cxt);
+	return true;
 }
 
 /*
@@ -599,9 +640,14 @@ rewrite_heap_dead_tuple(RewriteState state, HeapTuple old_tuple)
  * t_self of the tuple is set to the new TID of the tuple. If t_ctid of the
  * tuple is invalid on entry, it's replaced with the new TID as well (in
  * the inserted data only, not in the caller's copy).
+ *
+ * recently_dead must be set if the tuple is RECENTLY_DEAD.  Other backends
+ * may already consider such a tuple fully dead, and VACUUM or pruning of the
+ * TOAST relation may have removed its TOAST data.  In that case nobody can
+ * see the tuple anymore, so it is not inserted, and false is returned.
  */
-static void
-raw_heap_insert(RewriteState state, HeapTuple tup)
+static bool
+raw_heap_insert(RewriteState state, HeapTuple tup, bool recently_dead)
 {
 	Page		page;
 	Size		pageFreeSpace,
@@ -629,6 +675,7 @@ raw_heap_insert(RewriteState state, HeapTuple tup)
 	else if (HeapTupleHasExternal(tup) || tup->t_len > TOAST_TUPLE_THRESHOLD)
 	{
 		uint32		options = HEAP_INSERT_SKIP_FSM;
+		uint32		toastflags = recently_dead ? TOAST_MISSING_OK : 0;
 
 		/*
 		 * While rewriting the heap for REPACK, make sure data for the TOAST
@@ -638,7 +685,9 @@ raw_heap_insert(RewriteState state, HeapTuple tup)
 		options |= HEAP_INSERT_NO_LOGICAL;
 
 		heaptup = heap_toast_insert_or_update(state->rs_new_rel, tup, NULL,
-											  options);
+											  options, toastflags);
+		if (heaptup == NULL)
+			return false;
 	}
 	else
 		heaptup = tup;
@@ -712,6 +761,8 @@ raw_heap_insert(RewriteState state, HeapTuple tup)
 	/* If heaptup is a private copy, release it. */
 	if (heaptup != tup)
 		heap_freetuple(heaptup);
+
+	return true;
 }
 
 /* ------------------------------------------------------------------------
diff --git a/src/backend/access/table/toast_helper.c b/src/backend/access/table/toast_helper.c
index 2613d9dc095..e74e28d8e2c 100644
--- a/src/backend/access/table/toast_helper.c
+++ b/src/backend/access/table/toast_helper.c
@@ -36,9 +36,12 @@
  * toast_flags is just a single uint8, but toast_attr is a caller-provided
  * array with a length equal to tupleDesc->natts.  The caller need not
  * perform any initialization of the array before calling this function.
+ *
+ * If flags includes TOAST_MISSING_OK and an external value of the new tuple
+ * cannot be fetched, returns false instead of raising an error.
  */
-void
-toast_tuple_init(ToastTupleContext *ttc)
+bool
+toast_tuple_init(ToastTupleContext *ttc, uint32 flags)
 {
 	TupleDesc	tupleDesc = ttc->ttc_rel->rd_att;
 	int			numAttrs = tupleDesc->natts;
@@ -141,7 +144,25 @@ toast_tuple_init(ToastTupleContext *ttc)
 			if (VARATT_IS_EXTERNAL(new_value))
 			{
 				ttc->ttc_attr[i].tai_oldexternal = new_value;
-				if (att->attstorage == TYPSTORAGE_PLAIN)
+
+				if ((flags & TOAST_MISSING_OK) != 0 &&
+					VARATT_IS_EXTERNAL_ONDISK(new_value))
+				{
+					new_value = detoast_external_attr_extended(new_value);
+					if (new_value == NULL)
+						goto toast_missing;
+
+					/* PLAIN storage does not allow compressed values */
+					if (att->attstorage == TYPSTORAGE_PLAIN &&
+						VARATT_IS_COMPRESSED(new_value))
+					{
+						varlena    *decompressed = detoast_attr(new_value);
+
+						pfree(new_value);
+						new_value = decompressed;
+					}
+				}
+				else if (att->attstorage == TYPSTORAGE_PLAIN)
 					new_value = detoast_attr(new_value);
 				else
 					new_value = detoast_external_attr(new_value);
@@ -163,6 +184,20 @@ toast_tuple_init(ToastTupleContext *ttc)
 			ttc->ttc_attr[i].tai_colflags |= TOASTCOL_IGNORE;
 		}
 	}
+
+	return true;
+
+toast_missing:
+	/* release the values fetched so far */
+	for (; i >= 0; i--)
+	{
+		if (ttc->ttc_attr[i].tai_colflags & TOASTCOL_NEEDS_FREE)
+		{
+			pfree(DatumGetPointer(ttc->ttc_values[i]));
+			ttc->ttc_attr[i].tai_colflags &= ~TOASTCOL_NEEDS_FREE;
+		}
+	}
+	return false;
 }
 
 /*
diff --git a/src/include/access/detoast.h b/src/include/access/detoast.h
index 93b7a253760..c5411b34fcc 100644
--- a/src/include/access/detoast.h
+++ b/src/include/access/detoast.h
@@ -91,6 +91,15 @@ toast_external_info_get(const struct varlena *attr, toast_external_data *toast_e
  */
 extern varlena *detoast_external_attr(varlena *attr);
 
+/* ----------
+ * detoast_external_attr_extended() -
+ *
+ *		Like detoast_external_attr, but returns NULL if toast chunks
+ *		are missing (instead of raising an error).
+ * ----------
+ */
+extern varlena *detoast_external_attr_extended(varlena *attr);
+
 /* ----------
  * detoast_attr() -
  *
diff --git a/src/include/access/heaptoast.h b/src/include/access/heaptoast.h
index 4c3fda8a5f9..f51f965507f 100644
--- a/src/include/access/heaptoast.h
+++ b/src/include/access/heaptoast.h
@@ -105,11 +105,14 @@
 /* ----------
  * heap_toast_insert_or_update -
  *
- *	Called by heap_insert() and heap_update().
+ *	Called by heap_insert() and heap_update().  Returns NULL if flags
+ *	includes TOAST_MISSING_OK and an external value could not be fetched.
+ *	See toast_helper.h for the values of "flags".
  * ----------
  */
 extern HeapTuple heap_toast_insert_or_update(Relation rel, HeapTuple newtup,
-											 HeapTuple oldtup, uint32 options);
+											 HeapTuple oldtup, uint32 options,
+											 uint32 flags);
 
 /* ----------
  * heap_toast_delete -
@@ -153,11 +156,14 @@ extern HeapTuple toast_build_flattened_tuple(TupleDesc tupleDesc,
 /* ----------
  * heap_fetch_toast_slice
  *
- *	Fetch a slice from a toast value stored in a heap table.
+ *	Fetch a slice from a toast value stored in a heap table.  Returns
+ *	false if flags includes TOAST_MISSING_OK and chunks are missing.
+ *	See toast_helper.h for the values of "flags".
  * ----------
  */
-extern void heap_fetch_toast_slice(Relation toastrel, Oid8 valueid,
+extern bool heap_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								   int32 attrsize, int32 sliceoffset,
-								   int32 slicelength, varlena *result);
+								   int32 slicelength, varlena *result,
+								   uint32 flags);
 
 #endif							/* HEAPTOAST_H */
diff --git a/src/include/access/rewriteheap.h b/src/include/access/rewriteheap.h
index 6ccf7b45c04..63a78179071 100644
--- a/src/include/access/rewriteheap.h
+++ b/src/include/access/rewriteheap.h
@@ -24,9 +24,9 @@ typedef struct RewriteStateData *RewriteState;
 extern RewriteState begin_heap_rewrite(Relation old_heap, Relation new_heap,
 									   TransactionId oldest_xmin, TransactionId freeze_xid,
 									   MultiXactId cutoff_multi);
-extern void end_heap_rewrite(RewriteState state);
-extern void rewrite_heap_tuple(RewriteState state, HeapTuple old_tuple,
-							   HeapTuple new_tuple);
+extern double end_heap_rewrite(RewriteState state);
+extern bool rewrite_heap_tuple(RewriteState state, HeapTuple old_tuple,
+							   HeapTuple new_tuple, bool recently_dead);
 extern bool rewrite_heap_dead_tuple(RewriteState state, HeapTuple old_tuple);
 
 /*
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index 9d2fc7f9acb..70bf786ce08 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -782,12 +782,17 @@ typedef struct TableAmRoutine
 	 * This callback is invoked when detoasting a value stored in a toast
 	 * table implemented by this AM.  See table_relation_fetch_toast_slice()
 	 * for more details.
+	 *
+	 * Returns true on success.  If flags (see toast_helper.h) includes
+	 * TOAST_MISSING_OK, returns false instead of raising an error when chunks
+	 * are missing.
 	 */
-	void		(*relation_fetch_toast_slice) (Relation toastrel, Oid8 valueid,
+	bool		(*relation_fetch_toast_slice) (Relation toastrel, Oid8 valueid,
 											   int32 attrsize,
 											   int32 sliceoffset,
 											   int32 slicelength,
-											   varlena *result);
+											   varlena *result,
+											   uint32 flags);
 
 
 	/* ------------------------------------------------------------------------
@@ -2003,11 +2008,16 @@ table_relation_toast_am(Relation rel)
  *
  * result is caller-allocated space into which the fetched bytes should be
  * stored.
+ *
+ * flags are TOAST fetch flags, see toast_helper.h.  Returns true on success.
+ * If flags includes TOAST_MISSING_OK, returns false instead of raising an
+ * error when chunks are missing.
  */
-static inline void
+static inline bool
 table_relation_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 								 int32 attrsize, int32 sliceoffset,
-								 int32 slicelength, varlena *result)
+								 int32 slicelength, varlena *result,
+								 uint32 flags)
 {
 	if (attrsize < 0 || sliceoffset < 0 || slicelength < 0)
 		elog(ERROR, "invalid toast slice: offset %d, length %d, value size %d",
@@ -2015,10 +2025,10 @@ table_relation_fetch_toast_slice(Relation toastrel, Oid8 valueid,
 	if (slicelength > attrsize - sliceoffset)
 		elog(ERROR, "slice length is too big");
 
-	toastrel->rd_tableam->relation_fetch_toast_slice(toastrel, valueid,
-													 attrsize,
-													 sliceoffset, slicelength,
-													 result);
+	return toastrel->rd_tableam->relation_fetch_toast_slice(toastrel, valueid,
+															attrsize,
+															sliceoffset, slicelength,
+															result, flags);
 }
 
 
diff --git a/src/include/access/toast_helper.h b/src/include/access/toast_helper.h
index 2ec92397f26..622700e3131 100644
--- a/src/include/access/toast_helper.h
+++ b/src/include/access/toast_helper.h
@@ -101,7 +101,16 @@ typedef struct
 #define TOASTCOL_IGNORE						0x0010
 #define TOASTCOL_INCOMPRESSIBLE				0x0020
 
-extern void toast_tuple_init(ToastTupleContext *ttc);
+/*
+ * Flags for TOAST fetch operations.
+ *
+ * TOAST_MISSING_OK makes the fetch report missing or inconsistent chunks to
+ * the caller instead of raising an error.  This is only appropriate for
+ * RECENTLY_DEAD tuples, whose TOAST data may legitimately be gone already.
+ */
+#define TOAST_MISSING_OK		(1 << 0)
+
+extern bool toast_tuple_init(ToastTupleContext *ttc, uint32 flags);
 extern int	toast_tuple_find_biggest_attribute(ToastTupleContext *ttc,
 											   bool for_compression,
 											   bool check_main);
-- 
2.54.0 (Apple Git-157)

