From 029fc34ce9f27aacb471a0f2164643a34bdbb757 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 22 Apr 2024 16:02:34 -0400
Subject: [PATCH v1] Try again to add test coverage for pg_combinebackup
 w/tablespaces.

---
 src/bin/pg_basebackup/t/010_pg_basebackup.pl  | 21 +---
 .../pg_combinebackup/t/002_compare_backups.pl | 37 ++++++-
 src/test/perl/PostgreSQL/Test/Cluster.pm      | 96 +++++++++++++++++--
 3 files changed, 126 insertions(+), 28 deletions(-)

diff --git a/src/bin/pg_basebackup/t/010_pg_basebackup.pl b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
index 63f7bd2735..31c369688e 100644
--- a/src/bin/pg_basebackup/t/010_pg_basebackup.pl
+++ b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
@@ -407,25 +407,12 @@ SKIP:
 
 	my $node2 = PostgreSQL::Test::Cluster->new('replica');
 
-	# Recover main data directory
-	$node2->init_from_backup($node, 'tarbackup2', tar_program => $tar);
-
-	# Recover tablespace into a new directory (not where it was!)
-	my $repTsDir = "$tempdir/tblspc1replica";
-	my $realRepTsDir = "$real_sys_tempdir/tblspc1replica";
-	mkdir $repTsDir;
-	PostgreSQL::Test::Utils::system_or_bail($tar, 'xf', $tblspc_tars[0],
-		'-C', $repTsDir);
-
-	# Update tablespace map to point to new directory.
-	# XXX Ideally pg_basebackup would handle this.
+	# Recover the backup
 	$tblspc_tars[0] =~ m|/([0-9]*)\.tar$|;
 	my $tblspcoid = $1;
-	my $escapedRepTsDir = $realRepTsDir;
-	$escapedRepTsDir =~ s/\\/\\\\/g;
-	open my $mapfile, '>', $node2->data_dir . '/tablespace_map' or die $!;
-	print $mapfile "$tblspcoid $escapedRepTsDir\n";
-	close $mapfile;
+	my $realRepTsDir = "$real_sys_tempdir/tblspc1replica";
+	$node2->init_from_backup($node, 'tarbackup2', tar_program => $tar,
+		'tablespace_map' => { $tblspcoid => $realRepTsDir });
 
 	$node2->start;
 	my $result = $node2->safe_psql('postgres', 'SELECT * FROM test1');
diff --git a/src/bin/pg_combinebackup/t/002_compare_backups.pl b/src/bin/pg_combinebackup/t/002_compare_backups.pl
index 71e9a90d22..31342579cf 100644
--- a/src/bin/pg_combinebackup/t/002_compare_backups.pl
+++ b/src/bin/pg_combinebackup/t/002_compare_backups.pl
@@ -7,11 +7,15 @@ use PostgreSQL::Test::Cluster;
 use PostgreSQL::Test::Utils;
 use Test::More;
 
+my $tempdir = PostgreSQL::Test::Utils::tempdir_short();
+
 # Set up a new database instance.
 my $primary = PostgreSQL::Test::Cluster->new('primary');
 $primary->init(has_archiving => 1, allows_streaming => 1);
 $primary->append_conf('postgresql.conf', 'summarize_wal = on');
 $primary->start;
+my $tsprimary = $tempdir . '/ts';
+mkdir($tsprimary) || die "mkdir $tsprimary: $!";
 
 # Create some test tables, each containing one row of data, plus a whole
 # extra database.
@@ -29,24 +33,42 @@ INSERT INTO will_get_dropped VALUES (1, 'initial test row');
 CREATE TABLE will_get_rewritten (a int, b text);
 INSERT INTO will_get_rewritten VALUES (1, 'initial test row');
 CREATE DATABASE db_will_get_dropped;
+CREATE TABLESPACE ts1 LOCATION '$tsprimary';
+CREATE TABLE will_not_change_in_ts (a int, b text) TABLESPACE ts1;
+INSERT INTO will_not_change_in_ts VALUES (1, 'initial test row');
+CREATE TABLE will_change_in_ts (a int, b text) TABLESPACE ts1;
+INSERT INTO will_change_in_ts VALUES (1, 'initial test row');
+CREATE TABLE will_get_dropped_in_ts (a int, b text);
+INSERT INTO will_get_dropped_in_ts VALUES (1, 'initial test row');
 EOM
 
+# Read list of tablespace OIDs. There should be just one.
+my @tsoids = grep { /^\d+/ } slurp_dir($primary->data_dir . '/pg_tblspc');
+is(0+@tsoids, 1, "exactly one user-defined tablespace");
+my $tsoid = $tsoids[0];
+
 # Take a full backup.
 my $backup1path = $primary->backup_dir . '/backup1';
+my $tsbackup1path = $tempdir . '/ts1backup';
+mkdir($tsbackup1path) || die "mkdir $tsbackup1path: $!";
 $primary->command_ok(
-	[ 'pg_basebackup', '-D', $backup1path, '--no-sync', '-cfast' ],
-	"full backup");
+	[ 'pg_basebackup', '-D', $backup1path, '--no-sync', '-cfast',
+      "-T${tsprimary}=${tsbackup1path}" ], "full backup");
 
 # Now make some database changes.
 $primary->safe_psql('postgres', <<EOM);
 UPDATE will_change SET b = 'modified value' WHERE a = 1;
+UPDATE will_change_in_ts SET b = 'modified value' WHERE a = 1;
 INSERT INTO will_grow
 	SELECT g, 'additional row' FROM generate_series(2, 5000) g;
 TRUNCATE will_shrink;
 VACUUM will_get_vacuumed;
 DROP TABLE will_get_dropped;
+DROP TABLE will_get_dropped_in_ts;
 CREATE TABLE newly_created (a int, b text);
 INSERT INTO newly_created VALUES (1, 'row for new table');
+CREATE TABLE newly_created_in_ts (a int, b text) TABLESPACE ts1;
+INSERT INTO newly_created_in_ts VALUES (1, 'row for new table');
 VACUUM FULL will_get_rewritten;
 DROP DATABASE db_will_get_dropped;
 CREATE DATABASE db_newly_created;
@@ -54,8 +76,11 @@ EOM
 
 # Take an incremental backup.
 my $backup2path = $primary->backup_dir . '/backup2';
+my $tsbackup2path = $tempdir . '/tsbackup2';
+mkdir($tsbackup2path) || die "mkdir $tsbackup2path: $!";
 $primary->command_ok(
 	[ 'pg_basebackup', '-D', $backup2path, '--no-sync', '-cfast',
+      "-T${tsprimary}=${tsbackup2path}",
 	  '--incremental', $backup1path . '/backup_manifest' ],
 	"incremental backup");
 
@@ -78,9 +103,11 @@ $primary->poll_query_until('postgres', $archive_wait_query)
 # Perform PITR from the full backup. Disable archive_mode so that the archive
 # doesn't find out about the new timeline; that way, the later PITR below will
 # choose the same timeline.
+my $tspitr1path = $tempdir . '/tspitr1';
 my $pitr1 = PostgreSQL::Test::Cluster->new('pitr1');
 $pitr1->init_from_backup($primary, 'backup1',
-						 standby => 1, has_restoring => 1);
+						 standby => 1, has_restoring => 1,
+						 tablespace_map => { $tsoid => $tspitr1path });
 $pitr1->append_conf('postgresql.conf', qq{
 recovery_target_lsn = '$lsn'
 recovery_target_action = 'promote'
@@ -90,10 +117,12 @@ $pitr1->start();
 
 # Perform PITR to the same LSN from the incremental backup. Use the same
 # basic configuration as before.
+my $tspitr2path = $tempdir . '/tspitr2';
 my $pitr2 = PostgreSQL::Test::Cluster->new('pitr2');
 $pitr2->init_from_backup($primary, 'backup2',
 						 standby => 1, has_restoring => 1,
-						 combine_with_prior => [ 'backup1' ]);
+						 combine_with_prior => [ 'backup1' ],
+						 tablespace_map => { $tsbackup2path => $tspitr2path });
 $pitr2->append_conf('postgresql.conf', qq{
 recovery_target_lsn = '$lsn'
 recovery_target_action = 'promote'
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 9b2879c145..aa9646329f 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -777,7 +777,7 @@ sub backup_fs_cold
 
 =pod
 
-=item $node->init_from_backup(root_node, backup_name)
+=item $node->init_from_backup(root_node, backup_name, %params)
 
 Initialize a node from a backup, which may come from this node or a different
 node. root_node must be a PostgreSQL::Test::Cluster reference, backup_name the string name
@@ -787,8 +787,13 @@ Does not start the node after initializing it.
 
 By default, the backup is assumed to be plain format.  To restore from
 a tar-format backup, pass the name of the tar program to use in the
-keyword parameter tar_program.  Note that tablespace tar files aren't
-handled here.
+keyword parameter tar_program.
+
+If there are tablespace present in the backup, include tablespace_map as
+a keyword parameter whose values is a hash. When combine_with_prior is used,
+the hash keys are the tablespace pathnames used in the backup; otherwise,
+they are tablespace OIDs.  In either case, the values are the tablespace
+pathnames that should be used for the target cluster.
 
 To restore from an incremental backup, pass the parameter combine_with_prior
 as a reference to an array of prior backup names with which this backup
@@ -843,12 +848,20 @@ sub init_from_backup
 		}
 
 		local %ENV = $self->_get_env();
-		PostgreSQL::Test::Utils::system_or_bail('pg_combinebackup', '-d',
-			@prior_backup_path, $backup_path, '-o', $data_path);
+		my @combineargs = ('pg_combinebackup', '-d');
+		if (exists $params{tablespace_map})
+		{
+			while (my ($olddir, $newdir) = each %{$params{tablespace_map}})
+			{
+				push @combineargs, "-T$olddir=$newdir";
+			}
+		}
+		push @combineargs, @prior_backup_path, $backup_path, '-o', $data_path;
+		PostgreSQL::Test::Utils::system_or_bail(@combineargs);
 	}
 	elsif (defined $params{tar_program})
 	{
-		mkdir($data_path);
+		mkdir($data_path) || die "mkdir $data_path: $!";
 		PostgreSQL::Test::Utils::system_or_bail($params{tar_program}, 'xf',
 			$backup_path . '/base.tar',
 			'-C', $data_path);
@@ -856,11 +869,80 @@ sub init_from_backup
 			$params{tar_program}, 'xf',
 			$backup_path . '/pg_wal.tar', '-C',
 			$data_path . '/pg_wal');
+
+		# We need to generate a tablespace_map file.
+		open(my $tsmap, ">", "$data_path/tablespace_map")
+			|| die "$data_path/tablespace_map: $!";
+
+		# Extract tarfiles and add tablespace_map entries
+		my @tstars = grep { /^\d+.tar/ }
+			PostgreSQL::Test::Utils::slurp_dir($backup_path);
+		for my $tstar (@tstars)
+		{
+			my $tsoid = $tstar;
+			$tsoid =~ s/\.tar$//;
+
+			die "no tablespace mapping for $tstar"
+				if !exists $params{tablespace_map} ||
+				   !exists $params{tablespace_map}{$tsoid};
+			my $newdir = $params{tablespace_map}{$tsoid};
+
+			mkdir($newdir) || die "mkdir $newdir: $!";
+			PostgreSQL::Test::Utils::system_or_bail($params{tar_program}, 'xf',
+				$backup_path . '/' . $tstar, '-C', $newdir);
+
+			my $escaped_newdir = $newdir;
+			$escaped_newdir =~ s/\\/\\\\/g;
+			print $tsmap "$tsoid $escaped_newdir\n";
+		}
+
+		# Close tablespace_map.
+		close($tsmap);
 	}
 	else
 	{
+		my @tsoids;
 		rmdir($data_path);
-		PostgreSQL::Test::RecursiveCopy::copypath($backup_path, $data_path);
+
+		# Copy the main backup. If we see a tablespace directory for which we
+		# have a tablespace mapping, skip it, but remember that we saw it.
+		PostgreSQL::Test::RecursiveCopy::copypath($backup_path, $data_path,
+			'filterfn' => sub {
+				my ($path) = @_;
+				if ($path =~ /^pg_tblspc\/(\d+)$/ &&
+					exists $params{tablespace_map}{$1})
+				{
+					push @tsoids, $1;
+					return 0;
+				}
+				return 1;
+			});
+
+		if (@tsoids > 0)
+		{
+			# We need to generate a tablespace_map file.
+			open(my $tsmap, ">", "$data_path/tablespace_map")
+				|| die "$data_path/tablespace_map: $!";
+
+			# Now use the list of tablespace links to copy each tablespace.
+			for my $tsoid (@tsoids)
+			{
+				die "no tablespace mapping for $tsoid"
+					if !exists $params{tablespace_map} ||
+					   !exists $params{tablespace_map}{$tsoid};
+
+				my $olddir = $backup_path . '/pg_tblspc/' . $tsoid;
+				my $newdir = $params{tablespace_map}{$tsoid};
+				PostgreSQL::Test::RecursiveCopy::copypath($olddir, $newdir);
+
+				my $escaped_newdir = $newdir;
+				$escaped_newdir =~ s/\\/\\\\/g;
+				print $tsmap "$tsoid $escaped_newdir\n";
+			}
+
+			# Close tablespace_map.
+			close($tsmap);
+		}
 	}
 	chmod(0700, $data_path) or die $!;
 
-- 
2.39.3 (Apple Git-145)

