public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH 2/2] review reworks
3+ messages / 3 participants
[nested] [flat]

* [PATCH 2/2] review reworks
@ 2019-07-06 14:39  Tomas Vondra <[email protected]>
  0 siblings, 0 replies; 3+ messages in thread

From: Tomas Vondra @ 2019-07-06 14:39 UTC (permalink / raw)

---
 src/backend/access/heap/tuptoaster.c | 47 +++++++++++++++-------------
 src/common/pg_lzcompress.c           | 19 +++++------
 src/include/common/pg_lzcompress.h   |  2 +-
 3 files changed, 37 insertions(+), 31 deletions(-)

diff --git a/src/backend/access/heap/tuptoaster.c b/src/backend/access/heap/tuptoaster.c
index 1eb6ccaca2..809a52196c 100644
--- a/src/backend/access/heap/tuptoaster.c
+++ b/src/backend/access/heap/tuptoaster.c
@@ -62,7 +62,7 @@ typedef struct toast_compress_header
 #define TOAST_COMPRESS_HDRSZ		((int32) sizeof(toast_compress_header))
 #define TOAST_COMPRESS_RAWSIZE(ptr) (((toast_compress_header *) (ptr))->rawsize)
 #define TOAST_COMPRESS_SIZE(ptr)	((int32) VARSIZE(ptr) - TOAST_COMPRESS_HDRSZ)
-#define TOAST_COMPRESS_DATA(ptr) \
+#define TOAST_COMPRESS_RAWDATA(ptr) \
 	(((char *) (ptr)) + TOAST_COMPRESS_HDRSZ)
 #define TOAST_COMPRESS_SET_RAWSIZE(ptr, len) \
 	(((toast_compress_header *) (ptr))->rawsize = (len))
@@ -254,7 +254,7 @@ heap_tuple_untoast_attr(struct varlena *attr)
  *		Public entry point to get back part of a toasted value
  *		from compression or external storage.
  *
- * Note if slicelength is negative, return suffix of the value.
+ * Note: when slicelength is negative, return suffix of the value.
  * ----------
  */
 struct varlena *
@@ -277,17 +277,23 @@ heap_tuple_untoast_attr_slice(struct varlena *attr,
 			return toast_fetch_datum_slice(attr, sliceoffset, slicelength);
 
 		/*
-		 * If only need the prefix slice, fetch enough datums to decompress.
-		 * Otherwise, fetch all datums.
+		 * When a prefix of the value is requested, fetch enough slices to
+		 * decompress. Otherwise, fetch all slices.
 		 */
 		if (slicelength > 0 && sliceoffset >= 0)
 		{
 			int32 max_size;
+
+			/*
+			 * Determine maximum amount of compressed data needed for a prefix
+			 * of a given length (after decompression).
+			 */
 			max_size = pglz_maximum_compressed_size(sliceoffset + slicelength,
 													TOAST_COMPRESS_SIZE(attr));
+
 			/*
-			 * Be sure to get enough compressed slice
-			 * and compressed marker will get set automatically
+			 * Fetch enough compressed slices, compressed marker will get set
+			 * automatically.
 			 */
 			preslice = toast_fetch_datum_slice(attr, 0, max_size);
 		}
@@ -1409,7 +1415,7 @@ toast_compress_datum(Datum value)
 	 */
 	len = pglz_compress(VARDATA_ANY(DatumGetPointer(value)),
 						valsize,
-						TOAST_COMPRESS_DATA(tmp),
+						TOAST_COMPRESS_RAWDATA(tmp),
 						PGLZ_strategy_default);
 	if (len >= 0 &&
 		len + TOAST_COMPRESS_HDRSZ < valsize - 2)
@@ -2050,7 +2056,8 @@ toast_fetch_datum(struct varlena *attr)
  *	in the toast relation
  *
  *	Note that this function supports non-compressed external datums
- *	and compressed external datum slices at the start of the object.
+ *	and compressed external datums (in which case the requrested slice
+ *  has to be a prefix, i.e. sliceoffset has to be 0).
  * ----------
  */
 static struct varlena *
@@ -2092,7 +2099,8 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset, int32 length)
 
 	/*
 	 * It's meaningful to fetch slices at the start of a compressed datum,
-	 * since we can decompress the prefix raw data from it.
+	 * since we can decompress the prefix raw data without decompressing the
+	 * whole value.
 	 */
 	Assert(!VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) || 0 == sliceoffset);
 
@@ -2105,26 +2113,23 @@ toast_fetch_datum_slice(struct varlena *attr, int32 sliceoffset, int32 length)
 		length = 0;
 	}
 
+	/*
+	 * When fetching a prefix of a compressed external datum, account for
+	 * the rawsize tracking amount of raw data (it's stored at the beginning
+	 * as an int32 value).
+	 */
 	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer) && length > 0)
-	{
-		/*
-		 * If we are going to fetch the compressed external datum slice
-		 * at the start of the object, also should fetch rawsize field used
-		 * to record the size of the raw data.
-		 */
 		length = length + sizeof(int32);
-	}
 
 	if (((sliceoffset + length) > attrsize) || length < 0)
 		length = attrsize - sliceoffset;
 
 	result = (struct varlena *) palloc(length + VARHDRSZ);
 
-	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer)) {
+	if (VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer))
 		SET_VARSIZE_COMPRESSED(result, length + VARHDRSZ);
-	} else {
+	else
 		SET_VARSIZE(result, length + VARHDRSZ);
-	}
 
 	if (length == 0)
 		return result;			/* Can save a lot of work at this point! */
@@ -2307,7 +2312,7 @@ toast_decompress_datum(struct varlena *attr)
 		palloc(TOAST_COMPRESS_RAWSIZE(attr) + VARHDRSZ);
 	SET_VARSIZE(result, TOAST_COMPRESS_RAWSIZE(attr) + VARHDRSZ);
 
-	if (pglz_decompress(TOAST_COMPRESS_DATA(attr),
+	if (pglz_decompress(TOAST_COMPRESS_RAWDATA(attr),
 						TOAST_COMPRESS_SIZE(attr),
 						VARDATA(result),
 						TOAST_COMPRESS_RAWSIZE(attr), true) < 0)
@@ -2334,7 +2339,7 @@ toast_decompress_datum_slice(struct varlena *attr, int32 slicelength)
 
 	result = (struct varlena *) palloc(slicelength + VARHDRSZ);
 
-	rawsize = pglz_decompress(TOAST_COMPRESS_DATA(attr),
+	rawsize = pglz_decompress(TOAST_COMPRESS_RAWDATA(attr),
 							  VARSIZE(attr) - TOAST_COMPRESS_HDRSZ,
 							  VARDATA(result),
 							  slicelength, false);
diff --git a/src/common/pg_lzcompress.c b/src/common/pg_lzcompress.c
index ac7d66db77..18766ade02 100644
--- a/src/common/pg_lzcompress.c
+++ b/src/common/pg_lzcompress.c
@@ -773,27 +773,28 @@ pglz_decompress(const char *source, int32 slen, char *dest,
 }
 
 
-
 /* ----------
  * pglz_max_compressed_size -
  *
- * 		Calculate the maximum size of the compressed slice corresponding to the
- * 		raw slice. Return the maximum size, or total compressed size if maximum
- * 		size is larger than total compressed size.
+ *		Calculate the maximum compressed size for a given amount of raw data.
+ *		Return the maximum size, or total compressed size if maximum size is
+ *		larger than total compressed size.
  * ----------
  */
 int32
-pglz_maximum_compressed_size(int32 raw_slice_size, int32 total_compressed_size)
+pglz_maximum_compressed_size(int32 rawsize, int32 total_compressed_size)
 {
-	int32 result;
+	int32 compressed_size;
 
 	/*
 	 * Use int64 to prevent overflow during calculation.
 	 */
-	result = (int32)((int64)raw_slice_size * 9 + 8) / 8;
+	compressed_size = (int32) ((int64) rawsize * 9 + 8) / 8;
 
 	/*
-	 * Note that slice compressed size will never be larger than total compressed size.
+	 * Maximum compressed size can't be larger than total compressed size.
 	 */
-	return result > total_compressed_size ? total_compressed_size: result;
+	compressed_size = Min(compressed_size, total_compressed_size);
+
+	return compressed_size;
 }
diff --git a/src/include/common/pg_lzcompress.h b/src/include/common/pg_lzcompress.h
index cda3e1d364..e59fe2eaea 100644
--- a/src/include/common/pg_lzcompress.h
+++ b/src/include/common/pg_lzcompress.h
@@ -87,6 +87,6 @@ extern int32 pglz_compress(const char *source, int32 slen, char *dest,
 						   const PGLZ_Strategy *strategy);
 extern int32 pglz_decompress(const char *source, int32 slen, char *dest,
 							 int32 rawsize, bool check_complete);
-extern int32 pglz_maximum_compressed_size(int32 raw_slice_size, int32 raw_size);
+extern int32 pglz_maximum_compressed_size(int32 rawsize, int32 total_compressed_size);
 
 #endif							/* _PG_LZCOMPRESS_H_ */
-- 
2.20.1


--3wvarhewrzuvoyhd
Content-Type: application/x-sh
Content-Disposition: attachment; filename="random-bench2.sh"
Content-Transfer-Encoding: quoted-printable

#!/bin/bash=0A=0ALEN=3D100000=0AROWS=3D1000=0A=0Afor a in 0 100 1000 10000 =
100000; do=0A=0A	b=3D$((LEN - a))=0A=0A	dropdb --if-exists test > /dev/null=
 2>&1=0A	createdb test > /dev/null 2>&1=0A=0A	psql test -c "create table t =
(a text)" > /dev/null 2>&1=0A=0A	psql test -c "create extension randomstrin=
g" > /dev/null 2>&1=0A=0A	psql test -c "insert into t select random_string(=
$a) || repeat('a', $b) from generate_series(1,$ROWS) S(i)" > /dev/null 2>&1=
=0A=0A	psql test -c "vacuum analyze t" > /dev/null 2>&1=0A=0A	psql test -c =
"checkpoint" > /dev/null 2>&1=0A=0A	for p in 0 100 1000 10000 100000; do=0A=
=0A		for l in 100 1000 10000 100000; do=0A=0A			for r in `seq 1 10`; do=0A	=
			psql test > tmp.log <<EOF=0A\timing on=0Aselect sum(length(substr(a,$p,$=
l))) from t=0AEOF=0A=0A				t=3D`cat tmp.log | grep Time | awk '{print $2}'`=
=0A=0A				echo $LEN $a $b $p $l $r random-first $t=0A=0A			done=0A=0A		done=
=0A=0A	done=0A=0Adone=0A=0Afor a in 0 100 1000 10000 100000; do=0A=0A	b=3D$=
((LEN - a))=0A=0A	dropdb --if-exists test > /dev/null 2>&1=0A	createdb test=
 > /dev/null 2>&1=0A=0A	psql test -c "create table t (a text)" > /dev/null =
2>&1=0A=0A	psql test -c "create extension randomstring" > /dev/null 2>&1=0A=
=0A	psql test -c "insert into t select repeat('a', $b) || random_string($a)=
 from generate_series(1,$ROWS) S(i)" > /dev/null 2>&1=0A=0A	psql test -c "v=
acuum analyze t" > /dev/null 2>&1=0A=0A	psql test -c "checkpoint" > /dev/nu=
ll 2>&1=0A=0A	for p in 0 100 1000 10000 100000; do=0A=0A		for l in 100 1000=
 10000 100000; do=0A=0A			for r in `seq 1 10`; do=0A				psql test > tmp.log=
 <<EOF=0A\timing on=0Aselect sum(length(substr(a,$p,$l))) from t=0AEOF=0A=
=0A				t=3D`cat tmp.log | grep Time | awk '{print $2}'`=0A=0A				echo $LEN =
$a $b $p $l $r random-last $t=0A=0A			done=0A=0A		done=0A=0A	done=0A=0Adone=
=0A
--3wvarhewrzuvoyhd
Content-Type: application/vnd.oasis.opendocument.spreadsheet
Content-Disposition: attachment; filename="comparison.ods"
Content-Transfer-Encoding: base64

UEsDBBQAAAgAAGJp5k6FbDmKLgAAAC4AAAAIAAAAbWltZXR5cGVhcHBsaWNhdGlvbi92bmQu
b2FzaXMub3BlbmRvY3VtZW50LnNwcmVhZHNoZWV0UEsDBBQAAAgAAGJp5k7m41/j3iIAAN4i
AAAYAAAAVGh1bWJuYWlscy90aHVtYm5haWwucG5niVBORw0KGgoAAAANSUhEUgAAAL4AAAEA
CAMAAAA6HTweAAADAFBMVEUAAAAEDQcKAAALCwsFEAkRCwsVFRUAIAkMJBQANA8AOBESJhgd
KSAUPCAuBQUlHx81AQEwGxsiIiIvMC8wKCgwMDA2PDg6MzM4ODgAShYBVRoZRicdVC4eWDAA
Yx0CaiEBdyQjRC0mTDIpWTc7Qj0wXD0nZjoncj4wYD41W0Eua0Aqe0M1Z0Q3dUo+e1FLEBBZ
FBRMNDRULy9bOztrAABlFhZ6AAB4GxtjNzdERERFSkdGTUhLTEtHVkxWSkpUVVVVWFZZU1Nb
W1tEYExKaFNDfFRbYl1SdFxeZWBXfGJoR0dkWVl1SEh5VFRnZ2dpcmxudXB2Z2dycnJ0enV2
fHh6cnJ7e3sCiCoPjjUBli4EmzICqTUSr0IuhUkxj047iFI2l1M6p1s9r2A+smJGhVlEl11M
jmBKmWJdgmhSmmhHpGNDvmhUpWxZqXFQt25YuHVhiGxljnFsmXp4gntjqnhhtHpyoH9WxXdi
wX5+hYBmv4F5qod+so1rzIh2wo160pWEAACDHR2VAACNICCaIiKkAAC0AACiJSW2KSmITEyH
W1uXVFSFaGiGeHiZaGiYd3eiW1uiamqlc3O1Zma0enrMAADPFxfYAADKLi7OMjLWMTHpAAD+
AAD/HR3mNDT+OzvEbm7Nc3PYeXnif3//fn6IiYiJk4yOl5CYiIiWl5aPoJWAtI+IvJeYpJyd
paCcuaWhiYmjmJi7g4O5mJiioqKjqqWnramsoKCqqqqotKyttrC2p6ezs7OzurW8tLS7u7uK
w5uB1JqPyKCRzKOZ2q6d3bCuwrSg3rO3xbuq4ru8y8G90sS05cPMh4fMlJTUjIzalJTIpaXK
urrSra3YtLTogoLmmJj5jIz+kpLrpaXlvLz+qKj+srLAxb78wb/FxcXCysXGzMjOxcXNzc3H
1cvN2tHVycnT09PS29XW3Nja09Pb29vB6c3M6dXY5NzV8N3e7OLZ8uHmx8fp19f9ysr+0tLl
5eXm9ert+PD96Of18u3z8/Pz+vX2+/j+8/P+/v4YMDqGAAAfmUlEQVR42u1dC5gU1ZVmN7uY
bNgl8ggPwyMLiwTUJCbRoBPqm4JEpLF6pmgKYVgfGzEJINksmqGME1zjEx1BxDWrrOSx1VCs
rYPUSgGVqc3KMMjDCCiPDGNJF00JJdjjMNWP6qq791a/Z7qn+w7UfO337ak6XY9p8b+n/zrn
vs6tAQNABckIFfM/GFBR8Id+tuEPVxha/gzDp0VAfHbhj6jSABX/7FqfPAXI6GcX/ptkgP4M
c181NPszzH18xzlxxMSRFaIj/gIfvoIjwhCsr4sDsb6uDNGw4eP4KaAOZ3G4aQzJu9QD2XNB
K8h9E9pfV0xHy4JfpcL/QtW0ILAUrTR8j2XIJtDUsuykDzY03ZR1oGkqMGWZtRUVaLoaly1W
M2UDniv5UddLAJUSvUGo5cGfppOmzE8TGJGSmVIRWxtOapRM2MMlQi/H+oMFSmNkwhgpkZpX
auRokRMIgWR5ng2SCqlXiT4x3/okYDVA+jTgMcuCTwCJp+NVQPcOZ2ihtPWFKtYTIQAXLMf6
Q4Q3gZ+rUqsBL1dbKlcFDIq0eFmfx4kjGa9SDWQ+H74HMDDyUjqgyoUPCBpMM2Wu2tKNkvBJ
iQOKDeGr5cGXDI9NqgSCb4osYaoMaUP4DCeTtmoSQOG7W18UTFIQTaI88gg8gLW8aj9jKAxr
lCSP3/IzIuCAWB55ZAVwnMhzQFZlHy9oDGPwlqQZgqiLDGdzQJNyfMGIN2mSMTlaNVlaLTds
yR6QrOfZZXgeLL+m53me0v98H+r7NF1F1dBVdFlCDqRxhPoc1tdrBpI0ngwYwHF+P8dzfnhE
H70LPZjDEWZgzkXpf37zwBoOT7rVebpVmDRDvSTyhAdjVtlOWTjSo8rmy38gq4x0VBHS8OOU
BAyvN5hS1fbRoknTYhHuWzxzGeFf8FKnznopdecm2eIjEP+AAQyt07RgUyxlM2yVzrEsoDkv
Dz0vkEe0+yRaEmA8mKin4CuCAD2zTfp0m0AqCYAQBUAW8TyGTOE01U+BjGULqEWHTILWogQN
z/xxB36VaUpylXUT8Ek+m4LKBqtsWpV5Q4NOmIB+lZIMLdWIUEcAiNdjAYKAWm0DglOAt1YF
XqOI5zHx4KeRFxZ/QBnJvqZMZEWO9vmUZNgSGfUmiwSMAO0ussCvEIDRZV7k4yQgBQnEBVKm
M9yH8CFWgoJKwiIEJODxy4A0C1sfxEls+MW5HhVEwnwNaojTaI23kvBpfpoGg3WwShipkSJp
wKLoKBpCyxEQvk/wNlelydPupRSFYvmkMrxB8rRB8HQR7uubJoqXDb4l0z4BqmidClg+fzuy
PvzV9bhhGsCwDN2wLc0E6NxEbNCBEY8DSzOAnrRuaLihwivDsFMKy6gltaDnGWJqGkYdfqia
ok5B5kMJ65alX4BH04prDvdL/ZPWJTjOziGYvWwfWlYUwUwUUCSmlf6rZSbKgn9pft/Cg7+n
oxz5BAk8RnHha0Mvoc5TWgaVBz8tEZetDz2PHAA2z+qXx/rnV9Ts/9hXs2+Pf1/H6yF862PX
OA3eAwTB8Fwe+DWv7/9BYM/+73nPe0M1fbA+Pnmg84VhgjQvC3l+vPPjL3fsmBHw7q/5sa9m
f9R98sCwRUP41uWw/iehGvbm85/s/97pLf7Xqbf9+PBHYFrfVAhdYmX68pBnx5YtvhX+14nz
p1d0UIGA+9Y/ywcEWxHNy+N5zgcCn5wPbAl17DnfsTfguufBdZxD8RwntvVDmGELF75sxAwj
Fi6yoT8ld/gRR7UWTPhn3K00DDscw5G425WGIcBCtbqUlrb+ew6sLrT3+OjqhMdPY52fftrZ
dQ5ed+LDD2GHLZoEmofloPrLs35XEVMfntYUO0LO29xENR7itjXFamFp3PY8QyyDAsxZQNI6
IO1yydOVU4jMSee2rTHq09j0xs1NnfM6a7c37cKHjx11bdPjRF0yXk7ogvCzxs8WInm2vSnm
6YxV79rVPH1r49bG2u3bbPejrjkL9eYSbNAiynx0u7oRKH0B4T9zuKv6SGdsegxSpzbYZLvu
91VqGmtQXtmgKRnL83R1405s6+zqrZ9S1KFmj2dbbNeR2OZnztgukyeM7zi7CkHvKvJEu2x9
l/2+7XKVDdf6w9VsbzT0Uzb8RJK6Sm2ooescAaiwOs9wtZyOTYQ74Wyuex5L4oHJMRpUHQil
xs6Gqg62omoyTNhgGP1DXrMkA9/6+FFX8ICAFCd5qCpjlmh1oT7OlGFR146z5x6Zdp3yKRco
2pxrsH2wPmaVTXf8vtewSQqGroCEht1KdFOlsRfaLFYOj2RkYyKjsjQLg7nrlQbU1kWNRdQl
KoiA1kvCT0PNOcnciLN8dQSqsYNXaC1gu06eiFylyqiXnZFog1RIUIb1rXzu5Fhf5nk/UqBz
gBYl9x9dXRQlOyhZAKkuxkEZ1s8jT95vYMmyo0AzgS7jc9/lXrY86+dCz91skOq1dd3vYzcW
90VxJFFhlYah7e7Cx66y2Rb0NfGzSTV1E6p9Cda/EIpe+PDU6UgoEg1HXLc+9DyUB6izODZI
cVycCFAGyVN9t/4OaoVf3sIR7E4uWhOOWi5X2YwhJmws+nSnsYgGIGlOLDKKV5b1vaHozdGo
73RNpIbf8lrEcr/KBsMWlWos8hJg6V4jbynr7yHZiZGzNdE32QBH7XzT7IfGogf4FZvkFItQ
OOCBNbhe6j0FrZ9I79HoR6Gzs6LcH6LRCLT/qS2W61FX9d7EGjQtmzSlQNsLdq9TnkqRJ0RR
p6MrLkSjO9sjvO+s5XKVDVr/Uh1norDLdO5bldZFe9iIFdmMeM+bbvey4c4oGXaoC0diFWZ9
t+FjRl3YWBQ5YLK0hhTw0G2mtTD8I8WxXqzdBrX2yJ9rb/vztjVHupo+vei+9Q2RArxskhzU
IGuTMgvQXMQizrM36x9a09Q1b9sH02EBPLUXa//ceNF18iT9Purj9OSELVou1uoaduRicfzb
Nl+cfbFreu2290bUbmu87Zna92L9MDA6C9CdgPTCigNqLMJKA81KxSoOvXJ/25ouz5+7pn/Q
uG76B4fWHKk9sibm+pwGQ6xS5BrBp9ACY5ASmdRdJD73jzxTe2Rb7eZntjU2rul6r/HibZu3
xVwfGDUkSQa6YtmaYgM0eTupFr7nObJt26GuI4cudh2CZXzvYtenh/rA/SHKKSVY9i4NRJ8p
OaWcCsJDMHkVRFfddmVw0zuHisq2Hneag7hh63MjcWR48a9/ucC9EX/5d1/CkStHVFTYsv/u
hQ048p998DwSrK4LvInUQkeZM4DkT2oBx6lJ5f/rsS/1Bv/xFzc8/vDDj2x48fENLz/y8oYX
XsaHrw6XYIU3IKgUn1Rv0KcTis+oVhidKPTosp7LZP3Hb3hkw4aXX7jhxYe/veEGZ+sLfGce
ZxQQMG4SpA0IvwIon2JTdLBAul7Y6eO8XNZ/DH58GxYBwn/5hhsefvjlvpAHtlgpMwkfmpvg
IXzGKQJqFPbkfpzCsf6LvcGH1n/hYQf+i4889vgNLz7SB+ubYsDkBY3iRc3Li5A8NCKOQSAt
VGXTy5yJ5Fi/10f3MQj/YfiFF7/98oaXIXleeAwf/giRYSVb8Ju2wJl2gDdtidNBUo0C3NcY
xl++9Xsjz2MP3/DYhsfhI3sDfAjgc/vCI33ifv7/8LI2Fl13nCFX/X7sS//2HzhyzOWmehjX
+v++EUeOgwqLui7DD+E2V0yaBDrp3Voe/CtfKo71pa8/sfHX3/jOr6A++eijT2y88ZWNbW4P
zQ0xdQpl8RFlWv/XxeG/8uSjG1955ZXvfOeVjd+48ZUbn3zi166TB/r9VB+nWR78Xqy/EcKH
5n/0iRsf/fqvfvXov9746JPHXW+uoGmoKIfyMnD/ySc2Pgkt/8pLL31jI6TOjS892uZ6Y1Gl
hrMGSYngkq3/xDe+/kSK+7/e+OSTG//1xpf+3/Nc3qiLB999z4PXSTj8dMIqsvUUeNf9biq8
HuYPUc+3VUgThQrgep3HUgXYtoSV0QBnlAW/mESdj01cOOJnz57mLyTaw65bPzzE4CggBDpJ
QdCpS4LvoGffPE1xO8LE3Cgd9kXx4WN2EnZmsic8ZYWuEvATcz+MTPyQ8PBzLzAMt+JCP0wK
SGVPUJcF/oUatoppD5Mf8fwe7+mA6brfNzXSkLggJflV7yWTJ9EuSoxvp0ZEw4xJSyJwfXhC
83O8LQVMS+LNS+d+NBCAuulCQgknTr3WD5MCymtUlmt9JCbcUgIqbB7nsHc+wJGuypoQYw97
5ySOdLnueWxgovmXSbVL0f/KkvDbTp48caLt5Im+wceOuqaTPeHjNQ/Nm7OYEvlbV75bHPg7
31x38sTs2SfXzF69/pl1607edgIfPvakANRYZGBj0QebLFIAhYBeuX/QQfpBjmbkxHoI/6jn
5Opn159Y3Xbb+jUHXSdPcmgORl0PjFloZLH3aagFuZ8px/o1sAyzT76x7o3pa1avWT173Rvu
kwcNjHIqIFjVJmW+QC9u+dx/41kH/jttJ7/ZBqlz27trulyvMKvUl1nDQ0sGRe20aZrrO/fX
feub696ZPeyZN6bPXnfyjTdOPnPbu11uj+sOwvT7B4vD/6D/HSduY3HYoYtIui5mpCtHc+87
UnEzSs7gfd/1KhsemtSqeLk1JCdbwpkJbGenA2e+4br1U6n2Trq9yCpAbDzVS1NdTaapJFNV
0ikryTt2IAjszWwnaN4OzgTOgeY+ZE/gVtkGo1T71wSDDIiGR6NNUqF7y6AYqhapn8I7cmPA
5kWD1HmvzZiM0Yhu9sc8TidspbInaKn7GkD58NO2BnnpQo5Igk3FbdI2IHy9keEYA/RDqv0s
p7GI2ruCCBhW6i2DYlh369vpAzzZJaCoRyD4hhA43C/ZE+lUe6SGRyV1qL0+uiljZ62e+j2A
wbOGyMl0XCZ1AKlDSxJwvcp2ludFWxZNJ91eEwwQEnqpN3TnfoY48HiY5w+jf0lD/6JmgnOS
XWlpZ3ncz1g/53fIPhLJe5WVaj/o1bdLyoHMx9ufVFZj0Rr0Xy2FpHV36+6Wlt2t3e+fdz/h
1ZABQLxHqosG0BwtkkEE4bemULa25KBtTRfi5//yVsv//KLlrX/ZvfvVt1rPuz+P00cC6HW8
yPPoZJDQPadInVIIq6j1W9Nw87Cjz9bd//zjX3zt1ZkzWm559ZbdM3e3nO+Xobl0JyH0+z5m
J6AZqVijcdBDaY605po/fdr6ixm33NryPzNaZu6eecutt+4+7/48TiPbRYuiLtPbNFRk/Tzj
ZwoBD62QPD/+2UM3t7w1o+UXt/7sZzP/65/P90ulgUG5KxA+yl3hReDli+avIOsn4bZmHoIc
47d87a3Wibv/dwY8n/nWzFf7AB8z9WZwiKY4k2d0yw/riiKj2Ckt5nkQ6NZMCbo/wW/dcstD
D82YcWvLz99q+XkfyHMJYaucbs5BD7XmPq4trTkUcsrSshs9wak/7D5fWY3FxBfvX4UjH1fW
fJ7E5+9aiiMtlTWfBxd+q/ujK5wX+k6vhBSgfh6a9ts1NA84mu4D/K985R/vuuorU/9x0h1L
J/0Q3/rYo+pxhUqtUQII1MuGlvcUAjaMvICXe8K/u3f0V1899e67lt591ZilY+64+u5+sL6V
zJ7wpJP+amUnbDESA6QAtvXHXH3HVUvv+Nupk66+esyYSXe19MuyckxyUXEC9TDzrwEKhi1K
pcFWCRs+5MxVdyxd+rdL/2nS3WPuvrrFdcepMSN5zcM4ano42iRZn0FyNKDZnuv1lIQPaTNp
6lVjxiy9Y+rSSWPucp08gyzDMIAVt5IjLIbtrFVlR2AMM/rkef4ppUuX/vCH+NzH7WUbYuNE
3ZKP7iU7zgEDsQT363/xV1CuQPsVyUP3PV8GYlvfiGNs6mDn4EjEQGmTzlU8u+dukQtXjF+4
sG5hmVK38JdxXOsPBbIGLEkw0wraecNRWYCtQF7L612FnudsqeyJnCVgzCvm1yVlkbP30AXj
xo2vWzC/buH4hXXzF9b9sQ/ZE14BCLxMB6DyUFVaI1RaJRWfSpiERsatvEfXYDCyJ6JXzF9U
t8gBWldXqADjxi1YuGD0qLrRC0fXja7Dh49yV/KzJ2DsdFIPkEp+wEv5jy7WKtiJlPUzZcgt
CJKxo0aNX7RgVB3cRo8b3RfrA1EEs8zkCtgESC7iTUPoKPkDwhfy4Ot4q2Anktavy5Qhe0gW
acHCui/ULRi1aP7YceNH3T6uD/DtNwM2L+kUUk7SvEEmXi1zBoHU8Jgeo1tT3SAtHPLkED9j
9ez1uNsXfKHu9lF1dZA8o+aP6wN5RMYnWTxrILV5zoQtQA2IztiDDhSmvVulIcT4OBvL+vmY
F2VLAreFo8fePn/0qLF1ty9YNH5cn8iTHf8op76P9f6mXO5nCZ9ThrqcPyzqw6OLP4cZR6IZ
x1me/LHC2rqY8FdVVgd51G342K2tKEOhxuL2y2T9hV8YP27UqCvGjR5fN3oBPnz87AkNNlfU
MrMnSlt/LMRdN24+DFvjx813nTzhgShslb2Id0nrjx87FtZ5Ri0cO3YsdJ/zV/XDSgFU+dkT
Ja0/atTf/E0drLXVLRi9YNSCsatc76bS6OGcTtIB+zJ5Hoh91AL4M0Duj5q/yvWRRcs0LWc6
XkV4HncdZ+KK2xctqitjQzvUP1bWCjGJLy6px5G3Kyv5I/HFxe7Cx/c8KNVe5OMW1Mtg/cVT
lt85ZcrU5VOX1y9ejg8ffzlRGHVFXiORXjr8xVOWLF6+fPHkCT+dvHyS++RBfp/MDM2VbLck
Pl+CPJMmjJlSXz9m+YTlkyZNmbT87X7oos0OjJb0/VbK+iuLwZ+wpP6q+iVT6pdMve7OyUuu
fdv1OQ0o1R4ShxICZaTedCNPz1LcOWXZVagM9csn149ZfGer668P0FiWs0UubgllJD4lPl/q
0b3z6uX1dy6vr18CH92prnue8BCsxqLV03GuzNkv3XG6OzQHBr3+MY6EK21VvH2RrMSdvdtl
PHs/Hon2w6QADR7UpMZLvTR36N4cvB3Onn+5f28k9PqO/Z/sCUdCFyJR98OWlwSKl2dkqAYh
kuXCzwWelYCXW7FpxX+3z21fEbk1jA+/DxVmT+6EmBLLSA/aV8DmGY1EqI/DN6/wzg15O7z+
Jn+H3V/TUJ2oy5dexLu9OHnQ1Z7quRPb94du3tMYYL17AlHXexos2NZFq+L5FZMMcqXavHnc
L2D//aHQD17/qGN6R4e3o2b/pqjrFWaVJliToVWkwE9L5XC/oxj1IyGvN7SfovZEduyPBHwX
ov2yjHT5oauH9TuKP8ZQ3CZP56WGrfO9hq0Ot2eU4La2ljXgyIEKa6o78Osbkh/1mUOx/W33
PQ/PAJOmlThUtBKwo8WrbMsy4HMOWVk8efKY+gmTl/zouiUNd9bjWx/b86D363KKSUAl0TrM
O3k0sFcOeep7kgXeWnzP5PtWfnXyysnLZ650nzyDQM4rk0qvgp0hT9b8eSxqWPn3cGv46pR7
r/2HyVOWHXCbPNl5nNk1yIunXSZS5Knvbv7M6eLvN0x4oOGrK5dde9/kZde5Tp7BOj9RUii0
AryfM8mAxyQCnrI8T32+9VO/wZTlDT+acO21Dcu+v3LydT864Ho/jxlUVWDAipqhAdvULFiB
tsvwPPUFaI9k+YMNDQ8sW9nw05UNK5c3HOj/1lavITjtefKJn++HVmbL9HaFTYT8rIet+xpW
5cBbldRV2auG3NNVB9xfXiiuAKDJNlR4KcWhmo4WJs99Drb0lovcufrp4nuXNdz/k4aVyxpW
PfBgf4QtmgQyLdJI0SLGuqMyWY71VzWkCpP5HX76k3sn3HPNPRNmLLumYbL75IGex0iuy4bU
mYYacLRws8VC1s8x/qo0S7J67733P9jw1ckN19x/nfvkQX2cs5KNxUgye8KXzJ5gimRP3JcD
uCFr/EwBHrimoeH+CQ8um3HP9ycvu+eA6xVmO+IBfOp9W0EGLaHuaJHUmwz3GzLsz6fPdT/5
5QP/AH1/wzUNX7v/ngOu521p9CzO5HyaxdEaCPhkwCMt8tYzK839VTm2z7P+PQ823DPha9c1
/OT+hnuvffBAf2TN2WU3FzPcz7F99hHOlOqXcOub33d5YTOUt7U7I4Uz6Fqzh/MVt7CZiVYw
Q0uZ9VhZyHnbR3KJMGePopdWVVYP89BTznJrBRZhQ68oTK8m5+yWZScs18lj+WkQ93plw+vM
3OcdpekA4GmmkPWT699ZhRdGsj6iqPbTXnqHuOkPiU3hfnjPooGyJxSbZIMWKfud7AmPCFVH
twvBd7AXWgTPgjdrTkdJ7g/h6rnxuac2GYmE2+TJNBY9ZraxWFMsewLCTxrfKoAfkWcTL3/5
w+k/4LkAR9MrPgT9MDA6K9lY1FJJf4KT9IeyJwSpoPWtgux37iaiwg6SupD4XvQj7iNa491f
GUznJgqqh+NUiuVNkndeFwbVa1N8gXpP0vpWUfbv9dL/HaA30aYSSMzl2oH7c9l0XQemDlJq
Z46WboLC3E86yrwfILOqZVhPJAwNqpmIftQPngdgDS0ObY+nxIR7jsDraLyH2K6vRYv1dfvK
P7XhSFeFha3BLsPH9zxGe/dU+7ijhqhB7Vbt79X6B99tazt6sO3gs8+tP7r+aNu7R9u6+jXV
Hr0iFaXZayjVnlBIA2keeYYdzIA92R39+tnPtr07ZzYswfo5tx1dfWKO++TJH1kMoPcNJd85
RMsMEFkG5TLkWf9gPuSTueVY/xy0/mx4MvvonBNznn529Yku9z1PaoGb9MhiOtUeRV2aRS+y
KGL9HsVoW/9sEv6fVsP9uXVz/vSc+2sSWgZpC6JBCmh5IcYkFcYkkJqkSavVZrdx3isP9sL9
dSnrr0ZfmtM25+BzXa6PbWk+irP8LEq119Op9kCAqjAikJl87nSzfj79182Z/dzBObNXtz17
tK3tjaPwxzja5XonoV2yX7Og5zlZtuMkcaTqc25+nfzr67/1rW9NT++pj+IynRwwQMARdjDW
1/0Dsb6+deBTa3GkaWs38qh59ShT7f7ratijK1hy5W+PHT+G9mPo8zjae8ix9H782AfduZ/v
CYxMNnwg43l0UkLpELKeVMX00jzUQBHHiVLtMeo8v81gdk6O5RTg999de/x33/3uWqjPr336
+eNPv3/8IoTvpXWKDpgE6zG9XJXOsDSgWA9Pagi+RFOa4qWFYal+JXWEJsBGh24TUEmvYaMB
KxiP7MKzHJOp9jjWT2I+loaevoL779auPf7b3x+7/rfvH7t+zrE5v3n69471q0xTFKbZVcAn
+QAlMoANVgE6JPOaotOayHABVjOJrN+XBNTBSsI7BGEDggsCb60K6HCxPk4PpvUzsNMf6dPn
18LPp54/BvXpp5+eM+ep3yDrE2A7Z1RZJGAEBsJnYROagO07mZcFnSZVkbNVhiezfh/Ch7UA
Ai2RRsUtAjb5UDgt3GUcxku5RNbvDj5THgj/ODT6MUfff+p3c3639gMHvkz5p2kQvloNjx7e
YyH4Co/I493MkjzLCtVS2voSSUoqRQtIFYoW4wTLGCTLFgtbzEgem/s9zQ+35797/fNPXf/U
01DXHvvN2mNPPfUbBD8OgNOwAXAzTGBHLHRmo9R4YFoGiNumAcyUcbWhpgG/FIX/kemolcyd
N4pXGgwDm/uFzH/8fSjH3k8K8kz/+f6xiwA/WxqvyobVVrSvPHi8rdct//Ki291UuMuJBmO9
Sjz/3HZ/lQw8+IdjdsEt7myx7vfdXxlMQu/XLTGBsJv17fTuaCx1K9bYHIsx887EYlzwcFMw
tquzD/Cx3zGK3q8rmWTZ8O0sejvH3DG7efNWWIJOT2zX9F3zYrXnGmOuWz/58oayk/6y1k/a
3c6cIt3VZHs6Y9Vn5m3dNe9w47zGeWds99ckTGVPlA8/i9jOMgnpLj5GQfjVjdW1nbuaDtee
abJdz9sypCoFDUljcD9Nn9wiwMMZaO5d87bO6+xs2h47x8Vqtzbb7o+qS6LkTAoon/sZm+cX
Ibh9+3ux4CF4dq4zBh/gzuaYXWnLSAezeHOg53zm/LEPj27I3bA1tD3bE2v26JGNoD9G4Ek0
tZmVtpjr3khHdr47/Ah3nwIP73RkJjlH3V/E20xOBHMmhJUDP9JrAsLp/ZELe/eGwvvCkfMX
+iH5I0J5gErxjEpxbOnvD9rX2/T3SCRAreC2ePk9K3ZyESrsOvzwQBv5fR2Q3s4ysuay6Qcd
hX8Ban/kphU+NlTT4d3i39Iv5CEz01CtPpMn/SjsnOmbuHfv/u+9uYLvU/ZEn5I/eBmQXLtF
lM39jh5T+FOHj0OhH+w4H7kp0lET9u7blHB9OVGVqmLitFdB07BxuN/N36Sf3FnU6X3krJ2R
HfsiW+aGEpXnOHGkf5I/cMJWBpmzQ40Wgp06JCptRknQRmLZdsIGJtot23LOLOdud6m0nMWg
jSP9sAK8wIJM4hNHi32HH2ck22DoMwZNBZv5oL3dsF2vssH6PuWknaWyJ6h4iRpnijsF5DC/
1W5UYsSuM7GqeTZ9jusX68fJvKS/UO/w1V6osou3PYZNmja/lWmGjUX6jNsdJeHkGuSdTvaE
MyHGKMP6Dq17nNjSVps27OpY43Y7pvBB+hzvfnPFEKfJChVg2mmeiRNiiXrPiHzug9zjGYY6
00w1MTy9S7bPbLZr+eZ+mAwmywrQVTuVbmyV8+iCgtzRmpvP2OeC9rnmZsU+Z9tG0K60XrYK
c5zYfl/ttQc3+zqZPr46w91l5dKvjSlf/g8/3kemf6XESAAAAABJRU5ErkJgglBLAwQUAAAI
AABiaeZOAAAAAAAAAAAAAAAAGgAAAENvbmZpZ3VyYXRpb25zMi90b29scGFuZWwvUEsDBBQA
AAgAAGJp5k4AAAAAAAAAAAAAAAAYAAAAQ29uZmlndXJhdGlvbnMyL21lbnViYXIvUEsDBBQA
AAgAAGJp5k4AAAAAAAAAAAAAAAAaAAAAQ29uZmlndXJhdGlvbnMyL3N0YXR1c2Jhci9QSwME
FAAACAAAYmnmTgAAAAAAAAAAAAAAABgAAABDb25maWd1cmF0aW9uczIvZmxvYXRlci9QSwME
FAAACAAAYmnmTgAAAAAAAAAAAAAAABgAAABDb25maWd1cmF0aW9uczIvdG9vbGJhci9QSwME
FAAACAAAYmnmTgAAAAAAAAAAAAAAAB8AAABDb25maWd1cmF0aW9uczIvaW1hZ2VzL0JpdG1h
cHMvUEsDBBQAAAgAAGJp5k4AAAAAAAAAAAAAAAAcAAAAQ29uZmlndXJhdGlvbnMyL3Byb2dy
ZXNzYmFyL1BLAwQUAAAIAABiaeZOAAAAAAAAAAAAAAAAGgAAAENvbmZpZ3VyYXRpb25zMi9w
b3B1cG1lbnUvUEsDBBQAAAgAAGJp5k4AAAAAAAAAAAAAAAAcAAAAQ29uZmlndXJhdGlvbnMy
L2FjY2VsZXJhdG9yL1BLAwQUAAgICABiaeZOAAAAAAAAAAAAAAAACAAAAG1ldGEueG1sjZPL
btswEEX3/QqBzVYiqadFSArQRVYJUqAu0J0hkxOXLU0aJBU5f189XafxokvNnDv3DklV9+ej
Cl7BOml0jWhEUACaGyH1oUbftw/hBt03nyrz8iI5MGF4dwTtwyP4Nhik2rG5VaPOamZaJx3T
7REc85yZE+hVwq5pNhnNlbOS+neNfnp/Yhj3fR/1SWTsAdOyLPHUXVHBL9yps2qiBMegYHRw
mEYUr+yY8H9Djex1JGPMxWjE59CTXUxIiufvlT5YIdStBQY2wUPC1rfhq4T+MwqW9a8OPEbN
erpjjKaawnALrR+IcBBDExNahqQISb6llFHCaBZt8rQkRZ6TCt9QVIKzD9JslGZ5lGbphhZx
mVZ4xWZXENIPFx+Kzk6zmq9bGj8lxbfF40P/vYy/cQWuSf+hl/LMHkDDIDa2eZR7C8/T5jiP
aJRHyd2j1N1592OT7/I0uAJ2J2t+AfeYkrsvnVQiTBaTv/Pm+ZcX6vwQ0XnJg6nu272CkJtO
+xolaC5yUGqtxUWWLmWzH73WBkG4qfC7K8K3fofmD1BLBwiFBjxznAEAAEwDAABQSwMEFAAI
CAgAYmnmTgAAAAAAAAAAAAAAAAoAAABzdHlsZXMueG1s3Vptb9s2EP6+X2GowLABkyXbbRp7
iYNtXbYBbVC06faxoCVKJkqJAkW/pL9+R1KUqTdHcWKnmx0kEfnc8e65F5GyL662CR2sMc8J
Sy+d0dB3BjgNWEjS+NL5dHvtnjtX8+8uWBSRAM9CFqwSnAo3F3cU5wMQTvOZnrx0VjydMZST
fJaiBOczEcxYhlMjNLPRM7WUHlHK+oorsC0t8Fb0FZbYiixa9F9ZgW3pkKNNX2GJBU5t8Yj1
Fd7m1I2YG7AkQ4LUrNhSkn65dJZCZDPP22w2w81kyHjsjabTqadmS4ODEpetOFWoMPAwxXKx
3BsNR57BJligvvZJrG1SukoWmPemBgnUiGrGcQ4QcFfmZT9Ftkwlv9Zx7+xaxx00B0vEe+eZ
AldTZRL2T5VJaMsmSCw74nvuvYNJ9evd211e8aTvWhJboSrgJOvtpkbb8oyx0lQpoItdmTv2
/ZeevrbQm73wDScCcwse7IUHiAYl4yxpIw1wIw8QLl7LlDdoLp3u1PzK4zhjXJSGRP2bHbAz
Lkt1KRLaXapy1kBjHoatUDBn4kHZQtG4a4I3Lyq9bD//U0+B7P61V2DkexJTlgDQu2u2PC5v
BxFbpaEuO00G3maYEzmFqBKbVTTYGUPZASqLe4ilodJYCaam2kqXWtUw5ia5S1JIMpbNLOlK
5ef5RLSF4vaDJ+dceVeBvlmsY91Mx87c3DkjBnfNCAXYDXFA8/mF7njl8EBfSyMvnbcE2qdy
fvARpZCk0JYMNCH07tL5HmUs/7mG04POoKJa4t0Yp+A1FE++IXleQWREBNBe1ogTlRveftPe
cEZCtdrgDV6jFMUg2GlgK7qPmXe5wMkT2XmNKF2g4EsfKw32yWz0uuJfjOuNlPElxBFa0WJ7
ZTQXtqrSdQNMqWPgGeIo5ihbuhnkL+aCwJ5MTwEatLDMDUkuUCq3Z5OzTOxok0nbFFNmdiRh
xGYUpfEKxTCLUzUQQJkKDtZ9+ujUVbjQHVHaFQqFNfoM9OvSzBSKzcRvN0318j5N8bY7J6tL
lPAlqS9STv11o2LWEoz5hd7UFHubSoQ0Xze+UwMNiquEpKrHxCAXkpiIHJqDWqhFp4lPc4E3
2h6nOy+8buE/MZJ7+j3CgzKnyg2+W124K3VUHlAGG74Xvnqp1FCByslXUDB+CZm3G9Pb/RS2
HojuhjeYxEvoswtGQysK95BS+PV57H8eGRcg5TOK7twKYjA6yHfD28G+j84f4Hsxfoj34/3e
j5/J+/ERvb8Fax6V0Hvq5YYJfJBuZVTJVylRY002wZjLPY5rCIzgFQS7KKKYpYi6C+oKLrlJ
cWNOwHQ5J7UyHspzlz98DRU3yBmFvvji3Jfv7s5vBXGiXo0g+kcM4jVjIn0asrv9Kijo4xeB
jSEJnqA078AOffg+omOy5jB+UMAsnZB+ykZsMCpjOiAbEspzKFoJ1oHoMupQCj/CUX6VH6vC
/2AsPEh3YdYhNR4Eqsb7lKLvn5219NNjluINXgm+S5FTkVI0vj6kTKcnJ+VXdPIskXT0JCQI
Wu+6xyTkH8TTQ3eSdVK+Kcd+55zx03cE5WefWEfq9ShKHra3/iUIwNPnOTIc3a09JwYNOPDA
UJB2SC4UTJwoFx5aHjvaOo4aBW2HnTQeQdsD9rfPSttkL22TU9MWqld/R96TNVN+3MpF5D/X
8klpu1MKPFDIQQX21Lu4plW/IYFjxu/uN6yOfFyHa6Xeykg4VsSpm7MVlw/jIrI1yjnOMBIQ
l1TAIjCFaG4932x9yAch3SmFfbtAXDwmlH8jusL3M1aBnSCUjKeY9whkBXeoXV7tkWxxKQ88
CRIkcM2EURZjF+xhK1Fx412WjJwWUDMt5KdbsJNyExaCHOWuWOz4WWIU1p8JFmMRHJnhTzUd
5EPGZdGjxqPh+KzoaQniMUxRHMGEXx3kGl4bXTAh5Idnr4f+NLOzqs2kwpZnMFOwrMXGqj1e
Iwo9ojf+P0XPfiw0Hr6c7h4LWfuuDIX6Cx/+8NXICLXsVX35LtmxECQBphqJ0vD0P5BJ3yZf
92e119mtiokE5aWKsocVg1LTvo8a7DpoaXLa/PmF+nZNVvzNlxhr9Pzq6urCqw8WI1mNhFrk
ZRSr7b9xf9TclKu/l74UF9Js/dnKfGTWs8YaJhhVFdL3muA1eLyP2g/FNwn2MDtuMGs2CjFh
OrMfSrYe8OY/6P8EEdSG6usfG4RUVqwMqUqqWRHCfsowJb+eYN9nb+BYUILctdxHQIH5o6nr
v3b9M2fu+5768f3CCgmc/zQwBoMXvj9TP6XRbWlUte/5cssw7tkC6gPH+XRqC+ixI+Wi1179
Xvv3+eb/AlBLBwhpoa9Q3AYAAA8oAABQSwMEFAAICAgAYmnmTgAAAAAAAAAAAAAAAAwAAABt
YW5pZmVzdC5yZGbNk81ugzAQhO88hWXO2EAvBQVyKMq5ap/ANYZYBS/ymhLevo6TVlGkquqf
1OOuRjPfjrSb7WEcyIuyqMFUNGMpJcpIaLXpKzq7Lrml2zra2LYrH5od8WqDpZ8qunduKjlf
loUtNwxsz7OiKHia8zxPvCLB1ThxSAzGtI4ICR6NQmn15HwaOc7iCWZXUXTroJB59yA9i906
qaCyCmG2Ur2HtiCRgUCNCUzKhHSDHLpOS8UzlvNROcGh7eLHYL3Tg6I8YPArjs/Y3ogMpuVe
4L2w7lyD33yVaHruY3p108Xx3yOUYJwy7k/quzt5/+f+Ls//GeKvtHZEbEDOo2f6kOe08h9V
R69QSwcItPdo0gUBAACDAwAAUEsDBBQACAgIAGJp5k4AAAAAAAAAAAAAAAAMAAAAc2V0dGlu
Z3MueG1s7VpRc6JIEH6/X2Hxbowm60YqumXc5Mytt7HE5O72bYRWpzJMUzOD6P76G0BzkUBi
EFK3VTxZAvN109PTX38zXH5Zu6y2AiEp8q7RPDk1asBtdChfdI376U39wvjS++0S53Nqg+mg
7bvAVV2CUvoRWdPDuTTj213DF9xEIqk0OXFBmso20QO+G2Y+f9qMjMVX1ozyx66xVMozG40g
CE6CsxMUi0az0+k0oru7R23kc7o41FT89HNTiPhkKBwQOxMZa52enjfi/0Zt6+Sz0LSM3i4O
u9fvXW4NxD91qsANY1PbXg5d6xrapLmiEDxFzUgbtz/mgUo6Y9AXQKboGbubauPpm5Qro3d6
2XgJ8i7gEcxVOch/UUct06A/NTuti6Phh0AXy1TXm52L9sH4dZd4dcodWIOTtAVB+ixFY3R+
ic0hHkNw6yTclEroFDB6YUI03+VpCJrwc0p0RN5ydH+Ija5HBJXID8jCgS8kijFKqvQi+Ds1
4uf5pnMf+p9U6JyJOERBfyJXhFkeo+pPdCA5CUsURyQ6CEXtstAT3u8iVORCfe5/Cfh9W9EV
ROgTwhcZ4WnlA9/5W3Dx2sFOsirLkbjFFvEd6hUqhW6BwD8Q3alGKTSjQ9AHwvwk6naR540B
WUBYYV9Fb+cEt5YY/C5osnjPEBkQXTmV8CHn4uC2DiY4U1irO91dzBkGI1gQe5Nla06YzDCW
cvF1gspiBZdIBaIYRuiURgitnNAVIVSEUBFCRQgVIRxMCB5R9hKc/zsjNNsVI1SMUDFCxQgV
I+RihKzb0WbToes42obK2Of6b7/p6FpnC2RsRkTm7uLZ5/ZZldTHJnVo4EoAeRwLCHcpcyTd
IWZ+gMDIf1n8Cgrxv6MqC7qcZR+iDpChSE2XVvus1fqUs93Zm9YSgjIkUnvuu3yCwRCIA6Ic
I9YSQOlqUwL6rbzzFaMcrI07QyYtSHJ8IUaijB/qDoKFXYSukNc8LJ6Z6XTEIruVFifeFCfx
3k7xBmJg/U7xyUxpFiYgdXJln0C0Puesdkn4dIlxJLzlzxy6ojLT/YLA053PmzoxfH9NpbXR
7YhATn9mZ+mv3etsjwDTH5CgDj/ljS/4goTZ9J7j3j7T7z3WHZP6A2cDwm1gJVAXWcFDfJ59
xwcMZRlL9lZ+A8H7khI+9rmtfJIiHYswNFgSQWydpAPdYQqQ4WsV3sL1PY9t7iWIr0SR4mck
mnG9ikH5Se03IxLa51eUE7ExGgdDfdd/0jvxVIzsI/AbFDPqOMCf4nz8gfhIS32fvFT7W6kA
OSXCAP3QfgbqvZVzC0SrFsKTTcBrwdwffwULyke6ociNcM2dV8cfW+7eTH5f4YAw22dEZZaK
/Mmv3+3x3nM0dvYGWU459yH96AiJM9HgyFkesnoLPg7NjdDCFFyvnCkoW4f9wpKmPJ23rdRj
4oEIpze1+hfCxB+iaL5uv7izNE+UomNKFPPX7gycG+QqE/sIv8vVq6X2/5WyeyvrP0RalyQd
i1N3mYKp8eJD2UbWJ8S9fwFQSwcIcf3e+XkEAACELAAAUEsDBBQACAgIAGJp5k4AAAAAAAAA
AAAAAAAVAAAATUVUQS1JTkYvbWFuaWZlc3QueG1srZNBbsMgEEX3OYXFtjK0WVUoThaReoL0
ANQMDhIeEAxRcvtiK05cVZFiKTtghv/ffMRmd+5ddYKYrMeGffB3VgG2XlvsGvZ9+Ko/2W67
2vQKrYFEclpU5R6m27ZhOaL0KtkkUfWQJLXSB0Dt29wDkvzbL0en224GsGZXaefhPOnGTk5C
xmfUikr31QjOAaIdSspJb4xtQc4URqftqrqPYKyDurTHyx3AZOfqoOjYMPGQ6x4CaKtqugRo
mArB2XYEEifUfMyAz0fnKURQOh0BiIklKIdj7n9QWZcETUsesHuAYnvVgRjqi1z2Ho3tchxn
SGvx5KApIy8582x5O1dYZl6u0pBRUXrgS+URxVBepNsDqZeLJro4SK9nvZ7xqM0T4Zeut+Xo
QFT+9LPwG/Hvx29/AVBLBwgVOQGtMwEAACwEAABQSwMEFAAICAgAYmnmTgAAAAAAAAAAAAAA
AAsAAABjb250ZW50LnhtbOy9W3NdR64m+D6/osInet60lPeLp6r6oU1SvN+lieiYOMGyaJei
ZdFB0XX59wOstS3DMrc3AGXmplSu060qWxCV30okEvgSlz//z3/98PZP/7i9f//m7t1fvrKT
+epPt+++vXv95t33f/nq+mr3Wfnqf/71//rz3Xffvfn29uvXd9/+9MPtu4dn3969e4D//hP8
6Xfvv15+9y9f/XT/7uu7m/dv3n/97uaH2/dfP3z79d2Pt+9+/lNfU+mv579r+TfvH/79lv3H
Z2H6px9u//XA/cMo+6s/e/M3/t88C9M//fr+5p/cP4yy8FHpH//ujvuH//X+7bPv7uCr//Dj
zcObj1bxr7dv3v2fv3z194eHH79+/vyf//zn9E8/3d1//9zWWp/Pv/thwd9+kPvxp/u3s9Tr
b5/fvr3Fv+z9czvZ5z/L/nD7cMNdH8rSJb376Ye/3d6zP83Nw81vdvXH+9v3IAJwUTF5P4j+
mV/p1z++Z2vXP75f85m//fvNPVvPZuFfq4p/zVcV/5r+2R9uHv6+Zn/L82P4zfmX46Nf9Or+
B+7fhbK/+lTf3r/5kQ1zkaZ//u7u7sNS8Q8sh31erjMmPF/+mUj/83fF/3n/5uH2noh/+7vi
3968/fbDF7/74bGPBnL2OUg8u/0HqvyHQ4Qf4v2aP+CeL7/9Qfj967U/+v89Prr89u+3P9z8
Ivxms/CzN+/eP9y8++XL3OMmrEUan9/f/nh3//Dhw3zHN76wW+7D2v7+8MPb9aYDf/dn0e/v
X79+VBSW45+DGYFD/Owfb27/+V+/sq2/rw/1+SxE7env/gFrnqPMhyMJ2/2L8b///sP99N3d
T+9eL2Zg+Ri3//rx9v4N/tbN2/mPff2rn0A1+O2d4keu7jTyE35l6N/cvv359H+A9OiPubt7
9sN7UAZQ+rsfvyZ/+tf3xv0P/+L9OFT0u9ffffwTPzr0375/7x8e29uri+f4e8/w2oSLYfU3
EXfBffXXn32DxRa8f/7hX3wHPsKz726+vX32+vbbt+//+ufFxn/4139a/hnX/Zevjt7AhTF/
3j9d3ryDkwaG+GfRH968/fdfvvq/b368e///fCS3/Muv/vSrH43yz76/fQcfAszF+3++ef/+
VxI/vnn4FgzqP27u38za9/z3l/bN/d2b1/Pf9qdvbv9x8+7me/iDaxf4qDRnmf9+/3D7Q6N1
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
bFBLBQYAAAAAEQARAGUEAAAYvgAAAAA=

--3wvarhewrzuvoyhd--





^ permalink  raw  reply  [nested|flat] 3+ messages in thread

* [PATCH v8 3/4] Convert pg_restore's ready_list to a priority queue.
@ 2023-07-20 17:19  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 3+ messages in thread

From: Nathan Bossart @ 2023-07-20 17:19 UTC (permalink / raw)

Presently, we spend a lot of time sorting this list so that we pick
the largest items first.  With many tables, this sorting can become
a significant bottleneck.  There are a couple of reports from the
field about this, and it is easily reproducible, so this is not a
hypothetical issue.

This commit improves the performance of pg_restore with many tables
by converting its ready_list to a priority queue, i.e., a binary
heap.  We will first try to run the highest priority item, but if
it cannot be chosen due to the lock heuristic, we'll do a
sequential scan through the heap nodes until we find one that is
runnable.  This means that we might end up picking an item with
much lower priority, but since we expect that we'll typically be
able to choose one of the first few nodes, we should usually pick
an item with a relatively high priority.

On my machine, a basic test with 100,000 tables takes 11.5 minutes
without this patch and 1.5 minutes with it.  Pierre Ducroquet
claims to see a speedup from 30 minutes to 23 minutes for a
real-world dump of over 50,000 tables.

Suggested-by: Tom Lane
Tested-by: Pierre Ducroquet
Reviewed-by: Tom Lane
Discussion: https://postgr.es/m/3612876.1689443232%40sss.pgh.pa.us
---
 src/bin/pg_dump/pg_backup_archiver.c | 197 ++++++++-------------------
 1 file changed, 57 insertions(+), 140 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 4d83381d84..95b8f69ecf 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -34,6 +34,7 @@
 #include "compress_io.h"
 #include "dumputils.h"
 #include "fe_utils/string_utils.h"
+#include "lib/binaryheap.h"
 #include "lib/stringinfo.h"
 #include "libpq/libpq-fs.h"
 #include "parallel.h"
@@ -44,24 +45,6 @@
 #define TEXT_DUMP_HEADER "--\n-- PostgreSQL database dump\n--\n\n"
 #define TEXT_DUMPALL_HEADER "--\n-- PostgreSQL database cluster dump\n--\n\n"
 
-/*
- * State for tracking TocEntrys that are ready to process during a parallel
- * restore.  (This used to be a list, and we still call it that, though now
- * it's really an array so that we can apply qsort to it.)
- *
- * tes[] is sized large enough that we can't overrun it.
- * The valid entries are indexed first_te .. last_te inclusive.
- * We periodically sort the array to bring larger-by-dataLength entries to
- * the front; "sorted" is true if the valid entries are known sorted.
- */
-typedef struct _parallelReadyList
-{
-	TocEntry  **tes;			/* Ready-to-dump TocEntrys */
-	int			first_te;		/* index of first valid entry in tes[] */
-	int			last_te;		/* index of last valid entry in tes[] */
-	bool		sorted;			/* are valid entries currently sorted? */
-} ParallelReadyList;
-
 
 static ArchiveHandle *_allocAH(const char *FileSpec, const ArchiveFormat fmt,
 							   const pg_compress_specification compression_spec,
@@ -111,16 +94,12 @@ static void restore_toc_entries_postfork(ArchiveHandle *AH,
 static void pending_list_header_init(TocEntry *l);
 static void pending_list_append(TocEntry *l, TocEntry *te);
 static void pending_list_remove(TocEntry *te);
-static void ready_list_init(ParallelReadyList *ready_list, int tocCount);
-static void ready_list_free(ParallelReadyList *ready_list);
-static void ready_list_insert(ParallelReadyList *ready_list, TocEntry *te);
-static void ready_list_remove(ParallelReadyList *ready_list, int i);
-static void ready_list_sort(ParallelReadyList *ready_list);
-static int	TocEntrySizeCompare(const void *p1, const void *p2);
-static void move_to_ready_list(TocEntry *pending_list,
-							   ParallelReadyList *ready_list,
+static int	TocEntrySizeCompareQsort(const void *p1, const void *p2);
+static int	TocEntrySizeCompareBinaryheap(void *p1, void *p2, void *arg);
+static void move_to_ready_heap(TocEntry *pending_list,
+							   binaryheap *ready_heap,
 							   RestorePass pass);
-static TocEntry *pop_next_work_item(ParallelReadyList *ready_list,
+static TocEntry *pop_next_work_item(binaryheap *ready_heap,
 									ParallelState *pstate);
 static void mark_dump_job_done(ArchiveHandle *AH,
 							   TocEntry *te,
@@ -135,7 +114,7 @@ static bool has_lock_conflicts(TocEntry *te1, TocEntry *te2);
 static void repoint_table_dependencies(ArchiveHandle *AH);
 static void identify_locking_dependencies(ArchiveHandle *AH, TocEntry *te);
 static void reduce_dependencies(ArchiveHandle *AH, TocEntry *te,
-								ParallelReadyList *ready_list);
+								binaryheap *ready_heap);
 static void mark_create_done(ArchiveHandle *AH, TocEntry *te);
 static void inhibit_data_for_failed_table(ArchiveHandle *AH, TocEntry *te);
 
@@ -2384,7 +2363,7 @@ WriteDataChunks(ArchiveHandle *AH, ParallelState *pstate)
 		}
 
 		if (ntes > 1)
-			qsort(tes, ntes, sizeof(TocEntry *), TocEntrySizeCompare);
+			qsort(tes, ntes, sizeof(TocEntry *), TocEntrySizeCompareQsort);
 
 		for (int i = 0; i < ntes; i++)
 			DispatchJobForTocEntry(AH, pstate, tes[i], ACT_DUMP,
@@ -3984,7 +3963,7 @@ restore_toc_entries_prefork(ArchiveHandle *AH, TocEntry *pending_list)
 
 			(void) restore_toc_entry(AH, next_work_item, false);
 
-			/* Reduce dependencies, but don't move anything to ready_list */
+			/* Reduce dependencies, but don't move anything to ready_heap */
 			reduce_dependencies(AH, next_work_item, NULL);
 		}
 		else
@@ -4027,24 +4006,26 @@ static void
 restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 							 TocEntry *pending_list)
 {
-	ParallelReadyList ready_list;
+	binaryheap *ready_heap;
 	TocEntry   *next_work_item;
 
 	pg_log_debug("entering restore_toc_entries_parallel");
 
-	/* Set up ready_list with enough room for all known TocEntrys */
-	ready_list_init(&ready_list, AH->tocCount);
+	/* Set up ready_heap with enough room for all known TocEntrys */
+	ready_heap = binaryheap_allocate(AH->tocCount,
+									 TocEntrySizeCompareBinaryheap,
+									 NULL);
 
 	/*
 	 * The pending_list contains all items that we need to restore.  Move all
-	 * items that are available to process immediately into the ready_list.
+	 * items that are available to process immediately into the ready_heap.
 	 * After this setup, the pending list is everything that needs to be done
-	 * but is blocked by one or more dependencies, while the ready list
+	 * but is blocked by one or more dependencies, while the ready heap
 	 * contains items that have no remaining dependencies and are OK to
 	 * process in the current restore pass.
 	 */
 	AH->restorePass = RESTORE_PASS_MAIN;
-	move_to_ready_list(pending_list, &ready_list, AH->restorePass);
+	move_to_ready_heap(pending_list, ready_heap, AH->restorePass);
 
 	/*
 	 * main parent loop
@@ -4058,7 +4039,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 	for (;;)
 	{
 		/* Look for an item ready to be dispatched to a worker */
-		next_work_item = pop_next_work_item(&ready_list, pstate);
+		next_work_item = pop_next_work_item(ready_heap, pstate);
 		if (next_work_item != NULL)
 		{
 			/* If not to be restored, don't waste time launching a worker */
@@ -4068,7 +4049,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 							next_work_item->dumpId,
 							next_work_item->desc, next_work_item->tag);
 				/* Update its dependencies as though we'd completed it */
-				reduce_dependencies(AH, next_work_item, &ready_list);
+				reduce_dependencies(AH, next_work_item, ready_heap);
 				/* Loop around to see if anything else can be dispatched */
 				continue;
 			}
@@ -4079,7 +4060,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 
 			/* Dispatch to some worker */
 			DispatchJobForTocEntry(AH, pstate, next_work_item, ACT_RESTORE,
-								   mark_restore_job_done, &ready_list);
+								   mark_restore_job_done, ready_heap);
 		}
 		else if (IsEveryWorkerIdle(pstate))
 		{
@@ -4093,7 +4074,7 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 			/* Advance to next restore pass */
 			AH->restorePass++;
 			/* That probably allows some stuff to be made ready */
-			move_to_ready_list(pending_list, &ready_list, AH->restorePass);
+			move_to_ready_heap(pending_list, ready_heap, AH->restorePass);
 			/* Loop around to see if anything's now ready */
 			continue;
 		}
@@ -4122,10 +4103,10 @@ restore_toc_entries_parallel(ArchiveHandle *AH, ParallelState *pstate,
 					   next_work_item ? WFW_ONE_IDLE : WFW_GOT_STATUS);
 	}
 
-	/* There should now be nothing in ready_list. */
-	Assert(ready_list.first_te > ready_list.last_te);
+	/* There should now be nothing in ready_heap. */
+	Assert(binaryheap_empty(ready_heap));
 
-	ready_list_free(&ready_list);
+	binaryheap_free(ready_heap);
 
 	pg_log_info("finished main parallel loop");
 }
@@ -4225,80 +4206,9 @@ pending_list_remove(TocEntry *te)
 }
 
 
-/*
- * Initialize the ready_list with enough room for up to tocCount entries.
- */
-static void
-ready_list_init(ParallelReadyList *ready_list, int tocCount)
-{
-	ready_list->tes = (TocEntry **)
-		pg_malloc(tocCount * sizeof(TocEntry *));
-	ready_list->first_te = 0;
-	ready_list->last_te = -1;
-	ready_list->sorted = false;
-}
-
-/*
- * Free storage for a ready_list.
- */
-static void
-ready_list_free(ParallelReadyList *ready_list)
-{
-	pg_free(ready_list->tes);
-}
-
-/* Add te to the ready_list */
-static void
-ready_list_insert(ParallelReadyList *ready_list, TocEntry *te)
-{
-	ready_list->tes[++ready_list->last_te] = te;
-	/* List is (probably) not sorted anymore. */
-	ready_list->sorted = false;
-}
-
-/* Remove the i'th entry in the ready_list */
-static void
-ready_list_remove(ParallelReadyList *ready_list, int i)
-{
-	int			f = ready_list->first_te;
-
-	Assert(i >= f && i <= ready_list->last_te);
-
-	/*
-	 * In the typical case where the item to be removed is the first ready
-	 * entry, we need only increment first_te to remove it.  Otherwise, move
-	 * the entries before it to compact the list.  (This preserves sortedness,
-	 * if any.)  We could alternatively move the entries after i, but there
-	 * are typically many more of those.
-	 */
-	if (i > f)
-	{
-		TocEntry  **first_te_ptr = &ready_list->tes[f];
-
-		memmove(first_te_ptr + 1, first_te_ptr, (i - f) * sizeof(TocEntry *));
-	}
-	ready_list->first_te++;
-}
-
-/* Sort the ready_list into the desired order */
-static void
-ready_list_sort(ParallelReadyList *ready_list)
-{
-	if (!ready_list->sorted)
-	{
-		int			n = ready_list->last_te - ready_list->first_te + 1;
-
-		if (n > 1)
-			qsort(ready_list->tes + ready_list->first_te, n,
-				  sizeof(TocEntry *),
-				  TocEntrySizeCompare);
-		ready_list->sorted = true;
-	}
-}
-
 /* qsort comparator for sorting TocEntries by dataLength */
 static int
-TocEntrySizeCompare(const void *p1, const void *p2)
+TocEntrySizeCompareQsort(const void *p1, const void *p2)
 {
 	const TocEntry *te1 = *(const TocEntry *const *) p1;
 	const TocEntry *te2 = *(const TocEntry *const *) p2;
@@ -4318,17 +4228,24 @@ TocEntrySizeCompare(const void *p1, const void *p2)
 	return 0;
 }
 
+/* binaryheap comparator for sorting TocEntries by dataLength */
+static int
+TocEntrySizeCompareBinaryheap(void *p1, void *p2, void *arg)
+{
+	return TocEntrySizeCompareQsort(p1, p2);
+}
+
 
 /*
- * Move all immediately-ready items from pending_list to ready_list.
+ * Move all immediately-ready items from pending_list to ready_heap.
  *
  * Items are considered ready if they have no remaining dependencies and
  * they belong in the current restore pass.  (See also reduce_dependencies,
  * which applies the same logic one-at-a-time.)
  */
 static void
-move_to_ready_list(TocEntry *pending_list,
-				   ParallelReadyList *ready_list,
+move_to_ready_heap(TocEntry *pending_list,
+				   binaryheap *ready_heap,
 				   RestorePass pass)
 {
 	TocEntry   *te;
@@ -4344,38 +4261,38 @@ move_to_ready_list(TocEntry *pending_list,
 		{
 			/* Remove it from pending_list ... */
 			pending_list_remove(te);
-			/* ... and add to ready_list */
-			ready_list_insert(ready_list, te);
+			/* ... and add to ready_heap */
+			binaryheap_add(ready_heap, te);
 		}
 	}
 }
 
 /*
  * Find the next work item (if any) that is capable of being run now,
- * and remove it from the ready_list.
+ * and remove it from the ready_heap.
  *
  * Returns the item, or NULL if nothing is runnable.
  *
  * To qualify, the item must have no remaining dependencies
  * and no requirements for locks that are incompatible with
- * items currently running.  Items in the ready_list are known to have
+ * items currently running.  Items in the ready_heap are known to have
  * no remaining dependencies, but we have to check for lock conflicts.
  */
 static TocEntry *
-pop_next_work_item(ParallelReadyList *ready_list,
+pop_next_work_item(binaryheap *ready_heap,
 				   ParallelState *pstate)
 {
 	/*
-	 * Sort the ready_list so that we'll tackle larger jobs first.
-	 */
-	ready_list_sort(ready_list);
-
-	/*
-	 * Search the ready_list until we find a suitable item.
+	 * Search the ready_heap until we find a suitable item.  Note that we do a
+	 * sequential scan through the heap nodes, so even though we will first
+	 * try to choose the highest-priority item, we might end up picking
+	 * something with a much lower priority.  However, it is expected that we
+	 * will typically be able to pick one of the first few items, which should
+	 * usually have a relatively high priority.
 	 */
-	for (int i = ready_list->first_te; i <= ready_list->last_te; i++)
+	for (int i = 0; i < binaryheap_size(ready_heap); i++)
 	{
-		TocEntry   *te = ready_list->tes[i];
+		TocEntry   *te = (TocEntry *) binaryheap_get_node(ready_heap, i);
 		bool		conflicts = false;
 
 		/*
@@ -4401,7 +4318,7 @@ pop_next_work_item(ParallelReadyList *ready_list,
 			continue;
 
 		/* passed all tests, so this item can run */
-		ready_list_remove(ready_list, i);
+		binaryheap_remove_node(ready_heap, i);
 		return te;
 	}
 
@@ -4447,7 +4364,7 @@ mark_restore_job_done(ArchiveHandle *AH,
 					  int status,
 					  void *callback_data)
 {
-	ParallelReadyList *ready_list = (ParallelReadyList *) callback_data;
+	binaryheap *ready_heap = (binaryheap *) callback_data;
 
 	pg_log_info("finished item %d %s %s",
 				te->dumpId, te->desc, te->tag);
@@ -4465,7 +4382,7 @@ mark_restore_job_done(ArchiveHandle *AH,
 		pg_fatal("worker process failed: exit code %d",
 				 status);
 
-	reduce_dependencies(AH, te, ready_list);
+	reduce_dependencies(AH, te, ready_heap);
 }
 
 
@@ -4708,11 +4625,11 @@ identify_locking_dependencies(ArchiveHandle *AH, TocEntry *te)
 /*
  * Remove the specified TOC entry from the depCounts of items that depend on
  * it, thereby possibly making them ready-to-run.  Any pending item that
- * becomes ready should be moved to the ready_list, if that's provided.
+ * becomes ready should be moved to the ready_heap, if that's provided.
  */
 static void
 reduce_dependencies(ArchiveHandle *AH, TocEntry *te,
-					ParallelReadyList *ready_list)
+					binaryheap *ready_heap)
 {
 	int			i;
 
@@ -4730,18 +4647,18 @@ reduce_dependencies(ArchiveHandle *AH, TocEntry *te,
 		 * the current restore pass, and it is currently a member of the
 		 * pending list (that check is needed to prevent double restore in
 		 * some cases where a list-file forces out-of-order restoring).
-		 * However, if ready_list == NULL then caller doesn't want any list
+		 * However, if ready_heap == NULL then caller doesn't want any list
 		 * memberships changed.
 		 */
 		if (otherte->depCount == 0 &&
 			_tocEntryRestorePass(otherte) == AH->restorePass &&
 			otherte->pending_prev != NULL &&
-			ready_list != NULL)
+			ready_heap != NULL)
 		{
 			/* Remove it from pending list ... */
 			pending_list_remove(otherte);
-			/* ... and add to ready_list */
-			ready_list_insert(ready_list, otherte);
+			/* ... and add to ready_heap */
+			binaryheap_add(ready_heap, otherte);
 		}
 	}
 }
-- 
2.25.1


--gBBFr7Ir9EOA20Yy
Content-Type: text/x-diff; charset=us-ascii
Content-Disposition: attachment;
	filename="v8-0004-Remove-open-coded-binary-heap-in-pg_dump_sort.c.patch"



^ permalink  raw  reply  [nested|flat] 3+ messages in thread

* [PATCH 4/6] Add CONCURRENTLY option to REPACK command.
@ 2026-01-08 16:47  Antonin Houska <[email protected]>
  0 siblings, 0 replies; 3+ messages in thread

From: Antonin Houska @ 2026-01-08 16:47 UTC (permalink / raw)

The REPACK command copies the relation data into a new file, creates new
indexes and eventually swaps the files. To make sure that the old file does
not change during the copying, the relation is locked in an exclusive mode,
which prevents applications from both reading and writing. (To keep the data
consistent, we'd only need to prevent the applications from writing, but even
reading needs to be blocked before we can swap the files - otherwise some
applications could continue using the old file. Since we should not request a
stronger lock without releasing the weaker one first, we acquire the exclusive
lock in the beginning and keep it till the end of the processing.)

This patch introduces an alternative workflow, which only requires the
exclusive lock when the relation (and index) files are being swapped.
(Supposedly, the swapping should be pretty fast.) On the other hand, when we
copy the data to the new file, we allow applications to read from the relation
and even to write to it.

First, we scan the relation using a "historic snapshot", and insert all the
tuples satisfying this snapshot into the new file.

Second, logical decoding is used to capture the data changes done by
applications during the copying (i.e. changes that do not satisfy the historic
snapshot mentioned above), and those are applied to the new file before we
acquire the exclusive lock that we need to swap the files. (Of course, more
data changes can take place while we are waiting for the lock - these will be
applied to the new file after we have acquired the lock, before we swap the
files.)

Since the logical decoding system, during its startup, waits until all the
transactions which already have XID assigned have finished, there is a risk of
deadlock if a transaction that already changed anything in the database tries
to acquire a conflicting lock on the table REPACK CONCURRENTLY is working
on. As an example, consider transaction running CREATE INDEX command on the
table that is being REPACKed CONCURRENTLY. On the other hand, DML commands
(INSERT, UPDATE, DELETE) are not a problem as their lock does not conflict
with REPACK CONCURRENTLY.

The current approach is that we accept the risk. If we tried to avoid it, it'd
be necessary to unlock the table before the logical decoding is setup and lock
it again afterwards. Such temporary unlocking would imply re-checking if the
table still meets all the requirements for REPACK CONCURRENTLY.

Like the existing implementation of REPACK, the variant with the CONCURRENTLY
option also requires an extra space for the new relation and index files
(which coexist with the old files for some time). In addition, the
CONCURRENTLY option might introduce a lag in releasing WAL segments for
archiving / recycling. This is due to the decoding of the data changes done by
applications concurrently. When copying the table contents into the new file,
we check the lag periodically. If it exceeds the size of a WAL segment, we
decode all the available WAL before resuming the copying. (Of course, the
changes are not applied until the whole table contents is copied.) A
background worker might be a better approach for the decoding - let's consider
implementing it in the future.

The WAL records produced by running DML commands on the new relation do not
contain enough information to be processed by the logical decoding system. All
we need from the new relation is the file (relfilenode), while the actual
relation is eventually dropped. Thus there is no point in replaying the DMLs
anywhere.
---
 doc/src/sgml/monitoring.sgml                  |   37 +-
 doc/src/sgml/mvcc.sgml                        |   12 +-
 doc/src/sgml/ref/repack.sgml                  |  129 +-
 src/Makefile                                  |    1 +
 src/backend/access/heap/heapam.c              |   34 +-
 src/backend/access/heap/heapam_handler.c      |  227 ++-
 src/backend/access/heap/rewriteheap.c         |    6 +-
 src/backend/catalog/system_views.sql          |   19 +-
 src/backend/commands/cluster.c                | 1647 +++++++++++++++--
 src/backend/commands/matview.c                |    2 +-
 src/backend/commands/tablecmds.c              |    1 +
 src/backend/commands/vacuum.c                 |   12 +-
 src/backend/meson.build                       |    1 +
 src/backend/replication/logical/decode.c      |   40 +-
 src/backend/replication/logical/snapbuild.c   |   21 +
 .../replication/pgoutput_repack/Makefile      |   32 +
 .../replication/pgoutput_repack/meson.build   |   18 +
 .../pgoutput_repack/pgoutput_repack.c         |  240 +++
 .../storage/lmgr/generate-lwlocknames.pl      |    2 +-
 src/backend/utils/time/snapmgr.c              |    3 +-
 src/bin/psql/tab-complete.in.c                |    4 +-
 src/include/access/heapam.h                   |    5 +-
 src/include/access/heapam_xlog.h              |    2 +
 src/include/access/tableam.h                  |   10 +
 src/include/commands/cluster.h                |   88 +-
 src/include/commands/progress.h               |   17 +-
 src/include/replication/snapbuild.h           |    1 +
 src/include/storage/lockdefs.h                |    4 +-
 src/include/utils/snapmgr.h                   |    2 +
 src/test/modules/injection_points/Makefile    |    3 +
 .../injection_points/expected/repack.out      |  113 ++
 .../expected/repack_toast.out                 |   64 +
 .../modules/injection_points/logical.conf     |    1 +
 src/test/modules/injection_points/meson.build |    4 +
 .../injection_points/specs/repack.spec        |  142 ++
 .../injection_points/specs/repack_toast.spec  |  105 ++
 src/test/regress/expected/rules.out           |   19 +-
 src/tools/pgindent/typedefs.list              |    5 +
 38 files changed, 2834 insertions(+), 239 deletions(-)
 create mode 100644 src/backend/replication/pgoutput_repack/Makefile
 create mode 100644 src/backend/replication/pgoutput_repack/meson.build
 create mode 100644 src/backend/replication/pgoutput_repack/pgoutput_repack.c
 create mode 100644 src/test/modules/injection_points/expected/repack.out
 create mode 100644 src/test/modules/injection_points/expected/repack_toast.out
 create mode 100644 src/test/modules/injection_points/logical.conf
 create mode 100644 src/test/modules/injection_points/specs/repack.spec
 create mode 100644 src/test/modules/injection_points/specs/repack_toast.spec

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b07fe3294cd..ae56b09aeba 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -6202,14 +6202,35 @@ FROM pg_stat_get_backend_idset() AS backendid;
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>heap_tuples_written</structfield> <type>bigint</type>
+       <structfield>heap_tuples_inserted</structfield> <type>bigint</type>
       </para>
       <para>
-       Number of heap tuples written.
+       Number of heap tuples inserted.
        This counter only advances when the phase is
        <literal>seq scanning heap</literal>,
-       <literal>index scanning heap</literal>
-       or <literal>writing new heap</literal>.
+       <literal>index scanning heap</literal>,
+       <literal>writing new heap</literal>
+       or <literal>catch-up</literal>.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>heap_tuples_updated</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of heap tuples updated.
+       This counter only advances when the phase is <literal>catch-up</literal>.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>heap_tuples_deleted</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of heap tuples deleted.
+       This counter only advances when the phase is <literal>catch-up</literal>.
       </para></entry>
      </row>
 
@@ -6290,6 +6311,14 @@ FROM pg_stat_get_backend_idset() AS backendid;
        <command>REPACK</command> is currently writing the new heap.
      </entry>
     </row>
+    <row>
+     <entry><literal>catch-up</literal></entry>
+     <entry>
+       <command>REPACK CONCURRENTLY</command> is currently processing the DML
+       commands that other transactions executed during any of the preceding
+       phases.
+     </entry>
+    </row>
     <row>
      <entry><literal>swapping relation files</literal></entry>
      <entry>
diff --git a/doc/src/sgml/mvcc.sgml b/doc/src/sgml/mvcc.sgml
index 049ee75a4ba..0f5c34af542 100644
--- a/doc/src/sgml/mvcc.sgml
+++ b/doc/src/sgml/mvcc.sgml
@@ -1833,15 +1833,17 @@ SELECT pg_advisory_lock(q.id) FROM
    <title>Caveats</title>
 
    <para>
-    Some DDL commands, currently only <link linkend="sql-truncate"><command>TRUNCATE</command></link> and the
-    table-rewriting forms of <link linkend="sql-altertable"><command>ALTER TABLE</command></link>, are not
+    Some commands, currently only <link linkend="sql-truncate"><command>TRUNCATE</command></link>, the
+    table-rewriting forms of <link linkend="sql-altertable"><command>ALTER
+    TABLE</command></link> and <command>REPACK</command> with
+    the <literal>CONCURRENTLY</literal> option, are not
     MVCC-safe.  This means that after the truncation or rewrite commits, the
     table will appear empty to concurrent transactions, if they are using a
-    snapshot taken before the DDL command committed.  This will only be an
+    snapshot taken before the command committed.  This will only be an
     issue for a transaction that did not access the table in question
-    before the DDL command started &mdash; any transaction that has done so
+    before the command started &mdash; any transaction that has done so
     would hold at least an <literal>ACCESS SHARE</literal> table lock,
-    which would block the DDL command until that transaction completes.
+    which would block the truncating or rewriting command until that transaction completes.
     So these commands will not cause any apparent inconsistency in the
     table contents for successive queries on the target table, but they
     could cause visible inconsistency between the contents of the target
diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml
index 61d5c2cdef1..30c43c49069 100644
--- a/doc/src/sgml/ref/repack.sgml
+++ b/doc/src/sgml/ref/repack.sgml
@@ -28,6 +28,7 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
 
     VERBOSE [ <replaceable class="parameter">boolean</replaceable> ]
     ANALYZE [ <replaceable class="parameter">boolean</replaceable> ]
+    CONCURRENTLY [ <replaceable class="parameter">boolean</replaceable> ]
 
 <phrase>and <replaceable class="parameter">table_and_columns</replaceable> is:</phrase>
 
@@ -54,7 +55,8 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
    processes every table and materialized view in the current database that
    the current user has the <literal>MAINTAIN</literal> privilege on. This
    form of <command>REPACK</command> cannot be executed inside a transaction
-   block.
+   block.  Also, this form is not allowed if
+   the <literal>CONCURRENTLY</literal> option is used.
   </para>
 
   <para>
@@ -67,7 +69,8 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
    When a table is being repacked, an <literal>ACCESS EXCLUSIVE</literal> lock
    is acquired on it. This prevents any other database operations (both reads
    and writes) from operating on the table until the <command>REPACK</command>
-   is finished.
+   is finished. If you want to keep the table accessible during the repacking,
+   consider using the <literal>CONCURRENTLY</literal> option.
   </para>
 
   <refsect2 id="sql-repack-notes-on-clustering" xreflabel="Notes on Clustering">
@@ -195,6 +198,128 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>CONCURRENTLY</literal></term>
+    <listitem>
+     <para>
+      Allow other transactions to use the table while it is being repacked.
+     </para>
+
+     <para>
+      Internally, <command>REPACK</command> copies the contents of the table
+      (ignoring dead tuples) into a new file, sorted by the specified index,
+      and also creates a new file for each index. Then it swaps the old and
+      new files for the table and all the indexes, and deletes the old
+      files. The <literal>ACCESS EXCLUSIVE</literal> lock is needed to make
+      sure that the old files do not change during the processing because the
+      changes would get lost due to the swap.
+     </para>
+
+     <para>
+      With the <literal>CONCURRENTLY</literal> option, the <literal>ACCESS
+      EXCLUSIVE</literal> lock is only acquired to swap the table and index
+      files. The data changes that took place during the creation of the new
+      table and index files are captured using logical decoding
+      (<xref linkend="logicaldecoding"/>) and applied before
+      the <literal>ACCESS EXCLUSIVE</literal> lock is requested. Thus the lock
+      is typically held only for the time needed to swap the files, which
+      should be pretty short. However, the time might still be noticeable if
+      too many data changes have been done to the table while
+      <command>REPACK</command> was waiting for the lock: those changes must
+      be processed just before the files are swapped, while the
+      <literal>ACCESS EXCLUSIVE</literal> lock is being held.
+     </para>
+
+     <para>
+      Note that <command>REPACK</command> with the
+      <literal>CONCURRENTLY</literal> option does not try to order the rows
+      inserted into the table after the repacking started. Also
+      note <command>REPACK</command> might fail to complete due to DDL
+      commands executed on the table by other transactions during the
+      repacking.
+     </para>
+
+     <note>
+      <para>
+       In addition to the temporary space requirements explained in
+       <xref linkend="sql-repack-notes-on-resources"/>,
+       the <literal>CONCURRENTLY</literal> option can add to the usage of
+       temporary space a bit more. The reason is that other transactions can
+       perform DML operations which cannot be applied to the new file until
+       <command>REPACK</command> has copied all the tuples from the old
+       file. Thus the tuples inserted into the old file during the copying are
+       also stored separately in a temporary file, so they can eventually be
+       applied to the new file.
+      </para>
+
+      <para>
+       Furthermore, the data changes performed during the copying are
+       extracted from <link linkend="wal">write-ahead log</link> (WAL), and
+       this extraction (decoding) only takes place when certain amount of WAL
+       has been written. Therefore, WAL removal can be delayed by this
+       threshold. Currently the threshold is equal to the value of
+       the <link linkend="guc-wal-segment-size"><varname>wal_segment_size</varname></link>
+       configuration parameter.
+      </para>
+     </note>
+
+     <para>
+      The <literal>CONCURRENTLY</literal> option cannot be used in the
+      following cases:
+
+      <itemizedlist>
+       <listitem>
+        <para>
+          The table is <literal>UNLOGGED</literal>.
+        </para>
+       </listitem>
+
+       <listitem>
+        <para>
+          The table is partitioned.
+        </para>
+       </listitem>
+
+       <listitem>
+        <para>
+          The table is a system catalog or a <acronym>TOAST</acronym> table.
+        </para>
+       </listitem>
+
+       <listitem>
+        <para>
+         <command>REPACK</command> is executed inside a transaction block.
+        </para>
+       </listitem>
+
+       <listitem>
+        <para>
+          The <link linkend="guc-wal-level"><varname>wal_level</varname></link>
+          configuration parameter is less than <literal>logical</literal>.
+        </para>
+       </listitem>
+
+       <listitem>
+        <para>
+         The <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
+         configuration parameter does not allow for creation of an additional
+         replication slot.
+        </para>
+       </listitem>
+      </itemizedlist>
+     </para>
+
+     <warning>
+      <para>
+       <command>REPACK</command> with the <literal>CONCURRENTLY</literal>
+       option is not MVCC-safe, see <xref linkend="mvcc-caveats"/> for
+       details.
+      </para>
+     </warning>
+
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>VERBOSE</literal></term>
     <listitem>
diff --git a/src/Makefile b/src/Makefile
index 2f31a2f20a7..b18c9a14ffa 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -23,6 +23,7 @@ SUBDIRS = \
 	interfaces \
 	backend/replication/libpqwalreceiver \
 	backend/replication/pgoutput \
+	backend/replication/pgoutput_repack \
 	fe_utils \
 	bin \
 	pl \
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index ad9d6338ec2..f1cdd5d1d87 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -60,7 +60,8 @@ static HeapTuple heap_prepare_insert(Relation relation, HeapTuple tup,
 static XLogRecPtr log_heap_update(Relation reln, Buffer oldbuf,
 								  Buffer newbuf, HeapTuple oldtup,
 								  HeapTuple newtup, HeapTuple old_key_tuple,
-								  bool all_visible_cleared, bool new_all_visible_cleared);
+								  bool all_visible_cleared, bool new_all_visible_cleared,
+								  bool walLogical);
 #ifdef USE_ASSERT_CHECKING
 static void check_lock_if_inplace_updateable_rel(Relation relation,
 												 const ItemPointerData *otid,
@@ -2806,7 +2807,7 @@ xmax_infomask_changed(uint16 new_infomask, uint16 old_infomask)
 TM_Result
 heap_delete(Relation relation, const ItemPointerData *tid,
 			CommandId cid, Snapshot crosscheck, bool wait,
-			TM_FailureData *tmfd, bool changingPart)
+			TM_FailureData *tmfd, bool changingPart, bool walLogical)
 {
 	TM_Result	result;
 	TransactionId xid = GetCurrentTransactionId();
@@ -3053,7 +3054,8 @@ l1:
 	 * Compute replica identity tuple before entering the critical section so
 	 * we don't PANIC upon a memory allocation failure.
 	 */
-	old_key_tuple = ExtractReplicaIdentity(relation, &tp, true, &old_key_copied);
+	old_key_tuple = walLogical ?
+		ExtractReplicaIdentity(relation, &tp, true, &old_key_copied) : NULL;
 
 	/*
 	 * If this is the first possibly-multixact-able operation in the current
@@ -3143,6 +3145,15 @@ l1:
 				xlrec.flags |= XLH_DELETE_CONTAINS_OLD_KEY;
 		}
 
+		/*
+		 * Unlike UPDATE, DELETE is decoded even if there is no old key, so it
+		 * does not help to clear both XLH_DELETE_CONTAINS_OLD_TUPLE and
+		 * XLH_DELETE_CONTAINS_OLD_KEY. Thus we need an extra flag. TODO
+		 * Consider not decoding tuples w/o the old tuple/key instead.
+		 */
+		if (!walLogical)
+			xlrec.flags |= XLH_DELETE_NO_LOGICAL;
+
 		XLogBeginInsert();
 		XLogRegisterData(&xlrec, SizeOfHeapDelete);
 
@@ -3235,7 +3246,8 @@ simple_heap_delete(Relation relation, const ItemPointerData *tid)
 	result = heap_delete(relation, tid,
 						 GetCurrentCommandId(true), InvalidSnapshot,
 						 true /* wait for commit */ ,
-						 &tmfd, false /* changingPart */ );
+						 &tmfd, false,	/* changingPart */
+						 true /* walLogical */ );
 	switch (result)
 	{
 		case TM_SelfModified:
@@ -3276,7 +3288,7 @@ TM_Result
 heap_update(Relation relation, const ItemPointerData *otid, HeapTuple newtup,
 			CommandId cid, Snapshot crosscheck, bool wait,
 			TM_FailureData *tmfd, LockTupleMode *lockmode,
-			TU_UpdateIndexes *update_indexes)
+			TU_UpdateIndexes *update_indexes, bool walLogical)
 {
 	TM_Result	result;
 	TransactionId xid = GetCurrentTransactionId();
@@ -4169,7 +4181,8 @@ l2:
 								 newbuf, &oldtup, heaptup,
 								 old_key_tuple,
 								 all_visible_cleared,
-								 all_visible_cleared_new);
+								 all_visible_cleared_new,
+								 walLogical);
 		if (newbuf != buffer)
 		{
 			PageSetLSN(BufferGetPage(newbuf), recptr);
@@ -4527,7 +4540,8 @@ simple_heap_update(Relation relation, const ItemPointerData *otid, HeapTuple tup
 	result = heap_update(relation, otid, tup,
 						 GetCurrentCommandId(true), InvalidSnapshot,
 						 true /* wait for commit */ ,
-						 &tmfd, &lockmode, update_indexes);
+						 &tmfd, &lockmode, update_indexes,
+						 true /* walLogical */ );
 	switch (result)
 	{
 		case TM_SelfModified:
@@ -8883,7 +8897,8 @@ static XLogRecPtr
 log_heap_update(Relation reln, Buffer oldbuf,
 				Buffer newbuf, HeapTuple oldtup, HeapTuple newtup,
 				HeapTuple old_key_tuple,
-				bool all_visible_cleared, bool new_all_visible_cleared)
+				bool all_visible_cleared, bool new_all_visible_cleared,
+				bool walLogical)
 {
 	xl_heap_update xlrec;
 	xl_heap_header xlhdr;
@@ -8894,7 +8909,8 @@ log_heap_update(Relation reln, Buffer oldbuf,
 				suffixlen = 0;
 	XLogRecPtr	recptr;
 	Page		page = BufferGetPage(newbuf);
-	bool		need_tuple_data = RelationIsLogicallyLogged(reln);
+	bool		need_tuple_data = RelationIsLogicallyLogged(reln) &&
+		walLogical;
 	bool		init;
 	int			bufflags;
 
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 778377b9866..3526b6adcb5 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -33,6 +33,7 @@
 #include "catalog/index.h"
 #include "catalog/storage.h"
 #include "catalog/storage_xlog.h"
+#include "commands/cluster.h"
 #include "commands/progress.h"
 #include "executor/executor.h"
 #include "miscadmin.h"
@@ -309,7 +310,8 @@ heapam_tuple_delete(Relation relation, ItemPointer tid, CommandId cid,
 	 * the storage itself is cleaning the dead tuples by itself, it is the
 	 * time to call the index tuple deletion also.
 	 */
-	return heap_delete(relation, tid, cid, crosscheck, wait, tmfd, changingPart);
+	return heap_delete(relation, tid, cid, crosscheck, wait, tmfd, changingPart,
+					   true);
 }
 
 
@@ -328,7 +330,7 @@ heapam_tuple_update(Relation relation, ItemPointer otid, TupleTableSlot *slot,
 	tuple->t_tableOid = slot->tts_tableOid;
 
 	result = heap_update(relation, otid, tuple, cid, crosscheck, wait,
-						 tmfd, lockmode, update_indexes);
+						 tmfd, lockmode, update_indexes, true);
 	ItemPointerCopy(&tuple->t_self, &slot->tts_tid);
 
 	/*
@@ -685,13 +687,15 @@ static void
 heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 								 Relation OldIndex, bool use_sort,
 								 TransactionId OldestXmin,
+								 Snapshot snapshot,
+								 LogicalDecodingContext *decoding_ctx,
 								 TransactionId *xid_cutoff,
 								 MultiXactId *multi_cutoff,
 								 double *num_tuples,
 								 double *tups_vacuumed,
 								 double *tups_recently_dead)
 {
-	RewriteState rwstate;
+	RewriteState rwstate = NULL;
 	IndexScanDesc indexScan;
 	TableScanDesc tableScan;
 	HeapScanDesc heapScan;
@@ -705,6 +709,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	bool	   *isnull;
 	BufferHeapTupleTableSlot *hslot;
 	BlockNumber prev_cblock = InvalidBlockNumber;
+	bool		concurrent = snapshot != NULL;
+	XLogRecPtr	end_of_wal_prev = GetFlushRecPtr(NULL);
 
 	/* Remember if it's a system catalog */
 	is_system_catalog = IsSystemRelation(OldHeap);
@@ -720,9 +726,12 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	values = palloc_array(Datum, natts);
 	isnull = palloc_array(bool, natts);
 
-	/* Initialize the rewrite operation */
-	rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin, *xid_cutoff,
-								 *multi_cutoff);
+	/*
+	 * Initialize the rewrite operation.
+	 */
+	if (!concurrent)
+		rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin,
+									 *xid_cutoff, *multi_cutoff);
 
 
 	/* Set up sorting if wanted */
@@ -737,6 +746,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	 * Prepare to scan the OldHeap.  To ensure we see recently-dead tuples
 	 * that still need to be copied, we scan with SnapshotAny and use
 	 * HeapTupleSatisfiesVacuum for the visibility test.
+	 *
+	 * In the CONCURRENTLY case, we do regular MVCC visibility tests, using
+	 * the snapshot passed by the caller.
 	 */
 	if (OldIndex != NULL && !use_sort)
 	{
@@ -753,7 +765,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 
 		tableScan = NULL;
 		heapScan = NULL;
-		indexScan = index_beginscan(OldHeap, OldIndex, SnapshotAny, NULL, 0, 0);
+		indexScan = index_beginscan(OldHeap, OldIndex,
+									snapshot ? snapshot : SnapshotAny,
+									NULL, 0, 0);
 		index_rescan(indexScan, NULL, 0, NULL, 0);
 	}
 	else
@@ -762,7 +776,9 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 		pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
 									 PROGRESS_REPACK_PHASE_SEQ_SCAN_HEAP);
 
-		tableScan = table_beginscan(OldHeap, SnapshotAny, 0, (ScanKey) NULL);
+		tableScan = table_beginscan(OldHeap,
+									snapshot ? snapshot : SnapshotAny,
+									0, (ScanKey) NULL);
 		heapScan = (HeapScanDesc) tableScan;
 		indexScan = NULL;
 
@@ -837,70 +853,77 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 		tuple = ExecFetchSlotHeapTuple(slot, false, NULL);
 		buf = hslot->buffer;
 
-		LockBuffer(buf, BUFFER_LOCK_SHARE);
-
-		switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf))
+		/*
+		 * Regarding CONCURRENTLY, see the comments on MVCC snapshot above.
+		 */
+		if (!concurrent)
 		{
-			case HEAPTUPLE_DEAD:
-				/* Definitely dead */
-				isdead = true;
-				break;
-			case HEAPTUPLE_RECENTLY_DEAD:
-				*tups_recently_dead += 1;
-				/* fall through */
-			case HEAPTUPLE_LIVE:
-				/* Live or recently dead, must copy it */
-				isdead = false;
-				break;
-			case HEAPTUPLE_INSERT_IN_PROGRESS:
+			LockBuffer(buf, BUFFER_LOCK_SHARE);
 
-				/*
-				 * Since we hold exclusive lock on the relation, normally the
-				 * only way to see this is if it was inserted earlier in our
-				 * own transaction.  However, it can happen in system
-				 * catalogs, since we tend to release write lock before commit
-				 * there.  Give a warning if neither case applies; but in any
-				 * case we had better copy it.
-				 */
-				if (!is_system_catalog &&
-					!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data)))
-					elog(WARNING, "concurrent insert in progress within table \"%s\"",
-						 RelationGetRelationName(OldHeap));
-				/* treat as live */
-				isdead = false;
-				break;
-			case HEAPTUPLE_DELETE_IN_PROGRESS:
+			switch (HeapTupleSatisfiesVacuum(tuple, OldestXmin, buf))
+			{
+				case HEAPTUPLE_DEAD:
+					/* Definitely dead */
+					isdead = true;
+					break;
+				case HEAPTUPLE_RECENTLY_DEAD:
+					*tups_recently_dead += 1;
+					/* fall through */
+				case HEAPTUPLE_LIVE:
+					/* Live or recently dead, must copy it */
+					isdead = false;
+					break;
+				case HEAPTUPLE_INSERT_IN_PROGRESS:
 
-				/*
-				 * Similar situation to INSERT_IN_PROGRESS case.
-				 */
-				if (!is_system_catalog &&
-					!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data)))
-					elog(WARNING, "concurrent delete in progress within table \"%s\"",
-						 RelationGetRelationName(OldHeap));
-				/* treat as recently dead */
-				*tups_recently_dead += 1;
-				isdead = false;
-				break;
-			default:
-				elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
-				isdead = false; /* keep compiler quiet */
-				break;
-		}
+					/*
+					 * As long as we hold exclusive lock on the relation,
+					 * normally the only way to see this is if it was inserted
+					 * earlier in our own transaction.  However, it can happen
+					 * in system catalogs, since we tend to release write lock
+					 * before commit there. Give a warning if neither case
+					 * applies; but in any case we had better copy it.
+					 */
+					if (!is_system_catalog &&
+						!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetXmin(tuple->t_data)))
+						elog(WARNING, "concurrent insert in progress within table \"%s\"",
+							 RelationGetRelationName(OldHeap));
+					/* treat as live */
+					isdead = false;
+					break;
+				case HEAPTUPLE_DELETE_IN_PROGRESS:
+
+					/*
+					 * Similar situation to INSERT_IN_PROGRESS case.
+					 */
+					if (!is_system_catalog &&
+						!TransactionIdIsCurrentTransactionId(HeapTupleHeaderGetUpdateXid(tuple->t_data)))
+						elog(WARNING, "concurrent delete in progress within table \"%s\"",
+							 RelationGetRelationName(OldHeap));
+					/* treat as recently dead */
+					*tups_recently_dead += 1;
+					isdead = false;
+					break;
+				default:
+					elog(ERROR, "unexpected HeapTupleSatisfiesVacuum result");
+					isdead = false; /* keep compiler quiet */
+					break;
+			}
 
-		LockBuffer(buf, BUFFER_LOCK_UNLOCK);
+			LockBuffer(buf, BUFFER_LOCK_UNLOCK);
 
-		if (isdead)
-		{
-			*tups_vacuumed += 1;
-			/* heap rewrite module still needs to see it... */
-			if (rewrite_heap_dead_tuple(rwstate, tuple))
+			if (isdead)
 			{
-				/* A previous recently-dead tuple is now known dead */
 				*tups_vacuumed += 1;
-				*tups_recently_dead -= 1;
+				/* heap rewrite module still needs to see it... */
+				if (rewrite_heap_dead_tuple(rwstate, tuple))
+				{
+					/* A previous recently-dead tuple is now known dead */
+					*tups_vacuumed += 1;
+					*tups_recently_dead -= 1;
+				}
+
+				continue;
 			}
-			continue;
 		}
 
 		*num_tuples += 1;
@@ -919,7 +942,7 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 		{
 			const int	ct_index[] = {
 				PROGRESS_REPACK_HEAP_TUPLES_SCANNED,
-				PROGRESS_REPACK_HEAP_TUPLES_WRITTEN
+				PROGRESS_REPACK_HEAP_TUPLES_INSERTED
 			};
 			int64		ct_val[2];
 
@@ -934,6 +957,31 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 			ct_val[1] = *num_tuples;
 			pgstat_progress_update_multi_param(2, ct_index, ct_val);
 		}
+
+		/*
+		 * Process the WAL produced by the load, as well as by other
+		 * transactions, so that the replication slot can advance and WAL does
+		 * not pile up. Use wal_segment_size as a threshold so that we do not
+		 * introduce the decoding overhead too often.
+		 *
+		 * Of course, we must not apply the changes until the initial load has
+		 * completed.
+		 *
+		 * Note that our insertions into the new table should not be decoded
+		 * as we (intentionally) do not write the logical decoding specific
+		 * information to WAL.
+		 */
+		if (concurrent)
+		{
+			XLogRecPtr	end_of_wal;
+
+			end_of_wal = GetFlushRecPtr(NULL);
+			if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
+			{
+				repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+				end_of_wal_prev = end_of_wal;
+			}
+		}
 	}
 
 	if (indexScan != NULL)
@@ -977,15 +1025,32 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 									 values, isnull,
 									 rwstate);
 			/* Report n_tuples */
-			pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_WRITTEN,
+			pgstat_progress_update_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED,
 										 n_tuples);
+
+			/*
+			 * Try to keep the amount of not-yet-decoded WAL small, like
+			 * above.
+			 */
+			if (concurrent)
+			{
+				XLogRecPtr	end_of_wal;
+
+				end_of_wal = GetFlushRecPtr(NULL);
+				if ((end_of_wal - end_of_wal_prev) > wal_segment_size)
+				{
+					repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+					end_of_wal_prev = end_of_wal;
+				}
+			}
 		}
 
 		tuplesort_end(tuplesort);
 	}
 
 	/* Write out any remaining tuples, and fsync if needed */
-	end_heap_rewrite(rwstate);
+	if (rwstate)
+		end_heap_rewrite(rwstate);
 
 	/* Clean up */
 	pfree(values);
@@ -2376,6 +2441,10 @@ heapam_scan_sample_next_tuple(TableScanDesc scan, SampleScanState *scanstate,
  * SET WITHOUT OIDS.
  *
  * So, we must reconstruct the tuple from component Datums.
+ *
+ * If rwstate=NULL, use simple_heap_insert() instead of rewriting - in that
+ * case we still need to deform/form the tuple. TODO Shouldn't we rename the
+ * function, as might not do any rewrite?
  */
 static void
 reform_and_rewrite_tuple(HeapTuple tuple,
@@ -2398,8 +2467,28 @@ reform_and_rewrite_tuple(HeapTuple tuple,
 
 	copiedTuple = heap_form_tuple(newTupDesc, values, isnull);
 
-	/* The heap rewrite module does the rest */
-	rewrite_heap_tuple(rwstate, tuple, copiedTuple);
+	if (rwstate)
+		/* The heap rewrite module does the rest */
+		rewrite_heap_tuple(rwstate, tuple, copiedTuple);
+	else
+	{
+		/*
+		 * Insert tuple when processing REPACK CONCURRENTLY.
+		 *
+		 * rewriteheap.c is not used in the CONCURRENTLY case because it'd be
+		 * difficult to do the same in the catch-up phase (as the logical
+		 * decoding does not provide us with sufficient visibility
+		 * information). Thus we must use heap_insert() both during the
+		 * catch-up and here.
+		 *
+		 * The following is like simple_heap_insert() except that we pass the
+		 * flag to skip logical decoding: as soon as REPACK CONCURRENTLY swaps
+		 * the relation files, it drops this relation, so no logical
+		 * replication subscription should need the data.
+		 */
+		heap_insert(NewHeap, copiedTuple, GetCurrentCommandId(true),
+					HEAP_INSERT_NO_LOGICAL, NULL);
+	}
 
 	heap_freetuple(copiedTuple);
 }
diff --git a/src/backend/access/heap/rewriteheap.c b/src/backend/access/heap/rewriteheap.c
index bae3a2da77a..45892da1e14 100644
--- a/src/backend/access/heap/rewriteheap.c
+++ b/src/backend/access/heap/rewriteheap.c
@@ -617,9 +617,9 @@ raw_heap_insert(RewriteState state, HeapTuple tup)
 		int			options = HEAP_INSERT_SKIP_FSM;
 
 		/*
-		 * While rewriting the heap for VACUUM FULL / CLUSTER, make sure data
-		 * for the TOAST table are not logically decoded.  The main heap is
-		 * WAL-logged as XLOG FPI records, which are not logically decoded.
+		 * While rewriting the heap for REPACK, make sure data for the TOAST
+		 * table are not logically decoded.  The main heap is WAL-logged as
+		 * XLOG FPI records, which are not logically decoded.
 		 */
 		options |= HEAP_INSERT_NO_LOGICAL;
 
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 3f05ba3083a..d79eab5670c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1298,16 +1298,19 @@ CREATE VIEW pg_stat_progress_repack AS
                       WHEN 2 THEN 'index scanning heap'
                       WHEN 3 THEN 'sorting tuples'
                       WHEN 4 THEN 'writing new heap'
-                      WHEN 5 THEN 'swapping relation files'
-                      WHEN 6 THEN 'rebuilding index'
-                      WHEN 7 THEN 'performing final cleanup'
+                      WHEN 5 THEN 'catch-up'
+                      WHEN 6 THEN 'swapping relation files'
+                      WHEN 7 THEN 'rebuilding index'
+                      WHEN 8 THEN 'performing final cleanup'
                       END AS phase,
         CAST(S.param3 AS oid) AS repack_index_relid,
         S.param4 AS heap_tuples_scanned,
-        S.param5 AS heap_tuples_written,
-        S.param6 AS heap_blks_total,
-        S.param7 AS heap_blks_scanned,
-        S.param8 AS index_rebuild_count
+        S.param5 AS heap_tuples_inserted,
+        S.param6 AS heap_tuples_updated,
+        S.param7 AS heap_tuples_deleted,
+        S.param8 AS heap_blks_total,
+        S.param9 AS heap_blks_scanned,
+        S.param10 AS index_rebuild_count
     FROM pg_stat_get_progress_info('REPACK') AS S
         LEFT JOIN pg_database D ON S.datid = D.oid;
 
@@ -1325,7 +1328,7 @@ CREATE VIEW pg_stat_progress_cluster AS
         phase,
         repack_index_relid AS cluster_index_relid,
         heap_tuples_scanned,
-        heap_tuples_written,
+        heap_tuples_inserted + heap_tuples_updated AS heap_tuples_written,
         heap_blks_total,
         heap_blks_scanned,
         index_rebuild_count
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 094f3d36047..c3feb0c3de4 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -1,8 +1,23 @@
 /*-------------------------------------------------------------------------
  *
  * cluster.c
- *	  CLUSTER a table on an index.  This is now also used for VACUUM FULL and
- *	  REPACK.
+ *		Implementation of REPACK [CONCURRENTLY], also known as CLUSTER and
+ *		VACUUM FULL.
+ *
+ * There are two somewhat different ways to rewrite a table.  In non-
+ * concurrent mode, it's easy: take AccessExclusiveLock, create a new
+ * transient relation, copy the tuples over to the relfilenode of the new
+ * relation, swap the relfilenodes, then drop the old relation.
+ *
+ * In concurrent mode, we lock the table with only ShareUpdateExclusiveLock,
+ * then do an initial copy as above.  However, while the tuples are being
+ * copied, concurrent transactions could modify the table. To cope with those
+ * changes, we rely on logical decoding to obtain them from WAL.  The changes
+ * are accumulated in a tuplestore.  Once the initial copy is complete, we
+ * read the changes from the tuplestore and re-apply them on the new heap.
+ * Then we upgrade our ShareUpdateExclusiveLock to AccessExclusiveLock and
+ * swap the relfilenodes.  This way, the time we hold a strong lock on the
+ * table is much reduced, and the bloat is eliminated.
  *
  * There is hardly anything left of Paul Brown's original implementation...
  *
@@ -26,6 +41,10 @@
 #include "access/toast_internals.h"
 #include "access/transam.h"
 #include "access/xact.h"
+#include "access/xlog.h"
+#include "access/xlog_internal.h"
+#include "access/xloginsert.h"
+#include "access/xlogutils.h"
 #include "catalog/catalog.h"
 #include "catalog/dependency.h"
 #include "catalog/heap.h"
@@ -33,6 +52,7 @@
 #include "catalog/namespace.h"
 #include "catalog/objectaccess.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_control.h"
 #include "catalog/pg_inherits.h"
 #include "catalog/toasting.h"
 #include "commands/cluster.h"
@@ -40,15 +60,21 @@
 #include "commands/progress.h"
 #include "commands/tablecmds.h"
 #include "commands/vacuum.h"
+#include "executor/executor.h"
 #include "miscadmin.h"
 #include "optimizer/optimizer.h"
 #include "pgstat.h"
+#include "replication/decode.h"
+#include "replication/logical.h"
+#include "replication/snapbuild.h"
 #include "storage/bufmgr.h"
+#include "storage/ipc.h"
 #include "storage/lmgr.h"
 #include "storage/predicate.h"
 #include "utils/acl.h"
 #include "utils/fmgroids.h"
 #include "utils/guc.h"
+#include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -68,12 +94,62 @@ typedef struct
 	Oid			indexOid;
 } RelToCluster;
 
+/*
+ * The following definitions are used for concurrent processing.
+ */
+
+/*
+ * The locators are used to avoid logical decoding of data that we do not need
+ * for our table.
+ */
+static RelFileLocator repacked_rel_locator = {.relNumber = InvalidOid};
+static RelFileLocator repacked_rel_toast_locator = {.relNumber = InvalidOid};
+
+/*
+ * Everything we need to call ExecInsertIndexTuples().
+ */
+typedef struct IndexInsertState
+{
+	ResultRelInfo *rri;
+	EState	   *estate;
+} IndexInsertState;
+
+/* The WAL segment being decoded. */
+static XLogSegNo repack_current_segment = 0;
+
+/*
+ * Information needed to apply concurrent data changes.
+ */
+typedef struct ChangeDest
+{
+	/* The relation the changes are applied to. */
+	Relation	rel;
+
+	/*
+	 * The following is needed to find the existing tuple if the change is
+	 * UPDATE or DELETE. 'ident_key' should have all the fields except for
+	 * 'sk_argument' initialized.
+	 */
+	Relation	ident_index;
+	ScanKey		ident_key;
+	int			ident_key_nentries;
+
+	/* Needed to update indexes of rel_dst. */
+	IndexInsertState *iistate;
+} ChangeDest;
+
 static bool cluster_rel_recheck(RepackCommand cmd, Relation OldHeap,
-								Oid indexOid, Oid userid, int options);
-static void rebuild_relation(Relation OldHeap, Relation index, bool verbose);
+								Oid indexOid, Oid userid, LOCKMODE lmode,
+								int options);
+static void check_repack_concurrently_requirements(Relation rel);
+static void rebuild_relation(Relation OldHeap, Relation index, bool verbose,
+							 bool concurrent);
 static void copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
-							bool verbose, bool *pSwapToastByContent,
-							TransactionId *pFreezeXid, MultiXactId *pCutoffMulti);
+							Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+							bool verbose,
+							bool *pSwapToastByContent,
+							TransactionId *pFreezeXid,
+							MultiXactId *pCutoffMulti);
 static List *get_tables_to_repack(RepackCommand cmd, bool usingindex,
 								  MemoryContext permcxt);
 static List *get_tables_to_repack_partitioned(RepackCommand cmd,
@@ -81,13 +157,51 @@ static List *get_tables_to_repack_partitioned(RepackCommand cmd,
 											  MemoryContext permcxt);
 static bool cluster_is_permitted_for_relation(RepackCommand cmd,
 											  Oid relid, Oid userid);
+
+static void begin_concurrent_repack(Relation rel);
+static void end_concurrent_repack(void);
+static LogicalDecodingContext *setup_logical_decoding(Oid relid);
+static HeapTuple get_changed_tuple(char *change);
+static void apply_concurrent_changes(RepackDecodingState *dstate,
+									 ChangeDest *dest);
+static void apply_concurrent_insert(Relation rel, HeapTuple tup,
+									IndexInsertState *iistate,
+									TupleTableSlot *index_slot);
+static void apply_concurrent_update(Relation rel, HeapTuple tup,
+									HeapTuple tup_target,
+									IndexInsertState *iistate,
+									TupleTableSlot *index_slot);
+static void apply_concurrent_delete(Relation rel, HeapTuple tup_target);
+static HeapTuple find_target_tuple(Relation rel, ChangeDest *dest,
+								   HeapTuple tup_key,
+								   TupleTableSlot *ident_slot);
+static void process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
+									   XLogRecPtr end_of_wal,
+									   ChangeDest *dest);
+static IndexInsertState *get_index_insert_state(Relation relation,
+												Oid ident_index_id,
+												Relation *ident_index_p);
+static ScanKey build_identity_key(Oid ident_idx_oid, Relation rel_src,
+								  int *nentries);
+static void free_index_insert_state(IndexInsertState *iistate);
+static void cleanup_logical_decoding(LogicalDecodingContext *ctx);
+static void rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
+											   Relation cl_index,
+											   LogicalDecodingContext *decoding_ctx,
+											   TransactionId frozenXid,
+											   MultiXactId cutoffMulti);
+static List *build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes);
 static Relation process_single_relation(RepackStmt *stmt,
+										LOCKMODE lockmode,
+										bool isTopLevel,
 										ClusterParams *params);
 static Oid	determine_clustered_index(Relation rel, bool usingindex,
 									  const char *indexname);
 static const char *RepackCommandAsString(RepackCommand cmd);
 
 
+#define REPL_PLUGIN_NAME   "pgoutput_repack"
+
 /*
  * The repack code allows for processing multiple tables at once. Because
  * of this, we cannot just run everything on a single transaction, or we
@@ -117,6 +231,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	ClusterParams params = {0};
 	Relation	rel = NULL;
 	MemoryContext repack_context;
+	LOCKMODE	lockmode;
 	List	   *rtcs;
 
 	/* Parse option list */
@@ -127,6 +242,16 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 		else if (strcmp(opt->defname, "analyze") == 0 ||
 				 strcmp(opt->defname, "analyse") == 0)
 			params.options |= defGetBoolean(opt) ? CLUOPT_ANALYZE : 0;
+		else if (strcmp(opt->defname, "concurrently") == 0 &&
+				 defGetBoolean(opt))
+		{
+			if (stmt->command != REPACK_COMMAND_REPACK)
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("CONCURRENTLY option not supported for %s",
+							   RepackCommandAsString(stmt->command)));
+			params.options |= CLUOPT_CONCURRENT;
+		}
 		else
 			ereport(ERROR,
 					errcode(ERRCODE_SYNTAX_ERROR),
@@ -136,13 +261,25 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 					parser_errposition(pstate, opt->location));
 	}
 
+	/*
+	 * Determine the lock mode expected by cluster_rel().
+	 *
+	 * In the exclusive case, we obtain AccessExclusiveLock right away to
+	 * avoid lock-upgrade hazard in the single-transaction case. In the
+	 * CONCURRENTLY case, the AccessExclusiveLock will only be used at the end
+	 * of processing, supposedly for very short time. Until then, we'll have
+	 * to unlock the relation temporarily, so there's no lock-upgrade hazard.
+	 */
+	lockmode = (params.options & CLUOPT_CONCURRENT) == 0 ?
+		AccessExclusiveLock : ShareUpdateExclusiveLock;
+
 	/*
 	 * If a single relation is specified, process it and we're done ... unless
 	 * the relation is a partitioned table, in which case we fall through.
 	 */
 	if (stmt->relation != NULL)
 	{
-		rel = process_single_relation(stmt, &params);
+		rel = process_single_relation(stmt, lockmode, isTopLevel, &params);
 		if (rel == NULL)
 			return;				/* all done */
 	}
@@ -157,10 +294,29 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 				errmsg("cannot %s multiple tables", "REPACK (ANALYZE)"));
 
 	/*
-	 * By here, we know we are in a multi-table situation.  In order to avoid
-	 * holding locks for too long, we want to process each table in its own
-	 * transaction.  This forces us to disallow running inside a user
-	 * transaction block.
+	 * By here, we know we are in a multi-table situation.
+	 *
+	 * Concurrent processing is currently considered rather special (e.g. in
+	 * terms of resources consumed) so it is not performed in bulk.
+	 */
+	if (params.options & CLUOPT_CONCURRENT)
+	{
+		if (rel != NULL)
+		{
+			Assert(rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);
+			ereport(ERROR,
+					errmsg("REPACK CONCURRENTLY not supported for partitioned tables"),
+					errhint("Consider running the command for individual partitions."));
+		}
+		else
+			ereport(ERROR,
+					errmsg("REPACK CONCURRENTLY requires explicit table name"));
+	}
+
+	/*
+	 * In order to avoid holding locks for too long, we want to process each
+	 * table in its own transaction.  This forces us to disallow running
+	 * inside a user transaction block.
 	 */
 	PreventInTransactionBlock(isTopLevel, RepackCommandAsString(stmt->command));
 
@@ -243,7 +399,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 		 * Open the target table, coping with the case where it has been
 		 * dropped.
 		 */
-		rel = try_table_open(rtc->tableOid, AccessExclusiveLock);
+		rel = try_table_open(rtc->tableOid, lockmode);
 		if (rel == NULL)
 		{
 			CommitTransactionCommand();
@@ -254,7 +410,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 		PushActiveSnapshot(GetTransactionSnapshot());
 
 		/* Process this table */
-		cluster_rel(stmt->command, rel, rtc->indexOid, &params);
+		cluster_rel(stmt->command, rel, rtc->indexOid, &params, isTopLevel);
 		/* cluster_rel closes the relation, but keeps lock */
 
 		PopActiveSnapshot();
@@ -283,22 +439,53 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
  * If indexOid is InvalidOid, the table will be rewritten in physical order
  * instead of index order.
  *
+ * Note that, in the concurrent case, the function releases the lock at some
+ * point, in order to get AccessExclusiveLock for the final steps (i.e. to
+ * swap the relation files). To make things simpler, the caller should expect
+ * OldHeap to be closed on return, regardless CLUOPT_CONCURRENT. (The
+ * AccessExclusiveLock is kept till the end of the transaction.)
+ *
  * 'cmd' indicates which command is being executed, to be used for error
  * messages.
  */
 void
 cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
-			ClusterParams *params)
+			ClusterParams *params, bool isTopLevel)
 {
 	Oid			tableOid = RelationGetRelid(OldHeap);
+	Relation	index;
+	LOCKMODE	lmode;
 	Oid			save_userid;
 	int			save_sec_context;
 	int			save_nestlevel;
 	bool		verbose = ((params->options & CLUOPT_VERBOSE) != 0);
 	bool		recheck = ((params->options & CLUOPT_RECHECK) != 0);
-	Relation	index;
+	bool		concurrent = ((params->options & CLUOPT_CONCURRENT) != 0);
+
+	/*
+	 * The lock mode is AccessExclusiveLock for normal processing and
+	 * ShareUpdateExclusiveLock for concurrent processing (so that SELECT,
+	 * INSERT, UPDATE and DELETE commands work, but cluster_rel() cannot be
+	 * called concurrently for the same relation).
+	 */
+	lmode = !concurrent ? AccessExclusiveLock : ShareUpdateExclusiveLock;
 
-	Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false));
+	/* There are specific requirements on concurrent processing. */
+	if (concurrent)
+	{
+		/*
+		 * Make sure we have no XID assigned, otherwise call of
+		 * setup_logical_decoding() can cause a deadlock.
+		 *
+		 * The existence of transaction block actually does not imply that XID
+		 * was already assigned, but it very likely is. We might want to check
+		 * the result of GetCurrentTransactionIdIfAny() instead, but that
+		 * would be less clear from user's perspective.
+		 */
+		PreventInTransactionBlock(isTopLevel, "REPACK (CONCURRENTLY)");
+
+		check_repack_concurrently_requirements(OldHeap);
+	}
 
 	/* Check for user-requested abort. */
 	CHECK_FOR_INTERRUPTS();
@@ -324,10 +511,13 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 * If this is a single-transaction CLUSTER, we can skip these tests. We
 	 * *must* skip the one on indisclustered since it would reject an attempt
 	 * to cluster a not-previously-clustered index.
+	 *
+	 * XXX move [some of] these comments to where the RECHECK flag is
+	 * determined?
 	 */
 	if (recheck &&
 		!cluster_rel_recheck(cmd, OldHeap, indexOid, save_userid,
-							 params->options))
+							 lmode, params->options))
 		goto out;
 
 	/*
@@ -342,6 +532,12 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 				errmsg("cannot run %s on a shared catalog",
 					   RepackCommandAsString(cmd)));
 
+	/*
+	 * The CONCURRENTLY case should have been rejected earlier because it does
+	 * not support system catalogs.
+	 */
+	Assert(!(OldHeap->rd_rel->relisshared && concurrent));
+
 	/*
 	 * Don't process temp tables of other backends ... their local buffer
 	 * manager is not going to cope.
@@ -362,7 +558,7 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	if (OidIsValid(indexOid))
 	{
 		/* verify the index is good and lock it */
-		check_index_is_clusterable(OldHeap, indexOid, AccessExclusiveLock);
+		check_index_is_clusterable(OldHeap, indexOid, lmode);
 		/* also open it */
 		index = index_open(indexOid, NoLock);
 	}
@@ -397,7 +593,9 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	if (OldHeap->rd_rel->relkind == RELKIND_MATVIEW &&
 		!RelationIsPopulated(OldHeap))
 	{
-		relation_close(OldHeap, AccessExclusiveLock);
+		if (index)
+			index_close(index, lmode);
+		relation_close(OldHeap, lmode);
 		goto out;
 	}
 
@@ -410,11 +608,34 @@ cluster_rel(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 * invalid, because we move tuples around.  Promote them to relation
 	 * locks.  Predicate locks on indexes will be promoted when they are
 	 * reindexed.
+	 *
+	 * During concurrent processing, the heap as well as its indexes stay in
+	 * operation, so we postpone this step until they are locked using
+	 * AccessExclusiveLock near the end of the processing.
 	 */
-	TransferPredicateLocksToHeapRelation(OldHeap);
+	if (!concurrent)
+		TransferPredicateLocksToHeapRelation(OldHeap);
 
 	/* rebuild_relation does all the dirty work */
-	rebuild_relation(OldHeap, index, verbose);
+	PG_TRY();
+	{
+		/*
+		 * For concurrent processing, make sure that our logical decoding
+		 * ignores data changes of other tables than the one we are
+		 * processing.
+		 */
+		if (concurrent)
+			begin_concurrent_repack(OldHeap);
+
+		rebuild_relation(OldHeap, index, verbose, concurrent);
+	}
+	PG_FINALLY();
+	{
+		if (concurrent)
+			end_concurrent_repack();
+	}
+	PG_END_TRY();
+
 	/* rebuild_relation closes OldHeap, and index if valid */
 
 out:
@@ -433,14 +654,14 @@ out:
  */
 static bool
 cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
-					Oid userid, int options)
+					Oid userid, LOCKMODE lmode, int options)
 {
 	Oid			tableOid = RelationGetRelid(OldHeap);
 
 	/* Check that the user still has privileges for the relation */
 	if (!cluster_is_permitted_for_relation(cmd, tableOid, userid))
 	{
-		relation_close(OldHeap, AccessExclusiveLock);
+		relation_close(OldHeap, lmode);
 		return false;
 	}
 
@@ -454,7 +675,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 	 */
 	if (RELATION_IS_OTHER_TEMP(OldHeap))
 	{
-		relation_close(OldHeap, AccessExclusiveLock);
+		relation_close(OldHeap, lmode);
 		return false;
 	}
 
@@ -465,7 +686,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		 */
 		if (!SearchSysCacheExists1(RELOID, ObjectIdGetDatum(indexOid)))
 		{
-			relation_close(OldHeap, AccessExclusiveLock);
+			relation_close(OldHeap, lmode);
 			return false;
 		}
 
@@ -476,7 +697,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
 		if ((options & CLUOPT_RECHECK_ISCLUSTERED) != 0 &&
 			!get_index_isclustered(indexOid))
 		{
-			relation_close(OldHeap, AccessExclusiveLock);
+			relation_close(OldHeap, lmode);
 			return false;
 		}
 	}
@@ -488,7 +709,7 @@ cluster_rel_recheck(RepackCommand cmd, Relation OldHeap, Oid indexOid,
  * Verify that the specified heap and index are valid to cluster on
  *
  * Side effect: obtains lock on the index.  The caller may
- * in some cases already have AccessExclusiveLock on the table, but
+ * in some cases already have a lock of the same strength on the table, but
  * not in all cases so we can't rely on the table-level lock for
  * protection here.
  */
@@ -617,18 +838,87 @@ mark_index_clustered(Relation rel, Oid indexOid, bool is_internal)
 	table_close(pg_index, RowExclusiveLock);
 }
 
+/*
+ * Check if the CONCURRENTLY option is legal for the relation.
+ */
+static void
+check_repack_concurrently_requirements(Relation rel)
+{
+	char		relpersistence,
+				replident;
+	Oid			ident_idx;
+
+	/* Data changes in system relations are not logically decoded. */
+	if (IsCatalogRelation(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot repack relation \"%s\"",
+						RelationGetRelationName(rel)),
+				 errhint("REPACK CONCURRENTLY is not supported for catalog relations.")));
+
+	/*
+	 * reorderbuffer.c does not seem to handle processing of TOAST relation
+	 * alone.
+	 */
+	if (IsToastRelation(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot repack relation \"%s\"",
+						RelationGetRelationName(rel)),
+				 errhint("REPACK CONCURRENTLY is not supported for TOAST relations, unless the main relation is repacked too.")));
+
+	relpersistence = rel->rd_rel->relpersistence;
+	if (relpersistence != RELPERSISTENCE_PERMANENT)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("cannot repack relation \"%s\"",
+						RelationGetRelationName(rel)),
+				 errhint("REPACK CONCURRENTLY is only allowed for permanent relations.")));
+
+	/* With NOTHING, WAL does not contain the old tuple. */
+	replident = rel->rd_rel->relreplident;
+	if (replident == REPLICA_IDENTITY_NOTHING)
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("cannot repack relation \"%s\"",
+						RelationGetRelationName(rel)),
+				 errhint("Relation \"%s\" has insufficient replication identity.",
+						 RelationGetRelationName(rel))));
+
+	/*
+	 * Identity index is not set if the replica identity is FULL, but PK might
+	 * exist in such a case.
+	 */
+	ident_idx = RelationGetReplicaIndex(rel);
+	if (!OidIsValid(ident_idx) && OidIsValid(rel->rd_pkindex))
+		ident_idx = rel->rd_pkindex;
+	if (!OidIsValid(ident_idx))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+				 errmsg("cannot process relation \"%s\"",
+						RelationGetRelationName(rel)),
+				 (errhint("Relation \"%s\" has no identity index.",
+						  RelationGetRelationName(rel)))));
+}
+
+
 /*
  * rebuild_relation: rebuild an existing relation in index or physical order
  *
- * OldHeap: table to rebuild.
+ * OldHeap: table to rebuild.  See cluster_rel() for comments on the required
+ * lock strength.
+ *
  * index: index to cluster by, or NULL to rewrite in physical order.
  *
- * On entry, heap and index (if one is given) must be open, and
- * AccessExclusiveLock held on them.
- * On exit, they are closed, but locks on them are not released.
+ * On entry, heap and index (if one is given) must be open, and the
+ * appropriate lock held on them -- AccessExclusiveLock for exclusive
+ * processing and ShareUpdateExclusiveLock for concurrent processing.
+ *
+ * On exit, they are closed, but still locked with AccessExclusiveLock.
+ * (The function handles the lock upgrade if 'concurrent' is true.)
  */
 static void
-rebuild_relation(Relation OldHeap, Relation index, bool verbose)
+rebuild_relation(Relation OldHeap, Relation index, bool verbose, bool concurrent)
 {
 	Oid			tableOid = RelationGetRelid(OldHeap);
 	Oid			accessMethod = OldHeap->rd_rel->relam;
@@ -636,13 +926,38 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose)
 	Oid			OIDNewHeap;
 	Relation	NewHeap;
 	char		relpersistence;
-	bool		is_system_catalog;
 	bool		swap_toast_by_content;
 	TransactionId frozenXid;
 	MultiXactId cutoffMulti;
+	LogicalDecodingContext *decoding_ctx = NULL;
+	Snapshot	snapshot = NULL;
+#if USE_ASSERT_CHECKING
+	LOCKMODE	lmode;
+
+	lmode = concurrent ? ShareUpdateExclusiveLock : AccessExclusiveLock;
+
+	Assert(CheckRelationLockedByMe(OldHeap, lmode, false));
+	Assert(index == NULL || CheckRelationLockedByMe(index, lmode, false));
+#endif
+
+	if (concurrent)
+	{
+		/*
+		 * Prepare to capture the concurrent data changes.
+		 *
+		 * Note that this call waits for all transactions with XID already
+		 * assigned to finish. If some of those transactions is waiting for a
+		 * lock conflicting with ShareUpdateExclusiveLock on our table (e.g.
+		 * it runs CREATE INDEX), we can end up in a deadlock. Not sure this
+		 * risk is worth unlocking/locking the table (and its clustering
+		 * index) and checking again if its still eligible for REPACK
+		 * CONCURRENTLY.
+		 */
+		decoding_ctx = setup_logical_decoding(tableOid);
 
-	Assert(CheckRelationLockedByMe(OldHeap, AccessExclusiveLock, false) &&
-		   (index == NULL || CheckRelationLockedByMe(index, AccessExclusiveLock, false)));
+		snapshot = SnapBuildInitialSnapshotForRepack(decoding_ctx->snapshot_builder);
+		PushActiveSnapshot(snapshot);
+	}
 
 	/* for CLUSTER or REPACK USING INDEX, mark the index as the one to use */
 	if (index != NULL)
@@ -650,7 +965,6 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose)
 
 	/* Remember info about rel before closing OldHeap */
 	relpersistence = OldHeap->rd_rel->relpersistence;
-	is_system_catalog = IsSystemRelation(OldHeap);
 
 	/*
 	 * Create the transient table that will receive the re-ordered data.
@@ -666,30 +980,54 @@ rebuild_relation(Relation OldHeap, Relation index, bool verbose)
 	NewHeap = table_open(OIDNewHeap, NoLock);
 
 	/* Copy the heap data into the new table in the desired order */
-	copy_table_data(NewHeap, OldHeap, index, verbose,
+	copy_table_data(NewHeap, OldHeap, index, snapshot, decoding_ctx, verbose,
 					&swap_toast_by_content, &frozenXid, &cutoffMulti);
 
+	/* The historic snapshot won't be needed anymore. */
+	if (snapshot)
+	{
+		PopActiveSnapshot();
+		UpdateActiveSnapshotCommandId();
+	}
 
-	/* Close relcache entries, but keep lock until transaction commit */
-	table_close(OldHeap, NoLock);
-	if (index)
-		index_close(index, NoLock);
+	if (concurrent)
+	{
+		Assert(!swap_toast_by_content);
+		rebuild_relation_finish_concurrent(NewHeap, OldHeap, index,
+										   decoding_ctx,
+										   frozenXid, cutoffMulti);
 
-	/*
-	 * Close the new relation so it can be dropped as soon as the storage is
-	 * swapped. The relation is not visible to others, so no need to unlock it
-	 * explicitly.
-	 */
-	table_close(NewHeap, NoLock);
+		pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
+									 PROGRESS_REPACK_PHASE_FINAL_CLEANUP);
 
-	/*
-	 * Swap the physical files of the target and transient tables, then
-	 * rebuild the target's indexes and throw away the transient table.
-	 */
-	finish_heap_swap(tableOid, OIDNewHeap, is_system_catalog,
-					 swap_toast_by_content, false, true,
-					 frozenXid, cutoffMulti,
-					 relpersistence);
+		/* Done with decoding. */
+		cleanup_logical_decoding(decoding_ctx);
+	}
+	else
+	{
+		bool		is_system_catalog = IsSystemRelation(OldHeap);
+
+		/* Close relcache entries, but keep lock until transaction commit */
+		table_close(OldHeap, NoLock);
+		if (index)
+			index_close(index, NoLock);
+
+		/*
+		 * Close the new relation so it can be dropped as soon as the storage
+		 * is swapped. The relation is not visible to others, so no need to
+		 * unlock it explicitly.
+		 */
+		table_close(NewHeap, NoLock);
+
+		/*
+		 * Swap the physical files of the target and transient tables, then
+		 * rebuild the target's indexes and throw away the transient table.
+		 */
+		finish_heap_swap(tableOid, OIDNewHeap, is_system_catalog,
+						 swap_toast_by_content, false, true, true,
+						 frozenXid, cutoffMulti,
+						 relpersistence);
+	}
 }
 
 
@@ -824,15 +1162,19 @@ make_new_heap(Oid OIDOldHeap, Oid NewTableSpace, Oid NewAccessMethod,
 /*
  * Do the physical copying of table data.
  *
+ * 'snapshot' and 'decoding_ctx': see table_relation_copy_for_cluster(). Pass
+ * iff concurrent processing is required.
+ *
  * There are three output parameters:
  * *pSwapToastByContent is set true if toast tables must be swapped by content.
  * *pFreezeXid receives the TransactionId used as freeze cutoff point.
  * *pCutoffMulti receives the MultiXactId used as a cutoff point.
  */
 static void
-copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verbose,
-				bool *pSwapToastByContent, TransactionId *pFreezeXid,
-				MultiXactId *pCutoffMulti)
+copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
+				Snapshot snapshot, LogicalDecodingContext *decoding_ctx,
+				bool verbose, bool *pSwapToastByContent,
+				TransactionId *pFreezeXid, MultiXactId *pCutoffMulti)
 {
 	Relation	relRelation;
 	HeapTuple	reltup;
@@ -849,6 +1191,10 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 	int			elevel = verbose ? INFO : DEBUG2;
 	PGRUsage	ru0;
 	char	   *nspname;
+	bool		concurrent = snapshot != NULL;
+	LOCKMODE	lmode;
+
+	lmode = concurrent ? ShareUpdateExclusiveLock : AccessExclusiveLock;
 
 	pg_rusage_init(&ru0);
 
@@ -877,7 +1223,7 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 	 * will be held till end of transaction.
 	 */
 	if (OldHeap->rd_rel->reltoastrelid)
-		LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock);
+		LockRelationOid(OldHeap->rd_rel->reltoastrelid, lmode);
 
 	/*
 	 * If both tables have TOAST tables, perform toast swap by content.  It is
@@ -886,7 +1232,8 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 	 * swap by links.  This is okay because swap by content is only essential
 	 * for system catalogs, and we don't support schema changes for them.
 	 */
-	if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid)
+	if (OldHeap->rd_rel->reltoastrelid && NewHeap->rd_rel->reltoastrelid &&
+		!concurrent)
 	{
 		*pSwapToastByContent = true;
 
@@ -907,6 +1254,10 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 		 * follow the toast pointers to the wrong place.  (It would actually
 		 * work for values copied over from the old toast table, but not for
 		 * any values that we toast which were previously not toasted.)
+		 *
+		 * This would not work with CONCURRENTLY because we may need to delete
+		 * TOASTed tuples from the new heap. With this hack, we'd delete them
+		 * from the old heap.
 		 */
 		NewHeap->rd_toastoid = OldHeap->rd_rel->reltoastrelid;
 	}
@@ -982,7 +1333,9 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 	 * values (e.g. because the AM doesn't use freezing).
 	 */
 	table_relation_copy_for_cluster(OldHeap, NewHeap, OldIndex, use_sort,
-									cutoffs.OldestXmin, &cutoffs.FreezeLimit,
+									cutoffs.OldestXmin, snapshot,
+									decoding_ctx,
+									&cutoffs.FreezeLimit,
 									&cutoffs.MultiXactCutoff,
 									&num_tuples, &tups_vacuumed,
 									&tups_recently_dead);
@@ -991,7 +1344,11 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex, bool verb
 	*pFreezeXid = cutoffs.FreezeLimit;
 	*pCutoffMulti = cutoffs.MultiXactCutoff;
 
-	/* Reset rd_toastoid just to be tidy --- it shouldn't be looked at again */
+	/*
+	 * Reset rd_toastoid just to be tidy --- it shouldn't be looked at again.
+	 * In the CONCURRENTLY case, we need to set it again before applying the
+	 * concurrent changes.
+	 */
 	NewHeap->rd_toastoid = InvalidOid;
 
 	num_pages = RelationGetNumberOfBlocks(NewHeap);
@@ -1449,14 +1806,13 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 				 bool swap_toast_by_content,
 				 bool check_constraints,
 				 bool is_internal,
+				 bool reindex,
 				 TransactionId frozenXid,
 				 MultiXactId cutoffMulti,
 				 char newrelpersistence)
 {
 	ObjectAddress object;
 	Oid			mapped_tables[4];
-	int			reindex_flags;
-	ReindexParams reindex_params = {0};
 	int			i;
 
 	/* Report that we are now swapping relation files */
@@ -1482,39 +1838,47 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 	if (is_system_catalog)
 		CacheInvalidateCatalog(OIDOldHeap);
 
-	/*
-	 * Rebuild each index on the relation (but not the toast table, which is
-	 * all-new at this point).  It is important to do this before the DROP
-	 * step because if we are processing a system catalog that will be used
-	 * during DROP, we want to have its indexes available.  There is no
-	 * advantage to the other order anyway because this is all transactional,
-	 * so no chance to reclaim disk space before commit.  We do not need a
-	 * final CommandCounterIncrement() because reindex_relation does it.
-	 *
-	 * Note: because index_build is called via reindex_relation, it will never
-	 * set indcheckxmin true for the indexes.  This is OK even though in some
-	 * sense we are building new indexes rather than rebuilding existing ones,
-	 * because the new heap won't contain any HOT chains at all, let alone
-	 * broken ones, so it can't be necessary to set indcheckxmin.
-	 */
-	reindex_flags = REINDEX_REL_SUPPRESS_INDEX_USE;
-	if (check_constraints)
-		reindex_flags |= REINDEX_REL_CHECK_CONSTRAINTS;
+	if (reindex)
+	{
+		int			reindex_flags;
+		ReindexParams reindex_params = {0};
 
-	/*
-	 * Ensure that the indexes have the same persistence as the parent
-	 * relation.
-	 */
-	if (newrelpersistence == RELPERSISTENCE_UNLOGGED)
-		reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED;
-	else if (newrelpersistence == RELPERSISTENCE_PERMANENT)
-		reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT;
+		/*
+		 * Rebuild each index on the relation (but not the toast table, which
+		 * is all-new at this point).  It is important to do this before the
+		 * DROP step because if we are processing a system catalog that will
+		 * be used during DROP, we want to have its indexes available.  There
+		 * is no advantage to the other order anyway because this is all
+		 * transactional, so no chance to reclaim disk space before commit. We
+		 * do not need a final CommandCounterIncrement() because
+		 * reindex_relation does it.
+		 *
+		 * Note: because index_build is called via reindex_relation, it will
+		 * never set indcheckxmin true for the indexes.  This is OK even
+		 * though in some sense we are building new indexes rather than
+		 * rebuilding existing ones, because the new heap won't contain any
+		 * HOT chains at all, let alone broken ones, so it can't be necessary
+		 * to set indcheckxmin.
+		 */
+		reindex_flags = REINDEX_REL_SUPPRESS_INDEX_USE;
+		if (check_constraints)
+			reindex_flags |= REINDEX_REL_CHECK_CONSTRAINTS;
 
-	/* Report that we are now reindexing relations */
-	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
-								 PROGRESS_REPACK_PHASE_REBUILD_INDEX);
+		/*
+		 * Ensure that the indexes have the same persistence as the parent
+		 * relation.
+		 */
+		if (newrelpersistence == RELPERSISTENCE_UNLOGGED)
+			reindex_flags |= REINDEX_REL_FORCE_INDEXES_UNLOGGED;
+		else if (newrelpersistence == RELPERSISTENCE_PERMANENT)
+			reindex_flags |= REINDEX_REL_FORCE_INDEXES_PERMANENT;
+
+		/* Report that we are now reindexing relations */
+		pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
+									 PROGRESS_REPACK_PHASE_REBUILD_INDEX);
 
-	reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params);
+		reindex_relation(NULL, OIDOldHeap, reindex_flags, &reindex_params);
+	}
 
 	/* Report that we are now doing clean up */
 	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
@@ -1558,6 +1922,17 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 	object.objectId = OIDNewHeap;
 	object.objectSubId = 0;
 
+	if (!reindex)
+	{
+		/*
+		 * Make sure the changes in pg_class are visible. This is especially
+		 * important if !swap_toast_by_content, so that the correct TOAST
+		 * relation is dropped. (reindex_relation() above did not help in this
+		 * case))
+		 */
+		CommandCounterIncrement();
+	}
+
 	/*
 	 * The new relation is local to our transaction and we know nothing
 	 * depends on it, so DROP_RESTRICT should be OK.
@@ -1597,7 +1972,7 @@ finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 
 			/* Get the associated valid index to be renamed */
 			toastidx = toast_get_valid_index(newrel->rd_rel->reltoastrelid,
-											 NoLock);
+											 AccessExclusiveLock);
 
 			/* rename the toast table ... */
 			snprintf(NewToastName, NAMEDATALEN, "pg_toast_%u",
@@ -1857,7 +2232,8 @@ cluster_is_permitted_for_relation(RepackCommand cmd, Oid relid, Oid userid)
  * case, if an index name is given, it's up to the caller to resolve it.
  */
 static Relation
-process_single_relation(RepackStmt *stmt, ClusterParams *params)
+process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
+						ClusterParams *params)
 {
 	Relation	rel;
 	Oid			tableOid;
@@ -1866,13 +2242,9 @@ process_single_relation(RepackStmt *stmt, ClusterParams *params)
 	Assert(stmt->command == REPACK_COMMAND_CLUSTER ||
 		   stmt->command == REPACK_COMMAND_REPACK);
 
-	/*
-	 * Find, lock, and check permissions on the table.  We obtain
-	 * AccessExclusiveLock right away to avoid lock-upgrade hazard in the
-	 * single-transaction case.
-	 */
+	/* Find, lock, and check permissions on the table. */
 	tableOid = RangeVarGetRelidExtended(stmt->relation->relation,
-										AccessExclusiveLock,
+										lockmode,
 										0,
 										RangeVarCallbackMaintainsTable,
 										NULL);
@@ -1904,13 +2276,14 @@ process_single_relation(RepackStmt *stmt, ClusterParams *params)
 		return rel;
 	else
 	{
-		Oid			indexOid;
+		Oid			indexOid = InvalidOid;
 
 		indexOid = determine_clustered_index(rel, stmt->usingindex,
 											 stmt->indexname);
 		if (OidIsValid(indexOid))
-			check_index_is_clusterable(rel, indexOid, AccessExclusiveLock);
-		cluster_rel(stmt->command, rel, indexOid, params);
+			check_index_is_clusterable(rel, indexOid, lockmode);
+
+		cluster_rel(stmt->command, rel, indexOid, params, isTopLevel);
 
 		/* Do an analyze, if requested */
 		if (params->options & CLUOPT_ANALYZE)
@@ -1993,3 +2366,1067 @@ RepackCommandAsString(RepackCommand cmd)
 	}
 	return "???";
 }
+
+
+/*
+ * Call this function before REPACK CONCURRENTLY starts to setup logical
+ * decoding. It makes sure that other users of the table put enough
+ * information into WAL.
+ *
+ * The point is that at various places we expect that the table we're
+ * processing is treated like a system catalog. For example, we need to be
+ * able to scan it using a "historic snapshot" anytime during the processing
+ * (as opposed to scanning only at the start point of the decoding, as logical
+ * replication does during initial table synchronization), in order to apply
+ * concurrent UPDATE / DELETE commands.
+ *
+ * Note that TOAST table needs no attention here as it's not scanned using
+ * historic snapshot.
+ */
+static void
+begin_concurrent_repack(Relation rel)
+{
+	Oid			toastrelid;
+
+	/*
+	 * Avoid logical decoding of other relations by this backend. The lock we
+	 * have guarantees that the actual locator cannot be changed concurrently:
+	 * TRUNCATE needs AccessExclusiveLock.
+	 */
+	Assert(CheckRelationLockedByMe(rel, ShareUpdateExclusiveLock, false));
+	repacked_rel_locator = rel->rd_locator;
+	toastrelid = rel->rd_rel->reltoastrelid;
+	if (OidIsValid(toastrelid))
+	{
+		Relation	toastrel;
+
+		/* Avoid logical decoding of other TOAST relations. */
+		toastrel = table_open(toastrelid, AccessShareLock);
+		repacked_rel_toast_locator = toastrel->rd_locator;
+		table_close(toastrel, AccessShareLock);
+	}
+}
+
+/*
+ * Call this when done with REPACK CONCURRENTLY.
+ */
+static void
+end_concurrent_repack(void)
+{
+	/*
+	 * Restore normal function of (future) logical decoding for this backend.
+	 */
+	repacked_rel_locator.relNumber = InvalidOid;
+	repacked_rel_toast_locator.relNumber = InvalidOid;
+}
+
+/*
+ * Is this backend performing logical decoding on behalf of REPACK
+ * (CONCURRENTLY) ?
+ */
+bool
+am_decoding_for_repack(void)
+{
+	return OidIsValid(repacked_rel_locator.relNumber);
+}
+
+/*
+ * Does the WAL record contain a data change that this backend does not need
+ * to decode on behalf of REPACK (CONCURRENT)?
+ */
+bool
+change_useless_for_repack(XLogRecordBuffer *buf)
+{
+	XLogReaderState *r = buf->record;
+	RelFileLocator locator;
+
+	/* TOAST locator should not be set unless the main is. */
+	Assert(!OidIsValid(repacked_rel_toast_locator.relNumber) ||
+		   OidIsValid(repacked_rel_locator.relNumber));
+
+	/*
+	 * Backends not involved in REPACK (CONCURRENTLY) should not do the
+	 * filtering.
+	 */
+	if (!am_decoding_for_repack())
+		return false;
+
+	/*
+	 * If the record does not contain the block 0, it's probably not INSERT /
+	 * UPDATE / DELETE. In any case, we do not have enough information to
+	 * filter the change out.
+	 */
+	if (!XLogRecGetBlockTagExtended(r, 0, &locator, NULL, NULL, NULL))
+		return false;
+
+	/*
+	 * Decode the change if it belongs to the table we are repacking, or if it
+	 * belongs to its TOAST relation.
+	 */
+	if (RelFileLocatorEquals(locator, repacked_rel_locator))
+		return false;
+	if (OidIsValid(repacked_rel_toast_locator.relNumber) &&
+		RelFileLocatorEquals(locator, repacked_rel_toast_locator))
+		return false;
+
+	/* Filter out changes of other tables. */
+	return true;
+}
+
+/*
+ * This function is much like pg_create_logical_replication_slot() except that
+ * the new slot is neither released (if anyone else could read changes from
+ * our slot, we could miss changes other backends do while we copy the
+ * existing data into temporary table), nor persisted (it's easier to handle
+ * crash by restarting all the work from scratch).
+ */
+static LogicalDecodingContext *
+setup_logical_decoding(Oid relid)
+{
+	Relation	rel;
+	TupleDesc	tupdesc;
+	LogicalDecodingContext *ctx;
+	RepackDecodingState *dstate = palloc0_object(RepackDecodingState);
+
+	/*
+	 * REPACK CONCURRENTLY is not allowed in a transaction block, so this
+	 * should never fire.
+	 */
+	Assert(!TransactionIdIsValid(GetTopTransactionIdIfAny()));
+
+	/*
+	 * A single backend should not execute multiple REPACK commands at a time,
+	 * so use PID to make the slot unique.
+	 */
+	snprintf(NameStr(dstate->slotname), NAMEDATALEN, "repack_%d", MyProcPid);
+
+	/*
+	 * Check if we can use logical decoding.
+	 */
+	CheckSlotPermissions();
+	CheckLogicalDecodingRequirements();
+
+	/* RS_TEMPORARY so that the slot gets cleaned up on ERROR. */
+	ReplicationSlotCreate(NameStr(dstate->slotname), true, RS_TEMPORARY,
+						  false, false, false);
+
+	/*
+	 * Neither prepare_write nor do_write callback nor update_progress is
+	 * useful for us.
+	 */
+	ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME,
+									NIL,
+									true,
+									InvalidXLogRecPtr,
+									XL_ROUTINE(.page_read = read_local_xlog_page,
+											   .segment_open = wal_segment_open,
+											   .segment_close = wal_segment_close),
+									NULL, NULL, NULL);
+
+	/*
+	 * We don't have control on setting fast_forward, so at least check it.
+	 */
+	Assert(!ctx->fast_forward);
+
+	DecodingContextFindStartpoint(ctx);
+
+	/* Some WAL records should have been read. */
+	Assert(ctx->reader->EndRecPtr != InvalidXLogRecPtr);
+
+	XLByteToSeg(ctx->reader->EndRecPtr, repack_current_segment,
+				wal_segment_size);
+
+	/*
+	 * Setup structures to store decoded changes.
+	 */
+	dstate->relid = relid;
+	dstate->tstore = tuplestore_begin_heap(false, false,
+										   maintenance_work_mem);
+
+	/* Caller should already have the table locked. */
+	rel = table_open(relid, NoLock);
+	tupdesc = CreateTupleDescCopy(RelationGetDescr(rel));
+	dstate->tupdesc = tupdesc;
+	table_close(rel, NoLock);
+
+	/* Initialize the descriptor to store the changes ... */
+	dstate->tupdesc_change = CreateTemplateTupleDesc(1);
+
+	TupleDescInitEntry(dstate->tupdesc_change, 1, NULL, BYTEAOID, -1, 0);
+	/* ... as well as the corresponding slot. */
+	dstate->tsslot = MakeSingleTupleTableSlot(dstate->tupdesc_change,
+											  &TTSOpsMinimalTuple);
+
+	dstate->resowner = ResourceOwnerCreate(CurrentResourceOwner,
+										   "logical decoding");
+
+	ctx->output_writer_private = dstate;
+	return ctx;
+}
+
+/*
+ * Retrieve tuple from ConcurrentChange structure.
+ *
+ * The input data starts with the structure but it might not be appropriately
+ * aligned.
+ */
+static HeapTuple
+get_changed_tuple(char *change)
+{
+	HeapTupleData tup_data;
+	HeapTuple	result;
+	char	   *src;
+
+	/*
+	 * Ensure alignment before accessing the fields. (This is why we can't use
+	 * heap_copytuple() instead of this function.)
+	 */
+	src = change + offsetof(ConcurrentChange, tup_data);
+	memcpy(&tup_data, src, sizeof(HeapTupleData));
+
+	result = (HeapTuple) palloc(HEAPTUPLESIZE + tup_data.t_len);
+	memcpy(result, &tup_data, sizeof(HeapTupleData));
+	result->t_data = (HeapTupleHeader) ((char *) result + HEAPTUPLESIZE);
+	src = change + SizeOfConcurrentChange;
+	memcpy(result->t_data, src, result->t_len);
+
+	return result;
+}
+
+/*
+ * Decode logical changes from the WAL sequence up to end_of_wal.
+ */
+void
+repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
+								 XLogRecPtr end_of_wal)
+{
+	RepackDecodingState *dstate;
+	ResourceOwner resowner_old;
+
+	dstate = (RepackDecodingState *) ctx->output_writer_private;
+	resowner_old = CurrentResourceOwner;
+	CurrentResourceOwner = dstate->resowner;
+
+	PG_TRY();
+	{
+		while (ctx->reader->EndRecPtr < end_of_wal)
+		{
+			XLogRecord *record;
+			XLogSegNo	segno_new;
+			char	   *errm = NULL;
+			XLogRecPtr	end_lsn;
+
+			record = XLogReadRecord(ctx->reader, &errm);
+			if (errm)
+				elog(ERROR, "%s", errm);
+
+			if (record != NULL)
+				LogicalDecodingProcessRecord(ctx, ctx->reader);
+
+			/*
+			 * If WAL segment boundary has been crossed, inform the decoding
+			 * system that the catalog_xmin can advance. (We can confirm more
+			 * often, but a filling a single WAL segment should not take much
+			 * time.)
+			 */
+			end_lsn = ctx->reader->EndRecPtr;
+			XLByteToSeg(end_lsn, segno_new, wal_segment_size);
+			if (segno_new != repack_current_segment)
+			{
+				LogicalConfirmReceivedLocation(end_lsn);
+				elog(DEBUG1, "REPACK: confirmed receive location %X/%X",
+					 (uint32) (end_lsn >> 32), (uint32) end_lsn);
+				repack_current_segment = segno_new;
+			}
+
+			CHECK_FOR_INTERRUPTS();
+		}
+		InvalidateSystemCaches();
+		CurrentResourceOwner = resowner_old;
+	}
+	PG_CATCH();
+	{
+		/* clear all timetravel entries */
+		InvalidateSystemCaches();
+		CurrentResourceOwner = resowner_old;
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+}
+
+/*
+ * Apply changes stored in 'file'.
+ */
+static void
+apply_concurrent_changes(RepackDecodingState *dstate, ChangeDest *dest)
+{
+	Relation	rel = dest->rel;
+	TupleTableSlot *index_slot,
+			   *ident_slot;
+	HeapTuple	tup_old = NULL;
+
+	if (dstate->nchanges == 0)
+		return;
+
+	/* TupleTableSlot is needed to pass the tuple to ExecInsertIndexTuples(). */
+	index_slot = MakeSingleTupleTableSlot(dstate->tupdesc, &TTSOpsHeapTuple);
+
+	/* A slot to fetch tuples from identity index. */
+	ident_slot = table_slot_create(rel, NULL);
+
+	while (tuplestore_gettupleslot(dstate->tstore, true, false,
+								   dstate->tsslot))
+	{
+		bool		shouldFree;
+		HeapTuple	tup_change,
+					tup,
+					tup_exist;
+		char	   *change_raw,
+				   *src;
+		ConcurrentChange change;
+		bool		isnull[1];
+		Datum		values[1];
+
+		CHECK_FOR_INTERRUPTS();
+
+		/* Get the change from the single-column tuple. */
+		tup_change = ExecFetchSlotHeapTuple(dstate->tsslot, false, &shouldFree);
+		heap_deform_tuple(tup_change, dstate->tupdesc_change, values, isnull);
+		Assert(!isnull[0]);
+
+		/* Make sure we access aligned data. */
+		change_raw = (char *) DatumGetByteaP(values[0]);
+		src = (char *) VARDATA(change_raw);
+		memcpy(&change, src, SizeOfConcurrentChange);
+
+		/*
+		 * Extract the tuple from the change. The tuple is copied here because
+		 * it might be assigned to 'tup_old', in which case it needs to
+		 * survive into the next iteration.
+		 */
+		tup = get_changed_tuple(src);
+
+		if (change.kind == CHANGE_UPDATE_OLD)
+		{
+			Assert(tup_old == NULL);
+			tup_old = tup;
+		}
+		else if (change.kind == CHANGE_INSERT)
+		{
+			Assert(tup_old == NULL);
+
+			apply_concurrent_insert(rel, tup, dest->iistate, index_slot);
+
+			pfree(tup);
+		}
+		else if (change.kind == CHANGE_UPDATE_NEW ||
+				 change.kind == CHANGE_DELETE)
+		{
+			HeapTuple	tup_key;
+
+			if (change.kind == CHANGE_UPDATE_NEW)
+			{
+				tup_key = tup_old != NULL ? tup_old : tup;
+			}
+			else
+			{
+				Assert(tup_old == NULL);
+				tup_key = tup;
+			}
+
+			/*
+			 * Find the tuple to be updated or deleted.
+			 */
+			tup_exist = find_target_tuple(rel, dest, tup_key, ident_slot);
+			if (tup_exist == NULL)
+				elog(ERROR, "failed to find target tuple");
+
+			if (change.kind == CHANGE_UPDATE_NEW)
+				apply_concurrent_update(rel, tup, tup_exist, dest->iistate,
+										index_slot);
+			else
+				apply_concurrent_delete(rel, tup_exist);
+
+			if (tup_old != NULL)
+			{
+				pfree(tup_old);
+				tup_old = NULL;
+			}
+
+			pfree(tup);
+		}
+		else
+			elog(ERROR, "unrecognized kind of change: %d", change.kind);
+
+		/*
+		 * If a change was applied now, increment CID for next writes and
+		 * update the snapshot so it sees the changes we've applied so far.
+		 */
+		if (change.kind != CHANGE_UPDATE_OLD)
+		{
+			CommandCounterIncrement();
+			UpdateActiveSnapshotCommandId();
+		}
+
+		/* TTSOpsMinimalTuple has .get_heap_tuple==NULL. */
+		Assert(shouldFree);
+		pfree(tup_change);
+	}
+
+	tuplestore_clear(dstate->tstore);
+	dstate->nchanges = 0;
+
+	/* Cleanup. */
+	ExecDropSingleTupleTableSlot(index_slot);
+	ExecDropSingleTupleTableSlot(ident_slot);
+}
+
+static void
+apply_concurrent_insert(Relation rel, HeapTuple tup, IndexInsertState *iistate,
+						TupleTableSlot *index_slot)
+{
+	List	   *recheck;
+
+
+	/*
+	 * Like simple_heap_insert(), but make sure that the INSERT is not
+	 * logically decoded - see reform_and_rewrite_tuple() for more
+	 * information.
+	 */
+	heap_insert(rel, tup, GetCurrentCommandId(true), HEAP_INSERT_NO_LOGICAL,
+				NULL);
+
+	/*
+	 * Update indexes.
+	 *
+	 * In case functions in the index need the active snapshot and caller
+	 * hasn't set one.
+	 */
+	ExecStoreHeapTuple(tup, index_slot, false);
+	recheck = ExecInsertIndexTuples(iistate->rri,
+									index_slot,
+									iistate->estate,
+									false,	/* update */
+									false,	/* noDupErr */
+									NULL,	/* specConflict */
+									NIL,	/* arbiterIndexes */
+									false	/* onlySummarizing */
+		);
+
+	/*
+	 * If recheck is required, it must have been preformed on the source
+	 * relation by now. (All the logical changes we process here are already
+	 * committed.)
+	 */
+	list_free(recheck);
+
+	pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_INSERTED, 1);
+}
+
+static void
+apply_concurrent_update(Relation rel, HeapTuple tup, HeapTuple tup_target,
+						IndexInsertState *iistate, TupleTableSlot *index_slot)
+{
+	LockTupleMode lockmode;
+	TM_FailureData tmfd;
+	TU_UpdateIndexes update_indexes;
+	TM_Result	res;
+	List	   *recheck;
+
+	/*
+	 * Write the new tuple into the new heap. ('tup' gets the TID assigned
+	 * here.)
+	 *
+	 * Do it like in simple_heap_update(), except for 'wal_logical' (and
+	 * except for 'wait').
+	 */
+	res = heap_update(rel, &tup_target->t_self, tup,
+					  GetCurrentCommandId(true),
+					  InvalidSnapshot,
+					  false,	/* no wait - only we are doing changes */
+					  &tmfd, &lockmode, &update_indexes,
+					  false /* wal_logical */ );
+	if (res != TM_Ok)
+		ereport(ERROR, (errmsg("failed to apply concurrent UPDATE")));
+
+	ExecStoreHeapTuple(tup, index_slot, false);
+
+	if (update_indexes != TU_None)
+	{
+		recheck = ExecInsertIndexTuples(iistate->rri,
+										index_slot,
+										iistate->estate,
+										true,	/* update */
+										false,	/* noDupErr */
+										NULL,	/* specConflict */
+										NIL,	/* arbiterIndexes */
+		/* onlySummarizing */
+										update_indexes == TU_Summarizing);
+		list_free(recheck);
+	}
+
+	pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_UPDATED, 1);
+}
+
+static void
+apply_concurrent_delete(Relation rel, HeapTuple tup_target)
+{
+	TM_Result	res;
+	TM_FailureData tmfd;
+
+	/*
+	 * Delete tuple from the new heap.
+	 *
+	 * Do it like in simple_heap_delete(), except for 'wal_logical' (and
+	 * except for 'wait').
+	 */
+	res = heap_delete(rel, &tup_target->t_self, GetCurrentCommandId(true),
+					  InvalidSnapshot, false,
+					  &tmfd,
+					  false,	/* no wait - only we are doing changes */
+					  false /* wal_logical */ );
+
+	if (res != TM_Ok)
+		ereport(ERROR, (errmsg("failed to apply concurrent DELETE")));
+
+	pgstat_progress_incr_param(PROGRESS_REPACK_HEAP_TUPLES_DELETED, 1);
+}
+
+/*
+ * Find the tuple to be updated or deleted.
+ *
+ * 'tup_key' is a tuple containing the key values for the scan.
+ *
+ * On exit,'*scan_p' contains the scan descriptor used. The caller must close
+ * it when he no longer needs the tuple returned.
+ */
+static HeapTuple
+find_target_tuple(Relation rel, ChangeDest *dest, HeapTuple tup_key,
+				  TupleTableSlot *ident_slot)
+{
+	Relation	ident_index = dest->ident_index;
+	IndexScanDesc scan;
+	Form_pg_index ident_form;
+	int2vector *ident_indkey;
+	HeapTuple	result = NULL;
+
+	/* XXX no instrumentation for now */
+	scan = index_beginscan(rel, ident_index, GetActiveSnapshot(),
+						   NULL, dest->ident_key_nentries, 0);
+
+	/*
+	 * Scan key is passed by caller, so it does not have to be constructed
+	 * multiple times. Key entries have all fields initialized, except for
+	 * sk_argument.
+	 */
+	index_rescan(scan, dest->ident_key, dest->ident_key_nentries, NULL, 0);
+
+	/* Info needed to retrieve key values from heap tuple. */
+	ident_form = ident_index->rd_index;
+	ident_indkey = &ident_form->indkey;
+
+	/* Use the incoming tuple to finalize the scan key. */
+	for (int i = 0; i < scan->numberOfKeys; i++)
+	{
+		ScanKey		entry;
+		bool		isnull;
+		int16		attno_heap;
+
+		entry = &scan->keyData[i];
+		attno_heap = ident_indkey->values[i];
+		entry->sk_argument = heap_getattr(tup_key,
+										  attno_heap,
+										  rel->rd_att,
+										  &isnull);
+		Assert(!isnull);
+	}
+	if (index_getnext_slot(scan, ForwardScanDirection, ident_slot))
+	{
+		bool		shouldFree;
+
+		result = ExecFetchSlotHeapTuple(ident_slot, false, &shouldFree);
+		/* TTSOpsBufferHeapTuple has .get_heap_tuple != NULL. */
+		Assert(!shouldFree);
+	}
+	index_endscan(scan);
+
+	return result;
+}
+
+/*
+ * Decode and apply concurrent changes.
+ */
+static void
+process_concurrent_changes(LogicalDecodingContext *decoding_ctx,
+						   XLogRecPtr end_of_wal, ChangeDest *dest)
+{
+	RepackDecodingState *dstate;
+
+	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
+								 PROGRESS_REPACK_PHASE_CATCH_UP);
+
+	dstate = (RepackDecodingState *) decoding_ctx->output_writer_private;
+
+	repack_decode_concurrent_changes(decoding_ctx, end_of_wal);
+
+	if (dstate->nchanges == 0)
+		return;
+
+	apply_concurrent_changes(dstate, dest);
+}
+
+/*
+ * Initialize IndexInsertState for index specified by ident_index_id.
+ *
+ * While doing that, also return the identity index in *ident_index_p.
+ */
+static IndexInsertState *
+get_index_insert_state(Relation relation, Oid ident_index_id,
+					   Relation *ident_index_p)
+{
+	EState	   *estate;
+	int			i;
+	IndexInsertState *result;
+	Relation	ident_index = NULL;
+
+	result = (IndexInsertState *) palloc0(sizeof(IndexInsertState));
+	estate = CreateExecutorState();
+
+	result->rri = (ResultRelInfo *) palloc(sizeof(ResultRelInfo));
+	InitResultRelInfo(result->rri, relation, 0, 0, 0);
+	ExecOpenIndices(result->rri, false);
+
+	/*
+	 * Find the relcache entry of the identity index so that we spend no extra
+	 * effort to open / close it.
+	 */
+	for (i = 0; i < result->rri->ri_NumIndices; i++)
+	{
+		Relation	ind_rel;
+
+		ind_rel = result->rri->ri_IndexRelationDescs[i];
+		if (ind_rel->rd_id == ident_index_id)
+			ident_index = ind_rel;
+	}
+	if (ident_index == NULL)
+		elog(ERROR, "failed to open identity index");
+
+	/* Only initialize fields needed by ExecInsertIndexTuples(). */
+	result->estate = estate;
+
+	*ident_index_p = ident_index;
+	return result;
+}
+
+/*
+ * Build scan key to process logical changes.
+ */
+static ScanKey
+build_identity_key(Oid ident_idx_oid, Relation rel_src, int *nentries)
+{
+	Relation	ident_idx_rel;
+	Form_pg_index ident_idx;
+	int			n,
+				i;
+	ScanKey		result;
+
+	Assert(OidIsValid(ident_idx_oid));
+	ident_idx_rel = index_open(ident_idx_oid, AccessShareLock);
+	ident_idx = ident_idx_rel->rd_index;
+	n = ident_idx->indnatts;
+	result = (ScanKey) palloc(sizeof(ScanKeyData) * n);
+	for (i = 0; i < n; i++)
+	{
+		ScanKey		entry;
+		int16		relattno;
+		Form_pg_attribute att;
+		Oid			opfamily,
+					opcintype,
+					opno,
+					opcode;
+
+		entry = &result[i];
+		relattno = ident_idx->indkey.values[i];
+		if (relattno >= 1)
+		{
+			TupleDesc	desc;
+
+			desc = rel_src->rd_att;
+			att = TupleDescAttr(desc, relattno - 1);
+		}
+		else
+			elog(ERROR, "unexpected attribute number %d in index", relattno);
+
+		opfamily = ident_idx_rel->rd_opfamily[i];
+		opcintype = ident_idx_rel->rd_opcintype[i];
+		opno = get_opfamily_member(opfamily, opcintype, opcintype,
+								   BTEqualStrategyNumber);
+
+		if (!OidIsValid(opno))
+			elog(ERROR, "failed to find = operator for type %u", opcintype);
+
+		opcode = get_opcode(opno);
+		if (!OidIsValid(opcode))
+			elog(ERROR, "failed to find = operator for operator %u", opno);
+
+		/* Initialize everything but argument. */
+		ScanKeyInit(entry,
+					i + 1,
+					BTEqualStrategyNumber, opcode,
+					(Datum) NULL);
+		entry->sk_collation = att->attcollation;
+	}
+	index_close(ident_idx_rel, AccessShareLock);
+
+	*nentries = n;
+	return result;
+}
+
+static void
+free_index_insert_state(IndexInsertState *iistate)
+{
+	ExecCloseIndices(iistate->rri);
+	FreeExecutorState(iistate->estate);
+	pfree(iistate->rri);
+	pfree(iistate);
+}
+
+static void
+cleanup_logical_decoding(LogicalDecodingContext *ctx)
+{
+	RepackDecodingState *dstate;
+
+	dstate = (RepackDecodingState *) ctx->output_writer_private;
+
+	ExecDropSingleTupleTableSlot(dstate->tsslot);
+	FreeTupleDesc(dstate->tupdesc_change);
+	FreeTupleDesc(dstate->tupdesc);
+	tuplestore_end(dstate->tstore);
+
+	FreeDecodingContext(ctx);
+
+	ReplicationSlotRelease();
+	ReplicationSlotDrop(NameStr(dstate->slotname), false);
+	pfree(dstate);
+}
+
+/*
+ * The final steps of rebuild_relation() for concurrent processing.
+ *
+ * On entry, NewHeap is locked in AccessExclusiveLock mode. OldHeap and its
+ * clustering index (if one is passed) are still locked in a mode that allows
+ * concurrent data changes. On exit, both tables and their indexes are closed,
+ * but locked in AccessExclusiveLock mode.
+ */
+static void
+rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap,
+								   Relation cl_index,
+								   LogicalDecodingContext *decoding_ctx,
+								   TransactionId frozenXid,
+								   MultiXactId cutoffMulti)
+{
+	LOCKMODE	lockmode_old PG_USED_FOR_ASSERTS_ONLY;
+	List	   *ind_oids_new;
+	Oid			old_table_oid = RelationGetRelid(OldHeap);
+	Oid			new_table_oid = RelationGetRelid(NewHeap);
+	List	   *ind_oids_old = RelationGetIndexList(OldHeap);
+	ListCell   *lc,
+			   *lc2;
+	char		relpersistence;
+	bool		is_system_catalog;
+	Oid			ident_idx_old,
+				ident_idx_new;
+	XLogRecPtr	wal_insert_ptr,
+				end_of_wal;
+	char		dummy_rec_data = '\0';
+	Relation   *ind_refs,
+			   *ind_refs_p;
+	int			nind;
+	ChangeDest	chgdst;
+
+	/* Like in cluster_rel(). */
+	lockmode_old = ShareUpdateExclusiveLock;
+	Assert(CheckRelationLockedByMe(OldHeap, lockmode_old, false));
+	Assert(cl_index == NULL ||
+		   CheckRelationLockedByMe(cl_index, lockmode_old, false));
+	/* This is expected from the caller. */
+	Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false));
+
+	ident_idx_old = RelationGetReplicaIndex(OldHeap);
+
+	/*
+	 * Unlike the exclusive case, we build new indexes for the new relation
+	 * rather than swapping the storage and reindexing the old relation. The
+	 * point is that the index build can take some time, so we do it before we
+	 * get AccessExclusiveLock on the old heap and therefore we cannot swap
+	 * the heap storage yet.
+	 *
+	 * index_create() will lock the new indexes using AccessExclusiveLock - no
+	 * need to change that. At the same time, we use ShareUpdateExclusiveLock
+	 * to lock the existing indexes - that should be enough to prevent others
+	 * from changing them while we're repacking the relation. The lock on
+	 * table should prevent others from changing the index column list, but
+	 * might not be enough for commands like ALTER INDEX ... SET ... (Those
+	 * are not necessarily dangerous, but can make user confused if the
+	 * changes they do get lost due to REPACK.)
+	 */
+	ind_oids_new = build_new_indexes(NewHeap, OldHeap, ind_oids_old);
+
+	/*
+	 * Processing shouldn't start w/o valid identity index.
+	 */
+	Assert(OidIsValid(ident_idx_old));
+
+	/* Find "identity index" on the new relation. */
+	ident_idx_new = InvalidOid;
+	forboth(lc, ind_oids_old, lc2, ind_oids_new)
+	{
+		Oid			ind_old = lfirst_oid(lc);
+		Oid			ind_new = lfirst_oid(lc2);
+
+		if (ident_idx_old == ind_old)
+		{
+			ident_idx_new = ind_new;
+			break;
+		}
+	}
+	if (!OidIsValid(ident_idx_new))
+
+		/*
+		 * Should not happen, given our lock on the old relation.
+		 */
+		ereport(ERROR,
+				(errmsg("identity index missing on the new relation")));
+
+	/* Gather information to apply concurrent changes. */
+	chgdst.rel = NewHeap;
+	chgdst.iistate = get_index_insert_state(NewHeap, ident_idx_new,
+											&chgdst.ident_index);
+	chgdst.ident_key = build_identity_key(ident_idx_new, OldHeap,
+										  &chgdst.ident_key_nentries);
+
+	/*
+	 * During testing, wait for another backend to perform concurrent data
+	 * changes which we will process below.
+	 */
+	INJECTION_POINT("repack-concurrently-before-lock", NULL);
+
+	/*
+	 * Flush all WAL records inserted so far (possibly except for the last
+	 * incomplete page, see GetInsertRecPtr), to minimize the amount of data
+	 * we need to flush while holding exclusive lock on the source table.
+	 */
+	wal_insert_ptr = GetInsertRecPtr();
+	XLogFlush(wal_insert_ptr);
+	end_of_wal = GetFlushRecPtr(NULL);
+
+	/*
+	 * Apply concurrent changes first time, to minimize the time we need to
+	 * hold AccessExclusiveLock. (Quite some amount of WAL could have been
+	 * written during the data copying and index creation.)
+	 */
+	process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+
+	/*
+	 * Acquire AccessExclusiveLock on the table, its TOAST relation (if there
+	 * is one), all its indexes, so that we can swap the files.
+	 *
+	 * Before that, unlock the index temporarily to avoid deadlock in case
+	 * another transaction is trying to lock it while holding the lock on the
+	 * table.
+	 */
+	if (cl_index)
+	{
+		index_close(cl_index, ShareUpdateExclusiveLock);
+		cl_index = NULL;
+	}
+	/* For the same reason, unlock TOAST relation. */
+	if (OldHeap->rd_rel->reltoastrelid)
+		LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock);
+	/* Finally lock the table */
+	LockRelationOid(old_table_oid, AccessExclusiveLock);
+
+	/*
+	 * Lock all indexes now, not only the clustering one: all indexes need to
+	 * have their files swapped. While doing that, store their relation
+	 * references in an array, to handle predicate locks below.
+	 */
+	ind_refs_p = ind_refs = palloc_array(Relation, list_length(ind_oids_old));
+	nind = 0;
+	foreach_oid(ind_oid, ind_oids_old)
+	{
+		Relation	index;
+
+		index = index_open(ind_oid, AccessExclusiveLock);
+
+		/*
+		 * TODO 1) Do we need to check if ALTER INDEX was executed since the
+		 * new index was created in build_new_indexes()? 2) Specifically for
+		 * the clustering index, should check_index_is_clusterable() be called
+		 * here? (Not sure about the latter: ShareUpdateExclusiveLock on the
+		 * table probably blocks all commands that affect the result of
+		 * check_index_is_clusterable().)
+		 */
+		*ind_refs_p = index;
+		ind_refs_p++;
+		nind++;
+	}
+
+	/*
+	 * In addition, lock the OldHeap's TOAST relation exclusively - again, the
+	 * lock is needed to swap the files.
+	 */
+	if (OidIsValid(OldHeap->rd_rel->reltoastrelid))
+		LockRelationOid(OldHeap->rd_rel->reltoastrelid, AccessExclusiveLock);
+
+	/*
+	 * Tuples and pages of the old heap will be gone, but the heap will stay.
+	 */
+	TransferPredicateLocksToHeapRelation(OldHeap);
+	/* The same for indexes. */
+	for (int i = 0; i < nind; i++)
+	{
+		Relation	index = ind_refs[i];
+
+		TransferPredicateLocksToHeapRelation(index);
+
+		/*
+		 * References to indexes on the old relation are not needed anymore,
+		 * however locks stay till the end of the transaction.
+		 */
+		index_close(index, NoLock);
+	}
+	pfree(ind_refs);
+
+	/*
+	 * Flush anything we see in WAL, to make sure that all changes committed
+	 * while we were waiting for the exclusive lock are available for
+	 * decoding. This should not be necessary if all backends had
+	 * synchronous_commit set, but we can't rely on this setting.
+	 *
+	 * Unfortunately, GetInsertRecPtr() may lag behind the actual insert
+	 * position, and GetLastImportantRecPtr() points at the start of the last
+	 * record rather than at the end. Thus the simplest way to determine the
+	 * insert position is to insert a dummy record and use its LSN.
+	 *
+	 * XXX Consider using GetLastImportantRecPtr() and adding the size of the
+	 * last record (plus the total size of all the page headers the record
+	 * spans)?
+	 */
+	XLogBeginInsert();
+	XLogRegisterData(&dummy_rec_data, 1);
+	wal_insert_ptr = XLogInsert(RM_XLOG_ID, XLOG_NOOP);
+	XLogFlush(wal_insert_ptr);
+	end_of_wal = GetFlushRecPtr(NULL);
+
+	/* Apply the concurrent changes again. */
+	process_concurrent_changes(decoding_ctx, end_of_wal, &chgdst);
+
+	/* Remember info about rel before closing OldHeap */
+	relpersistence = OldHeap->rd_rel->relpersistence;
+	is_system_catalog = IsSystemRelation(OldHeap);
+
+	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
+								 PROGRESS_REPACK_PHASE_SWAP_REL_FILES);
+
+	/*
+	 * Even ShareUpdateExclusiveLock should have prevented others from
+	 * creating / dropping indexes (even using the CONCURRENTLY option), so we
+	 * do not need to check whether the lists match.
+	 */
+	forboth(lc, ind_oids_old, lc2, ind_oids_new)
+	{
+		Oid			ind_old = lfirst_oid(lc);
+		Oid			ind_new = lfirst_oid(lc2);
+		Oid			mapped_tables[4];
+
+		/* Zero out possible results from swapped_relation_files */
+		memset(mapped_tables, 0, sizeof(mapped_tables));
+
+		swap_relation_files(ind_old, ind_new,
+							(old_table_oid == RelationRelationId),
+							false,	/* swap_toast_by_content */
+							true,
+							InvalidTransactionId,
+							InvalidMultiXactId,
+							mapped_tables);
+
+#ifdef USE_ASSERT_CHECKING
+
+		/*
+		 * Concurrent processing is not supported for system relations, so
+		 * there should be no mapped tables.
+		 */
+		for (int i = 0; i < 4; i++)
+			Assert(mapped_tables[i] == 0);
+#endif
+	}
+
+	/* The new indexes must be visible for deletion. */
+	CommandCounterIncrement();
+
+	/* Close the old heap but keep lock until transaction commit. */
+	table_close(OldHeap, NoLock);
+	/* Close the new heap. (We didn't have to open its indexes). */
+	table_close(NewHeap, NoLock);
+
+	/* Cleanup what we don't need anymore. (And close the identity index.) */
+	pfree(chgdst.ident_key);
+	free_index_insert_state(chgdst.iistate);
+
+	/*
+	 * Swap the relations and their TOAST relations and TOAST indexes. This
+	 * also drops the new relation and its indexes.
+	 *
+	 * (System catalogs are currently not supported.)
+	 */
+	Assert(!is_system_catalog);
+	finish_heap_swap(old_table_oid, new_table_oid,
+					 is_system_catalog,
+					 false,		/* swap_toast_by_content */
+					 false, true, false,
+					 frozenXid, cutoffMulti,
+					 relpersistence);
+}
+
+/*
+ * Build indexes on NewHeap according to those on OldHeap.
+ *
+ * OldIndexes is the list of index OIDs on OldHeap. The contained indexes end
+ * up locked using ShareUpdateExclusiveLock.
+ *
+ * A list of OIDs of the corresponding indexes created on NewHeap is
+ * returned. The order of items does match, so we can use these arrays to swap
+ * index storage.
+ */
+static List *
+build_new_indexes(Relation NewHeap, Relation OldHeap, List *OldIndexes)
+{
+	List	   *result = NIL;
+
+	pgstat_progress_update_param(PROGRESS_REPACK_PHASE,
+								 PROGRESS_REPACK_PHASE_REBUILD_INDEX);
+
+	foreach_oid(ind_oid, OldIndexes)
+	{
+		Oid			ind_oid_new;
+		char	   *newName;
+		Relation	ind;
+
+		ind = index_open(ind_oid, ShareUpdateExclusiveLock);
+
+		newName = ChooseRelationName(get_rel_name(ind_oid),
+									 NULL,
+									 "repacknew",
+									 get_rel_namespace(ind->rd_index->indrelid),
+									 false);
+		ind_oid_new = index_create_copy(NewHeap, ind_oid,
+										ind->rd_rel->reltablespace, newName,
+										false);
+		result = lappend_oid(result, ind_oid_new);
+
+		index_close(ind, NoLock);
+	}
+
+	return result;
+}
diff --git a/src/backend/commands/matview.c b/src/backend/commands/matview.c
index 81a55a33ef2..ebc70f5bead 100644
--- a/src/backend/commands/matview.c
+++ b/src/backend/commands/matview.c
@@ -892,7 +892,7 @@ refresh_by_match_merge(Oid matviewOid, Oid tempOid, Oid relowner,
 static void
 refresh_by_heap_swap(Oid matviewOid, Oid OIDNewHeap, char relpersistence)
 {
-	finish_heap_swap(matviewOid, OIDNewHeap, false, false, true, true,
+	finish_heap_swap(matviewOid, OIDNewHeap, false, false, true, true, true,
 					 RecentXmin, ReadNextMultiXactId(), relpersistence);
 }
 
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index f976c0e5c7e..296387c7889 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -6025,6 +6025,7 @@ ATRewriteTables(AlterTableStmt *parsetree, List **wqueue, LOCKMODE lockmode,
 			finish_heap_swap(tab->relid, OIDNewHeap,
 							 false, false, true,
 							 !OidIsValid(tab->newTableSpace),
+							 true,
 							 RecentXmin,
 							 ReadNextMultiXactId(),
 							 persistence);
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index c363467a9cc..30a2b2578f7 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -126,7 +126,7 @@ static void vac_truncate_clog(TransactionId frozenXID,
 							  TransactionId lastSaneFrozenXid,
 							  MultiXactId lastSaneMinMulti);
 static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-					   BufferAccessStrategy bstrategy);
+					   BufferAccessStrategy bstrategy, bool isTopLevel);
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
@@ -629,7 +629,8 @@ vacuum(List *relations, const VacuumParams params, BufferAccessStrategy bstrateg
 
 			if (params.options & VACOPT_VACUUM)
 			{
-				if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy))
+				if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy,
+								isTopLevel))
 					continue;
 			}
 
@@ -1999,7 +2000,7 @@ vac_truncate_clog(TransactionId frozenXID,
  */
 static bool
 vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-		   BufferAccessStrategy bstrategy)
+		   BufferAccessStrategy bstrategy, bool isTopLevel)
 {
 	LOCKMODE	lmode;
 	Relation	rel;
@@ -2290,7 +2291,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 
 			/* VACUUM FULL is a variant of REPACK; see cluster.c */
 			cluster_rel(REPACK_COMMAND_VACUUMFULL, rel, InvalidOid,
-						&cluster_params);
+						&cluster_params, isTopLevel);
 			/* cluster_rel closes the relation, but keeps lock */
 
 			rel = NULL;
@@ -2333,7 +2334,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
 		toast_vacuum_params.toast_parent = relid;
 
-		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy);
+		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
+				   isTopLevel);
 	}
 
 	/*
diff --git a/src/backend/meson.build b/src/backend/meson.build
index de0a5fa1f7d..41f30a69dbd 100644
--- a/src/backend/meson.build
+++ b/src/backend/meson.build
@@ -194,5 +194,6 @@ pg_test_mod_args = pg_mod_args + {
 subdir('jit/llvm')
 subdir('replication/libpqwalreceiver')
 subdir('replication/pgoutput')
+subdir('replication/pgoutput_repack')
 subdir('snowball')
 subdir('utils/mb/conversion_procs')
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index e25dd6bc366..dc8c7be2aca 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -33,6 +33,7 @@
 #include "access/xlogreader.h"
 #include "access/xlogrecord.h"
 #include "catalog/pg_control.h"
+#include "commands/cluster.h"
 #include "replication/decode.h"
 #include "replication/logical.h"
 #include "replication/message.h"
@@ -467,6 +468,15 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 	TransactionId xid = XLogRecGetXid(buf->record);
 	SnapBuild  *builder = ctx->snapshot_builder;
 
+	/*
+	 * XXX Should we return here if change_useless_for_repack() returns true,
+	 * instead of calling the function below? Unlike the fast-forward case, we
+	 * shouldn't need the base snapshot for the containing transaction until
+	 * we receive a change that belongs to the table being REPACKed. Thus it
+	 * should be fine to skip SnapBuildProcessChange(), and therefore
+	 * reorderbuffer.c can create the transaction later.
+	 */
+
 	ReorderBufferProcessXid(ctx->reorder, xid, buf->origptr);
 
 	/*
@@ -484,7 +494,8 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 	{
 		case XLOG_HEAP_INSERT:
 			if (SnapBuildProcessChange(builder, xid, buf->origptr) &&
-				!ctx->fast_forward)
+				!ctx->fast_forward &&
+				!change_useless_for_repack(buf))
 				DecodeInsert(ctx, buf);
 			break;
 
@@ -496,17 +507,31 @@ heap_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 		case XLOG_HEAP_HOT_UPDATE:
 		case XLOG_HEAP_UPDATE:
 			if (SnapBuildProcessChange(builder, xid, buf->origptr) &&
-				!ctx->fast_forward)
+				!ctx->fast_forward &&
+				!change_useless_for_repack(buf))
 				DecodeUpdate(ctx, buf);
 			break;
 
 		case XLOG_HEAP_DELETE:
 			if (SnapBuildProcessChange(builder, xid, buf->origptr) &&
-				!ctx->fast_forward)
+				!ctx->fast_forward &&
+				!change_useless_for_repack(buf))
 				DecodeDelete(ctx, buf);
 			break;
 
 		case XLOG_HEAP_TRUNCATE:
+			/* Is REPACK (CONCURRENTLY) being run by this backend? */
+			if (am_decoding_for_repack())
+			{
+				/*
+				 * TRUNCATE changes rd_locator of the relation, so it'd break
+				 * REPACK (CONCURRENTLY). In fact it should not happen because
+				 * TRUNCATE needs AccessExclusiveLock on the table. Should we
+				 * only use Assert() here?
+				 */
+				ereport(ERROR,
+						(errmsg("TRUNCATE encountered while doing REPACK (CONCURRENTLY)")));
+			}
 			if (SnapBuildProcessChange(builder, xid, buf->origptr) &&
 				!ctx->fast_forward)
 				DecodeTruncate(ctx, buf);
@@ -1021,6 +1046,15 @@ DecodeDelete(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 
 	xlrec = (xl_heap_delete *) XLogRecGetData(r);
 
+	/*
+	 * Ignore changes which are considered useless for logical
+	 * decoding. Currently such changes are created by REPACK (CONCURRENTLY)
+	 * when replays DELETE commands on the new table (which is not yet visible
+	 * to other transactions).
+	 */
+	if (xlrec->flags & XLH_DELETE_NO_LOGICAL)
+		return;
+
 	/* only interested in our database */
 	XLogRecGetBlockTag(r, 0, &target_locator, NULL, NULL);
 	if (target_locator.dbOid != ctx->slot->data.database)
diff --git a/src/backend/replication/logical/snapbuild.c b/src/backend/replication/logical/snapbuild.c
index 95f230f8e9b..e238bcd73cd 100644
--- a/src/backend/replication/logical/snapbuild.c
+++ b/src/backend/replication/logical/snapbuild.c
@@ -486,6 +486,27 @@ SnapBuildInitialSnapshot(SnapBuild *builder)
 	return SnapBuildMVCCFromHistoric(snap, true);
 }
 
+/*
+ * Build an MVCC snapshot for the initial data load performed by REPACK
+ * CONCURRENTLY command.
+ *
+ * The snapshot will only be used to scan one particular relation, which is
+ * treated like a catalog (therefore ->building_full_snapshot is not
+ * important), and the caller should already have a replication slot setup (so
+ * we do not set MyProc->xmin). XXX Do we yet need to add some restrictions?
+ */
+Snapshot
+SnapBuildInitialSnapshotForRepack(SnapBuild *builder)
+{
+	Snapshot	snap;
+
+	Assert(builder->state == SNAPBUILD_CONSISTENT);
+	Assert(builder->building_full_snapshot);
+
+	snap = SnapBuildBuildSnapshot(builder);
+	return SnapBuildMVCCFromHistoric(snap, false);
+}
+
 /*
  * Turn a historic MVCC snapshot into an ordinary MVCC snapshot.
  *
diff --git a/src/backend/replication/pgoutput_repack/Makefile b/src/backend/replication/pgoutput_repack/Makefile
new file mode 100644
index 00000000000..4efeb713b70
--- /dev/null
+++ b/src/backend/replication/pgoutput_repack/Makefile
@@ -0,0 +1,32 @@
+#-------------------------------------------------------------------------
+#
+# Makefile--
+#    Makefile for src/backend/replication/pgoutput_repack
+#
+# IDENTIFICATION
+#    src/backend/replication/pgoutput_repack
+#
+#-------------------------------------------------------------------------
+
+subdir = src/backend/replication/pgoutput_repack
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+
+OBJS = \
+	$(WIN32RES) \
+	pgoutput_repack.o
+PGFILEDESC = "pgoutput_repack - logical replication output plugin for REPACK command"
+NAME = pgoutput_repack
+
+all: all-shared-lib
+
+include $(top_srcdir)/src/Makefile.shlib
+
+install: all installdirs install-lib
+
+installdirs: installdirs-lib
+
+uninstall: uninstall-lib
+
+clean distclean: clean-lib
+	rm -f $(OBJS)
diff --git a/src/backend/replication/pgoutput_repack/meson.build b/src/backend/replication/pgoutput_repack/meson.build
new file mode 100644
index 00000000000..133e865a4a0
--- /dev/null
+++ b/src/backend/replication/pgoutput_repack/meson.build
@@ -0,0 +1,18 @@
+# Copyright (c) 2022-2024, PostgreSQL Global Development Group
+
+pgoutput_repack_sources = files(
+  'pgoutput_repack.c',
+)
+
+if host_system == 'windows'
+  pgoutput_repack_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'pgoutput_repack',
+    '--FILEDESC', 'pgoutput_repack - logical replication output plugin for REPACK command',])
+endif
+
+pgoutput_repack = shared_module('pgoutput_repack',
+  pgoutput_repack_sources,
+  kwargs: pg_mod_args,
+)
+
+backend_targets += pgoutput_repack
diff --git a/src/backend/replication/pgoutput_repack/pgoutput_repack.c b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
new file mode 100644
index 00000000000..c8930640a0d
--- /dev/null
+++ b/src/backend/replication/pgoutput_repack/pgoutput_repack.c
@@ -0,0 +1,240 @@
+/*-------------------------------------------------------------------------
+ *
+ * pgoutput_repack.c
+ *		Logical Replication output plugin for REPACK command
+ *
+ * Copyright (c) 2012-2025, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/backend/replication/pgoutput_repack/pgoutput_repack.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/heaptoast.h"
+#include "commands/cluster.h"
+#include "replication/snapbuild.h"
+#include "utils/memutils.h"
+
+PG_MODULE_MAGIC;
+
+static void plugin_startup(LogicalDecodingContext *ctx,
+						   OutputPluginOptions *opt, bool is_init);
+static void plugin_shutdown(LogicalDecodingContext *ctx);
+static void plugin_begin_txn(LogicalDecodingContext *ctx,
+							 ReorderBufferTXN *txn);
+static void plugin_commit_txn(LogicalDecodingContext *ctx,
+							  ReorderBufferTXN *txn, XLogRecPtr commit_lsn);
+static void plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
+						  Relation rel, ReorderBufferChange *change);
+static void store_change(LogicalDecodingContext *ctx,
+						 ConcurrentChangeKind kind, HeapTuple tuple);
+
+void
+_PG_output_plugin_init(OutputPluginCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_output_plugin_init, LogicalOutputPluginInit);
+
+	cb->startup_cb = plugin_startup;
+	cb->begin_cb = plugin_begin_txn;
+	cb->change_cb = plugin_change;
+	cb->commit_cb = plugin_commit_txn;
+	cb->shutdown_cb = plugin_shutdown;
+}
+
+
+/* initialize this plugin */
+static void
+plugin_startup(LogicalDecodingContext *ctx, OutputPluginOptions *opt,
+			   bool is_init)
+{
+	ctx->output_plugin_private = NULL;
+
+	/* Probably unnecessary, as we don't use the SQL interface ... */
+	opt->output_type = OUTPUT_PLUGIN_BINARY_OUTPUT;
+
+	if (ctx->output_plugin_options != NIL)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("This plugin does not expect any options")));
+	}
+}
+
+static void
+plugin_shutdown(LogicalDecodingContext *ctx)
+{
+}
+
+/*
+ * As we don't release the slot during processing of particular table, there's
+ * no room for SQL interface, even for debugging purposes. Therefore we need
+ * neither OutputPluginPrepareWrite() nor OutputPluginWrite() in the plugin
+ * callbacks. (Although we might want to write custom callbacks, this API
+ * seems to be unnecessarily generic for our purposes.)
+ */
+
+/* BEGIN callback */
+static void
+plugin_begin_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn)
+{
+}
+
+/* COMMIT callback */
+static void
+plugin_commit_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
+				  XLogRecPtr commit_lsn)
+{
+}
+
+/*
+ * Callback for individual changed tuples
+ */
+static void
+plugin_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
+			  Relation relation, ReorderBufferChange *change)
+{
+	RepackDecodingState *dstate;
+
+	dstate = (RepackDecodingState *) ctx->output_writer_private;
+
+	/* Only interested in one particular relation. */
+	if (relation->rd_id != dstate->relid)
+		return;
+
+	/* Decode entry depending on its type */
+	switch (change->action)
+	{
+		case REORDER_BUFFER_CHANGE_INSERT:
+			{
+				HeapTuple	newtuple;
+
+				newtuple = change->data.tp.newtuple != NULL ?
+					change->data.tp.newtuple : NULL;
+
+				/*
+				 * Identity checks in the main function should have made this
+				 * impossible.
+				 */
+				if (newtuple == NULL)
+					elog(ERROR, "Incomplete insert info.");
+
+				store_change(ctx, CHANGE_INSERT, newtuple);
+			}
+			break;
+		case REORDER_BUFFER_CHANGE_UPDATE:
+			{
+				HeapTuple	oldtuple,
+							newtuple;
+
+				oldtuple = change->data.tp.oldtuple != NULL ?
+					change->data.tp.oldtuple : NULL;
+				newtuple = change->data.tp.newtuple != NULL ?
+					change->data.tp.newtuple : NULL;
+
+				if (newtuple == NULL)
+					elog(ERROR, "Incomplete update info.");
+
+				if (oldtuple != NULL)
+					store_change(ctx, CHANGE_UPDATE_OLD, oldtuple);
+
+				store_change(ctx, CHANGE_UPDATE_NEW, newtuple);
+			}
+			break;
+		case REORDER_BUFFER_CHANGE_DELETE:
+			{
+				HeapTuple	oldtuple;
+
+				oldtuple = change->data.tp.oldtuple ?
+					change->data.tp.oldtuple : NULL;
+
+				if (oldtuple == NULL)
+					elog(ERROR, "Incomplete delete info.");
+
+				store_change(ctx, CHANGE_DELETE, oldtuple);
+			}
+			break;
+		default:
+			/* Should not come here */
+			Assert(false);
+			break;
+	}
+}
+
+/* Store concurrent data change. */
+static void
+store_change(LogicalDecodingContext *ctx, ConcurrentChangeKind kind,
+			 HeapTuple tuple)
+{
+	RepackDecodingState *dstate;
+	char	   *change_raw;
+	ConcurrentChange change;
+	bool		flattened = false;
+	Size		size;
+	Datum		values[1];
+	bool		isnull[1];
+	char	   *dst;
+
+	dstate = (RepackDecodingState *) ctx->output_writer_private;
+
+	size = VARHDRSZ + SizeOfConcurrentChange;
+
+	/*
+	 * ReorderBufferCommit() stores the TOAST chunks in its private memory
+	 * context and frees them after having called apply_change().  Therefore
+	 * we need flat copy (including TOAST) that we eventually copy into the
+	 * memory context which is available to decode_concurrent_changes().
+	 */
+	if (HeapTupleHasExternal(tuple))
+	{
+		/*
+		 * toast_flatten_tuple_to_datum() might be more convenient but we
+		 * don't want the decompression it does.
+		 */
+		tuple = toast_flatten_tuple(tuple, dstate->tupdesc);
+		flattened = true;
+	}
+
+	size += tuple->t_len;
+	if (size >= MaxAllocSize)
+		elog(ERROR, "Change is too big.");
+
+	/* Construct the change. */
+	change_raw = (char *) palloc0(size);
+	SET_VARSIZE(change_raw, size);
+
+	/*
+	 * Since the varlena alignment might not be sufficient for the structure,
+	 * set the fields in a local instance and remember where it should
+	 * eventually be copied.
+	 */
+	change.kind = kind;
+	dst = (char *) VARDATA(change_raw);
+
+	/*
+	 * Copy the tuple.
+	 *
+	 * Note: change->tup_data.t_data must be fixed on retrieval!
+	 */
+	memcpy(&change.tup_data, tuple, sizeof(HeapTupleData));
+	memcpy(dst, &change, SizeOfConcurrentChange);
+	dst += SizeOfConcurrentChange;
+	memcpy(dst, tuple->t_data, tuple->t_len);
+
+	/* The data has been copied. */
+	if (flattened)
+		pfree(tuple);
+
+	/* Store as tuple of 1 bytea column. */
+	values[0] = PointerGetDatum(change_raw);
+	isnull[0] = false;
+	tuplestore_putvalues(dstate->tstore, dstate->tupdesc_change,
+						 values, isnull);
+
+	/* Accounting. */
+	dstate->nchanges++;
+
+	/* Cleanup. */
+	pfree(change_raw);
+}
diff --git a/src/backend/storage/lmgr/generate-lwlocknames.pl b/src/backend/storage/lmgr/generate-lwlocknames.pl
index b49007167b0..2e7f1054e62 100644
--- a/src/backend/storage/lmgr/generate-lwlocknames.pl
+++ b/src/backend/storage/lmgr/generate-lwlocknames.pl
@@ -162,7 +162,7 @@ while (<$lwlocklist>)
 
 die
   "$wait_event_lwlocks[$lwlock_count] defined in wait_event_names.txt but "
-  . " missing from lwlocklist.h"
+  . "missing from lwlocklist.h"
   if $lwlock_count < scalar @wait_event_lwlocks;
 
 die
diff --git a/src/backend/utils/time/snapmgr.c b/src/backend/utils/time/snapmgr.c
index 3af1b366adf..fdf3427b43f 100644
--- a/src/backend/utils/time/snapmgr.c
+++ b/src/backend/utils/time/snapmgr.c
@@ -214,7 +214,6 @@ static List *exportedSnapshots = NIL;
 
 /* Prototypes for local functions */
 static void UnregisterSnapshotNoOwner(Snapshot snapshot);
-static void FreeSnapshot(Snapshot snapshot);
 static void SnapshotResetXmin(void);
 
 /* ResourceOwner callbacks to track snapshot references */
@@ -659,7 +658,7 @@ CopySnapshot(Snapshot snapshot)
  * FreeSnapshot
  *		Free the memory associated with a snapshot.
  */
-static void
+void
 FreeSnapshot(Snapshot snapshot)
 {
 	Assert(snapshot->regd_count == 0);
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 2a1bb47ff03..0ec0f4c4790 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -5121,8 +5121,8 @@ match_previous_words(int pattern_id,
 		 * one word, so the above test is correct.
 		 */
 		if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
-			COMPLETE_WITH("ANALYZE", "VERBOSE");
-		else if (TailMatches("ANALYZE", "VERBOSE"))
+			COMPLETE_WITH("ANALYZE", "CONCURRENTLY", "VERBOSE");
+		else if (TailMatches("ANALYZE", "CONCURRENTLY", "VERBOSE"))
 			COMPLETE_WITH("ON", "OFF");
 	}
 
diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index ce48fac42ba..90cccea5bb5 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -361,14 +361,15 @@ extern void heap_multi_insert(Relation relation, TupleTableSlot **slots,
 							  BulkInsertState bistate);
 extern TM_Result heap_delete(Relation relation, const ItemPointerData *tid,
 							 CommandId cid, Snapshot crosscheck, bool wait,
-							 TM_FailureData *tmfd, bool changingPart);
+							 TM_FailureData *tmfd, bool changingPart,
+							 bool wal_logical);
 extern void heap_finish_speculative(Relation relation, const ItemPointerData *tid);
 extern void heap_abort_speculative(Relation relation, const ItemPointerData *tid);
 extern TM_Result heap_update(Relation relation, const ItemPointerData *otid,
 							 HeapTuple newtup,
 							 CommandId cid, Snapshot crosscheck, bool wait,
 							 TM_FailureData *tmfd, LockTupleMode *lockmode,
-							 TU_UpdateIndexes *update_indexes);
+							 TU_UpdateIndexes *update_indexes, bool wal_logical);
 extern TM_Result heap_lock_tuple(Relation relation, HeapTuple tuple,
 								 CommandId cid, LockTupleMode mode, LockWaitPolicy wait_policy,
 								 bool follow_updates,
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index ce3566ba949..f1f5495556b 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -104,6 +104,8 @@
 #define XLH_DELETE_CONTAINS_OLD_KEY				(1<<2)
 #define XLH_DELETE_IS_SUPER						(1<<3)
 #define XLH_DELETE_IS_PARTITION_MOVE			(1<<4)
+/* See heap_delete() */
+#define XLH_DELETE_NO_LOGICAL					(1<<5)
 
 /* convenience macro for checking whether any form of old tuple was logged */
 #define XLH_DELETE_CONTAINS_OLD						\
diff --git a/src/include/access/tableam.h b/src/include/access/tableam.h
index e2ec5289d4d..76aa993009a 100644
--- a/src/include/access/tableam.h
+++ b/src/include/access/tableam.h
@@ -22,6 +22,7 @@
 #include "access/xact.h"
 #include "commands/vacuum.h"
 #include "executor/tuptable.h"
+#include "replication/logical.h"
 #include "storage/read_stream.h"
 #include "utils/rel.h"
 #include "utils/snapshot.h"
@@ -629,6 +630,8 @@ typedef struct TableAmRoutine
 											  Relation OldIndex,
 											  bool use_sort,
 											  TransactionId OldestXmin,
+											  Snapshot snapshot,
+											  LogicalDecodingContext *decoding_ctx,
 											  TransactionId *xid_cutoff,
 											  MultiXactId *multi_cutoff,
 											  double *num_tuples,
@@ -1646,6 +1649,10 @@ table_relation_copy_data(Relation rel, const RelFileLocator *newrlocator)
  *   not needed for the relation's AM
  * - *xid_cutoff - ditto
  * - *multi_cutoff - ditto
+ * - snapshot - if != NULL, ignore data changes done by transactions that this
+ *	 (MVCC) snapshot considers still in-progress or in the future.
+ * - decoding_ctx - logical decoding context, to capture concurrent data
+ *   changes.
  *
  * Output parameters:
  * - *xid_cutoff - rel's new relfrozenxid value, may be invalid
@@ -1658,6 +1665,8 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
 								Relation OldIndex,
 								bool use_sort,
 								TransactionId OldestXmin,
+								Snapshot snapshot,
+								LogicalDecodingContext *decoding_ctx,
 								TransactionId *xid_cutoff,
 								MultiXactId *multi_cutoff,
 								double *num_tuples,
@@ -1666,6 +1675,7 @@ table_relation_copy_for_cluster(Relation OldTable, Relation NewTable,
 {
 	OldTable->rd_tableam->relation_copy_for_cluster(OldTable, NewTable, OldIndex,
 													use_sort, OldestXmin,
+													snapshot, decoding_ctx,
 													xid_cutoff, multi_cutoff,
 													num_tuples, tups_vacuumed,
 													tups_recently_dead);
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 28741988478..6a5c476294a 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -13,10 +13,15 @@
 #ifndef CLUSTER_H
 #define CLUSTER_H
 
+#include "nodes/execnodes.h"
 #include "nodes/parsenodes.h"
 #include "parser/parse_node.h"
+#include "replication/decode.h"
+#include "replication/logical.h"
 #include "storage/lock.h"
 #include "utils/relcache.h"
+#include "utils/resowner.h"
+#include "utils/tuplestore.h"
 
 
 /* flag bits for ClusterParams->options */
@@ -25,6 +30,8 @@
 #define CLUOPT_RECHECK_ISCLUSTERED 0x04 /* recheck relation state for
 										 * indisclustered */
 #define CLUOPT_ANALYZE 0x08		/* do an ANALYZE */
+#define CLUOPT_CONCURRENT 0x10	/* allow concurrent data changes */
+
 
 /* options for CLUSTER */
 typedef struct ClusterParams
@@ -33,10 +40,84 @@ typedef struct ClusterParams
 } ClusterParams;
 
 
+/*
+ * The following definitions are used by REPACK CONCURRENTLY.
+ */
+
+typedef enum
+{
+	CHANGE_INSERT,
+	CHANGE_UPDATE_OLD,
+	CHANGE_UPDATE_NEW,
+	CHANGE_DELETE
+} ConcurrentChangeKind;
+
+typedef struct ConcurrentChange
+{
+	/* See the enum above. */
+	ConcurrentChangeKind kind;
+
+	/*
+	 * The actual tuple.
+	 *
+	 * The tuple data follows the ConcurrentChange structure. Before use make
+	 * sure the tuple is correctly aligned (ConcurrentChange can be stored as
+	 * bytea) and that tuple->t_data is fixed.
+	 */
+	HeapTupleData tup_data;
+} ConcurrentChange;
+
+#define SizeOfConcurrentChange (offsetof(ConcurrentChange, tup_data) + \
+								sizeof(HeapTupleData))
+
+/*
+ * Logical decoding state.
+ *
+ * Here we store the data changes that we decode from WAL while the table
+ * contents is being copied to a new storage. Also the necessary metadata
+ * needed to apply these changes to the table is stored here.
+ */
+typedef struct RepackDecodingState
+{
+	/* The relation whose changes we're decoding. */
+	Oid			relid;
+
+	/* Replication slot name. */
+	NameData	slotname;
+
+	/*
+	 * Decoded changes are stored here. Although we try to avoid excessive
+	 * batches, it can happen that the changes need to be stored to disk. The
+	 * tuplestore does this transparently.
+	 */
+	Tuplestorestate *tstore;
+
+	/* The current number of changes in tstore. */
+	double		nchanges;
+
+	/*
+	 * Descriptor to store the ConcurrentChange structure serialized (bytea).
+	 * We can't store the tuple directly because tuplestore only supports
+	 * minimum tuple and we may need to transfer OID system column from the
+	 * output plugin. Also we need to transfer the change kind, so it's better
+	 * to put everything in the structure than to use 2 tuplestores "in
+	 * parallel".
+	 */
+	TupleDesc	tupdesc_change;
+
+	/* Tuple descriptor needed to update indexes. */
+	TupleDesc	tupdesc;
+
+	/* Slot to retrieve data from tstore. */
+	TupleTableSlot *tsslot;
+
+	ResourceOwner resowner;
+} RepackDecodingState;
+
 extern void ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel);
 
 extern void cluster_rel(RepackCommand command, Relation OldHeap, Oid indexOid,
-						ClusterParams *params);
+						ClusterParams *params, bool isTopLevel);
 extern void check_index_is_clusterable(Relation OldHeap, Oid indexOid,
 									   LOCKMODE lockmode);
 extern void mark_index_clustered(Relation rel, Oid indexOid, bool is_internal);
@@ -48,8 +129,13 @@ extern void finish_heap_swap(Oid OIDOldHeap, Oid OIDNewHeap,
 							 bool swap_toast_by_content,
 							 bool check_constraints,
 							 bool is_internal,
+							 bool reindex,
 							 TransactionId frozenXid,
 							 MultiXactId cutoffMulti,
 							 char newrelpersistence);
 
+extern bool am_decoding_for_repack(void);
+extern bool change_useless_for_repack(XLogRecordBuffer *buf);
+extern void repack_decode_concurrent_changes(LogicalDecodingContext *ctx,
+											 XLogRecPtr end_of_wal);
 #endif							/* CLUSTER_H */
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index f00e39b937d..4445724a463 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -86,10 +86,12 @@
 #define PROGRESS_REPACK_PHASE					1
 #define PROGRESS_REPACK_INDEX_RELID				2
 #define PROGRESS_REPACK_HEAP_TUPLES_SCANNED		3
-#define PROGRESS_REPACK_HEAP_TUPLES_WRITTEN		4
-#define PROGRESS_REPACK_TOTAL_HEAP_BLKS			5
-#define PROGRESS_REPACK_HEAP_BLKS_SCANNED		6
-#define PROGRESS_REPACK_INDEX_REBUILD_COUNT		7
+#define PROGRESS_REPACK_HEAP_TUPLES_INSERTED	4
+#define PROGRESS_REPACK_HEAP_TUPLES_UPDATED		5
+#define PROGRESS_REPACK_HEAP_TUPLES_DELETED		6
+#define PROGRESS_REPACK_TOTAL_HEAP_BLKS			7
+#define PROGRESS_REPACK_HEAP_BLKS_SCANNED		8
+#define PROGRESS_REPACK_INDEX_REBUILD_COUNT		9
 
 /*
  * Phases of repack (as advertised via PROGRESS_REPACK_PHASE).
@@ -98,9 +100,10 @@
 #define PROGRESS_REPACK_PHASE_INDEX_SCAN_HEAP	2
 #define PROGRESS_REPACK_PHASE_SORT_TUPLES		3
 #define PROGRESS_REPACK_PHASE_WRITE_NEW_HEAP	4
-#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES	5
-#define PROGRESS_REPACK_PHASE_REBUILD_INDEX		6
-#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP		7
+#define PROGRESS_REPACK_PHASE_CATCH_UP			5
+#define PROGRESS_REPACK_PHASE_SWAP_REL_FILES	6
+#define PROGRESS_REPACK_PHASE_REBUILD_INDEX		7
+#define PROGRESS_REPACK_PHASE_FINAL_CLEANUP		8
 
 /* Progress parameters for CREATE INDEX */
 /* 3, 4 and 5 reserved for "waitfor" metrics */
diff --git a/src/include/replication/snapbuild.h b/src/include/replication/snapbuild.h
index 34383dea776..5ee267d1c90 100644
--- a/src/include/replication/snapbuild.h
+++ b/src/include/replication/snapbuild.h
@@ -73,6 +73,7 @@ extern void FreeSnapshotBuilder(SnapBuild *builder);
 extern void SnapBuildSnapDecRefcount(Snapshot snap);
 
 extern Snapshot SnapBuildInitialSnapshot(SnapBuild *builder);
+extern Snapshot SnapBuildInitialSnapshotForRepack(SnapBuild *builder);
 extern Snapshot SnapBuildMVCCFromHistoric(Snapshot snapshot, bool in_place);
 extern const char *SnapBuildExportSnapshot(SnapBuild *builder);
 extern void SnapBuildClearExportedSnapshot(void);
diff --git a/src/include/storage/lockdefs.h b/src/include/storage/lockdefs.h
index b73bb5618e6..3785b009808 100644
--- a/src/include/storage/lockdefs.h
+++ b/src/include/storage/lockdefs.h
@@ -36,8 +36,8 @@ typedef int LOCKMODE;
 #define AccessShareLock			1	/* SELECT */
 #define RowShareLock			2	/* SELECT FOR UPDATE/FOR SHARE */
 #define RowExclusiveLock		3	/* INSERT, UPDATE, DELETE */
-#define ShareUpdateExclusiveLock 4	/* VACUUM (non-FULL), ANALYZE, CREATE
-									 * INDEX CONCURRENTLY */
+#define ShareUpdateExclusiveLock 4	/* VACUUM (non-exclusive), ANALYZE, CREATE
+									 * INDEX CONCURRENTLY, REPACK CONCURRENTLY */
 #define ShareLock				5	/* CREATE INDEX (WITHOUT CONCURRENTLY) */
 #define ShareRowExclusiveLock	6	/* like EXCLUSIVE MODE, but allows ROW
 									 * SHARE */
diff --git a/src/include/utils/snapmgr.h b/src/include/utils/snapmgr.h
index de824945f0b..0eb8ced76d3 100644
--- a/src/include/utils/snapmgr.h
+++ b/src/include/utils/snapmgr.h
@@ -64,6 +64,8 @@ extern Snapshot GetLatestSnapshot(void);
 extern void SnapshotSetCommandId(CommandId curcid);
 
 extern Snapshot CopySnapshot(Snapshot snapshot);
+extern void FreeSnapshot(Snapshot snapshot);
+
 extern Snapshot GetCatalogSnapshot(Oid relid);
 extern Snapshot GetNonHistoricCatalogSnapshot(Oid relid);
 extern void InvalidateCatalogSnapshot(void);
diff --git a/src/test/modules/injection_points/Makefile b/src/test/modules/injection_points/Makefile
index a41d781f8c9..6b6a3b84a57 100644
--- a/src/test/modules/injection_points/Makefile
+++ b/src/test/modules/injection_points/Makefile
@@ -14,8 +14,11 @@ REGRESS_OPTS = --dlpath=$(top_builddir)/src/test/regress
 
 ISOLATION = basic \
 	    inplace \
+	    repack \
+	    repack_toast \
 	    syscache-update-pruned \
 	    heap_lock_update
+ISOLATION_OPTS = --temp-config $(top_srcdir)/src/test/modules/injection_points/logical.conf
 
 # The injection points are cluster-wide, so disable installcheck
 NO_INSTALLCHECK = 1
diff --git a/src/test/modules/injection_points/expected/repack.out b/src/test/modules/injection_points/expected/repack.out
new file mode 100644
index 00000000000..b575e9052ee
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack.out
@@ -0,0 +1,113 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock change_existing change_new change_subxact1 change_subxact2 check2 wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_test USING INDEX repack_test_pkey;
+ <waiting ...>
+step change_existing: 
+	UPDATE repack_test SET i=10 where i=1;
+	UPDATE repack_test SET j=20 where i=2;
+	UPDATE repack_test SET i=30 where i=3;
+	UPDATE repack_test SET i=40 where i=30;
+	DELETE FROM repack_test WHERE i=4;
+
+step change_new: 
+	INSERT INTO repack_test(i, j) VALUES (5, 5), (6, 6), (7, 7), (8, 8);
+	UPDATE repack_test SET i=50 where i=5;
+	UPDATE repack_test SET j=60 where i=6;
+	DELETE FROM repack_test WHERE i=7;
+
+step change_subxact1: 
+	BEGIN;
+	INSERT INTO repack_test(i, j) VALUES (100, 100);
+	SAVEPOINT s1;
+	UPDATE repack_test SET i=101 where i=100;
+	SAVEPOINT s2;
+	UPDATE repack_test SET i=102 where i=101;
+	COMMIT;
+
+step change_subxact2: 
+	BEGIN;
+	SAVEPOINT s1;
+	INSERT INTO repack_test(i, j) VALUES (110, 110);
+	ROLLBACK TO SAVEPOINT s1;
+	INSERT INTO repack_test(i, j) VALUES (110, 111);
+	COMMIT;
+
+step check2: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_test';
+
+	SELECT i, j FROM repack_test ORDER BY i, j;
+
+	INSERT INTO data_s2(i, j)
+	SELECT i, j FROM repack_test;
+
+  i|  j
+---+---
+  2| 20
+  6| 60
+  8|  8
+ 10|  1
+ 40|  3
+ 50|  5
+102|100
+110|111
+(8 rows)
+
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_test';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_test ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_test;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    2
+(1 row)
+
+  i|  j
+---+---
+  2| 20
+  6| 60
+  8|  8
+ 10|  1
+ 40|  3
+ 50|  5
+102|100
+110|111
+(8 rows)
+
+count
+-----
+    0
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/expected/repack_toast.out b/src/test/modules/injection_points/expected/repack_toast.out
new file mode 100644
index 00000000000..4f866a74e32
--- /dev/null
+++ b/src/test/modules/injection_points/expected/repack_toast.out
@@ -0,0 +1,64 @@
+Parsed test spec with 2 sessions
+
+starting permutation: wait_before_lock change check2 wakeup_before_lock check1
+injection_points_attach
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: 
+	REPACK (CONCURRENTLY) repack_test;
+ <waiting ...>
+step change: 
+	UPDATE repack_test SET j=get_long_string() where i=2;
+	DELETE FROM repack_test WHERE i=3;
+	INSERT INTO repack_test(i, j) VALUES (4, get_long_string());
+
+step check2: 
+	INSERT INTO relfilenodes(node)
+	SELECT c2.relfilenode
+	FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid
+	WHERE c1.relname='repack_test';
+
+	INSERT INTO data_s2(i, j)
+	SELECT i, j FROM repack_test;
+
+step wakeup_before_lock: 
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+
+injection_points_wakeup
+-----------------------
+                       
+(1 row)
+
+step wait_before_lock: <... completed>
+step check1: 
+	INSERT INTO relfilenodes(node)
+	SELECT c2.relfilenode
+	FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid
+	WHERE c1.relname='repack_test';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_test;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+
+count
+-----
+    4
+(1 row)
+
+count
+-----
+    0
+(1 row)
+
+injection_points_detach
+-----------------------
+                       
+(1 row)
+
diff --git a/src/test/modules/injection_points/logical.conf b/src/test/modules/injection_points/logical.conf
new file mode 100644
index 00000000000..e3d257315fa
--- /dev/null
+++ b/src/test/modules/injection_points/logical.conf
@@ -0,0 +1 @@
+wal_level = logical
diff --git a/src/test/modules/injection_points/meson.build b/src/test/modules/injection_points/meson.build
index fcc85414515..bbf1f6e665c 100644
--- a/src/test/modules/injection_points/meson.build
+++ b/src/test/modules/injection_points/meson.build
@@ -45,11 +45,15 @@ tests += {
     'specs': [
       'basic',
       'inplace',
+      'repack',
+      'repack_toast',
       'syscache-update-pruned',
       'heap_lock_update',
     ],
     'runningcheck': false, # see syscache-update-pruned
     # Some tests wait for all snapshots, so avoid parallel execution
     'runningcheck-parallel': false,
+    # 'repack' requires wal_level = 'logical'.
+    'regress_args': ['--temp-config', files('logical.conf')],
   },
 }
diff --git a/src/test/modules/injection_points/specs/repack.spec b/src/test/modules/injection_points/specs/repack.spec
new file mode 100644
index 00000000000..d727a9b056b
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack.spec
@@ -0,0 +1,142 @@
+# REPACK (CONCURRENTLY) ... USING INDEX ...;
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	CREATE TABLE repack_test(i int PRIMARY KEY, j int);
+	INSERT INTO repack_test(i, j) VALUES (1, 1), (2, 2), (3, 3), (4, 4);
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j int);
+	CREATE TABLE data_s2(i int, j int);
+}
+
+teardown
+{
+	DROP TABLE repack_test;
+	DROP EXTENSION injection_points;
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_test USING INDEX repack_test_pkey;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_test';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	SELECT i, j FROM repack_test ORDER BY i, j;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_test;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+	SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+# Change the existing data. UPDATE changes both key and non-key columns. Also
+# update one row twice to test whether tuple version generated by this session
+# can be found.
+step change_existing
+{
+	UPDATE repack_test SET i=10 where i=1;
+	UPDATE repack_test SET j=20 where i=2;
+	UPDATE repack_test SET i=30 where i=3;
+	UPDATE repack_test SET i=40 where i=30;
+	DELETE FROM repack_test WHERE i=4;
+}
+# Insert new rows and UPDATE / DELETE some of them. Again, update both key and
+# non-key column.
+step change_new
+{
+	INSERT INTO repack_test(i, j) VALUES (5, 5), (6, 6), (7, 7), (8, 8);
+	UPDATE repack_test SET i=50 where i=5;
+	UPDATE repack_test SET j=60 where i=6;
+	DELETE FROM repack_test WHERE i=7;
+}
+
+# When applying concurrent data changes, we should see the effects of an
+# in-progress subtransaction.
+#
+# XXX Not sure this test is useful now - it was designed for the patch that
+# preserves tuple visibility and which therefore modifies
+# TransactionIdIsCurrentTransactionId().
+step change_subxact1
+{
+	BEGIN;
+	INSERT INTO repack_test(i, j) VALUES (100, 100);
+	SAVEPOINT s1;
+	UPDATE repack_test SET i=101 where i=100;
+	SAVEPOINT s2;
+	UPDATE repack_test SET i=102 where i=101;
+	COMMIT;
+}
+
+# When applying concurrent data changes, we should not see the effects of a
+# rolled back subtransaction.
+#
+# XXX Is this test useful? See above.
+step change_subxact2
+{
+	BEGIN;
+	SAVEPOINT s1;
+	INSERT INTO repack_test(i, j) VALUES (110, 110);
+	ROLLBACK TO SAVEPOINT s1;
+	INSERT INTO repack_test(i, j) VALUES (110, 111);
+	COMMIT;
+}
+
+# Check the table from the perspective of s2.
+step check2
+{
+	INSERT INTO relfilenodes(node)
+	SELECT relfilenode FROM pg_class WHERE relname='repack_test';
+
+	SELECT i, j FROM repack_test ORDER BY i, j;
+
+	INSERT INTO data_s2(i, j)
+	SELECT i, j FROM repack_test;
+}
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	change_existing
+	change_new
+	change_subxact1
+	change_subxact2
+	check2
+	wakeup_before_lock
+	check1
diff --git a/src/test/modules/injection_points/specs/repack_toast.spec b/src/test/modules/injection_points/specs/repack_toast.spec
new file mode 100644
index 00000000000..b48abf21450
--- /dev/null
+++ b/src/test/modules/injection_points/specs/repack_toast.spec
@@ -0,0 +1,105 @@
+# REPACK (CONCURRENTLY);
+#
+# Test handling of TOAST. At the same time, no tuplesort.
+setup
+{
+	CREATE EXTENSION injection_points;
+
+	-- Return a string that needs to be TOASTed.
+	CREATE FUNCTION get_long_string()
+	RETURNS text
+	LANGUAGE sql as $$
+		SELECT string_agg(chr(65 + trunc(25 * random())::int), '')
+		FROM generate_series(1, 2048) s(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());
+
+	CREATE TABLE relfilenodes(node oid);
+
+	CREATE TABLE data_s1(i int, j text);
+	CREATE TABLE data_s2(i int, j text);
+}
+
+teardown
+{
+	DROP TABLE repack_test;
+	DROP EXTENSION injection_points;
+	DROP FUNCTION get_long_string();
+
+	DROP TABLE relfilenodes;
+	DROP TABLE data_s1;
+	DROP TABLE data_s2;
+}
+
+session s1
+setup
+{
+	SELECT injection_points_set_local();
+	SELECT injection_points_attach('repack-concurrently-before-lock', 'wait');
+}
+# Perform the initial load and wait for s2 to do some data changes.
+step wait_before_lock
+{
+	REPACK (CONCURRENTLY) repack_test;
+}
+# Check the table from the perspective of s1.
+#
+# Besides the contents, we also check that relfilenode has changed.
+
+# Have each session write the contents into a table and use FULL JOIN to check
+# if the outputs are identical.
+step check1
+{
+	INSERT INTO relfilenodes(node)
+	SELECT c2.relfilenode
+	FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid
+	WHERE c1.relname='repack_test';
+
+	SELECT count(DISTINCT node) FROM relfilenodes;
+
+	INSERT INTO data_s1(i, j)
+	SELECT i, j FROM repack_test;
+
+	SELECT count(*)
+	FROM data_s1 d1 FULL JOIN data_s2 d2 USING (i, j)
+	WHERE d1.i ISNULL OR d2.i ISNULL;
+}
+teardown
+{
+    SELECT injection_points_detach('repack-concurrently-before-lock');
+}
+
+session s2
+step change
+{
+	UPDATE repack_test SET j=get_long_string() where i=2;
+	DELETE FROM repack_test WHERE i=3;
+	INSERT INTO repack_test(i, j) VALUES (4, get_long_string());
+}
+# Check the table from the perspective of s2.
+step check2
+{
+	INSERT INTO relfilenodes(node)
+	SELECT c2.relfilenode
+	FROM pg_class c1 JOIN pg_class c2 ON c2.oid = c1.oid OR c2.oid = c1.reltoastrelid
+	WHERE c1.relname='repack_test';
+
+	INSERT INTO data_s2(i, j)
+	SELECT i, j FROM repack_test;
+}
+step wakeup_before_lock
+{
+	SELECT injection_points_wakeup('repack-concurrently-before-lock');
+}
+
+# Test if data changes introduced while one session is performing REPACK
+# CONCURRENTLY find their way into the table.
+permutation
+	wait_before_lock
+	change
+	check2
+	wakeup_before_lock
+	check1
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 48461550636..470920f0d16 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2014,7 +2014,7 @@ pg_stat_progress_cluster| SELECT pid,
     phase,
     repack_index_relid AS cluster_index_relid,
     heap_tuples_scanned,
-    heap_tuples_written,
+    (heap_tuples_inserted + heap_tuples_updated) AS heap_tuples_written,
     heap_blks_total,
     heap_blks_scanned,
     index_rebuild_count
@@ -2094,17 +2094,20 @@ pg_stat_progress_repack| SELECT s.pid,
             WHEN 2 THEN 'index scanning heap'::text
             WHEN 3 THEN 'sorting tuples'::text
             WHEN 4 THEN 'writing new heap'::text
-            WHEN 5 THEN 'swapping relation files'::text
-            WHEN 6 THEN 'rebuilding index'::text
-            WHEN 7 THEN 'performing final cleanup'::text
+            WHEN 5 THEN 'catch-up'::text
+            WHEN 6 THEN 'swapping relation files'::text
+            WHEN 7 THEN 'rebuilding index'::text
+            WHEN 8 THEN 'performing final cleanup'::text
             ELSE NULL::text
         END AS phase,
     (s.param3)::oid AS repack_index_relid,
     s.param4 AS heap_tuples_scanned,
-    s.param5 AS heap_tuples_written,
-    s.param6 AS heap_blks_total,
-    s.param7 AS heap_blks_scanned,
-    s.param8 AS index_rebuild_count
+    s.param5 AS heap_tuples_inserted,
+    s.param6 AS heap_tuples_updated,
+    s.param7 AS heap_tuples_deleted,
+    s.param8 AS heap_blks_total,
+    s.param9 AS heap_blks_scanned,
+    s.param10 AS index_rebuild_count
    FROM (pg_stat_get_progress_info('REPACK'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
      LEFT JOIN pg_database d ON ((s.datid = d.oid)));
 pg_stat_progress_vacuum| SELECT s.pid,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c81d93d0e5a..a0b7b38a5e2 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -419,6 +419,7 @@ CatCacheHeader
 CatalogId
 CatalogIdMapEntry
 CatalogIndexState
+ChangeDest
 ChangeVarNodes_callback
 ChangeVarNodes_context
 CheckPoint
@@ -495,6 +496,8 @@ CompressFileHandle
 CompressionLocation
 CompressorState
 ComputeXidHorizonsResult
+ConcurrentChange
+ConcurrentChangeKind
 ConditionVariable
 ConditionVariableMinimallyPadded
 ConditionalStack
@@ -1274,6 +1277,7 @@ IndexElem
 IndexFetchHeapData
 IndexFetchTableData
 IndexInfo
+IndexInsertState
 IndexList
 IndexOnlyScan
 IndexOnlyScanState
@@ -2570,6 +2574,7 @@ ReorderBufferUpdateProgressTxnCB
 ReorderTuple
 RepOriginId
 RepackCommand
+RepackDecodingState
 RepackStmt
 ReparameterizeForeignPathByChild_function
 ReplaceVarsFromTargetList_context
-- 
2.47.3


--=-=-=
Content-Type: text/x-diff
Content-Disposition: attachment;
 filename=v29-0005-Use-background-worker-to-do-logical-decoding.patch



^ permalink  raw  reply  [nested|flat] 3+ messages in thread


end of thread, other threads:[~2026-01-08 16:47 UTC | newest]

Thread overview: 3+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2019-07-06 14:39 [PATCH 2/2] review reworks Tomas Vondra <[email protected]>
2023-07-20 17:19 [PATCH v8 3/4] Convert pg_restore's ready_list to a priority queue. Nathan Bossart <[email protected]>
2026-01-08 16:47 [PATCH 4/6] Add CONCURRENTLY option to REPACK command. Antonin Houska <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox