Code snippet – Reestablishing the backup log chain with PowerShell

We have a certain customer that has an incredible fondness for undermining the restorability of their databases. Almost all of their databases are in the full recovery model and all-too-frequently the customer will start backing up transaction logs with the dreaded NO_LOG option, which destroys the transaction log backup chain:

Msg 4214, Level 16, State 1, Line 1
BACKUP LOG cannot be performed because there is no current database backup.
Msg 3013, Level 16, State 1, Line 1
BACKUP LOG is terminating abnormally.

In a future blog post, I will go into why users resort to this bad practice and what they should be doing instead.

This solution is based on a great blog post from Kalen Delaney (blog, Twitter), that showed how the [sys].[database_recovery_status] catalog view can reveal databases suffering from this problem, using a query like so:

SELECT DB_Name([database_id]) AS [DatabaseName]
FROM [sys].[database_recovery_status]
WHERE (
    ([last_log_backup_lsn] IS NULL)
    AND
    (Cast(DatabasePropertyEx(DB_Name([database_id]), 'Recovery') AS NVarChar(16)) = N'FULL')
    AND
    (Cast(DatabasePropertyEx(DB_Name([database_id]), 'Status') AS NVarChar(16)) = N'ONLINE')
)
ORDER BY [DatabaseName] ASC;

The following PowerShell function uses the query to retrieve a list of problematic databases and then automatically backs up each one:

Function Repair-LogBackupChain (
    [String]$ServerName = $(Throw (New-Object -TypeName 'System.ArgumentNullException' -ArgumentList '$ServerName')),
    [String]$BackupDirectoryPath = $(Throw (New-Object -TypeName 'System.ArgumentNullException' -ArgumentList '$BackupDirectoryPath')),
    [String]$OutputDirectoryPath = $(Throw (New-Object -TypeName 'System.ArgumentNullException' -ArgumentList '$OutputDirectoryPath'))
) {
    [Int]$BackedUpDatabaseCount = 0;
    [String]$BackupFilePath = '';
    [String]$DatabaseName = '';
    [String]$ErrorMessage = '';
    [String]$OutputFilePath = '';
    [String]$Query = "SET NOCOUNT ON; SELECT DB_Name([database_id]) AS [DatabaseName] FROM [sys].[database_recovery_status] WHERE (([last_log_backup_lsn] IS NULL) AND (Cast(DatabasePropertyEx(DB_Name([database_id]), 'Recovery') AS NVarChar(16)) = N'FULL') AND (Cast(DatabasePropertyEx(DB_Name([database_id]), 'Status') AS NVarChar(16)) = N'ONLINE')) ORDER BY [DatabaseName] ASC;";
    [String]$TSQLCode = '';

    #   Create the necessary directories, if they don't already exist.
    $BackupDirectoryPath = Add-Directory -Path $BackupDirectoryPath;
    $OutputDirectoryPath = Add-Directory -Path $OutputDirectoryPath;

    #   Lookup the names of the databases, in the full recovery model, that 
    #   lack an intact transaction log backup chain.
    [String[]]$DatabaseNames = @(& 'SQLCmd.exe' -E -S $ServerName -d 'tempdb' -Q $Query -b -h-1 -W);
    $DatabaseNames | Write-Debug;

    #   Verify that the list of databases was successfully retrieved.
    If ((Get-Item -Path Variable:'LastExitCode').Value -eq 0) {
        #   Iterate through the databases that need backups.
        $DatabaseNames | ForEach-Object {
            $DatabaseName = $_;
            Write-Host "Backing up the [$($DatabaseName)] database..." -ForegroundColor 'Cyan';

            $OutputFilePath = "$($OutputDirectoryPath)$(Get-EscapedFileNameComponent -FileNameComponent $ServerName),Backup database,$(Get-EscapedFileNameComponent -FileNameComponent $DatabaseName),$([DateTime]::Now.ToString('yyyyMMddTHHmmssfff')).out";
            Write-Debug "`$OutputFilePath = `"$($OutputFilePath)`"";

            #   Construct the name of the backup file.
            $BackupFilePath = [String]::Format(
                '{0}{1}_{2}_{3}.bak',
                $BackupDirectoryPath,
                (Get-EscapedFileNameComponent -FileNameComponent $ServerName),
                (Get-EscapedFileNameComponent -FileNameComponent $DatabaseName),
                ([DateTime]::Now.ToString('yyyyMMddTHHmmssfff'))
            );

            #   Generate the T-SQL for backing up the database.
            $TSQLCode = [String]::Format(
                "BACKUP DATABASE [{0}] TO DISK = N'{1}';",
                $DatabaseName,
                $BackupFilePath
            );
            Write-Debug "`$TSQLCode = `"$($TSQLCode)`"";

            #   Backup the database.
            & 'SQLCmd.exe' -E -S $ServerName -d 'tempdb' -Q $TSQLCode -o $OutputFilePath -b;

            If (Test-Path -LiteralPath $BackupFilePath -PathType 'Leaf') {
                $BackedUpDatabaseCount++;
            }
            Else {
                $ErrorMessage = "There was a problem backing up the [$($DatabaseName)] database.";
                Throw (New-Object -TypeName 'System.InvalidOperationException' -ArgumentList $ErrorMessage);
            }
        }
    }
    Else {
        $ErrorMessage = [String]::Format(
            "There was a problem querying the [sys].[database_recovery_status] catalog view:`r`n{0}",
            (
                [String]::Join(
                    "`r`n",
                    $DatabaseNames
                )
            )
        );
        Throw (New-Object -TypeName 'System.InvalidOperationException' -ArgumentList $ErrorMessage);
    }

    Write-Host "A total of $($BackedUpDatabaseCount) database(s) was/were backed up." -ForegroundColor 'Green';

    Return;
}

Here’s an example of using the function:

Repair-LogBackupChain -ServerName 'DatabaseServer01\InstanceA' -BackupDirectoryPath 'C:\Temp\' -OutputDirectoryPath 'C:\Temp\';

Note: The code snippet is dependent on two functions that are included in the download for my PowerShell cookbook presentation, Add-Directory and Get-EscapedFileNameComponent.

Maybe by the time that I write the follow-up blog post, I will have received permission to break the customer of this nasty habit (using a tip from Paul Randal (blog, Twitter)).

Hopefully this PowerShell snippet will save you as much time as it has for me!

About tnbarkhouse

I'm a SQL Server database administrator with a few tools to share.
This entry was posted in PowerShell, SQL Server and tagged , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s