v17-0001-pg_waldump-Preparatory-refactoring-for-tar-archi.patch

text/x-patch

Filename: v17-0001-pg_waldump-Preparatory-refactoring-for-tar-archi.patch
Type: text/x-patch
Part: 0
Message: Re: 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 v17-0001
Subject: pg_waldump: Preparatory refactoring for tar archive WAL decoding
File+
src/bin/pg_basebackup/pg_basebackup.c 11 25
src/bin/pg_verifybackup/pg_verifybackup.c 1 11
src/bin/pg_waldump/pg_waldump.c 44 34
src/bin/pg_waldump/pg_waldump.h 26 0
src/bin/pg_waldump/t/001_basic.pl 79 61
src/common/compression.c 30 0
src/include/common/compression.h 2 0
From d439059e8fb8b697b86f4b5093fbdb94c245d3d0 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Wed, 11 Mar 2026 11:24:34 -0400
Subject: [PATCH v17 1/3] pg_waldump: Preparatory refactoring for tar archive
 WAL decoding

Several refactoring steps in preparation for adding tar archive WAL
decoding support to pg_waldump:

- Move tar archive detection and compression-type logic
  (parse_tar_compress_algorithm) into src/common/compression.c so it can
  be shared by pg_basebackup, pg_verifybackup, and pg_waldump.

- Move XLogDumpPrivate and related declarations into a new pg_waldump.h
  header, allowing a second source file to share them.

- Factor out required_read_len() so the read-size calculation can be
  reused for both regular WAL files and tar-archived WAL.

- Move the WAL segment size variable into XLogDumpPrivate and rename it
  to segsize, making it accessible to the archive streamer code.

- Restructure the TAP tests so the core test cases run inside a loop,
  allowing them to be re-executed against tar archives.

Author: Amul Sul <sulamul@gmail.com>
Reviewed-by: Robert Haas <robertmhaas@gmail.com>
Reviewed-by: Jakub Wartak <jakub.wartak@enterprisedb.com>
Reviewed-by: Andrew Dunstan <andrew@dunslane.net>
discussion: https://postgr.es/m/CAAJ_b94bqdWN3h2J-PzzzQ2Npbwct5ZQHggn_QoYGhC2rn-=WQ@mail.gmail.com
---
 src/bin/pg_basebackup/pg_basebackup.c     |  36 ++----
 src/bin/pg_verifybackup/pg_verifybackup.c |  12 +-
 src/bin/pg_waldump/pg_waldump.c           |  78 ++++++------
 src/bin/pg_waldump/pg_waldump.h           |  26 ++++
 src/bin/pg_waldump/t/001_basic.pl         | 140 ++++++++++++----------
 src/common/compression.c                  |  30 +++++
 src/include/common/compression.h          |   2 +
 7 files changed, 193 insertions(+), 131 deletions(-)
 create mode 100644 src/bin/pg_waldump/pg_waldump.h

diff --git a/src/bin/pg_basebackup/pg_basebackup.c b/src/bin/pg_basebackup/pg_basebackup.c
index fa169a8d642..c1a4672aa6f 100644
--- a/src/bin/pg_basebackup/pg_basebackup.c
+++ b/src/bin/pg_basebackup/pg_basebackup.c
@@ -1070,12 +1070,9 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 	astreamer  *manifest_inject_streamer = NULL;
 	bool		inject_manifest;
 	bool		is_tar,
-				is_tar_gz,
-				is_tar_lz4,
-				is_tar_zstd,
 				is_compressed_tar;
+	pg_compress_algorithm compressed_tar_algorithm;
 	bool		must_parse_archive;
-	int			archive_name_len = strlen(archive_name);
 
 	/*
 	 * Normally, we emit the backup manifest as a separate file, but when
@@ -1084,24 +1081,13 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 	 */
 	inject_manifest = (format == 't' && strcmp(basedir, "-") == 0 && manifest);
 
-	/* Is this a tar archive? */
-	is_tar = (archive_name_len > 4 &&
-			  strcmp(archive_name + archive_name_len - 4, ".tar") == 0);
-
-	/* Is this a .tar.gz archive? */
-	is_tar_gz = (archive_name_len > 7 &&
-				 strcmp(archive_name + archive_name_len - 7, ".tar.gz") == 0);
-
-	/* Is this a .tar.lz4 archive? */
-	is_tar_lz4 = (archive_name_len > 8 &&
-				  strcmp(archive_name + archive_name_len - 8, ".tar.lz4") == 0);
-
-	/* Is this a .tar.zst archive? */
-	is_tar_zstd = (archive_name_len > 8 &&
-				   strcmp(archive_name + archive_name_len - 8, ".tar.zst") == 0);
+	/* Check whether it is a tar archive and its compression type */
+	is_tar = parse_tar_compress_algorithm(archive_name,
+										  &compressed_tar_algorithm);
 
 	/* Is this any kind of compressed tar? */
-	is_compressed_tar = is_tar_gz || is_tar_lz4 || is_tar_zstd;
+	is_compressed_tar = (is_tar &&
+						 compressed_tar_algorithm != PG_COMPRESSION_NONE);
 
 	/*
 	 * Injecting the manifest into a compressed tar file would be possible if
@@ -1128,7 +1114,7 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 						  (spclocation == NULL && writerecoveryconf));
 
 	/* At present, we only know how to parse tar archives. */
-	if (must_parse_archive && !is_tar && !is_compressed_tar)
+	if (must_parse_archive && !is_tar)
 	{
 		pg_log_error("cannot parse archive \"%s\"", archive_name);
 		pg_log_error_detail("Only tar archives can be parsed.");
@@ -1263,13 +1249,13 @@ CreateBackupStreamer(char *archive_name, char *spclocation,
 	 * If the user has requested a server compressed archive along with
 	 * archive extraction at client then we need to decompress it.
 	 */
-	if (format == 'p')
+	if (format == 'p' && is_compressed_tar)
 	{
-		if (is_tar_gz)
+		if (compressed_tar_algorithm == PG_COMPRESSION_GZIP)
 			streamer = astreamer_gzip_decompressor_new(streamer);
-		else if (is_tar_lz4)
+		else if (compressed_tar_algorithm == PG_COMPRESSION_LZ4)
 			streamer = astreamer_lz4_decompressor_new(streamer);
-		else if (is_tar_zstd)
+		else if (compressed_tar_algorithm == PG_COMPRESSION_ZSTD)
 			streamer = astreamer_zstd_decompressor_new(streamer);
 	}
 
diff --git a/src/bin/pg_verifybackup/pg_verifybackup.c b/src/bin/pg_verifybackup/pg_verifybackup.c
index cbc9447384f..31f606c45b1 100644
--- a/src/bin/pg_verifybackup/pg_verifybackup.c
+++ b/src/bin/pg_verifybackup/pg_verifybackup.c
@@ -941,17 +941,7 @@ precheck_tar_backup_file(verifier_context *context, char *relpath,
 	}
 
 	/* Now, check the compression type of the tar */
-	if (strcmp(suffix, ".tar") == 0)
-		compress_algorithm = PG_COMPRESSION_NONE;
-	else if (strcmp(suffix, ".tgz") == 0)
-		compress_algorithm = PG_COMPRESSION_GZIP;
-	else if (strcmp(suffix, ".tar.gz") == 0)
-		compress_algorithm = PG_COMPRESSION_GZIP;
-	else if (strcmp(suffix, ".tar.lz4") == 0)
-		compress_algorithm = PG_COMPRESSION_LZ4;
-	else if (strcmp(suffix, ".tar.zst") == 0)
-		compress_algorithm = PG_COMPRESSION_ZSTD;
-	else
+	if (!parse_tar_compress_algorithm(suffix, &compress_algorithm))
 	{
 		report_backup_error(context,
 							"file \"%s\" is not expected in a tar format backup",
diff --git a/src/bin/pg_waldump/pg_waldump.c b/src/bin/pg_waldump/pg_waldump.c
index f3446385d6a..5d31b15dbd8 100644
--- a/src/bin/pg_waldump/pg_waldump.c
+++ b/src/bin/pg_waldump/pg_waldump.c
@@ -29,6 +29,7 @@
 #include "common/logging.h"
 #include "common/relpath.h"
 #include "getopt_long.h"
+#include "pg_waldump.h"
 #include "rmgrdesc.h"
 #include "storage/bufpage.h"
 
@@ -43,14 +44,6 @@ static volatile sig_atomic_t time_to_stop = false;
 
 static const RelFileLocator emptyRelFileLocator = {0, 0, 0};
 
-typedef struct XLogDumpPrivate
-{
-	TimeLineID	timeline;
-	XLogRecPtr	startptr;
-	XLogRecPtr	endptr;
-	bool		endptr_reached;
-} XLogDumpPrivate;
-
 typedef struct XLogDumpConfig
 {
 	/* display options */
@@ -333,6 +326,32 @@ identify_target_directory(char *directory, char *fname, int *WalSegSz)
 	return NULL;				/* not reached */
 }
 
+/*
+ * Returns the size in bytes of the data to be read. Returns -1 if the end
+ * point has already been reached.
+ */
+static inline int
+required_read_len(XLogDumpPrivate *private, XLogRecPtr targetPagePtr,
+				  int reqLen)
+{
+	int			count = XLOG_BLCKSZ;
+
+	if (XLogRecPtrIsValid(private->endptr))
+	{
+		if (targetPagePtr + XLOG_BLCKSZ <= private->endptr)
+			count = XLOG_BLCKSZ;
+		else if (targetPagePtr + reqLen <= private->endptr)
+			count = private->endptr - targetPagePtr;
+		else
+		{
+			private->endptr_reached = true;
+			return -1;
+		}
+	}
+
+	return count;
+}
+
 /* pg_waldump's XLogReaderRoutine->segment_open callback */
 static void
 WALDumpOpenSegment(XLogReaderState *state, XLogSegNo nextSegNo,
@@ -390,21 +409,12 @@ WALDumpReadPage(XLogReaderState *state, XLogRecPtr targetPagePtr, int reqLen,
 				XLogRecPtr targetPtr, char *readBuff)
 {
 	XLogDumpPrivate *private = state->private_data;
-	int			count = XLOG_BLCKSZ;
+	int			count = required_read_len(private, targetPagePtr, reqLen);
 	WALReadError errinfo;
 
-	if (XLogRecPtrIsValid(private->endptr))
-	{
-		if (targetPagePtr + XLOG_BLCKSZ <= private->endptr)
-			count = XLOG_BLCKSZ;
-		else if (targetPagePtr + reqLen <= private->endptr)
-			count = private->endptr - targetPagePtr;
-		else
-		{
-			private->endptr_reached = true;
-			return -1;
-		}
-	}
+	/* Bail out if the count to be read is not valid */
+	if (count < 0)
+		return -1;
 
 	if (!WALRead(state, readBuff, targetPagePtr, count, private->timeline,
 				 &errinfo))
@@ -801,7 +811,6 @@ main(int argc, char **argv)
 	XLogRecPtr	first_record;
 	char	   *waldir = NULL;
 	char	   *errormsg;
-	int			WalSegSz;
 
 	static struct option long_options[] = {
 		{"bkp-details", no_argument, NULL, 'b'},
@@ -855,6 +864,7 @@ main(int argc, char **argv)
 	memset(&stats, 0, sizeof(XLogStats));
 
 	private.timeline = 1;
+	private.segsize = 0;
 	private.startptr = InvalidXLogRecPtr;
 	private.endptr = InvalidXLogRecPtr;
 	private.endptr_reached = false;
@@ -1128,18 +1138,18 @@ main(int argc, char **argv)
 				pg_fatal("could not open directory \"%s\": %m", waldir);
 		}
 
-		waldir = identify_target_directory(waldir, fname, &WalSegSz);
+		waldir = identify_target_directory(waldir, fname, &private.segsize);
 		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);
+		XLogFromFileName(fname, &private.timeline, &segno, private.segsize);
 
 		if (!XLogRecPtrIsValid(private.startptr))
-			XLogSegNoOffsetToRecPtr(segno, 0, WalSegSz, private.startptr);
-		else if (!XLByteInSeg(private.startptr, segno, WalSegSz))
+			XLogSegNoOffsetToRecPtr(segno, 0, private.segsize, private.startptr);
+		else if (!XLByteInSeg(private.startptr, segno, private.segsize))
 		{
 			pg_log_error("start WAL location %X/%08X is not inside file \"%s\"",
 						 LSN_FORMAT_ARGS(private.startptr),
@@ -1149,7 +1159,7 @@ main(int argc, char **argv)
 
 		/* no second file specified, set end position */
 		if (!(optind + 1 < argc) && !XLogRecPtrIsValid(private.endptr))
-			XLogSegNoOffsetToRecPtr(segno + 1, 0, WalSegSz, private.endptr);
+			XLogSegNoOffsetToRecPtr(segno + 1, 0, private.segsize, private.endptr);
 
 		/* parse ENDSEG if passed */
 		if (optind + 1 < argc)
@@ -1165,14 +1175,14 @@ main(int argc, char **argv)
 			close(fd);
 
 			/* parse position from file */
-			XLogFromFileName(fname, &private.timeline, &endsegno, WalSegSz);
+			XLogFromFileName(fname, &private.timeline, &endsegno, private.segsize);
 
 			if (endsegno < segno)
 				pg_fatal("ENDSEG %s is before STARTSEG %s",
 						 argv[optind + 1], argv[optind]);
 
 			if (!XLogRecPtrIsValid(private.endptr))
-				XLogSegNoOffsetToRecPtr(endsegno + 1, 0, WalSegSz,
+				XLogSegNoOffsetToRecPtr(endsegno + 1, 0, private.segsize,
 										private.endptr);
 
 			/* set segno to endsegno for check of --end */
@@ -1180,8 +1190,8 @@ main(int argc, char **argv)
 		}
 
 
-		if (!XLByteInSeg(private.endptr, segno, WalSegSz) &&
-			private.endptr != (segno + 1) * WalSegSz)
+		if (!XLByteInSeg(private.endptr, segno, private.segsize) &&
+			private.endptr != (segno + 1) * private.segsize)
 		{
 			pg_log_error("end WAL location %X/%08X is not inside file \"%s\"",
 						 LSN_FORMAT_ARGS(private.endptr),
@@ -1190,7 +1200,7 @@ main(int argc, char **argv)
 		}
 	}
 	else
-		waldir = identify_target_directory(waldir, NULL, &WalSegSz);
+		waldir = identify_target_directory(waldir, NULL, &private.segsize);
 
 	/* we don't know what to print */
 	if (!XLogRecPtrIsValid(private.startptr))
@@ -1203,7 +1213,7 @@ main(int argc, char **argv)
 
 	/* we have everything we need, start reading */
 	xlogreader_state =
-		XLogReaderAllocate(WalSegSz, waldir,
+		XLogReaderAllocate(private.segsize, waldir,
 						   XL_ROUTINE(.page_read = WALDumpReadPage,
 									  .segment_open = WALDumpOpenSegment,
 									  .segment_close = WALDumpCloseSegment),
@@ -1224,7 +1234,7 @@ main(int argc, char **argv)
 	 * a segment (e.g. we were used in file mode).
 	 */
 	if (first_record != private.startptr &&
-		XLogSegmentOffset(private.startptr, WalSegSz) != 0)
+		XLogSegmentOffset(private.startptr, private.segsize) != 0)
 		pg_log_info(ngettext("first record is after %X/%08X, at %X/%08X, skipping over %u byte",
 							 "first record is after %X/%08X, at %X/%08X, skipping over %u bytes",
 							 (first_record - private.startptr)),
diff --git a/src/bin/pg_waldump/pg_waldump.h b/src/bin/pg_waldump/pg_waldump.h
new file mode 100644
index 00000000000..013b051506f
--- /dev/null
+++ b/src/bin/pg_waldump/pg_waldump.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_waldump.h - decode and display WAL
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  src/bin/pg_waldump/pg_waldump.h
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_WALDUMP_H
+#define PG_WALDUMP_H
+
+#include "access/xlogdefs.h"
+
+/* Contains the necessary information to drive WAL decoding */
+typedef struct XLogDumpPrivate
+{
+	TimeLineID	timeline;
+	int			segsize;
+	XLogRecPtr	startptr;
+	XLogRecPtr	endptr;
+	bool		endptr_reached;
+} XLogDumpPrivate;
+
+#endif							/* PG_WALDUMP_H */
diff --git a/src/bin/pg_waldump/t/001_basic.pl b/src/bin/pg_waldump/t/001_basic.pl
index 5db5d20136f..f12ba52cbfc 100644
--- a/src/bin/pg_waldump/t/001_basic.pl
+++ b/src/bin/pg_waldump/t/001_basic.pl
@@ -198,28 +198,6 @@ command_like(
 	],
 	qr/./,
 	'runs with start and end segment specified');
-command_fails_like(
-	[ 'pg_waldump', '--path' => $node->data_dir ],
-	qr/error: no start WAL location given/,
-	'path option requires start location');
-command_like(
-	[
-		'pg_waldump',
-		'--path' => $node->data_dir,
-		'--start' => $start_lsn,
-		'--end' => $end_lsn,
-	],
-	qr/./,
-	'runs with path option and start and end locations');
-command_fails_like(
-	[
-		'pg_waldump',
-		'--path' => $node->data_dir,
-		'--start' => $start_lsn,
-	],
-	qr/error: error in WAL record at/,
-	'falling off the end of the WAL results in an error');
-
 command_like(
 	[
 		'pg_waldump', '--quiet',
@@ -227,22 +205,16 @@ command_like(
 	],
 	qr/^$/,
 	'no output with --quiet option');
-command_fails_like(
-	[
-		'pg_waldump', '--quiet',
-		'--path' => $node->data_dir,
-		'--start' => $start_lsn
-	],
-	qr/error: error in WAL record at/,
-	'errors are shown with --quiet');
-
 
 # Test for: Display a message that we're skipping data if `from`
 # wasn't a pointer to the start of a record.
+sub test_pg_waldump_skip_bytes
 {
+	my ($path, $startlsn, $endlsn) = @_;
+
 	# Construct a new LSN that is one byte past the original
 	# start_lsn.
-	my ($part1, $part2) = split qr{/}, $start_lsn;
+	my ($part1, $part2) = split qr{/}, $startlsn;
 	my $lsn2 = hex $part2;
 	$lsn2++;
 	my $new_start = sprintf("%s/%X", $part1, $lsn2);
@@ -252,7 +224,8 @@ command_fails_like(
 	my $result = IPC::Run::run [
 		'pg_waldump',
 		'--start' => $new_start,
-		$node->data_dir . '/pg_wal/' . $start_walfile
+		'--end' => $endlsn,
+		'--path' => $path,
 	  ],
 	  '>' => \$stdout,
 	  '2>' => \$stderr;
@@ -266,15 +239,15 @@ command_fails_like(
 sub test_pg_waldump
 {
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
-	my @opts = @_;
+	my ($path, $startlsn, $endlsn, @opts) = @_;
 
 	my ($stdout, $stderr);
 
 	my $result = IPC::Run::run [
 		'pg_waldump',
-		'--path' => $node->data_dir,
-		'--start' => $start_lsn,
-		'--end' => $end_lsn,
+		'--start' => $startlsn,
+		'--end' => $endlsn,
+		'--path' => $path,
 		@opts
 	  ],
 	  '>' => \$stdout,
@@ -288,38 +261,83 @@ sub test_pg_waldump
 
 my @lines;
 
-@lines = test_pg_waldump;
-is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
+my @scenarios = (
+	{
+		'path' => $node->data_dir
+	});
 
-@lines = test_pg_waldump('--limit' => 6);
-is(@lines, 6, 'limit option observed');
+for my $scenario (@scenarios)
+{
+	my $path = $scenario->{'path'};
 
-@lines = test_pg_waldump('--fullpage');
-is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
+	SKIP:
+	{
+		command_fails_like(
+			[ 'pg_waldump', '--path' => $path ],
+			qr/error: no start WAL location given/,
+			'path option requires start location');
+		command_like(
+			[
+				'pg_waldump',
+				'--path' => $path,
+				'--start' => $start_lsn,
+				'--end' => $end_lsn,
+			],
+			qr/./,
+			'runs with path option and start and end locations');
+		command_fails_like(
+			[
+				'pg_waldump',
+				'--path' => $path,
+				'--start' => $start_lsn,
+			],
+			qr/error: error in WAL record at/,
+			'falling off the end of the WAL results in an error');
 
-@lines = test_pg_waldump('--stats');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+		command_fails_like(
+			[
+				'pg_waldump', '--quiet',
+				'--path' => $path,
+				'--start' => $start_lsn
+			],
+			qr/error: error in WAL record at/,
+			'errors are shown with --quiet');
 
-@lines = test_pg_waldump('--stats=record');
-like($lines[0], qr/WAL statistics/, "statistics on stdout");
-is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
+		test_pg_waldump_skip_bytes($path, $start_lsn, $end_lsn);
 
-@lines = test_pg_waldump('--rmgr' => 'Btree');
-is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+		@lines = test_pg_waldump($path, $start_lsn, $end_lsn);
+		is(grep(!/^rmgr: \w/, @lines), 0, 'all output lines are rmgr lines');
 
-@lines = test_pg_waldump('--fork' => 'init');
-is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+		@lines = test_pg_waldump($path, $start_lsn, $end_lsn, '--limit' => 6);
+		is(@lines, 6, 'limit option observed');
 
-@lines = test_pg_waldump(
-	'--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, $start_lsn, $end_lsn, '--fullpage');
+		is(grep(!/^rmgr:.*\bFPW\b/, @lines), 0, 'all output lines are FPW');
 
-@lines = test_pg_waldump(
-	'--relation' => "$default_ts_oid/$postgres_db_oid/$rel_i1a_oid",
-	'--block' => 1);
-is(grep(!/\bblk 1\b/, @lines), 0, 'only lines for selected block');
+		@lines = test_pg_waldump($path, $start_lsn, $end_lsn, '--stats');
+		like($lines[0], qr/WAL statistics/, "statistics on stdout");
+		is(grep(/^rmgr:/, @lines), 0, 'no rmgr lines output');
 
+		@lines = test_pg_waldump($path, $start_lsn, $end_lsn, '--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, $start_lsn, $end_lsn, '--rmgr' => 'Btree');
+		is(grep(!/^rmgr: Btree/, @lines), 0, 'only Btree lines');
+
+		@lines = test_pg_waldump($path, $start_lsn, $end_lsn, '--fork' => 'init');
+		is(grep(!/fork init/, @lines), 0, 'only init fork lines');
+
+		@lines = test_pg_waldump($path, $start_lsn, $end_lsn,
+			'--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, $start_lsn, $end_lsn,
+			'--relation' => "$default_ts_oid/$postgres_db_oid/$rel_i1a_oid",
+			'--block' => 1);
+		is(grep(!/\bblk 1\b/, @lines), 0, 'only lines for selected block');
+	}
+}
 
 done_testing();
diff --git a/src/common/compression.c b/src/common/compression.c
index 92cd4ec7a0d..fb27501d297 100644
--- a/src/common/compression.c
+++ b/src/common/compression.c
@@ -41,6 +41,36 @@ static int	expect_integer_value(char *keyword, char *value,
 static bool expect_boolean_value(char *keyword, char *value,
 								 pg_compress_specification *result);
 
+/*
+ * Look up a compression algorithm by archive file extension. Returns true and
+ * sets *algorithm if the name is recognized. Otherwise returns false.
+ */
+bool
+parse_tar_compress_algorithm(const char *fname, pg_compress_algorithm *algorithm)
+{
+	int			fname_len = strlen(fname);
+
+	if (fname_len >= 4 &&
+		strcmp(fname + fname_len - 4, ".tar") == 0)
+		*algorithm = PG_COMPRESSION_NONE;
+	else if (fname_len >= 4 &&
+			 strcmp(fname + fname_len - 4, ".tgz") == 0)
+		*algorithm = PG_COMPRESSION_GZIP;
+	else if (fname_len >= 7 &&
+			 strcmp(fname + fname_len - 7, ".tar.gz") == 0)
+		*algorithm = PG_COMPRESSION_GZIP;
+	else if (fname_len >= 8 &&
+			 strcmp(fname + fname_len - 8, ".tar.lz4") == 0)
+		*algorithm = PG_COMPRESSION_LZ4;
+	else if (fname_len >= 8 &&
+			 strcmp(fname + fname_len - 8, ".tar.zst") == 0)
+		*algorithm = PG_COMPRESSION_ZSTD;
+	else
+		return false;
+
+	return true;
+}
+
 /*
  * Look up a compression algorithm by name. Returns true and sets *algorithm
  * if the name is recognized. Otherwise returns false.
diff --git a/src/include/common/compression.h b/src/include/common/compression.h
index 6c745b90066..f99c747cdd3 100644
--- a/src/include/common/compression.h
+++ b/src/include/common/compression.h
@@ -41,6 +41,8 @@ typedef struct pg_compress_specification
 
 extern void parse_compress_options(const char *option, char **algorithm,
 								   char **detail);
+extern bool parse_tar_compress_algorithm(const char *fname,
+										 pg_compress_algorithm *algorithm);
 extern bool parse_compress_algorithm(char *name, pg_compress_algorithm *algorithm);
 extern const char *get_compress_algorithm_name(pg_compress_algorithm algorithm);
 
-- 
2.43.0