1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
|
# Copyright (c) 2021-2025, PostgreSQL Global Development Group
use strict;
use warnings FATAL => 'all';
use File::Compare qw(compare_text);
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
my $tempdir = PostgreSQL::Test::Utils::tempdir_short();
# Can be changed to test the other modes.
my $mode = $ENV{PG_TEST_PG_COMBINEBACKUP_MODE} || '--copy';
note "testing using mode $mode";
# 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.
$primary->safe_psql('postgres', <<EOM);
CREATE TABLE will_change (a int, b text);
INSERT INTO will_change VALUES (1, 'initial test row');
CREATE TABLE will_grow (a int, b text);
INSERT INTO will_grow VALUES (1, 'initial test row');
CREATE TABLE will_shrink (a int, b text);
INSERT INTO will_shrink VALUES (1, 'initial test row');
CREATE TABLE will_get_vacuumed (a int, b text);
INSERT INTO will_get_vacuumed VALUES (1, 'initial test row');
CREATE TABLE will_get_dropped (a int, b text);
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',
'--no-sync',
'--pgdata' => $backup1path,
'--checkpoint' => 'fast',
'--tablespace-mapping' => "${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;
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',
'--no-sync',
'--pgdata' => $backup2path,
'--checkpoint' => 'fast',
'--tablespace-mapping' => "${tsprimary}=${tsbackup2path}",
'--incremental' => $backup1path . '/backup_manifest'
],
"incremental backup");
# Find an LSN to which either backup can be recovered.
my $lsn = $primary->safe_psql('postgres', "SELECT pg_current_wal_lsn();");
# Make sure that the WAL segment containing that LSN has been archived.
# PostgreSQL won't issue two consecutive XLOG_SWITCH records, and the backup
# just issued one, so call txid_current() to generate some WAL activity
# before calling pg_switch_wal().
$primary->safe_psql('postgres', 'SELECT txid_current();');
$primary->safe_psql('postgres', 'SELECT pg_switch_wal()');
# Now wait for the LSN we chose above to be archived.
my $archive_wait_query =
"SELECT pg_walfile_name('$lsn') <= last_archived_wal FROM pg_stat_archiver;";
$primary->poll_query_until('postgres', $archive_wait_query)
or die "Timed out while waiting for WAL segment to be archived";
# 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,
tablespace_map => { $tsoid => $tspitr1path });
$pitr1->append_conf(
'postgresql.conf', qq{
recovery_target_lsn = '$lsn'
recovery_target_action = 'promote'
archive_mode = 'off'
});
$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'],
tablespace_map => { $tsbackup2path => $tspitr2path },
combine_mode => $mode);
$pitr2->append_conf(
'postgresql.conf', qq{
recovery_target_lsn = '$lsn'
recovery_target_action = 'promote'
archive_mode = 'off'
});
$pitr2->start();
# Wait until both servers exit recovery.
$pitr1->poll_query_until('postgres', "SELECT NOT pg_is_in_recovery();")
or die "Timed out while waiting apply to reach LSN $lsn";
$pitr2->poll_query_until('postgres', "SELECT NOT pg_is_in_recovery();")
or die "Timed out while waiting apply to reach LSN $lsn";
# Perform a logical dump of each server, and check that they match.
# It would be much nicer if we could physically compare the data files, but
# that doesn't really work. The contents of the page hole aren't guaranteed to
# be identical, and there can be other discrepancies as well. To make this work
# we'd need the equivalent of each AM's rm_mask function written or at least
# callable from Perl, and that doesn't seem practical.
#
# NB: We're just using the primary's backup directory for scratch space here.
# This could equally well be any other directory we wanted to pick.
my $backupdir = $primary->backup_dir;
my $dump1 = $backupdir . '/pitr1.dump';
my $dump2 = $backupdir . '/pitr2.dump';
$pitr1->command_ok(
[
'pg_dumpall',
'--no-sync',
'--no-unlogged-table-data',
'--file' => $dump1,
'--dbname' => $pitr1->connstr('postgres'),
],
'dump from PITR 1');
$pitr2->command_ok(
[
'pg_dumpall',
'--no-sync',
'--no-unlogged-table-data',
'--file' => $dump2,
'--dbname' => $pitr2->connstr('postgres'),
],
'dump from PITR 2');
# Compare the two dumps, there should be no differences other than
# the tablespace paths.
compare_files(
$dump1, $dump2,
"contents of dumps match for both PITRs",
sub {
s{create tablespace .* location .*\btspitr\K[12]}{N}i for @_;
return $_[0] ne $_[1];
});
done_testing();
|