v1-0005-pg_waldump-Add-support-for-archived-WAL-decoding.patch

application/x-patch

Filename: v1-0005-pg_waldump-Add-support-for-archived-WAL-decoding.patch
Type: application/x-patch
Part: 4
Message: pg_waldump: support decoding of WAL inside tarfile

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v1-0005
Subject: pg_waldump: Add support for archived WAL decoding.
File+
doc/src/sgml/ref/pg_waldump.sgml 6 2
src/bin/pg_waldump/astreamer_waldump.c 378 0
src/bin/pg_waldump/Makefile 6 1
src/bin/pg_waldump/meson.build 3 1
src/bin/pg_waldump/pg_waldump.c 299 62
src/bin/pg_waldump/pg_waldump.h 20 1
src/bin/pg_waldump/t/001_basic.pl 52 12
src/tools/pgindent/typedefs.list 1 0
From 21d9d604ca4b4ab08c5bc32decf1afc8d881c43c Mon Sep 17 00:00:00 2001
From: Amul Sul <sulamul@gmail.com>
Date: Wed, 16 Jul 2025 18:37:59 +0530
Subject: [PATCH v1 5/9] pg_waldump: Add support for archived WAL decoding.

pg_waldump can now accept the path to a tar archive containing WAL
files and decode them. This feature was added primarily for
pg_verifybackup, which previously disabled WAL parsing for
tar-formatted backups.

Note that this patch requires that the WAL files within the archive be
in sequential order; an error will be reported otherwise. The next
patch is planned to remove this restriction.
---
 doc/src/sgml/ref/pg_waldump.sgml       |   8 +-
 src/bin/pg_waldump/Makefile            |   7 +-
 src/bin/pg_waldump/astreamer_waldump.c | 378 +++++++++++++++++++++++++
 src/bin/pg_waldump/meson.build         |   4 +-
 src/bin/pg_waldump/pg_waldump.c        | 361 +++++++++++++++++++----
 src/bin/pg_waldump/pg_waldump.h        |  21 +-
 src/bin/pg_waldump/t/001_basic.pl      |  64 ++++-
 src/tools/pgindent/typedefs.list       |   1 +
 8 files changed, 765 insertions(+), 79 deletions(-)
 create mode 100644 src/bin/pg_waldump/astreamer_waldump.c

diff --git a/doc/src/sgml/ref/pg_waldump.sgml b/doc/src/sgml/ref/pg_waldump.sgml
index ce23add5577..d004bb0f67e 100644
--- a/doc/src/sgml/ref/pg_waldump.sgml
+++ b/doc/src/sgml/ref/pg_waldump.sgml
@@ -141,13 +141,17 @@ PostgreSQL documentation
       <term><option>--path=<replaceable>path</replaceable></option></term>
       <listitem>
        <para>
-        Specifies a directory to search for WAL segment files or a
-        directory with a <literal>pg_wal</literal> subdirectory that
+        Specifies a tar archive or a directory to search for WAL segment files
+        or a directory with a <literal>pg_wal</literal> subdirectory that
         contains such files.  The default is to search in the current
         directory, the <literal>pg_wal</literal> subdirectory of the
         current directory, and the <literal>pg_wal</literal> subdirectory
         of <envar>PGDATA</envar>.
        </para>
+       <para>
+        If a tar archive is provided, its WAL segment files must be in
+        sequential order; otherwise, an error will be reported.
+       </para>
       </listitem>
      </varlistentry>
 
diff --git a/src/bin/pg_waldump/Makefile b/src/bin/pg_waldump/Makefile
index 4c1ee649501..b234613eb50 100644
--- a/src/bin/pg_waldump/Makefile
+++ b/src/bin/pg_waldump/Makefile
@@ -3,6 +3,9 @@
 PGFILEDESC = "pg_waldump - decode and display WAL"
 PGAPPICON=win32
 
+# make these available to TAP test scripts
+export TAR
+
 subdir = src/bin/pg_waldump
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
@@ -12,11 +15,13 @@ OBJS = \
 	$(WIN32RES) \
 	compat.o \
 	pg_waldump.o \
+	astreamer_waldump.o \
 	rmgrdesc.o \
 	xlogreader.o \
 	xlogstats.o
 
-override CPPFLAGS := -DFRONTEND $(CPPFLAGS)
+override CPPFLAGS := -DFRONTEND -I$(libpq_srcdir) $(CPPFLAGS)
+LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils
 
 RMGRDESCSOURCES = $(sort $(notdir $(wildcard $(top_srcdir)/src/backend/access/rmgrdesc/*desc*.c)))
 RMGRDESCOBJS = $(patsubst %.c,%.o,$(RMGRDESCSOURCES))
diff --git a/src/bin/pg_waldump/astreamer_waldump.c b/src/bin/pg_waldump/astreamer_waldump.c
new file mode 100644
index 00000000000..d0ac903c54e
--- /dev/null
+++ b/src/bin/pg_waldump/astreamer_waldump.c
@@ -0,0 +1,378 @@
+/*-------------------------------------------------------------------------
+ *
+ * astreamer_waldump.c
+ *		A generic facility for reading WAL data from tar archives via archive
+ *		streamer.
+ *
+ * Portions Copyright (c) 2025, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/bin/pg_waldump/astreamer_waldump.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <unistd.h>
+
+#include "access/xlog_internal.h"
+#include "access/xlogdefs.h"
+#include "common/logging.h"
+#include "fe_utils/simple_list.h"
+#include "pg_waldump.h"
+
+/*
+ * How many bytes should we try to read from a file at once?
+ */
+#define READ_CHUNK_SIZE				(128 * 1024)
+
+typedef struct astreamer_waldump
+{
+	/* These fields don't change once initialized. */
+	astreamer	base;
+	XLogSegNo	startSegNo;
+	XLogSegNo	endSegNo;
+	XLogDumpPrivate *privateInfo;
+
+	/* These fields change with archive member. */
+	bool		skipThisSeg;
+	XLogSegNo	nextSegNo;		/* Next expected segment to stream */
+} astreamer_waldump;
+
+static int	astreamer_archive_read(XLogDumpPrivate *privateInfo);
+static void astreamer_waldump_content(astreamer *streamer,
+									  astreamer_member *member,
+									  const char *data, int len,
+									  astreamer_archive_context context);
+static void astreamer_waldump_finalize(astreamer *streamer);
+static void astreamer_waldump_free(astreamer *streamer);
+
+static bool member_is_relevant_wal(astreamer_member *member,
+								   TimeLineID startTimeLineID,
+								   XLogSegNo startSegNo,
+								   XLogSegNo endSegNo,
+								   XLogSegNo nextSegNo,
+								   XLogSegNo *curSegNo,
+								   TimeLineID *curSegTimeline);
+
+static const astreamer_ops astreamer_waldump_ops = {
+	.content = astreamer_waldump_content,
+	.finalize = astreamer_waldump_finalize,
+	.free = astreamer_waldump_free
+};
+
+/*
+ * Copies WAL data from astreamer to readBuff; if unavailable, fetches more
+ * from the tar archive via astreamer.
+ */
+int
+astreamer_wal_read(char *readBuff, XLogRecPtr targetPagePtr, Size count,
+				   XLogDumpPrivate *privateInfo)
+{
+	char	   *p = readBuff;
+	Size		nbytes = count;
+	XLogRecPtr	recptr = targetPagePtr;
+	volatile StringInfo astreamer_buf = privateInfo->archive_streamer_buf;
+
+	while (nbytes > 0)
+	{
+		char	   *buf = astreamer_buf->data;
+		int			len = astreamer_buf->len;
+
+		/* WAL record range that the buffer contains */
+		XLogRecPtr	endPtr = privateInfo->archive_streamer_read_ptr;
+		XLogRecPtr	startPtr = (endPtr > len) ? endPtr - len : 0;
+
+		/*
+		 * Ignore existing data if the required target page has not yet been
+		 * read.
+		 */
+		if (recptr >= endPtr)
+		{
+			len = 0;
+
+			/* Reset the buffer */
+			resetStringInfo(astreamer_buf);
+		}
+
+		if (len > 0 && recptr > startPtr)
+		{
+			int			skipBytes = 0;
+
+			/*
+			 * The required offset is not at the start of the archive streamer
+			 * buffer, so skip bytes until reaching the desired offset of the
+			 * target page.
+			 */
+			skipBytes = recptr - startPtr;
+
+			buf += skipBytes;
+			len -= skipBytes;
+		}
+
+		if (len > 0)
+		{
+			int			readBytes = len >= nbytes ? nbytes : len;
+
+			/*
+			 * Ensure we are reading the correct page, unless we've received an
+			 * invalid record pointer. In that specific case, it's acceptable
+			 * to read any page.
+			 */
+			Assert(XLogRecPtrIsInvalid(recptr) ||
+				   (recptr >= startPtr && recptr < endPtr));
+
+			memcpy(p, buf, readBytes);
+
+			/* Update state for read */
+			nbytes -= readBytes;
+			p += readBytes;
+			recptr += readBytes;
+		}
+		else
+		{
+			/* Fetch more data */
+			if (astreamer_archive_read(privateInfo) == 0)
+				break;			/* No data remaining */
+		}
+	}
+
+	return (count - nbytes) ? (count - nbytes) : -1;
+}
+
+/*
+ * Reads the archive and passes it to the archive streamer for decompression.
+ */
+static int
+astreamer_archive_read(XLogDumpPrivate *privateInfo)
+{
+	int			rc;
+	char	   *buffer;
+
+	buffer = pg_malloc(READ_CHUNK_SIZE * sizeof(uint8));
+
+	/* Read more data from the tar file */
+	rc = read(privateInfo->archive_fd, buffer, READ_CHUNK_SIZE);
+	if (rc < 0)
+		pg_fatal("could not read file \"%s\": %m",
+				 privateInfo->archive_name);
+
+	/*
+	 * Decrypt (if required), and then parse the previously read contents of
+	 * the tar file.
+	 */
+	if (rc > 0)
+		astreamer_content(privateInfo->archive_streamer, NULL,
+						  buffer, rc, ASTREAMER_UNKNOWN);
+	pg_free(buffer);
+
+	return rc;
+}
+
+/*
+ * Create an astreamer that can read WAL from tar file.
+ */
+astreamer *
+astreamer_waldump_content_new(astreamer *next, XLogRecPtr startptr,
+							  XLogRecPtr endPtr, XLogDumpPrivate *privateInfo)
+{
+	astreamer_waldump *streamer;
+
+	streamer = palloc0(sizeof(astreamer_waldump));
+	*((const astreamer_ops **) &streamer->base.bbs_ops) =
+		&astreamer_waldump_ops;
+
+	streamer->base.bbs_next = next;
+	initStringInfo(&streamer->base.bbs_buffer);
+
+	if (XLogRecPtrIsInvalid(startptr))
+		streamer->startSegNo = 0;
+	else
+	{
+		XLByteToSeg(startptr, streamer->startSegNo, WalSegSz);
+
+		/*
+		 * Initialize the record pointer to the beginning of the first
+		 * segment; this pointer will track the WAL record reading status.
+		 */
+		XLogSegNoOffsetToRecPtr(streamer->startSegNo, 0, WalSegSz,
+								privateInfo->archive_streamer_read_ptr);
+	}
+
+	if (XLogRecPtrIsInvalid(endPtr))
+		streamer->endSegNo = UINT64_MAX;
+	else
+		XLByteToSeg(endPtr, streamer->endSegNo, WalSegSz);
+
+	streamer->nextSegNo = streamer->startSegNo;
+	streamer->privateInfo = privateInfo;
+
+	return &streamer->base;
+}
+
+/*
+ * Main entry point of the archive streamer for reading WAL from a tar file.
+ */
+static void
+astreamer_waldump_content(astreamer *streamer, astreamer_member *member,
+						  const char *data, int len,
+						  astreamer_archive_context context)
+{
+	astreamer_waldump *mystreamer = (astreamer_waldump *) streamer;
+	XLogDumpPrivate *privateInfo = mystreamer->privateInfo;
+
+	Assert(context != ASTREAMER_UNKNOWN);
+
+	switch (context)
+	{
+		case ASTREAMER_MEMBER_HEADER:
+			{
+				XLogSegNo	segNo;
+				TimeLineID	timeline;
+
+				pg_log_debug("pg_waldump: reading \"%s\"", member->pathname);
+
+				mystreamer->skipThisSeg = false;
+
+				if (!member_is_relevant_wal(member,
+											privateInfo->timeline,
+											mystreamer->startSegNo,
+											mystreamer->endSegNo,
+											mystreamer->nextSegNo,
+											&segNo, &timeline))
+				{
+					mystreamer->skipThisSeg = true;
+					break;
+				}
+
+				/*
+				 * If nextSegNo is 0, the check is skipped, and any WAL file
+				 * can be read -- this typically occurs during initial
+				 * verification.
+				 */
+				if (mystreamer->nextSegNo == 0)
+					break;
+
+				/* WAL segments must be archived in order */
+				if (mystreamer->nextSegNo != segNo)
+				{
+					pg_log_error("WAL files are not archived in sequential order");
+					pg_log_error_detail("Expecting segment number " UINT64_FORMAT " but found " UINT64_FORMAT ".",
+										mystreamer->nextSegNo, segNo);
+					exit(1);
+				}
+
+				/*
+				 * We track the reading of WAL segment records using a pointer
+				 * that's continuously incremented by the length of the
+				 * received data. This pointer is crucial for serving WAL page
+				 * requests from the WAL decoding routine, so it must be
+				 * accurate.
+				 */
+#ifdef USE_ASSERT_CHECKING
+				if (mystreamer->nextSegNo != 0)
+				{
+					XLogRecPtr	recPtr;
+
+					XLogSegNoOffsetToRecPtr(segNo, 0, WalSegSz, recPtr);
+					Assert(privateInfo->archive_streamer_read_ptr == recPtr);
+				}
+#endif
+
+				/* Save the timeline */
+				privateInfo->timeline = timeline;
+
+				/* Update the next expected segment number */
+				mystreamer->nextSegNo += 1;
+			}
+			break;
+
+		case ASTREAMER_MEMBER_CONTENTS:
+			/* Skip this segment */
+			if (mystreamer->skipThisSeg)
+				break;
+
+			/* Or, copy contents to buffer */
+			privateInfo->archive_streamer_read_ptr += len;
+			astreamer_buffer_bytes(streamer, &data, &len, len);
+			break;
+
+		case ASTREAMER_MEMBER_TRAILER:
+			break;
+
+		case ASTREAMER_ARCHIVE_TRAILER:
+			break;
+
+		default:
+			/* Shouldn't happen. */
+			pg_fatal("unexpected state while parsing tar file");
+	}
+}
+
+/*
+ * End-of-stream processing for a astreamer_waldump stream.
+ */
+static void
+astreamer_waldump_finalize(astreamer *streamer)
+{
+	Assert(streamer->bbs_next == NULL);
+}
+
+/*
+ * Free memory associated with a astreamer_waldump stream.
+ */
+static void
+astreamer_waldump_free(astreamer *streamer)
+{
+	Assert(streamer->bbs_next == NULL);
+
+	pfree(streamer->bbs_buffer.data);
+	pfree(streamer);
+}
+
+/*
+ * Returns true if the archive member name matches the WAL naming format and
+ * the corresponding WAL segment falls within the WAL decoding target range;
+ * otherwise, returns false.
+ */
+static bool
+member_is_relevant_wal(astreamer_member *member, TimeLineID startTimeLineID,
+					   XLogSegNo startSegNo, XLogSegNo endSegNo,
+					   XLogSegNo nextSegNo, XLogSegNo *curSegNo,
+					   TimeLineID *curSegTimeline)
+{
+	int			pathlen;
+	XLogSegNo	segNo;
+	TimeLineID	timeline;
+	char	   *fname;
+
+	/* We are only interested in normal files. */
+	if (member->is_directory || member->is_link)
+		return false;
+
+	pathlen = strlen(member->pathname);
+	if (pathlen < XLOG_FNAME_LEN)
+		return false;
+
+	/* WAL file could be with full path */
+	fname = member->pathname + (pathlen - XLOG_FNAME_LEN);
+	if (!IsXLogFileName(fname))
+		return false;
+
+	/* Parse position from file */
+	XLogFromFileName(fname, &timeline, &segNo, WalSegSz);
+
+	/* Ignore the older timeline */
+	if (startTimeLineID > timeline)
+		return false;
+
+	/* Skip if the current segment is not the desired one */
+	if (startSegNo > segNo || endSegNo < segNo)
+		return false;
+
+	*curSegNo = segNo;
+	*curSegTimeline = timeline;
+
+	return true;
+}
diff --git a/src/bin/pg_waldump/meson.build b/src/bin/pg_waldump/meson.build
index 937e0d68841..2a0300dc339 100644
--- a/src/bin/pg_waldump/meson.build
+++ b/src/bin/pg_waldump/meson.build
@@ -3,6 +3,7 @@
 pg_waldump_sources = files(
   'compat.c',
   'pg_waldump.c',
+  'astreamer_waldump.c',
   'rmgrdesc.c',
 )
 
@@ -18,7 +19,7 @@ endif
 
 pg_waldump = executable('pg_waldump',
   pg_waldump_sources,
-  dependencies: [frontend_code, lz4, zstd],
+  dependencies: [frontend_code, lz4, zstd, libpq],
   c_args: ['-DFRONTEND'], # needed for xlogreader et al
   kwargs: default_bin_args,
 )
@@ -29,6 +30,7 @@ tests += {
   'sd': meson.current_source_dir(),
   'bd': meson.current_build_dir(),
   'tap': {
+    'env': {'TAR': tar.found() ? tar.full_path() : ''},
     'tests': [
       't/001_basic.pl',
       't/002_save_fullpage.pl',
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index 4775275c07a..64f3a65b735 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -182,10 +182,9 @@ open_file_in_directory(const char *directory, const char *fname)
 {
 	int			fd = -1;
 	char		fpath[MAXPGPATH];
+	char	   *dir = directory ? (char *) directory : ".";
 
-	Assert(directory != NULL);
-
-	snprintf(fpath, MAXPGPATH, "%s/%s", directory, fname);
+	snprintf(fpath, MAXPGPATH, "%s/%s", dir, fname);
 	fd = open(fpath, O_RDONLY | PG_BINARY, 0);
 
 	if (fd < 0 && errno != ENOENT)
@@ -326,6 +325,160 @@ identify_target_directory(char *directory, char *fname)
 	return NULL;				/* not reached */
 }
 
+/*
+ * Returns true if the given file is a tar archive and outputs its compression
+ * algorithm.
+ */
+static bool
+is_tar_file(const char *fname, pg_compress_algorithm *compression)
+{
+	int			fname_len = strlen(fname);
+	pg_compress_algorithm compress_algo;
+
+	/* Now, check the compression type of the tar */
+	if (fname_len > 4 &&
+		strcmp(fname + fname_len - 4, ".tar") == 0)
+		compress_algo = PG_COMPRESSION_NONE;
+	else if (fname_len > 4 &&
+			 strcmp(fname + fname_len - 4, ".tgz") == 0)
+		compress_algo = PG_COMPRESSION_GZIP;
+	else if (fname_len > 7 &&
+			 strcmp(fname + fname_len - 7, ".tar.gz") == 0)
+		compress_algo = PG_COMPRESSION_GZIP;
+	else if (fname_len > 8 &&
+			 strcmp(fname + fname_len - 8, ".tar.lz4") == 0)
+		compress_algo = PG_COMPRESSION_LZ4;
+	else if (fname_len > 8 &&
+			 strcmp(fname + fname_len - 8, ".tar.zst") == 0)
+		compress_algo = PG_COMPRESSION_ZSTD;
+	else
+		return false;
+
+	*compression = compress_algo;
+
+	return true;
+}
+
+/*
+ * Creates an appropriate chain of archive streamers for reading the given
+ * tar archive.
+ */
+static void
+setup_astreamer(XLogDumpPrivate *private, pg_compress_algorithm compression,
+				XLogRecPtr startptr, XLogRecPtr endptr)
+{
+	astreamer  *streamer = NULL;
+
+	streamer = astreamer_waldump_content_new(NULL, startptr, endptr, private);
+
+	/*
+	 * Final extracted WAL data will reside in this streamer. However, since
+	 * it sits at the bottom of the stack and isn't designed to propagate data
+	 * upward, we need to hold a pointer to its data buffer in order to copy.
+	 */
+	private->archive_streamer_buf = &streamer->bbs_buffer;
+
+	/* Before that we must parse the tar archive. */
+	streamer = astreamer_tar_parser_new(streamer);
+
+	/* Before that we must decompress, if archive is compressed. */
+	if (compression == PG_COMPRESSION_GZIP)
+		streamer = astreamer_gzip_decompressor_new(streamer);
+	else if (compression == PG_COMPRESSION_LZ4)
+		streamer = astreamer_lz4_decompressor_new(streamer);
+	else if (compression == PG_COMPRESSION_ZSTD)
+		streamer = astreamer_zstd_decompressor_new(streamer);
+
+	private->archive_streamer = streamer;
+}
+
+/*
+ * Initializes the archive reader for a tar file.
+ */
+static void
+init_tar_archive_reader(XLogDumpPrivate *private, char *waldir,
+						pg_compress_algorithm compression)
+{
+	int			fd;
+
+	/* Now, the tar archive and store its file descriptor */
+	fd = open_file_in_directory(waldir, private->archive_name);
+
+	if (fd < 0)
+		pg_fatal("could not open file \"%s\"", private->archive_name);
+
+	private->archive_fd = fd;
+
+	/* Setup tar archive reading facility */
+	setup_astreamer(private, compression, private->startptr, private->endptr);
+}
+
+/*
+ * Release the archive streamer chain and close the archive file.
+ */
+static void
+free_tar_archive_reader(XLogDumpPrivate *private)
+{
+	/*
+	 * NB: Normally, astreamer_finalize() is called before astreamer_free() to
+	 * flush any remaining buffered data or to ensure the end of the tar
+	 * archive is reached. However, when decoding a WAL file, once we hit the
+	 * end LSN, any remaining WAL data in the buffer or the tar archive's
+	 * unreached end can be safely ignored.
+	 */
+	astreamer_free(private->archive_streamer);
+
+	/* Close the file. */
+	if (close(private->archive_fd) != 0)
+		pg_log_error("could not close file \"%s\": %m",
+					 private->archive_name);
+}
+
+/*
+ * Reads a WAL page from the archive and verifies WAL segment size.
+ */
+static void
+verify_tar_archive(XLogDumpPrivate *private, const char *waldir,
+				   pg_compress_algorithm compression)
+{
+	PGAlignedXLogBlock buf;
+	int			r;
+
+	setup_astreamer(private, compression, InvalidXLogRecPtr, InvalidXLogRecPtr);
+
+	/* Now, the tar archive and store its file descriptor */
+	private->archive_fd = open_file_in_directory(waldir, private->archive_name);
+
+	if (private->archive_fd < 0)
+		pg_fatal("could not open file \"%s\"", private->archive_name);
+
+	/* Read a wal page */
+	r = astreamer_wal_read(buf.data, InvalidXLogRecPtr, XLOG_BLCKSZ, private);
+
+	/* Set WalSegSz if WAL data is successfully read */
+	if (r == XLOG_BLCKSZ)
+	{
+		XLogLongPageHeader longhdr = (XLogLongPageHeader) buf.data;
+
+		WalSegSz = longhdr->xlp_seg_size;
+
+		if (!IsValidWalSegSize(WalSegSz))
+		{
+			pg_log_error(ngettext("invalid WAL segment size in WAL file \"%s\" (%d byte)",
+								  "invalid WAL segment size in WAL file \"%s\" (%d bytes)",
+								  WalSegSz),
+						 private->archive_name, WalSegSz);
+			pg_log_error_detail("The WAL segment size must be a power of two between 1 MB and 1 GB.");
+			exit(1);
+		}
+	}
+	else
+		pg_fatal("could not read WAL data from \"%s\" archive: read %d of %d",
+				 private->archive_name, r, XLOG_BLCKSZ);
+
+	free_tar_archive_reader(private);
+}
+
 /* Returns the size in bytes of the data to be read. */
 static inline int
 required_read_len(XLogDumpPrivate *private, XLogRecPtr targetPagePtr,
@@ -406,7 +559,7 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
 				XLogRecPtr targetPtr, char *readBuff)
 {
 	XLogDumpPrivate *private = state->private_data;
-	int			count = required_read_len(private, targetPagePtr, reqLen);
+	int			count = required_read_len(private, targetPtr, reqLen);
 	WALReadError errinfo;
 
 	if (private->endptr_reached)
@@ -436,6 +589,44 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
 	return count;
 }
 
+/*
+ * pg_waldump's XLogReaderRoutine->segment_open callback to support dumping WAL
+ * files from tar archives.
+ */
+static void
+TarWALDumpOpenSegment(XLogReaderState *state, XLogSegNo nextSegNo,
+					  TimeLineID *tli_p)
+{
+	/* No action needed */
+}
+
+/*
+ * pg_waldump's XLogReaderRoutine->segment_close callback.
+ */
+static void
+TarWALDumpCloseSegment(XLogReaderState *state)
+{
+	/* No action needed */
+}
+
+/*
+ * pg_waldump's XLogReaderRoutine->page_read callback to support dumping WAL
+ * files from tar archives.
+ */
+static int
+TarWALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
+				   XLogRecPtr targetPtr, char *readBuff)
+{
+	XLogDumpPrivate *private = state->private_data;
+	int			count = required_read_len(private, targetPtr, reqLen);
+
+	if (private->endptr_reached)
+		return -1;
+
+	/* Read the WAL page from the archive streamer */
+	return astreamer_wal_read(readBuff, targetPagePtr, count, private);
+}
+
 /*
  * Boolean to return whether the given WAL record matches a specific relation
  * and optionally block.
@@ -773,8 +964,8 @@ usage(void)
 	printf(_("  -F, --fork=FORK        only show records that modify blocks in fork FORK;\n"
 			 "                         valid names are main, fsm, vm, init\n"));
 	printf(_("  -n, --limit=N          number of records to display\n"));
-	printf(_("  -p, --path=PATH        directory in which to find WAL segment files or a\n"
-			 "                         directory with a ./pg_wal that contains such files\n"
+	printf(_("  -p, --path=PATH        tar archive or a directory in which to find WAL segment files or\n"
+			 "                         a directory with a ./pg_wal that contains such files\n"
 			 "                         (default: current directory, ./pg_wal, $PGDATA/pg_wal)\n"));
 	printf(_("  -q, --quiet            do not print any output, except for errors\n"));
 	printf(_("  -r, --rmgr=RMGR        only show records generated by resource manager RMGR;\n"
@@ -806,7 +997,11 @@ main(int argc, char **argv)
 	XLogRecord *record;
 	XLogRecPtr	first_record;
 	char	   *waldir = NULL;
+	char	   *walpath = NULL;
 	char	   *errormsg;
+	bool		is_tar = false;
+	XLogReaderRoutine *routine = NULL;
+	pg_compress_algorithm compression;
 
 	static struct option long_options[] = {
 		{"bkp-details", no_argument, NULL, 'b'},
@@ -938,7 +1133,7 @@ main(int argc, char **argv)
 				}
 				break;
 			case 'p':
-				waldir = pg_strdup(optarg);
+				walpath = pg_strdup(optarg);
 				break;
 			case 'q':
 				config.quiet = true;
@@ -1102,10 +1297,20 @@ main(int argc, char **argv)
 		goto bad_argument;
 	}
 
-	if (waldir != NULL)
+	if (walpath != NULL)
 	{
+		/* validate path points to tar archive */
+		if (is_tar_file(walpath, &compression))
+		{
+			char	   *fname = NULL;
+
+			split_path(walpath, &waldir, &fname);
+
+			private.archive_name = fname;
+			is_tar = true;
+		}
 		/* validate path points to directory */
-		if (!verify_directory(waldir))
+		else if (!verify_directory(walpath))
 		{
 			pg_log_error("could not open directory \"%s\": %m", waldir);
 			goto bad_argument;
@@ -1129,44 +1334,23 @@ main(int argc, char **argv)
 
 		split_path(argv[optind], &directory, &fname);
 
-		if (waldir == NULL && directory != NULL)
+		if (walpath == NULL && directory != NULL)
 		{
-			waldir = directory;
+			walpath = directory;
 
-			if (!verify_directory(waldir))
+			if (!verify_directory(walpath))
 				pg_fatal("could not open directory \"%s\": %m", waldir);
 		}
 
-		waldir = identify_target_directory(waldir, fname);
-		fd = open_file_in_directory(waldir, fname);
-		if (fd < 0)
-			pg_fatal("could not open file \"%s\"", fname);
-		close(fd);
-
-		/* parse position from file */
-		XLogFromFileName(fname, &private.timeline, &segno, WalSegSz);
-
-		if (XLogRecPtrIsInvalid(private.startptr))
-			XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr);
-		else if (!XLByteInSeg(private.startptr, segno, WalSegSz))
+		if (fname != NULL && is_tar_file(fname, &compression))
 		{
-			pg_log_error("start WAL location %X/%08X is not inside file \"%s\"",
-						 LSN_FORMAT_ARGS(private.startptr),
-						 fname);
-			goto bad_argument;
+			private.archive_name = fname;
+			waldir = walpath;
+			is_tar = true;
 		}
-
-		/* no second file specified, set end position */
-		if (!(optind + 1 < argc) && XLogRecPtrIsInvalid(private.endptr))
-			XLogSegNoOffsetToRecPtr(segno + 1, 0, WalSegSz, private.endptr);
-
-		/* parse ENDSEG if passed */
-		if (optind + 1 < argc)
+		else
 		{
-			XLogSegNo	endsegno;
-
-			/* ignore directory, already have that */
-			split_path(argv[optind + 1], &directory, &fname);
+			waldir = identify_target_directory(walpath, fname);
 
 			fd = open_file_in_directory(waldir, fname);
 			if (fd < 0)
@@ -1174,32 +1358,67 @@ main(int argc, char **argv)
 			close(fd);
 
 			/* parse position from file */
-			XLogFromFileName(fname, &private.timeline, &endsegno, WalSegSz);
+			XLogFromFileName(fname, &private.timeline, &segno, WalSegSz);
 
-			if (endsegno < segno)
-				pg_fatal("ENDSEG %s is before STARTSEG %s",
-						 argv[optind + 1], argv[optind]);
+			if (XLogRecPtrIsInvalid(private.startptr))
+				XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr);
+			else if (!XLByteInSeg(private.startptr, segno, WalSegSz))
+			{
+				pg_log_error("start WAL location %X/%08X is not inside file \"%s\"",
+							 LSN_FORMAT_ARGS(private.startptr),
+							 fname);
+				goto bad_argument;
+			}
 
-			if (XLogRecPtrIsInvalid(private.endptr))
-				XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz,
-										private.endptr);
+			/* no second file specified, set end position */
+			if (!(optind + 1 < argc) && XLogRecPtrIsInvalid(private.endptr))
+				XLogSegNoOffsetToRecPtr(segno + 1, 0, WalSegSz, private.endptr);
 
-			/* set segno to endsegno for check of --end */
-			segno = endsegno;
-		}
+			/* parse ENDSEG if passed */
+			if (optind + 1 < argc)
+			{
+				XLogSegNo	endsegno;
 
+				/* ignore directory, already have that */
+				split_path(argv[optind + 1], &directory, &fname);
 
-		if (!XLByteInSeg(private.endptr, segno, WalSegSz) &&
-			private.endptr != (segno + 1) * WalSegSz)
-		{
-			pg_log_error("end WAL location %X/%08X is not inside file \"%s\"",
-						 LSN_FORMAT_ARGS(private.endptr),
-						 argv[argc - 1]);
-			goto bad_argument;
+				fd = open_file_in_directory(waldir, fname);
+				if (fd < 0)
+					pg_fatal("could not open file \"%s\"", fname);
+				close(fd);
+
+				/* parse position from file */
+				XLogFromFileName(fname, &private.timeline, &endsegno, WalSegSz);
+
+				if (endsegno < segno)
+					pg_fatal("ENDSEG %s is before STARTSEG %s",
+							 argv[optind + 1], argv[optind]);
+
+				if (XLogRecPtrIsInvalid(private.endptr))
+					XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz,
+											private.endptr);
+
+				/* set segno to endsegno for check of --end */
+				segno = endsegno;
+			}
+
+
+			if (!XLByteInSeg(private.endptr, segno, WalSegSz) &&
+				private.endptr != (segno + 1) * WalSegSz)
+			{
+				pg_log_error("end WAL location %X/%08X is not inside file \"%s\"",
+							 LSN_FORMAT_ARGS(private.endptr),
+							 argv[argc - 1]);
+				goto bad_argument;
+			}
 		}
 	}
-	else
-		waldir = identify_target_directory(waldir, NULL);
+	else if (!is_tar)
+		waldir = identify_target_directory(walpath, NULL);
+
+	/* Verify that the archive contains valid WAL files */
+	if (is_tar)
+		verify_tar_archive(&private, waldir, compression);
 
 	/* we don't know what to print */
 	if (XLogRecPtrIsInvalid(private.startptr))
@@ -1211,11 +1430,26 @@ main(int argc, char **argv)
 	/* done with argument parsing, do the actual work */
 
 	/* we have everything we need, start reading */
+	if (is_tar)
+	{
+		/* Set up for reading tar file */
+		init_tar_archive_reader(&private, waldir, compression);
+
+		/* Routine to decode WAL files in tar archive */
+		routine = XL_ROUTINE(.page_read = TarWALDumpReadPage,
+							 .segment_open = TarWALDumpOpenSegment,
+							 .segment_close = TarWALDumpCloseSegment);
+	}
+	else
+	{
+		/* Routine to decode WAL files */
+		routine = XL_ROUTINE(.page_read = WALDumpReadPage,
+							 .segment_open = WALDumpOpenSegment,
+							 .segment_close = WALDumpCloseSegment);
+	}
+
 	xlogreader_state =
-		XLogReaderAllocate(WalSegSz, waldir,
-						   XL_ROUTINE(.page_read = WALDumpReadPage,
-									  .segment_open = WALDumpOpenSegment,
-									  .segment_close = WALDumpCloseSegment),
+		XLogReaderAllocate(WalSegSz, waldir, routine,
 						   &private);
 	if (!xlogreader_state)
 		pg_fatal("out of memory while allocating a WAL reading processor");
@@ -1325,6 +1559,9 @@ main(int argc, char **argv)
 
 	XLogReaderFree(xlogreader_state);
 
+	if (is_tar)
+		free_tar_archive_reader(&private);
+
 	return EXIT_SUCCESS;
 
 bad_argument:
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
index cd9a36d7447..d2c2307d6c2 100644
--- a/src/bin/pg_waldump/pg_waldump.h
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -12,6 +12,8 @@
 #define PG_WALDUMP_H
 
 #include "access/xlogdefs.h"
+#include "fe_utils/astreamer.h"
+#include "lib/stringinfo.h"
 
 extern int WalSegSz;
 
@@ -22,6 +24,23 @@ typedef struct XLogDumpPrivate
 	XLogRecPtr	startptr;
 	XLogRecPtr	endptr;
 	bool		endptr_reached;
+
+	/* Fields required to read WAL from archive */
+	char	   *archive_name;	/* Tar archive name */
+	int			archive_fd;		/* File descriptor for the open tar file */
+
+	astreamer  *archive_streamer;
+	StringInfo	archive_streamer_buf;	/* Buffer for receiving WAL data */
+	XLogRecPtr	archive_streamer_read_ptr; /* Populate the buffer with records
+											  until this record pointer */
 } XLogDumpPrivate;
 
-#endif		/* end of PG_WALDUMP_H */
+
+extern astreamer *astreamer_waldump_content_new(astreamer *next,
+												XLogRecPtr startptr,
+												XLogRecPtr endptr,
+												XLogDumpPrivate *privateInfo);
+extern int	astreamer_wal_read(char *readBuff, XLogRecPtr startptr, Size count,
+							   XLogDumpPrivate *privateInfo);
+
+#endif							/* end of PG_WALDUMP_H */
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 1b712e8d74d..80298d2a51d 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -3,10 +3,13 @@
 
 use strict;
 use warnings FATAL => 'all';
+use Cwd;
 use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
 
+my $tar = $ENV{TAR};
+
 program_help_ok('pg_waldump');
 program_version_ok('pg_waldump');
 program_options_handling_ok('pg_waldump');
@@ -235,7 +238,7 @@ command_like(
 sub test_pg_waldump
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my @opts = @_;
+	my ($path, @opts) = @_;
 
 	my ($stdout, $stderr);
 
@@ -243,6 +246,7 @@ sub test_pg_waldump
 		'pg_waldump',
 		'--start' => $start_lsn,
 		'--end' => $end_lsn,
+		'--path' => $path,
 		@opts
 	  ],
 	  '>' => \$stdout,
@@ -254,11 +258,27 @@ sub test_pg_waldump
 	return @lines;
 }
 
-my @lines;
+my $tmp_dir = PostgreSQL::Test::Utils::tempdir_short();
 
 my @scenario = (
 	{
-		'path' => $node->data_dir
+		'path' => $node->data_dir,
+		'is_archive' => 0,
+		'enabled' => 1
+	},
+	{
+		'path' => "$tmp_dir/pg_wal.tar",
+		'compression_method' => 'none',
+		'compression_flags' => '-cf',
+		'is_archive' => 1,
+		'enabled' => 1
+	},
+	{
+		'path' => "$tmp_dir/pg_wal.tar.gz",
+		'compression_method' => 'gzip',
+		'compression_flags' => '-czf',
+		'is_archive' => 1,
+		'enabled' => check_pg_config("#define HAVE_LIBZ 1")
 	});
 
 for my $scenario (@scenario)
@@ -267,6 +287,22 @@ for my $scenario (@scenario)
 
 	SKIP:
 	{
+		skip "tar command is not available", 3
+		  if !defined $tar;
+		skip "$scenario->{'compression_method'} compression not supported by this build", 3
+		  if !$scenario->{'enabled'} && $scenario->{'is_archive'};
+
+		  # create pg_wal archive
+		  if ($scenario->{'is_archive'})
+		  {
+			  # move into the WAL directory before archiving files
+			  my $cwd = getcwd;
+			  chdir($node->data_dir . '/pg_wal/') || die "chdir: $!";
+			  command_ok(
+				  [ $tar, $scenario->{'compression_flags'}, $path , '.' ]);
+			  chdir($cwd) || die "chdir: $!";
+		  }
+
 		command_fails_like(
 			[ 'pg_waldump', '--path' => $path ],
 			qr/error: no start WAL location given/,
@@ -298,38 +334,42 @@ for my $scenario (@scenario)
 			qr/error: error in WAL record at/,
 			'errors are shown with --quiet');
 
-		@lines = test_pg_waldump('--path' => $path);
+		my @lines;
+		@lines = test_pg_waldump($path);
 		is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
 
-		@lines = test_pg_waldump('--path' => $path, '--limit' => 6);
+		@lines = test_pg_waldump($path, '--limit' => 6);
 		is(@lines, 6, 'limit option observed');
 
-		@lines = test_pg_waldump('--path' => $path, '--fullpage');
+		@lines = test_pg_waldump($path, '--fullpage');
 		is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
 
-		@lines = test_pg_waldump('--path' => $path, '--stats');
+		@lines = test_pg_waldump($path, '--stats');
 		like($lines[0], qr/WAL statistics/, "statistics on stdout");
 		is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
 
-		@lines = test_pg_waldump('--path' => $path, '--stats=record');
+		@lines = test_pg_waldump($path, '--stats=record');
 		like($lines[0], qr/WAL statistics/, "statistics on stdout");
 		is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
 
-		@lines = test_pg_waldump('--path' => $path, '--rmgr' => 'Btree');
+		@lines = test_pg_waldump($path, '--rmgr' => 'Btree');
 		is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
 
-		@lines = test_pg_waldump('--path' => $path, '--fork' => 'init');
+		@lines = test_pg_waldump($path, '--fork' => 'init');
 		is(grep(!/fork init/, @lines), 0, 'only init fork lines');
 
-		@lines = test_pg_waldump('--path' => $path,
+		@lines = test_pg_waldump($path,
 			'--relation' => "$default_ts_oid/$postgres_db_oid/$rel_t1_oid");
 		is(grep(!/rel $default_ts_oid\/$postgres_db_oid\/$rel_t1_oid/, @lines),
 			0, 'only lines for selected relation');
 
-		@lines = test_pg_waldump('--path' => $path,
+		@lines = test_pg_waldump($path,
 			'--relation' => "$default_ts_oid/$postgres_db_oid/$rel_i1a_oid",
 			'--block' => 1);
 		is(grep(!/\bblk 1\b/, @lines), 0, 'only lines for selected block');
+
+		# Cleanup.
+		unlink $path if $scenario->{'is_archive'};
 	}
 }
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e6f2e93b2d6..d8428ce2352 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3445,6 +3445,7 @@ astreamer_recovery_injector
 astreamer_tar_archiver
 astreamer_tar_parser
 astreamer_verify
+astreamer_waldump
 astreamer_zstd_frame
 auth_password_hook_typ
 autovac_table
-- 
2.47.1