####################################################################################################################################
# Test All Commands On PostgreSQL Clusters
####################################################################################################################################
package pgBackRestTest::Module::Real::RealAllTest;
use parent 'pgBackRestTest::Env::HostEnvTest';

####################################################################################################################################
# Perl includes
####################################################################################################################################
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);

use File::Basename qw(dirname);

use pgBackRestDoc::Common::Exception;
use pgBackRestDoc::Common::Ini;
use pgBackRestDoc::Common::Log;
use pgBackRestDoc::ProjectInfo;

use pgBackRestTest::Common::ContainerTest;
use pgBackRestTest::Common::DbVersion;
use pgBackRestTest::Common::ExecuteTest;
use pgBackRestTest::Common::FileTest;
use pgBackRestTest::Common::RunTest;
use pgBackRestTest::Common::VmTest;
use pgBackRestTest::Common::Storage;
use pgBackRestTest::Common::StoragePosix;
use pgBackRestTest::Common::StorageRepo;
use pgBackRestTest::Common::Wait;
use pgBackRestTest::Env::ArchiveInfo;
use pgBackRestTest::Env::BackupInfo;
use pgBackRestTest::Env::InfoCommon;
use pgBackRestTest::Env::Host::HostBaseTest;
use pgBackRestTest::Env::Host::HostBackupTest;
use pgBackRestTest::Env::Host::HostDbTest;
use pgBackRestTest::Env::Host::HostDbTest;
use pgBackRestTest::Env::HostEnvTest;
use pgBackRestTest::Env::Manifest;

####################################################################################################################################
# Backup advisory lock
####################################################################################################################################
use constant DB_BACKUP_ADVISORY_LOCK                                => '12340078987004321';

####################################################################################################################################
# run
####################################################################################################################################
sub run
{
    my $self = shift;

    foreach my $rhRun
    (
        {pg => PG_VERSION_83, repoDest => HOST_DB_PRIMARY, tls => 0, storage => POSIX, encrypt => 0, compress => NONE, repo => 2},
        {pg => PG_VERSION_84, repoDest =>     HOST_BACKUP, tls => 1, storage => AZURE, encrypt => 1, compress =>   GZ, repo => 1},
        {pg => PG_VERSION_90, repoDest => HOST_DB_PRIMARY, tls => 0, storage =>   GCS, encrypt => 1, compress =>  BZ2, repo => 2},
        {pg => PG_VERSION_91, repoDest => HOST_DB_STANDBY, tls => 1, storage =>   GCS, encrypt => 0, compress =>   GZ, repo => 1},
        {pg => PG_VERSION_92, repoDest => HOST_DB_STANDBY, tls => 0, storage => POSIX, encrypt => 1, compress => NONE, repo => 1},
        {pg => PG_VERSION_93, repoDest =>     HOST_BACKUP, tls => 0, storage => AZURE, encrypt => 0, compress => NONE, repo => 2},
        {pg => PG_VERSION_94, repoDest => HOST_DB_STANDBY, tls => 0, storage => POSIX, encrypt => 1, compress =>  LZ4, repo => 1},
        {pg => PG_VERSION_95, repoDest =>     HOST_BACKUP, tls => 1, storage =>    S3, encrypt => 0, compress =>  BZ2, repo => 1},
        {pg => PG_VERSION_96, repoDest =>     HOST_BACKUP, tls => 0, storage => POSIX, encrypt => 0, compress => NONE, repo => 2},
        {pg => PG_VERSION_10, repoDest => HOST_DB_STANDBY, tls => 1, storage =>    S3, encrypt => 1, compress =>   GZ, repo => 2},
        {pg => PG_VERSION_11, repoDest =>     HOST_BACKUP, tls => 1, storage => AZURE, encrypt => 0, compress =>  ZST, repo => 2},
        {pg => PG_VERSION_12, repoDest =>     HOST_BACKUP, tls => 0, storage =>    S3, encrypt => 1, compress =>  LZ4, repo => 1},
        {pg => PG_VERSION_13, repoDest => HOST_DB_STANDBY, tls => 1, storage =>   GCS, encrypt => 0, compress =>  ZST, repo => 1},
        {pg => PG_VERSION_14, repoDest =>     HOST_BACKUP, tls => 0, storage => POSIX, encrypt => 1, compress =>  LZ4, repo => 2},
    )
    {
        # Only run tests for this pg version
        next if ($rhRun->{pg} ne $self->pgVersion());

        # Get run parameters
        my $bHostBackup = $rhRun->{repoDest} eq HOST_BACKUP ? true : false;
        my $bHostStandby = $self->pgVersion() >= PG_VERSION_HOT_STANDBY ? true : false;
        my $bTls = $rhRun->{tls};
        my $strBackupDestination = $rhRun->{repoDest};
        my $strStorage = $rhRun->{storage};
        my $bRepoEncrypt = $rhRun->{encrypt};
        my $strCompressType = $rhRun->{compress};
        my $iRepoTotal = $rhRun->{repo};

        # Use a specific VM and version of PostgreSQL for expect testing. This version will also be used to run tests that are not
        # version specific.
        my $bExpectVersion = $self->vm() eq VM_EXPECT && $self->pgVersion() eq PG_VERSION_96;

        # Increment the run, log, and decide whether this unit test should be run
        next if (!$self->begin(
            "bkp ${bHostBackup}, sby ${bHostStandby}, tls ${bTls}, dst ${strBackupDestination}, cmp ${strCompressType}" .
                ", storage ${strStorage}, enc ${bRepoEncrypt}",
            $bExpectVersion));

        # Create hosts, file object, and config
        my ($oHostDbPrimary, $oHostDbStandby, $oHostBackup) = $self->setup(
            false, $self->expect(),
            {bHostBackup => $bHostBackup, bStandby => $bHostStandby, bTls => $bTls, strBackupDestination => $strBackupDestination,
             strCompressType => $strCompressType, bArchiveAsync => false, strStorage => $strStorage,
             bRepoEncrypt => $bRepoEncrypt, iRepoTotal => $iRepoTotal});

        # Some commands will fail because of the bogus host created when a standby is present. These options reset the bogus host
        # so it won't interfere with commands that won't tolerate a connection failure.
        my $strBogusReset = $oHostBackup->bogusHost() ?
            ' --reset-pg2-host --reset-pg2-host-type --reset-pg2-host-cmd --reset-pg2-host-config --reset-pg2-host-user' .
                ' --reset-pg2-path' :
            '';

        # If S3 set process max to 2.  This seems like the best place for parallel testing since it will help speed S3 processing
        # without slowing down the other tests too much.
        if ($strStorage eq S3)
        {
            $oHostBackup->configUpdate({&CFGDEF_SECTION_GLOBAL => {'process-max' => 2}});
            $oHostDbPrimary->configUpdate({&CFGDEF_SECTION_GLOBAL => {'process-max' => 2}});
        }

        $oHostDbPrimary->clusterCreate();

        # Create the stanza
        $oHostDbPrimary->stanzaCreate('main create stanza info files');

        # Get passphrase to access the Manifest file from backup.info - returns undefined if repo not encrypted
        my $strCipherPass =
            (new pgBackRestTest::Env::BackupInfo($oHostBackup->repoBackupPath()))->cipherPassSub();

        # Create a manifest with the pg version to get version-specific paths
        my $oManifest = new pgBackRestTest::Env::Manifest(BOGUS, {bLoad => false, strDbVersion => $self->pgVersion(),
            iDbCatalogVersion => $self->dbCatalogVersion($self->pgVersion()),
            strCipherPass => $strCipherPass, strCipherPassSub => $bRepoEncrypt ? ENCRYPTION_KEY_BACKUPSET : undef});

        # Static backup parameters
        my $fTestDelay = 1;

        # Restore test string
        my $strDefaultMessage = 'default';
        my $strFullMessage = 'full';
        my $strStandbyMessage = 'standby';
        my $strIncrMessage = 'incr';
        my $strTimeMessage = 'time';
        my $strXidMessage = 'xid';
        my $strNameMessage = 'name';
        my $strTimelineMessage = 'timeline';

        # Create two new databases
        $oHostDbPrimary->sqlExecute('create database test1', {bAutoCommit => true});
        $oHostDbPrimary->sqlExecute('create database test2', {bAutoCommit => true});

        # ??? Removed temporarily until manifest build can be brought back into the check command
        # Create a directory in pg_data location that is only readable by root to ensure manifest->build is called by check
        # --------------------------------------------------------------------------------------------------------------------------
        # my $strDir = $oHostDbPrimary->dbBasePath() . '/rootreaddir';
        # executeTest('sudo mkdir ' . $strDir);
        # executeTest("sudo chown root:root ${strDir}");
        # executeTest("sudo chmod 400 ${strDir}");
        #
        # $strComment = 'confirm primary manifest->build executed';
        # $oHostDbPrimary->check($strComment, {iTimeout => 5, iExpectedExitStatus => ERROR_PATH_OPEN});
        # executeTest("sudo rmdir ${strDir}");

        # --------------------------------------------------------------------------------------------------------------------------
        my $strComment = 'verify check command runs successfully';

        $oHostDbPrimary->check($strComment, {iTimeout => 5});

        # Also run check on the backup host when present
        if ($bHostBackup)
        {
            $oHostBackup->check($strComment, {iTimeout => 5, strOptionalParam => $strBogusReset});
        }

        # Restart the cluster ignoring any errors in the postgresql log
        $oHostDbPrimary->clusterRestart({bIgnoreLogError => true});

        # Full backup
        #---------------------------------------------------------------------------------------------------------------------------
        # Create the table where test messages will be stored
        $oHostDbPrimary->sqlExecute("create table test (message text not null)");
        $oHostDbPrimary->sqlWalRotate();
        $oHostDbPrimary->sqlExecute("insert into test values ('$strDefaultMessage')");

        # Acquire the backup advisory lock so it looks like a backup is running
        if (!$oHostDbPrimary->sqlSelectOne('select pg_try_advisory_lock(' . DB_BACKUP_ADVISORY_LOCK . ')'))
        {
            confess 'unable to acquire advisory lock for testing';
        }

        $oHostBackup->backup(
            CFGOPTVAL_BACKUP_TYPE_FULL, 'fail on backup lock exists', {iExpectedExitStatus => ERROR_LOCK_ACQUIRE});

        # Release the backup advisory lock so the next backup will succeed
        if (!$oHostDbPrimary->sqlSelectOne('select pg_advisory_unlock(' . DB_BACKUP_ADVISORY_LOCK . ')'))
        {
            confess 'unable to release advisory lock';
        }

        $oHostDbPrimary->sqlExecute("update test set message = '$strFullMessage'");

        # Required to set hint bits to be sent to the standby to make the heap match on both sides
        $oHostDbPrimary->sqlSelectOneTest('select message from test', $strFullMessage);

        # Backup to repo1
        my $strFullBackup = $oHostBackup->backup(
            CFGOPTVAL_BACKUP_TYPE_FULL, 'repo1',
            {strOptionalParam => ' --buffer-size=16384'});

        # Backup to repo2 if it exists
        if ($iRepoTotal == 2)
        {
            $oHostBackup->backup(CFGOPTVAL_BACKUP_TYPE_FULL, 'repo2', {iRepo => 2});
        }

        # Make a new backup with expire-auto disabled then run the expire command and compare backup numbers to ensure that expire
        # was really disabled. This test is not version specific so is run on only the expect version.
        #---------------------------------------------------------------------------------------------------------------------------
        if ($bExpectVersion)
        {
            my $oBackupInfo = new pgBackRestTest::Env::BackupInfo($oHostBackup->repoBackupPath());
            push(my @backupLst1, $oBackupInfo->list());

            $strFullBackup = $oHostBackup->backup(
                CFGOPTVAL_BACKUP_TYPE_FULL, 'with disabled expire-auto',
                {strOptionalParam => ' --repo1-retention-full='.scalar(@backupLst1). ' --no-expire-auto'});

            $oBackupInfo = new pgBackRestTest::Env::BackupInfo($oHostBackup->repoBackupPath());
            push(my @backupLst2, $oBackupInfo->list());

            &log(INFO, "    run the expire command");
            $oHostBackup->expire({iRetentionFull => scalar(@backupLst1)});
            $oBackupInfo = new pgBackRestTest::Env::BackupInfo($oHostBackup->repoBackupPath());
            push(my @backupLst3, $oBackupInfo->list());

            unless (scalar(@backupLst2) == scalar(@backupLst1) + 1 && scalar(@backupLst1) == scalar(@backupLst3))
            {
                confess "expire-auto option didn't work as expected";
            }
        }

        # Enabled async archiving
        $oHostBackup->configUpdate({&CFGDEF_SECTION_GLOBAL => {'archive-async' => 'y'}});

        # Kick out a bunch of archive logs to exercise async archiving.  Only do this when compressed and remote to slow it
        # down enough to make it evident that the async process is working.
        if ($strCompressType ne NONE && $strBackupDestination eq HOST_BACKUP)
        {
            &log(INFO, '    multiple wal switches to exercise async archiving');
            $oHostDbPrimary->sqlExecute("create table wal_activity (id int)");
            $oHostDbPrimary->sqlWalRotate();
            $oHostDbPrimary->sqlExecute("insert into wal_activity values (1)");
            $oHostDbPrimary->sqlWalRotate();
            $oHostDbPrimary->sqlExecute("insert into wal_activity values (2)");
            $oHostDbPrimary->sqlWalRotate();
            $oHostDbPrimary->sqlExecute("insert into wal_activity values (3)");
            $oHostDbPrimary->sqlWalRotate();
            $oHostDbPrimary->sqlExecute("insert into wal_activity values (4)");
            $oHostDbPrimary->sqlWalRotate();
        }

        # Setup replica
        #---------------------------------------------------------------------------------------------------------------------------
        if ($bHostStandby)
        {
            my %oRemapHash;
            $oRemapHash{&MANIFEST_TARGET_PGDATA} = $oHostDbStandby->dbBasePath();

            if ($oHostDbStandby->pgVersion() >= PG_VERSION_92)
            {
                $oHostDbStandby->linkRemap($oManifest->walPath(), $oHostDbStandby->dbPath() . '/' . $oManifest->walPath());
            }

            $oHostDbStandby->restore(
                'restore backup on replica', 'latest',
                {rhRemapHash => \%oRemapHash, strType => CFGOPTVAL_RESTORE_TYPE_STANDBY,
                    strOptionalParam =>
                        ' --recovery-option="primary_conninfo=host=' . HOST_DB_PRIMARY .
                        ' port=' . $oHostDbPrimary->pgPort() . ' user=replicator"'});

            $oHostDbStandby->clusterStart({bHotStandby => true});

            # Make sure streaming replication is on
            $oHostDbPrimary->sqlSelectOneTest(
                "select client_addr || '-' || state from pg_stat_replication", $oHostDbStandby->ipGet() . '/32-streaming');

            # Check that the cluster was restored properly
            $oHostDbStandby->sqlSelectOneTest('select message from test', $strFullMessage);

            # Update message for standby
            $oHostDbPrimary->sqlExecute("update test set message = '$strStandbyMessage'");

            if ($oHostDbStandby->pgVersion() >= PG_VERSION_BACKUP_STANDBY && !$bTls)
            {
                # If there is only a primary and a replica and the replica is the backup destination, then if pg2-host and
                # pg256-host are BOGUS, confirm failure to reach the primary
                if (!$bHostBackup && $bHostStandby && $strBackupDestination eq HOST_DB_STANDBY)
                {
                    my $strStandbyBackup = $oHostBackup->backup(
                        CFGOPTVAL_BACKUP_TYPE_FULL, 'backup from standby, failure to reach primary',
                        {bStandby => true, iExpectedExitStatus => ERROR_DB_CONNECT, strOptionalParam => '--pg256-host=' . BOGUS});
                }
                else
                {
                    my $strStandbyBackup = $oHostBackup->backup(
                        CFGOPTVAL_BACKUP_TYPE_FULL, 'backup from standby, failure to access at least one standby',
                        {bStandby => true, iExpectedExitStatus => ERROR_DB_CONNECT, strOptionalParam => '--pg256-host=' . BOGUS});
                }
            }

            my $strStandbyBackup = $oHostBackup->backup(
                CFGOPTVAL_BACKUP_TYPE_FULL, 'backup from standby',
                {bStandby => true,
                 iExpectedExitStatus => $oHostDbStandby->pgVersion() >= PG_VERSION_BACKUP_STANDBY ? undef : ERROR_CONFIG,
                 strOptionalParam => '--repo1-retention-full=1'});

            if ($oHostDbStandby->pgVersion() >= PG_VERSION_BACKUP_STANDBY)
            {
                $strFullBackup = $strStandbyBackup;
            }

            # ??? Removed temporarily until manifest build can be brought back into the check command
            # # Create a directory in pg_data location that is only readable by root to ensure manifest->build is called by check
            # my $strDir = $oHostDbStandby->dbBasePath() . '/rootreaddir';
            # executeTest('sudo mkdir ' . $strDir);
            # executeTest("sudo chown root:root ${strDir}");
            # executeTest("sudo chmod 400 ${strDir}");
            #
            # my $strComment = 'confirm standby manifest->build executed';
            #
            # # If there is an invalid host, the final error returned from check will be the inability to resolve the name which is
            # # an open error instead of a read error
            # if (!$oHostDbStandby->bogusHost())
            # {
            #     $oHostDbStandby->check($strComment, {iTimeout => 5, iExpectedExitStatus => ERROR_PATH_OPEN});
            # }
            # else
            # {
            #     $oHostDbStandby->check($strComment, {iTimeout => 5, iExpectedExitStatus => ERROR_FILE_READ});
            # }
            #
            # # Remove the directory in pg_data location that is only readable by root
            # executeTest("sudo rmdir ${strDir}");

            # Confirm the check command runs without error on a standby (when a bogus host is not configured)
            $oHostDbStandby->check('verify check command on standby', {strOptionalParam => $strBogusReset});

            # Shutdown the standby before creating tablespaces (this will error since paths are different)
            $oHostDbStandby->clusterStop({bIgnoreLogError => true});
        }

        my $strAdhocBackup;

        # Execute stop and make sure the backup fails
        #---------------------------------------------------------------------------------------------------------------------------
        # Restart the cluster to check for any errors before continuing since the stop tests will definitely create errors and the
        # logs will to be deleted to avoid causing issues further down the line. This test is not version specific so is run on only
        # the expect version.
        if ($bExpectVersion)
        {
            confess "test must be performed on posix storage" if $strStorage ne POSIX;

            $oHostDbPrimary->clusterRestart();

            # Add backup for adhoc expire
            $strAdhocBackup = $oHostBackup->backup(CFGOPTVAL_BACKUP_TYPE_DIFF, 'backup for adhoc expire');

            $oHostDbPrimary->stop();

            $oHostBackup->backup(
                CFGOPTVAL_BACKUP_TYPE_INCR, 'attempt backup when stopped',
                {iExpectedExitStatus => $oHostBackup == $oHostDbPrimary ? ERROR_STOP : ERROR_DB_CONNECT});

            $oHostDbPrimary->start();
        }

        # Setup the time targets
        #---------------------------------------------------------------------------------------------------------------------------
        # If the tests are running quickly then the time target might end up the same as the end time of the prior full backup. That
        # means restore auto-select will not pick it as a candidate and restore the last backup instead causing the restore compare
        # to fail. So, sleep one second.
        sleep(1);

        $oHostDbPrimary->sqlExecute("update test set message = '$strTimeMessage'");
        $oHostDbPrimary->sqlWalRotate();
        my $strTimeTarget = $oHostDbPrimary->sqlSelectOne("select current_timestamp");
        &log(INFO, "        time target is ${strTimeTarget}");

        # Incr backup - fail on archive_mode=always when version >= 9.5
        #---------------------------------------------------------------------------------------------------------------------------
        if ($oHostDbPrimary->pgVersion() >= PG_VERSION_95)
        {
            # Set archive_mode=always
            $oHostDbPrimary->clusterRestart({bArchiveAlways => true});

            $oHostBackup->backup(
                CFGOPTVAL_BACKUP_TYPE_INCR, 'fail on archive_mode=always', {iExpectedExitStatus => ERROR_FEATURE_NOT_SUPPORTED});

            # Reset the cluster to a normal state so the next test will work
            $oHostDbPrimary->clusterRestart();
        }

        # Incr backup
        #---------------------------------------------------------------------------------------------------------------------------
        # Create a tablespace directory
        storageTest()->pathCreate($oHostDbPrimary->tablespacePath(1), {strMode => '0700', bCreateParent => true});

        # Also create it on the standby so replay won't fail
        if (defined($oHostDbStandby))
        {
            storageTest()->pathCreate($oHostDbStandby->tablespacePath(1), {strMode => '0700', bCreateParent => true});
        }

        $oHostDbPrimary->sqlExecute(
            "create tablespace ts1 location '" . $oHostDbPrimary->tablespacePath(1) . "'", {bAutoCommit => true});
        $oHostDbPrimary->sqlExecute("alter table test set tablespace ts1");

        # Create a table in the tablespace that will not be modified again to be sure it does get full page writes in the WAL later
        $oHostDbPrimary->sqlExecute("create table test_exists (id int) tablespace ts1", {bCommit => true, bCheckPoint => true});

        # Create a table in the tablespace
        $oHostDbPrimary->sqlExecute("create table test_remove (id int)");
        $oHostDbPrimary->sqlWalRotate();
        $oHostDbPrimary->sqlExecute("update test set message = '$strDefaultMessage'");
        $oHostDbPrimary->sqlWalRotate();

        # Create a database in the tablespace and a table to check
        $oHostDbPrimary->sqlExecute("create database test3 with tablespace ts1", {bAutoCommit => true});
        $oHostDbPrimary->sqlExecute(
            'create table test3_exists (id int);' .
            'insert into test3_exists values (1);',
            {strDb => 'test3', bAutoCommit => true});

        # Create a table in test1 to check - test1 will not be restored
        $oHostDbPrimary->sqlExecute(
            'create table test1_zeroed (id int);' .
            'insert into test1_zeroed values (1);',
            {strDb => 'test1', bAutoCommit => true});

        # Start a backup so the next backup has to restart it.  This test is not required for PostgreSQL >= 9.6 since backups
        # are run in non-exclusive mode.
        if ($oHostDbPrimary->pgVersion() >= PG_VERSION_93 && $oHostDbPrimary->pgVersion() < PG_VERSION_96)
        {
            $oHostDbPrimary->sqlSelectOne("select pg_start_backup('test backup that will cause an error', true)");

            # Verify that an error is returned if the backup is already running
            $oHostBackup->backup(
                CFGOPTVAL_BACKUP_TYPE_INCR, 'fail on backup already running', {iExpectedExitStatus => ERROR_DB_QUERY});

            # Restart the cluster ignoring any errors in the postgresql log
            $oHostDbPrimary->clusterRestart({bIgnoreLogError => true});

            # Start a new backup to make the next test restarts it
            $oHostDbPrimary->sqlSelectOne("select pg_start_backup('test backup that will be restarted', true)");
        }

        if (defined($strAdhocBackup))
        {
            # Adhoc expire the latest backup - no other tests should be affected
            $oHostBackup->expire({strOptionalParam => '--set=' . $strAdhocBackup});
        }

        # Drop a table
        $oHostDbPrimary->sqlExecute('drop table test_remove');
        $oHostDbPrimary->sqlWalRotate();
        $oHostDbPrimary->sqlExecute("update test set message = '$strIncrMessage'", {bCommit => true});

        # Exercise --delta checksum option
        my $strIncrBackup = $oHostBackup->backup(
            CFGOPTVAL_BACKUP_TYPE_INCR, 'delta',
            {strOptionalParam => '--stop-auto --buffer-size=32768 --delta', iRepo => $iRepoTotal});

        # Ensure the check command runs properly with a tablespace
        $oHostBackup->check( 'check command with tablespace', {iTimeout => 5, strOptionalParam => $strBogusReset});

        # Setup the xid target
        #---------------------------------------------------------------------------------------------------------------------------
        my $strXidTarget = undef;

        $oHostDbPrimary->sqlExecute("update test set message = '$strXidMessage'", {bCommit => false});
        $oHostDbPrimary->sqlWalRotate();
        $strXidTarget = $oHostDbPrimary->sqlSelectOne("select txid_current()");
        $oHostDbPrimary->sqlCommit();
        &log(INFO, "        xid target is ${strXidTarget}");

        # Setup the name target
        #---------------------------------------------------------------------------------------------------------------------------
        my $strNameTarget = 'backrest';

        $oHostDbPrimary->sqlExecute("update test set message = '$strNameMessage'", {bCommit => true});
        $oHostDbPrimary->sqlWalRotate();

        if ($oHostDbPrimary->pgVersion() >= PG_VERSION_91)
        {
            $oHostDbPrimary->sqlExecute("select pg_create_restore_point('${strNameTarget}')");
        }

        &log(INFO, "        name target is ${strNameTarget}");

        # Create a table and data in database test2
        #---------------------------------------------------------------------------------------------------------------------------
        # Initialize variables for SHA1 and path of the pg_filenode.map for the database that will not be restored
        my $strDb1TablePath;
        my $strDb1TableSha1;

        $oHostDbPrimary->sqlExecute(
            'create table test (id int);' .
            'insert into test values (1);' .
            'create table test_ts1 (id int) tablespace ts1;' .
            'insert into test_ts1 values (2);',
            {strDb => 'test2', bAutoCommit => true});

        $oHostDbPrimary->sqlWalRotate();

        # Get the SHA1 and path of the table for the database that will not be restored
        $strDb1TablePath =  $oHostDbPrimary->dbBasePath(). "/base/" .
            $oHostDbPrimary->sqlSelectOne("select oid from pg_database where datname='test1'") . "/" .
            $oHostDbPrimary->sqlSelectOne("select relfilenode from pg_class where relname='test1_zeroed'", {strDb => 'test1'});
        $strDb1TableSha1 = storageTest()->hashSize($strDb1TablePath);

        # Restore (type = default)
        #---------------------------------------------------------------------------------------------------------------------------
        # Expect failure because pg (appears to be) running
        $oHostDbPrimary->restore('pg running', 'latest', {iExpectedExitStatus => ERROR_PG_RUNNING});

        $oHostDbPrimary->clusterStop();

        # Expect failure because db path is not empty
        $oHostDbPrimary->restore('path not empty', 'latest', {iExpectedExitStatus => ERROR_PATH_NOT_EMPTY});

        # Drop and recreate db path
        testPathRemove($oHostDbPrimary->dbBasePath());
        storageTest()->pathCreate($oHostDbPrimary->dbBasePath(), {strMode => '0700'});
        testPathRemove($oHostDbPrimary->dbPath() . qw{/} . $oManifest->walPath());
        storageTest()->pathCreate($oHostDbPrimary->dbPath() . qw{/} . $oManifest->walPath(), {strMode => '0700'});
        testPathRemove($oHostDbPrimary->tablespacePath(1));
        storageTest()->pathCreate($oHostDbPrimary->tablespacePath(1), {strMode => '0700'});

        # Now the restore should work
        $oHostDbPrimary->restore(
            undef, 'latest',
            {strOptionalParam => ' --db-include=test2 --db-include=test3 --buffer-size=16384', iRepo => $iRepoTotal});

        # Test that the first database has not been restored since --db-include did not include test1
        my ($strSHA1, $lSize) = storageTest()->hashSize($strDb1TablePath);

        # Create a zeroed sparse file in the test directory that is the same size as the filenode.map.  We need to use the
        # posix driver directly to do this because handles cannot be passed back from the C code.
        my $oStorageTrunc = new pgBackRestTest::Common::Storage($self->testPath(), new pgBackRestTest::Common::StoragePosix());

        my $strTestTable = $self->testPath() . "/testtable";
        my $oDestinationFileIo = $oStorageTrunc->openWrite($strTestTable);
        $oDestinationFileIo->open();

        # Truncate to the original size which will create a sparse file.
        if (!truncate($oDestinationFileIo->handle(), $lSize))
        {
            confess "unable to truncate '$strTestTable' with handle " . $oDestinationFileIo->handle();
        }
        $oDestinationFileIo->close();

        # Confirm the test filenode.map and the database test1 filenode.map are zeroed
        my ($strSHA1Test, $lSizeTest) = storageTest()->hashSize($strTestTable);
        $self->testResult(sub {($strSHA1Test eq $strSHA1) && ($lSizeTest == $lSize) && ($strSHA1 ne $strDb1TableSha1)},
            true, 'database test1 not restored');

        $oHostDbPrimary->clusterStart();
        $oHostDbPrimary->sqlSelectOneTest('select message from test', $strNameMessage);

        # Once the cluster is back online, make sure the database & table in the tablespace exists properly
        $oHostDbPrimary->sqlSelectOneTest('select id from test_ts1', 2, {strDb => 'test2'});
        $oHostDbPrimary->sqlDisconnect({strDb => 'test2'});

        $oHostDbPrimary->sqlSelectOneTest('select id from test3_exists', 1, {strDb => 'test3'});
        $oHostDbPrimary->sqlDisconnect({strDb => 'test3'});

        # The tablespace path should exist and have files in it
        my $strTablespacePath = $oHostDbPrimary->tablespacePath(1);

        # Version <= 8.4 always places a PG_VERSION file in the tablespace
        if ($oHostDbPrimary->pgVersion() <= PG_VERSION_84)
        {
            if (!storageTest()->exists("${strTablespacePath}/" . DB_FILE_PGVERSION))
            {
                confess &log(ASSERT, "unable to find '" . DB_FILE_PGVERSION . "' in tablespace path '${strTablespacePath}'");
            }
        }
        # Version >= 9.0 creates a special path using the version and catalog number
        else
        {
            # Backup info will have the catalog number
            my $oBackupInfo = new pgBackRestDoc::Common::Ini(
                storageRepo(), $oHostBackup->repoBackupPath(FILE_BACKUP_INFO),
                {bLoad => false, strContent => ${storageRepo()->get($oHostBackup->repoBackupPath(FILE_BACKUP_INFO))}});

            # Construct the special path
            $strTablespacePath .=
                '/PG_' . $oHostDbPrimary->pgVersion() . qw{_} . $oBackupInfo->get(INFO_BACKUP_SECTION_DB, INFO_BACKUP_KEY_CATALOG);

            # Check that path exists
            if (!storageTest()->pathExists($strTablespacePath))
            {
                confess &log(ASSERT, "unable to find tablespace path '${strTablespacePath}'");
            }
        }

        # Make sure there are some files in the tablespace path (exclude PG_VERSION if <= 8.4 since that was tested above)
        if (grep(!/^PG\_VERSION$/i, storageTest()->list($strTablespacePath)) == 0)
        {
            confess &log(ASSERT, "no files found in tablespace path '${strTablespacePath}'");
        }

        # This table should exist to prove that the tablespace was restored.  It has not been updated since it was created so it
        # should not be created by any full page writes.  Once it is verified to exist it can be dropped.
        $oHostDbPrimary->sqlSelectOneTest("select count(*) from test_exists", 0);
        $oHostDbPrimary->sqlExecute('drop table test_exists');

        # Now it should be OK to drop database test2 and test3
        $oHostDbPrimary->sqlExecute('drop database test2', {bAutoCommit => true});

        # The test table lives in ts1 so it needs to be moved or dropped
        if ($oHostDbPrimary->pgVersion() >= PG_VERSION_90)
        {
            $oHostDbPrimary->sqlExecute('alter table test set tablespace pg_default');
        }
        # Drop for older versions
        else
        {
            $oHostDbPrimary->sqlExecute('drop table test');
        }

        # And drop the tablespace
        $oHostDbPrimary->sqlExecute('drop database test3', {bAutoCommit => true});
        $oHostDbPrimary->sqlExecute("drop tablespace ts1", {bAutoCommit => true});

        # Restore (restore type = immediate, inclusive)
        #---------------------------------------------------------------------------------------------------------------------------
        if ($oHostDbPrimary->pgVersion() >= PG_VERSION_94)
        {
            &log(INFO, '    testing recovery type = ' . CFGOPTVAL_RESTORE_TYPE_IMMEDIATE);

            $oHostDbPrimary->clusterStop();

            $oHostDbPrimary->restore(
                undef, $strFullBackup, {bForce => true, strType => CFGOPTVAL_RESTORE_TYPE_IMMEDIATE, strTargetAction => 'promote'});

            $oHostDbPrimary->clusterStart();
            $oHostDbPrimary->sqlSelectOneTest(
                'select message from test', ($bHostStandby ? $strStandbyMessage : $strFullMessage));
        }

        # Restore (restore type = xid, inclusive)
        #---------------------------------------------------------------------------------------------------------------------------
        my $strRecoveryFile = undef;

        &log(INFO, '    testing recovery type = ' . CFGOPTVAL_RESTORE_TYPE_XID);

        $oHostDbPrimary->clusterStop();

        executeTest('rm -rf ' . $oHostDbPrimary->dbBasePath() . "/*");
        executeTest('rm -rf ' . $oHostDbPrimary->dbPath() . qw{/} . $oManifest->walPath() . '/*');

        $oHostDbPrimary->restore(
            undef, $strIncrBackup,
            {bForce => true, strType => CFGOPTVAL_RESTORE_TYPE_XID, strTarget => $strXidTarget,
                strTargetAction => $oHostDbPrimary->pgVersion() >= PG_VERSION_91 ? 'promote' : undef,
                strTargetTimeline => $oHostDbPrimary->pgVersion() >= PG_VERSION_12 ? 'current' : undef,
                strOptionalParam => '--tablespace-map-all=../../tablespace', bTablespace => false,
                iRepo => $iRepoTotal});

        # Save recovery file to test so we can use it in the next test
        $strRecoveryFile = $oHostDbPrimary->pgVersion() >= PG_VERSION_12 ? 'postgresql.auto.conf' : DB_FILE_RECOVERYCONF;

        storageTest()->copy(
            $oHostDbPrimary->dbBasePath() . qw{/} . $strRecoveryFile, $self->testPath() . qw{/} . $strRecoveryFile);

        $oHostDbPrimary->clusterStart();
        $oHostDbPrimary->sqlSelectOneTest('select message from test', $strXidMessage);

        $oHostDbPrimary->sqlExecute("update test set message = '$strTimelineMessage'");

        # Restore (restore type = preserve, inclusive)
        #---------------------------------------------------------------------------------------------------------------------------
        &log(INFO, '    testing recovery type = ' . CFGOPTVAL_RESTORE_TYPE_PRESERVE);

        $oHostDbPrimary->clusterStop();

        executeTest('rm -rf ' . $oHostDbPrimary->dbBasePath() . "/*");
        executeTest('rm -rf ' . $oHostDbPrimary->dbPath() . qw{/} . $oManifest->walPath() . '/*');
        executeTest('rm -rf ' . $oHostDbPrimary->tablespacePath(1) . "/*");

        # Restore recovery file that was saved in last test
        storageTest()->move($self->testPath . "/${strRecoveryFile}", $oHostDbPrimary->dbBasePath() . "/${strRecoveryFile}");

        # Also touch recovery.signal when required
        if ($oHostDbPrimary->pgVersion() >= PG_VERSION_12)
        {
            storageTest()->put($oHostDbPrimary->dbBasePath() . "/" . DB_FILE_RECOVERYSIGNAL);
        }

        $oHostDbPrimary->restore(undef, 'latest', {strType => CFGOPTVAL_RESTORE_TYPE_PRESERVE});

        $oHostDbPrimary->clusterStart();
        $oHostDbPrimary->sqlSelectOneTest('select message from test', $strXidMessage);

        $oHostDbPrimary->sqlExecute("update test set message = '$strTimelineMessage'");

        # Restore (restore type = time, inclusive, automatically select backup) - there is no exclusive time test because I can't
        # find a way to find the exact commit time of a transaction.
        #---------------------------------------------------------------------------------------------------------------------------
        &log(INFO, '    testing recovery type = ' . CFGOPTVAL_RESTORE_TYPE_TIME);

        $oHostDbPrimary->clusterStop();

        $oHostDbPrimary->restore(
            undef, 'latest',
            {bDelta => true, strType => CFGOPTVAL_RESTORE_TYPE_TIME, strTarget => $strTimeTarget,
                strTargetAction => $oHostDbPrimary->pgVersion() >= PG_VERSION_91 ? 'promote' : undef,
                strTargetTimeline => $oHostDbPrimary->pgVersion() >= PG_VERSION_12 ? 'current' : undef,
                strBackupExpected => $strFullBackup});

        $oHostDbPrimary->clusterStart();
        $oHostDbPrimary->sqlSelectOneTest('select message from test', $strTimeMessage);

        # Restore (restore type = xid, exclusive)
        #---------------------------------------------------------------------------------------------------------------------------
        &log(INFO, '    testing recovery type = ' . CFGOPTVAL_RESTORE_TYPE_XID);

        $oHostDbPrimary->clusterStop();

        $oHostDbPrimary->restore(
            undef, $strIncrBackup,
            {bDelta => true, strType => CFGOPTVAL_RESTORE_TYPE_XID, strTarget => $strXidTarget, bTargetExclusive => true,
                strTargetAction => $oHostDbPrimary->pgVersion() >= PG_VERSION_91 ? 'promote' : undef,
                strTargetTimeline => $oHostDbPrimary->pgVersion() >= PG_VERSION_12 ? 'current' : undef,
                iRepo => $iRepoTotal});

        $oHostDbPrimary->clusterStart();
        $oHostDbPrimary->sqlSelectOneTest('select message from test', $strIncrMessage);

        # Restore (restore type = name)
        #---------------------------------------------------------------------------------------------------------------------------
        if ($oHostDbPrimary->pgVersion() >= PG_VERSION_91)
        {
            &log(INFO, '    testing recovery type = ' . CFGOPTVAL_RESTORE_TYPE_NAME);

            $oHostDbPrimary->clusterStop();

            $oHostDbPrimary->restore(
                undef, 'latest',
                {bDelta => true, bForce => true, strType => CFGOPTVAL_RESTORE_TYPE_NAME, strTarget => $strNameTarget,
                    strTargetAction => 'promote',
                    strTargetTimeline => $oHostDbPrimary->pgVersion() >= PG_VERSION_12 ? 'current' : undef});

            $oHostDbPrimary->clusterStart();
            $oHostDbPrimary->sqlSelectOneTest('select message from test', $strNameMessage);
        }

        # Restore (restore type = default, timeline = created by type = xid, inclusive recovery)
        #---------------------------------------------------------------------------------------------------------------------------
        if ($oHostDbPrimary->pgVersion() >= PG_VERSION_84)
        {
            &log(INFO, '    testing recovery type = ' . CFGOPTVAL_RESTORE_TYPE_DEFAULT);

            $oHostDbPrimary->clusterStop();

            # The timeline to use for this test is subject to change based on tests being added or removed above.  The best thing
            # would be to automatically grab the timeline after the restore, but since this test has been stable for a long time
            # it does not seem worth the effort to automate.
            $oHostDbPrimary->restore(
                undef, $strIncrBackup,
                {bDelta => true,
                    strType => $oHostDbPrimary->pgVersion() >= PG_VERSION_90 ?
                        CFGOPTVAL_RESTORE_TYPE_STANDBY : CFGOPTVAL_RESTORE_TYPE_DEFAULT,
                    strTargetTimeline => 4, iRepo => $iRepoTotal});

            $oHostDbPrimary->clusterStart({bHotStandby => true});
            $oHostDbPrimary->sqlSelectOneTest('select message from test', $strTimelineMessage, {iTimeout => 120});
        }

        # Stop clusters to catch any errors in the postgres log
        #---------------------------------------------------------------------------------------------------------------------------
        $oHostDbPrimary->clusterStop();

        # Stanza-delete --force without access to pgbackrest on database host. This test is not version specific so is run on only
        # the expect version.
        #---------------------------------------------------------------------------------------------------------------------------
        if ($bExpectVersion)
        {
            # Make sure this test has a backup host to work with
            confess "test must run with backup dst = " . HOST_BACKUP if !$bHostBackup;

            $oHostDbPrimary->stop();
            $oHostBackup->stop({strStanza => $self->stanza});
            $oHostBackup->stanzaDelete(
                "delete stanza with --force when pgbackrest on pg host not accessible", {strOptionalParam => ' --force'});
            $oHostDbPrimary->start();
            $oHostBackup->start();
        }
    }
}

1;
